Merge branch 'flutter_app'
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m59s
Build Docker and Deploy / Deploy to Server (push) Successful in 7s

This commit is contained in:
Mike Schwörer 2024-06-12 00:36:12 +02:00
commit dac268f40b
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
37 changed files with 1650 additions and 402 deletions

2
flutter/.gitignore vendored
View File

@ -2,6 +2,8 @@
*.keystore *.keystore
firepit-log.txt
flutter_jank_*
####################################################################################################################### #######################################################################################################################

View File

@ -7,6 +7,9 @@ run:
test: test:
dart analyze dart analyze
fix:
dart fix --apply
gen: gen:
dart run build_runner build dart run build_runner build

View File

@ -24,7 +24,7 @@
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )" pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )"
if [ -z "$pid" ]; then if [ -z "$pid" ]; then
red "No [flutter run] process found - exiting" red "No [flutter run] process found - exiting"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,6 @@ import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/request_log.dart';
@ -182,6 +181,16 @@ class APIClient {
); );
} }
static Future<ChannelWithSubscription> getChannel(TokenSource auth, String cid) async {
return await _request(
name: 'getChannel',
method: 'GET',
relURL: 'users/${auth.getUserID()}/channels/${cid}',
fn: ChannelWithSubscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async { static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
return await _request( return await _request(
name: 'getMessageList', name: 'getMessageList',

View File

@ -1,6 +1,6 @@
class APIException implements Exception { class APIException implements Exception {
final int httpStatus; final int httpStatus;
final String error; final int error;
final String errHighlight; final String errHighlight;
final String message; final String message;

View File

@ -11,46 +11,64 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
required this.showThemeSwitch, required this.showThemeSwitch,
required this.showDebug, required this.showDebug,
required this.showSearch, required this.showSearch,
required this.showShare,
this.onShare = null,
}) : super(key: key); }) : super(key: key);
final String? title; final String? title;
final bool showThemeSwitch; final bool showThemeSwitch;
final bool showDebug; final bool showDebug;
final bool showSearch; final bool showSearch;
final bool showShare;
final void Function()? onShare;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var actions = <Widget>[];
if (showThemeSwitch) {
actions.add(Consumer<AppTheme>(
builder: (context, appTheme, child) => IconButton(
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
onPressed: appTheme.switchDarkMode,
),
));
} else {
actions.add(SizedBox.square(dimension: 40));
}
if (showDebug) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug',
onPressed: () {
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
},
));
} else {
actions.add(SizedBox.square(dimension: 40));
}
if (showSearch) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
tooltip: 'Search',
onPressed: () {/*TODO*/},
));
} else if (showShare) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidShareNodes),
tooltip: 'Share',
onPressed: onShare ?? () {},
));
} else {
actions.add(SizedBox.square(dimension: 40));
}
return AppBar( return AppBar(
title: Text(title ?? 'Simple Cloud Notifier 2.0'), title: Text(title ?? 'Simple Cloud Notifier 2.0'),
actions: <Widget>[ actions: actions,
if (showThemeSwitch)
Consumer<AppTheme>(
builder: (context, appTheme, child) => IconButton(
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
tooltip: 'Debug',
onPressed: () {
appTheme.switchDarkMode();
},
),
),
if (!showThemeSwitch) SizedBox.square(dimension: 40),
if (showDebug)
IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug',
onPressed: () {
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
},
),
if (!showDebug) SizedBox.square(dimension: 40),
if (showSearch)
IconButton(
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
tooltip: 'Search',
onPressed: () {},
),
if (!showSearch) SizedBox.square(dimension: 40),
],
backgroundColor: Theme.of(context).secondaryHeaderColor, backgroundColor: Theme.of(context).secondaryHeaderColor,
); );
} }

View File

