✨ Background image & appearance settings
This commit is contained in:
		| @@ -14,6 +14,7 @@ | ||||
|   "screenAccountPublisherNew": "New Publisher", | ||||
|   "screenAccountPublisherEdit": "Edit Publisher", | ||||
|   "screenAccountProfileEdit": "Edit Profile", | ||||
|   "screenSettings": "Settings", | ||||
|   "dialogOkay": "Okay", | ||||
|   "dialogCancel": "Cancel", | ||||
|   "dialogConfirm": "Confirm", | ||||
| @@ -80,5 +81,22 @@ | ||||
|   "postPublish": "Publish", | ||||
|   "postEditingNotice": "You're about to editing a post that posted {}.", | ||||
|   "postReplyingNotice": "You're about to reply to a post that posted {}.", | ||||
|   "postRepostingNotice": "You're about to repost a post that posted {}." | ||||
|   "postRepostingNotice": "You're about to repost a post that posted {}.", | ||||
|   "postReact": "React", | ||||
|   "postComments": { | ||||
|     "one": "{} comment", | ||||
|     "other": "{} comments" | ||||
|   }, | ||||
|   "settingsAppearance": "Appearance", | ||||
|   "settingsBackgroundImage": "Background Image", | ||||
|   "settingsBackgroundImageDescription": "Set the background image that will be applied globally.", | ||||
|   "settingsBackgroundImageClear": "Clear Existing Background Image", | ||||
|   "settingsBackgroundImageClearDescription": "Reset the background image to blank.", | ||||
|   "settingsThemeMaterial3": "Use Material You Design", | ||||
|   "settingsThemeMaterial3Description": "Set the application theme to Material 3 Design.", | ||||
|   "settingsNetwork": "Network", | ||||
|   "settingsNetworkServer": "HyperNet Server", | ||||
|   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", | ||||
|   "settingsNetworkServerReset": "Reset to Official Server", | ||||
|   "settingsNetworkServerResetDescription": "Reset to the official server address of Solar Network." | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
|   "screenAccountPublisherNew": "新建发布者", | ||||
|   "screenAccountPublisherEdit": "编辑发布者", | ||||
|   "screenAccountProfileEdit": "编辑资料", | ||||
|   "screenSettings": "设置", | ||||
|   "dialogOkay": "好的", | ||||
|   "dialogCancel": "取消", | ||||
|   "dialogConfirm": "确认", | ||||
| @@ -80,5 +81,22 @@ | ||||
|   "postPublish": "发布", | ||||
|   "postEditingNotice": "你正在修改由 {} 发布的帖子。", | ||||
|   "postReplyingNotice": "你正在回复由 {} 发布的帖子。", | ||||
|   "postRepostingNotice": "你正在转发由 {} 发布的帖子。" | ||||
|   "postRepostingNotice": "你正在转发由 {} 发布的帖子。", | ||||
|   "postReact": "反应", | ||||
|   "postComments": { | ||||
|     "one": "{} 条评论", | ||||
|     "other": "{} 条评论" | ||||
|   }, | ||||
|   "settingsAppearance": "外观", | ||||
|   "settingsBackgroundImage": "背景图片", | ||||
|   "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。", | ||||
|   "settingsBackgroundImageClear": "清除现存背景图", | ||||
|   "settingsBackgroundImageClearDescription": "将应用背景图重置为空白。", | ||||
|   "settingsThemeMaterial3": "使用 Material You 设计范式", | ||||
|   "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", | ||||
|   "settingsNetwork": "网络", | ||||
|   "settingsNetworkServer": "HyperNet 服务器", | ||||
|   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", | ||||
|   "settingsNetworkServerReset": "重设为官方服务器", | ||||
|   "settingsNetworkServerResetDescription": "重设为 Solar Network 的服务器地址。" | ||||
| } | ||||
|   | ||||
| @@ -37,25 +37,7 @@ class SolianApp extends StatelessWidget { | ||||
|             ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (_) => ThemeProvider()), | ||||
|           ], | ||||
|           child: Builder(builder: (context) { | ||||
|             // Initialize some providers | ||||
|             context.read<UserProvider>(); | ||||
|  | ||||
|             final th = context.watch<ThemeProvider>(); | ||||
|  | ||||
|             return MaterialApp.router( | ||||
|               theme: th.theme.light, | ||||
|               darkTheme: th.theme.dark, | ||||
|               locale: context.locale, | ||||
|               supportedLocales: context.supportedLocales, | ||||
|               localizationsDelegates: [ | ||||
|                 CroppyLocalizations.delegate, | ||||
|                 RelativeTimeLocalizations.delegate, | ||||
|                 ...context.localizationDelegates, | ||||
|               ], | ||||
|               routerConfig: appRouter, | ||||
|             ); | ||||
|           }), | ||||
|           child: AppMainContent(), | ||||
|         ), | ||||
|       ), | ||||
|       breakpoints: [ | ||||
| @@ -66,3 +48,27 @@ class SolianApp extends StatelessWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AppMainContent extends StatelessWidget { | ||||
|   const AppMainContent({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     context.read<UserProvider>(); | ||||
|  | ||||
|     final th = context.watch<ThemeProvider>(); | ||||
|  | ||||
|     return MaterialApp.router( | ||||
|       theme: th.theme?.light, | ||||
|       darkTheme: th.theme?.dark, | ||||
|       locale: context.locale, | ||||
|       supportedLocales: context.supportedLocales, | ||||
|       localizationsDelegates: [ | ||||
|         CroppyLocalizations.delegate, | ||||
|         RelativeTimeLocalizations.delegate, | ||||
|         ...context.localizationDelegates, | ||||
|       ], | ||||
|       routerConfig: appRouter, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,9 +2,19 @@ import 'package:flutter/foundation.dart'; | ||||
| import 'package:surface/theme.dart'; | ||||
|  | ||||
| class ThemeProvider extends ChangeNotifier { | ||||
|   late ThemeSet theme; | ||||
|   ThemeSet? theme; | ||||
|  | ||||
|   ThemeProvider() { | ||||
|     theme = createAppThemeSet(); | ||||
|     createAppThemeSet().then((value) { | ||||
|       theme = value; | ||||
|       notifyListeners(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void reloadTheme({bool? useMaterial3}) { | ||||
|     createAppThemeSet().then((value) { | ||||
|       theme = value; | ||||
|       notifyListeners(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:surface/screens/auth/register.dart'; | ||||
| import 'package:surface/screens/explore.dart'; | ||||
| import 'package:surface/screens/home.dart'; | ||||
| import 'package:surface/screens/post/post_editor.dart'; | ||||
| import 'package:surface/screens/settings.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| final appRouter = GoRouter( | ||||
| @@ -99,5 +100,18 @@ final appRouter = GoRouter( | ||||
|         ), | ||||
|       ], | ||||
|     ), | ||||
|     ShellRoute( | ||||
|       builder: (context, state, child) => AppScaffold( | ||||
|         body: child, | ||||
|         autoImplyAppBar: true, | ||||
|       ), | ||||
|       routes: [ | ||||
|         GoRoute( | ||||
|           path: '/settings', | ||||
|           name: 'settings', | ||||
|           builder: (context, state) => const SettingsScreen(), | ||||
|         ), | ||||
|       ], | ||||
|     ), | ||||
|   ], | ||||
| ); | ||||
|   | ||||
| @@ -20,6 +20,14 @@ class AccountScreen extends StatelessWidget { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text("screenAccount").tr(), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.settings, fill: 1), | ||||
|             onPressed: () { | ||||
|               GoRouter.of(context).pushNamed('settings'); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: ua.isAuthorized | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -468,8 +469,17 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     onTapOutside: (_) => | ||||
|                         FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ) | ||||
|                 ].expand((ele) => [ele, const Gap(8)]).toList() | ||||
|                   ..removeLast(), | ||||
|                 ] | ||||
|                     .expandIndexed( | ||||
|                       (idx, ele) => [ | ||||
|                         if (idx != 0 || | ||||
|                             ![_editingOg, _replyingTo, _repostingTo] | ||||
|                                 .any((x) => x != null)) | ||||
|                           const Gap(8), | ||||
|                         ele, | ||||
|                       ], | ||||
|                     ) | ||||
|                     .toList(), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|   | ||||
							
								
								
									
										137
									
								
								lib/screens/settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								lib/screens/settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.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/theme.dart'; | ||||
| import 'package:surface/theme.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class SettingsScreen extends StatefulWidget { | ||||
|   const SettingsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<SettingsScreen> createState() => _SettingsScreenState(); | ||||
| } | ||||
|  | ||||
| class _SettingsScreenState extends State<SettingsScreen> { | ||||
|   SharedPreferences? _prefs; | ||||
|   String _docBasepath = '/'; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     getApplicationDocumentsDirectory().then((dir) { | ||||
|       _docBasepath = dir.path; | ||||
|       if (mounted) { | ||||
|         setState(() {}); | ||||
|       } | ||||
|     }); | ||||
|     SharedPreferences.getInstance().then((prefs) { | ||||
|       setState(() { | ||||
|         _prefs = prefs; | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('settingsAppearance') | ||||
|                     .bold() | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20), | ||||
|                 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'); | ||||
|  | ||||
|                       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(); | ||||
|                             setState(() {}); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }), | ||||
|                 if (_prefs != null) | ||||
|                   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.watch<ThemeProvider>(); | ||||
|                       th.reloadTheme(useMaterial3: value ?? false); | ||||
|                     }, | ||||
|                   ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('settingsNetwork') | ||||
|                     .bold() | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20), | ||||
|               ], | ||||
|             ), | ||||
|           ].expand((ele) => [ele, const Gap(16)]).toList(), | ||||
|         ).padding(vertical: 20), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,7 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| const kMaterialYouToggleStoreKey = 'app_theme_material_you'; | ||||
|  | ||||
| class ThemeSet { | ||||
|   ThemeData light; | ||||
| @@ -7,21 +10,28 @@ class ThemeSet { | ||||
|   ThemeSet({required this.light, required this.dark}); | ||||
| } | ||||
|  | ||||
| ThemeSet createAppThemeSet() { | ||||
| Future<ThemeSet> createAppThemeSet({bool? useMaterial3}) async { | ||||
|   return ThemeSet( | ||||
|     light: createAppTheme(Brightness.light), | ||||
|     dark: createAppTheme(Brightness.dark), | ||||
|     light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3), | ||||
|     dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| ThemeData createAppTheme(Brightness brightness) { | ||||
| Future<ThemeData> createAppTheme( | ||||
|   Brightness brightness, { | ||||
|   bool? useMaterial3, | ||||
| }) async { | ||||
|   final prefs = await SharedPreferences.getInstance(); | ||||
|  | ||||
|   return ThemeData( | ||||
|     useMaterial3: false, | ||||
|     useMaterial3: | ||||
|         useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), | ||||
|     colorScheme: ColorScheme.fromSeed( | ||||
|       seedColor: Colors.indigo, | ||||
|       brightness: brightness, | ||||
|     ), | ||||
|     brightness: brightness, | ||||
|     iconTheme: const IconThemeData(fill: 0, weight: 400, opticalSize: 20), | ||||
|     scaffoldBackgroundColor: Colors.transparent, | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,8 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
|  | ||||
| class AppBackground extends StatelessWidget { | ||||
|   final Widget child; | ||||
| @@ -6,6 +10,39 @@ class AppBackground extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ScaffoldMessenger(child: child); | ||||
|     return ScaffoldMessenger( | ||||
|       child: FutureBuilder( | ||||
|         future: | ||||
|             kIsWeb ? Future.value(null) : getApplicationDocumentsDirectory(), | ||||
|         builder: (context, snapshot) { | ||||
|           if (snapshot.hasData) { | ||||
|             final path = '${snapshot.data!.path}/app_background_image'; | ||||
|             final file = File(path); | ||||
|             if (file.existsSync()) { | ||||
|               return Container( | ||||
|                 color: Theme.of(context).colorScheme.surface, | ||||
|                 child: Container( | ||||
|                   decoration: BoxDecoration( | ||||
|                     backgroundBlendMode: BlendMode.darken, | ||||
|                     color: Theme.of(context).colorScheme.surface, | ||||
|                     image: DecorationImage( | ||||
|                       opacity: 0.2, | ||||
|                       image: FileImage(file), | ||||
|                       fit: BoxFit.cover, | ||||
|                     ), | ||||
|                   ), | ||||
|                   child: child, | ||||
|                 ), | ||||
|               ); | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           return Material( | ||||
|             color: Theme.of(context).colorScheme.surface, | ||||
|             child: child, | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -21,10 +21,59 @@ class PostItem extends StatelessWidget { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         _PostContentHeader(data: data), | ||||
|         _PostContentHeader(data: data).padding(horizontal: 12, vertical: 8), | ||||
|         _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), | ||||
|         if (data.preload?.attachments?.isNotEmpty ?? true) | ||||
|           AttachmentList(data: data.preload!.attachments!, bordered: true), | ||||
|         _PostBottomAction(data: data) | ||||
|             .padding(left: 20, right: 26, top: 8, bottom: 2), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostBottomAction extends StatelessWidget { | ||||
|   final SnPost data; | ||||
|   const _PostBottomAction({super.key, required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final iconColor = Theme.of(context).colorScheme.onSurface.withAlpha( | ||||
|           (255 * 0.8).round(), | ||||
|         ); | ||||
|     return Row( | ||||
|       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|       children: [ | ||||
|         Row( | ||||
|           children: [ | ||||
|             InkWell( | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   Icon(Symbols.add_reaction, size: 20, color: iconColor), | ||||
|                   const Gap(8), | ||||
|                   Text('postReact').tr(), | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () {}, | ||||
|             ), | ||||
|             const Gap(16), | ||||
|             InkWell( | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   Icon(Symbols.comment, size: 20, color: iconColor), | ||||
|                   const Gap(8), | ||||
|                   Text('postComments').plural(data.metric.replyCount), | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () {}, | ||||
|             ), | ||||
|           ].expand((ele) => [ele, const Gap(8)]).toList() | ||||
|             ..removeLast(), | ||||
|         ), | ||||
|         InkWell( | ||||
|           child: Icon(Symbols.share, size: 20, color: iconColor), | ||||
|           onTap: () {}, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| @@ -139,7 +188,7 @@ class _PostContentHeader extends StatelessWidget { | ||||
|           ], | ||||
|         ), | ||||
|       ], | ||||
|     ).padding(horizontal: 12, vertical: 8); | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -987,7 +987,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "1.9.0" | ||||
|   path_provider: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: path_provider | ||||
|       sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" | ||||
|   | ||||
| @@ -30,6 +30,8 @@ environment: | ||||
| dependencies: | ||||
|   flutter: | ||||
|     sdk: flutter | ||||
|   collection: | ||||
|     sdk: flutter | ||||
|  | ||||
|   # The following adds the Cupertino Icons font to your application. | ||||
|   # Use with the CupertinoIcons class for iOS style icons. | ||||
| @@ -71,6 +73,7 @@ dependencies: | ||||
|   uuid: ^4.5.1 | ||||
|   photo_view: ^0.15.0 | ||||
|   shared_preferences: ^2.3.3 | ||||
|   path_provider: ^2.1.5 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user