Add various informative alert-boxes

This commit is contained in:
2025-11-09 21:31:03 +01:00
parent c108859899
commit fd5e714074
23 changed files with 426 additions and 130 deletions

View File

@@ -123,7 +123,7 @@ class APIClient {
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
Toaster.error("Error", apierr.message); 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 { try {

View File

@@ -3,8 +3,9 @@ class APIException implements Exception {
final int error; final int error;
final int errHighlight; final int errHighlight;
final String message; 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 @override
String toString() { String toString() {

View File

@@ -2,50 +2,104 @@ import 'package:flutter/material.dart';
enum BadgeMode { error, warn, info } enum BadgeMode { error, warn, info }
class BadgeDisplay extends StatelessWidget { class BadgeDisplay extends StatefulWidget {
final String text; final String text;
final BadgeMode mode; final BadgeMode mode;
final IconData? icon; final IconData? icon;
final TextAlign textAlign;
final bool closable;
final EdgeInsets extraPadding;
final bool hidden;
const BadgeDisplay({ const BadgeDisplay({
Key? key, Key? key,
required this.text, required this.text,
required this.mode, required this.mode,
required this.icon, required this.icon,
this.textAlign = TextAlign.center,
this.closable = false,
this.extraPadding = EdgeInsets.zero,
this.hidden = false,
}) : super(key: key); }) : super(key: key);
@override
State<BadgeDisplay> createState() => _BadgeDisplayState();
}
class _BadgeDisplayState extends State<BadgeDisplay> {
bool _isVisible = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_isVisible || widget.hidden) return const SizedBox.shrink();
var col = Colors.grey; var col = Colors.grey;
var colFG = Colors.black; var colFG = Colors.black;
if (mode == BadgeMode.error) col = Colors.red; if (widget.mode == BadgeMode.error) col = Colors.red;
if (mode == BadgeMode.warn) col = Colors.orange; if (widget.mode == BadgeMode.warn) col = Colors.orange;
if (mode == BadgeMode.info) col = Colors.blue; if (widget.mode == BadgeMode.info) col = Colors.blue;
if (mode == BadgeMode.error) colFG = Colors.red[900]!; if (widget.mode == BadgeMode.error) colFG = Colors.red[900]!;
if (mode == BadgeMode.warn) colFG = Colors.black; if (widget.mode == BadgeMode.warn) colFG = Colors.black;
if (mode == BadgeMode.info) colFG = Colors.black; if (widget.mode == BadgeMode.info) colFG = Colors.black;
return Container( return Container(
padding: const EdgeInsets.fromLTRB(8, 2, 8, 2), margin: widget.extraPadding,
decoration: BoxDecoration( child: Stack(
color: col[100], clipBehavior: Clip.none,
border: Border.all(color: col[300]!),
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
children: [ children: [
if (icon != null) Icon(icon!, color: colFG, size: 16.0), // The badge itself
Expanded( Container(
child: Text( padding: EdgeInsets.fromLTRB(8, 2, widget.closable ? 16 : 8, 2),
text, decoration: BoxDecoration(
textAlign: TextAlign.center, color: col[100],
style: TextStyle(color: colFG, fontSize: 14.0), 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,
),
),
),
),
);
}
} }

View File

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/error_display/error_display.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/account/login.dart'; import 'package:simplecloudnotifier/pages/account/login.dart';
@@ -166,6 +167,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
_futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length); _futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
_futureUser = ImmediateFuture.ofValue(user); _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) { } catch (exc, trace) {
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to refresh account data'); Toaster.error("Error", 'Failed to refresh account data');
@@ -417,7 +421,13 @@ class _AccountRootPageState extends State<AccountRootPage> {
], ],
), ),
onTap: () { 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<AccountRootPage> {
context: context, context: context,
builder: (context) => ShowTokenModal(account: acc, isAfterRegister: true), 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); ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to create user account'); Toaster.error("Error", 'Failed to create user account');
ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
} finally { } finally {
setState(() => loading = false); setState(() => loading = false);
} }
@@ -574,6 +587,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
//TODO clear messages/channels/etc in open views //TODO clear messages/channels/etc in open views
acc.clear(); acc.clear();
await acc.save(); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to delete user'); Toaster.error("Error", 'Failed to delete user');
ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace);
@@ -601,6 +617,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
Toaster.success("Success", 'Username changed'); Toaster.success("Success", 'Username changed');
_backgroundRefresh(); _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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to update username'); Toaster.error("Error", 'Failed to update username');
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
@@ -162,9 +163,12 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
Toaster.success("Login", "Successfully logged in"); Toaster.success("Login", "Successfully logged in");
Navi.popToRoot(context); 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); ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
} catch (exc, trace) {
Toaster.error("Error", 'Failed to verify token'); Toaster.error("Error", 'Failed to verify token');
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
} finally { } finally {
setState(() => loading = false); setState(() => loading = false);
} }

