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
This commit is contained in:
Inex Code 2020-10-14 04:21:28 +03:00 committed by GitHub
parent 57481c2fb0
commit f956476bf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 875 additions and 601 deletions

View File

@ -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<ThemesSettings> {
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: <Widget>[
RadioListTile<Themes>(
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<Themes>(
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<Themes>(
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);
});
},
),
),
],
);
}
}

View File

@ -114,6 +114,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"avatar": "Avatar",
"@avatar": {
"type": "text",
"placeholders": {}
},
"avatarHasBeenChanged": "Avatar has been changed", "avatarHasBeenChanged": "Avatar has been changed",
"@avatarHasBeenChanged": { "@avatarHasBeenChanged": {
"type": "text", "type": "text",
@ -207,6 +212,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"changeThePassword": "Change the password",
"@changeThePassword": {
"type": "text",
"placeholders": {}
},
"changedTheGuestAccessRules": "{username} changed the guest access rules", "changedTheGuestAccessRules": "{username} changed the guest access rules",
"@changedTheGuestAccessRules": { "@changedTheGuestAccessRules": {
"type": "text", "type": "text",
@ -681,6 +691,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"homeserver": "Homeserver",
"@homeserver": {
"type": "text",
"placeholders": {}
},
"homeserverIsNotCompatible": "Homeserver is not compatible", "homeserverIsNotCompatible": "Homeserver is not compatible",
"@homeserverIsNotCompatible": { "@homeserverIsNotCompatible": {
"type": "text", "type": "text",

View File

@ -20,9 +20,9 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:matrix_link_text/link_text.dart'; import 'package:matrix_link_text/link_text.dart';
import './settings_emotes.dart'; import 'package:furrychat/views/settings/settings_emotes.dart';
import './settings_multiple_emotes.dart'; import 'package:furrychat/views/settings/settings_multiple_emotes.dart';
import '../utils/url_launcher.dart'; import 'package:furrychat/utils/url_launcher.dart';
class ChatDetails extends StatefulWidget { class ChatDetails extends StatefulWidget {
final Room room; final Room room;

View File

@ -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/config/app_config.dart';
import 'package:furrychat/utils/platform_infos.dart'; import 'package:furrychat/views/settings/settings_account.dart';
import 'package:furrychat/views/settings_devices.dart'; import 'package:furrychat/views/settings/settings_chat.dart';
import 'package:furrychat/views/settings_ignore_list.dart'; import 'package:furrychat/views/settings/settings_devices.dart';
import 'package:flutter/foundation.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/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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 'package:url_launcher/url_launcher.dart';
import '../components/adaptive_page_layout.dart'; import 'package:furrychat/components/adaptive_page_layout.dart';
import '../components/content_banner.dart'; import 'package:furrychat/components/matrix.dart';
import '../components/dialogs/simple_dialogs.dart'; import 'package:furrychat/utils/app_route.dart';
import '../components/matrix.dart'; import 'package:furrychat/views/settings/settings_emotes.dart';
import '../utils/app_route.dart';
import 'app_info.dart'; enum SettingsViews {
import 'chat_list.dart'; account,
import 'settings_emotes.dart'; homeserver,
themes,
chat,
emotes,
encryption,
devices,
}
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.FIRST,
firstScaffold: ChatList(), firstScaffold: Settings(
secondScaffold: Settings(), isInFocus: true,
),
secondScaffold: Scaffold(
body: Center(
child: Image.asset('assets/logo.png', width: 100, height: 100),
),
),
); );
} }
} }
class Settings extends StatefulWidget { class Settings extends StatefulWidget {
final bool isInFocus;
final SettingsViews currentSetting;
const Settings({this.isInFocus = false, this.currentSetting, Key key})
: super(key: key);
@override @override
_SettingsState createState() => _SettingsState(); _SettingsState createState() => _SettingsState();
} }
@ -43,196 +55,21 @@ class Settings extends StatefulWidget {
class _SettingsState extends State<Settings> { class _SettingsState extends State<Settings> {
Future<dynamic> profileFuture; Future<dynamic> profileFuture;
dynamic profile; dynamic profile;
Future<bool> crossSigningCachedFuture;
bool crossSigningCached;
Future<bool> megolmBackupCachedFuture;
bool megolmBackupCached;
void logoutAction(BuildContext context) async { void _handleTap(Widget child) {
if (await SimpleDialogs(context).askConfirmation() == false) { widget.isInFocus
return; ? Navigator.of(context).push(
} AppRoute.defaultRoute(
var matrix = Matrix.of(context); context,
await SimpleDialogs(context) child,
.tryRequestWithLoadingDialog(matrix.client.logout()); ),
} )
: Navigator.of(context).pushReplacement(
void _changePasswordAccountAction(BuildContext context) async { AppRoute.defaultRoute(
final oldPassword = await SimpleDialogs(context).enterText( context,
password: true, child,
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<void> 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 @override
@ -242,323 +79,120 @@ class _SettingsState extends State<Settings> {
if (mounted) setState(() => profile = p); if (mounted) setState(() => profile = p);
return 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( return Scaffold(
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) =>
<Widget>[ <Widget>[
SliverAppBar( SliverAppBar(
expandedHeight: 300.0, leading: !widget.isInFocus
? IconButton(
icon: Icon(Icons.close_outlined),
onPressed: () =>
{Navigator.of(context).popUntil((r) => r.isFirst)},
)
: null,
floating: true, floating: true,
pinned: true, pinned: true,
backgroundColor: Theme.of(context).appBarTheme.color, backgroundColor: Theme.of(context).appBarTheme.color,
flexibleSpace: FlexibleSpaceBar( title: Text(L10n.of(context).settings),
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),
),
),
), ),
], ],
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text( leading: Icon(
L10n.of(context).changeTheme, Icons.person_outlined,
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,
),
), ),
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( ListTile(
title: Text(L10n.of(context).renderRichContent), leading: Icon(
trailing: Switch( Icons.dns_outlined,
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);
},
), ),
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( 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), title: Text(L10n.of(context).emoteSettings),
onTap: () async => await Navigator.of(context).push( selected:
AppRoute.defaultRoute( widget.currentSetting == SettingsViews.emotes ? true : false,
context, selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
EmotesSettingsView(), 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( ListTile(
title: Text( leading: Icon(
L10n.of(context).account, Icons.devices_other_outlined,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
), ),
),
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), title: Text(L10n.of(context).devices),
onTap: () async => await Navigator.of(context).push( selected:
AppRoute.defaultRoute( widget.currentSetting == SettingsViews.devices ? true : false,
context, selectedTileColor: Theme.of(context).primaryColor.withAlpha(30),
DevicesSettingsView(), onTap: () => _handleTap(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(),
),
),
), ),
Divider(thickness: 1), Divider(thickness: 1),
ListTile( ListTile(
trailing: Icon(Icons.vpn_key), leading: Icon(
title: Text( Icons.help_outline_outlined,
'Change password',
), ),
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), title: Text(L10n.of(context).help),
onTap: () => launch(AppConfig.supportUrl), onTap: () => launch(AppConfig.supportUrl),
), ),
ListTile( ListTile(
trailing: Icon(Icons.privacy_tip_rounded), leading: Icon(
Icons.privacy_tip_outlined,
),
title: Text(L10n.of(context).privacy), title: Text(L10n.of(context).privacy),
onTap: () => launch(AppConfig.privacyUrl), onTap: () => launch(AppConfig.privacyUrl),
), ),
ListTile( ListTile(
trailing: Icon(Icons.link), leading: Icon(
Icons.link_outlined,
),
title: Text(L10n.of(context).license), title: Text(L10n.of(context).license),
onTap: () => showLicensePage( onTap: () => showLicensePage(
context: context, context: context,
@ -568,7 +202,9 @@ class _SettingsState extends State<Settings> {
), ),
), ),
ListTile( ListTile(
trailing: Icon(Icons.code), leading: Icon(
Icons.code_outlined,
),
title: Text(L10n.of(context).sourceCode), title: Text(L10n.of(context).sourceCode),
onTap: () => launch(AppConfig.sourceCodeUrl), onTap: () => launch(AppConfig.sourceCodeUrl),
), ),

View File

@ -1,7 +1,7 @@
import 'package:furrychat/components/adaptive_page_layout.dart'; import 'package:furrychat/components/adaptive_page_layout.dart';
import 'package:furrychat/components/matrix.dart'; import 'package:furrychat/components/matrix.dart';
import 'package:furrychat/utils/beautify_string_extension.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/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
@ -11,7 +11,7 @@ class AppInfoView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(), firstScaffold: Settings(),
secondScaffold: AppInfo(), secondScaffold: AppInfo(),
); );
} }

View File

@ -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<AccountSettings> {
Future<dynamic> 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),
),
],
),
);
}
}

