style: New settings design

This commit is contained in:
Krille 2023-02-04 18:32:56 +01:00
parent 90482009fc
commit ede5fdc348
10 changed files with 401 additions and 458 deletions

View File

@ -2,7 +2,6 @@ import 'dart:developer';
import 'package:fluffychat/pages/chat_list/chat_list_body.dart'; import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -120,17 +119,6 @@ extension DefaultFlowExtensions on WidgetTester {
), ),
); );
await tester.pumpAndSettle(); 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.tap(find.text('Logout'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.maybeUppercaseText('Yes')); await tester.tap(find.maybeUppercaseText('Yes'));

View File

@ -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/new_space/new_space.dart';
import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pages/settings_3pid/settings_3pid.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_chat/settings_chat.dart';
import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart'; import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart';
import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart'; import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart';
@ -345,12 +344,7 @@ class AppRoutes {
], ],
), ),
VWidget( VWidget(
path: 'account', path: 'addaccount',
widget: const SettingsAccount(),
buildTransition: _dynamicTransition,
stackedRoutes: [
VWidget(
path: 'add',
widget: const HomeserverPicker(), widget: const HomeserverPicker(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
stackedRoutes: [ stackedRoutes: [
@ -377,8 +371,6 @@ class AppRoutes {
]), ]),
], ],
), ),
],
),
VWidget( VWidget(
path: 'security', path: 'security',
widget: const SettingsSecurity(), widget: const SettingsSecurity(),

View File

@ -15,6 +15,7 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.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/famedlysdk_store.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_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/fluffy_chat_app.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
import '../bootstrap/bootstrap_dialog.dart'; import '../bootstrap/bootstrap_dialog.dart';
import '../settings_account/settings_account.dart';
import 'package:fluffychat/utils/tor_stub.dart' import 'package:fluffychat/utils/tor_stub.dart'
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
@ -688,7 +688,7 @@ class ChatListController extends State<ChatList>
} }
Future<void> dehydrate() => Future<void> dehydrate() =>
SettingsAccountController.dehydrateDevice(context); SettingsSecurityController.dehydrateDevice(context);
} }
enum EditBundleAction { addToBundle, removeFromBundle } enum EditBundleAction { addToBundle, removeFromBundle }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
@ -212,6 +213,8 @@ class ClientChooserButton extends StatelessWidget {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
child: Hero(
tag: 'profilesettings',
child: Avatar( child: Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ?? name: snapshot.data?.displayName ??
@ -221,6 +224,7 @@ class ClientChooserButton extends StatelessWidget {
), ),
), ),
), ),
),
], ],
), ),
); );
@ -240,7 +244,7 @@ class ClientChooserButton extends StatelessWidget {
void _clientSelected( void _clientSelected(
Object object, Object object,
BuildContext context, BuildContext context,
) { ) async {
if (object is Client) { if (object is Client) {
controller.setActiveClient(object); controller.setActiveClient(object);
} else if (object is String) { } else if (object is String) {
@ -248,7 +252,15 @@ class ClientChooserButton extends StatelessWidget {
} else if (object is SettingsAction) { } else if (object is SettingsAction) {
switch (object) { switch (object) {
case SettingsAction.addAccount: 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; break;
case SettingsAction.newStory: case SettingsAction.newStory:
VRouter.of(context).to('/stories/create'); VRouter.of(context).to('/stories/create');

View File

@ -22,16 +22,61 @@ class Settings extends StatefulWidget {
} }
class SettingsController extends State<Settings> { class SettingsController extends State<Settings> {
Future<dynamic>? profileFuture; Future<Profile>? profileFuture;
Profile? profile;
bool profileUpdated = false; bool profileUpdated = false;
void updateProfile() => setState(() { void updateProfile() => setState(() {
profileUpdated = true; 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 { void setAvatarAction() async {
final profile = await profileFuture;
final actions = [ final actions = [
if (PlatformInfos.isMobile) if (PlatformInfos.isMobile)
SheetAction( SheetAction(
@ -131,9 +176,9 @@ class SettingsController extends State<Settings> {
} }
bool? crossSigningCached; bool? crossSigningCached;
bool showChatBackupBanner = false; bool? showChatBackupBanner;
void firstRunBootstrapAction() async { void firstRunBootstrapAction([_]) async {
await BootstrapDialog( await BootstrapDialog(
client: Matrix.of(context).client, client: Matrix.of(context).client,
).show(context); ).show(context);
@ -143,16 +188,11 @@ class SettingsController extends State<Settings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
profileFuture ??= client profileFuture ??= client.getProfileFromUserId(
.getProfileFromUserId(
client.userID!, client.userID!,
cache: !profileUpdated, cache: !profileUpdated,
getFromRooms: !profileUpdated, getFromRooms: !profileUpdated,
) );
.then((p) {
if (mounted) setState(() => profile = p);
return p;
});
return SettingsView(this); return SettingsView(this);
} }
} }

View File

@ -1,13 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:vrouter/vrouter.dart'; import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import '../../config/themes.dart'; import 'package:fluffychat/widgets/avatar.dart';
import '../../widgets/content_banner.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'settings.dart'; import 'settings.dart';
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {
@ -18,97 +19,183 @@ class SettingsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: NestedScrollView( appBar: AppBar(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) =>
<Widget>[
SliverAppBar(
expandedHeight: 300.0,
floating: true,
pinned: true,
title: Text(L10n.of(context)!.settings), title: Text(L10n.of(context)!.settings),
backgroundColor: Theme.of(context).appBarTheme.backgroundColor, actions: [
flexibleSpace: FlexibleSpaceBar( TextButton.icon(
background: ContentBanner( onPressed: controller.logoutAction,
mxContent: controller.profile?.avatarUrl, label: Text(L10n.of(context)!.logout),
onEdit: controller.setAvatarAction, icon: const Icon(Icons.logout_outlined),
defaultIcon: Icons.account_circle_outlined,
),
),
), ),
], ],
),
body: ListTileTheme( body: ListTileTheme(
iconColor: Theme.of(context).colorScheme.onBackground, iconColor: Theme.of(context).colorScheme.onBackground,
child: ListView( child: ListView(
key: const Key('SettingsListViewContent'), key: const Key('SettingsListViewContent'),
children: <Widget>[ children: <Widget>[
AnimatedContainer( FutureBuilder<Profile>(
height: controller.showChatBackupBanner ? 54 : 0, future: controller.profileFuture,
duration: FluffyThemes.animationDuration, builder: (context, snapshot) {
curve: FluffyThemes.animationCurve, final profile = snapshot.data;
clipBehavior: Clip.hardEdge, final mxid = Matrix.of(context).client.userID ??
decoration: const BoxDecoration(), 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( child: ListTile(
leading: const Icon(Icons.backup_outlined), contentPadding: EdgeInsets.zero,
title: Text(L10n.of(context)!.enableAutoBackups), title: Align(
trailing: const Icon( alignment: Alignment.centerLeft,
Icons.warning_outlined, child: TextButton.icon(
color: Colors.orange, onPressed: controller.setDisplaynameAction,
icon: const Icon(
Icons.edit_outlined,
size: 18,
), ),
onTap: controller.firstRunBootstrapAction, 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), const Divider(thickness: 1),
ListTile( ListTile(
leading: const Icon(Icons.format_paint_outlined), leading: const Icon(Icons.format_paint_outlined),
title: Text(L10n.of(context)!.changeTheme), title: Text(L10n.of(context)!.changeTheme),
onTap: () => VRouter.of(context).to('/settings/style'), onTap: () => VRouter.of(context).to('/settings/style'),
trailing: const Icon(Icons.chevron_right_outlined),
), ),
const Divider(thickness: 1),
ListTile( ListTile(
leading: const Icon(Icons.notifications_outlined), leading: const Icon(Icons.notifications_outlined),
title: Text(L10n.of(context)!.notifications), title: Text(L10n.of(context)!.notifications),
onTap: () => VRouter.of(context).to('/settings/notifications'), onTap: () => VRouter.of(context).to('/settings/notifications'),
trailing: const Icon(Icons.chevron_right_outlined),
), ),
ListTile( ListTile(
leading: const Icon(Icons.devices_outlined), leading: const Icon(Icons.devices_outlined),
title: Text(L10n.of(context)!.devices), title: Text(L10n.of(context)!.devices),
onTap: () => VRouter.of(context).to('/settings/devices'), onTap: () => VRouter.of(context).to('/settings/devices'),
trailing: const Icon(Icons.chevron_right_outlined),
), ),
ListTile( ListTile(
leading: const Icon(Icons.chat_bubble_outline_outlined), leading: const Icon(Icons.chat_bubble_outline_outlined),
title: Text(L10n.of(context)!.chat), title: Text(L10n.of(context)!.chat),
onTap: () => VRouter.of(context).to('/settings/chat'), onTap: () => VRouter.of(context).to('/settings/chat'),
), trailing: const Icon(Icons.chevron_right_outlined),
ListTile(
leading: const Icon(Icons.account_circle_outlined),
title: Text(L10n.of(context)!.account),
onTap: () => VRouter.of(context).to('/settings/account'),
), ),
ListTile( ListTile(
leading: const Icon(Icons.shield_outlined), leading: const Icon(Icons.shield_outlined),
title: Text(L10n.of(context)!.security), title: Text(L10n.of(context)!.security),
onTap: () => VRouter.of(context).to('/settings/security'), onTap: () => VRouter.of(context).to('/settings/security'),
trailing: const Icon(Icons.chevron_right_outlined),
), ),
const Divider(thickness: 1), const Divider(thickness: 1),
ListTile( ListTile(
leading: const Icon(Icons.help_outline_outlined), leading: const Icon(Icons.help_outline_outlined),
title: Text(L10n.of(context)!.help), title: Text(L10n.of(context)!.help),
onTap: () => launchUrlString(AppConfig.supportUrl), onTap: () => launchUrlString(AppConfig.supportUrl),
trailing: const Icon(Icons.open_in_new_outlined),
), ),
ListTile( ListTile(
leading: const Icon(Icons.shield_sharp), leading: const Icon(Icons.shield_sharp),
title: Text(L10n.of(context)!.privacy), title: Text(L10n.of(context)!.privacy),
onTap: () => launchUrlString(AppConfig.privacyUrl), onTap: () => launchUrlString(AppConfig.privacyUrl),
trailing: const Icon(Icons.open_in_new_outlined),
), ),
ListTile( ListTile(
leading: const Icon(Icons.info_outline_rounded), leading: const Icon(Icons.info_outline_rounded),
title: Text(L10n.of(context)!.about), title: Text(L10n.of(context)!.about),
onTap: () => PlatformInfos.showDialog(context), onTap: () => PlatformInfos.showDialog(context),
trailing: const Icon(Icons.chevron_right_outlined),
), ),
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -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<SettingsAccount> {
Future<dynamic>? 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<void> dehydrateAction() => dehydrateDevice(context);
static Future<void> 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);
}
},
);
}
}

View File

@ -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,
),
],
),
),
),
);
}
}

View File

@ -1,10 +1,16 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.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_app_lock/flutter_app_lock.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:future_loading_dialog/future_loading_dialog.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/config/setting_keys.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
@ -93,12 +99,102 @@ class SettingsSecurityController extends State<SettingsSecurity> {
} }
} }
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 { void showBootstrapDialog(BuildContext context) async {
await BootstrapDialog( await BootstrapDialog(
client: Matrix.of(context).client, client: Matrix.of(context).client,
).show(context); ).show(context);
} }
Future<void> dehydrateAction() => dehydrateDevice(context);
static Future<void> 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 @override
Widget build(BuildContext context) => SettingsSecurityView(this); Widget build(BuildContext context) => SettingsSecurityView(this);
} }

View File

@ -19,30 +19,34 @@ class SettingsSecurityView extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(L10n.of(context)!.security)), appBar: AppBar(title: Text(L10n.of(context)!.security)),
body: ListTileTheme( body: ListTileTheme(
iconColor: Theme.of(context).textTheme.bodyLarge!.color, iconColor: Theme.of(context).colorScheme.onBackground,
child: MaxWidthBody( child: MaxWidthBody(
withScrolling: true, withScrolling: true,
child: Column( child: Column(
children: [ children: [
ListTile( 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), title: Text(L10n.of(context)!.whoCanSeeMyStories),
onTap: () => VRouter.of(context).to('stories'), onTap: () => VRouter.of(context).to('stories'),
), ),
ListTile( ListTile(
trailing: const Icon(Icons.close), leading: const Icon(Icons.close),
trailing: const Icon(Icons.chevron_right_outlined),
title: Text(L10n.of(context)!.ignoredUsers), title: Text(L10n.of(context)!.ignoredUsers),
onTap: () => VRouter.of(context).to('ignorelist'), onTap: () => VRouter.of(context).to('ignorelist'),
), ),
ListTile( ListTile(
trailing: const Icon(Icons.vpn_key_outlined), leading: const Icon(Icons.password_outlined),
trailing: const Icon(Icons.chevron_right_outlined),
title: Text( title: Text(
L10n.of(context)!.changePassword, L10n.of(context)!.changePassword,
), ),
onTap: controller.changePasswordAccountAction, onTap: controller.changePasswordAccountAction,
), ),
ListTile( 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), title: Text(L10n.of(context)!.passwordRecovery),
onTap: () => VRouter.of(context).to('3pid'), onTap: () => VRouter.of(context).to('3pid'),
), ),
@ -50,7 +54,8 @@ class SettingsSecurityView extends StatelessWidget {
const Divider(thickness: 1), const Divider(thickness: 1),
if (PlatformInfos.isMobile) if (PlatformInfos.isMobile)
ListTile( 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), title: Text(L10n.of(context)!.appLock),
onTap: controller.setAppLockAction, onTap: controller.setAppLockAction,
), ),
@ -64,48 +69,32 @@ class SettingsSecurityView extends StatelessWidget {
Matrix.of(context).client.fingerprintKey.beautified, Matrix.of(context).client.fingerprintKey.beautified,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
), ),
trailing: const Icon(Icons.vpn_key_outlined), subtitle: Text(
), Matrix.of(context).client.fingerprintKey.beautified,
if (!Matrix.of(context).client.encryption!.crossSigning.enabled) style: const TextStyle(fontFamily: 'monospace'),
ListTile(
title: Text(L10n.of(context)!.crossSigningEnabled),
trailing: const Icon(Icons.error, color: Colors.red),
onTap: () => controller.showBootstrapDialog(context),
),
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,
),
], ],
), ),
), ),