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/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'));

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/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(

View File

@ -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<ChatList>
}
Future<void> dehydrate() =>
SettingsAccountController.dehydrateDevice(context);
SettingsSecurityController.dehydrateDevice(context);
}
enum EditBundleAction { addToBundle, removeFromBundle }

View File

@ -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');

View File

@ -22,16 +22,61 @@ class Settings extends StatefulWidget {
}
class SettingsController extends State<Settings> {
Future<dynamic>? profileFuture;
Profile? profile;
Future<Profile>? 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<Settings> {
}
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<Settings> {
@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);
}
}

View File

@ -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) =>
<Widget>[
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: <Widget>[
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: <Widget>[
FutureBuilder<Profile>(
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),
),
],
),
),
);

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: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<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 {
await BootstrapDialog(
client: Matrix.of(context).client,
).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
Widget build(BuildContext context) => SettingsSecurityView(this);
}

View File

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