View File

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

View File

@ -1,19 +1,19 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:furrychat/views/settings.dart';
import 'package:furrychat/components/dialogs/simple_dialogs.dart'; import 'package:furrychat/components/dialogs/simple_dialogs.dart';
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 '../components/adaptive_page_layout.dart'; import 'package:furrychat/components/adaptive_page_layout.dart';
import '../components/matrix.dart'; import 'package:furrychat/components/matrix.dart';
import '../utils/date_time_extension.dart'; import 'package:furrychat/utils/date_time_extension.dart';
import 'chat_list.dart';
class DevicesSettingsView extends StatelessWidget { class DevicesSettingsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(), firstScaffold: Settings(currentSetting: SettingsViews.devices),
secondScaffold: DevicesSettings(), secondScaffold: DevicesSettings(),
); );
} }
@ -110,7 +110,7 @@ class DevicesSettingsState extends State<DevicesSettings> {
L10n.of(context).removeAllOtherDevices, L10n.of(context).removeAllOtherDevices,
style: TextStyle(color: Colors.red), style: TextStyle(color: Colors.red),
), ),
trailing: Icon(Icons.delete_outline), leading: Icon(Icons.delete_outline_outlined),
onTap: () => _removeDevicesAction(context, devices), onTap: () => _removeDevicesAction(context, devices),
), ),
Divider(height: 1), Divider(height: 1),

