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,38 +344,31 @@ class AppRoutes {
], ],
), ),
VWidget( VWidget(
path: 'account', path: 'addaccount',
widget: const SettingsAccount(), widget: const HomeserverPicker(),
buildTransition: _dynamicTransition, buildTransition: _fadeTransition,
stackedRoutes: [ stackedRoutes: [
VWidget( VWidget(
path: 'add', path: 'login',
widget: const HomeserverPicker(), widget: const Login(),
buildTransition: _fadeTransition, 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( VWidget(

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,12 +213,15 @@ class ClientChooserButton extends StatelessWidget {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
child: Avatar( child: Hero(
mxContent: snapshot.data?.avatarUrl, tag: 'profilesettings',
name: snapshot.data?.displayName ?? child: Avatar(
matrix.client.userID!.localpart, mxContent: snapshot.data?.avatarUrl,
size: 28, name: snapshot.data?.displayName ??
fontSize: 12, matrix.client.userID!.localpart,
size: 28,
fontSize: 12,
),
), ),
), ),
), ),
@ -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,95 +19,181 @@ 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) => title: Text(L10n.of(context)!.settings),
<Widget>[ actions: [
SliverAppBar( TextButton.icon(
expandedHeight: 300.0, onPressed: controller.logoutAction,
floating: true, label: Text(L10n.of(context)!.logout),
pinned: true, icon: const Icon(Icons.logout_outlined),
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,
),
),
), ),
], ],
body: ListTileTheme( ),
iconColor: Theme.of(context).colorScheme.onBackground, body: ListTileTheme(
child: ListView( iconColor: Theme.of(context).colorScheme.onBackground,
key: const Key('SettingsListViewContent'), child: ListView(
children: <Widget>[ key: const Key('SettingsListViewContent'),
AnimatedContainer( children: <Widget>[
height: controller.showChatBackupBanner ? 54 : 0, FutureBuilder<Profile>(
duration: FluffyThemes.animationDuration, future: controller.profileFuture,
curve: FluffyThemes.animationCurve, builder: (context, snapshot) {
clipBehavior: Clip.hardEdge, final profile = snapshot.data;
decoration: const BoxDecoration(), final mxid = Matrix.of(context).client.userID ??
child: ListTile( L10n.of(context)!.user;
leading: const Icon(Icons.backup_outlined), final displayname =
title: Text(L10n.of(context)!.enableAutoBackups), profile?.displayName ?? mxid.localpart ?? mxid;
trailing: const Icon( return Row(
Icons.warning_outlined, children: [
color: Colors.orange, Padding(
), padding: const EdgeInsets.all(32.0),
onTap: controller.firstRunBootstrapAction, child: Stack(
), children: [
), Material(
const Divider(thickness: 1), elevation: Theme.of(context)
ListTile( .appBarTheme
leading: const Icon(Icons.format_paint_outlined), .scrolledUnderElevation ??
title: Text(L10n.of(context)!.changeTheme), 4,
onTap: () => VRouter.of(context).to('/settings/style'), shadowColor:
), Theme.of(context).appBarTheme.shadowColor,
const Divider(thickness: 1), shape: RoundedRectangleBorder(
ListTile( side: BorderSide(
leading: const Icon(Icons.notifications_outlined), color: Theme.of(context).dividerColor,
title: Text(L10n.of(context)!.notifications), ),
onTap: () => VRouter.of(context).to('/settings/notifications'), borderRadius: BorderRadius.circular(
), Avatar.defaultSize * 2.5),
ListTile( ),
leading: const Icon(Icons.devices_outlined), child: Hero(
title: Text(L10n.of(context)!.devices), tag: 'profilesettings',
onTap: () => VRouter.of(context).to('/settings/devices'), child: Avatar(
), mxContent: profile?.avatarUrl,
ListTile( name: displayname,
leading: const Icon(Icons.chat_bubble_outline_outlined), size: Avatar.defaultSize * 2.5,
title: Text(L10n.of(context)!.chat), fontSize: 18 * 2.5,
onTap: () => VRouter.of(context).to('/settings/chat'), ),
), ),
ListTile( ),
leading: const Icon(Icons.account_circle_outlined), if (profile != null)
title: Text(L10n.of(context)!.account), Positioned(
onTap: () => VRouter.of(context).to('/settings/account'), bottom: 0,
), right: 0,
ListTile( child: FloatingActionButton.small(
leading: const Icon(Icons.shield_outlined), onPressed: controller.setAvatarAction,
title: Text(L10n.of(context)!.security), heroTag: null,
onTap: () => VRouter.of(context).to('/settings/security'), child: const Icon(Icons.camera_alt_outlined),
), ),
const Divider(thickness: 1), ),
ListTile( ],
leading: const Icon(Icons.help_outline_outlined), ),
title: Text(L10n.of(context)!.help), ),
onTap: () => launchUrlString(AppConfig.supportUrl), Expanded(
), child: ListTile(
ListTile( contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.shield_sharp), title: Align(
title: Text(L10n.of(context)!.privacy), alignment: Alignment.centerLeft,
onTap: () => launchUrlString(AppConfig.privacyUrl), child: TextButton.icon(
), onPressed: controller.setDisplaynameAction,
ListTile( icon: const Icon(
leading: const Icon(Icons.info_outline_rounded), Icons.edit_outlined,
title: Text(L10n.of(context)!.about), size: 18,
onTap: () => PlatformInfos.showDialog(context), ),
), 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),
),
],
), ),
), ),
); );

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) leading: const Icon(Icons.vpn_key_outlined),
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),
),
), ),
}, },
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,
),
], ],
), ),
), ),