From ac70624c4ea16a3ab593032baaeea1ed457ff2ef Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 10 Nov 2024 21:48:42 +0800 Subject: [PATCH] :sparkles: Background image & appearance settings --- assets/translations/en-US.json | 20 ++- assets/translations/zh-CN.json | 20 ++- lib/main.dart | 44 ++++--- lib/providers/theme.dart | 14 ++- lib/router.dart | 14 +++ lib/screens/account.dart | 8 ++ lib/screens/post/post_editor.dart | 14 ++- lib/screens/settings.dart | 137 +++++++++++++++++++++ lib/theme.dart | 20 ++- lib/widgets/navigation/app_background.dart | 39 +++++- lib/widgets/post/post_item.dart | 53 +++++++- pubspec.lock | 2 +- pubspec.yaml | 3 + 13 files changed, 354 insertions(+), 34 deletions(-) create mode 100644 lib/screens/settings.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 91b1fd6..71ac580 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -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." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 3771aa0..54dfbbc 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -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 的服务器地址。" } diff --git a/lib/main.dart b/lib/main.dart index 710fb32..7186d3e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); - - final th = context.watch(); - - 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(); + + final th = context.watch(); + + 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, + ); + } +} diff --git a/lib/providers/theme.dart b/lib/providers/theme.dart index d1cf683..be7ddd6 100644 --- a/lib/providers/theme.dart +++ b/lib/providers/theme.dart @@ -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(); + }); } } diff --git a/lib/router.dart b/lib/router.dart index da0f991..5335c67 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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(), + ), + ], + ), ], ); diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 53a2a9d..29efea4 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -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 diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 392d90d..4a77f50 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -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 { 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(), ), ), ), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart new file mode 100644 index 0000000..f5af0f8 --- /dev/null +++ b/lib/screens/settings.dart @@ -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 createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + 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( + 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(); + 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), + ), + ); + } +} diff --git a/lib/theme.dart b/lib/theme.dart index 18efbfd..cf9447f 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -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 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 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, ); } diff --git a/lib/widgets/navigation/app_background.dart b/lib/widgets/navigation/app_background.dart index f8c0ab5..16d5e7a 100644 --- a/lib/widgets/navigation/app_background.dart +++ b/lib/widgets/navigation/app_background.dart @@ -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, + ); + }, + ), + ); } } diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 9d2fbd5..c2dd9fb 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -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); + ); } } diff --git a/pubspec.lock b/pubspec.lock index 0b10651..02018a6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -987,7 +987,7 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" diff --git a/pubspec.yaml b/pubspec.yaml index 880a886..c907290 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: