401 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			401 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:io';
 | |
| 
 | |
| import 'package:collection/collection.dart';
 | |
| 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_colorpicker/flutter_colorpicker.dart';
 | |
| import 'package:go_router/go_router.dart';
 | |
| import 'package:image_picker/image_picker.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| import 'package:path_provider/path_provider.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:shared_preferences/shared_preferences.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| import 'package:surface/providers/config.dart';
 | |
| import 'package:surface/providers/sn_network.dart';
 | |
| import 'package:surface/providers/theme.dart';
 | |
| import 'package:surface/theme.dart';
 | |
| import 'package:surface/widgets/dialog.dart';
 | |
| 
 | |
| const Map<String, Color> kColorSchemes = {
 | |
|   'colorSchemeIndigo': Colors.indigo,
 | |
|   'colorSchemeBlue': Colors.blue,
 | |
|   'colorSchemeGreen': Colors.green,
 | |
|   'colorSchemeYellow': Colors.yellow,
 | |
|   'colorSchemeOrange': Colors.orange,
 | |
|   'colorSchemeRed': Colors.red,
 | |
|   'colorSchemeWhite': Colors.white,
 | |
|   'colorSchemeBlack': Colors.black,
 | |
| };
 | |
| 
 | |
| class SettingsScreen extends StatefulWidget {
 | |
|   const SettingsScreen({super.key});
 | |
| 
 | |
|   @override
 | |
|   State<SettingsScreen> createState() => _SettingsScreenState();
 | |
| }
 | |
| 
 | |
