Compare commits

..

4 Commits

Author SHA1 Message Date
5364aecf74 Optimize image cache size 2024-11-10 22:56:18 +08:00
a673beb87c Post detail 2024-11-10 22:56:09 +08:00
49cabd1f39 Base url 2024-11-10 22:14:27 +08:00
ac70624c4e Background image & appearance settings 2024-11-10 21:48:42 +08:00
17 changed files with 659 additions and 57 deletions

View File

@ -14,6 +14,7 @@
"screenAccountPublisherNew": "New Publisher",
"screenAccountPublisherEdit": "Edit Publisher",
"screenAccountProfileEdit": "Edit Profile",
"screenSettings": "Settings",
"dialogOkay": "Okay",
"dialogCancel": "Cancel",
"dialogConfirm": "Confirm",
@ -36,6 +37,9 @@
"report": "Report",
"repost": "Repost",
"reply": "Reply",
"untitled": "Untitled",
"postDetail": "Post detail",
"postNoun": "Post",
"fieldUsername": "Username",
"fieldNickname": "Nickname",
"fieldEmail": "Email address",
@ -80,5 +84,26 @@
"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": {
"zero": "Comment",
"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.",
"settingsNetworkServerPreset": "Present HyperNet Server",
"settingsNetworkServerPresetDescription": "You can choose one of our preset HyperNet server addresses from the list on the right.",
"settingsNetworkServerSaved": "Server address saved."
}

View File

@ -14,6 +14,7 @@
"screenAccountPublisherNew": "新建发布者",
"screenAccountPublisherEdit": "编辑发布者",
"screenAccountProfileEdit": "编辑资料",
"screenSettings": "设置",
"dialogOkay": "好的",
"dialogCancel": "取消",
"dialogConfirm": "确认",
@ -36,6 +37,9 @@
"report": "检举",
"repost": "转帖",
"reply": "回贴",
"untitled": "无题",
"postDetail": "帖子详情",
"postNoun": "帖子",
"fieldUsername": "用户名",
"fieldNickname": "显示名",
"fieldEmail": "电子邮箱地址",
@ -80,5 +84,26 @@
"postPublish": "发布",
"postEditingNotice": "你正在修改由 {} 发布的帖子。",
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
"postRepostingNotice": "你正在转发由 {} 发布的帖子。"
"postRepostingNotice": "你正在转发由 {} 发布的帖子。",
"postReact": "反应",
"postComments": {
"zero": "评论",
"one": "{} 条评论",
"other": "{} 条评论"
},
"settingsAppearance": "外观",
"settingsBackgroundImage": "背景图片",
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
"settingsBackgroundImageClear": "清除现存背景图",
"settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
"settingsThemeMaterial3": "使用 Material You 设计范式",
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsNetwork": "网络",
"settingsNetworkServer": "HyperNet 服务器",
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
"settingsNetworkServerReset": "重设为官方服务器",
"settingsNetworkServerResetDescription": "重设为 Solar Network 的服务器地址。",
"settingsNetworkServerPreset": "预设的 HyperNet 服务器",
"settingsNetworkServerPresetDescription": "你可以在旁边的列表中选择我们提供的预设 HyperNet 服务器地址。",
"settingsNetworkServerSaved": "服务器地址已保存。"
}

View File

@ -1,6 +1,7 @@
import 'package:croppy/croppy.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
@ -15,6 +16,10 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
if (!kReleaseMode) {
debugInvertOversizedImages = true;
}
runApp(const SolianApp());
}
@ -37,25 +42,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 +53,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,
);
}
}

View File

