Implement proper handling for inactive/active subscriptions

This commit is contained in:
Mike Schwörer 2025-04-18 00:11:01 +02:00
parent a43a3b441f
commit 63bc71c405
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
10 changed files with 268 additions and 48 deletions

View File

@ -524,6 +524,32 @@ class APIClient {
); );
} }
static Future<Subscription> activateSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'activateSubscription',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
jsonBody: {
'active': true,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> deactivateSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'deactivateSubscription',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
jsonBody: {
'active': false,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<SendMessageResponse> sendMessage(String userid, String keytoken, String text, {String? channel, String? content, String? messageID, int? priority, String? senderName, DateTime? timestamp}) async { static Future<SendMessageResponse> sendMessage(String userid, String keytoken, String text, {String? channel, String? content, String? messageID, int? priority, String? senderName, DateTime? timestamp}) async {
return await _request( return await _request(
name: 'sendMessage', name: 'sendMessage',

View File

@ -6,6 +6,7 @@ class Subscription {
final String channelInternalName; final String channelInternalName;
final String timestampCreated; final String timestampCreated;
final bool confirmed; final bool confirmed;
final bool active;
const Subscription({ const Subscription({
required this.subscriptionID, required this.subscriptionID,
@ -15,6 +16,7 @@ class Subscription {
required this.channelInternalName, required this.channelInternalName,
required this.timestampCreated, required this.timestampCreated,
required this.confirmed, required this.confirmed,
required this.active,
}); });
factory Subscription.fromJson(Map<String, dynamic> json) { factory Subscription.fromJson(Map<String, dynamic> json) {
@ -26,6 +28,7 @@ class Subscription {
channelInternalName: json['channel_internal_name'] as String, channelInternalName: json['channel_internal_name'] as String,
timestampCreated: json['timestamp_created'] as String, timestampCreated: json['timestamp_created'] as String,
confirmed: json['confirmed'] as bool, confirmed: json['confirmed'] as bool,
active: json['active'] as bool,
); );
} }

View File

@ -145,11 +145,11 @@ class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'fab_channel_list_qr', heroTag: 'fab_channel_list_extended-plus',
onPressed: () { onPressed: () {
Navi.push(context, () => ChannelScannerPage()); Navi.push(context, () => ChannelScannerPage());
}, },
child: const Icon(FontAwesomeIcons.qrcode), child: const Icon(FontAwesomeIcons.plus),
), ),
); );
} }

View File

@ -138,32 +138,68 @@ class _ChannelListItemState extends State<ChannelListItem> {
} }
Widget _buildIcon(BuildContext context) { Widget _buildIcon(BuildContext context) {
if (widget.subscription == null) { final acc = AppAuth();
Widget result = Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
if (widget.subscription == null && widget.channel.ownerUserID == acc.userID) {
// not-subscribed (own channel)
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
result = GestureDetector(onTap: () => _subscribe(), child: result); result = GestureDetector(onTap: () => _subscribe(), child: result);
return result; return result;
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) { } else if (widget.subscription == null) {
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel) // not-subscribed (foreign channel)
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result); Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
return result;
} else if (widget.subscription!.confirmed) {
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
return result;
} else {
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
return result; return result;
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
// inactive (own channel)
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
return result;
} else {
// inactive (foreign channel)
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), size: 32);
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
return result;
}
} else if (widget.subscription!.confirmed && widget.subscription!.active) {
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
// subscribed+active (own channel)
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32);
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
return result;
} else {
// subscribed+active (foreign channel)
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32);
result = GestureDetector(onTap: () => _deactivate(widget.subscription!), child: result);
return result;
}
} else if (!widget.subscription!.confirmed) {
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
// requested (own channel)
return SizedBox(width: 32, height: 32);
} else {
// requested (foreign channel)
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32);
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
return result;
}
} }
// fallback
return SizedBox(width: 32, height: 32);
} }
Widget _buildSubscriptionStateText(BuildContext context) { Widget _buildSubscriptionStateText(BuildContext context) {
if (widget.subscription == null) { if (widget.subscription == null) {
return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))); return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) { } else if (widget.subscription!.confirmed && widget.subscription!.active && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))); return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else if (widget.subscription!.confirmed) { } else if (widget.subscription!.confirmed && !widget.subscription!.active && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
return Text("subscripted (foreign channe)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))); return Text("inactive (own channel)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else if (widget.subscription!.confirmed && widget.subscription!.active) {
return Text("subscribed & active (foreign channel)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
return Text("subscribed (foreign channel) (inactive)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else { } else {
return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))); return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} }
@ -194,12 +230,12 @@ class _ChannelListItemState extends State<ChannelListItem> {
void _unsubscribe(Subscription sub) async { void _unsubscribe(Subscription sub) async {
final acc = AppAuth(); final acc = AppAuth();
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID() && widget.subscription != null) { if (acc.isAuth()) {
try { try {
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID); await APIClient.deleteSubscription(acc, sub.channelID, sub.subscriptionID);
widget.onChannelListReloadTrigger.call(); widget.onChannelListReloadTrigger.call();
widget.onSubscriptionChanged.call(widget.channel.channelID, null); widget.onSubscriptionChanged.call(sub.channelID, null);
Toaster.success("Success", 'Unsubscribed from channel'); Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) { } catch (exc, trace) {
@ -208,4 +244,40 @@ class _ChannelListItemState extends State<ChannelListItem> {
} }
} }
} }
void _deactivate(Subscription sub) async {
final acc = AppAuth();
if (acc.isAuth()) {
try {
var newSub = await APIClient.deactivateSubscription(acc, sub.channelID, sub.subscriptionID);
widget.onChannelListReloadTrigger.call();
widget.onSubscriptionChanged.call(sub.channelID, newSub);
Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
}
}
}
void _activate(Subscription sub) async {
final acc = AppAuth();
if (acc.isAuth()) {
try {
var newSub = await APIClient.activateSubscription(acc, sub.channelID, sub.subscriptionID);
widget.onChannelListReloadTrigger.call();
widget.onSubscriptionChanged.call(sub.channelID, newSub);
Toaster.success("Success", 'Subscribed to channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
}
}
}
} }

