From 95353735b087db1b226266607bf0abf59a414d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 13 Apr 2025 00:17:06 +0200 Subject: [PATCH] Implement Scanner-View --- .gitignore | 1 + flutter/Makefile | 11 +- flutter/TODO.md | 16 +- flutter/_utils/autoreload.sh | 29 ++- flutter/lib/api/api_client.dart | 48 +++- flutter/lib/models/channel.dart | 10 +- flutter/lib/models/scan_result.dart | 22 +- flutter/lib/models/send_message_response.dart | 55 ++++ .../lib/pages/channel_list/channel_list.dart | 2 +- .../pages/channel_list/channel_scanner.dart | 117 --------- .../channel_scanner/channel_scanner.dart | 159 ++++++++++++ ...annel_scanner_result_channelsubscribe.dart | 198 +++++++++++++++ .../channel_scanner_result_channelview.dart | 157 ++++++++++++ .../channel_scanner_result_messagesend.dart | 239 ++++++++++++++++++ .../lib/pages/channel_view/channel_view.dart | 59 +++-- .../lib/pages/message_view/message_view.dart | 2 +- flutter/lib/pages/send/send.dart | 3 +- flutter/lib/utils/navi.dart | 9 + flutter/lib/utils/ui.dart | 3 +- 19 files changed, 961 insertions(+), 179 deletions(-) create mode 100644 .gitignore create mode 100644 flutter/lib/models/send_message_response.dart delete mode 100644 flutter/lib/pages/channel_list/channel_scanner.dart create mode 100644 flutter/lib/pages/channel_scanner/channel_scanner.dart create mode 100644 flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart create mode 100644 flutter/lib/pages/channel_scanner/channel_scanner_result_channelview.dart create mode 100644 flutter/lib/pages/channel_scanner/channel_scanner_result_messagesend.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0ac3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.aider* diff --git a/flutter/Makefile b/flutter/Makefile index 86e905a..4ae6dc5 100644 --- a/flutter/Makefile +++ b/flutter/Makefile @@ -14,9 +14,9 @@ run-linux: _JAVA_OPTIONS="" flutter run -d linux # runs app locally (web | not really supported) -run-linux: +run-web: dart run build_runner build - _JAVA_OPTIONS="" flutter run -d web + _JAVA_OPTIONS="" flutter run -d chrome # runs on android device (must have network adb enabled teh correct IP) run-android: @@ -42,7 +42,7 @@ fix: dart fix --apply gen: - flutter pub run build_runner build + dart run build_runner build # run `make run` in another terminal (or another variant of flutter run) autoreload: @@ -63,4 +63,7 @@ clean: upgrade: flutter upgrade flutter pub upgrade - flutter doctor \ No newline at end of file + flutter doctor + +aider: + aider --model gemini-2.5-pro --no-auto-commits --no-dirty-commits --test-cmd "flutter build linux" --auto-test --subtree-only \ No newline at end of file diff --git a/flutter/TODO.md b/flutter/TODO.md index 6f7c89b..ed98fa2 100644 --- a/flutter/TODO.md +++ b/flutter/TODO.md @@ -1,17 +1,17 @@ # TODO - - [ ] Message List + - [x] Message List * [ ] CRUD - - [ ] Message Big-View - - [ ] Search/Filter Messages - - [ ] Channel List - * [ ] Show subs + - [x] Message Big-View + - [x] Search/Filter Messages + - [x] Channel List + * [x] Show subs * [ ] CRUD * [ ] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?) - - [ ] Sub List - * [ ] Sub/Unsub/Accept/Deny - - [ ] Debug List (Show logs, requests) + - [x] Sub List + * [x] Sub/Unsub/Accept/Deny + - [x] Debug List (Show logs, requests) - [ ] Key List * [ ] CRUD - [ ] Auto R-only key for admin, use for QR+link+send diff --git a/flutter/_utils/autoreload.sh b/flutter/_utils/autoreload.sh index 0823697..e33a1e7 100755 --- a/flutter/_utils/autoreload.sh +++ b/flutter/_utils/autoreload.sh @@ -3,8 +3,8 @@ # shellcheck disable=SC2002 # disable useless-cat warning set -o nounset # disallow usage of unset vars ( set -u ) - set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e ) - set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E ) + #set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e ) + #set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E ) set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status IFS=$'\n\t' # Set $IFS to only newline and tab. @@ -24,9 +24,9 @@ -pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )" +pids="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )" -if [ -z "$pid" ]; then +if [ -z "$pids" ]; then red "No [flutter run] process found - exiting" exit 1 fi @@ -37,10 +37,21 @@ trap 'echo "reseived SIGNAL - exiting"; exit 0' SIGTERM trap 'echo "reseived SIGNAL - exiting"; exit 0' SIGQUIT echo "" -blue "Listening for changes in lib/ directory - sending signals to ${pid}..." +while IFS= read -r pid; do + blue "Listening for changes in lib/ directory - sending signals to ${pid}..." +done <<< "$pids" + echo "" -while true; do - find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid"; - yellow 'File list changed - restart'; -done \ No newline at end of file +while IFS= read -r pid; do + { + while true; do + find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid"; + yellow 'File list changed - restart'; + done + } & +done <<< "$pids" + +wait # wait for all background jobs to finish + +echo "DONE." \ No newline at end of file diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index d0dc986..052b02c 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/models/api_error.dart'; import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/models/send_message_response.dart'; import 'package:simplecloudnotifier/models/sender_name_statistics.dart'; import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/user.dart'; @@ -50,7 +51,8 @@ class MessageFilter { } class APIClient { - static const String _base = 'https://simplecloudnotifier.de/api/v2'; + static const String _base = 'https://simplecloudnotifier.de'; + static const String _prefix = '/api/v2'; static Future _request({ required String name, @@ -61,10 +63,11 @@ class APIClient { dynamic jsonBody, String? authToken, Map? header, + bool? nonAPI, }) async { final t0 = DateTime.now(); - final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {}); + final uri = Uri.parse('$_base${(nonAPI ?? false) ? '' : _prefix}/$relURL').replace(queryParameters: query ?? {}); final req = http.Request(method, uri); @@ -380,9 +383,9 @@ class APIClient { ); } - static Future getKeyTokenPreview(TokenSource auth, String kid) async { + static Future getKeyTokenPreviewByID(TokenSource auth, String kid) async { return await _request( - name: 'getKeyTokenPreview', + name: 'getKeyTokenPreviewByID', method: 'GET', relURL: 'preview/keys/$kid', fn: KeyTokenPreview.fromJson, @@ -390,6 +393,16 @@ class APIClient { ); } + static Future getKeyTokenPreviewByToken(TokenSource auth, String tok) async { + return await _request( + name: 'getKeyTokenPreviewByToken', + method: 'GET', + relURL: 'preview/keys/$tok', + fn: KeyTokenPreview.fromJson, + authToken: auth.getToken(), + ); + } + static Future getKeyTokenByToken(String userid, String token) async { return await _request( name: 'getCurrentKeyToken', @@ -410,11 +423,14 @@ class APIClient { ); } - static Future subscribeToChannelbyID(TokenSource auth, String channelID) async { + static Future subscribeToChannelbyID(TokenSource auth, String channelID, {String? subscribeKey}) async { return await _request( name: 'subscribeToChannelbyID', method: 'POST', relURL: 'users/${auth.getUserID()}/subscriptions', + query: { + if (subscribeKey != null) 'chan_subscribe_key': [subscribeKey], + }, jsonBody: { 'channel_id': channelID, }, @@ -458,4 +474,26 @@ class APIClient { authToken: auth.getToken(), ); } + + static Future sendMessage(String userid, String keytoken, String text, {String? channel, String? content, String? messageID, int? priority, String? senderName, DateTime? timestamp}) async { + return await _request( + name: 'sendMessage', + method: 'POST', + relURL: '/send', + nonAPI: true, + jsonBody: { + 'user_id': userid, + 'key': keytoken, + 'title': text, + if (channel != null) 'channel': channel, + if (content != null) 'content': content, + if (priority != null) 'priority': priority, + if (messageID != null) 'msg_id': messageID, + if (timestamp != null) 'timestamp': (timestamp.microsecondsSinceEpoch / 1000).toInt(), + if (senderName != null) 'sender_name': senderName, + }, + fn: SendMessageResponse.fromJson, + authToken: null, + ); + } } diff --git a/flutter/lib/models/channel.dart b/flutter/lib/models/channel.dart index d04bf25..7f8da5f 100644 --- a/flutter/lib/models/channel.dart +++ b/flutter/lib/models/channel.dart @@ -71,13 +71,15 @@ class Channel extends HiveObject implements FieldDebuggable { ]; } - ChannelPreview toPreview() { + ChannelPreview toPreview(Subscription? sub) { return ChannelPreview( channelID: this.channelID, ownerUserID: this.ownerUserID, internalName: this.internalName, displayName: this.displayName, descriptionName: this.descriptionName, + messagesSent: this.messagesSent, + subscription: sub, ); } } @@ -109,6 +111,8 @@ class ChannelPreview { final String internalName; final String displayName; final String? descriptionName; + final int messagesSent; + final Subscription? subscription; const ChannelPreview({ required this.channelID, @@ -116,6 +120,8 @@ class ChannelPreview { required this.internalName, required this.displayName, required this.descriptionName, + required this.messagesSent, + required this.subscription, }); factory ChannelPreview.fromJson(Map json) { @@ -125,6 +131,8 @@ class ChannelPreview { internalName: json['internal_name'] as String, displayName: json['display_name'] as String, descriptionName: json['description_name'] as String?, + messagesSent: json['messages_sent'] as int, + subscription: json['subscription'] == null ? null : Subscription.fromJson(json['subscription'] as Map), ); } } diff --git a/flutter/lib/models/scan_result.dart b/flutter/lib/models/scan_result.dart index afc6abb..9985627 100644 --- a/flutter/lib/models/scan_result.dart +++ b/flutter/lib/models/scan_result.dart @@ -1,6 +1,6 @@ import 'package:simplecloudnotifier/models/channel.dart'; -enum ScanResultMode { ChannelSubscribe, MessageSend, Channel } +enum ScanResultMode { ChannelSubscribe, MessageSend, Channel, Error } abstract class ScanResult { ScanResultMode get mode; @@ -12,10 +12,10 @@ abstract class ScanResult { final v = Uri.tryParse(lines[0]); if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) { - return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: v.queryParameters['preset_user_key']); + return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: v.queryParameters['preset_user_key'], url: lines[0]); } if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) { - return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null); + return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null, url: lines[0]); } } @@ -24,12 +24,12 @@ abstract class ScanResult { } if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') { - if (lines.length != 4) return null; + if (lines.length != 5) return null; return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]); } - return null; + return ScanResultError(message: 'Invalid QR code'); } static String createChannelQR(Channel channel) { @@ -44,8 +44,9 @@ abstract class ScanResult { class ScanResultMessageSend extends ScanResult { final String userID; final String? userKey; + final String url; - ScanResultMessageSend({required this.userID, required this.userKey}); + ScanResultMessageSend({required this.userID, required this.userKey, required this.url}); @override ScanResultMode get mode => ScanResultMode.MessageSend; @@ -73,3 +74,12 @@ class ScanResultChannelSubscribe extends ScanResult { @override ScanResultMode get mode => ScanResultMode.ChannelSubscribe; } + +class ScanResultError extends ScanResult { + final String message; + + ScanResultError({required this.message}); + + @override + ScanResultMode get mode => ScanResultMode.Error; +} diff --git a/flutter/lib/models/send_message_response.dart b/flutter/lib/models/send_message_response.dart new file mode 100644 index 0000000..515fdab --- /dev/null +++ b/flutter/lib/models/send_message_response.dart @@ -0,0 +1,55 @@ +class SendMessageResponse { + final bool success; + final int errorID; + final int errorHighlight; + final String message; + final bool suppressSend; + final int messageCount; + final int quota; + final bool isPro; + final int quotaMax; + final String scnMessageID; + + SendMessageResponse({ + required this.success, + required this.errorID, + required this.errorHighlight, + required this.message, + required this.suppressSend, + required this.messageCount, + required this.quota, + required this.isPro, + required this.quotaMax, + required this.scnMessageID, + }); + + factory SendMessageResponse.fromJson(Map json) { + return SendMessageResponse( + success: json['success'] as bool, + errorID: json['error'] as int, + errorHighlight: json['errhighlight'] as int, + message: json['message'] as String, + suppressSend: json['suppress_send'] as bool, + messageCount: json['messagecount'] as int, + quota: json['quota'] as int, + isPro: json['is_pro'] as bool, + quotaMax: json['quota_max'] as int, + scnMessageID: json['scn_msg_id'] as String, + ); + } + + Map toJson() { + return { + 'success': success, + 'error': errorID, + 'errhighlight': errorHighlight, + 'message': message, + 'suppress_send': suppressSend, + 'messagecount': messageCount, + 'quota': quota, + 'is_pro': isPro, + 'quota_max': quotaMax, + 'scn_msg_id': scnMessageID, + }; + } +} diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart index 3b19b55..ba5c500 100644 --- a/flutter/lib/pages/channel_list/channel_list.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -4,7 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; -import 'package:simplecloudnotifier/pages/channel_list/channel_scanner.dart'; +import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; diff --git a/flutter/lib/pages/channel_list/channel_scanner.dart b/flutter/lib/pages/channel_list/channel_scanner.dart deleted file mode 100644 index 126eefb..0000000 --- a/flutter/lib/pages/channel_list/channel_scanner.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:simplecloudnotifier/components/layout/scaffold.dart'; -import 'package:simplecloudnotifier/models/scan_result.dart'; -import 'package:simplecloudnotifier/utils/ui.dart'; - -class ChannelScannerPage extends StatefulWidget { - const ChannelScannerPage({super.key}); - - @override - State createState() => _ChannelScannerPageState(); -} - -class _ChannelScannerPageState extends State { - final MobileScannerController _controller = MobileScannerController( - formats: const [BarcodeFormat.qrCode], - ); - - ScanResult? scanResult = null; - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return SCNScaffold( - title: "Scanner", - showSearch: false, - showShare: false, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), - child: Column( - children: [ - SizedBox(height: 16), - Center( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(UI.DefaultBorderRadius), - ), - clipBehavior: Clip.hardEdge, - child: SizedBox( - height: 300, - width: 300, - child: MobileScanner( - fit: BoxFit.cover, - controller: _controller, - onDetect: _handleBarcode, - ), - ), - ), - ), - SizedBox(height: 16), - _buildScanResult(context), - ], - ), - ), - ), - ); - } - - void _handleBarcode(BarcodeCapture barcodes) { - setState(() { - if (barcodes.barcodes.isEmpty) { - scanResult = null; - } else { - print('parsed: ${barcodes.barcodes[0].rawValue}'); - scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? ''); - } - }); - } - - Widget _buildScanResult(BuildContext context) { - if (scanResult == null) { - return UI.box( - padding: EdgeInsets.fromLTRB(16, 2, 4, 2), //TODO - context: context, - child: Center( - child: Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128)), - ), - ); - } - - if (scanResult! is ScanResultMessageSend) { - return UI.box( - padding: EdgeInsets.fromLTRB(16, 2, 4, 2), - context: context, - child: Text("TODO -- ScanResultMessageSend"), //TODO - ); - } - - if (scanResult! is ScanResultChannel) { - return UI.box( - padding: EdgeInsets.fromLTRB(16, 2, 4, 2), - context: context, - child: Text("TODO -- ScanResultChannel"), //TODO - ); - } - - if (scanResult! is ScanResultChannelSubscribe) { - return UI.box( - padding: EdgeInsets.fromLTRB(16, 2, 4, 2), - context: context, - child: Text("TODO -- ScanResultChannelSubscribe"), //TODO - ); - } - - return UI.box( - padding: EdgeInsets.fromLTRB(16, 2, 4, 2), - context: context, - child: Text("TODO -- ERROR"), //TODO - ); - } -} diff --git a/flutter/lib/pages/channel_scanner/channel_scanner.dart b/flutter/lib/pages/channel_scanner/channel_scanner.dart new file mode 100644 index 0000000..33d797e --- /dev/null +++ b/flutter/lib/pages/channel_scanner/channel_scanner.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/models/scan_result.dart'; +import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart'; +import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelview.dart'; +import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_messagesend.dart'; +import 'package:simplecloudnotifier/utils/ui.dart'; + +class ChannelScannerPage extends StatefulWidget { + const ChannelScannerPage({super.key}); + + @override + State createState() => _ChannelScannerPageState(); +} + +class _ChannelScannerPageState extends State { + final MobileScannerController _controller = MobileScannerController( + formats: const [BarcodeFormat.qrCode], + ); + + ScanResult? scanResult = null; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: "Scanner", + showSearch: false, + showShare: false, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), + child: Column( + children: [ + SizedBox(height: 16), + if (scanResult == null) ...[ + Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(UI.DefaultBorderRadius), + ), + clipBehavior: Clip.hardEdge, + child: SizedBox( + height: 300, + width: 300, + child: MobileScanner( + fit: BoxFit.cover, + controller: _controller, + onDetect: _handleBarcode, + ), + ), + ), + ), + SizedBox(height: 16), + ], + Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 300, minWidth: 300, minHeight: 200), + child: _buildScanResult(context), + ), + ), + ], + ), + ), + ), + ); + } + + void _handleBarcode(BarcodeCapture barcodes) { + setState(() { + if (barcodes.barcodes.isEmpty) { + scanResult = null; + } else { + scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? ''); + print('parsed: ${jsonEncode(barcodes.barcodes[0].rawValue)} as ${scanResult.runtimeType.toString()}'); + } + }); + } + + Widget _buildScanResult(BuildContext context) { + if (scanResult == null) { + return UI.box( + padding: EdgeInsets.fromLTRB(16, 32, 16, 8), + context: context, + child: Center( + child: Column( + spacing: 32, + children: [ + Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(48)), + Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center), + ], + ), + ), + ); + } + + if (scanResult! is ScanResultMessageSend) { + return UI.box( + padding: EdgeInsets.fromLTRB(16, 8, 16, 8), + context: context, + child: ChannelScannerResultMessageSend(value: scanResult! as ScanResultMessageSend), + ); + } + + if (scanResult! is ScanResultChannel) { + return UI.box( + padding: EdgeInsets.fromLTRB(16, 8, 16, 8), + context: context, + child: ChannelScannerResultChannelView(value: scanResult! as ScanResultChannel), + ); + } + + if (scanResult! is ScanResultChannelSubscribe) { + return UI.box( + padding: EdgeInsets.fromLTRB(16, 8, 16, 8), + context: context, + child: ChannelScannerResultChannelSubscribe(value: scanResult! as ScanResultChannelSubscribe), + ); + } + + if (scanResult! is ScanResultError) { + return UI.box( + padding: EdgeInsets.fromLTRB(16, 32, 16, 8), + context: context, + child: Center( + child: Column( + spacing: 32, + children: [ + Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]), + Text((scanResult! as ScanResultError).message, textAlign: TextAlign.center), + ], + ), + ), + ); + } + + return UI.box( + padding: EdgeInsets.fromLTRB(16, 32, 16, 8), + context: context, + child: Center( + child: Column( + spacing: 32, + children: [ + Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]), + Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center), + ], + ), + ), + ); + } +} diff --git a/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart b/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart new file mode 100644 index 0000000..1b0775a --- /dev/null +++ b/flutter/lib/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/scan_result.dart'; +import 'package:simplecloudnotifier/models/subscription.dart'; +import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/utils/navi.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; +import 'package:simplecloudnotifier/utils/ui.dart'; + +class ChannelScannerResultChannelSubscribe extends StatefulWidget { + final ScanResultChannelSubscribe value; + + const ChannelScannerResultChannelSubscribe({required this.value}) : super(); + + @override + State createState() => _ChannelScannerResultChannelSubscribeState(); +} + +class _ChannelScannerResultChannelSubscribeState extends State { + Future<(ChannelPreview, UserPreview)?> _fetchDataFuture; + + _ChannelScannerResultChannelSubscribeState() : _fetchDataFuture = Future.value(null); // Initial dummy future + + Subscription? overrideSubscription = null; + + @override + void initState() { + super.initState(); + + final auth = Provider.of(context, listen: false); + setState(() { + _fetchDataFuture = _fetchData(auth); + }); + } + + Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async { + ChannelPreview? channel = null; + try { + channel = await APIClient.getChannelPreview(auth, widget.value.channelID); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}'); + ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace); + return null; + } + + UserPreview? user = null; + try { + user = await APIClient.getUserPreview(auth, widget.value.ownerUserID); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}'); + ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace); + return null; + } + + return (channel, user); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder<(ChannelPreview, UserPreview)?>( + future: _fetchDataFuture, + builder: (context, snapshot) { + final auth = Provider.of(context, listen: false); + + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); //TODO better error display + } + + if (snapshot.data == null) { + return Column( + spacing: 32, + children: [ + Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]), + Text("Failed to parse QR", textAlign: TextAlign.center), + ], + ); + } + + final (channel, user) = snapshot.data!; + + final sub = overrideSubscription ?? channel.subscription; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center), + const SizedBox(height: 16), + Row( + children: [ + ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)), + ], + ), + if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty) + Row( + children: [ + ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text((user.username ?? user.userID) + ((auth.userID != null && auth.userID! == user.userID) ? "\n(you)" : "")), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(sub)), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 48), + if (sub == null) + UI.button( + text: 'Request Subscription', + onPressed: _onSubscribe, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + if (sub != null && sub.confirmed) + UI.button( + text: 'Go to channel', + onPressed: () { + Navi.pushOnRoot(context, () => ChannelViewPage(channelID: widget.value.channelID, preloadedData: null, needsReload: null)); + }, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ], + ); + }, + ); + } + + void _onSubscribe() async { + final auth = Provider.of(context, listen: false); + try { + var sub = await APIClient.subscribeToChannelbyID(auth, widget.value.channelID, subscribeKey: widget.value.subscribeKey); + if (sub.confirmed) { + Toaster.success("Success", "Subscription request sent and auto-confirmed"); + } else { + Toaster.success("Success", "Subscription request sent - pending confirmation"); + } + setState(() { + overrideSubscription = sub; + }); + } catch (e) { + Toaster.error("Error", 'Failed to send subscription-request: ${e.toString()}'); + } + } + + String _formatSubscriptionStatus(Subscription? sub) { + if (sub == null) { + return "Not Subscribed"; + } else if (sub.confirmed) { + return "Already Subscribed"; + } else { + return "Unconfirmed Subscription"; + } + } +} diff --git a/flutter/lib/pages/channel_scanner/channel_scanner_result_channelview.dart b/flutter/lib/pages/channel_scanner/channel_scanner_result_channelview.dart new file mode 100644 index 0000000..3db0e79 --- /dev/null +++ b/flutter/lib/pages/channel_scanner/channel_scanner_result_channelview.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/scan_result.dart'; +import 'package:simplecloudnotifier/models/subscription.dart'; +import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; + +class ChannelScannerResultChannelView extends StatefulWidget { + final ScanResultChannel value; + + const ChannelScannerResultChannelView({required this.value}) : super(); + + @override + State createState() => _ChannelScannerResultChannelViewState(); +} + +class _ChannelScannerResultChannelViewState extends State { + Future<(ChannelPreview, UserPreview)?> _fetchDataFuture; + + _ChannelScannerResultChannelViewState() : _fetchDataFuture = Future.value(null); // Initial dummy future + + @override + void initState() { + super.initState(); + + final auth = Provider.of(context, listen: false); + setState(() { + _fetchDataFuture = _fetchData(auth); + }); + } + + Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async { + ChannelPreview? channel = null; + try { + channel = await APIClient.getChannelPreview(auth, widget.value.channelID); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}'); + ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace); + return null; + } + + UserPreview? user = null; + try { + user = await APIClient.getUserPreview(auth, widget.value.ownerUserID); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}'); + ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace); + return null; + } + + return (channel, user); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder<(ChannelPreview, UserPreview)?>( + future: _fetchDataFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); //TODO better error display + } + + if (snapshot.data == null) { + return Column( + spacing: 32, + children: [ + Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]), + Text("Failed to parse QR", textAlign: TextAlign.center), + ], + ); + } + + final (channel, user) = snapshot.data!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center), + const SizedBox(height: 16), + Row( + children: [ + ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)), + ], + ), + if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty) + Row( + children: [ + ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(user.username ?? user.userID), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(channel.subscription)), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 48), + Text('QR Code contains no subscription-key\nCannot subscribe to channel', textAlign: TextAlign.center, style: const TextStyle(fontStyle: FontStyle.italic)), + ], + ); + }, + ); + } + + String _formatSubscriptionStatus(Subscription? sub) { + if (sub == null) { + return "Not Subscribed"; + } else if (sub.confirmed) { + return "Already Subscribed"; + } else { + return "Unconfirmed Subscription"; + } + } +} diff --git a/flutter/lib/pages/channel_scanner/channel_scanner_result_messagesend.dart b/flutter/lib/pages/channel_scanner/channel_scanner_result_messagesend.dart new file mode 100644 index 0000000..8d02287 --- /dev/null +++ b/flutter/lib/pages/channel_scanner/channel_scanner_result_messagesend.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/models/scan_result.dart'; +import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; +import 'package:simplecloudnotifier/utils/ui.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ChannelScannerResultMessageSend extends StatefulWidget { + final ScanResultMessageSend value; + + const ChannelScannerResultMessageSend({required this.value}) : super(); + + @override + State createState() => _ChannelScannerResultMessageSendState(); +} + +class _ChannelScannerResultMessageSendState extends State { + Future<(UserPreview, KeyTokenPreview?)?> _fetchDataFuture; + + _ChannelScannerResultMessageSendState() : _fetchDataFuture = Future.value(null); // Initial dummy future + + late TextEditingController _ctrlMessage; + + @override + void initState() { + super.initState(); + + _ctrlMessage = TextEditingController(); + + final auth = Provider.of(context, listen: false); + setState(() { + _fetchDataFuture = _fetchData(auth); + }); + } + + @override + void dispose() { + _ctrlMessage.dispose(); + super.dispose(); + } + + Future<(UserPreview, KeyTokenPreview?)?> _fetchData(AppAuth auth) async { + UserPreview? user = null; + try { + user = await APIClient.getUserPreview(auth, widget.value.userID); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}'); + ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.userID}', trace: stackTrace); + return null; + } + + KeyTokenPreview? key = null; + if (widget.value.userKey != null) { + try { + key = await APIClient.getKeyTokenPreviewByToken(auth, widget.value.userKey!); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to fetch keytoken preview: ${e.toString()}'); + ApplicationLog.error('Failed to fetch keytoken (preview) for ${widget.value.userID}', trace: stackTrace); + return null; + } + } + + return (user, key); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder<(UserPreview, KeyTokenPreview?)?>( + future: _fetchDataFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); //TODO better error display + } + + if (snapshot.data == null) { + return Column( + spacing: 32, + children: [ + Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]), + Text("Failed to parse QR", textAlign: TextAlign.center), + ], + ); + } + + final (user, key) = snapshot.data!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text((widget.value.userKey == null) ? "SCN User" : "SCN User & Key", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center), + const SizedBox(height: 16), + if (user.username != null) + Row( + children: [ + ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(user.username!), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + const SizedBox(height: 16), + if (key != null) ...[ + Row( + children: [ + ConstrainedBox(child: Text("KeyID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(key.keytokenID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)), + ], + ), + Row( + children: [ + ConstrainedBox(child: Text("KeyName:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded(child: SingleChildScrollView(child: Text(key.name), scrollDirection: Axis.horizontal)), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox(child: Text("Permissions:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)), + Expanded( + child: SingleChildScrollView( + child: Text(_formatPermissions(key.permissions) + "\n" + (key.allChannels ? "(all channels)" : '(${key.channels.length} channels)')), + scrollDirection: Axis.horizontal, + ), + ), + ], + ), + ], + const SizedBox(height: 16), + if (widget.value.userKey == null) + Text( + 'QR Code contains no key\nCannot send messages', + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + if (widget.value.userKey != null) ..._buildSend(context), + ], + ); + }, + ); + } + + List _buildSend(BuildContext context) { + return [ + FractionallySizedBox( + widthFactor: 1.0, + child: TextField( + controller: _ctrlMessage, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Text', + ), + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: UI.button( + text: 'Send Message', + onPressed: _onSend, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + const SizedBox(width: 8), + UI.button( + text: 'Web', + onPressed: _onOpenWeb, + color: Theme.of(context).colorScheme.secondary, + textColor: Theme.of(context).colorScheme.onSecondary, + ), + ], + ), + ]; + } + + void _onSend() async { + if (_ctrlMessage.text.isEmpty) { + Toaster.error("Error", 'Please enter a message'); + return; + } + + if (widget.value.userKey == null) return; + + try { + await APIClient.sendMessage(widget.value.userID, widget.value.userKey!, _ctrlMessage.text); + Toaster.success("Success", 'Message sent'); + } catch (e, stackTrace) { + Toaster.error("Error", 'Failed to send message: ${e.toString()}'); + ApplicationLog.error('Failed to send message', trace: stackTrace); + } + } + + void _onOpenWeb() async { + try { + final Uri uri = Uri.parse(widget.value.url); + + ApplicationLog.debug('Opening URL: [ ${uri.toString()} ]'); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + Toaster.error("Error", 'Cannot open URL on this system'); + } + } catch (exc, trace) { + ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${widget.value.url}', trace: trace); + } + } + + String _formatPermissions(String v) { + var splt = v.split(';'); + + if (splt.length == 0) return "None"; + + List result = []; + + if (splt.contains("A")) result.add(" - Admin"); + if (splt.contains("UR")) result.add(" - Read Account"); + if (splt.contains("CR")) result.add(" - Read Messages"); + if (splt.contains("CS")) result.add(" - Send Messages"); + + return result.join("\n"); + } +} diff --git a/flutter/lib/pages/channel_view/channel_view.dart b/flutter/lib/pages/channel_view/channel_view.dart index 6168b74..635bd68 100644 --- a/flutter/lib/pages/channel_view/channel_view.dart +++ b/flutter/lib/pages/channel_view/channel_view.dart @@ -73,20 +73,27 @@ class _ChannelViewPageState extends State { final userAcc = Provider.of(context, listen: false); if (widget.preloadedData != null && usePreload) { - channelPreview = widget.preloadedData!.$1.toPreview(); channel = widget.preloadedData!.$1; subscription = widget.preloadedData!.$2; + channelPreview = widget.preloadedData!.$1.toPreview(widget.preloadedData!.$2); } else { try { var p = await APIClient.getChannelPreview(userAcc, widget.channelID); - channelPreview = p; + setState(() { + channelPreview = p; + subscription = p.subscription; + }); + if (p.ownerUserID == userAcc.userID) { var r = await APIClient.getChannel(userAcc, widget.channelID); - channel = r.channel; - subscription = r.subscription; + setState(() { + channel = r.channel; + subscription = r.subscription; + }); } else { - channel = null; - subscription = null; //TODO get own subscription on this channel, even though its foreign channel + setState(() { + channel = null; + }); } } catch (exc, trace) { ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace); @@ -97,32 +104,34 @@ class _ChannelViewPageState extends State { } } - this.loadingState = ChannelViewPageInitState.okay; + setState(() { + this.loadingState = ChannelViewPageInitState.okay; - assert(channelPreview != null); + assert(channelPreview != null); - if (this.channelPreview!.ownerUserID == userAcc.userID) { - if (this.channel != null && this.channel!.subscribeKey != null) { - _futureSubscribeKey = ImmediateFuture.ofValue(this.channel!.subscribeKey); + if (this.channelPreview!.ownerUserID == userAcc.userID) { + if (this.channel != null && this.channel!.subscribeKey != null) { + _futureSubscribeKey = ImmediateFuture.ofValue(this.channel!.subscribeKey); + } else { + _futureSubscribeKey = ImmediateFuture.ofFuture(_getSubscribeKey(userAcc)); + } + _futureSubscriptions = ImmediateFuture>.ofFuture(_listSubscriptions(userAcc)); } else { - _futureSubscribeKey = ImmediateFuture.ofFuture(_getSubscribeKey(userAcc)); + _futureSubscribeKey = ImmediateFuture.ofValue(null); + _futureSubscriptions = ImmediateFuture>.ofValue([]); } - _futureSubscriptions = ImmediateFuture>.ofFuture(_listSubscriptions(userAcc)); - } else { - _futureSubscribeKey = ImmediateFuture.ofValue(null); - _futureSubscriptions = ImmediateFuture>.ofValue([]); - } - if (this.channelPreview!.ownerUserID == userAcc.userID) { - var cacheUser = userAcc.getUserOrNull(); - if (cacheUser != null) { - _futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview()); + if (this.channelPreview!.ownerUserID == userAcc.userID) { + var cacheUser = userAcc.getUserOrNull(); + if (cacheUser != null) { + _futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview()); + } else { + _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc)); + } } else { - _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc)); + _futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID)); } - } else { - _futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID)); - } + }); } @override diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index d38e632..0fdf559 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -61,7 +61,7 @@ class _MessageViewPageState extends State { final msg = await APIClient.getMessage(acc, widget.messageID); final fut_chn = APIClient.getChannelPreview(acc, msg.channelID); - final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID); + final fut_key = APIClient.getKeyTokenPreviewByID(acc, msg.usedKeyID); final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID); final chn = await fut_chn; diff --git a/flutter/lib/pages/send/send.dart b/flutter/lib/pages/send/send.dart index b3f47a5..f375fef 100644 --- a/flutter/lib/pages/send/send.dart +++ b/flutter/lib/pages/send/send.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; @@ -135,7 +136,7 @@ class _SendRootPageState extends State { if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { - // TODO ("Cannot open URL"); + Toaster.error("Error", 'Cannot open URL on this system'); } } catch (exc, trace) { ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace); diff --git a/flutter/lib/utils/navi.dart b/flutter/lib/utils/navi.dart index c3e6510..8339215 100644 --- a/flutter/lib/utils/navi.dart +++ b/flutter/lib/utils/navi.dart @@ -13,6 +13,15 @@ class Navi { Navigator.push(context, MaterialPageRoute(builder: (context) => builder())); } + static void pushOnRoot(BuildContext context, T Function() builder) { + Provider.of(context, listen: false).setLoadingIndeterminate(false); + Provider.of(context, listen: false).setShowSearchField(false); + + Navigator.popUntil(context, (route) => route.isFirst); + + Navigator.push(context, MaterialPageRoute(builder: (context) => builder())); + } + static void popToRoot(BuildContext context) { Provider.of(context, listen: false).setLoadingIndeterminate(false); Provider.of(context, listen: false).setShowSearchField(false); diff --git a/flutter/lib/utils/ui.dart b/flutter/lib/utils/ui.dart index d00cb4f..379c8ed 100644 --- a/flutter/lib/utils/ui.dart +++ b/flutter/lib/utils/ui.dart @@ -4,7 +4,7 @@ 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}) { + static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, Color? textColor = null, bool tonal = false, IconData? icon = null}) { final double fontSize = big ? 24 : 14; final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null; @@ -12,6 +12,7 @@ class UI { textStyle: TextStyle(fontSize: fontSize), padding: padding, backgroundColor: color, + foregroundColor: textColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)), );