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 createState() => _KeyTokenViewPageState(); } enum EditState { none, editing, saving } enum KeyTokenViewPageInitState { loading, okay, error } class _KeyTokenViewPageState extends State { static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting late ImmediateFuture _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 _initStateAsync(bool usePreload) async { final userAcc = Provider.of(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.ofValue(cacheUser.toPreview()); } else { _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc)); } } else { _futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID)); } }); } @override void dispose() { _ctrlName.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final userAcc = Provider.of(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(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 _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 _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, //TODO show channel names iconActions: [(FontAwesomeIcons.penToSquare, _editChannels)], ); } else { w2 = UI.metaCard( context: context, icon: FontAwesomeIcons.solidSnake, title: 'Channels', values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names ); } return [w1, w2]; } List _formatPermissions(String v) { var splt = v.split(';'); if (splt.length == 0) return ["None"]; List result = []; if (splt.contains("A")) result.add("Admin"); if (splt.contains("UR")) result.add("Read Account"); if (splt.contains("CR")) result.add("Read Messages"); if (splt.contains("CS")) result.add("Send Messages"); return result; } void _editPermissions() { final acc = Provider.of(context, listen: false); //TODO prevent editing current admin/read token //TODO Toaster.info("Not implemented", "Currently not implemented"); } void _editChannels() { final acc = Provider.of(context, listen: false); //TODO prevent editing current admin/read token //TODO Toaster.info("Not implemented", "Currently not implemented"); } void _deleteKey() { final acc = Provider.of(context, listen: false); //TODO prevent deleting current admin/read token //TODO Toaster.info("Not implemented", "Currently not implemented"); } }