implement keytoken list and all-messages list
Some checks failed
Build Docker and Deploy / Build Docker Container (push) Successful in 50s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 11m15s
Build Docker and Deploy / Deploy to Server (push) Has been skipped

This commit is contained in:
Mike Schwörer 2025-04-13 19:47:18 +02:00
parent e9c5c5fb99
commit ab4b40ab75
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
11 changed files with 685 additions and 14 deletions

View File

@ -39,6 +39,7 @@ class MessageFilter {
DateTime? timeBefore; DateTime? timeBefore;
DateTime? timeAfter; DateTime? timeAfter;
bool? hasSenderName; bool? hasSenderName;
List<String>? senderUserID;
MessageFilter({ MessageFilter({
this.channelIDs, this.channelIDs,
@ -49,6 +50,7 @@ class MessageFilter {
this.priority, this.priority,
this.timeBefore, this.timeBefore,
this.timeAfter, this.timeAfter,
this.senderUserID,
}); });
} }
@ -281,7 +283,7 @@ class APIClient {
); );
} }
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter}) async { static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter, bool? includeNonSuscribed}) async {
return await _request( return await _request(
name: 'getMessageList', name: 'getMessageList',
method: 'GET', method: 'GET',
@ -298,6 +300,8 @@ class APIClient {
if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()], if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()],
if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(), if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(),
if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!, if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!,
if (filter?.senderUserID != null) 'sender_user_id': filter!.senderUserID!,
if (includeNonSuscribed ?? false) 'subscription_status': ['all'],
}, },
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
authToken: auth.getToken(), authToken: auth.getToken(),
@ -426,6 +430,22 @@ class APIClient {
); );
} }
static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async {
return await _request(
name: 'updateKeyToken',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/keys/${kid}',
jsonBody: {
if (name != null) 'name': name,
if (allChannels != null) 'all_channels': allChannels,
if (channels != null) 'channels': channels,
if (permissions != null) 'permissions': permissions,
},
fn: KeyToken.fromJson,
authToken: auth.getToken(),
);
}
static Future<KeyTokenWithToken> createKeyToken(TokenSource auth, String name, String perm, bool allChannels, {List<String>? channels}) async { static Future<KeyTokenWithToken> createKeyToken(TokenSource auth, String name, String perm, bool allChannels, {List<String>? channels}) async {
return await _request( return await _request(
name: 'createKeyToken', name: 'createKeyToken',

View File

@ -2,7 +2,7 @@ class KeyToken {
final String keytokenID; final String keytokenID;
final String name; final String name;
final String timestampCreated; final String timestampCreated;
final String? timestampLastused; final String? timestampLastUsed;
final String ownerUserID; final String ownerUserID;
final bool allChannels; final bool allChannels;
final List<String> channels; final List<String> channels;
@ -13,7 +13,7 @@ class KeyToken {
required this.keytokenID, required this.keytokenID,
required this.name, required this.name,
required this.timestampCreated, required this.timestampCreated,
required this.timestampLastused, required this.timestampLastUsed,
required this.ownerUserID, required this.ownerUserID,
required this.allChannels, required this.allChannels,
required this.channels, required this.channels,
@ -26,7 +26,7 @@ class KeyToken {
keytokenID: json['keytoken_id'] as String, keytokenID: json['keytoken_id'] as String,
name: json['name'] as String, name: json['name'] as String,
timestampCreated: json['timestamp_created'] as String, timestampCreated: json['timestamp_created'] as String,
timestampLastused: json['timestamp_lastused'] as String?, timestampLastUsed: json['timestamp_lastused'] as String?,
ownerUserID: json['owner_user_id'] as String, ownerUserID: json['owner_user_id'] as String,
allChannels: json['all_channels'] as bool, allChannels: json['all_channels'] as bool,
channels: (json['channels'] as List<dynamic>).map((e) => e as String).toList(), channels: (json['channels'] as List<dynamic>).map((e) => e as String).toList(),
@ -38,6 +38,17 @@ class KeyToken {
static List<KeyToken> fromJsonArray(List<dynamic> jsonArr) { static List<KeyToken> fromJsonArray(List<dynamic> jsonArr) {
return jsonArr.map<KeyToken>((e) => KeyToken.fromJson(e as Map<String, dynamic>)).toList(); return jsonArr.map<KeyToken>((e) => KeyToken.fromJson(e as Map<String, dynamic>)).toList();
} }
KeyTokenPreview toPreview() {
return KeyTokenPreview(
keytokenID: keytokenID,
name: name,
ownerUserID: ownerUserID,
allChannels: allChannels,
channels: channels,
permissions: permissions,
);
}
} }
class KeyTokenWithToken { class KeyTokenWithToken {

View File

@ -9,6 +9,8 @@ import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/account/login.dart'; import 'package:simplecloudnotifier/pages/account/login.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart'; import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart';
import 'package:simplecloudnotifier/pages/client_list/client_list.dart'; import 'package:simplecloudnotifier/pages/client_list/client_list.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list.dart';
import 'package:simplecloudnotifier/pages/sender_list/sender_list.dart'; import 'package:simplecloudnotifier/pages/sender_list/sender_list.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';
@ -384,8 +386,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}), _buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
_buildNumberCard(context, 'Clients', futureClientCount, () { _buildNumberCard(context, 'Clients', futureClientCount, () {
Navi.push(context, () => ClientListPage()); Navi.push(context, () => ClientListPage());
}), }),
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}), _buildNumberCard(context, 'Keys', futureKeyCount, () {
Navi.push(context, () => KeyTokenListPage());
}),
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () { _buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {
Navi.push(context, () => ChannelListExtendedPage()); Navi.push(context, () => ChannelListExtendedPage());
}), }),
@ -402,7 +406,9 @@ class _AccountRootPageState extends State<AccountRootPage> {
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
], ],
), ),
onTap: () {/*TODO*/}, onTap: () {
Navi.push(context, () => FilteredMessageViewPage(title: "All Messages", filter: MessageFilter(senderUserID: [user.userID])));
},
), ),
]; ];
} }

