From f956476bf35b502938e7d7e251eaca316f66f628 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 14 Oct 2020 04:21:28 +0300 Subject: [PATCH] Settings redesign (#4) * Stashed settings redesign work * Move settings views to the separate folder * Move theme settings * Finish moving around the settings * Fix settings navigation on wide screens * Give visual hints of selected settings view --- lib/components/settings_themes.dart | 85 --- lib/l10n/intl_en.arb | 15 + lib/views/chat_details.dart | 6 +- lib/views/settings.dart | 622 ++++-------------- lib/views/{ => settings}/app_info.dart | 4 +- lib/views/settings/settings_account.dart | 226 +++++++ lib/views/settings/settings_chat.dart | 49 ++ .../{ => settings}/settings_devices.dart | 12 +- lib/views/{ => settings}/settings_emotes.dart | 10 +- lib/views/settings/settings_encryption.dart | 234 +++++++ lib/views/settings/settings_homeserver.dart | 44 ++ .../{ => settings}/settings_ignore_list.dart | 6 +- .../settings_multiple_emotes.dart | 8 +- lib/views/settings/settings_themes.dart | 155 +++++ 14 files changed, 875 insertions(+), 601 deletions(-) delete mode 100644 lib/components/settings_themes.dart rename lib/views/{ => settings}/app_info.dart (96%) create mode 100644 lib/views/settings/settings_account.dart create mode 100644 lib/views/settings/settings_chat.dart rename lib/views/{ => settings}/settings_devices.dart (94%) rename lib/views/{ => settings}/settings_emotes.dart (98%) create mode 100644 lib/views/settings/settings_encryption.dart create mode 100644 lib/views/settings/settings_homeserver.dart rename lib/views/{ => settings}/settings_ignore_list.dart (95%) rename lib/views/{ => settings}/settings_multiple_emotes.dart (90%) create mode 100644 lib/views/settings/settings_themes.dart diff --git a/lib/components/settings_themes.dart b/lib/components/settings_themes.dart deleted file mode 100644 index 6837a43..0000000 --- a/lib/components/settings_themes.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../components/matrix.dart'; -import '../components/theme_switcher.dart'; - -class ThemesSettings extends StatefulWidget { - @override - ThemesSettingsState createState() => ThemesSettingsState(); -} - -class ThemesSettingsState extends State { - Themes _selectedTheme; - bool _amoledEnabled; - - @override - Widget build(BuildContext context) { - final matrix = Matrix.of(context); - final themeEngine = ThemeSwitcherWidget.of(context); - _selectedTheme = themeEngine.selectedTheme; - _amoledEnabled = themeEngine.amoledEnabled; - - return Column( - children: [ - RadioListTile( - title: Text( - L10n.of(context).systemTheme, - ), - value: Themes.system, - groupValue: _selectedTheme, - activeColor: Theme.of(context).primaryColor, - onChanged: (Themes value) { - setState(() { - _selectedTheme = value; - themeEngine.switchTheme(matrix, value, _amoledEnabled); - }); - }, - ), - RadioListTile( - title: Text( - L10n.of(context).lightTheme, - ), - value: Themes.light, - groupValue: _selectedTheme, - activeColor: Theme.of(context).primaryColor, - onChanged: (Themes value) { - setState(() { - _selectedTheme = value; - themeEngine.switchTheme(matrix, value, _amoledEnabled); - }); - }, - ), - RadioListTile( - title: Text( - L10n.of(context).darkTheme, - ), - value: Themes.dark, - groupValue: _selectedTheme, - activeColor: Theme.of(context).primaryColor, - onChanged: (Themes value) { - setState(() { - _selectedTheme = value; - themeEngine.switchTheme(matrix, value, _amoledEnabled); - }); - }, - ), - ListTile( - title: Text( - L10n.of(context).useAmoledTheme, - ), - trailing: Switch( - value: _amoledEnabled, - activeColor: Theme.of(context).primaryColor, - onChanged: (bool value) { - setState(() { - _amoledEnabled = value; - themeEngine.switchTheme(matrix, _selectedTheme, value); - }); - }, - ), - ), - ], - ); - } -} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6172189..588e861 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -114,6 +114,11 @@ "type": "text", "placeholders": {} }, + "avatar": "Avatar", + "@avatar": { + "type": "text", + "placeholders": {} + }, "avatarHasBeenChanged": "Avatar has been changed", "@avatarHasBeenChanged": { "type": "text", @@ -207,6 +212,11 @@ "type": "text", "placeholders": {} }, + "changeThePassword": "Change the password", + "@changeThePassword": { + "type": "text", + "placeholders": {} + }, "changedTheGuestAccessRules": "{username} changed the guest access rules", "@changedTheGuestAccessRules": { "type": "text", @@ -681,6 +691,11 @@ "type": "text", "placeholders": {} }, + "homeserver": "Homeserver", + "@homeserver": { + "type": "text", + "placeholders": {} + }, "homeserverIsNotCompatible": "Homeserver is not compatible", "@homeserverIsNotCompatible": { "type": "text", diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index c6bccd7..b5d396e 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -20,9 +20,9 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix_link_text/link_text.dart'; -import './settings_emotes.dart'; -import './settings_multiple_emotes.dart'; -import '../utils/url_launcher.dart'; +import 'package:furrychat/views/settings/settings_emotes.dart'; +import 'package:furrychat/views/settings/settings_multiple_emotes.dart'; +import 'package:furrychat/utils/url_launcher.dart'; class ChatDetails extends StatefulWidget { final Room room; diff --git a/lib/views/settings.dart b/lib/views/settings.dart index d7df0f4..4e5db81 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -1,41 +1,53 @@ -import 'dart:io'; - -import 'package:bot_toast/bot_toast.dart'; -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:file_picker_cross/file_picker_cross.dart'; - -import 'package:furrychat/components/settings_themes.dart'; import 'package:furrychat/config/app_config.dart'; -import 'package:furrychat/utils/platform_infos.dart'; -import 'package:furrychat/views/settings_devices.dart'; -import 'package:furrychat/views/settings_ignore_list.dart'; -import 'package:flutter/foundation.dart'; +import 'package:furrychat/views/settings/settings_account.dart'; +import 'package:furrychat/views/settings/settings_chat.dart'; +import 'package:furrychat/views/settings/settings_devices.dart'; +import 'package:furrychat/views/settings/settings_encryption.dart'; +import 'package:furrychat/views/settings/settings_homeserver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:furrychat/views/settings/settings_themes.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../components/adaptive_page_layout.dart'; -import '../components/content_banner.dart'; -import '../components/dialogs/simple_dialogs.dart'; -import '../components/matrix.dart'; -import '../utils/app_route.dart'; -import 'app_info.dart'; -import 'chat_list.dart'; -import 'settings_emotes.dart'; +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:furrychat/utils/app_route.dart'; +import 'package:furrychat/views/settings/settings_emotes.dart'; + +enum SettingsViews { + account, + homeserver, + themes, + chat, + emotes, + encryption, + devices, +} class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { return AdaptivePageLayout( - primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(), - secondScaffold: Settings(), + primaryPage: FocusPage.FIRST, + firstScaffold: Settings( + isInFocus: true, + ), + secondScaffold: Scaffold( + body: Center( + child: Image.asset('assets/logo.png', width: 100, height: 100), + ), + ), ); } } class Settings extends StatefulWidget { + final bool isInFocus; + final SettingsViews currentSetting; + + const Settings({this.isInFocus = false, this.currentSetting, Key key}) + : super(key: key); + @override _SettingsState createState() => _SettingsState(); } @@ -43,196 +55,21 @@ class Settings extends StatefulWidget { class _SettingsState extends State { Future profileFuture; dynamic profile; - Future crossSigningCachedFuture; - bool crossSigningCached; - Future megolmBackupCachedFuture; - bool megolmBackupCached; - void logoutAction(BuildContext context) async { - if (await SimpleDialogs(context).askConfirmation() == false) { - return; - } - var matrix = Matrix.of(context); - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(matrix.client.logout()); - } - - void _changePasswordAccountAction(BuildContext context) async { - final oldPassword = await SimpleDialogs(context).enterText( - password: true, - titleText: L10n.of(context).pleaseEnterYourPassword, - ); - if (oldPassword == null) return; - final newPassword = await SimpleDialogs(context).enterText( - password: true, - titleText: L10n.of(context).chooseAStrongPassword, - ); - if (newPassword == null) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context) - .client - .changePassword(newPassword, oldPassword: oldPassword), - ); - BotToast.showText(text: L10n.of(context).passwordHasBeenChanged); - } - - void _deleteAccountAction(BuildContext context) async { - if (await SimpleDialogs(context).askConfirmation( - titleText: L10n.of(context).warning, - contentText: L10n.of(context).deactivateAccountWarning, - dangerous: true, - ) == - false) { - return; - } - if (await SimpleDialogs(context).askConfirmation(dangerous: true) == - false) { - return; - } - final password = await SimpleDialogs(context).enterText( - password: true, - titleText: L10n.of(context).pleaseEnterYourPassword, - ); - if (password == null) return; - await SimpleDialogs(context).tryRequestWithLoadingDialog( - Matrix.of(context).client.deactivateAccount(auth: { - 'type': 'm.login.password', - 'user': Matrix.of(context).client.userID, - 'password': password, - }), - ); - } - - void setJitsiInstanceAction(BuildContext context) async { - var jitsi = await SimpleDialogs(context).enterText( - titleText: L10n.of(context).editJitsiInstance, - hintText: Matrix.of(context).jitsiInstance, - labelText: L10n.of(context).editJitsiInstance, - ); - if (jitsi == null) return; - if (!jitsi.endsWith('/')) { - jitsi += '/'; - } - final matrix = Matrix.of(context); - await matrix.store.setItem('chat.fluffy.jitsi_instance', jitsi); - matrix.jitsiInstance = jitsi; - } - - void setDisplaynameAction(BuildContext context) async { - final displayname = await SimpleDialogs(context).enterText( - titleText: L10n.of(context).editDisplayname, - hintText: - profile?.displayname ?? Matrix.of(context).client.userID.localpart, - labelText: L10n.of(context).enterAUsername, - ); - if (displayname == null) return; - final matrix = Matrix.of(context); - final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( - matrix.client.setDisplayname(matrix.client.userID, displayname), - ); - if (success != false) { - setState(() { - profileFuture = null; - profile = null; - }); - } - } - - void setAvatarAction(BuildContext context) async { - MatrixFile file; - if (PlatformInfos.isMobile) { - final result = await ImagePicker().getImage( - source: ImageSource.gallery, - imageQuality: 50, - maxWidth: 1600, - maxHeight: 1600); - if (result == null) return; - file = MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); - } else { - final result = - await FilePickerCross.importFromStorage(type: FileTypeCross.image); - if (result == null) return; - file = MatrixFile( - bytes: result.toUint8List(), - name: result.fileName, - ); - } - final matrix = Matrix.of(context); - final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( - matrix.client.setAvatar(file), - ); - if (success != false) { - setState(() { - profileFuture = null; - profile = null; - }); - } - } - - void setWallpaperAction(BuildContext context) async { - final wallpaper = await ImagePicker().getImage(source: ImageSource.gallery); - if (wallpaper == null) return; - Matrix.of(context).wallpaper = File(wallpaper.path); - await Matrix.of(context) - .store - .setItem('chat.fluffy.wallpaper', wallpaper.path); - setState(() => null); - } - - void deleteWallpaperAction(BuildContext context) async { - Matrix.of(context).wallpaper = null; - await Matrix.of(context).store.setItem('chat.fluffy.wallpaper', null); - setState(() => null); - } - - Future requestSSSSCache(BuildContext context) async { - final handle = Matrix.of(context).client.encryption.ssss.open(); - final str = await SimpleDialogs(context).enterText( - titleText: L10n.of(context).askSSSSCache, - hintText: L10n.of(context).passphraseOrKey, - password: true, - ); - if (str != null) { - SimpleDialogs(context).showLoadingDialog(context); - // make sure the loading spinner shows before we test the keys - await Future.delayed(Duration(milliseconds: 100)); - var valid = false; - try { - handle.unlock(recoveryKey: str); - valid = true; - } catch (e, s) { - debugPrint('Couldn\'t use recovery key: ' + e.toString()); - debugPrint(s.toString()); - try { - handle.unlock(passphrase: str); - valid = true; - } catch (e, s) { - debugPrint('Couldn\'t use recovery passphrase: ' + e.toString()); - debugPrint(s.toString()); - valid = false; - } - } - await Navigator.of(context)?.pop(); - if (valid) { - await handle.maybeCacheAll(); - await SimpleDialogs(context).inform( - contentText: L10n.of(context).cachedKeys, - ); - setState(() { - crossSigningCachedFuture = null; - crossSigningCached = null; - megolmBackupCachedFuture = null; - megolmBackupCached = null; - }); - } else { - await SimpleDialogs(context).inform( - contentText: L10n.of(context).incorrectPassphraseOrKey, - ); - } - } + void _handleTap(Widget child) { + widget.isInFocus + ? Navigator.of(context).push( + AppRoute.defaultRoute( + context, + child, + ), + ) + : Navigator.of(context).pushReplacement( + AppRoute.defaultRoute( + context, + child, + ), + ); } @override @@ -242,323 +79,120 @@ class _SettingsState extends State { if (mounted) setState(() => profile = p); return p; }); - crossSigningCachedFuture ??= - client.encryption.crossSigning.isCached().then((c) { - if (mounted) setState(() => crossSigningCached = c); - return c; - }); - megolmBackupCachedFuture ??= - client.encryption.keyManager.isCached().then((c) { - if (mounted) setState(() => megolmBackupCached = c); - return c; - }); + return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => [ SliverAppBar( - expandedHeight: 300.0, + leading: !widget.isInFocus + ? IconButton( + icon: Icon(Icons.close_outlined), + onPressed: () => + {Navigator.of(context).popUntil((r) => r.isFirst)}, + ) + : null, floating: true, pinned: true, backgroundColor: Theme.of(context).appBarTheme.color, - flexibleSpace: FlexibleSpaceBar( - title: Text( - L10n.of(context).settings, - style: TextStyle( - color: Theme.of(context) - .appBarTheme - .textTheme - .headline6 - .color), - ), - background: ContentBanner( - profile?.avatarUrl, - height: 300, - defaultIcon: Icons.account_circle, - loading: profile == null, - onEdit: () => setAvatarAction(context), - ), - ), + title: Text(L10n.of(context).settings), ), ], body: ListView( children: [ ListTile( - title: Text( - L10n.of(context).changeTheme, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ThemesSettings(), - if (!kIsWeb && Matrix.of(context).store != null) - Divider(thickness: 1), - if (!kIsWeb && Matrix.of(context).store != null) - ListTile( - title: Text( - L10n.of(context).wallpaper, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - if (Matrix.of(context).wallpaper != null) - ListTile( - title: Image.file( - Matrix.of(context).wallpaper, - height: 38, - fit: BoxFit.cover, - ), - trailing: Icon( - Icons.delete_forever, - color: Colors.red, - ), - onTap: () => deleteWallpaperAction(context), - ), - if (!kIsWeb && Matrix.of(context).store != null) - Builder(builder: (context) { - return ListTile( - title: Text(L10n.of(context).changeWallpaper), - trailing: Icon(Icons.wallpaper), - onTap: () => setWallpaperAction(context), - ); - }), - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).chat, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), + leading: Icon( + Icons.person_outlined, ), + title: Text(profile?.displayname ?? L10n.of(context).account), + selected: + widget.currentSetting == SettingsViews.account ? true : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + subtitle: Text(client.userID), + onTap: () => _handleTap(AccountSettingsView()), ), ListTile( - title: Text(L10n.of(context).renderRichContent), - trailing: Switch( - value: Matrix.of(context).renderHtml, - activeColor: Theme.of(context).primaryColor, - onChanged: (bool newValue) async { - Matrix.of(context).renderHtml = newValue; - await Matrix.of(context) - .store - .setItem('chat.fluffy.renderHtml', newValue ? '1' : '0'); - setState(() => null); - }, + leading: Icon( + Icons.dns_outlined, ), + title: Text(L10n.of(context).homeserver), + selected: widget.currentSetting == SettingsViews.homeserver + ? true + : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + subtitle: Text(client.homeserver.host), + onTap: () => _handleTap(HomeserverSettingsView()), ), ListTile( + leading: Icon( + Icons.color_lens_outlined, + ), + title: Text(L10n.of(context).changeTheme), + selected: + widget.currentSetting == SettingsViews.themes ? true : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + onTap: () => _handleTap(ThemesSettingsView()), + ), + ListTile( + leading: Icon( + Icons.chat_outlined, + ), + title: Text(L10n.of(context).chat), + selected: + widget.currentSetting == SettingsViews.chat ? true : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + onTap: () => _handleTap(ChatSettingsView()), + ), + ListTile( + leading: Icon( + Icons.insert_emoticon_outlined, + ), title: Text(L10n.of(context).emoteSettings), - onTap: () async => await Navigator.of(context).push( - AppRoute.defaultRoute( - context, - EmotesSettingsView(), - ), + selected: + widget.currentSetting == SettingsViews.emotes ? true : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + onTap: () => _handleTap(EmotesSettingsView()), + ), + ListTile( + leading: Icon( + Icons.lock_outline, ), - trailing: Icon(Icons.insert_emoticon), + title: Text(L10n.of(context).encryption), + selected: widget.currentSetting == SettingsViews.encryption + ? true + : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + onTap: () => _handleTap(EncryptionSettingsView()), ), - Divider(thickness: 1), ListTile( - title: Text( - L10n.of(context).account, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), + leading: Icon( + Icons.devices_other_outlined, ), - ), - ListTile( - trailing: Icon(Icons.edit), - title: Text(L10n.of(context).editDisplayname), - subtitle: Text(profile?.displayname ?? client.userID.localpart), - onTap: () => setDisplaynameAction(context), - ), - ListTile( - trailing: Icon(Icons.phone), - title: Text(L10n.of(context).editJitsiInstance), - subtitle: Text(Matrix.of(context).jitsiInstance), - onTap: () => setJitsiInstanceAction(context), - ), - ListTile( - trailing: Icon(Icons.devices_other), title: Text(L10n.of(context).devices), - onTap: () async => await Navigator.of(context).push( - AppRoute.defaultRoute( - context, - DevicesSettingsView(), - ), - ), - ), - ListTile( - trailing: Icon(Icons.block), - title: Text(L10n.of(context).ignoredUsers), - onTap: () async => await Navigator.of(context).push( - AppRoute.defaultRoute( - context, - SettingsIgnoreListView(), - ), - ), - ), - ListTile( - trailing: Icon(Icons.account_circle), - title: Text(L10n.of(context).accountInformation), - onTap: () => Navigator.of(context).push( - AppRoute.defaultRoute( - context, - AppInfoView(), - ), - ), + selected: + widget.currentSetting == SettingsViews.devices ? true : false, + selectedTileColor: Theme.of(context).primaryColor.withAlpha(30), + onTap: () => _handleTap(DevicesSettingsView()), ), Divider(thickness: 1), ListTile( - trailing: Icon(Icons.vpn_key), - title: Text( - 'Change password', + leading: Icon( + Icons.help_outline_outlined, ), - onTap: () => _changePasswordAccountAction(context), - ), - ListTile( - trailing: Icon(Icons.exit_to_app), - title: Text(L10n.of(context).logout), - onTap: () => logoutAction(context), - ), - ListTile( - trailing: Icon(Icons.delete_forever), - title: Text( - L10n.of(context).deleteAccount, - style: TextStyle(color: Colors.red), - ), - onTap: () => _deleteAccountAction(context), - ), - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).encryption, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - trailing: Icon(Icons.compare_arrows), - title: Text(client.encryption.crossSigning.enabled - ? L10n.of(context).crossSigningEnabled - : L10n.of(context).crossSigningDisabled), - subtitle: client.encryption.crossSigning.enabled - ? Text(client.isUnknownSession - ? L10n.of(context).unknownSessionVerify - : L10n.of(context).sessionVerified + - ', ' + - (crossSigningCached == null - ? '⌛' - : (crossSigningCached - ? L10n.of(context).keysCached - : L10n.of(context).keysMissing))) - : null, - onTap: () async { - if (!client.encryption.crossSigning.enabled) { - await SimpleDialogs(context).inform( - contentText: L10n.of(context).noCrossSignBootstrap, - ); - return; - } - if (client.isUnknownSession) { - final str = await SimpleDialogs(context).enterText( - titleText: L10n.of(context).askSSSSVerify, - hintText: L10n.of(context).passphraseOrKey, - password: true, - ); - if (str != null) { - SimpleDialogs(context).showLoadingDialog(context); - // make sure the loading spinner shows before we test the keys - await Future.delayed(Duration(milliseconds: 100)); - var valid = false; - try { - await client.encryption.crossSigning - .selfSign(recoveryKey: str); - valid = true; - } catch (_) { - try { - await client.encryption.crossSigning - .selfSign(passphrase: str); - valid = true; - } catch (_) { - valid = false; - } - } - await Navigator.of(context)?.pop(); - if (valid) { - await SimpleDialogs(context).inform( - contentText: L10n.of(context).verifiedSession, - ); - setState(() { - crossSigningCachedFuture = null; - crossSigningCached = null; - megolmBackupCachedFuture = null; - megolmBackupCached = null; - }); - } else { - await SimpleDialogs(context).inform( - contentText: L10n.of(context).incorrectPassphraseOrKey, - ); - } - } - } - if (!(await client.encryption.crossSigning.isCached())) { - await requestSSSSCache(context); - } - }, - ), - ListTile( - trailing: Icon(Icons.wb_cloudy), - title: Text(client.encryption.keyManager.enabled - ? L10n.of(context).onlineKeyBackupEnabled - : L10n.of(context).onlineKeyBackupDisabled), - subtitle: client.encryption.keyManager.enabled - ? Text(megolmBackupCached == null - ? '⌛' - : (megolmBackupCached - ? L10n.of(context).keysCached - : L10n.of(context).keysMissing)) - : null, - onTap: () async { - if (!client.encryption.keyManager.enabled) { - await SimpleDialogs(context).inform( - contentText: L10n.of(context).noMegolmBootstrap, - ); - return; - } - if (!(await client.encryption.keyManager.isCached())) { - await requestSSSSCache(context); - } - }, - ), - Divider(thickness: 1), - ListTile( - title: Text( - L10n.of(context).about, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - trailing: Icon(Icons.help), title: Text(L10n.of(context).help), onTap: () => launch(AppConfig.supportUrl), ), ListTile( - trailing: Icon(Icons.privacy_tip_rounded), + leading: Icon( + Icons.privacy_tip_outlined, + ), title: Text(L10n.of(context).privacy), onTap: () => launch(AppConfig.privacyUrl), ), ListTile( - trailing: Icon(Icons.link), + leading: Icon( + Icons.link_outlined, + ), title: Text(L10n.of(context).license), onTap: () => showLicensePage( context: context, @@ -568,7 +202,9 @@ class _SettingsState extends State { ), ), ListTile( - trailing: Icon(Icons.code), + leading: Icon( + Icons.code_outlined, + ), title: Text(L10n.of(context).sourceCode), onTap: () => launch(AppConfig.sourceCodeUrl), ), diff --git a/lib/views/app_info.dart b/lib/views/settings/app_info.dart similarity index 96% rename from lib/views/app_info.dart rename to lib/views/settings/app_info.dart index 9772c72..d10d441 100644 --- a/lib/views/app_info.dart +++ b/lib/views/settings/app_info.dart @@ -1,7 +1,7 @@ import 'package:furrychat/components/adaptive_page_layout.dart'; import 'package:furrychat/components/matrix.dart'; import 'package:furrychat/utils/beautify_string_extension.dart'; -import 'package:furrychat/views/chat_list.dart'; +import 'package:furrychat/views/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:olm/olm.dart' as olm; @@ -11,7 +11,7 @@ class AppInfoView extends StatelessWidget { Widget build(BuildContext context) { return AdaptivePageLayout( primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(), + firstScaffold: Settings(), secondScaffold: AppInfo(), ); } diff --git a/lib/views/settings/settings_account.dart b/lib/views/settings/settings_account.dart new file mode 100644 index 0000000..4cb251f --- /dev/null +++ b/lib/views/settings/settings_account.dart @@ -0,0 +1,226 @@ +import 'package:bot_toast/bot_toast.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:file_picker_cross/file_picker_cross.dart'; +import 'package:furrychat/components/avatar.dart'; +import 'package:furrychat/utils/app_route.dart'; +import 'package:furrychat/utils/platform_infos.dart'; +import 'package:furrychat/components/dialogs/simple_dialogs.dart'; +import 'package:furrychat/views/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:furrychat/views/settings/settings_ignore_list.dart'; +import 'package:image_picker/image_picker.dart'; + +class AccountSettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: Settings(currentSetting: SettingsViews.account), + secondScaffold: AccountSettings(), + ); + } +} + +class AccountSettings extends StatefulWidget { + @override + _AccountSettingsState createState() => _AccountSettingsState(); +} + +class _AccountSettingsState extends State { + Future profileFuture; + dynamic profile; + + void logoutAction(BuildContext context) async { + if (await SimpleDialogs(context).askConfirmation() == false) { + return; + } + var matrix = Matrix.of(context); + await SimpleDialogs(context) + .tryRequestWithLoadingDialog(matrix.client.logout()); + } + + void _changePasswordAccountAction(BuildContext context) async { + final oldPassword = await SimpleDialogs(context).enterText( + password: true, + titleText: L10n.of(context).pleaseEnterYourPassword, + ); + if (oldPassword == null) return; + final newPassword = await SimpleDialogs(context).enterText( + password: true, + titleText: L10n.of(context).chooseAStrongPassword, + ); + if (newPassword == null) return; + await SimpleDialogs(context).tryRequestWithLoadingDialog( + Matrix.of(context) + .client + .changePassword(newPassword, oldPassword: oldPassword), + ); + BotToast.showText(text: L10n.of(context).passwordHasBeenChanged); + } + + void _deleteAccountAction(BuildContext context) async { + if (await SimpleDialogs(context).askConfirmation( + titleText: L10n.of(context).warning, + contentText: L10n.of(context).deactivateAccountWarning, + dangerous: true, + ) == + false) { + return; + } + if (await SimpleDialogs(context).askConfirmation(dangerous: true) == + false) { + return; + } + final password = await SimpleDialogs(context).enterText( + password: true, + titleText: L10n.of(context).pleaseEnterYourPassword, + ); + if (password == null) return; + await SimpleDialogs(context).tryRequestWithLoadingDialog( + Matrix.of(context).client.deactivateAccount(auth: { + 'type': 'm.login.password', + 'user': Matrix.of(context).client.userID, + 'password': password, + }), + ); + } + + void setJitsiInstanceAction(BuildContext context) async { + var jitsi = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).editJitsiInstance, + hintText: Matrix.of(context).jitsiInstance, + labelText: L10n.of(context).editJitsiInstance, + ); + if (jitsi == null) return; + if (!jitsi.endsWith('/')) { + jitsi += '/'; + } + final matrix = Matrix.of(context); + await matrix.store.setItem('chat.fluffy.jitsi_instance', jitsi); + matrix.jitsiInstance = jitsi; + } + + void setDisplaynameAction(BuildContext context) async { + final displayname = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).editDisplayname, + hintText: + profile?.displayname ?? Matrix.of(context).client.userID.localpart, + labelText: L10n.of(context).enterAUsername, + ); + if (displayname == null) return; + final matrix = Matrix.of(context); + final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( + matrix.client.setDisplayname(matrix.client.userID, displayname), + ); + if (success != false) { + setState(() { + profileFuture = null; + profile = null; + }); + } + } + + void setAvatarAction(BuildContext context) async { + MatrixFile file; + if (PlatformInfos.isMobile) { + final result = await ImagePicker().getImage( + source: ImageSource.gallery, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (result == null) return; + file = MatrixFile( + bytes: await result.readAsBytes(), + name: result.path, + ); + } else { + final result = + await FilePickerCross.importFromStorage(type: FileTypeCross.image); + if (result == null) return; + file = MatrixFile( + bytes: result.toUint8List(), + name: result.fileName, + ); + } + final matrix = Matrix.of(context); + final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( + matrix.client.setAvatar(file), + ); + if (success != false) { + setState(() { + profileFuture = null; + profile = null; + }); + } + } + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + profileFuture ??= client.ownProfile.then((p) { + if (mounted) setState(() => profile = p); + return p; + }); + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context).account)), + body: ListView( + children: [ + ListTile( + leading: Avatar( + profile?.avatarUrl, + profile?.displayname ?? client.userID.toString(), + size: 24.0, + ), + title: Text(L10n.of(context).avatar), + onTap: () => setAvatarAction(context), + ), + ListTile( + leading: Icon(Icons.edit_outlined), + title: Text(L10n.of(context).editDisplayname), + subtitle: Text(profile?.displayname ?? client.userID.localpart), + onTap: () => setDisplaynameAction(context), + ), + ListTile( + leading: Icon(Icons.phone_outlined), + title: Text(L10n.of(context).editJitsiInstance), + subtitle: Text(Matrix.of(context).jitsiInstance), + onTap: () => setJitsiInstanceAction(context), + ), + ListTile( + leading: Icon(Icons.block_outlined), + title: Text(L10n.of(context).ignoredUsers), + onTap: () async => await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + SettingsIgnoreListView(), + ), + ), + ), + Divider(thickness: 1), + ListTile( + leading: Icon(Icons.vpn_key_outlined), + title: Text(L10n.of(context).changeThePassword), + onTap: () => _changePasswordAccountAction(context), + ), + ListTile( + leading: Icon(Icons.exit_to_app_outlined), + title: Text(L10n.of(context).logout), + onTap: () => logoutAction(context), + ), + ListTile( + leading: Icon(Icons.delete_forever_outlined), + title: Text( + L10n.of(context).deleteAccount, + style: TextStyle(color: Colors.red), + ), + onTap: () => _deleteAccountAction(context), + ), + ], + ), + ); + } +} diff --git a/lib/views/settings/settings_chat.dart b/lib/views/settings/settings_chat.dart new file mode 100644 index 0000000..d68d346 --- /dev/null +++ b/lib/views/settings/settings_chat.dart @@ -0,0 +1,49 @@ +import 'package:furrychat/views/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; + +class ChatSettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: Settings(currentSetting: SettingsViews.chat), + secondScaffold: ChatSettings(), + ); + } +} + +class ChatSettings extends StatefulWidget { + @override + _ChatSettingsState createState() => _ChatSettingsState(); +} + +class _ChatSettingsState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context).chat)), + body: ListView( + children: [ + ListTile( + title: Text(L10n.of(context).renderRichContent), + trailing: Switch( + value: Matrix.of(context).renderHtml, + activeColor: Theme.of(context).primaryColor, + onChanged: (bool newValue) async { + Matrix.of(context).renderHtml = newValue; + await Matrix.of(context) + .store + .setItem('chat.fluffy.renderHtml', newValue ? '1' : '0'); + setState(() => null); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/settings_devices.dart b/lib/views/settings/settings_devices.dart similarity index 94% rename from lib/views/settings_devices.dart rename to lib/views/settings/settings_devices.dart index d050deb..009503f 100644 --- a/lib/views/settings_devices.dart +++ b/lib/views/settings/settings_devices.dart @@ -1,19 +1,19 @@ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:furrychat/views/settings.dart'; import 'package:furrychat/components/dialogs/simple_dialogs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../components/adaptive_page_layout.dart'; -import '../components/matrix.dart'; -import '../utils/date_time_extension.dart'; -import 'chat_list.dart'; +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:furrychat/utils/date_time_extension.dart'; class DevicesSettingsView extends StatelessWidget { @override Widget build(BuildContext context) { return AdaptivePageLayout( primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(), + firstScaffold: Settings(currentSetting: SettingsViews.devices), secondScaffold: DevicesSettings(), ); } @@ -110,7 +110,7 @@ class DevicesSettingsState extends State { L10n.of(context).removeAllOtherDevices, style: TextStyle(color: Colors.red), ), - trailing: Icon(Icons.delete_outline), + leading: Icon(Icons.delete_outline_outlined), onTap: () => _removeDevicesAction(context, devices), ), Divider(height: 1), diff --git a/lib/views/settings_emotes.dart b/lib/views/settings/settings_emotes.dart similarity index 98% rename from lib/views/settings_emotes.dart rename to lib/views/settings/settings_emotes.dart index c2159ef..ac2c63b 100644 --- a/lib/views/settings_emotes.dart +++ b/lib/views/settings/settings_emotes.dart @@ -9,10 +9,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; -import '../components/adaptive_page_layout.dart'; -import '../components/dialogs/simple_dialogs.dart'; -import '../components/matrix.dart'; -import 'chat_list.dart'; +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/dialogs/simple_dialogs.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:furrychat/views/settings.dart'; class EmotesSettingsView extends StatelessWidget { final Room room; @@ -24,7 +24,7 @@ class EmotesSettingsView extends StatelessWidget { Widget build(BuildContext context) { return AdaptivePageLayout( primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(), + firstScaffold: Settings(currentSetting: SettingsViews.emotes), secondScaffold: EmotesSettings(room: room, stateKey: stateKey), ); } diff --git a/lib/views/settings/settings_encryption.dart b/lib/views/settings/settings_encryption.dart new file mode 100644 index 0000000..23b8f1a --- /dev/null +++ b/lib/views/settings/settings_encryption.dart @@ -0,0 +1,234 @@ +import 'package:furrychat/views/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:furrychat/utils/beautify_string_extension.dart'; + +import 'package:furrychat/components/dialogs/simple_dialogs.dart'; +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:olm/olm.dart' as olm; + +class EncryptionSettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: Settings(currentSetting: SettingsViews.encryption), + secondScaffold: EncryptionSettings(), + ); + } +} + +class EncryptionSettings extends StatefulWidget { + @override + _EncryptionSettingsState createState() => _EncryptionSettingsState(); +} + +class _EncryptionSettingsState extends State { + Future profileFuture; + dynamic profile; + Future crossSigningCachedFuture; + bool crossSigningCached; + Future megolmBackupCachedFuture; + bool megolmBackupCached; + + Future requestSSSSCache(BuildContext context) async { + final handle = Matrix.of(context).client.encryption.ssss.open(); + final str = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).askSSSSCache, + hintText: L10n.of(context).passphraseOrKey, + password: true, + ); + if (str != null) { + SimpleDialogs(context).showLoadingDialog(context); + // make sure the loading spinner shows before we test the keys + await Future.delayed(Duration(milliseconds: 100)); + var valid = false; + try { + handle.unlock(recoveryKey: str); + valid = true; + } catch (e, s) { + debugPrint('Couldn\'t use recovery key: ' + e.toString()); + debugPrint(s.toString()); + try { + handle.unlock(passphrase: str); + valid = true; + } catch (e, s) { + debugPrint('Couldn\'t use recovery passphrase: ' + e.toString()); + debugPrint(s.toString()); + valid = false; + } + } + await Navigator.of(context)?.pop(); + if (valid) { + await handle.maybeCacheAll(); + await SimpleDialogs(context).inform( + contentText: L10n.of(context).cachedKeys, + ); + setState(() { + crossSigningCachedFuture = null; + crossSigningCached = null; + megolmBackupCachedFuture = null; + megolmBackupCached = null; + }); + } else { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).incorrectPassphraseOrKey, + ); + } + } + } + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + profileFuture ??= client.ownProfile.then((p) { + if (mounted) setState(() => profile = p); + return p; + }); + crossSigningCachedFuture ??= + client.encryption.crossSigning.isCached().then((c) { + if (mounted) setState(() => crossSigningCached = c); + return c; + }); + megolmBackupCachedFuture ??= + client.encryption.keyManager.isCached().then((c) { + if (mounted) setState(() => megolmBackupCached = c); + return c; + }); + + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context).encryption)), + body: ListView( + children: [ + ListTile( + leading: Icon(Icons.compare_arrows_outlined), + title: Text(client.encryption.crossSigning.enabled + ? L10n.of(context).crossSigningEnabled + : L10n.of(context).crossSigningDisabled), + subtitle: client.encryption.crossSigning.enabled + ? Text(client.isUnknownSession + ? L10n.of(context).unknownSessionVerify + : L10n.of(context).sessionVerified + + ', ' + + (crossSigningCached == null + ? '⌛' + : (crossSigningCached + ? L10n.of(context).keysCached + : L10n.of(context).keysMissing))) + : null, + onTap: () async { + if (!client.encryption.crossSigning.enabled) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).noCrossSignBootstrap, + ); + return; + } + if (client.isUnknownSession) { + final str = await SimpleDialogs(context).enterText( + titleText: L10n.of(context).askSSSSVerify, + hintText: L10n.of(context).passphraseOrKey, + password: true, + ); + if (str != null) { + SimpleDialogs(context).showLoadingDialog(context); + // make sure the loading spinner shows before we test the keys + await Future.delayed(Duration(milliseconds: 100)); + var valid = false; + try { + await client.encryption.crossSigning + .selfSign(recoveryKey: str); + valid = true; + } catch (_) { + try { + await client.encryption.crossSigning + .selfSign(passphrase: str); + valid = true; + } catch (_) { + valid = false; + } + } + await Navigator.of(context)?.pop(); + if (valid) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).verifiedSession, + ); + setState(() { + crossSigningCachedFuture = null; + crossSigningCached = null; + megolmBackupCachedFuture = null; + megolmBackupCached = null; + }); + } else { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).incorrectPassphraseOrKey, + ); + } + } + } + if (!(await client.encryption.crossSigning.isCached())) { + await requestSSSSCache(context); + } + }, + ), + ListTile( + leading: Icon(Icons.wb_cloudy_outlined), + title: Text(client.encryption.keyManager.enabled + ? L10n.of(context).onlineKeyBackupEnabled + : L10n.of(context).onlineKeyBackupDisabled), + subtitle: client.encryption.keyManager.enabled + ? Text(megolmBackupCached == null + ? '⌛' + : (megolmBackupCached + ? L10n.of(context).keysCached + : L10n.of(context).keysMissing)) + : null, + onTap: () async { + if (!client.encryption.keyManager.enabled) { + await SimpleDialogs(context).inform( + contentText: L10n.of(context).noMegolmBootstrap, + ); + return; + } + if (!(await client.encryption.keyManager.isCached())) { + await requestSSSSCache(context); + } + }, + ), + Divider(thickness: 1), + ListTile( + title: Text('Device name:'), + subtitle: Text(client.userDeviceKeys[client.userID] + ?.deviceKeys[client.deviceID]?.deviceDisplayName ?? + L10n.of(context).unknownDevice), + ), + ListTile( + title: Text('Device ID:'), + subtitle: Text(client.deviceID), + ), + ListTile( + title: Text('Encryption enabled:'), + subtitle: Text(client.encryptionEnabled.toString()), + ), + if (client.encryptionEnabled) + Column( + children: [ + ListTile( + title: Text('Your public fingerprint key:'), + subtitle: Text(client.fingerprintKey.beautified), + ), + ListTile( + title: Text('Your public identity key:'), + subtitle: Text(client.identityKey.beautified), + ), + ListTile( + title: Text('LibOlm version:'), + subtitle: Text(olm.get_library_version().join('.')), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/views/settings/settings_homeserver.dart b/lib/views/settings/settings_homeserver.dart new file mode 100644 index 0000000..29d562f --- /dev/null +++ b/lib/views/settings/settings_homeserver.dart @@ -0,0 +1,44 @@ +import 'package:furrychat/views/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; + +class HomeserverSettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: Settings(currentSetting: SettingsViews.homeserver), + secondScaffold: HomeserverSettings(), + ); + } +} + +class HomeserverSettings extends StatefulWidget { + @override + _HomeserverSettingsState createState() => _HomeserverSettingsState(); +} + +class _HomeserverSettingsState extends State { + @override + Widget build(BuildContext context) { + var client = Matrix.of(context).client; + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context).homeserver)), + body: ListView( + children: [ + ListTile( + title: Text(L10n.of(context).yourOwnUsername + ':'), + subtitle: Text(client.userID), + ), + ListTile( + title: Text('Homeserver:'), + subtitle: Text(client.homeserver.toString()), + ), + ], + ), + ); + } +} diff --git a/lib/views/settings_ignore_list.dart b/lib/views/settings/settings_ignore_list.dart similarity index 95% rename from lib/views/settings_ignore_list.dart rename to lib/views/settings/settings_ignore_list.dart index a5cd8fd..dbddbba 100644 --- a/lib/views/settings_ignore_list.dart +++ b/lib/views/settings/settings_ignore_list.dart @@ -5,15 +5,15 @@ import 'package:furrychat/components/dialogs/simple_dialogs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../components/matrix.dart'; -import 'chat_list.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:furrychat/views/settings.dart'; class SettingsIgnoreListView extends StatelessWidget { @override Widget build(BuildContext context) { return AdaptivePageLayout( primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(), + firstScaffold: Settings(currentSetting: SettingsViews.account), secondScaffold: SettingsIgnoreList(), ); } diff --git a/lib/views/settings_multiple_emotes.dart b/lib/views/settings/settings_multiple_emotes.dart similarity index 90% rename from lib/views/settings_multiple_emotes.dart rename to lib/views/settings/settings_multiple_emotes.dart index 76aa358..38db907 100644 --- a/lib/views/settings_multiple_emotes.dart +++ b/lib/views/settings/settings_multiple_emotes.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../components/adaptive_page_layout.dart'; -import '../utils/app_route.dart'; -import 'chat_list.dart'; +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/utils/app_route.dart'; +import 'package:furrychat/views/settings.dart'; import 'settings_emotes.dart'; class MultipleEmotesSettingsView extends StatelessWidget { @@ -15,7 +15,7 @@ class MultipleEmotesSettingsView extends StatelessWidget { Widget build(BuildContext context) { return AdaptivePageLayout( primaryPage: FocusPage.SECOND, - firstScaffold: ChatList(), + firstScaffold: Settings(currentSetting: SettingsViews.emotes), secondScaffold: MultipleEmotesSettings(room: room), ); } diff --git a/lib/views/settings/settings_themes.dart b/lib/views/settings/settings_themes.dart new file mode 100644 index 0000000..09085a9 --- /dev/null +++ b/lib/views/settings/settings_themes.dart @@ -0,0 +1,155 @@ +import 'dart:io'; + +import 'package:furrychat/views/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:furrychat/components/adaptive_page_layout.dart'; +import 'package:furrychat/components/matrix.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'package:furrychat/components/theme_switcher.dart'; + +class ThemesSettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: Settings(currentSetting: SettingsViews.themes), + secondScaffold: ThemesSettings(), + ); + } +} + +class ThemesSettings extends StatefulWidget { + @override + _ThemesSettingsState createState() => _ThemesSettingsState(); +} + +class _ThemesSettingsState extends State { + Themes _selectedTheme; + bool _amoledEnabled; + + void setWallpaperAction(BuildContext context) async { + final wallpaper = await ImagePicker().getImage(source: ImageSource.gallery); + if (wallpaper == null) return; + Matrix.of(context).wallpaper = File(wallpaper.path); + await Matrix.of(context) + .store + .setItem('chat.fluffy.wallpaper', wallpaper.path); + setState(() => null); + } + + void deleteWallpaperAction(BuildContext context) async { + Matrix.of(context).wallpaper = null; + await Matrix.of(context).store.setItem('chat.fluffy.wallpaper', null); + setState(() => null); + } + + @override + Widget build(BuildContext context) { + final matrix = Matrix.of(context); + final themeEngine = ThemeSwitcherWidget.of(context); + _selectedTheme = themeEngine.selectedTheme; + _amoledEnabled = themeEngine.amoledEnabled; + + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context).changeTheme)), + body: ListView(children: [ + Column( + children: [ + RadioListTile( + title: Text( + L10n.of(context).systemTheme, + ), + value: Themes.system, + groupValue: _selectedTheme, + activeColor: Theme.of(context).primaryColor, + onChanged: (Themes value) { + setState(() { + _selectedTheme = value; + themeEngine.switchTheme(matrix, value, _amoledEnabled); + }); + }, + ), + RadioListTile( + title: Text( + L10n.of(context).lightTheme, + ), + value: Themes.light, + groupValue: _selectedTheme, + activeColor: Theme.of(context).primaryColor, + onChanged: (Themes value) { + setState(() { + _selectedTheme = value; + themeEngine.switchTheme(matrix, value, _amoledEnabled); + }); + }, + ), + RadioListTile( + title: Text( + L10n.of(context).darkTheme, + ), + value: Themes.dark, + groupValue: _selectedTheme, + activeColor: Theme.of(context).primaryColor, + onChanged: (Themes value) { + setState(() { + _selectedTheme = value; + themeEngine.switchTheme(matrix, value, _amoledEnabled); + }); + }, + ), + ListTile( + title: Text( + L10n.of(context).useAmoledTheme, + ), + trailing: Switch( + value: _amoledEnabled, + activeColor: Theme.of(context).primaryColor, + onChanged: (bool value) { + setState(() { + _amoledEnabled = value; + themeEngine.switchTheme(matrix, _selectedTheme, value); + }); + }, + ), + ), + ], + ), + //if (!kIsWeb && Matrix.of(context).store != null) Divider(thickness: 1), + //if (!kIsWeb && Matrix.of(context).store != null) + ListTile( + title: Text( + L10n.of(context).wallpaper, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + if (Matrix.of(context).wallpaper != null) + ListTile( + title: Image.file( + Matrix.of(context).wallpaper, + height: 38, + fit: BoxFit.cover, + ), + trailing: Icon( + Icons.delete_forever, + color: Colors.red, + ), + onTap: () => deleteWallpaperAction(context), + ), + //if (!kIsWeb && Matrix.of(context).store != null) + Builder(builder: (context) { + return ListTile( + title: Text(L10n.of(context).changeWallpaper), + trailing: Icon(Icons.wallpaper), + onTap: () => setWallpaperAction(context), + ); + }), + ]), + ); + } +}