View File

@ -190,7 +190,11 @@ class _ChannelScannerResultChannelSubscribeState extends State<ChannelScannerRes
if (sub == null) { if (sub == null) {
return "Not Subscribed"; return "Not Subscribed";
} else if (sub.confirmed) { } else if (sub.confirmed) {
return "Already Subscribed"; if (sub.active) {
return "Already Subscribed";
} else {
return "Already Subscribed (inactive)";
}
} else { } else {
return "Unconfirmed Subscription"; return "Unconfirmed Subscription";
} }

View File

@ -149,7 +149,11 @@ class _ChannelScannerResultChannelViewState extends State<ChannelScannerResultCh
if (sub == null) { if (sub == null) {
return "Not Subscribed"; return "Not Subscribed";
} else if (sub.confirmed) { } else if (sub.confirmed) {
return "Already Subscribed"; if (sub.active) {
return "Already Subscribed";
} else {
return "Already Subscribed (inactive)";
}
} else { } else {
return "Unconfirmed Subscription"; return "Unconfirmed Subscription";
} }

View File

@ -9,10 +9,12 @@ import 'package:simplecloudnotifier/models/scan_result.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/pages/channel_message_view/channel_message_view.dart'; import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart'; import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
@ -196,7 +198,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidDiagramSubtask, icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (own)', title: 'Subscription (own)',
values: [_formatSubscriptionStatus(this.subscription)], values: [_formatSubscriptionStatus(this.subscription)],
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)], iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, null, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
), ),
_buildForeignSubscriptions(context), _buildForeignSubscriptions(context),
_buildOwnerCard(context, true), _buildOwnerCard(context, true),
@ -217,7 +219,48 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
} }
Widget _buildForeignChannelView(BuildContext context, ChannelPreview channel) { Widget _buildForeignChannelView(BuildContext context, ChannelPreview channel) {
final isSubscribed = (subscription != null && subscription!.confirmed); Widget subCard;
if (subscription != null && subscription!.confirmed && subscription!.active) {
subCard = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareXmark, null, _deactivate)],
);
} else if (subscription != null && subscription!.confirmed && !subscription!.active) {
subCard = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really (permantenly) delete your subscription to this channel?')), (FontAwesomeIcons.solidSquareRss, null, _activate)],
);
} else if (subscription != null && !subscription!.confirmed) {
subCard = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really withdraw your subscription-request to this channel?'))],
);
} else if (subscription == null) {
subCard = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
);
} else {
subCard = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
);
}
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
@ -240,15 +283,16 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
), ),
_buildDisplayNameCard(context, false), _buildDisplayNameCard(context, false),
_buildDescriptionNameCard(context, false), _buildDescriptionNameCard(context, false),
UI.metaCard( subCard,
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
),
_buildForeignSubscriptions(context), _buildForeignSubscriptions(context),
_buildOwnerCard(context, false), _buildOwnerCard(context, false),
UI.metaCard(
context: context,
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,
),
], ],
), ),
), ),
@ -269,7 +313,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidDiagramSuccessor, icon: FontAwesomeIcons.solidDiagramSuccessor,
title: 'Subscription (' + (user?.username ?? user?.userID ?? 'other') + ')', title: 'Subscription (' + (user?.username ?? user?.userID ?? 'other') + ')',
values: [_formatSubscriptionStatus(sub)], values: [_formatSubscriptionStatus(sub)],
iconActions: _getForeignSubActions(sub), iconActions: _getForeignIncomingSubActions(sub),
), ),
], ],
); );
@ -371,7 +415,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidInputText, icon: FontAwesomeIcons.solidInputText,
title: 'DisplayName', title: 'DisplayName',
values: [_displayNameOverride ?? channelPreview!.displayName], values: [_displayNameOverride ?? channelPreview!.displayName],
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditDisplayName)] : [], iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditDisplayName)] : [],
); );
} else if (_editDisplayName == EditState.saving) { } else if (_editDisplayName == EditState.saving) {
return Padding( return Padding(
@ -427,7 +471,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
icon: FontAwesomeIcons.solidInputPipe, icon: FontAwesomeIcons.solidInputPipe,
title: 'Description', title: 'Description',
values: [_descriptionNameOverride ?? channelPreview?.descriptionName ?? ''], values: [_descriptionNameOverride ?? channelPreview?.descriptionName ?? ''],
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditDescriptionName)] : [], iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditDescriptionName)] : [],
); );
} else if (_editDescriptionName == EditState.saving) { } else if (_editDescriptionName == EditState.saving) {
return Padding( return Padding(
@ -536,11 +580,16 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
} }
} }
void _unsubscribe() async { void _unsubscribe({String? confirm = null}) async {
final acc = AppAuth(); final acc = AppAuth();
if (subscription == null) return; if (subscription == null) return;
if (confirm != null) {
final r = await UIDialogs.showConfirmDialog(context, confirm, okText: 'Unsubscribe', cancelText: 'Cancel');
if (!r) return;
}
try { try {
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID); await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call(); widget.needsReload?.call();
@ -554,6 +603,42 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
} }
} }
void _deactivate() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.deactivateSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
}
}
void _activate() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.activateSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscribed to channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
}
}
void _cancelForeignSubscription(Subscription sub) async { void _cancelForeignSubscription(Subscription sub) async {
final acc = AppAuth(); final acc = AppAuth();
@ -605,10 +690,14 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
String _formatSubscriptionStatus(Subscription? subscription) { String _formatSubscriptionStatus(Subscription? subscription) {
if (subscription == null) { if (subscription == null) {
return 'Not Subscribed'; return 'Not Subscribed';
} else if (subscription.confirmed) { } else if (subscription.confirmed && subscription.active) {
return 'Subscribed'; return 'Subscribed & Active';
} else { } else if (subscription.confirmed && !subscription.active) {
return 'Subscribed & Inactive';
} else if (!subscription.confirmed) {
return 'Requested'; return 'Requested';
} else {
return '?';
} }
} }
@ -662,13 +751,13 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
} }
} }
List<(IconData, void Function())> _getForeignSubActions(Subscription sub) { List<(IconData, Color?, void Function())> _getForeignIncomingSubActions(Subscription sub) {
if (sub.confirmed) { if (sub.confirmed) {
return [(FontAwesomeIcons.solidSquareXmark, () => _cancelForeignSubscription(sub))]; return [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _cancelForeignSubscription(sub))];
} else { } else {
return [ return [
(FontAwesomeIcons.solidSquareCheck, () => _confirmForeignSubscription(sub)), (FontAwesomeIcons.solidSquareCheck, Colors.green[900], () => _confirmForeignSubscription(sub)),
(FontAwesomeIcons.solidSquareXmark, () => _denyForeignSubscription(sub)), (FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _denyForeignSubscription(sub)),
]; ];
} }
} }

