diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index ec40684..25344d7 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -123,7 +123,7 @@ class APIClient { RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); Toaster.error("Error", apierr.message); - throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message); + throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message, true); } try { diff --git a/flutter/lib/api/api_exception.dart b/flutter/lib/api/api_exception.dart index a6a72b1..319e50a 100644 --- a/flutter/lib/api/api_exception.dart +++ b/flutter/lib/api/api_exception.dart @@ -3,8 +3,9 @@ class APIException implements Exception { final int error; final int errHighlight; final String message; + final bool toastShown; - APIException(this.httpStatus, this.error, this.errHighlight, this.message); + APIException(this.httpStatus, this.error, this.errHighlight, this.message, this.toastShown); @override String toString() { diff --git a/flutter/lib/components/badge_display/badge_display.dart b/flutter/lib/components/badge_display/badge_display.dart index c2ba8fb..efaa995 100644 --- a/flutter/lib/components/badge_display/badge_display.dart +++ b/flutter/lib/components/badge_display/badge_display.dart @@ -2,50 +2,104 @@ import 'package:flutter/material.dart'; enum BadgeMode { error, warn, info } -class BadgeDisplay extends StatelessWidget { +class BadgeDisplay extends StatefulWidget { final String text; final BadgeMode mode; final IconData? icon; + final TextAlign textAlign; + final bool closable; + final EdgeInsets extraPadding; + final bool hidden; const BadgeDisplay({ Key? key, required this.text, required this.mode, required this.icon, + this.textAlign = TextAlign.center, + this.closable = false, + this.extraPadding = EdgeInsets.zero, + this.hidden = false, }) : super(key: key); + @override + State createState() => _BadgeDisplayState(); +} + +class _BadgeDisplayState extends State { + bool _isVisible = true; + @override Widget build(BuildContext context) { + if (!_isVisible || widget.hidden) return const SizedBox.shrink(); + var col = Colors.grey; var colFG = Colors.black; - if (mode == BadgeMode.error) col = Colors.red; - if (mode == BadgeMode.warn) col = Colors.orange; - if (mode == BadgeMode.info) col = Colors.blue; + if (widget.mode == BadgeMode.error) col = Colors.red; + if (widget.mode == BadgeMode.warn) col = Colors.orange; + if (widget.mode == BadgeMode.info) col = Colors.blue; - if (mode == BadgeMode.error) colFG = Colors.red[900]!; - if (mode == BadgeMode.warn) colFG = Colors.black; - if (mode == BadgeMode.info) colFG = Colors.black; + if (widget.mode == BadgeMode.error) colFG = Colors.red[900]!; + if (widget.mode == BadgeMode.warn) colFG = Colors.black; + if (widget.mode == BadgeMode.info) colFG = Colors.black; return Container( - padding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - decoration: BoxDecoration( - color: col[100], - border: Border.all(color: col[300]!), - borderRadius: BorderRadius.circular(4.0), - ), - child: Row( + margin: widget.extraPadding, + child: Stack( + clipBehavior: Clip.none, children: [ - if (icon != null) Icon(icon!, color: colFG, size: 16.0), - Expanded( - child: Text( - text, - textAlign: TextAlign.center, - style: TextStyle(color: colFG, fontSize: 14.0), + // The badge itself + Container( + padding: EdgeInsets.fromLTRB(8, 2, widget.closable ? 16 : 8, 2), + decoration: BoxDecoration( + color: col[100], + border: Border.all(color: col[300]!), + borderRadius: BorderRadius.circular(4.0), + ), + child: Row( + children: [ + if (widget.icon != null) Icon(widget.icon!, color: colFG, size: 16.0), + Expanded( + child: Text( + widget.text, + textAlign: widget.textAlign, + style: TextStyle(color: colFG, fontSize: 14.0), + ), + ), + ], ), ), + + if (widget.closable) _buildCloseButton(context, colFG), ], ), ); } + + Positioned _buildCloseButton(BuildContext context, Color colFG) { + return Positioned( + top: 1, + right: 1, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() { + _isVisible = false; + }); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(2), + child: Icon( + Icons.close, + size: 14, + color: colFG, + ), + ), + ), + ), + ); + } } diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart index 6f8a50e..7d287ac 100644 --- a/flutter/lib/pages/account/account.dart +++ b/flutter/lib/pages/account/account.dart @@ -7,6 +7,7 @@ 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/api/api_exception.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/pages/account/login.dart'; @@ -166,6 +167,9 @@ class _AccountRootPageState extends State { _futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length); _futureUser = ImmediateFuture.ofValue(user); }); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to refresh account data'); } catch (exc, trace) { ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace); Toaster.error("Error", 'Failed to refresh account data'); @@ -417,7 +421,13 @@ class _AccountRootPageState extends State { ], ), onTap: () { - Navi.push(context, () => FilteredMessageViewPage(title: "All Messages", filter: MessageFilter(senderUserID: [user.userID]))); + Navi.push( + context, + () => FilteredMessageViewPage( + title: "All Messages", + alertText: "All messages sent from your account", + filter: MessageFilter(senderUserID: [user.userID]), + )); }, ), ]; @@ -528,9 +538,12 @@ class _AccountRootPageState extends State { context: context, builder: (context) => ShowTokenModal(account: acc, isAfterRegister: true), ); - } catch (exc, trace) { + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to create user account'); ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace); + } catch (exc, trace) { Toaster.error("Error", 'Failed to create user account'); + ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace); } finally { setState(() => loading = false); } @@ -574,6 +587,9 @@ class _AccountRootPageState extends State { //TODO clear messages/channels/etc in open views acc.clear(); await acc.save(); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to delete user'); + ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to delete user'); ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace); @@ -601,6 +617,9 @@ class _AccountRootPageState extends State { Toaster.success("Success", 'Username changed'); _backgroundRefresh(); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to update username'); + ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to update username'); ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace); diff --git a/flutter/lib/pages/account/login.dart b/flutter/lib/pages/account/login.dart index 1f239f3..7b20334 100644 --- a/flutter/lib/pages/account/login.dart +++ b/flutter/lib/pages/account/login.dart @@ -3,6 +3,7 @@ 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/api/api_exception.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; @@ -162,9 +163,12 @@ class _AccountLoginPageState extends State { Toaster.success("Login", "Successfully logged in"); Navi.popToRoot(context); - } catch (exc, trace) { + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to verify token'); ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace); + } catch (exc, trace) { Toaster.error("Error", 'Failed to verify token'); + ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace); } finally { setState(() => loading = false); } diff --git a/flutter/lib/pages/account/show_token_modal.dart b/flutter/lib/pages/account/show_token_modal.dart index e5afd8c..6c81317 100644 --- a/flutter/lib/pages/account/show_token_modal.dart +++ b/flutter/lib/pages/account/show_token_modal.dart @@ -32,6 +32,7 @@ class ShowTokenModal extends StatelessWidget { text: alertText, icon: null, mode: BadgeMode.info, + textAlign: TextAlign.left, ), const SizedBox(height: 16), if (this.account.userID != null) diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart index ba5c500..c918fc2 100644 --- a/flutter/lib/pages/channel_list/channel_list.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -144,28 +144,7 @@ class _ChannelRootPageState extends State with RouteAware { @override Widget build(BuildContext context) { return Scaffold( - body: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => ChannelListItem( - channel: item.channel, - subscription: item.subscription, - mode: ChannelListItemMode.Messages, - onChannelListReloadTrigger: _enqueueReload, - onSubscriptionChanged: (channelID, subscription) { - setState(() { - final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID); - if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription); - }); - }, - ), - ), - ), - ), + body: _buildList(context), floatingActionButton: FloatingActionButton( heroTag: 'fab_channel_list_qr', onPressed: () { @@ -176,6 +155,31 @@ class _ChannelRootPageState extends State with RouteAware { ); } + Widget _buildList(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ChannelListItem( + channel: item.channel, + subscription: item.subscription, + mode: ChannelListItemMode.Messages, + onChannelListReloadTrigger: _enqueueReload, + onSubscriptionChanged: (channelID, subscription) { + setState(() { + final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID); + if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription); + }); + }, + ), + ), + ), + ); + } + void _enqueueReload() { _reloadEnqueued = true; } diff --git a/flutter/lib/pages/channel_list/channel_list_extended.dart b/flutter/lib/pages/channel_list/channel_list_extended.dart index cc56953..57a045b 100644 --- a/flutter/lib/pages/channel_list/channel_list_extended.dart +++ b/flutter/lib/pages/channel_list/channel_list_extended.dart @@ -3,10 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart'; @@ -121,27 +123,21 @@ class _ChannelListExtendedPageState extends State with showShare: false, child: Padding( padding: EdgeInsets.fromLTRB(8, 4, 8, 4), - child: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => ChannelListItem( - channel: item.channel, - subscription: item.subscription, - mode: ChannelListItemMode.Extended, - onChannelListReloadTrigger: _enqueueReload, - onSubscriptionChanged: (channelID, subscription) { - setState(() { - final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID); - if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription); - }); - }, - ), + child: Column( + children: [ + BadgeDisplay( + text: "All channels accessible from this account\n(Your own channels and subscribed channels)", + icon: null, + mode: BadgeMode.info, + textAlign: TextAlign.left, + closable: true, + extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), + hidden: !AppSettings().showInfoAlerts, ), - ), + Expanded( + child: _buildList(context), + ) + ], ), ), floatingActionButton: FloatingActionButton( @@ -154,6 +150,31 @@ class _ChannelListExtendedPageState extends State with ); } + Widget _buildList(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ChannelListItem( + channel: item.channel, + subscription: item.subscription, + mode: ChannelListItemMode.Extended, + onChannelListReloadTrigger: _enqueueReload, + onSubscriptionChanged: (channelID, subscription) { + setState(() { + final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID); + if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription); + }); + }, + ), + ), + ), + ); + } + void _enqueueReload() { _reloadEnqueued = true; } diff --git a/flutter/lib/pages/channel_list/channel_list_item.dart b/flutter/lib/pages/channel_list/channel_list_item.dart index 76c16c8..080da6f 100644 --- a/flutter/lib/pages/channel_list/channel_list_item.dart +++ b/flutter/lib/pages/channel_list/channel_list_item.dart @@ -2,6 +2,7 @@ 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/api/api_exception.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/subscription.dart'; @@ -220,6 +221,9 @@ class _ChannelListItemState extends State { } else { Toaster.success("Success", 'Requested widget.subscription to channel'); } + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel'); + ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to subscribe to channel'); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); @@ -238,6 +242,9 @@ class _ChannelListItemState extends State { widget.onSubscriptionChanged.call(sub.channelID, null); Toaster.success("Success", 'Unsubscribed from channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel'); + ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to unsubscribe from channel'); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); @@ -256,6 +263,9 @@ class _ChannelListItemState extends State { widget.onSubscriptionChanged.call(sub.channelID, newSub); Toaster.success("Success", 'Unsubscribed from channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel'); + ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to unsubscribe from channel'); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); @@ -274,6 +284,9 @@ class _ChannelListItemState extends State { widget.onSubscriptionChanged.call(sub.channelID, newSub); Toaster.success("Success", 'Subscribed to channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel'); + ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to subscribe to channel'); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); diff --git a/flutter/lib/pages/channel_view/channel_view.dart b/flutter/lib/pages/channel_view/channel_view.dart index 8dc2125..145a30f 100644 --- a/flutter/lib/pages/channel_view/channel_view.dart +++ b/flutter/lib/pages/channel_view/channel_view.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; @@ -305,7 +306,7 @@ class _ChannelViewPageState extends State { icon: FontAwesomeIcons.solidEnvelope, title: 'Messages', values: [channel.messagesSent.toString()], - mainAction: (subscription != null && subscription!.confirmed) ? () => Navi.push(context, () => FilteredMessageViewPage(title: channel.displayName, filter: MessageFilter(channelIDs: [channel.channelID]))) : null, + mainAction: (subscription != null && subscription!.confirmed) ? () => Navi.push(context, () => FilteredMessageViewPage(title: channel.displayName, alertText: null, filter: MessageFilter(channelIDs: [channel.channelID]))) : null, ), ], ), @@ -537,6 +538,9 @@ class _ChannelViewPageState extends State { }); widget.needsReload?.call(); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to save DisplayName'); } catch (exc, trace) { ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace); Toaster.error("Error", 'Failed to save DisplayName'); @@ -569,9 +573,12 @@ class _ChannelViewPageState extends State { }); widget.needsReload?.call(); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to save description'); } catch (exc, trace) { ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: trace); - Toaster.error("Error", 'Failed to save DescriptionName'); + Toaster.error("Error", 'Failed to save description'); } } @@ -589,6 +596,9 @@ class _ChannelViewPageState extends State { } else { Toaster.success("Success", 'Requested subscription to channel'); } + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel'); + ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to subscribe to channel'); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); @@ -612,6 +622,9 @@ class _ChannelViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Unsubscribed from channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel'); + ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to unsubscribe from channel'); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); @@ -630,6 +643,9 @@ class _ChannelViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Unsubscribed from channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel'); + ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to unsubscribe from channel'); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); @@ -648,6 +664,9 @@ class _ChannelViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Subscribed to channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel'); + ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to subscribe to channel'); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); @@ -664,6 +683,9 @@ class _ChannelViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Subscription succesfully revoked'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to revoke subscription'); + ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to revoke subscription'); ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace); @@ -680,6 +702,9 @@ class _ChannelViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Subscription succesfully confirmed'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to confirm subscription'); + ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to confirm subscription'); ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace); @@ -696,6 +721,9 @@ class _ChannelViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Subscription request succesfully denied'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to deny subscription'); + ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to deny subscription'); ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace); diff --git a/flutter/lib/pages/client_list/client_list.dart b/flutter/lib/pages/client_list/client_list.dart index fc84dc5..3ab2a8b 100644 --- a/flutter/lib/pages/client_list/client_list.dart +++ b/flutter/lib/pages/client_list/client_list.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/client.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/client_list/client_list_item.dart'; @@ -69,16 +71,35 @@ class _ClientListPageState extends State { showShare: false, child: Padding( padding: EdgeInsets.fromLTRB(8, 4, 8, 4), - child: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => ClientListItem(item: item), + child: Column( + children: [ + BadgeDisplay( + text: "All clients connected with this account", + icon: null, + mode: BadgeMode.info, + textAlign: TextAlign.left, + closable: true, + extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), + hidden: !AppSettings().showInfoAlerts, ), - ), + Expanded( + child: _buildList(context), + ) + ], + ), + ), + ); + } + + Widget _buildList(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => ClientListItem(item: item), ), ), ); diff --git a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart index 96252a2..86f2d0f 100644 --- a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart +++ b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; @@ -17,11 +18,13 @@ class FilteredMessageViewPage extends StatefulWidget { const FilteredMessageViewPage({ required this.title, required this.filter, + required this.alertText, super.key, }); final String title; final MessageFilter filter; + final String? alertText; @override State createState() => _FilteredMessageViewPageState(); @@ -87,31 +90,52 @@ class _FilteredMessageViewPageState extends State { @override Widget build(BuildContext context) { + Widget child = _buildMessageList(context); + + if (widget.alertText != null) { + child = Column( + children: [ + BadgeDisplay( + text: widget.alertText!, + icon: null, + mode: BadgeMode.info, + textAlign: TextAlign.left, + closable: true, + extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), + hidden: !AppSettings().showInfoAlerts, + ), + Expanded( + child: child, + ) + ], + ); + } + return SCNScaffold( title: this.widget.title, showSearch: false, showShare: false, - child: _buildMessageList(context), + child: Padding( + padding: EdgeInsets.fromLTRB(8, 4, 8, 4), + child: child, + ), ); } Widget _buildMessageList(BuildContext context) { - return Padding( - padding: EdgeInsets.fromLTRB(8, 4, 8, 4), - child: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => MessageListItem( - message: item, - allChannels: _channels ?? {}, - onPressed: () { - Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,))); - }, - ), + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => MessageListItem( + message: item, + allChannels: _channels ?? {}, + onPressed: () { + Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,))); + }, ), ), ), diff --git a/flutter/lib/pages/keytoken_list/keytoken_list.dart b/flutter/lib/pages/keytoken_list/keytoken_list.dart index 0b058a1..04f7eac 100644 --- a/flutter/lib/pages/keytoken_list/keytoken_list.dart +++ b/flutter/lib/pages/keytoken_list/keytoken_list.dart @@ -3,10 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_modal.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart'; @@ -72,16 +74,21 @@ class _KeyTokenListPageState extends State { showShare: false, child: Padding( padding: EdgeInsets.fromLTRB(8, 4, 8, 4), - child: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh), + child: Column( + children: [ + BadgeDisplay( + text: "These are your keys.\nKeys can be used to send messages and access your account.\n\nKeys can have different sets of permissions, the Admin-Key has full-access to your account", + icon: null, + mode: BadgeMode.info, + textAlign: TextAlign.left, + closable: true, + extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), + hidden: !AppSettings().showInfoAlerts, ), - ), + Expanded( + child: _buildList(context), + ) + ], ), ), floatingActionButton: FloatingActionButton( @@ -97,6 +104,20 @@ class _KeyTokenListPageState extends State { ); } + Widget _buildList(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh), + ), + ), + ); + } + void _created(KeyToken token, String tokValue) { setState(() { _pagingController.itemList?.insert(0, token); diff --git a/flutter/lib/pages/keytoken_list/keytoken_list_item.dart b/flutter/lib/pages/keytoken_list/keytoken_list_item.dart index bf84f44..edd7d23 100644 --- a/flutter/lib/pages/keytoken_list/keytoken_list_item.dart +++ b/flutter/lib/pages/keytoken_list/keytoken_list_item.dart @@ -78,7 +78,7 @@ class KeyTokenListItem extends StatelessWidget { SizedBox(width: 4), GestureDetector( onTap: () { - Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(usedKeys: [item.keytokenID]))); + Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent with the key \'${item.name}\'', filter: MessageFilter(usedKeys: [item.keytokenID]))); }, child: Padding( padding: const EdgeInsets.all(8), diff --git a/flutter/lib/pages/keytoken_view/keytoken_view.dart b/flutter/lib/pages/keytoken_view/keytoken_view.dart index c149b51..182435f 100644 --- a/flutter/lib/pages/keytoken_view/keytoken_view.dart +++ b/flutter/lib/pages/keytoken_view/keytoken_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; @@ -230,7 +231,7 @@ class _KeyTokenViewPageState extends State { title: 'Messages', values: [keytoken.messagesSent.toString()], mainAction: () { - Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, filter: MessageFilter(usedKeys: [keytoken.keytokenID]))); + Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, alertText: 'All message sent with the key \'${keytoken.name}\'', filter: MessageFilter(usedKeys: [keytoken.keytokenID]))); }, ), ..._buildPermissionCard(context, true, keytoken.toPreview()), @@ -543,6 +544,9 @@ class _KeyTokenViewPageState extends State { Toaster.info('Logout', 'Successfully deleted the key'); Navi.pop(context); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to delete key'); } catch (exc, trace) { Toaster.error("Error", 'Failed to delete key'); ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace); @@ -563,6 +567,9 @@ class _KeyTokenViewPageState extends State { Toaster.info("Success", "Key updated"); widget.needsReload?.call(); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to update key'); } catch (exc, trace) { ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); Toaster.error("Error", 'Failed to update key'); @@ -583,6 +590,9 @@ class _KeyTokenViewPageState extends State { Toaster.info("Success", "Key updated"); widget.needsReload?.call(); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to update key'); } catch (exc, trace) { ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); Toaster.error("Error", 'Failed to update key'); @@ -603,6 +613,9 @@ class _KeyTokenViewPageState extends State { Toaster.info("Success", "Key updated"); widget.needsReload?.call(); + } on APIException catch (exc, trace) { + ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); + if (!exc.toastShown) Toaster.error("Error", 'Failed to update key'); } catch (exc, trace) { ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); Toaster.error("Error", 'Failed to update key'); diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index 6301107..4798a18 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -157,7 +157,7 @@ class _MessageViewPageState extends State { title: 'Sender', values: [message.senderName!], mainAction: () => { - Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, filter: MessageFilter(senderNames: [message.senderName!]))) + Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, alertText: 'All message sent from \'${message.senderName!}\'', filter: MessageFilter(senderNames: [message.senderName!]))) }, ), UI.metaCard( @@ -169,7 +169,7 @@ class _MessageViewPageState extends State { if (message.senderUserID == userAccUserID) { Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null)); } else { - Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID]))); + Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, alertText: 'All message sent with the specified key', filter: MessageFilter(usedKeys: [message.usedKeyID]))); } }, ), @@ -201,14 +201,14 @@ class _MessageViewPageState extends State { icon: FontAwesomeIcons.solidUser, title: 'User', values: [user?.userID ?? message.senderUserID, user?.username ?? ''], - mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, filter: MessageFilter(senderUserID: [message.senderUserID]))), + mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, alertText: 'All message sent by the specified account', filter: MessageFilter(senderUserID: [message.senderUserID]))), ), UI.metaCard( context: context, icon: FontAwesomeIcons.solidBolt, title: 'Priority', values: [_prettyPrintPriority(message.priority)], - mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: "Priority ${message.priority}", filter: MessageFilter(priority: [message.priority]))), + mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: "Priority ${message.priority}", alertText: 'All message sent with priority ${message.priority}', filter: MessageFilter(priority: [message.priority]))), ), if (message.senderUserID == userAccUserID) UI.button( diff --git a/flutter/lib/pages/send/send.dart b/flutter/lib/pages/send/send.dart index be6e873..57ed1e5 100644 --- a/flutter/lib/pages/send/send.dart +++ b/flutter/lib/pages/send/send.dart @@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; @@ -289,6 +290,9 @@ class _SendRootPageState extends State { _msgTitle.clear(); _msgContent.clear(); }); + } on APIException catch (e, stackTrace) { + if (!e.toastShown) Toaster.error("Error", 'Failed to send message: ${e.toString()}'); + ApplicationLog.error('Failed to send message', trace: stackTrace); } catch (e, stackTrace) { Toaster.error("Error", 'Failed to send message: ${e.toString()}'); ApplicationLog.error('Failed to send message', trace: stackTrace); @@ -308,6 +312,9 @@ class _SendRootPageState extends State { _msgTitle.clear(); _msgContent.clear(); }); + } on APIException catch (e, stackTrace) { + if (!e.toastShown) Toaster.error("Error", 'Failed to send message: ${e.toString()}'); + ApplicationLog.error('Failed to send message', trace: stackTrace); } catch (e, stackTrace) { Toaster.error("Error", 'Failed to send message: ${e.toString()}'); ApplicationLog.error('Failed to send message', trace: stackTrace); diff --git a/flutter/lib/pages/sender_list/sender_list.dart b/flutter/lib/pages/sender_list/sender_list.dart index dd4b189..1abbf5f 100644 --- a/flutter/lib/pages/sender_list/sender_list.dart +++ b/flutter/lib/pages/sender_list/sender_list.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/sender_name_statistics.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/sender_list/sender_list_item.dart'; @@ -69,16 +71,35 @@ class _SenderListPageState extends State { showShare: false, child: Padding( padding: EdgeInsets.fromLTRB(8, 4, 8, 4), - child: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => SenderListItem(item: item), + child: Column( + children: [ + BadgeDisplay( + text: "All sender used to send messages to this account", + icon: null, + mode: BadgeMode.info, + textAlign: TextAlign.left, + closable: true, + extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), + hidden: !AppSettings().showInfoAlerts, ), - ), + Expanded( + child: _buildList(context), + ) + ], + ), + ), + ); + } + + Widget _buildList(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => SenderListItem(item: item), ), ), ); diff --git a/flutter/lib/pages/sender_list/sender_list_item.dart b/flutter/lib/pages/sender_list/sender_list_item.dart index 151a0bd..24b7f61 100644 --- a/flutter/lib/pages/sender_list/sender_list_item.dart +++ b/flutter/lib/pages/sender_list/sender_list_item.dart @@ -30,7 +30,7 @@ class SenderListItem extends StatelessWidget { color: Theme.of(context).cardTheme.color, child: InkWell( onTap: () { - Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name]))); + Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent from \'${item.name!}\'', filter: MessageFilter(senderNames: [item.name]))); }, child: Padding( padding: const EdgeInsets.all(8), @@ -71,7 +71,7 @@ class SenderListItem extends StatelessWidget { SizedBox(width: 4), GestureDetector( onTap: () { - Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name]))); + Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent from \'${item.name!}\'', filter: MessageFilter(senderNames: [item.name]))); }, child: Padding( padding: const EdgeInsets.all(8), diff --git a/flutter/lib/pages/settings/settings_view.dart b/flutter/lib/pages/settings/settings_view.dart index 726feb5..6c358c6 100644 --- a/flutter/lib/pages/settings/settings_view.dart +++ b/flutter/lib/pages/settings/settings_view.dart @@ -146,6 +146,12 @@ class _SettingsRootPageState extends State { title: Text('Refresh messages on app resume'), onToggle: (value) => AppSettings().update((p) => p.alwaysBackgroundRefreshMessageListOnLifecycleResume = !p.alwaysBackgroundRefreshMessageListOnLifecycleResume), ), + SettingsTile.switchTile( + initialValue: cfg.showInfoAlerts, + leading: Icon(FontAwesomeIcons.solidCircleInfo), + title: Text('Show various helpful info boxes'), + onToggle: (value) => AppSettings().update((p) => p.showInfoAlerts = !p.showInfoAlerts), + ), ], ), SettingsSection( diff --git a/flutter/lib/pages/subscription_list/subscription_list.dart b/flutter/lib/pages/subscription_list/subscription_list.dart index 3ba6ba6..d37d622 100644 --- a/flutter/lib/pages/subscription_list/subscription_list.dart +++ b/flutter/lib/pages/subscription_list/subscription_list.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart'; @@ -93,16 +95,35 @@ class _SubscriptionListPageState extends State { showShare: false, child: Padding( padding: EdgeInsets.fromLTRB(8, 4, 8, 4), - child: RefreshIndicator( - onRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh), + child: Column( + children: [ + BadgeDisplay( + text: "These are subscriptions to individual Channels\n\nThey contain to your own channels, subscriptions to foreign channels and subscriptions of other users to your channels (active and requested).", + icon: null, + mode: BadgeMode.info, + textAlign: TextAlign.left, + closable: true, + extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16), + hidden: !AppSettings().showInfoAlerts, ), - ), + Expanded( + child: _buildList(context), + ) + ], + ), + ), + ); + } + + Widget _buildList(BuildContext context) { + return RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh), ), ), ); diff --git a/flutter/lib/pages/subscription_view/subscription_view.dart b/flutter/lib/pages/subscription_view/subscription_view.dart index 2cd98b8..e673f53 100644 --- a/flutter/lib/pages/subscription_view/subscription_view.dart +++ b/flutter/lib/pages/subscription_view/subscription_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; @@ -407,6 +408,9 @@ class _SubscriptionViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Subscription succesfully confirmed'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to confirm subscription'); + ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to confirm subscription'); ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace); @@ -429,6 +433,9 @@ class _SubscriptionViewPageState extends State { Toaster.success("Success", 'Unsubscribed from channel'); Navi.pop(context); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel'); + ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to unsubscribe from channel'); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); @@ -447,6 +454,9 @@ class _SubscriptionViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Unsubscribed from channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel'); + ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to unsubscribe from channel'); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); @@ -465,6 +475,9 @@ class _SubscriptionViewPageState extends State { await _initStateAsync(false); Toaster.success("Success", 'Subscribed to channel'); + } on APIException catch (exc, trace) { + if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel'); + ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); } catch (exc, trace) { Toaster.error("Error", 'Failed to subscribe to channel'); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); diff --git a/flutter/lib/state/app_settings.dart b/flutter/lib/state/app_settings.dart index d9fe283..a197f23 100644 --- a/flutter/lib/state/app_settings.dart +++ b/flutter/lib/state/app_settings.dart @@ -56,6 +56,7 @@ class AppSettings extends ChangeNotifier { bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true; AppSettingsDateFormat dateFormat = AppSettingsDateFormat.ISO; int messagePreviewLength = 3; + bool showInfoAlerts = true; AppNotificationSettings notification0 = AppNotificationSettings(); AppNotificationSettings notification1 = AppNotificationSettings(); @@ -80,6 +81,7 @@ class AppSettings extends ChangeNotifier { alwaysBackgroundRefreshMessageListOnLifecycleResume = true; dateFormat = AppSettingsDateFormat.ISO; messagePreviewLength = 3; + showInfoAlerts = true; notification0 = AppNotificationSettings(); notification1 = AppNotificationSettings(); @@ -97,6 +99,7 @@ class AppSettings extends ChangeNotifier { alwaysBackgroundRefreshMessageListOnLifecycleResume = Globals().sharedPrefs.getBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume') ?? alwaysBackgroundRefreshMessageListOnLifecycleResume; dateFormat = AppSettingsDateFormat.parse(Globals().sharedPrefs.getString('settings.dateFormat')) ?? dateFormat; messagePreviewLength = Globals().sharedPrefs.getInt('settings.messagePreviewLength') ?? messagePreviewLength; + showInfoAlerts = Globals().sharedPrefs.getBool('settings.showInfoAlerts') ?? showInfoAlerts; notification0 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification0'); notification1 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification1'); @@ -112,6 +115,7 @@ class AppSettings extends ChangeNotifier { await Globals().sharedPrefs.setBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume', alwaysBackgroundRefreshMessageListOnLifecycleResume); await Globals().sharedPrefs.setString('settings.dateFormat', dateFormat.key); await Globals().sharedPrefs.setInt('settings.messagePreviewLength', messagePreviewLength); + await Globals().sharedPrefs.setBool('settings.showInfoAlerts', showInfoAlerts); await notification0.save(Globals().sharedPrefs, 'settings.notification0'); await notification1.save(Globals().sharedPrefs, 'settings.notification1');