@ -9,6 +9,8 @@ class SCNScaffold extends StatelessWidget {
this.showThemeSwitch = true, this.showThemeSwitch = true,
this.showDebug = true, this.showDebug = true,
this.showSearch = true, this.showSearch = true,
this.showShare = false,
this.onShare = null,
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
@ -16,6 +18,8 @@ class SCNScaffold extends StatelessWidget {
final bool showThemeSwitch; final bool showThemeSwitch;
final bool showDebug; final bool showDebug;
final bool showSearch; final bool showSearch;
final bool showShare;
final void Function()? onShare;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,6 +29,8 @@ class SCNScaffold extends StatelessWidget {
showThemeSwitch: showThemeSwitch, showThemeSwitch: showThemeSwitch,
showDebug: showDebug, showDebug: showDebug,
showSearch: showSearch, showSearch: showSearch,
showShare: showShare,
onShare: onShare ?? () {},
), ),
body: child, body: child,
); );

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -17,15 +19,24 @@ import 'firebase_options.dart';
void main() async { void main() async {
print('[INIT] Application starting...'); print('[INIT] Application starting...');
print('[INIT] Ensure WidgetsFlutterBinding...');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter(); print('[INIT] Init Globals...');
await Globals().init(); await Globals().init();
print('[INIT] Init Hive...');
await Hive.initFlutter();
Hive.registerAdapter(SCNRequestAdapter()); Hive.registerAdapter(SCNRequestAdapter());
Hive.registerAdapter(SCNLogAdapter()); Hive.registerAdapter(SCNLogAdapter());
Hive.registerAdapter(SCNLogLevelAdapter()); Hive.registerAdapter(SCNLogLevelAdapter());
print('[INIT] Load Hive<scn-requests>...');
try { try {
await Hive.openBox<SCNRequest>('scn-requests'); await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) { } catch (exc, trace) {
@ -34,6 +45,8 @@ void main() async {
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
} }
print('[INIT] Load Hive<scn-logs>...');
try { try {
await Hive.openBox<SCNLog>('scn-logs'); await Hive.openBox<SCNLog>('scn-logs');
} catch (exc, trace) { } catch (exc, trace) {
@ -42,45 +55,58 @@ void main() async {
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
} }
print('[INIT] Load AppAuth...');
final appAuth = AppAuth(); // ensure UserAccount is loaded final appAuth = AppAuth(); // ensure UserAccount is loaded
if (appAuth.isAuth()) { if (appAuth.isAuth()) {
try { try {
print('[INIT] Load User...');
await appAuth.loadUser(); await appAuth.loadUser();
//TODO fallback to cached user (perhaps event use cached client (if exists) directly and only update/load in background)
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace);
} }
try { try {
print('[INIT] Load Client...');
await appAuth.loadClient(); await appAuth.loadClient();
//TODO fallback to cached client (perhaps event use cached client (if exists) directly and only update/load in background)
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace);
} }
} }
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); if (!Platform.isLinux) {
print('[INIT] Init Firebase...');
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission(provisional: true); print('[INIT] Request Notification permissions...');
await FirebaseMessaging.instance.requestPermission(provisional: true);
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
try {
setFirebaseToken(fcmToken);
} catch (exc, trace) {
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace);
}
}).onError((dynamic err) {
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
});
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
try { try {
setFirebaseToken(fcmToken); print('[INIT] Query firebase token...');
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
setFirebaseToken(fcmToken);
}
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to set firebase token: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
} }
}).onError((dynamic err) { } else {
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? '')); print('[INIT] Skip Firebase init (Platform == Linux)...');
});
try {
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
setFirebaseToken(fcmToken);
}
} catch (exc, trace) {
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
} }
ApplicationLog.debug('Application started'); ApplicationLog.debug('[INIT] Application started');
runApp( runApp(
MultiProvider( MultiProvider(
@ -112,7 +138,7 @@ void setFirebaseToken(String fcmToken) async {
return; return;
} }
if (oldToken != null && oldToken == fcmToken && client != null && client!.fcmToken == fcmToken) { if (oldToken != null && oldToken == fcmToken && client != null && client.fcmToken == fcmToken) {
ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken'); ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken');
return; return;
} }

View File

@ -1,9 +1,61 @@
class APIError { class APIError {
final String success; final bool success;
final String error; final int error;
final String errhighlight; final String errhighlight;
final String message; final String message;
static final MISSING_UID = 1101;
static final MISSING_TOK = 1102;
static final MISSING_TITLE = 1103;
static final INVALID_PRIO = 1104;
static final REQ_METHOD = 1105;
static final INVALID_CLIENTTYPE = 1106;
static final PAGETOKEN_ERROR = 1121;
static final BINDFAIL_QUERY_PARAM = 1151;
static final BINDFAIL_BODY_PARAM = 1152;
static final BINDFAIL_URI_PARAM = 1153;
static final INVALID_BODY_PARAM = 1161;
static final INVALID_ENUM_VALUE = 1171;
static final NO_TITLE = 1201;
static final TITLE_TOO_LONG = 1202;
static final CONTENT_TOO_LONG = 1203;
static final USR_MSG_ID_TOO_LONG = 1204;
static final TIMESTAMP_OUT_OF_RANGE = 1205;
static final SENDERNAME_TOO_LONG = 1206;
static final CHANNEL_TOO_LONG = 1207;
static final CHANNEL_DESCRIPTION_TOO_LONG = 1208;
static final CHANNEL_NAME_EMPTY = 1209;
static final USER_NOT_FOUND = 1301;
static final CLIENT_NOT_FOUND = 1302;
static final CHANNEL_NOT_FOUND = 1303;
static final SUBSCRIPTION_NOT_FOUND = 1304;
static final MESSAGE_NOT_FOUND = 1305;
static final SUBSCRIPTION_USER_MISMATCH = 1306;
static final KEY_NOT_FOUND = 1307;
static final USER_AUTH_FAILED = 1311;
static final NO_DEVICE_LINKED = 1401;
static final CHANNEL_ALREADY_EXISTS = 1501;
static final CANNOT_SELFDELETE_KEY = 1511;
static final CANNOT_SELFUPDATE_KEY = 1512;
static final QUOTA_REACHED = 2101;
static final FAILED_VERIFY_PRO_TOKEN = 3001;
static final INVALID_PRO_TOKEN = 3002;
static final COMMIT_FAILED = 9001;
static final DATABASE_ERROR = 9002;
static final PERM_QUERY_FAIL = 9003;
static final FIREBASE_COM_FAILED = 9901;
static final FIREBASE_COM_ERRORED = 9902;
static final INTERNAL_EXCEPTION = 9903;
static final PANIC = 9904;
static final NOT_IMPLEMENTED = 9905;
const APIError({ const APIError({
required this.success, required this.success,
required this.error, required this.error,
@ -13,8 +65,8 @@ class APIError {
factory APIError.fromJson(Map<String, dynamic> json) { factory APIError.fromJson(Map<String, dynamic> json) {
return APIError( return APIError(
success: json['success'] as String, success: json['success'] as bool,
error: json['error'] as String, error: (json['error'] as double).toInt(),
errhighlight: json['errhighlight'] as String, errhighlight: json['errhighlight'] as String,
message: json['message'] as String, message: json['message'] as String,
); );

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_lazy_indexed_stack/flutter_lazy_indexed_stack.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/components/hidable_fab/hidable_fab.dart'; import 'package:simplecloudnotifier/components/hidable_fab/hidable_fab.dart';
@ -62,15 +61,16 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
title: null, title: null,
showDebug: true, showDebug: true,
showSearch: _selectedIndex == 0 || _selectedIndex == 1, showSearch: _selectedIndex == 0 || _selectedIndex == 1,
showShare: false,
showThemeSwitch: true, showThemeSwitch: true,
), ),
body: LazyIndexedStack( body: IndexedStack(
children: [ children: [
MessageListPage(), ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage()),
ChannelRootPage(), ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage()),
AccountRootPage(), ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage()),
SettingsRootPage(), ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage()),
SendRootPage(), ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage()),
], ],
index: _selectedIndex, index: _selectedIndex,
), ),

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -9,6 +11,8 @@ import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
import 'package:uuid/uuid.dart';
class AccountRootPage extends StatefulWidget { class AccountRootPage extends StatefulWidget {
const AccountRootPage({super.key}); const AccountRootPage({super.key});
@ -140,22 +144,23 @@ class _AccountRootPageState extends State<AccountRootPage> {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
FilledButton( UI.button(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)), text: 'Create new account',
onPressed: () { onPressed: () {
if (loading) return; if (loading) return;
_createNewAccount(); _createNewAccount();
}, },
child: const Text('Create new account'), big: true,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
FilledButton.tonal( UI.button(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)), text: 'Use existing account',
onPressed: () { onPressed: () {
if (loading) return; if (loading) return;
Navigator.push(context, MaterialPageRoute<AccountLoginPage>(builder: (context) => AccountLoginPage())); Navigator.push(context, MaterialPageRoute<AccountLoginPage>(builder: (context) => AccountLoginPage()));
}, },
child: const Text('Use existing account'), tonal: true,
big: true,
), ),
], ],
), ),
@ -164,28 +169,22 @@ class _AccountRootPageState extends State<AccountRootPage> {
} }
Widget _buildShowAccount(BuildContext context, AppAuth acc, User user) { Widget _buildShowAccount(BuildContext context, AppAuth acc, User user) {
//TODO better layout return SingleChildScrollView(
return Column( child: Padding(
children: [ padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0),
SingleChildScrollView( child: Column(
scrollDirection: Axis.vertical, children: [
child: Padding( _buildHeader(context, user),
padding: const EdgeInsets.fromLTRB(8.0, 24.0, 8.0, 8.0), const SizedBox(height: 16),
child: Column( Text(user.username ?? user.userID, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
children: [ const SizedBox(height: 16),
_buildHeader(context, user), ..._buildCards(context, user),
const SizedBox(height: 16), SizedBox(height: 16),
Text(user.username ?? user.userID, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), _buildFooter(context, user),
const SizedBox(height: 16), SizedBox(height: 40),
..._buildCards(context, user), ],
],
),
),
), ),
const Expanded(child: SizedBox(height: 16)), ),
_buildFooter(context, user),
SizedBox(height: 40)
],
); );
} }
@ -272,23 +271,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
Column( Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
IconButton( UI.buttonIconOnly(
icon: FaIcon(FontAwesomeIcons.pen),
iconSize: 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
onPressed: () {/*TODO*/}, onPressed: () {/*TODO*/},
icon: FontAwesomeIcons.pen,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
if (!user.isPro) if (!user.isPro)
IconButton( UI.buttonIconOnly(
icon: FaIcon(FontAwesomeIcons.cartCircleArrowUp),
iconSize: 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
onPressed: () {/*TODO*/}, onPressed: () {/*TODO*/},
icon: FontAwesomeIcons.cartCircleArrowUp,
), ),
], ],
), ),
@ -298,132 +289,97 @@ class _AccountRootPageState extends State<AccountRootPage> {
List<Widget> _buildCards(BuildContext context, User user) { List<Widget> _buildCards(BuildContext context, User user) {
return [ return [
Card.filled( UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), child: Row(
color: Theme.of(context).cardTheme.color, children: [
child: InkWell( FutureBuilder(
splashColor: Theme.of(context).splashColor, future: futureSubscriptionCount,
onTap: () {/*TODO*/}, builder: (context, snapshot) {
child: Padding( if (snapshot.connectionState == ConnectionState.done) {
padding: const EdgeInsets.all(16), return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
child: Row( }
children: [ return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
FutureBuilder( },
future: futureSubscriptionCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
), const SizedBox(width: 12),
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
onTap: () {/*TODO*/},
), ),
Card.filled( UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), child: Row(
color: Theme.of(context).cardTheme.color, children: [
child: InkWell( FutureBuilder(
splashColor: Theme.of(context).splashColor, future: futureClientCount,
onTap: () {/*TODO*/}, builder: (context, snapshot) {
child: Padding( if (snapshot.connectionState == ConnectionState.done) {
padding: const EdgeInsets.all(16), return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
child: Row( }
children: [ return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
FutureBuilder( },
future: futureClientCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
), const SizedBox(width: 12),
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
onTap: () {/*TODO*/},
), ),
Card.filled( UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), child: Row(
color: Theme.of(context).cardTheme.color, children: [
child: InkWell( FutureBuilder(
splashColor: Theme.of(context).splashColor, future: futureKeyCount,
onTap: () {/*TODO*/}, builder: (context, snapshot) {
child: Padding( if (snapshot.connectionState == ConnectionState.done) {
padding: const EdgeInsets.all(16), return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
child: Row( }
children: [ return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
FutureBuilder( },
future: futureKeyCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
), const SizedBox(width: 12),
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
onTap: () {/*TODO*/},
), ),
Card.filled( UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), child: Row(
color: Theme.of(context).cardTheme.color, children: [
child: InkWell( FutureBuilder(
splashColor: Theme.of(context).splashColor, future: futureChannelSubscribedCount,
onTap: () {/*TODO*/}, builder: (context, snapshot) {
child: Padding( if (snapshot.connectionState == ConnectionState.done) {
padding: const EdgeInsets.all(16), return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
child: Row( }
children: [ return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
FutureBuilder( },
future: futureChannelSubscribedCount,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
}
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
},
),
const SizedBox(width: 12),
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
), const SizedBox(width: 12),
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
), ),
onTap: () {/*TODO*/},
), ),
Card.filled( UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), child: Row(
color: Theme.of(context).cardTheme.color, children: [
child: InkWell( Text('${user.messagesSent}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
splashColor: Theme.of(context).splashColor, const SizedBox(width: 12),
onTap: () {/*TODO*/}, Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
child: Padding( ],
padding: const EdgeInsets.all(16),
child: Row(
children: [
Text('${user.messagesSent}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(width: 12),
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
),
), ),
onTap: () {/*TODO*/},
), ),
]; ];
} }
@ -433,9 +389,19 @@ class _AccountRootPageState extends State<AccountRootPage> {
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Row( child: Row(
children: [ children: [
Expanded(child: FilledButton(onPressed: _logout, child: Text('Logout'), style: TextButton.styleFrom(backgroundColor: Colors.orange))), Expanded(
child: UI.button(
text: 'Logout',
onPressed: _logout,
color: Colors.orange,
)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: FilledButton(onPressed: _deleteAccount, child: Text('Delete Account'), style: TextButton.styleFrom(backgroundColor: Colors.red))), Expanded(
child: UI.button(
text: 'Delete Account',
onPressed: _deleteAccount,
color: Colors.red,
)),
], ],
), ),
); );
@ -447,15 +413,21 @@ class _AccountRootPageState extends State<AccountRootPage> {
final acc = Provider.of<AppAuth>(context, listen: false); final acc = Provider.of<AppAuth>(context, listen: false);
try { try {
final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true); final String? fcmToken;
if (Platform.isLinux) {
Toaster.warn("Unsupported Platform", 'Your platform is not supported by FCM - notifications will not work');
fcmToken = '(linux-' + Uuid().v4() + ')';
} else {
final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) { if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) {
Toaster.error("Missing Permission", 'Please allow notifications to create an account'); Toaster.error("Missing Permission", 'Please allow notifications to create an account');
return; return;
}
fcmToken = await FirebaseMessaging.instance.getToken();
} }
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) { if (fcmToken == null) {
Toaster.warn("Missing Token", 'No FCM Token found, please allow notifications, ensure you have a network connection and restart the app'); Toaster.warn("Missing Token", 'No FCM Token found, please allow notifications, ensure you have a network connection and restart the app');
return; return;

View File

@ -9,6 +9,7 @@ import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/state/token_source.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class AccountLoginPage extends StatefulWidget { class AccountLoginPage extends StatefulWidget {
const AccountLoginPage({super.key}); const AccountLoginPage({super.key});
@ -102,10 +103,10 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
FilledButton( UI.button(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)), text: 'Login',
big: true,
onPressed: _login, onPressed: _login,
child: const Text('Login'),
), ),
], ],
), ),

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:toastification/toastification.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
class DebugActionsPage extends StatefulWidget { class DebugActionsPage extends StatefulWidget {
@override @override
@ -17,36 +17,40 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
FilledButton( UI.button(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), big: false,
onPressed: () => Toaster.success("Hello World", "This was a triumph!"), onPressed: () => Toaster.success("Hello World", "This was a triumph!"),
child: const Text('Show Success Notification'), text: 'Show Success Notification',
), ),
FilledButton( SizedBox(height: 4),
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), UI.button(
big: false,
onPressed: () => Toaster.info("Hello World", "This was a triumph!"), onPressed: () => Toaster.info("Hello World", "This was a triumph!"),
child: const Text('Show Info Notification'), text: 'Show Info Notification',
), ),
FilledButton( SizedBox(height: 4),
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), UI.button(
big: false,
onPressed: () => Toaster.warn("Hello World", "This was a triumph!"), onPressed: () => Toaster.warn("Hello World", "This was a triumph!"),
child: const Text('Show Warn Notification'), text: 'Show Warn Notification',
), ),
FilledButton( SizedBox(height: 4),
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), UI.button(
big: false,
onPressed: () => Toaster.error("Hello World", "This was a triumph!"), onPressed: () => Toaster.error("Hello World", "This was a triumph!"),
child: const Text('Show Info Notification'), text: 'Show Info Notification',
), ),
FilledButton( SizedBox(height: 4),
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), UI.button(
big: false,
onPressed: () => Toaster.simple("Hello World"), onPressed: () => Toaster.simple("Hello World"),
child: const Text('Show Simple Notification'), text: 'Show Simple Notification',
), ),
SizedBox(height: 20), SizedBox(height: 20),
FilledButton( UI.button(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), big: false,
onPressed: _sendTokenToServer, onPressed: _sendTokenToServer,
child: const Text('Send FCM Token to Server'), text: 'Send FCM Token to Server',
), ),
], ],
), ),

View File

@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/request_log.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class DebugRequestViewPage extends StatelessWidget { class DebugRequestViewPage extends StatelessWidget {
final SCNRequest request; final SCNRequest request;
@ -55,17 +56,13 @@ class DebugRequestViewPage extends StatelessWidget {
Expanded( Expanded(
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)), child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
), ),
IconButton( UI.buttonIconOnly(
icon: FaIcon(
FontAwesomeIcons.copy,
),
iconSize: 14, iconSize: 14,
padding: EdgeInsets.fromLTRB(0, 0, 4, 0),
constraints: BoxConstraints(),
onPressed: () { onPressed: () {
Clipboard.setData(new ClipboardData(text: value)); Clipboard.setData(new ClipboardData(text: title));
Toaster.info("Clipboard", 'Copied text to Clipboard'); Toaster.info("Clipboard", 'Copied text to Clipboard');
}, },
icon: FontAwesomeIcons.copy,
), ),
], ],
), ),

View File

@ -12,6 +12,9 @@ import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
class MessageListPage extends StatefulWidget { class MessageListPage extends StatefulWidget {
const MessageListPage({super.key}); const MessageListPage({super.key});
//TODO reload on switch to tab
//TODO reload on app to foreground
@override @override
State<MessageListPage> createState() => _MessageListPageState(); State<MessageListPage> createState() => _MessageListPageState();
} }
@ -25,6 +28,7 @@ class _MessageListPageState extends State<MessageListPage> {
@override @override
void initState() { void initState() {
//TODO init with state from cache - also allow tho show cache on error
_pagingController.addPageRequestListener((pageKey) { _pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey); _fetchPage(pageKey);
}); });

View File

@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/message.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class MessageListItem extends StatelessWidget { class MessageListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
@ -102,19 +103,10 @@ class MessageListItem extends StatelessWidget {
if (message.priority == 2) SizedBox(width: 4), if (message.priority == 2) SizedBox(width: 4),
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]), if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
if (message.priority == 0) SizedBox(width: 4), if (message.priority == 0) SizedBox(width: 4),
Container( UI.channelChip(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), context: context,
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0), text: resolveChannelName(message),
decoration: BoxDecoration( margin: EdgeInsets.fromLTRB(0, 0, 4, 0),
color: Theme.of(context).hintColor,
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Text(
resolveChannelName(message),
style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: 12),
overflow: TextOverflow.clip,
maxLines: 1,
),
), ),
Expanded(child: SizedBox()), Expanded(child: SizedBox()),
Text( Text(

View File

@ -1,9 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/api_error.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/message.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class MessageViewPage extends StatefulWidget { class MessageViewPage extends StatefulWidget {
const MessageViewPage({super.key, required this.message}); const MessageViewPage({super.key, required this.message});
@ -15,18 +25,54 @@ class MessageViewPage extends StatefulWidget {
} }
class _MessageViewPageState extends State<MessageViewPage> { class _MessageViewPageState extends State<MessageViewPage> {
late Future<Message>? futureMessage; late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture;
(Message, ChannelWithSubscription?, KeyToken?)? mainFutureSnapshot = null;
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
bool _monospaceMode = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
futureMessage = fetchMessage(); mainFuture = fetchData();
} }
Future<Message> fetchMessage() async { Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async {
final acc = Provider.of<AppAuth>(context, listen: false); final acc = Provider.of<AppAuth>(context, listen: false);
return await APIClient.getMessage(acc, widget.message.messageID); final msg = await APIClient.getMessage(acc, widget.message.messageID);
ChannelWithSubscription? chn = null;
try {
chn = await APIClient.getChannel(acc, msg.channelID); //TODO getShortChannel (?) -> no perm
} on APIException catch (e) {
if (e.error == APIError.USER_AUTH_FAILED) {
chn = null;
} else {
rethrow;
}
}
KeyToken? tok = null;
try {
tok = await APIClient.getKeyToken(acc, msg.usedKeyID); //TODO getShortKeyToken (?) -> no perm
} on APIException catch (e) {
if (e.error == APIError.USER_AUTH_FAILED) {
tok = null;
} else {
rethrow;
}
}
//TODO getShortUser for sender
//await Future.delayed(const Duration(seconds: 2), () {});
final r = (msg, chn, tok);
mainFutureSnapshot = r;
return r;
} }
@override @override
@ -39,15 +85,18 @@ class _MessageViewPageState extends State<MessageViewPage> {
return SCNScaffold( return SCNScaffold(
title: 'Message', title: 'Message',
showSearch: false, showSearch: false,
child: FutureBuilder<Message>( showShare: true,
future: futureMessage, onShare: _share,
child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>(
future: mainFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return buildMessageView(snapshot.data!, false); final (msg, chn, tok) = snapshot.data!;
return _buildMessageView(context, msg, chn, tok, false);
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}')); //TODO nice error page return Center(child: Text('${snapshot.error}')); //TODO nice error page
} else if (!widget.message.trimmed) { } else if (!widget.message.trimmed) {
return buildMessageView(widget.message, true); return _buildMessageView(context, widget.message, null, null, true);
} else { } else {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@ -56,15 +105,172 @@ class _MessageViewPageState extends State<MessageViewPage> {
); );
} }
Widget buildMessageView(Message message, bool loading) { void _share() async {
//TODO loading true/false indicator var msg = widget.message;
return Center( if (mainFutureSnapshot != null) {
child: Column( (msg, _, _) = mainFutureSnapshot!;
children: [ }
Text(message.title),
Text(message.content ?? ''), if (msg.content != null) {
], final result = await Share.share(msg.content!, subject: msg.title);
if (result.status == ShareResultStatus.unavailable) {
Toaster.error('Error', "Failed to open share dialog");
}
} else {
final result = await Share.share(msg.title);
if (result.status == ShareResultStatus.unavailable) {
Toaster.error('Error', "Failed to open share dialog");
}
}
}
Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
..._buildMessageHeader(context, message, channel, token, loading),
SizedBox(height: 8),
if (message.content != null) ..._buildMessageContent(context, message, channel, token),
SizedBox(height: 8),
if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [message.usedKeyID, if (token != null) token.name], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidIdCardClip, 'MessageID', [message.messageID, if (message.userMessageID != null) message.userMessageID!], null),
_buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.channel.displayName ?? message.channelInternalName], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null),
_buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', ['TODO'], () => {/*TODO*/}), //TODO
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
],
),
), ),
); );
} }
String _resolveChannelName(ChannelWithSubscription? channel, Message message) {
return channel?.channel.displayName ?? message.channelInternalName;
}
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
return [
Row(
children: [
UI.channelChip(
context: context,
text: _resolveChannelName(channel, message),
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
fontSize: 16,
),
Expanded(child: SizedBox()),
Text(_dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
],
),
SizedBox(height: 8),
if (!loading) Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
if (loading)
Stack(
children: [
Row(
children: [
Flexible(child: Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
SizedBox(height: 20, width: 20),
],
),
Row(
children: [
Expanded(child: SizedBox(width: 0)),
SizedBox(child: CircularProgressIndicator(), height: 20, width: 20),
],
),
],
),
];
}
List<Widget> _buildMessageContent(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) {
return [
Row(
children: [
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
Expanded(child: SizedBox()),
UI.buttonIconOnly(
onPressed: () {
Clipboard.setData(new ClipboardData(text: message.content ?? ''));
Toaster.info("Clipboard", 'Copied text to Clipboard');
},
icon: FontAwesomeIcons.copy,
),
UI.buttonIconOnly(
icon: _monospaceMode ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft,
onPressed: () {
setState(() {
_monospaceMode = !_monospaceMode;
});
},
),
],
),
_monospaceMode
? UI.box(
context: context,
padding: const EdgeInsets.all(4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
message.content ?? '',
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"]),
),
),
borderColor: (message.priority == 2) ? Colors.red[900] : null,
)
: UI.box(
context: context,
padding: const EdgeInsets.all(4),
child: Text(message.content ?? ''),
borderColor: (message.priority == 2) ? Colors.red[900] : null,
)
];
}
Widget _buildMetaCard(BuildContext context, IconData icn, String title, List<String> values, void Function()? action) {
final container = UI.box(
context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
FaIcon(icn, size: 18),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
for (final val in values) Text(val, style: const TextStyle(fontSize: 14)),
],
),
],
),
);
if (action == null) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: container,
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: action,
child: container,
),
);
}
}
} }