| class _SettingsScreenState extends State<SettingsScreen> {
 | |
|   late final SharedPreferences _prefs;
 | |
|   String _docBasepath = '/';
 | |
| 
 | |
|   final TextEditingController _serverUrlController = TextEditingController();
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     getApplicationDocumentsDirectory().then((dir) {
 | |
|       _docBasepath = dir.path;
 | |
|       if (mounted) {
 | |
|         setState(() {});
 | |
|       }
 | |
|     });
 | |
|     final config = context.read<ConfigProvider>();
 | |
|     _prefs = config.prefs;
 | |
|     _serverUrlController.text = config.serverUrl;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _serverUrlController.dispose();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final sn = context.read<SnNetworkProvider>();
 | |
| 
 | |
|     return Scaffold(
 | |
|       body: SingleChildScrollView(
 | |
|         child: Column(
 | |
|           spacing: 16,
 | |
|           crossAxisAlignment: CrossAxisAlignment.start,
 | |
|           children: [
 | |
|             Column(
 | |
|               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|               children: [
 | |
|                 Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | |
|                 if (!kIsWeb)
 | |
|                   ListTile(
 | |
|                     title: Text('settingsBackgroundImage').tr(),
 | |
|                     subtitle: Text('settingsBackgroundImageDescription').tr(),
 | |
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                     leading: const Icon(Symbols.image),
 | |
|                     trailing: const Icon(Symbols.chevron_right),
 | |
|                     onTap: () async {
 | |
|                       final image = await ImagePicker().pickImage(source: ImageSource.gallery);
 | |
|                       if (image == null) return;
 | |
| 
 | |
|                       await File(image.path).copy('$_docBasepath/app_background_image');
 | |
|                       _prefs.setBool(kAppBackgroundStoreKey, true);
 | |
| 
 | |
|                       setState(() {});
 | |
|                     },
 | |
|                   ),
 | |
|                 if (!kIsWeb)
 | |
|                   FutureBuilder<bool>(
 | |
|                       future: File('$_docBasepath/app_background_image').exists(),
 | |
|                       builder: (context, snapshot) {
 | |
|                         if (!snapshot.hasData || !snapshot.data!) {
 | |
|                           return const SizedBox.shrink();
 | |
|                         }
 | |
| 
 | |
|                         return ListTile(
 | |
|                           title: Text('settingsBackgroundImageClear').tr(),
 | |
|                           subtitle: Text('settingsBackgroundImageClearDescription').tr(),
 | |
|                           contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                           leading: const Icon(Symbols.texture),
 | |
|                           trailing: const Icon(Symbols.chevron_right),
 | |
|                           onTap: () {
 | |
|                             File('$_docBasepath/app_background_image').deleteSync();
 | |
|                             _prefs.remove(kAppBackgroundStoreKey);
 | |
|                             setState(() {});
 | |
|                           },
 | |
|                         );
 | |
|                       }),
 | |
|                 CheckboxListTile(
 | |
|                   title: Text('settingsThemeMaterial3').tr(),
 | |
|                   subtitle: Text('settingsThemeMaterial3Description').tr(),
 | |
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | |
|                   secondary: const Icon(Symbols.new_releases),
 | |
|                   value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
 | |
|                   onChanged: (value) {
 | |
|                     setState(() {
 | |
|                       _prefs.setBool(
 | |
|                         kMaterialYouToggleStoreKey,
 | |
|                         value ?? false,
 | |
|                       );
 | |
|                     });
 | |
|                     final th = context.read<ThemeProvider>();
 | |
|                     th.reloadTheme(useMaterial3: value ?? false);
 | |
|                   },
 | |
|                 ),
 | |
|                 ListTile(
 | |
|                   leading: const Icon(Symbols.format_paint),
 | |
|                   title: Text('settingsColorScheme').tr(),
 | |
|                   subtitle: Text('settingsColorSchemeDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                   trailing: const Icon(Symbols.chevron_right),
 | |
|                   onTap: () async {
 | |
|                     Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
 | |
|                     final color = await showDialog<Color?>(
 | |
|                       context: context,
 | |
|                       builder: (context) => AlertDialog(
 | |
|                         content: SingleChildScrollView(
 | |
|                           child: ColorPicker(
 | |
|                             pickerColor: pickerColor,
 | |
|                             onColorChanged: (color) => pickerColor = color,
 | |
|                             enableAlpha: false,
 | |
|                             hexInputBar: true,
 | |
|                           ),
 | |
|                         ),
 | |
|                         actions: <Widget>[
 | |
|                           TextButton(
 | |
|                             child: const Text('dialogDismiss').tr(),
 | |
|                             onPressed: () {
 | |
|                               Navigator.of(context).pop();
 | |
|                             },
 | |
|                           ),
 | |
|                           TextButton(
 | |
|                             child: const Text('dialogConfirm').tr(),
 | |
|                             onPressed: () {
 | |
|                               Navigator.of(context).pop(pickerColor);
 | |
|                             },
 | |
|                           ),
 | |
|                         ],
 | |
|                       ),
 | |
|                     );
 | |
| 
 | |
|                     if (color == null || !context.mounted) return;
 | |
| 
 | |
|                     _prefs.setInt(kAppColorSchemeStoreKey, color.value);
 | |
|                     final th = context.read<ThemeProvider>();
 | |
|                     th.reloadTheme(seedColorOverride: color);
 | |
|                     setState(() {});
 | |
| 
 | |
|                     context.showSnackbar('colorSchemeApplied'.tr());
 | |
|                   },
 | |
|                 ),
 | |
|                 ListTile(
 | |
|                   leading: const Icon(Symbols.palette),
 | |
|                   title: Text('settingsColorSeed').tr(),
 | |
|                   subtitle: Text('settingsColorSeedDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | |
|                   trailing: DropdownButtonHideUnderline(
 | |
|                     child: DropdownButton2<int?>(
 | |
|                       isExpanded: true,
 | |
|                       items: [
 | |
|                         ...kColorSchemes.entries.mapIndexed((idx, ele) {
 | |
|                           return DropdownMenuItem<int>(
 | |
|                             value: idx,
 | |
|                             child: Text(ele.key).tr(),
 | |
|                           );
 | |
|                         }),
 | |
|                         DropdownMenuItem<int>(
 | |
|                           value: -1,
 | |
|                           child: Text('custom').tr(),
 | |
|                         ),
 | |
|                       ],
 | |
|                       value: _prefs.getInt(kAppColorSchemeStoreKey) == null
 | |
|                           ? 1
 | |
|                           : kColorSchemes.values
 | |
|                               .toList()
 | |
|                               .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
 | |
|                       onChanged: (int? value) {
 | |
|                         if (value != null && value != -1) {
 | |
|                           _prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
 | |
|                           final th = context.read<ThemeProvider>();
 | |
|                           th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
 | |
|                           setState(() {});
 | |
| 
 | |
|                           context.showSnackbar('colorSchemeApplied'.tr());
 | |
|                         }
 | |
|                       },
 | |
|                       buttonStyleData: const ButtonStyleData(
 | |
|                         padding: EdgeInsets.symmetric(
 | |
|                           horizontal: 16,
 | |
|                           vertical: 5,
 | |
|                         ),
 | |
|                         height: 40,
 | |
|                         width: 160,
 | |
|                       ),
 | |
|                       menuItemStyleData: const MenuItemStyleData(
 | |
|                         height: 40,
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ),
 | |
|                 CheckboxListTile(
 | |
|                   secondary: const Icon(Symbols.blur_on),
 | |
|                   title: Text('settingsAppBarTransparent').tr(),
 | |
|                   subtitle: Text('settingsAppBarTransparentDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | |
|                   value: _prefs.getBool(kAppbarTransparentStoreKey) ?? false,
 | |
|                   onChanged: (value) {
 | |
|                     _prefs.setBool(kAppbarTransparentStoreKey, value ?? false);
 | |
|                     final th = context.read<ThemeProvider>();
 | |
|                     th.reloadTheme();
 | |
|                     setState(() {});
 | |
|                   },
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|             Column(
 | |
|               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|               children: [
 | |
|                 Text('settingsNetwork').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | |
|                 TextField(
 | |
|                   controller: _serverUrlController,
 | |
|                   decoration: InputDecoration(
 | |
|                     border: const OutlineInputBorder(),
 | |
|                     labelText: 'settingsNetworkServer'.tr(),
 | |
|                     helperText: 'settingsNetworkServerDescription'.tr(),
 | |
|                     prefixIcon: const Icon(Symbols.dns),
 | |
|                     suffixIcon: IconButton(
 | |
|                       icon: const Icon(Symbols.save),
 | |
|                       onPressed: () {
 | |
|                         sn.setBaseUrl(_serverUrlController.text);
 | |
|                         _prefs.setString(
 | |
|                           kNetworkServerStoreKey,
 | |
|                           _serverUrlController.text,
 | |
|                         );
 | |
|                         context.showSnackbar('settingsNetworkServerSaved'.tr());
 | |
|                         setState(() {});
 | |
|                       },
 | |
|                     ),
 | |
|                   ),
 | |
|                   onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | |
|                 ).padding(horizontal: 16, top: 8, bottom: 4),
 | |
|                 ListTile(
 | |
|                   title: Text('settingsNetworkServerPreset').tr(),
 | |
|                   subtitle: Text('settingsNetworkServerPresetDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                   leading: const Icon(Symbols.lists),
 | |
|                   trailing: DropdownButtonHideUnderline(
 | |
|                     child: DropdownButton2<String>(
 | |
|                       isExpanded: true,
 | |
|                       items: [
 | |
|                         ...kNetworkServerDirectory,
 | |
|                         if (!kNetworkServerDirectory.map((ele) => ele.$2).contains(_serverUrlController.text))
 | |
|                           ('Custom', _serverUrlController.text),
 | |
|                       ]
 | |
|                           .map(
 | |
|                             (item) => DropdownMenuItem<String>(
 | |
|                               value: item.$2,
 | |
|                               child: Column(
 | |
|                                 mainAxisSize: MainAxisSize.max,
 | |
|                                 mainAxisAlignment: MainAxisAlignment.center,
 | |
|                                 crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                                 children: [
 | |
|                                   Text(item.$1).fontSize(14),
 | |
|                                   Text(item.$2, overflow: TextOverflow.ellipsis).fontSize(11)
 | |
|                                 ],
 | |
|                               ),
 | |
|                             ),
 | |
|                           )
 | |
|                           .toList(),
 | |
|                       value: _serverUrlController.text,
 | |
|                       onChanged: (String? value) {
 | |
|                         if (value == null) return;
 | |
|                         _serverUrlController.text = value;
 | |
|                         _prefs.setString(kNetworkServerStoreKey, value);
 | |
|                         context.showSnackbar('settingsNetworkServerSaved'.tr());
 | |
|                         setState(() {});
 | |
|                       },
 | |
|                       buttonStyleData: const ButtonStyleData(
 | |
|                         padding: EdgeInsets.symmetric(
 | |
|                           horizontal: 16,
 | |
|                           vertical: 5,
 | |
|                         ),
 | |
|                         height: 56,
 | |
|                         width: 160,
 | |
|                       ),
 | |
|                       menuItemStyleData: const MenuItemStyleData(
 | |
|                         height: 60,
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ),
 | |
|                 ListTile(
 | |
|                   title: Text('settingsNetworkServerReset').tr(),
 | |
|                   subtitle: Text('settingsNetworkServerResetDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                   leading: const Icon(Symbols.reset_wrench),
 | |
|                   trailing: const Icon(Symbols.chevron_right),
 | |
|                   onTap: () {
 | |
|                     _serverUrlController.text = kNetworkServerDefault;
 | |
|                     _prefs.remove(kNetworkServerStoreKey);
 | |
|                     context.showSnackbar('settingsNetworkServerSaved'.tr());
 | |
|                     setState(() {});
 | |
|                   },
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|             Column(
 | |
|               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|               children: [
 | |
|                 Text('settingsPerformance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | |
|                 ListTile(
 | |
|                   title: Text('settingsImageQuality').tr(),
 | |
|                   subtitle: Text('settingsImageQualityDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                   leading: const Icon(Symbols.image),
 | |
|                   trailing: DropdownButtonHideUnderline(
 | |
|                     child: DropdownButton2<FilterQuality>(
 | |
|                       value: kImageQualityLevel.values.elementAtOrNull(_prefs.getInt('app_image_quality') ?? 3) ??
 | |
|                           FilterQuality.high,
 | |
|                       isExpanded: true,
 | |
|                       items: kImageQualityLevel.entries
 | |
|                           .map(
 | |
|                             (item) => DropdownMenuItem<FilterQuality>(
 | |
|                               value: item.value,
 | |
|                               child: Text(item.key).tr().fontSize(14),
 | |
|                             ),
 | |
|                           )
 | |
|                           .toList(),
 | |
|                       onChanged: (FilterQuality? value) {
 | |
|                         if (value == null) return;
 | |
|                         _prefs.setInt('app_image_quality', kImageQualityLevel.values.toList().indexOf(value));
 | |
|                         setState(() {});
 | |
|                       },
 | |
|                       buttonStyleData: const ButtonStyleData(
 | |
|                         padding: EdgeInsets.symmetric(
 | |
|                           horizontal: 16,
 | |
|                           vertical: 5,
 | |
|                         ),
 | |
|                         height: 40,
 | |
|                         width: 160,
 | |
|                       ),
 | |
|                       menuItemStyleData: const MenuItemStyleData(
 | |
|                         height: 60,
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|             Column(
 | |
|               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|               children: [
 | |
|                 Text('settingsMisc').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | |
|                 ListTile(
 | |
|                   title: Text('settingsMiscAbout').tr(),
 | |
|                   subtitle: Text('settingsMiscAboutDescription').tr(),
 | |
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | |
|                   leading: const Icon(Symbols.info),
 | |
|                   trailing: const Icon(Symbols.chevron_right),
 | |
|                   onTap: () async {
 | |
|                     GoRouter.of(context).pushNamed('about');
 | |
|                   },
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           ],
 | |
|         ).padding(vertical: 20),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |