✨ Background image & appearance settings
This commit is contained in:
parent
c1e10916ee
commit
ac70624c4e
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user