View File

@ -1,5 +1,4 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/client.dart';
@ -63,6 +62,8 @@ class AppAuth extends ChangeNotifier implements TokenSource {
} }
void load() { void load() {
//final cdat = Globals().sharedPrefs.getString('auth.cdate');
//final mdat = Globals().sharedPrefs.getString('auth.mdate');
final uid = Globals().sharedPrefs.getString('auth.userid'); final uid = Globals().sharedPrefs.getString('auth.userid');
final cid = Globals().sharedPrefs.getString('auth.clientid'); final cid = Globals().sharedPrefs.getString('auth.clientid');
final toka = Globals().sharedPrefs.getString('auth.tokenadmin'); final toka = Globals().sharedPrefs.getString('auth.tokenadmin');
@ -85,17 +86,23 @@ class AppAuth extends ChangeNotifier implements TokenSource {
} }
Future<void> save() async { Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
if (_clientID == null || _userID == null || _tokenAdmin == null || _tokenSend == null) { if (_clientID == null || _userID == null || _tokenAdmin == null || _tokenSend == null) {
await prefs.remove('auth.userid'); await Globals().sharedPrefs.remove('auth.userid');
await prefs.remove('auth.tokenadmin'); await Globals().sharedPrefs.remove('auth.clientid');
await prefs.remove('auth.tokensend'); await Globals().sharedPrefs.remove('auth.tokenadmin');
await Globals().sharedPrefs.remove('auth.tokensend');
await Globals().sharedPrefs.setString('auth.cdate', "");
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
} else { } else {
await prefs.setString('auth.userid', _userID!); await Globals().sharedPrefs.setString('auth.userid', _userID!);
await prefs.setString('auth.clientid', _clientID!); await Globals().sharedPrefs.setString('auth.clientid', _clientID!);
await prefs.setString('auth.tokenadmin', _tokenAdmin!); await Globals().sharedPrefs.setString('auth.tokenadmin', _tokenAdmin!);
await prefs.setString('auth.tokensend', _tokenSend!); await Globals().sharedPrefs.setString('auth.tokensend', _tokenSend!);
if (Globals().sharedPrefs.getString('auth.cdate') == null) await Globals().sharedPrefs.setString('auth.cdate', DateTime.now().toIso8601String());
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
} }
Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
} }
Future<User> loadUser({bool force = false}) async { Future<User> loadUser({bool force = false}) async {

View File

@ -0,0 +1 @@
class Navi {}

109
flutter/lib/utils/ui.dart Normal file
View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class UI {
static const double DefaultBorderRadius = 4;
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, bool tonal = false, IconData? icon = null}) {
final double fontSize = big ? 24 : 14;
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
final style = FilledButton.styleFrom(
textStyle: TextStyle(fontSize: fontSize),
padding: padding,
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
);
if (tonal) {
if (icon != null) {
return FilledButton.tonalIcon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
);
} else {
return FilledButton.tonal(
style: style,
onPressed: onPressed,
child: Text(text),
);
}
} else {
if (icon != null) {
return FilledButton.icon(
style: style,
onPressed: onPressed,
icon: Icon(icon),
label: Text(text),
);
} else {
return FilledButton(
style: style,
onPressed: onPressed,
child: Text(text),
);
}
}
}
static Widget buttonIconOnly({
required void Function() onPressed,
required IconData icon,
double? iconSize = null,
}) {
return IconButton(
icon: FaIcon(icon),
iconSize: iconSize ?? 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
onPressed: onPressed,
);
}
static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) {
return Card.filled(
margin: margin,
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
);
}
static Widget channelChip({required BuildContext context, required String text, EdgeInsets? margin = null, double fontSize = 12}) {
return Container(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0),
margin: margin,
decoration: BoxDecoration(
color: Theme.of(context).hintColor,
borderRadius: BorderRadius.all(Radius.circular(DefaultBorderRadius)),
),
child: Text(
text,
style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: fontSize),
overflow: TextOverflow.clip,
maxLines: 1,
),
);
}
static Widget box({required BuildContext context, required Widget child, required EdgeInsets? padding, Color? borderColor = null}) {
return Container(
padding: padding ?? EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(color: borderColor ?? Theme.of(context).hintColor),
borderRadius: BorderRadius.circular(DefaultBorderRadius),
),
child: child,
);
}
}