View File

@@ -32,6 +32,7 @@ class ShowTokenModal extends StatelessWidget {
text: alertText, text: alertText,
icon: null, icon: null,
mode: BadgeMode.info, mode: BadgeMode.info,
textAlign: TextAlign.left,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (this.account.userID != null) if (this.account.userID != null)

View File

@@ -144,28 +144,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: RefreshIndicator( body: _buildList(context),
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
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);
});
},
),
),
),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'fab_channel_list_qr', heroTag: 'fab_channel_list_qr',
onPressed: () { onPressed: () {
@@ -176,6 +155,31 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
); );
} }
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
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() { void _enqueueReload() {
_reloadEnqueued = true; _reloadEnqueued = true;
} }

View File

@@ -3,10 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart'; import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart';
import 'package:simplecloudnotifier/state/app_bar_state.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/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
@@ -121,27 +123,21 @@ class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with
showShare: false, showShare: false,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator( child: Column(
onRefresh: () => Future.sync( children: [
() => _pagingController.refresh(), BadgeDisplay(
), text: "All channels accessible from this account\n(Your own channels and subscribed channels)",
child: PagedListView<int, ChannelWithSubscription>( icon: null,
pagingController: _pagingController, mode: BadgeMode.info,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>( textAlign: TextAlign.left,
itemBuilder: (context, item, index) => ChannelListItem( closable: true,
channel: item.channel, extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
subscription: item.subscription, hidden: !AppSettings().showInfoAlerts,
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);
});
},
),
), ),
), Expanded(
child: _buildList(context),
)
],
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@@ -154,6 +150,31 @@ class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with
); );
} }
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
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() { void _enqueueReload() {
_reloadEnqueued = true; _reloadEnqueued = true;
} }

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/subscription.dart';
@@ -220,6 +221,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
} else { } else {
Toaster.success("Success", 'Requested widget.subscription to channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel'); Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -238,6 +242,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
widget.onSubscriptionChanged.call(sub.channelID, null); widget.onSubscriptionChanged.call(sub.channelID, null);
Toaster.success("Success", 'Unsubscribed from channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel'); Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -256,6 +263,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
widget.onSubscriptionChanged.call(sub.channelID, newSub); widget.onSubscriptionChanged.call(sub.channelID, newSub);
Toaster.success("Success", 'Unsubscribed from channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel'); Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -274,6 +284,9 @@ class _ChannelListItemState extends State<ChannelListItem> {
widget.onSubscriptionChanged.call(sub.channelID, newSub); widget.onSubscriptionChanged.call(sub.channelID, newSub);
Toaster.success("Success", 'Subscribed to channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel'); Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);

View File

@@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
@@ -305,7 +306,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidEnvelope, icon: FontAwesomeIcons.solidEnvelope,
title: 'Messages', title: 'Messages',
values: [channel.messagesSent.toString()], 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<ChannelViewPage> {
}); });
widget.needsReload?.call(); 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) { } catch (exc, trace) {
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to save DisplayName'); Toaster.error("Error", 'Failed to save DisplayName');
@@ -569,9 +573,12 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
}); });
widget.needsReload?.call(); 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) { } catch (exc, trace) {
ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: 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<ChannelViewPage> {
} else { } else {
Toaster.success("Success", 'Requested subscription to channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel'); Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -612,6 +622,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel'); Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -630,6 +643,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel'); Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -648,6 +664,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Subscribed to channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel'); Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
@@ -664,6 +683,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully revoked'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to revoke subscription'); Toaster.error("Error", 'Failed to revoke subscription');
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
@@ -680,6 +702,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription'); Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
@@ -696,6 +721,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Subscription request succesfully denied'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to deny subscription'); Toaster.error("Error", 'Failed to deny subscription');
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/client.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/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/client_list/client_list_item.dart'; import 'package:simplecloudnotifier/pages/client_list/client_list_item.dart';
@@ -69,16 +71,35 @@ class _ClientListPageState extends State<ClientListPage> {
showShare: false, showShare: false,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator( child: Column(
onRefresh: () => Future.sync( children: [
() => _pagingController.refresh(), BadgeDisplay(
), text: "All clients connected with this account",
child: PagedListView<int, Client>( icon: null,
pagingController: _pagingController, mode: BadgeMode.info,
builderDelegate: PagedChildBuilderDelegate<Client>( textAlign: TextAlign.left,
itemBuilder: (context, item, index) => ClientListItem(item: item), 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<int, Client>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Client>(
itemBuilder: (context, item, index) => ClientListItem(item: item),
), ),
), ),
); );

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:simplecloudnotifier/api/api_client.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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
@@ -17,11 +18,13 @@ class FilteredMessageViewPage extends StatefulWidget {
const FilteredMessageViewPage({ const FilteredMessageViewPage({
required this.title, required this.title,
required this.filter, required this.filter,
required this.alertText,
super.key, super.key,
}); });
final String title; final String title;
final MessageFilter filter; final MessageFilter filter;
final String? alertText;
@override @override
State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState(); State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState();
@@ -87,31 +90,52 @@ class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> {
@override @override
Widget build(BuildContext context) { 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( return SCNScaffold(
title: this.widget.title, title: this.widget.title,
showSearch: false, showSearch: false,
showShare: false, showShare: false,
child: _buildMessageList(context), child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: child,
),
); );
} }
Widget _buildMessageList(BuildContext context) { Widget _buildMessageList(BuildContext context) {
return Padding( return RefreshIndicator(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), onRefresh: () => Future.sync(
child: RefreshIndicator( () => _pagingController.refresh(),
onRefresh: () => Future.sync( ),
() => _pagingController.refresh(), child: PagedListView<String, SCNMessage>(
), pagingController: _pagingController,
child: PagedListView<String, SCNMessage>( builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
pagingController: _pagingController, itemBuilder: (context, item, index) => MessageListItem(
builderDelegate: PagedChildBuilderDelegate<SCNMessage>( message: item,
itemBuilder: (context, item, index) => MessageListItem( allChannels: _channels ?? {},
message: item, onPressed: () {
allChannels: _channels ?? {}, Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
onPressed: () { },
Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
},
),
), ),
), ),
), ),

View File

@@ -3,10 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_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/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
@@ -72,16 +74,21 @@ class _KeyTokenListPageState extends State<KeyTokenListPage> {
showShare: false, showShare: false,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator( child: Column(
onRefresh: () => Future.sync( children: [
() => _pagingController.refresh(), 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",
child: PagedListView<int, KeyToken>( icon: null,
pagingController: _pagingController, mode: BadgeMode.info,
builderDelegate: PagedChildBuilderDelegate<KeyToken>( textAlign: TextAlign.left,
itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh), closable: true,
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
hidden: !AppSettings().showInfoAlerts,
), ),
), Expanded(
child: _buildList(context),
)
],
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@@ -97,6 +104,20 @@ class _KeyTokenListPageState extends State<KeyTokenListPage> {
); );
} }
Widget _buildList(BuildContext context) {
return RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, KeyToken>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<KeyToken>(
itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh),
),
),
);
}
void _created(KeyToken token, String tokValue) { void _created(KeyToken token, String tokValue) {
setState(() { setState(() {
_pagingController.itemList?.insert(0, token); _pagingController.itemList?.insert(0, token);

View File

@@ -78,7 +78,7 @@ class KeyTokenListItem extends StatelessWidget {
SizedBox(width: 4), SizedBox(width: 4),
GestureDetector( GestureDetector(
onTap: () { 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( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
@@ -230,7 +231,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
title: 'Messages', title: 'Messages',
values: [keytoken.messagesSent.toString()], values: [keytoken.messagesSent.toString()],
mainAction: () { 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()), ..._buildPermissionCard(context, true, keytoken.toPreview()),
@@ -543,6 +544,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info('Logout', 'Successfully deleted the key'); Toaster.info('Logout', 'Successfully deleted the key');
Navi.pop(context); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to delete key'); Toaster.error("Error", 'Failed to delete key');
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
@@ -563,6 +567,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info("Success", "Key updated"); Toaster.info("Success", "Key updated");
widget.needsReload?.call(); 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) { } catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to update key'); Toaster.error("Error", 'Failed to update key');
@@ -583,6 +590,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info("Success", "Key updated"); Toaster.info("Success", "Key updated");
widget.needsReload?.call(); 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) { } catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to update key'); Toaster.error("Error", 'Failed to update key');
@@ -603,6 +613,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
Toaster.info("Success", "Key updated"); Toaster.info("Success", "Key updated");
widget.needsReload?.call(); 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) { } catch (exc, trace) {
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to update key'); Toaster.error("Error", 'Failed to update key');

View File

@@ -157,7 +157,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
title: 'Sender', title: 'Sender',
values: [message.senderName!], values: [message.senderName!],
mainAction: () => { 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( UI.metaCard(
@@ -169,7 +169,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
if (message.senderUserID == userAccUserID) { if (message.senderUserID == userAccUserID) {
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null)); Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
} else { } 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<MessageViewPage> {
icon: FontAwesomeIcons.solidUser, icon: FontAwesomeIcons.solidUser,
title: 'User', title: 'User',
values: [user?.userID ?? message.senderUserID, user?.username ?? ''], 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( UI.metaCard(
context: context, context: context,
icon: FontAwesomeIcons.solidBolt, icon: FontAwesomeIcons.solidBolt,
title: 'Priority', title: 'Priority',
values: [_prettyPrintPriority(message.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) if (message.senderUserID == userAccUserID)
UI.button( UI.button(

View File

@@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
@@ -289,6 +290,9 @@ class _SendRootPageState extends State<SendRootPage> {
_msgTitle.clear(); _msgTitle.clear();
_msgContent.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) { } catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}'); Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace); ApplicationLog.error('Failed to send message', trace: stackTrace);
@@ -308,6 +312,9 @@ class _SendRootPageState extends State<SendRootPage> {
_msgTitle.clear(); _msgTitle.clear();
_msgContent.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) { } catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}'); Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace); ApplicationLog.error('Failed to send message', trace: stackTrace);

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/sender_name_statistics.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/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/sender_list/sender_list_item.dart'; import 'package:simplecloudnotifier/pages/sender_list/sender_list_item.dart';
@@ -69,16 +71,35 @@ class _SenderListPageState extends State<SenderListPage> {
showShare: false, showShare: false,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator( child: Column(
onRefresh: () => Future.sync( children: [
() => _pagingController.refresh(), BadgeDisplay(
), text: "All sender used to send messages to this account",
child: PagedListView<int, SenderNameStatistics>( icon: null,
pagingController: _pagingController, mode: BadgeMode.info,
builderDelegate: PagedChildBuilderDelegate<SenderNameStatistics>( textAlign: TextAlign.left,
itemBuilder: (context, item, index) => SenderListItem(item: item), 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<int, SenderNameStatistics>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<SenderNameStatistics>(
itemBuilder: (context, item, index) => SenderListItem(item: item),
), ),
), ),
); );

View File

@@ -30,7 +30,7 @@ class SenderListItem extends StatelessWidget {
color: Theme.of(context).cardTheme.color, color: Theme.of(context).cardTheme.color,
child: InkWell( child: InkWell(
onTap: () { 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( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -71,7 +71,7 @@ class SenderListItem extends StatelessWidget {
SizedBox(width: 4), SizedBox(width: 4),
GestureDetector( GestureDetector(
onTap: () { 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( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),

View File

@@ -146,6 +146,12 @@ class _SettingsRootPageState extends State<SettingsRootPage> {
title: Text('Refresh messages on app resume'), title: Text('Refresh messages on app resume'),
onToggle: (value) => AppSettings().update((p) => p.alwaysBackgroundRefreshMessageListOnLifecycleResume = !p.alwaysBackgroundRefreshMessageListOnLifecycleResume), 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( SettingsSection(

View File

@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.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/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart'; import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart';
@@ -93,16 +95,35 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
showShare: false, showShare: false,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator( child: Column(
onRefresh: () => Future.sync( children: [
() => _pagingController.refresh(), 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).",
child: PagedListView<int, Subscription>( icon: null,
pagingController: _pagingController, mode: BadgeMode.info,
builderDelegate: PagedChildBuilderDelegate<Subscription>( textAlign: TextAlign.left,
itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh), 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<int, Subscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Subscription>(
itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh),
), ),
), ),
); );

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
@@ -407,6 +408,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription'); Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
@@ -429,6 +433,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
Toaster.success("Success", 'Unsubscribed from channel'); Toaster.success("Success", 'Unsubscribed from channel');
Navi.pop(context); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel'); Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -447,6 +454,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel'); Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
@@ -465,6 +475,9 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
await _initStateAsync(false); await _initStateAsync(false);
Toaster.success("Success", 'Subscribed to channel'); 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) { } catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel'); Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);

View File

@@ -56,6 +56,7 @@ class AppSettings extends ChangeNotifier {
bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true; bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
AppSettingsDateFormat dateFormat = AppSettingsDateFormat.ISO; AppSettingsDateFormat dateFormat = AppSettingsDateFormat.ISO;
int messagePreviewLength = 3; int messagePreviewLength = 3;
bool showInfoAlerts = true;
AppNotificationSettings notification0 = AppNotificationSettings(); AppNotificationSettings notification0 = AppNotificationSettings();
AppNotificationSettings notification1 = AppNotificationSettings(); AppNotificationSettings notification1 = AppNotificationSettings();
@@ -80,6 +81,7 @@ class AppSettings extends ChangeNotifier {
alwaysBackgroundRefreshMessageListOnLifecycleResume = true; alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
dateFormat = AppSettingsDateFormat.ISO; dateFormat = AppSettingsDateFormat.ISO;
messagePreviewLength = 3; messagePreviewLength = 3;
showInfoAlerts = true;
notification0 = AppNotificationSettings(); notification0 = AppNotificationSettings();
notification1 = AppNotificationSettings(); notification1 = AppNotificationSettings();
@@ -97,6 +99,7 @@ class AppSettings extends ChangeNotifier {
alwaysBackgroundRefreshMessageListOnLifecycleResume = Globals().sharedPrefs.getBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume') ?? alwaysBackgroundRefreshMessageListOnLifecycleResume; alwaysBackgroundRefreshMessageListOnLifecycleResume = Globals().sharedPrefs.getBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume') ?? alwaysBackgroundRefreshMessageListOnLifecycleResume;
dateFormat = AppSettingsDateFormat.parse(Globals().sharedPrefs.getString('settings.dateFormat')) ?? dateFormat; dateFormat = AppSettingsDateFormat.parse(Globals().sharedPrefs.getString('settings.dateFormat')) ?? dateFormat;
messagePreviewLength = Globals().sharedPrefs.getInt('settings.messagePreviewLength') ?? messagePreviewLength; messagePreviewLength = Globals().sharedPrefs.getInt('settings.messagePreviewLength') ?? messagePreviewLength;
showInfoAlerts = Globals().sharedPrefs.getBool('settings.showInfoAlerts') ?? showInfoAlerts;
notification0 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification0'); notification0 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification0');
notification1 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification1'); 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.setBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume', alwaysBackgroundRefreshMessageListOnLifecycleResume);
await Globals().sharedPrefs.setString('settings.dateFormat', dateFormat.key); await Globals().sharedPrefs.setString('settings.dateFormat', dateFormat.key);
await Globals().sharedPrefs.setInt('settings.messagePreviewLength', messagePreviewLength); await Globals().sharedPrefs.setInt('settings.messagePreviewLength', messagePreviewLength);
await Globals().sharedPrefs.setBool('settings.showInfoAlerts', showInfoAlerts);
await notification0.save(Globals().sharedPrefs, 'settings.notification0'); await notification0.save(Globals().sharedPrefs, 'settings.notification0');
await notification1.save(Globals().sharedPrefs, 'settings.notification1'); await notification1.save(Globals().sharedPrefs, 'settings.notification1');