✨ Post detail
This commit is contained in:
		| @@ -37,6 +37,9 @@ | ||||
|   "report": "Report", | ||||
|   "repost": "Repost", | ||||
|   "reply": "Reply", | ||||
|   "untitled": "Untitled", | ||||
|   "postDetail": "Post detail", | ||||
|   "postNoun": "Post", | ||||
|   "fieldUsername": "Username", | ||||
|   "fieldNickname": "Nickname", | ||||
|   "fieldEmail": "Email address", | ||||
| @@ -84,6 +87,7 @@ | ||||
|   "postRepostingNotice": "You're about to repost a post that posted {}.", | ||||
|   "postReact": "React", | ||||
|   "postComments": { | ||||
|     "zero": "Comment", | ||||
|     "one": "{} comment", | ||||
|     "other": "{} comments" | ||||
|   }, | ||||
|   | ||||
| @@ -37,6 +37,9 @@ | ||||
|   "report": "检举", | ||||
|   "repost": "转帖", | ||||
|   "reply": "回贴", | ||||
|   "untitled": "无题", | ||||
|   "postDetail": "帖子详情", | ||||
|   "postNoun": "帖子", | ||||
|   "fieldUsername": "用户名", | ||||
|   "fieldNickname": "显示名", | ||||
|   "fieldEmail": "电子邮箱地址", | ||||
| @@ -84,6 +87,7 @@ | ||||
|   "postRepostingNotice": "你正在转发由 {} 发布的帖子。", | ||||
|   "postReact": "反应", | ||||
|   "postComments": { | ||||
|     "zero": "评论", | ||||
|     "one": "{} 条评论", | ||||
|     "other": "{} 条评论" | ||||
|   }, | ||||
|   | ||||
| @@ -8,8 +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( | ||||
| @@ -58,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( | ||||
|   | ||||
| @@ -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(), | ||||
|             ) | ||||
|   | ||||
							
								
								
									
										102
									
								
								lib/screens/post/post_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								lib/screens/post/post_detail.dart
									
									
									
									
									
										Normal 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!), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,3 +1,5 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| @@ -276,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), | ||||
|   | ||||
| @@ -70,7 +70,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     .bold() | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20), | ||||
|                     .padding(horizontal: 20, bottom: 4), | ||||
|                 if (!kIsWeb) | ||||
|                   ListTile( | ||||
|                     title: Text('settingsBackgroundImage').tr(), | ||||
| @@ -141,7 +141,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     .bold() | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20), | ||||
|                     .padding(horizontal: 20, bottom: 4), | ||||
|                 TextField( | ||||
|                   controller: _serverUrlController, | ||||
|                   decoration: InputDecoration( | ||||
|   | ||||
| @@ -23,15 +23,22 @@ Future<ThemeData> createAppTheme( | ||||
| }) async { | ||||
|   final prefs = await SharedPreferences.getInstance(); | ||||
|  | ||||
|   final colorScheme = ColorScheme.fromSeed( | ||||
|     seedColor: Colors.indigo, | ||||
|     brightness: brightness, | ||||
|   ); | ||||
|  | ||||
|   return ThemeData( | ||||
|     useMaterial3: | ||||
|         useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), | ||||
|     colorScheme: ColorScheme.fromSeed( | ||||
|       seedColor: Colors.indigo, | ||||
|       brightness: brightness, | ||||
|     ), | ||||
|     colorScheme: colorScheme, | ||||
|     brightness: brightness, | ||||
|     iconTheme: const IconThemeData(fill: 0, weight: 400, opticalSize: 20), | ||||
|     iconTheme: IconThemeData( | ||||
|       fill: 0, | ||||
|       weight: 400, | ||||
|       opticalSize: 20, | ||||
|       color: colorScheme.onSurface, | ||||
|     ), | ||||
|     scaffoldBackgroundColor: Colors.transparent, | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -199,7 +199,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "4.10.1" | ||||
|   collection: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: collection | ||||
|       sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf | ||||
|   | ||||
| @@ -30,8 +30,6 @@ 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. | ||||
| @@ -74,6 +72,7 @@ dependencies: | ||||
|   photo_view: ^0.15.0 | ||||
|   shared_preferences: ^2.3.3 | ||||
|   path_provider: ^2.1.5 | ||||
|   collection: ^1.19.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user