From aaa505e83e4146f1371d898f3c71bf0e9415ed2d Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 28 May 2025 01:08:18 +0800 Subject: [PATCH] :sparkles: Accoun settings --- assets/i18n/en-US.json | 27 +- lib/pods/websocket.dart | 2 +- lib/screens/account.dart | 10 + lib/screens/account/me/settings.dart | 280 ++++++++++++- lib/screens/chat/room.dart | 1 + lib/screens/posts/compose.dart | 1 - lib/screens/settings.dart | 596 +++++++++++++++++++-------- 7 files changed, 735 insertions(+), 182 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index af7c3bc..5dcab48 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -283,5 +283,30 @@ "postDescription": "Description", "call": "Call", "done": "Done", - "loginResetPasswordSent": "Password reset link sent, please check your email inbox." + "loginResetPasswordSent": "Password reset link sent, please check your email inbox.", + "accountDeletion": "Delete Account", + "accountDeletionHint": "Are you sure to delete your account? If you confirmed, we will send an confirmation email to your primary email address, you can continue the deletion process by follow the insturctions in the email.", + "accountDeletionSent": "Account deletion confirmation email sent, please check your email inbox.", + "accountSecurityTitle": "Security", + "accountPrivacyTitle": "Privacy", + "accountDangerZoneTitle": "Danger Zone", + "accountPassword": "Password", + "accountPasswordDescription": "Change your account password", + "accountPasswordChange": "Change Password", + "accountPasswordChangeSent": "Password reset link sent, please check your email inbox.", + "accountPasswordChangeDescription": "We will send an email to your primary email address to reset your password.", + "accountTwoFactor": "Two-Factor Authentication", + "accountTwoFactorDescription": "Add an extra layer of security to your account", + "accountTwoFactorSetup": "Set Up 2FA", + "accountTwoFactorSetupDescription": "Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to sign in.", + "accountPrivacy": "Privacy Settings", + "accountPrivacyDescription": "Control who can see your profile and content", + "accountDataExport": "Export Your Data", + "accountDataExportDescription": "Download a copy of your data", + "accountDataExportConfirmation": "We'll prepare an export of your data which may take some time. You'll receive an email when it's ready to download.", + "accountDataExportConfirm": "Request Export", + "accountDataExportRequested": "Data export requested. You'll receive an email when it's ready.", + "accountDeletionDescription": "Permanently delete your account and all your data", + "accountSettingsHelp": "Account Settings Help", + "accountSettingsHelpContent": "This page allows you to manage your account security, privacy, and other settings. If you need assistance, please contact support." } diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart index b1662e4..f6ed332 100644 --- a/lib/pods/websocket.dart +++ b/lib/pods/websocket.dart @@ -157,7 +157,7 @@ class WebSocketStateNotifier extends StateNotifier { await service.connect(ref); state = const WebSocketState.connected(); service.statusStream.listen((event) { - state = event; + if (mounted) state = event; }); } catch (err) { state = WebSocketState.error('Failed to connect: $err'); diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 4badc2f..4cf288f 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -235,6 +235,16 @@ class AccountScreen extends HookConsumerWidget { context.router.push(SettingsRoute()); }, ), + ListTile( + minTileHeight: 48, + leading: const Icon(Symbols.manage_accounts), + trailing: const Icon(Symbols.chevron_right), + contentPadding: EdgeInsets.symmetric(horizontal: 24), + title: Text('accountSettings').tr(), + onTap: () { + context.router.push(AccountSettingsRoute()); + }, + ), if (kDebugMode) const Divider(height: 1).padding(vertical: 8), if (kDebugMode) ListTile( diff --git a/lib/screens/account/me/settings.dart b/lib/screens/account/me/settings.dart index 1aba1f8..0948edb 100644 --- a/lib/screens/account/me/settings.dart +++ b/lib/screens/account/me/settings.dart @@ -1,8 +1,19 @@ +import 'dart:io'; + import 'package:auto_route/annotations.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/pods/userinfo.dart'; +import 'package:island/screens/auth/captcha.dart'; +import 'package:island/services/responsive.dart'; +import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; @RoutePage() class AccountSettingsScreen extends HookConsumerWidget { @@ -10,9 +21,274 @@ class AccountSettingsScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isDesktop = + !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); + final isWide = isWideScreen(context); + + Future requestAccountDeletion() async { + final confirm = await showConfirmAlert( + 'accountDeletionHint'.tr(), + 'accountDeletion'.tr(), + ); + if (!confirm || !context.mounted) return; + try { + final client = ref.read(apiClientProvider); + await client.delete('/accounts/me'); + if (context.mounted) { + showSnackBar(context, 'accountDeletionSent'.tr()); + } + } catch (err) { + showErrorAlert(err); + } + } + + Future requestResetPassword() async { + final confirm = await showConfirmAlert( + 'accountPasswordChangeDescription'.tr(), + 'accountPassword'.tr(), + ); + if (!confirm || !context.mounted) return; + final captchaTk = await Navigator.of( + context, + ).push(MaterialPageRoute(builder: (context) => CaptchaScreen())); + if (captchaTk == null) return; + try { + final userInfo = ref.read(userInfoProvider); + final client = ref.read(apiClientProvider); + await client.post( + '/accounts/recovery/password', + data: {'account': userInfo.value!.name, 'captcha_token': captchaTk}, + ); + if (context.mounted) { + showSnackBar(context, 'accountPasswordChangeSent'.tr()); + } + } catch (err) { + showErrorAlert(err); + } + } + + // Group settings into categories for better organization + final securitySettings = [ + ListTile( + minLeadingWidth: 48, + title: Text('accountPassword').tr(), + subtitle: Text('accountPasswordDescription').tr().fontSize(12), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.password), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + requestResetPassword(); + }, + ), + ListTile( + minLeadingWidth: 48, + title: Text('accountTwoFactor').tr(), + subtitle: Text('accountTwoFactorDescription').tr().fontSize(12), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.security), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + // Navigate to two-factor authentication settings + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: Text('accountTwoFactor').tr(), + content: Text('accountTwoFactorSetupDescription').tr(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close').tr(), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + // Add navigation to 2FA setup screen + }, + child: Text('accountTwoFactorSetup').tr(), + ), + ], + ), + ); + }, + ), + ]; + + final privacySettings = [ + // ListTile( + // minLeadingWidth: 48, + // title: Text('accountPrivacy').tr(), + // subtitle: Text('accountPrivacyDescription').tr().fontSize(12), + // contentPadding: const EdgeInsets.only(left: 24, right: 17), + // leading: const Icon(Symbols.visibility), + // trailing: const Icon(Symbols.chevron_right), + // onTap: () { + // // Navigate to privacy settings + // }, + // ), + ListTile( + minLeadingWidth: 48, + title: Text('accountDataExport').tr(), + subtitle: Text('accountDataExportDescription').tr().fontSize(12), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.download), + trailing: const Icon(Symbols.chevron_right), + onTap: () async { + final confirm = await showConfirmAlert( + 'accountDataExportConfirmation'.tr(), + 'accountDataExport'.tr(), + ); + if (!confirm || !context.mounted) return; + // Add data export logic + showSnackBar(context, 'accountDataExportRequested'.tr()); + }, + ), + ]; + + final dangerZoneSettings = [ + ListTile( + minLeadingWidth: 48, + title: Text('accountDeletion').tr(), + subtitle: Text('accountDeletionDescription').tr().fontSize(12), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.delete_forever, color: Colors.red), + trailing: const Icon(Symbols.chevron_right), + onTap: requestAccountDeletion, + ), + ]; + + // Create a responsive layout based on screen width + Widget buildSettingsList() { + if (isWide) { + // Two-column layout for wide screens + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SettingsSection( + title: 'accountSecurityTitle', + children: securitySettings, + ), + _SettingsSection( + title: 'accountPrivacyTitle', + children: privacySettings, + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SettingsSection( + title: 'accountDangerZoneTitle', + children: dangerZoneSettings, + ), + ], + ), + ), + ], + ).padding(horizontal: 16); + } else { + // Single column layout for narrow screens + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SettingsSection( + title: 'accountSecurityTitle', + children: securitySettings, + ), + _SettingsSection( + title: 'accountPrivacyTitle', + children: privacySettings, + ), + _SettingsSection( + title: 'accountDangerZoneTitle', + children: dangerZoneSettings, + ), + ], + ); + } + } + return AppScaffold( - appBar: AppBar(title: Text('accountSettings').tr()), - body: SingleChildScrollView(child: Column(children: [])), + appBar: AppBar( + title: Text('accountSettings').tr(), + actions: + isDesktop + ? [ + IconButton( + icon: const Icon(Symbols.help_outline), + onPressed: () { + // Show help dialog + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: Text('accountSettingsHelp').tr(), + content: Text('accountSettingsHelpContent').tr(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close').tr(), + ), + ], + ), + ); + }, + ), + ] + : null, + ), + body: Focus( + autofocus: true, + onKeyEvent: (node, event) { + // Add keyboard shortcuts for desktop + if (isDesktop && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.of(context).pop(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 16), + child: buildSettingsList(), + ), + ), + ); + } +} + +// Helper widget for displaying settings sections with titles +class _SettingsSection extends StatelessWidget { + final String title; + final List children; + + const _SettingsSection({required this.title, required this.children}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), + child: Text( + title.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ...children, + const SizedBox(height: 16), + ], ); } } diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 24e9db8..7bde3ee 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -889,6 +889,7 @@ class _ChatInput extends ConsumerWidget { onKey: (event) => _handleKeyPress(context, ref, event), child: TextField( controller: messageController, + onSubmitted: enterToSend ? (_) => onSend() : null, inputFormatters: [ if (enterToSend) TextInputFormatter.withFunction((oldValue, newValue) { diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index 3f60e2a..80f51e6 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 13c67c1..a83f934 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -6,10 +6,12 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/pods/network.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -27,6 +29,9 @@ class SettingsScreen extends HookConsumerWidget { final prefs = ref.watch(sharedPreferencesProvider); final controller = TextEditingController(text: serverUrl); final settings = ref.watch(appSettingsProvider); + final isDesktop = + !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); + final isWide = isWideScreen(context); final docBasepath = useState(null); @@ -37,200 +42,437 @@ class SettingsScreen extends HookConsumerWidget { return null; }, []); - return AppScaffold( - noBackground: false, - appBar: AppBar(title: const Text('Settings')), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - minLeadingWidth: 48, - title: Text('settingsDisplayLanguage').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.translate), - trailing: DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - items: [ - ...EasyLocalization.of( - context, - )!.supportedLocales.mapIndexed((idx, ele) { - return DropdownMenuItem( - value: ele, - child: Text( - '${ele.languageCode}-${ele.countryCode}', - ).fontSize(14), - ); - }), - DropdownMenuItem( - value: null, - child: Text('languageFollowSystem').tr().fontSize(14), - ), - ], - value: EasyLocalization.of(context)!.currentLocale, - onChanged: (Locale? value) { - if (value != null) { - EasyLocalization.of(context)!.setLocale(value); - } else { - EasyLocalization.of(context)!.resetLocale(); - } - }, - buttonStyleData: const ButtonStyleData( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), - height: 40, - width: 160, - ), - menuItemStyleData: const MenuItemStyleData(height: 40), - ), + // Group settings into categories for better organization + final appearanceSettings = [ + // Language settings + ListTile( + minLeadingWidth: 48, + title: Text('settingsDisplayLanguage').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.translate), + trailing: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + items: [ + ...EasyLocalization.of(context)!.supportedLocales.mapIndexed(( + idx, + ele, + ) { + return DropdownMenuItem( + value: ele, + child: Text( + '${ele.languageCode}-${ele.countryCode}', + ).fontSize(14), + ); + }), + DropdownMenuItem( + value: null, + child: Text('languageFollowSystem').tr().fontSize(14), ), + ], + value: EasyLocalization.of(context)!.currentLocale, + onChanged: (Locale? value) { + if (value != null) { + EasyLocalization.of(context)!.setLocale(value); + } else { + EasyLocalization.of(context)!.resetLocale(); + } + }, + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5), + height: 40, + width: 160, ), - ListTile( - isThreeLine: true, - minLeadingWidth: 48, - title: Text('settingsServerUrl').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.link), - subtitle: Padding( - padding: const EdgeInsets.only(top: 6), - child: TextField( - controller: controller, - decoration: InputDecoration( - hintText: kNetworkServerDefault, - suffixIcon: IconButton( - icon: const Icon(Symbols.restart_alt), - onPressed: () { - controller.text = kNetworkServerDefault; - prefs.setString( - kNetworkServerStoreKey, - kNetworkServerDefault, - ); - ref.invalidate(serverUrlProvider); - showSnackBar(context, 'settingsApplied'.tr()); - }, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - isDense: true, - ), - onSubmitted: (value) { - if (value.isNotEmpty) { - prefs.setString(kNetworkServerStoreKey, value); - ref.invalidate(serverUrlProvider); - showSnackBar(context, 'settingsApplied'.tr()); - } - }, + menuItemStyleData: const MenuItemStyleData(height: 40), + ), + ), + ), + + // Background image settings (only for non-web platforms) + if (!kIsWeb && docBasepath.value != null) + ListTile( + minLeadingWidth: 48, + title: Text('settingsBackgroundImage').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.image), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isDesktop) + Tooltip( + message: 'settingsBackgroundImageTooltip'.tr(), + padding: EdgeInsets.only(left: 8), + child: const Icon(Symbols.info, size: 18), ), + const Icon(Symbols.chevron_right), + ], + ), + onTap: () async { + final imagePicker = ref.read(imagePickerProvider); + final image = await imagePicker.pickImage( + source: ImageSource.gallery, + ); + if (image == null) return; + + await File( + image.path, + ).copy('${docBasepath.value}/$kAppBackgroundImagePath'); + prefs.setBool(kAppBackgroundStoreKey, true); + ref.invalidate(backgroundImageFileProvider); + if (context.mounted) { + showSnackBar(context, 'settingsApplied'.tr()); + } + }, + ), + + // Clear background image option + if (!kIsWeb && docBasepath.value != null) + FutureBuilder( + future: File('${docBasepath.value}/app_background_image').exists(), + builder: (context, snapshot) { + if (!snapshot.hasData || !snapshot.data!) { + return const SizedBox.shrink(); + } + + return ListTile( + title: Text('settingsBackgroundImageClear').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.texture), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + File( + '${docBasepath.value}/$kAppBackgroundImagePath', + ).deleteSync(); + prefs.remove(kAppBackgroundStoreKey); + ref.invalidate(backgroundImageFileProvider); + if (context.mounted) { + showSnackBar(context, 'settingsApplied'.tr()); + } + }, + ); + }, + ), + ]; + + final serverSettings = [ + // Server URL settings + ListTile( + isThreeLine: true, + minLeadingWidth: 48, + title: Text('settingsServerUrl').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.link), + subtitle: Padding( + padding: const EdgeInsets.only(top: 6), + child: TextField( + controller: controller, + decoration: InputDecoration( + hintText: kNetworkServerDefault, + suffixIcon: IconButton( + icon: const Icon(Symbols.restart_alt), + onPressed: () { + controller.text = kNetworkServerDefault; + prefs.setString( + kNetworkServerStoreKey, + kNetworkServerDefault, + ); + ref.invalidate(serverUrlProvider); + showSnackBar(context, 'settingsApplied'.tr()); + }, ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, ), - if (!kIsWeb && docBasepath.value != null) + onSubmitted: (value) { + if (value.isNotEmpty) { + prefs.setString(kNetworkServerStoreKey, value); + ref.invalidate(serverUrlProvider); + showSnackBar(context, 'settingsApplied'.tr()); + } + }, + ), + ), + ), + ]; + + final behaviorSettings = [ + // Auto translate settings + ListTile( + minLeadingWidth: 48, + title: Text('settingsAutoTranslate').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.translate), + trailing: Switch( + value: settings.autoTranslate, + onChanged: (value) { + ref.read(appSettingsProvider.notifier).setAutoTranslate(value); + }, + ), + ), + + // Sound effects settings + ListTile( + minLeadingWidth: 48, + title: Text('settingsSoundEffects').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.volume_up), + trailing: Switch( + value: settings.soundEffects, + onChanged: (value) { + ref.read(appSettingsProvider.notifier).setSoundEffects(value); + }, + ), + ), + + // April Fool features settings + ListTile( + minLeadingWidth: 48, + title: Text('settingsAprilFoolFeatures').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.celebration), + trailing: Switch( + value: settings.aprilFoolFeatures, + onChanged: (value) { + ref.read(appSettingsProvider.notifier).setAprilFoolFeatures(value); + }, + ), + ), + + // Enter to send settings + ListTile( + minLeadingWidth: 48, + title: Text('settingsEnterToSend').tr(), + subtitle: + isDesktop + ? Text('settingsEnterToSendDesktopHint').tr().fontSize(12) + : null, + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.send), + trailing: Switch( + value: settings.enterToSend, + onChanged: (value) { + ref.read(appSettingsProvider.notifier).setEnterToSend(value); + }, + ), + ), + ]; + + // Desktop-specific settings + final desktopSettings = + !isDesktop + ? [] + : [ ListTile( minLeadingWidth: 48, - title: Text('settingsBackgroundImage').tr(), + title: Text('settingsKeyboardShortcuts').tr(), contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.image), + leading: const Icon(Symbols.keyboard), + onTap: () { + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: Text('settingsKeyboardShortcuts').tr(), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ShortcutRow( + shortcut: 'Ctrl+F', + description: 'Search', + ), + _ShortcutRow( + shortcut: 'Ctrl+,', + description: 'Settings', + ), + _ShortcutRow( + shortcut: 'Ctrl+N', + description: 'New Message', + ), + _ShortcutRow( + shortcut: 'Esc', + description: 'Close Dialog', + ), + // Add more shortcuts as needed + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close').tr(), + ), + ], + ), + ); + }, trailing: const Icon(Symbols.chevron_right), - onTap: () async { - final imagePicker = ref.read(imagePickerProvider); - final image = await imagePicker.pickImage( - source: ImageSource.gallery, - ); - if (image == null) return; + ), + ]; - await File( - image.path, - ).copy('${docBasepath.value}/$kAppBackgroundImagePath'); - prefs.setBool(kAppBackgroundStoreKey, true); - ref.invalidate(backgroundImageFileProvider); - if (context.mounted) { - showSnackBar(context, 'settingsApplied'.tr()); - } - }, - ), - if (!kIsWeb && docBasepath.value != null) - FutureBuilder( - future: - File('${docBasepath.value}/app_background_image').exists(), - builder: (context, snapshot) { - if (!snapshot.hasData || !snapshot.data!) { - return const SizedBox.shrink(); - } - - return ListTile( - title: Text('settingsBackgroundImageClear').tr(), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.texture), - trailing: const Icon(Symbols.chevron_right), - onTap: () { - File( - '${docBasepath.value}/$kAppBackgroundImagePath', - ).deleteSync(); - prefs.remove(kAppBackgroundStoreKey); - ref.invalidate(backgroundImageFileProvider); - if (context.mounted) { - showSnackBar(context, 'settingsApplied'.tr()); - } - }, - ); - }, - ), - const Divider(), - ListTile( - minLeadingWidth: 48, - title: Text('settingsAutoTranslate').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.translate), - trailing: Switch( - value: settings.autoTranslate, - onChanged: (value) { - ref - .read(appSettingsProvider.notifier) - .setAutoTranslate(value); - }, + // Create a responsive layout based on screen width + Widget buildSettingsList() { + if (isWide) { + // Two-column layout for wide screens + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SettingsSection( + title: 'Appearance', + children: appearanceSettings, + ), + _SettingsSection(title: 'Server', children: serverSettings), + ], ), ), - ListTile( - minLeadingWidth: 48, - title: Text('settingsSoundEffects').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.volume_up), - trailing: Switch( - value: settings.soundEffects, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).setSoundEffects(value); - }, - ), - ), - ListTile( - minLeadingWidth: 48, - title: Text('settingsAprilFoolFeatures').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.celebration), - trailing: Switch( - value: settings.aprilFoolFeatures, - onChanged: (value) { - ref - .read(appSettingsProvider.notifier) - .setAprilFoolFeatures(value); - }, - ), - ), - ListTile( - minLeadingWidth: 48, - title: Text('settingsEnterToSend').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - leading: const Icon(Symbols.send), - trailing: Switch( - value: settings.enterToSend, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).setEnterToSend(value); - }, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SettingsSection( + title: 'Behavior', + children: behaviorSettings, + ), + if (desktopSettings.isNotEmpty) + _SettingsSection( + title: 'Desktop', + children: desktopSettings, + ), + ], ), ), ], + ).padding(horizontal: 16); + } else { + // Single column layout for narrow screens + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SettingsSection(title: 'Appearance', children: appearanceSettings), + _SettingsSection(title: 'Server', children: serverSettings), + _SettingsSection(title: 'Behavior', children: behaviorSettings), + if (desktopSettings.isNotEmpty) + _SettingsSection(title: 'Desktop', children: desktopSettings), + ], + ); + } + } + + return AppScaffold( + noBackground: false, + appBar: AppBar( + title: Text('Settings').tr(), + actions: + isDesktop + ? [ + IconButton( + icon: const Icon(Symbols.help_outline), + onPressed: () { + // Show help dialog + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: Text('settingsHelp').tr(), + content: Text('settingsHelpContent').tr(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close').tr(), + ), + ], + ), + ); + }, + ), + ] + : null, + ), + body: Focus( + autofocus: true, + onKeyEvent: (node, event) { + // Add keyboard shortcuts for desktop + if (isDesktop && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + context.router.pop(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 16), + child: buildSettingsList(), ), ), ); } } + +// Helper widget for displaying settings sections with titles +class _SettingsSection extends StatelessWidget { + final String title; + final List children; + + const _SettingsSection({required this.title, required this.children}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ...children, + const SizedBox(height: 16), + ], + ); + } +} + +// Helper widget for displaying keyboard shortcuts +class _ShortcutRow extends StatelessWidget { + final String shortcut; + final String description; + + const _ShortcutRow({required this.shortcut, required this.description}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + ), + ), + child: Text(shortcut, style: TextStyle(fontFamily: 'monospace')), + ), + SizedBox(width: 16), + Text(description), + ], + ), + ); + } +}