View File

@ -10,6 +10,7 @@ import firebase_core
import firebase_messaging import firebase_messaging
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -161,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
url: "https://pub.dev"
source: hosted
version: "0.3.4+1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -687,6 +695,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544
url: "https://pub.dev"
source: hosted
version: "9.0.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -941,7 +965,7 @@ packages:
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
uuid: uuid:
dependency: transitive dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"

View File

@ -29,6 +29,8 @@ dependencies:
firebase_messaging: ^14.9.4 firebase_messaging: ^14.9.4
device_info_plus: ^10.1.0 device_info_plus: ^10.1.0
toastification: ^2.0.0 toastification: ^2.0.0
uuid: ^4.4.0
share_plus: ^9.0.0
dependency_overrides: dependency_overrides:

View File

@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar( FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
firebase_core firebase_core
share_plus
url_launcher_windows url_launcher_windows
) )

View File

@ -0,0 +1,127 @@
package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"errors"
"github.com/gin-gonic/gin"
"net/http"
)
// GetUserPreview swaggerdoc
//
// @Summary Get a user (similar to api-user-get, but can be called from anyone and only returns a subset of fields)
// @ID api-user-get-preview
// @Tags API-v2
//
// @Param uid path string true "UserID"
//
// @Success 200 {object} models.UserPreviewJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "user not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/preview/users/{uid} [GET]
func (h APIHandler) GetUserPreview(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
user, err := h.database.GetUser(ctx, u.UserID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.USER_NOT_FOUND, "User not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, user.JSONPreview()))
}
// GetChannelPreview swaggerdoc
//
// @Summary Get a single channel (similar to api-channels-get, but can be called from anyone and only returns a subset of fields)
// @ID api-channels-get-preview
// @Tags API-v2
//
// @Param cid path string true "ChannelID"
//
// @Success 200 {object} models.ChannelPreviewJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "channel not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/preview/channels/{cid} [GET]
func (h APIHandler) GetChannelPreview(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
ChannelID models.ChannelID `uri:"cid" binding:"entityid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
channel, err := h.database.GetChannelByID(ctx, u.ChannelID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSONPreview()))
}
// GetUserKeyPreview swaggerdoc
//
// @Summary Get a single key (similar to api-tokenkeys-get, but can be called from anyone and only returns a subset of fields)
// @ID api-tokenkeys-get-preview
// @Tags API-v2
//
// @Param kid path string true "TokenKeyID"
//
// @Success 200 {object} models.KeyTokenPreviewJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 404 {object} ginresp.apiError "message not found"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/v2/preview/keys/{kid} [GET]
func (h APIHandler) GetUserKeyPreview(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid" binding:"entityid"`
KeyID models.KeyTokenID `uri:"kid" binding:"entityid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
keytoken, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
}
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, keytoken.JSONPreview()))
}

View File

@ -157,6 +157,10 @@ func (r *Router) Init(e *gin.Engine) error {
apiv2.GET("/messages", r.Wrap(r.apiHandler.ListMessages)) apiv2.GET("/messages", r.Wrap(r.apiHandler.ListMessages))
apiv2.GET("/messages/:mid", r.Wrap(r.apiHandler.GetMessage)) apiv2.GET("/messages/:mid", r.Wrap(r.apiHandler.GetMessage))
apiv2.DELETE("/messages/:mid", r.Wrap(r.apiHandler.DeleteMessage)) apiv2.DELETE("/messages/:mid", r.Wrap(r.apiHandler.DeleteMessage))
apiv2.GET("/preview/users/:uid", r.Wrap(r.apiHandler.GetUserPreview))
apiv2.GET("/preview/keys/:kid", r.Wrap(r.apiHandler.GetUserKeyPreview))
apiv2.GET("/preview/channels/:cid", r.Wrap(r.apiHandler.GetChannelPreview))
} }
// ================ Send API (unversioned) ================ // ================ Send API (unversioned) ================

View File

@ -41,6 +41,16 @@ func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
} }
} }
func (c Channel) JSONPreview() ChannelPreviewJSON {
return ChannelPreviewJSON{
ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID,
InternalName: c.InternalName,
DisplayName: c.DisplayName,
DescriptionName: c.DescriptionName,
}
}
type ChannelWithSubscription struct { type ChannelWithSubscription struct {
Channel Channel
Subscription *Subscription Subscription *Subscription
@ -74,6 +84,14 @@ type ChannelWithSubscriptionJSON struct {
Subscription *SubscriptionJSON `json:"subscription"` Subscription *SubscriptionJSON `json:"subscription"`
} }
type ChannelPreviewJSON struct {
ChannelID ChannelID `json:"channel_id"`
OwnerUserID UserID `json:"owner_user_id"`
InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
DescriptionName *string `json:"description_name"`
}
type ChannelDB struct { type ChannelDB struct {
ChannelID ChannelID `db:"channel_id"` ChannelID ChannelID `db:"channel_id"`
OwnerUserID UserID `db:"owner_user_id"` OwnerUserID UserID `db:"owner_user_id"`

View File

@ -5,7 +5,7 @@ package models
import "gogs.mikescher.com/BlackForestBytes/goext/langext" import "gogs.mikescher.com/BlackForestBytes/goext/langext"
import "gogs.mikescher.com/BlackForestBytes/goext/enums" import "gogs.mikescher.com/BlackForestBytes/goext/enums"
const ChecksumEnumGenerator = "5b115c5f107801af608630d2c5adce57cd4b050d176c8cd3db5c132020bf153c" // GoExtVersion: 0.0.463 const ChecksumEnumGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463
// ================================ ClientType ================================ // ================================ ClientType ================================
// //

View File

@ -15,7 +15,7 @@ import "reflect"
import "regexp" import "regexp"
import "strings" import "strings"
const ChecksumCharsetIDGenerator = "5b115c5f107801af608630d2c5adce57cd4b050d176c8cd3db5c132020bf153c" // GoExtVersion: 0.0.463 const ChecksumCharsetIDGenerator = "e500346e3f60b3abf78558ec3df128c3be2a1cefa71c4f1feba9293d14eb85d1" // GoExtVersion: 0.0.463
const idlen = 24 const idlen = 24

View File

@ -92,6 +92,17 @@ func (k KeyToken) JSON() KeyTokenJSON {
} }
} }
func (k KeyToken) JSONPreview() KeyTokenPreviewJSON {
return KeyTokenPreviewJSON{
KeyTokenID: k.KeyTokenID,
Name: k.Name,
OwnerUserID: k.OwnerUserID,
AllChannels: k.AllChannels,
Channels: k.Channels,
Permissions: k.Permissions.String(),
}
}
type KeyTokenJSON struct { type KeyTokenJSON struct {
KeyTokenID KeyTokenID `json:"keytoken_id"` KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"` Name string `json:"name"`
@ -109,6 +120,15 @@ type KeyTokenWithTokenJSON struct {
Token string `json:"token"` Token string `json:"token"`
} }
type KeyTokenPreviewJSON struct {
KeyTokenID KeyTokenID `json:"keytoken_id"`
Name string `json:"name"`
OwnerUserID UserID `json:"owner_user_id"`
AllChannels bool `json:"all_channels"`
Channels []ChannelID `json:"channels"`
Permissions string `json:"permissions"`
}
func (j KeyTokenJSON) WithToken(tok string) KeyTokenWithTokenJSON { func (j KeyTokenJSON) WithToken(tok string) KeyTokenWithTokenJSON {
return KeyTokenWithTokenJSON{ return KeyTokenWithTokenJSON{
KeyTokenJSON: j, KeyTokenJSON: j,

View File

@ -116,6 +116,13 @@ func (u User) MaxTimestampDiffHours() int {
return 24 return 24
} }
func (u User) JSONPreview() UserPreviewJSON {
return UserPreviewJSON{
UserID: u.UserID,
Username: u.Username,
}
}
type UserJSON struct { type UserJSON struct {
UserID UserID `json:"user_id"` UserID UserID `json:"user_id"`
Username *string `json:"username"` Username *string `json:"username"`
@ -137,6 +144,11 @@ type UserJSON struct {
MaxUserMessageIDLength int `json:"max_user_message_id_length"` MaxUserMessageIDLength int `json:"max_user_message_id_length"`
} }
type UserPreviewJSON struct {
UserID UserID `json:"user_id"`
Username *string `json:"username"`
}
type UserJSONWithClientsAndKeys struct { type UserJSONWithClientsAndKeys struct {
UserJSON UserJSON
Clients []ClientJSON `json:"clients"` Clients []ClientJSON `json:"clients"`

View File

@ -19,37 +19,61 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{
"type": "integer",
"name": "user_id",
"in": "query"
},
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "7725",
"name": "user_id",
"in": "query" "in": "query"
}, },
{ {
@ -62,37 +86,61 @@
}, },
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "7725",
"name": "user_id",
"in": "formData" "in": "formData"
} }
], ],
@ -1009,6 +1057,156 @@
} }
} }
}, },
"/api/v2/preview/channels/{cid}": {
"get": {
"tags": [
"API-v2"
],
"summary": "Get a single channel (similar to api-channels-get, but can be called from anyone and only returns a subset of fields)",
"operationId": "api-channels-get-preview",
"parameters": [
{
"type": "string",
"description": "ChannelID",
"name": "cid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.ChannelPreviewJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "channel not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/preview/keys/{kid}": {
"get": {
"tags": [
"API-v2"
],
"summary": "Get a single key (similar to api-tokenkeys-get, but can be called from anyone and only returns a subset of fields)",
"operationId": "api-tokenkeys-get-preview",
"parameters": [
{
"type": "string",
"description": "TokenKeyID",
"name": "kid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.KeyTokenPreviewJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "message not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/preview/users/{uid}": {
"get": {
"tags": [
"API-v2"
],
"summary": "Get a user (similar to api-user-get, but can be called from anyone and only returns a subset of fields)",
"operationId": "api-user-get-preview",
"parameters": [
{
"type": "string",
"description": "UserID",
"name": "uid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.UserPreviewJSON"
}
},
"400": {
"description": "supplied values/parameters cannot be parsed / are invalid",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"401": {
"description": "user is not authorized / has missing permissions",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/ginresp.apiError"
}
}
}
}
},
"/api/v2/users": { "/api/v2/users": {
"post": { "post": {
"tags": [ "tags": [
@ -2567,37 +2765,61 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{
"type": "integer",
"name": "user_id",
"in": "query"
},
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "7725",
"name": "user_id",
"in": "query" "in": "query"
}, },
{ {
@ -2610,37 +2832,61 @@
}, },
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "formData"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "7725",
"name": "user_id",
"in": "formData" "in": "formData"
} }
], ],
@ -2689,72 +2935,120 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"example": "test",
"name": "channel",
"in": "query"
},
{
"type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "query"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "query" "in": "query"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "query" "in": "query"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "query"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "query" "in": "query"
}, },
{ {
"type": "integer", "type": "string",
"example": "7725",
"name": "user_id", "name": "user_id",
"in": "query" "in": "query"
}, },
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "test",
"in": "query" "name": "channel",
"in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "This is a message",
"name": "content", "name": "content",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "P3TNH8mvv14fm",
"name": "key",
"in": "formData"
},
{
"type": "string",
"example": "db8b0e6a-a08c-4646",
"name": "msg_id", "name": "msg_id",
"in": "formData" "in": "formData"
}, },
{ {
"enum": [
0,
1,
2
],
"type": "integer", "type": "integer",
"example": 1,
"name": "priority", "name": "priority",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"example": "example-server",
"name": "sender_name",
"in": "formData"
},
{ {
"type": "number", "type": "number",
"example": 1669824037,
"name": "timestamp", "name": "timestamp",
"in": "formData" "in": "formData"
}, },
{ {
"type": "string", "type": "string",
"example": "Hello World",
"name": "title", "name": "title",
"in": "formData" "in": "formData"
}, },
{
"type": "integer",
"name": "user_id",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"name": "user_key", "example": "7725",
"name": "user_id",
"in": "formData" "in": "formData"
} }
], ],
@ -3251,26 +3545,46 @@
"handler.SendMessage.combined": { "handler.SendMessage.combined": {
"type": "object", "type": "object",
"properties": { "properties": {
"channel": {
"type": "string",
"example": "test"
},
"content": { "content": {
"type": "string" "type": "string",
"example": "This is a message"
},
"key": {
"type": "string",
"example": "P3TNH8mvv14fm"
}, },
"msg_id": { "msg_id": {
"type": "string" "type": "string",
"example": "db8b0e6a-a08c-4646"
}, },
"priority": { "priority": {
"type": "integer" "type": "integer",
"enum": [
0,
1,
2
],
"example": 1
},
"sender_name": {
"type": "string",
"example": "example-server"
}, },
"timestamp": { "timestamp": {
"type": "number" "type": "number",
"example": 1669824037
}, },
"title": { "title": {
"type": "string" "type": "string",
"example": "Hello World"
}, },
"user_id": { "user_id": {
"type": "integer" "type": "string",
}, "example": "7725"
"user_key": {
"type": "string"
} }
} }
}, },
@ -3299,7 +3613,7 @@
"type": "integer" "type": "integer"
}, },
"scn_msg_id": { "scn_msg_id": {
"type": "integer" "type": "string"
}, },
"success": { "success": {
"type": "boolean" "type": "boolean"
@ -3487,6 +3801,26 @@
} }
} }
}, },
"models.ChannelPreviewJSON": {
"type": "object",
"properties": {
"channel_id": {
"type": "string"
},
"description_name": {
"type": "string"
},
"display_name": {
"type": "string"
},
"internal_name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
}
}
},
"models.ChannelWithSubscriptionJSON": { "models.ChannelWithSubscriptionJSON": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3630,6 +3964,32 @@
} }
} }
}, },
"models.KeyTokenPreviewJSON": {
"type": "object",
"properties": {
"all_channels": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"type": "string"
}
},
"keytoken_id": {
"type": "string"
},
"name": {
"type": "string"
},
"owner_user_id": {
"type": "string"
},
"permissions": {
"type": "string"
}
}
},
"models.KeyTokenWithTokenJSON": { "models.KeyTokenWithTokenJSON": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3870,6 +4230,17 @@
"type": "string" "type": "string"
} }
} }
},
"models.UserPreviewJSON": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
},
"username": {
"type": "string"
}
}
} }
}, },
"tags": [ "tags": [

View File

@ -327,19 +327,36 @@ definitions:
type: object type: object
handler.SendMessage.combined: handler.SendMessage.combined:
properties: properties:
channel:
example: test
type: string
content: content:
example: This is a message
type: string
key:
example: P3TNH8mvv14fm
type: string type: string
msg_id: msg_id:
example: db8b0e6a-a08c-4646
type: string type: string
priority: priority:
enum:
- 0
- 1
- 2
example: 1
type: integer type: integer
sender_name:
example: example-server
type: string
timestamp: timestamp:
example: 1669824037
type: number type: number
title: title:
example: Hello World
type: string type: string
user_id: user_id:
type: integer example: "7725"
user_key:
type: string type: string
type: object type: object
handler.SendMessage.response: handler.SendMessage.response:
@ -359,7 +376,7 @@ definitions:
quota_max: quota_max:
type: integer type: integer
scn_msg_id: scn_msg_id:
type: integer type: string
success: success:
type: boolean type: boolean
suppress_send: suppress_send:
@ -480,6 +497,19 @@ definitions:
uri: uri:
type: string type: string
type: object type: object
models.ChannelPreviewJSON:
properties:
channel_id:
type: string
description_name:
type: string
display_name:
type: string
internal_name:
type: string
owner_user_id:
type: string
type: object
models.ChannelWithSubscriptionJSON: models.ChannelWithSubscriptionJSON:
properties: properties:
channel_id: channel_id:
@ -577,6 +607,23 @@ definitions:
timestamp_lastused: timestamp_lastused:
type: string type: string
type: object type: object
models.KeyTokenPreviewJSON:
properties:
all_channels:
type: boolean
channels:
items:
type: string
type: array
keytoken_id:
type: string
name:
type: string
owner_user_id:
type: string
permissions:
type: string
type: object
models.KeyTokenWithTokenJSON: models.KeyTokenWithTokenJSON:
properties: properties:
all_channels: all_channels:
@ -736,6 +783,13 @@ definitions:
username: username:
type: string type: string
type: object type: object
models.UserPreviewJSON:
properties:
user_id:
type: string
username:
type: string
type: object
host: simplecloudnotifier.de host: simplecloudnotifier.de
info: info:
contact: {} contact: {}
@ -748,52 +802,90 @@ paths:
description: All parameter can be set via query-parameter or the json body. description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- in: query - example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- in: query - example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- in: query - enum:
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- in: query - example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- in: query - example: Hello World
in: query
name: title name: title
type: string type: string
- in: query - example: "7725"
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string type: string
- description: ' ' - description: ' '
in: body in: body
name: post_body name: post_body
schema: schema:
$ref: '#/definitions/handler.SendMessage.combined' $ref: '#/definitions/handler.SendMessage.combined'
- in: formData - example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- in: formData - example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- in: formData - enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- in: formData - example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- in: formData - example: Hello World
in: formData
name: title name: title
type: string type: string
- in: formData - example: "7725"
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":
@ -1422,6 +1514,108 @@ paths:
summary: Get a single message (untrimmed) summary: Get a single message (untrimmed)
tags: tags:
- API-v2 - API-v2
/api/v2/preview/channels/{cid}:
get:
operationId: api-channels-get-preview
parameters:
- description: ChannelID
in: path
name: cid
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.ChannelPreviewJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: channel not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single channel (similar to api-channels-get, but can be called
from anyone and only returns a subset of fields)
tags:
- API-v2
/api/v2/preview/keys/{kid}:
get:
operationId: api-tokenkeys-get-preview
parameters:
- description: TokenKeyID
in: path
name: kid
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.KeyTokenPreviewJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: message not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a single key (similar to api-tokenkeys-get, but can be called from
anyone and only returns a subset of fields)
tags:
- API-v2
/api/v2/preview/users/{uid}:
get:
operationId: api-user-get-preview
parameters:
- description: UserID
in: path
name: uid
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.UserPreviewJSON'
"400":
description: supplied values/parameters cannot be parsed / are invalid
schema:
$ref: '#/definitions/ginresp.apiError'
"401":
description: user is not authorized / has missing permissions
schema:
$ref: '#/definitions/ginresp.apiError'
"404":
description: user not found
schema:
$ref: '#/definitions/ginresp.apiError'
"500":
description: internal server error
schema:
$ref: '#/definitions/ginresp.apiError'
summary: Get a user (similar to api-user-get, but can be called from anyone
and only returns a subset of fields)
tags:
- API-v2
/api/v2/users: /api/v2/users:
post: post:
operationId: api-user-create operationId: api-user-create
@ -2491,52 +2685,90 @@ paths:
description: All parameter can be set via query-parameter or the json body. description: All parameter can be set via query-parameter or the json body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- in: query - example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- in: query - example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- in: query - enum:
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- in: query - example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- in: query - example: Hello World
in: query
name: title name: title
type: string type: string
- in: query - example: "7725"
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string type: string
- description: ' ' - description: ' '
in: body in: body
name: post_body name: post_body
schema: schema:
$ref: '#/definitions/handler.SendMessage.combined' $ref: '#/definitions/handler.SendMessage.combined'
- in: formData - example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- in: formData - example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- in: formData - enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- in: formData - example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- in: formData - example: Hello World
in: formData
name: title name: title
type: string type: string
- in: formData - example: "7725"
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":
@ -2569,47 +2801,85 @@ paths:
description: All parameter can be set via query-parameter or form-data body. description: All parameter can be set via query-parameter or form-data body.
Only UserID, UserKey and Title are required Only UserID, UserKey and Title are required
parameters: parameters:
- in: query - example: test
in: query
name: channel
type: string
- example: This is a message
in: query
name: content name: content
type: string type: string
- in: query - example: P3TNH8mvv14fm
in: query
name: key
type: string
- example: db8b0e6a-a08c-4646
in: query
name: msg_id name: msg_id
type: string type: string
- in: query - enum:
- 0
- 1
- 2
example: 1
in: query
name: priority name: priority
type: integer type: integer
- in: query - example: example-server
in: query
name: sender_name
type: string
- example: 1669824037
in: query
name: timestamp name: timestamp
type: number type: number
- in: query - example: Hello World
in: query
name: title name: title
type: string type: string
- in: query - example: "7725"
in: query
name: user_id name: user_id
type: integer
- in: query
name: user_key
type: string type: string
- in: formData - example: test
in: formData
name: channel
type: string
- example: This is a message
in: formData
name: content name: content
type: string type: string
- in: formData - example: P3TNH8mvv14fm
in: formData
name: key
type: string
- example: db8b0e6a-a08c-4646
in: formData
name: msg_id name: msg_id
type: string type: string
- in: formData - enum:
- 0
- 1
- 2
example: 1
in: formData
name: priority name: priority
type: integer type: integer
- in: formData - example: example-server
in: formData
name: sender_name
type: string
- example: 1669824037
in: formData
name: timestamp name: timestamp
type: number type: number
- in: formData - example: Hello World
in: formData
name: title name: title
type: string type: string
- in: formData - example: "7725"
in: formData
name: user_id name: user_id
type: integer
- in: formData
name: user_key
type: string type: string
responses: responses:
"200": "200":