From ede5fdc34834aaed626dc7eb5c6f252ed8073ecd Mon Sep 17 00:00:00 2001 From: Krille Date: Sat, 4 Feb 2023 18:32:56 +0100 Subject: [PATCH] style: New settings design --- .../extensions/default_flows.dart | 12 - lib/config/routes.dart | 50 ++-- lib/pages/chat_list/chat_list.dart | 4 +- .../chat_list/client_chooser_button.dart | 28 +- lib/pages/settings/settings.dart | 64 ++++- lib/pages/settings/settings_view.dart | 265 ++++++++++++------ .../settings_account/settings_account.dart | 185 ------------ .../settings_account_view.dart | 76 ----- .../settings_security/settings_security.dart | 96 +++++++ .../settings_security_view.dart | 79 +++--- 10 files changed, 401 insertions(+), 458 deletions(-) delete mode 100644 lib/pages/settings_account/settings_account.dart delete mode 100644 lib/pages/settings_account/settings_account_view.dart diff --git a/integration_test/extensions/default_flows.dart b/integration_test/extensions/default_flows.dart index b61f6aff..aa435078 100644 --- a/integration_test/extensions/default_flows.dart +++ b/integration_test/extensions/default_flows.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:fluffychat/pages/chat_list/chat_list_body.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; -import 'package:fluffychat/pages/settings_account/settings_account_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -120,17 +119,6 @@ extension DefaultFlowExtensions on WidgetTester { ), ); await tester.pumpAndSettle(); - await tester.tap(find.text('Account')); - await tester.pumpAndSettle(); - await tester.scrollUntilVisible( - find.text('Logout'), - 500, - scrollable: find.descendant( - of: find.byType(SettingsAccountView), - matching: find.byType(Scrollable), - ), - ); - await tester.pumpAndSettle(); await tester.tap(find.text('Logout')); await tester.pumpAndSettle(); await tester.tap(find.maybeUppercaseText('Yes')); diff --git a/lib/config/routes.dart b/lib/config/routes.dart index b932749a..8d6ec936 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -19,7 +19,6 @@ import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; import 'package:fluffychat/pages/new_space/new_space.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; -import 'package:fluffychat/pages/settings_account/settings_account.dart'; import 'package:fluffychat/pages/settings_chat/settings_chat.dart'; import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart'; import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart'; @@ -345,38 +344,31 @@ class AppRoutes { ], ), VWidget( - path: 'account', - widget: const SettingsAccount(), - buildTransition: _dynamicTransition, + path: 'addaccount', + widget: const HomeserverPicker(), + buildTransition: _fadeTransition, stackedRoutes: [ VWidget( - path: 'add', - widget: const HomeserverPicker(), + path: 'login', + widget: const Login(), buildTransition: _fadeTransition, - stackedRoutes: [ - VWidget( - path: 'login', - widget: const Login(), - buildTransition: _fadeTransition, - ), - VWidget( - path: 'connect', - widget: const ConnectPage(), - buildTransition: _fadeTransition, - stackedRoutes: [ - VWidget( - path: 'login', - widget: const Login(), - buildTransition: _fadeTransition, - ), - VWidget( - path: 'signup', - widget: const SignupPage(), - buildTransition: _fadeTransition, - ), - ]), - ], ), + VWidget( + path: 'connect', + widget: const ConnectPage(), + buildTransition: _fadeTransition, + stackedRoutes: [ + VWidget( + path: 'login', + widget: const Login(), + buildTransition: _fadeTransition, + ), + VWidget( + path: 'signup', + widget: const SignupPage(), + buildTransition: _fadeTransition, + ), + ]), ], ), VWidget( diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index b452a161..9bf682bc 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -15,6 +15,7 @@ import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; +import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/utils/famedlysdk_store.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; @@ -27,7 +28,6 @@ import '../../utils/voip/callkeep_manager.dart'; import '../../widgets/fluffy_chat_app.dart'; import '../../widgets/matrix.dart'; import '../bootstrap/bootstrap_dialog.dart'; -import '../settings_account/settings_account.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; @@ -688,7 +688,7 @@ class ChatListController extends State } Future dehydrate() => - SettingsAccountController.dehydrateDevice(context); + SettingsSecurityController.dehydrateDevice(context); } enum EditBundleAction { addToBundle, removeFromBundle } diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index a74622aa..da9b27f4 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; @@ -212,12 +213,15 @@ class ClientChooserButton extends StatelessWidget { child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(99), - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - matrix.client.userID!.localpart, - size: 28, - fontSize: 12, + child: Hero( + tag: 'profilesettings', + child: Avatar( + mxContent: snapshot.data?.avatarUrl, + name: snapshot.data?.displayName ?? + matrix.client.userID!.localpart, + size: 28, + fontSize: 12, + ), ), ), ), @@ -240,7 +244,7 @@ class ClientChooserButton extends StatelessWidget { void _clientSelected( Object object, BuildContext context, - ) { + ) async { if (object is Client) { controller.setActiveClient(object); } else if (object is String) { @@ -248,7 +252,15 @@ class ClientChooserButton extends StatelessWidget { } else if (object is SettingsAction) { switch (object) { case SettingsAction.addAccount: - VRouter.of(context).to('/settings/account'); + final consent = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context)!.addAccount, + message: L10n.of(context)!.enableMultiAccounts, + okLabel: L10n.of(context)!.next, + cancelLabel: L10n.of(context)!.cancel, + ); + if (consent != OkCancelResult.ok) return; + VRouter.of(context).to('/settings/addaccount'); break; case SettingsAction.newStory: VRouter.of(context).to('/stories/create'); diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index ddeea37a..a5768273 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -22,16 +22,61 @@ class Settings extends StatefulWidget { } class SettingsController extends State { - Future? profileFuture; - Profile? profile; + Future? profileFuture; bool profileUpdated = false; void updateProfile() => setState(() { profileUpdated = true; - profile = profileFuture = null; + profileFuture = null; }); + void setDisplaynameAction() async { + final profile = await profileFuture; + final input = await showTextInputDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.editDisplayname, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + textFields: [ + DialogTextField( + initialText: profile?.displayName ?? + Matrix.of(context).client.userID!.localpart, + ) + ], + ); + if (input == null) return; + final matrix = Matrix.of(context); + final success = await showFutureLoadingDialog( + context: context, + future: () => + matrix.client.setDisplayName(matrix.client.userID!, input.single), + ); + if (success.error == null) { + updateProfile(); + } + } + + void logoutAction() async { + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSureYouWantToLogout, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.cancel, + ) == + OkCancelResult.cancel) { + return; + } + final matrix = Matrix.of(context); + await showFutureLoadingDialog( + context: context, + future: () => matrix.client.logout(), + ); + } + void setAvatarAction() async { + final profile = await profileFuture; final actions = [ if (PlatformInfos.isMobile) SheetAction( @@ -131,9 +176,9 @@ class SettingsController extends State { } bool? crossSigningCached; - bool showChatBackupBanner = false; + bool? showChatBackupBanner; - void firstRunBootstrapAction() async { + void firstRunBootstrapAction([_]) async { await BootstrapDialog( client: Matrix.of(context).client, ).show(context); @@ -143,16 +188,11 @@ class SettingsController extends State { @override Widget build(BuildContext context) { final client = Matrix.of(context).client; - profileFuture ??= client - .getProfileFromUserId( + profileFuture ??= client.getProfileFromUserId( client.userID!, cache: !profileUpdated, getFromRooms: !profileUpdated, - ) - .then((p) { - if (mounted) setState(() => profile = p); - return p; - }); + ); return SettingsView(this); } } diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index be96ca4e..8062c6e8 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import '../../config/themes.dart'; -import '../../widgets/content_banner.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'settings.dart'; class SettingsView extends StatelessWidget { @@ -18,95 +19,181 @@ class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => - [ - SliverAppBar( - expandedHeight: 300.0, - floating: true, - pinned: true, - title: Text(L10n.of(context)!.settings), - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, - flexibleSpace: FlexibleSpaceBar( - background: ContentBanner( - mxContent: controller.profile?.avatarUrl, - onEdit: controller.setAvatarAction, - defaultIcon: Icons.account_circle_outlined, - ), - ), + appBar: AppBar( + title: Text(L10n.of(context)!.settings), + actions: [ + TextButton.icon( + onPressed: controller.logoutAction, + label: Text(L10n.of(context)!.logout), + icon: const Icon(Icons.logout_outlined), ), ], - body: ListTileTheme( - iconColor: Theme.of(context).colorScheme.onBackground, - child: ListView( - key: const Key('SettingsListViewContent'), - children: [ - AnimatedContainer( - height: controller.showChatBackupBanner ? 54 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: ListTile( - leading: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.enableAutoBackups), - trailing: const Icon( - Icons.warning_outlined, - color: Colors.orange, - ), - onTap: controller.firstRunBootstrapAction, - ), - ), - const Divider(thickness: 1), - ListTile( - leading: const Icon(Icons.format_paint_outlined), - title: Text(L10n.of(context)!.changeTheme), - onTap: () => VRouter.of(context).to('/settings/style'), - ), - const Divider(thickness: 1), - ListTile( - leading: const Icon(Icons.notifications_outlined), - title: Text(L10n.of(context)!.notifications), - onTap: () => VRouter.of(context).to('/settings/notifications'), - ), - ListTile( - leading: const Icon(Icons.devices_outlined), - title: Text(L10n.of(context)!.devices), - onTap: () => VRouter.of(context).to('/settings/devices'), - ), - ListTile( - leading: const Icon(Icons.chat_bubble_outline_outlined), - title: Text(L10n.of(context)!.chat), - onTap: () => VRouter.of(context).to('/settings/chat'), - ), - ListTile( - leading: const Icon(Icons.account_circle_outlined), - title: Text(L10n.of(context)!.account), - onTap: () => VRouter.of(context).to('/settings/account'), - ), - ListTile( - leading: const Icon(Icons.shield_outlined), - title: Text(L10n.of(context)!.security), - onTap: () => VRouter.of(context).to('/settings/security'), - ), - const Divider(thickness: 1), - ListTile( - leading: const Icon(Icons.help_outline_outlined), - title: Text(L10n.of(context)!.help), - onTap: () => launchUrlString(AppConfig.supportUrl), - ), - ListTile( - leading: const Icon(Icons.shield_sharp), - title: Text(L10n.of(context)!.privacy), - onTap: () => launchUrlString(AppConfig.privacyUrl), - ), - ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text(L10n.of(context)!.about), - onTap: () => PlatformInfos.showDialog(context), - ), - ], - ), + ), + body: ListTileTheme( + iconColor: Theme.of(context).colorScheme.onBackground, + child: ListView( + key: const Key('SettingsListViewContent'), + children: [ + FutureBuilder( + future: controller.profileFuture, + builder: (context, snapshot) { + final profile = snapshot.data; + final mxid = Matrix.of(context).client.userID ?? + L10n.of(context)!.user; + final displayname = + profile?.displayName ?? mxid.localpart ?? mxid; + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(32.0), + child: Stack( + children: [ + Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: + Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + Avatar.defaultSize * 2.5), + ), + child: Hero( + tag: 'profilesettings', + child: Avatar( + mxContent: profile?.avatarUrl, + name: displayname, + size: Avatar.defaultSize * 2.5, + fontSize: 18 * 2.5, + ), + ), + ), + if (profile != null) + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton.small( + onPressed: controller.setAvatarAction, + heroTag: null, + child: const Icon(Icons.camera_alt_outlined), + ), + ), + ], + ), + ), + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: controller.setDisplaynameAction, + icon: const Icon( + Icons.edit_outlined, + size: 18, + ), + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.onBackground, + ), + label: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 18), + ), + ), + ), + subtitle: Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: () {}, + icon: const Icon( + Icons.copy_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.secondary, + ), + label: Text( + mxid, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), + ), + ), + ), + ), + ], + ); + }), + const Divider(thickness: 1), + SwitchListTile.adaptive( + controlAffinity: ListTileControlAffinity.trailing, + value: controller.showChatBackupBanner == false, + secondary: const Icon(Icons.backup_outlined), + title: Text(L10n.of(context)!.chatBackup), + onChanged: controller.showChatBackupBanner != false + ? controller.firstRunBootstrapAction + : null, + ), + const Divider(thickness: 1), + ListTile( + leading: const Icon(Icons.format_paint_outlined), + title: Text(L10n.of(context)!.changeTheme), + onTap: () => VRouter.of(context).to('/settings/style'), + trailing: const Icon(Icons.chevron_right_outlined), + ), + ListTile( + leading: const Icon(Icons.notifications_outlined), + title: Text(L10n.of(context)!.notifications), + onTap: () => VRouter.of(context).to('/settings/notifications'), + trailing: const Icon(Icons.chevron_right_outlined), + ), + ListTile( + leading: const Icon(Icons.devices_outlined), + title: Text(L10n.of(context)!.devices), + onTap: () => VRouter.of(context).to('/settings/devices'), + trailing: const Icon(Icons.chevron_right_outlined), + ), + ListTile( + leading: const Icon(Icons.chat_bubble_outline_outlined), + title: Text(L10n.of(context)!.chat), + onTap: () => VRouter.of(context).to('/settings/chat'), + trailing: const Icon(Icons.chevron_right_outlined), + ), + ListTile( + leading: const Icon(Icons.shield_outlined), + title: Text(L10n.of(context)!.security), + onTap: () => VRouter.of(context).to('/settings/security'), + trailing: const Icon(Icons.chevron_right_outlined), + ), + const Divider(thickness: 1), + ListTile( + leading: const Icon(Icons.help_outline_outlined), + title: Text(L10n.of(context)!.help), + onTap: () => launchUrlString(AppConfig.supportUrl), + trailing: const Icon(Icons.open_in_new_outlined), + ), + ListTile( + leading: const Icon(Icons.shield_sharp), + title: Text(L10n.of(context)!.privacy), + onTap: () => launchUrlString(AppConfig.privacyUrl), + trailing: const Icon(Icons.open_in_new_outlined), + ), + ListTile( + leading: const Icon(Icons.info_outline_rounded), + title: Text(L10n.of(context)!.about), + onTap: () => PlatformInfos.showDialog(context), + trailing: const Icon(Icons.chevron_right_outlined), + ), + ], ), ), ); diff --git a/lib/pages/settings_account/settings_account.dart b/lib/pages/settings_account/settings_account.dart deleted file mode 100644 index 18d294e2..00000000 --- a/lib/pages/settings_account/settings_account.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:file_picker_cross/file_picker_cross.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:intl/intl.dart'; -import 'package:matrix/matrix.dart'; -import 'package:vrouter/vrouter.dart'; - -import 'package:fluffychat/pages/settings_account/settings_account_view.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class SettingsAccount extends StatefulWidget { - const SettingsAccount({Key? key}) : super(key: key); - - @override - SettingsAccountController createState() => SettingsAccountController(); -} - -class SettingsAccountController extends State { - Future? profileFuture; - Profile? profile; - bool profileUpdated = false; - - void updateProfile() => setState(() { - profileUpdated = true; - profile = profileFuture = null; - }); - - void setDisplaynameAction() async { - final input = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.editDisplayname, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [ - DialogTextField( - initialText: profile?.displayName ?? - Matrix.of(context).client.userID!.localpart, - ) - ], - ); - if (input == null) return; - final matrix = Matrix.of(context); - final success = await showFutureLoadingDialog( - context: context, - future: () => - matrix.client.setDisplayName(matrix.client.userID!, input.single), - ); - if (success.error == null) { - updateProfile(); - } - } - - void logoutAction() async { - if (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSureYouWantToLogout, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.cancel, - ) == - OkCancelResult.cancel) { - return; - } - final matrix = Matrix.of(context); - await showFutureLoadingDialog( - context: context, - future: () => matrix.client.logout(), - ); - } - - void deleteAccountAction() async { - if (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.warning, - message: L10n.of(context)!.deactivateAccountWarning, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - ) == - OkCancelResult.cancel) { - return; - } - final supposedMxid = Matrix.of(context).client.userID!; - final mxids = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.confirmMatrixId, - textFields: [ - DialogTextField( - validator: (text) => text == supposedMxid - ? null - : L10n.of(context)!.supposedMxid(supposedMxid), - ), - ], - okLabel: L10n.of(context)!.delete, - cancelLabel: L10n.of(context)!.cancel, - ); - if (mxids == null || mxids.length != 1 || mxids.single != supposedMxid) { - return; - } - final input = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.pleaseEnterYourPassword, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [ - const DialogTextField( - obscureText: true, - hintText: '******', - minLines: 1, - maxLines: 1, - ) - ], - ); - if (input == null) return; - await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context).client.deactivateAccount( - auth: AuthenticationPassword( - password: input.single, - identifier: AuthenticationUserIdentifier( - user: Matrix.of(context).client.userID!), - ), - ), - ); - } - - void addAccountAction() => VRouter.of(context).to('add'); - - @override - Widget build(BuildContext context) { - final client = Matrix.of(context).client; - profileFuture ??= client - .getProfileFromUserId( - client.userID!, - cache: !profileUpdated, - getFromRooms: !profileUpdated, - ) - .then((p) { - if (mounted) setState(() => profile = p); - return p; - }); - return SettingsAccountView(this); - } - - Future dehydrateAction() => dehydrateDevice(context); - - static Future dehydrateDevice(BuildContext context) async { - final response = await showOkCancelAlertDialog( - context: context, - isDestructiveAction: true, - title: L10n.of(context)!.dehydrate, - message: L10n.of(context)!.dehydrateWarning, - ); - if (response != OkCancelResult.ok) { - return; - } - await showFutureLoadingDialog( - context: context, - future: () async { - try { - final export = await Matrix.of(context).client.exportDump(); - final filePickerCross = FilePickerCross( - Uint8List.fromList(const Utf8Codec().encode(export!)), - path: - '/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup', - fileExtension: 'fluffybackup'); - await filePickerCross.exportToStorage( - subject: L10n.of(context)!.dehydrateShare, - ); - } catch (e, s) { - Logs().e('Export error', e, s); - } - }, - ); - } -} diff --git a/lib/pages/settings_account/settings_account_view.dart b/lib/pages/settings_account/settings_account_view.dart deleted file mode 100644 index bb8433af..00000000 --- a/lib/pages/settings_account/settings_account_view.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'settings_account.dart'; - -class SettingsAccountView extends StatelessWidget { - final SettingsAccountController controller; - const SettingsAccountView(this.controller, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(L10n.of(context)!.account)), - body: ListTileTheme( - iconColor: Theme.of(context).textTheme.bodyLarge!.color, - child: MaxWidthBody( - withScrolling: true, - child: Column( - children: [ - ListTile( - title: Text(L10n.of(context)!.yourUserId), - subtitle: Text(Matrix.of(context).client.userID!), - trailing: const Icon(Icons.copy_outlined), - onTap: () => FluffyShare.share( - Matrix.of(context).client.userID!, - context, - ), - ), - ListTile( - trailing: const Icon(Icons.edit_outlined), - title: Text(L10n.of(context)!.editDisplayname), - subtitle: Text(controller.profile?.displayName ?? - Matrix.of(context).client.userID!.localpart!), - onTap: controller.setDisplaynameAction, - ), - const Divider(height: 1), - ListTile( - trailing: const Icon(Icons.person_add_outlined), - title: Text(L10n.of(context)!.addAccount), - subtitle: Text(L10n.of(context)!.enableMultiAccounts), - onTap: controller.addAccountAction, - ), - ListTile( - trailing: const Icon(Icons.exit_to_app_outlined), - title: Text(L10n.of(context)!.logout), - onTap: controller.logoutAction, - ), - const Divider(height: 1), - ListTile( - trailing: const Icon(Icons.tap_and_play), - title: Text( - L10n.of(context)!.dehydrate, - style: const TextStyle(color: Colors.red), - ), - onTap: controller.dehydrateAction, - ), - ListTile( - trailing: const Icon(Icons.delete_outlined), - title: Text( - L10n.of(context)!.deleteAccount, - style: const TextStyle(color: Colors.red), - ), - onTap: controller.deleteAccountAction, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 40c867f1..265e534f 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -1,10 +1,16 @@ +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:flutter_app_lock/flutter_app_lock.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:intl/intl.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -93,12 +99,102 @@ class SettingsSecurityController extends State { } } + void deleteAccountAction() async { + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.warning, + message: L10n.of(context)!.deactivateAccountWarning, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + ) == + OkCancelResult.cancel) { + return; + } + final supposedMxid = Matrix.of(context).client.userID!; + final mxids = await showTextInputDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.confirmMatrixId, + textFields: [ + DialogTextField( + validator: (text) => text == supposedMxid + ? null + : L10n.of(context)!.supposedMxid(supposedMxid), + ), + ], + okLabel: L10n.of(context)!.delete, + cancelLabel: L10n.of(context)!.cancel, + ); + if (mxids == null || mxids.length != 1 || mxids.single != supposedMxid) { + return; + } + final input = await showTextInputDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.pleaseEnterYourPassword, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + textFields: [ + const DialogTextField( + obscureText: true, + hintText: '******', + minLines: 1, + maxLines: 1, + ) + ], + ); + if (input == null) return; + await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).client.deactivateAccount( + auth: AuthenticationPassword( + password: input.single, + identifier: AuthenticationUserIdentifier( + user: Matrix.of(context).client.userID!), + ), + ), + ); + } + void showBootstrapDialog(BuildContext context) async { await BootstrapDialog( client: Matrix.of(context).client, ).show(context); } + Future dehydrateAction() => dehydrateDevice(context); + + static Future dehydrateDevice(BuildContext context) async { + final response = await showOkCancelAlertDialog( + context: context, + isDestructiveAction: true, + title: L10n.of(context)!.dehydrate, + message: L10n.of(context)!.dehydrateWarning, + ); + if (response != OkCancelResult.ok) { + return; + } + await showFutureLoadingDialog( + context: context, + future: () async { + try { + final export = await Matrix.of(context).client.exportDump(); + final filePickerCross = FilePickerCross( + Uint8List.fromList(const Utf8Codec().encode(export!)), + path: + '/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup', + fileExtension: 'fluffybackup'); + await filePickerCross.exportToStorage( + subject: L10n.of(context)!.dehydrateShare, + ); + } catch (e, s) { + Logs().e('Export error', e, s); + } + }, + ); + } + @override Widget build(BuildContext context) => SettingsSecurityView(this); } diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index 77e5898d..539b9636 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -19,30 +19,34 @@ class SettingsSecurityView extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(L10n.of(context)!.security)), body: ListTileTheme( - iconColor: Theme.of(context).textTheme.bodyLarge!.color, + iconColor: Theme.of(context).colorScheme.onBackground, child: MaxWidthBody( withScrolling: true, child: Column( children: [ ListTile( - trailing: const Icon(Icons.panorama_fish_eye), + leading: const Icon(Icons.panorama_fish_eye), + trailing: const Icon(Icons.chevron_right_outlined), title: Text(L10n.of(context)!.whoCanSeeMyStories), onTap: () => VRouter.of(context).to('stories'), ), ListTile( - trailing: const Icon(Icons.close), + leading: const Icon(Icons.close), + trailing: const Icon(Icons.chevron_right_outlined), title: Text(L10n.of(context)!.ignoredUsers), onTap: () => VRouter.of(context).to('ignorelist'), ), ListTile( - trailing: const Icon(Icons.vpn_key_outlined), + leading: const Icon(Icons.password_outlined), + trailing: const Icon(Icons.chevron_right_outlined), title: Text( L10n.of(context)!.changePassword, ), onTap: controller.changePasswordAccountAction, ), ListTile( - trailing: const Icon(Icons.mail_outlined), + leading: const Icon(Icons.mail_outlined), + trailing: const Icon(Icons.chevron_right_outlined), title: Text(L10n.of(context)!.passwordRecovery), onTap: () => VRouter.of(context).to('3pid'), ), @@ -50,7 +54,8 @@ class SettingsSecurityView extends StatelessWidget { const Divider(thickness: 1), if (PlatformInfos.isMobile) ListTile( - trailing: const Icon(Icons.lock_outlined), + leading: const Icon(Icons.lock_outlined), + trailing: const Icon(Icons.chevron_right_outlined), title: Text(L10n.of(context)!.appLock), onTap: controller.setAppLockAction, ), @@ -64,48 +69,32 @@ class SettingsSecurityView extends StatelessWidget { Matrix.of(context).client.fingerprintKey.beautified, okLabel: L10n.of(context)!.ok, ), - trailing: const Icon(Icons.vpn_key_outlined), - ), - if (!Matrix.of(context).client.encryption!.crossSigning.enabled) - ListTile( - title: Text(L10n.of(context)!.crossSigningEnabled), - trailing: const Icon(Icons.error, color: Colors.red), - onTap: () => controller.showBootstrapDialog(context), + subtitle: Text( + Matrix.of(context).client.fingerprintKey.beautified, + style: const TextStyle(fontFamily: 'monospace'), ), - if (!Matrix.of(context).client.encryption!.keyManager.enabled) - ListTile( - title: Text(L10n.of(context)!.onlineKeyBackupEnabled), - trailing: const Icon(Icons.error, color: Colors.red), - onTap: () => controller.showBootstrapDialog(context), - ), - if (Matrix.of(context).client.isUnknownSession) - ListTile( - title: const Text('Session verified'), - trailing: const Icon(Icons.error, color: Colors.red), - onTap: () => controller.showBootstrapDialog(context), - ), - FutureBuilder( - future: () async { - return (await Matrix.of(context) - .client - .encryption! - .keyManager - .isCached()) && - (await Matrix.of(context) - .client - .encryption! - .crossSigning - .isCached()); - }(), - builder: (context, snapshot) => snapshot.data == true - ? Container() - : ListTile( - title: Text(L10n.of(context)!.keysCached), - trailing: const Icon(Icons.error, color: Colors.red), - onTap: () => controller.showBootstrapDialog(context), - ), + leading: const Icon(Icons.vpn_key_outlined), ), }, + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.tap_and_play), + trailing: const Icon(Icons.chevron_right_outlined), + title: Text( + L10n.of(context)!.dehydrate, + style: const TextStyle(color: Colors.red), + ), + onTap: controller.dehydrateAction, + ), + ListTile( + leading: const Icon(Icons.delete_outlined), + trailing: const Icon(Icons.chevron_right_outlined), + title: Text( + L10n.of(context)!.deleteAccount, + style: const TextStyle(color: Colors.red), + ), + onTap: controller.deleteAccountAction, + ), ], ), ),