@ -4,25 +4,30 @@ import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/adapters/sn_network_universal.dart';
const kUseLocalNetwork = true;
const kAtkStoreKey = 'nex_user_atk';
const kRtkStoreKey = 'nex_user_rtk';
const kNetworkServerDefault = 'https://api.sn-next.solsynth.dev';
const kNetworkServerStoreKey = 'app_server_url';
const kNetworkServerDirectory = [
('SN Preview', 'https://api.sn-next.solsynth.dev'),
('SN Stable', 'https://api.sn.solsynth.dev'),
('Local', 'http://localhost:8001'),
];
class SnNetworkProvider {
late Dio client;
late final SharedPreferences _prefs;
late final FlutterSecureStorage _storage = FlutterSecureStorage();
SnNetworkProvider() {
client = Dio();
client.options.baseUrl = kUseLocalNetwork
? 'http://localhost:8001'
: 'https://api.sn.solsynth.dev';
client.interceptors.add(RetryInterceptor(
dio: client,
retries: 3,
@ -86,6 +91,12 @@ class SnNetworkProvider {
);
client = addClientAdapter(client);
SharedPreferences.getInstance().then((prefs) {
_prefs = prefs;
client.options.baseUrl =
_prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
});
}
String getAttachmentUrl(String ky) {
@ -112,9 +123,7 @@ class SnNetworkProvider {
if (rtk == null) return null;
final dio = Dio();
dio.options.baseUrl = kUseLocalNetwork
? 'http://localhost:8001'
: 'https://api.sn.solsynth.dev';
dio.options.baseUrl = client.options.baseUrl;
final resp = await dio.post('/cgi/id/auth/token', data: {
'grant_type': 'refresh_token',
@ -127,4 +136,8 @@ class SnNetworkProvider {
return atk;
}
void setBaseUrl(String url) {
client.options.baseUrl = url;
}
}

View File

@ -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();
});
}
}

View File

@ -8,7 +8,10 @@ import 'package:surface/screens/auth/login.dart';
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_detail.dart';
import 'package:surface/screens/post/post_editor.dart';
import 'package:surface/screens/settings.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
final appRouter = GoRouter(
@ -57,6 +60,14 @@ final appRouter = GoRouter(
),
),
),
GoRoute(
path: '/post/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
)
],
),
ShellRoute(
@ -99,5 +110,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(),
),
],
),
],
);

View File

@ -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

View File

@ -171,7 +171,16 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts,
itemBuilder: (context, idx) {
return PostItem(data: _posts[idx]);
return GestureDetector(
child: PostItem(data: _posts[idx]),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
);
},
separatorBuilder: (context, index) => const Divider(),
)

View File

@ -0,0 +1,102 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart';
class PostDetailScreen extends StatefulWidget {
final String slug;
final SnPost? preload;
const PostDetailScreen({
super.key,
required this.slug,
this.preload,
});
@override
State<PostDetailScreen> createState() => _PostDetailScreenState();
}
class _PostDetailScreenState extends State<PostDetailScreen> {
bool _isBusy = false;
SnPost? _data;
void _fetchPost() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
final resp = await sn.client.get('/cgi/co/posts/${widget.slug}');
if (!mounted) return;
final attachments = await attach.getMultiple(
resp.data['body']['attachments']?.cast<String>() ?? [],
);
if (!mounted) return;
setState(() {
_data = SnPost.fromJson(resp.data).copyWith(
preload: SnPostPreload(
attachments: attachments,
),
);
});
} catch (err) {
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
if (widget.preload != null) {
_data = widget.preload;
}
_fetchPost();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (GoRouter.of(context).canPop()) {
Navigator.pop(context);
}
GoRouter.of(context).replaceNamed('explore');
},
),
flexibleSpace: Column(
children: [
Text(_data?.body['title'] ?? 'postNoun'.tr())
.textStyle(Theme.of(context).textTheme.titleLarge!)
.textColor(Colors.white),
Text('postDetail')
.tr()
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
],
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
),
body: SingleChildScrollView(
child: Column(
children: [
LoadingIndicator(isActive: _isBusy),
if (_data != null) PostItem(data: _data!),
],
),
),
);
}
}

View File