View File

@ -166,6 +166,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
} }
Widget _buildOwnedChannelView(BuildContext context, Channel channel) { Widget _buildOwnedChannelView(BuildContext context, Channel channel) {
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
final isSubscribed = (subscription != null && subscription!.confirmed); final isSubscribed = (subscription != null && subscription!.confirmed);
return SingleChildScrollView( return SingleChildScrollView(
@ -208,6 +209,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
Navi.push(context, () => ChannelMessageViewPage(channel: channel)); Navi.push(context, () => ChannelMessageViewPage(channel: channel));
}, },
), ),
if (channel.ownerUserID == userAccUserID) UI.button(text: "Delete Channel", onPressed: () {/*TODO*/}, color: Colors.red[900]),
], ],
), ),
), ),

View File

@ -64,7 +64,7 @@ class _ClientListPageState extends State<ClientListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SCNScaffold( return SCNScaffold(
title: "Client", title: "Clients",
showSearch: false, showSearch: false,
showShare: false, showShare: false,
child: Padding( child: Padding(

View File

@ -1,11 +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:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
enum ClientListItemMode { enum ClientListItemMode {
Messages, Messages,

View File

@ -70,7 +70,7 @@ class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> {
}); });
} }
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter); final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter, includeNonSuscribed: true);
SCNDataCache().addToMessageCache(newItems); // no await SCNDataCache().addToMessageCache(newItems); // no await

View File

@ -0,0 +1,86 @@
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/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/keytoken.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';
class KeyTokenListPage extends StatefulWidget {
const KeyTokenListPage({super.key});
@override
State<KeyTokenListPage> createState() => _KeyTokenListPageState();
}
class _KeyTokenListPageState extends State<KeyTokenListPage> {
final PagingController<int, KeyToken> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(_fetchPage);
_pagingController.refresh();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
void dispose() {
ApplicationLog.debug('KeyTokenListPage::dispose');
_pagingController.dispose();
super.dispose();
}
Future<void> _fetchPage(int pageKey) async {
final acc = Provider.of<AppAuth>(context, listen: false);
ApplicationLog.debug('Start KeyTokenListPage::_pagingController::_fetchPage [ ${pageKey} ]');
if (!acc.isAuth()) {
_pagingController.error = 'Not logged in';
return;
}
try {
final items = (await APIClient.getKeyTokenList(acc)).toList();
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
} catch (exc, trace) {
_pagingController.error = exc.toString();
ApplicationLog.error('Failed to list keytokens: ' + exc.toString(), trace: trace);
}
}
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: "Keys",
showSearch: false,
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, KeyToken>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<KeyToken>(
itemBuilder: (context, item, index) => KeyTokenListItem(item: item),
),
),
),
),
);
}
}

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
enum KeyTokenListItemMode {
Messages,
Extended,
}
class KeyTokenListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const KeyTokenListItem({
required this.item,
super.key,
});
final KeyToken item;
@override
Widget build(BuildContext context) {
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: Theme.of(context).cardTheme.color,
child: InkWell(
onTap: () {
Navi.push(context, () => KeyTokenViewPage(keytokenID: item.keytokenID, preloadedData: item, needsReload: null));
},
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Icon(FontAwesomeIcons.solidGearCode, color: Theme.of(context).colorScheme.outline, size: 32),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: Text(
item.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Text(
(item.timestampLastUsed == null) ? '' : KeyTokenListItem._dateFormat.format(DateTime.parse(item.timestampLastUsed!).toLocal()),
style: const TextStyle(fontSize: 14),
),
],
),
SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
"Permissions: " + _formatPermissions(item.permissions, item.allChannels, item.channels),
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
),
),
Text(item.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
],
),
],
),
),
SizedBox(width: 4),
GestureDetector(
onTap: () {
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(usedKeys: [item.keytokenID])));
},
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
),
),
],
),
),
),
);
}
String _formatPermissions(String v, bool allChannels, List<String> channels) {
var splt = v.split(';');
if (splt.length == 0) return "None";
var a = splt.contains("A");
var ur = splt.contains("UR");
var cr = splt.contains("CR");
var cs = splt.contains("CS");
if (a) return "Admin";
if (cr && cs && allChannels) return "Read+Send";
if (cr && cs && !allChannels) return "Read+Send (${channels.length} channel${channels.length == 1 ? '' : 's'})";
if (ur && !cr && !cs) return "Account-Read";
if (cr && !cs && !allChannels) return "Read-only (${channels.length} channel${channels.length == 1 ? '' : 's'})";
if (cr && !cs && allChannels) return "Read-only";
if (cs && !allChannels) return "Send-Only (${channels.length} channel${channels.length == 1 ? '' : 's'})";
if (cs && allChannels) return "Send-Only";
return "{ " + v + " | " + (allChannels ? 'all' : '${channels.length}') + " }";
}
}

View File

@ -0,0 +1,435 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
import 'package:provider/provider.dart';
class KeyTokenViewPage extends StatefulWidget {
const KeyTokenViewPage({
required this.keytokenID,
required this.preloadedData,
required this.needsReload,
super.key,
});
final String keytokenID;
final KeyToken? preloadedData;
final void Function()? needsReload;
@override
State<KeyTokenViewPage> createState() => _KeyTokenViewPageState();
}
enum EditState { none, editing, saving }
enum KeyTokenViewPageInitState { loading, okay, error }
class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
late ImmediateFuture<UserPreview> _futureOwner;
final TextEditingController _ctrlName = TextEditingController();
int _loadingIndeterminateCounter = 0;
EditState _editName = EditState.none;
String? _nameOverride = null;
KeyTokenPreview? keytokenPreview;
KeyToken? keytoken;
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
String errorMessage = '';
@override
void initState() {
_initStateAsync(true);
super.initState();
}
Future<void> _initStateAsync(bool usePreload) async {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (widget.preloadedData != null && usePreload) {
keytoken = widget.preloadedData!;
keytokenPreview = widget.preloadedData!.toPreview();
} else {
try {
var p = await APIClient.getKeyTokenPreviewByID(userAcc, widget.keytokenID);
setState(() {
keytokenPreview = p;
});
if (p.ownerUserID == userAcc.userID) {
var r = await APIClient.getKeyToken(userAcc, widget.keytokenID);
setState(() {
keytoken = r;
});
} else {
setState(() {
keytoken = null;
});
}
} catch (exc, trace) {
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to load data');
this.errorMessage = 'Failed to load data: ' + exc.toString();
this.loadingState = KeyTokenViewPageInitState.error;
return;
}
}
setState(() {
this.loadingState = KeyTokenViewPageInitState.okay;
assert(keytokenPreview != null);
if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
var cacheUser = userAcc.getUserOrNull();
if (cacheUser != null) {
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
} else {
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
}
} else {
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID));
}
});
}
@override
void dispose() {
_ctrlName.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final userAcc = Provider.of<AppAuth>(context, listen: false);
var title = "Key";
Widget child;
if (loadingState == KeyTokenViewPageInitState.loading) {
child = Center(child: CircularProgressIndicator());
} else if (loadingState == KeyTokenViewPageInitState.error) {
child = Center(child: Text('Error: ' + errorMessage)); //TODO better error
} else if (loadingState == KeyTokenViewPageInitState.okay && keytokenPreview!.ownerUserID == userAcc.userID) {
child = _buildOwnedKeyTokenView(context, this.keytoken!);
title = this.keytoken!.name;
} else {
child = _buildForeignKeyTokenView(context, this.keytokenPreview!);
title = keytokenPreview!.name;
}
return SCNScaffold(
title: title,
showSearch: false,
showShare: false,
child: child,
);
}
Widget _buildOwnedKeyTokenView(BuildContext context, KeyToken keytoken) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [keytoken.keytokenID],
),
_buildNameCard(context, true),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.clockTwo,
title: 'Last Used',
values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
),
_buildOwnerCard(context, true),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidEnvelope,
title: 'Messages',
values: [keytoken.messagesSent.toString()],
mainAction: () {
Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, filter: MessageFilter(usedKeys: [keytoken.keytokenID])));
},
),
..._buildPermissionCard(context, true, keytoken.toPreview()),
UI.button(text: "Delete Key", onPressed: _deleteKey, color: Colors.red[900]),
],
),
),
);
}
Widget _buildForeignKeyTokenView(BuildContext context, KeyTokenPreview keytoken) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 8),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID',
values: [keytoken.keytokenID],
),
_buildNameCard(context, false),
_buildOwnerCard(context, false),
..._buildPermissionCard(context, false, keytoken),
],
),
),
);
}
Widget _buildOwnerCard(BuildContext context, bool isOwned) {
return FutureBuilder(
future: _futureOwner.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
);
} else {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : '')],
);
}
},
);
}
Widget _buildNameCard(BuildContext context, bool isOwned) {
if (_editName == EditState.editing) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: UI.box(
context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43),
SizedBox(width: 16),
Expanded(
child: TextField(
autofocus: true,
controller: _ctrlName,
decoration: new InputDecoration.collapsed(hintText: 'Name'),
),
),
SizedBox(width: 12),
SizedBox(width: 4),
IconButton(icon: FaIcon(FontAwesomeIcons.solidFloppyDisk), onPressed: _saveName),
],
),
),
);
} else if (_editName == EditState.none) {
return UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputText,
title: 'Name',
values: [_nameOverride ?? keytokenPreview!.name],
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditName)] : [],
);
} else if (_editName == EditState.saving) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: UI.box(
context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43),
SizedBox(width: 16),
Expanded(child: SizedBox()),
SizedBox(width: 12),
SizedBox(width: 4),
Padding(padding: const EdgeInsets.all(8.0), child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator())),
],
),
),
);
} else {
throw 'Invalid EditDisplayNameState: $_editName';
}
}
void _showEditName() {
setState(() {
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
_editName = EditState.editing;
if (_editName == EditState.editing) _editName = EditState.none;
});
}
void _saveName() async {
final userAcc = Provider.of<AppAuth>(context, listen: false);
final newName = _ctrlName.text;
try {
setState(() {
_editName = EditState.saving;
});
final newKeyToken = await APIClient.updateKeyToken(userAcc, widget.keytokenID, name: newName);
setState(() {
_editName = EditState.none;
_nameOverride = newKeyToken.name;
});
widget.needsReload?.call();
} catch (exc, trace) {
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
Toaster.error("Error", 'Failed to save DisplayName');
}
}
Future<UserPreview> _getOwner(AppAuth auth) async {
try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
_incLoadingIndeterminateCounter(1);
final owner = APIClient.getUserPreview(auth, keytokenPreview!.ownerUserID);
//await Future.delayed(const Duration(seconds: 10), () {});
return owner;
} finally {
_incLoadingIndeterminateCounter(-1);
}
}
void _incLoadingIndeterminateCounter(int delta) {
setState(() {
_loadingIndeterminateCounter += delta;
AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0);
});
}
List<Widget> _buildPermissionCard(BuildContext context, bool isOwned, KeyTokenPreview keyToken) {
Widget w1;
Widget w2;
if (isOwned) {
w1 = UI.metaCard(
context: context,
icon: FontAwesomeIcons.shieldKeyhole,
title: 'Permissions',
values: _formatPermissions(keyToken.permissions),
iconActions: [(FontAwesomeIcons.penToSquare, _editPermissions)],
);
} else {
w1 = UI.metaCard(
context: context,
icon: FontAwesomeIcons.shieldKeyhole,
title: 'Permissions',
values: _formatPermissions(keyToken.permissions),
);
}
if (isOwned) {
w2 = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channels',
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels,
iconActions: [(FontAwesomeIcons.penToSquare, _editChannels)],
);
} else {
w2 = UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidSnake,
title: 'Channels',
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels,
);
}
return [w1, w2];
}
List<String> _formatPermissions(String v) {
var splt = v.split(';');
if (splt.length == 0) return ["None"];
List<String> result = [];
if (splt.contains("A")) result.add("Admin");
if (splt.contains("UR")) result.add("Read Account");
if (splt.contains("CR")) result.add("Read Messages");
if (splt.contains("CS")) result.add("Send Messages");
return result;
}
void _editPermissions() {
final acc = Provider.of<AppAuth>(context, listen: false);
//TODO prevent editing current admin/read token
//TODO
Toaster.info("Not implemented", "Currently not implemented");
}
void _editChannels() {
final acc = Provider.of<AppAuth>(context, listen: false);
//TODO prevent editing current admin/read token
//TODO
Toaster.info("Not implemented", "Currently not implemented");
}
void _deleteKey() {
final acc = Provider.of<AppAuth>(context, listen: false);
//TODO prevent deleting current admin/read token
//TODO
Toaster.info("Not implemented", "Currently not implemented");
}
}

View File

@ -162,7 +162,9 @@ class _MessageViewPageState extends State<MessageViewPage> {
icon: FontAwesomeIcons.solidGearCode, icon: FontAwesomeIcons.solidGearCode,
title: 'KeyToken', title: 'KeyToken',
values: [message.usedKeyID, token?.name ?? '...'], values: [message.usedKeyID, token?.name ?? '...'],
mainAction: () => {/*TODO*/}, mainAction: () => {
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID])))
},
), ),
UI.metaCard( UI.metaCard(
context: context, context: context,