View File

@ -9,10 +9,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../components/adaptive_page_layout.dart'; import 'package:furrychat/components/adaptive_page_layout.dart';
import '../components/dialogs/simple_dialogs.dart'; import 'package:furrychat/components/dialogs/simple_dialogs.dart';
import '../components/matrix.dart'; import 'package:furrychat/components/matrix.dart';
import 'chat_list.dart'; import 'package:furrychat/views/settings.dart';
class EmotesSettingsView extends StatelessWidget { class EmotesSettingsView extends StatelessWidget {
final Room room; final Room room;
@ -24,7 +24,7 @@ class EmotesSettingsView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(), firstScaffold: Settings(currentSetting: SettingsViews.emotes),
secondScaffold: EmotesSettings(room: room, stateKey: stateKey), secondScaffold: EmotesSettings(room: room, stateKey: stateKey),
); );
} }

View File

@ -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<EncryptionSettings> {
Future<dynamic> profileFuture;
dynamic profile;
Future<bool> crossSigningCachedFuture;
bool crossSigningCached;
Future<bool> megolmBackupCachedFuture;
bool megolmBackupCached;
Future<void> 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: <Widget>[
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('.')),
),
],
),
],
),
);
}
}

View File

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

View File

@ -5,15 +5,15 @@ import 'package:furrychat/components/dialogs/simple_dialogs.dart';
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 '../components/matrix.dart'; import 'package:furrychat/components/matrix.dart';
import 'chat_list.dart'; import 'package:furrychat/views/settings.dart';
class SettingsIgnoreListView extends StatelessWidget { class SettingsIgnoreListView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(), firstScaffold: Settings(currentSetting: SettingsViews.account),
secondScaffold: SettingsIgnoreList(), secondScaffold: SettingsIgnoreList(),
); );
} }

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../components/adaptive_page_layout.dart'; import 'package:furrychat/components/adaptive_page_layout.dart';
import '../utils/app_route.dart'; import 'package:furrychat/utils/app_route.dart';
import 'chat_list.dart'; import 'package:furrychat/views/settings.dart';
import 'settings_emotes.dart'; import 'settings_emotes.dart';
class MultipleEmotesSettingsView extends StatelessWidget { class MultipleEmotesSettingsView extends StatelessWidget {
@ -15,7 +15,7 @@ class MultipleEmotesSettingsView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AdaptivePageLayout( return AdaptivePageLayout(
primaryPage: FocusPage.SECOND, primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(), firstScaffold: Settings(currentSetting: SettingsViews.emotes),
secondScaffold: MultipleEmotesSettings(room: room), secondScaffold: MultipleEmotesSettings(room: room),
); );
} }

View File

@ -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<ThemesSettings> {
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: <Widget>[
RadioListTile<Themes>(
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<Themes>(
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<Themes>(
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),
);
}),
]),
);
}
}