@ -1,3 +1,6 @@
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
@ -275,14 +278,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
flexibleSpace: Column(
children: [
Text(_title ?? 'Untitled')
Text(_title ?? 'untitled'.tr())
.textStyle(Theme.of(context).textTheme.titleLarge!)
.textColor(Colors.white),
Text(_kTitleMap[widget.mode]!)
.tr()
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
],
).padding(top: MediaQuery.of(context).padding.top),
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
actions: [
IconButton(
icon: const Icon(Symbols.tune),
@ -468,8 +471,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(),
),
),
),

241
lib/screens/settings.dart Normal file
View File

@ -0,0 +1,241 @@
import 'dart:io';
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: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/sn_network.dart';
import 'package:surface/providers/theme.dart';
import 'package:surface/theme.dart';
import 'package:surface/widgets/dialog.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 = '/';
final TextEditingController _serverUrlController = TextEditingController();
@override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((dir) {
_docBasepath = dir.path;
if (mounted) {
setState(() {});
}
});
SharedPreferences.getInstance().then((prefs) {
setState(() {
_prefs = prefs;
_serverUrlController.text =
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
});
});
}
@override
void dispose() {
_serverUrlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return AppScaffold(
body: SingleChildScrollView(
child: Column(
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');
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, 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: 40,
width: 140,
),
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(() {});
},
),
],
),
].expand((ele) => [ele, const Gap(16)]).toList(),
).padding(vertical: 20),
),
);
}
}

View File

@ -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,35 @@ 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) {
return ThemeData(
useMaterial3: false,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: brightness,
),
Future<ThemeData> createAppTheme(
Brightness brightness, {
bool? useMaterial3,
}) async {
final prefs = await SharedPreferences.getInstance();
final colorScheme = ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: brightness,
iconTheme: const IconThemeData(fill: 0, weight: 400, opticalSize: 20),
);
return ThemeData(
useMaterial3:
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
colorScheme: colorScheme,
brightness: brightness,
iconTheme: IconThemeData(
fill: 0,
weight: 400,
opticalSize: 20,
color: colorScheme.onSurface,
),
scaffoldBackgroundColor: Colors.transparent,
);
}

View File

@ -23,10 +23,14 @@ class AttachmentItem extends StatelessWidget {
case 'image':
return Hero(
tag: 'attachment-${data.rid}-$heroTag',
child: UniversalImage(
sn.getAttachmentUrl(data.rid),
fit: BoxFit.cover,
),
child: LayoutBuilder(builder: (context, constraints) {
return UniversalImage(
sn.getAttachmentUrl(data.rid),
fit: BoxFit.cover,
cacheHeight: constraints.maxHeight,
cacheWidth: constraints.maxWidth,
);
}),
);
default:
return const Placeholder();

View File

@ -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,52 @@ class AppBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScaffoldMessenger(child: child);
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
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: LayoutBuilder(
builder: (context, constraints) {
return Container(
decoration: BoxDecoration(
backgroundBlendMode: BlendMode.darken,
color: Theme.of(context).colorScheme.surface,
image: DecorationImage(
opacity: 0.2,
image: ResizeImage(
FileImage(file),
width: (constraints.maxWidth * devicePixelRatio)
.round(),
height: (constraints.maxHeight * devicePixelRatio)
.round(),
policy: ResizeImagePolicy.fit,
),
fit: BoxFit.cover,
),
),
child: child,
);
},
),
);
}
}
return Material(
color: Theme.of(context).colorScheme.surface,
child: child,
);
},
),
);
}
}

View File

@ -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({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);
);
}
}

View File

@ -199,7 +199,7 @@ packages:
source: hosted
version: "4.10.1"
collection:
dependency: transitive
dependency: "direct main"
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
@ -987,7 +987,7 @@ packages:
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"

View File

@ -71,6 +71,8 @@ dependencies:
uuid: ^4.5.1
photo_view: ^0.15.0
shared_preferences: ^2.3.3
path_provider: ^2.1.5
collection: ^1.19.0
dev_dependencies:
flutter_test: