From edd86eda776d47c75efdc80182b321fdd705bc4f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 22 Feb 2025 13:13:38 +0800 Subject: [PATCH] :sparkles: Realm posts --- lib/providers/post.dart | 2 + lib/screens/post/post_editor.dart | 120 +++++++++++++++++++--------- lib/screens/realm/realm_detail.dart | 72 ++++++++++++++++- 3 files changed, 157 insertions(+), 37 deletions(-) diff --git a/lib/providers/post.dart b/lib/providers/post.dart index 98cd71f..b39f6ee 100644 --- a/lib/providers/post.dart +++ b/lib/providers/post.dart @@ -126,6 +126,7 @@ class SnPostContentProvider { String? author, Iterable? categories, Iterable? tags, + String? realm, }) async { final resp = await _sn.client.get('/cgi/co/posts', queryParameters: { 'take': take, @@ -134,6 +135,7 @@ class SnPostContentProvider { if (author != null) 'author': author, if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), + if (realm != null) 'realm': realm, }); final List out = await _preloadRelatedDataInBatch( List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 6b17f88..d1d46e6 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -368,14 +368,17 @@ class _PostEditorScreenState extends State { 'articles' => _PostArticleEditor( controller: _writeController, onTapPublisher: _showPublisherPopup, + onTapRealm: _showRealmPopup, ), 'questions' => _PostQuestionEditor( controller: _writeController, onTapPublisher: _showPublisherPopup, + onTapRealm: _showRealmPopup, ), 'videos' => _PostVideoEditor( controller: _writeController, onTapPublisher: _showPublisherPopup, + onTapRealm: _showRealmPopup, ), _ => const Placeholder(), }) @@ -494,7 +497,7 @@ class _PostEditorScreenState extends State { ), if (_writeController.mode == 'articles') IconButton( - icon: Icon(Symbols.image, color: Theme.of(context).colorScheme.primary), + icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary), style: ButtonStyle( backgroundColor: _writeController.thumbnail == null ? null @@ -686,7 +689,7 @@ class _PostStoryEditor extends StatelessWidget { ), ), ), - const Gap(10), + const Gap(11), Material( elevation: 1, borderRadius: const BorderRadius.all(Radius.circular(24)), @@ -744,8 +747,9 @@ class _PostStoryEditor extends StatelessWidget { class _PostArticleEditor extends StatelessWidget { final PostWriteController controller; final Function? onTapPublisher; + final Function? onTapRealm; - const _PostArticleEditor({required this.controller, this.onTapPublisher}); + const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); @override Widget build(BuildContext context) { @@ -766,6 +770,21 @@ class _PostArticleEditor extends StatelessWidget { ], ), ), + Material( + elevation: 1, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapRealm?.call(); + }, + child: AccountImage( + content: controller.realm?.avatar, + fallbackWidget: const Icon(Symbols.globe, size: 20), + radius: 14, + ), + ), + ), + const Gap(8), ], ).padding(horizontal: 12, vertical: 8), onTap: () { @@ -885,8 +904,9 @@ class _PostArticleEditor extends StatelessWidget { class _PostQuestionEditor extends StatelessWidget { final PostWriteController controller; final Function? onTapPublisher; + final Function? onTapRealm; - const _PostQuestionEditor({required this.controller, this.onTapPublisher}); + const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); @override Widget build(BuildContext context) { @@ -896,17 +916,36 @@ class _PostQuestionEditor extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Material( - elevation: 1, - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: GestureDetector( - onTap: () { - onTapPublisher?.call(); - }, - child: AccountImage( - content: controller.publisher?.avatar, + Column( + children: [ + Material( + elevation: 2, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapPublisher?.call(); + }, + child: AccountImage( + content: controller.publisher?.avatar, + ), + ), ), - ), + const Gap(11), + Material( + elevation: 1, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapRealm?.call(); + }, + child: AccountImage( + content: controller.realm?.avatar, + fallbackWidget: const Icon(Symbols.globe, size: 20), + radius: 14, + ), + ), + ), + ], ), Expanded( child: Column( @@ -960,8 +999,9 @@ class _PostQuestionEditor extends StatelessWidget { class _PostVideoEditor extends StatelessWidget { final PostWriteController controller; final Function? onTapPublisher; + final Function? onTapRealm; - const _PostVideoEditor({required this.controller, this.onTapPublisher}); + const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm}); void _selectVideo(BuildContext context) async { final video = await showDialog( @@ -1049,28 +1089,36 @@ class _PostVideoEditor extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Material( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: InkWell( - child: Row( - children: [ - AccountImage(content: controller.publisher?.avatar, radius: 20), - const Gap(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(controller.publisher?.nick ?? 'loading'.tr()).bold(), - Text('@${controller.publisher?.name}'), - ], - ), + Column( + children: [ + Material( + elevation: 2, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapPublisher?.call(); + }, + child: AccountImage( + content: controller.publisher?.avatar, ), - ], - ).padding(horizontal: 12, vertical: 8), - onTap: () { - onTapPublisher?.call(); - }, - ), + ), + ), + const Gap(11), + Material( + elevation: 1, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: GestureDetector( + onTap: () { + onTapRealm?.call(); + }, + child: AccountImage( + content: controller.realm?.avatar, + fallbackWidget: const Icon(Symbols.globe, size: 20), + radius: 14, + ), + ), + ), + ], ), const Gap(16), TextField( diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index d117409..cdd11f6 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.dart'; @@ -16,6 +17,7 @@ import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:surface/widgets/post/post_item.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class RealmDetailScreen extends StatefulWidget { @@ -90,7 +92,7 @@ class _RealmDetailScreenState extends State { @override Widget build(BuildContext context) { return DefaultTabController( - length: 3, + length: 4, child: AppScaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { @@ -102,6 +104,7 @@ class _RealmDetailScreenState extends State { bottom: TabBar( tabs: [ Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)), + Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)), Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)), Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)), ], @@ -113,6 +116,7 @@ class _RealmDetailScreenState extends State { body: TabBarView( children: [ _RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels), + _RealmPostListWidget(realm: _realm), _RealmMemberListWidget(realm: _realm), _RealmSettingsWidget( realm: _realm, @@ -232,6 +236,72 @@ class _RealmDetailHomeWidget extends StatelessWidget { } } +class _RealmPostListWidget extends StatefulWidget { + final SnRealm? realm; + + const _RealmPostListWidget({this.realm}); + + @override + State<_RealmPostListWidget> createState() => _RealmPostListWidgetState(); +} + +class _RealmPostListWidgetState extends State<_RealmPostListWidget> { + bool _isBusy = false; + int? _totalCount; + final List _posts = List.empty(growable: true); + + Future _fetchPosts() async { + setState(() => _isBusy = true); + + try { + final pt = context.read(); + final out = await pt.listPosts( + take: 10, + offset: _posts.length, + realm: widget.realm?.id.toString(), + ); + _totalCount = out.$2; + _posts.addAll(out.$1); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + Widget build(BuildContext context) { + return MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: _fetchPosts, + child: InfiniteList( + itemCount: _posts.length, + isLoading: _isBusy, + hasReachedMax: _totalCount != null && _posts.length >= _totalCount!, + onFetchData: _fetchPosts, + itemBuilder: (context, idx) { + final post = _posts[idx]; + return OpenablePostItem( + data: post, + maxWidth: 640, + onChanged: (data) { + setState(() => _posts[idx] = data); + }, + onDeleted: () { + setState(() => _posts.removeAt(idx)); + }, + ); + }, + separatorBuilder: (_, __) => const Gap(8), + ), + ), + ).padding(top: 8); + } +} + class _RealmMemberListWidget extends StatefulWidget { final SnRealm? realm;