View File

@ -267,7 +267,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
icon: FontAwesomeIcons.solidInputText, icon: FontAwesomeIcons.solidInputText,
title: 'Name', title: 'Name',
values: [_nameOverride ?? keytokenPreview!.name], values: [_nameOverride ?? keytokenPreview!.name],
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditName)] : [], iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditName)] : [],
); );
} else if (_editName == EditState.saving) { } else if (_editName == EditState.saving) {
return Padding( return Padding(
@ -357,7 +357,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
icon: FontAwesomeIcons.shieldKeyhole, icon: FontAwesomeIcons.shieldKeyhole,
title: 'Permissions', title: 'Permissions',
values: _formatPermissions(keyToken.permissions), values: _formatPermissions(keyToken.permissions),
iconActions: [(FontAwesomeIcons.penToSquare, _editPermissions)], iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)],
); );
} else { } else {
w1 = UI.metaCard( w1 = UI.metaCard(
@ -374,7 +374,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
icon: FontAwesomeIcons.solidSnake, icon: FontAwesomeIcons.solidSnake,
title: 'Channels', title: 'Channels',
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names
iconActions: [(FontAwesomeIcons.penToSquare, _editChannels)], iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
); );
} else { } else {
w2 = UI.metaCard( w2 = UI.metaCard(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class UIDialogs { class UIDialogs {
static Future<String?> showTextInput(BuildContext context, String title, String hintText) { static Future<String?> showTextInput(BuildContext context, String title, String hintText) {
var _textFieldController = TextEditingController(); var _textFieldController = TextEditingController();
@ -26,4 +27,25 @@ class UIDialogs {
), ),
); );
} }
static Future<bool> showConfirmDialog(BuildContext context, String title, {String? text, String? okText, String? cancelText}) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: (text != null) ? Text(text) : null,
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(cancelText ?? 'Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(okText ?? 'OK'),
),
],
),
).then((value) => value ?? false);
}
} }

View File

@ -124,7 +124,7 @@ class UI {
); );
} }
static Widget metaCard({required BuildContext context, required IconData icon, required String title, required List<String> values, void Function()? mainAction, List<(IconData, void Function())>? iconActions}) { static Widget metaCard({required BuildContext context, required IconData icon, required String title, required List<String> values, void Function()? mainAction, List<(IconData, Color?, void Function())>? iconActions}) {
final container = UI.box( final container = UI.box(
context: context, context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2), padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
@ -145,7 +145,7 @@ class UI {
SizedBox(width: 12), SizedBox(width: 12),
for (final iconAction in iconActions) ...[ for (final iconAction in iconActions) ...[
SizedBox(width: 4), SizedBox(width: 4),
IconButton(icon: FaIcon(iconAction.$1), onPressed: iconAction.$2), IconButton(icon: FaIcon(iconAction.$1), color: iconAction.$2, onPressed: iconAction.$3),
], ],
], ],
], ],