diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 6f50ca8..4b7e2f3 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -881,5 +881,11 @@ "appInitUserDirectory": "Initializing User Directory", "appInitRealm": "Initializing Realms", "appInitChat": "Initializing Chat", - "appInitDone": "Completed" + "appInitDone": "Completed", + "community": "Community", + "realmCommunity": "{}'s Community", + "postTotalCount": { + "one": "Total {} post", + "other": "Total {} posts" + } } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index e53ccee..5e89eaf 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -879,5 +879,11 @@ "appInitUserDirectory": "正在初始化用户目录", "appInitRealm": "正在初始化领域信息", "appInitChat": "正在初始化聊天", - "appInitDone": "完成" + "appInitDone": "完成", + "community": "社区", + "realmCommunity": "{}的社区", + "postTotalCount": { + "zero": "没有帖子", + "one": "共 {} 条帖子" + } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1ac558f..1b17a49 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -232,6 +232,8 @@ PODS: - sqlite3/common - sqlite3/fts5 (3.49.1): - sqlite3/common + - sqlite3/math (3.49.1): + - sqlite3/common - sqlite3/perf-threadsafe (3.49.1): - sqlite3/common - sqlite3/rtree (3.49.1): @@ -242,6 +244,7 @@ PODS: - sqlite3 (~> 3.49.1) - sqlite3/dbstatvtab - sqlite3/fts5 + - sqlite3/math - sqlite3/perf-threadsafe - sqlite3/rtree - SwiftyGif (5.4.5) @@ -457,7 +460,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 - sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db + sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe diff --git a/lib/router.dart b/lib/router.dart index 4ca24de..88cc20e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -39,6 +39,7 @@ import 'package:surface/screens/post/post_shuffle.dart'; import 'package:surface/screens/post/publisher_page.dart'; import 'package:surface/screens/post/post_search.dart'; import 'package:surface/screens/realm.dart'; +import 'package:surface/screens/realm/community.dart'; import 'package:surface/screens/realm/manage.dart'; import 'package:surface/screens/realm/realm_detail.dart'; import 'package:surface/screens/realm/realm_discovery.dart'; @@ -259,6 +260,13 @@ final _appRoutes = [ child: const RealmScreen(), ), routes: [ + GoRoute( + path: '/:alias/community', + name: 'realmCommunity', + builder: (context, state) => RealmCommunityScreen( + alias: state.pathParameters['alias']!, + ), + ), GoRoute( path: '/manage', name: 'realmManage', diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 7ee0942..372f28c 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -45,12 +45,14 @@ class PostEditorExtra { final String? title; final String? description; final List? attachments; + final SnRealm? realm; const PostEditorExtra({ this.text, this.title, this.description, this.attachments, + this.realm, }); } @@ -263,6 +265,7 @@ class _PostEditorScreenState extends State _writeController.descriptionController.text = widget.extraProps!.description ?? ''; _writeController.addAttachments(widget.extraProps!.attachments ?? []); + _writeController.setRealm(widget.extraProps!.realm); } } diff --git a/lib/screens/realm/community.dart b/lib/screens/realm/community.dart new file mode 100644 index 0000000..2d8ed33 --- /dev/null +++ b/lib/screens/realm/community.dart @@ -0,0 +1,149 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +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/screens/post/post_editor.dart'; +import 'package:surface/types/post.dart'; +import 'package:surface/types/realm.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 RealmCommunityScreen extends StatefulWidget { + final String alias; + const RealmCommunityScreen({super.key, required this.alias}); + + @override + State createState() => _RealmCommunityScreenState(); +} + +class _RealmCommunityScreenState extends State { + SnRealm? _realm; + + Future _fetchRealm() async { + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/id/realms/${widget.alias}'); + _realm = SnRealm.fromJson(resp.data); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + rethrow; + } finally { + setState(() {}); + } + } + + 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: _realm?.id.toString(), + ); + _totalCount = out.$2; + _posts.addAll(out.$1); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchRealm(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + title: Text(_realm?.name ?? 'loading'.tr()), + ), + floatingActionButton: _realm != null + ? FloatingActionButton( + child: const Icon(Symbols.edit), + onPressed: () { + GoRouter.of(context).pushNamed( + 'postEditor', + extra: PostEditorExtra(realm: _realm!), + ); + }, + ) + : null, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_realm == null) + Expanded( + child: Center( + child: CircularProgressIndicator().center(), + ), + ), + if (_realm != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('realmCommunity'.tr(args: [_realm!.name])) + .fontSize(17) + .padding(horizontal: 20, bottom: 4), + Text('postTotalCount'.plural(_totalCount ?? 0)) + .fontSize(13) + .opacity(0.8) + .padding(horizontal: 20, bottom: 4), + ], + ).padding(horizontal: 20, vertical: 16), + const Divider(height: 1), + if (_realm != null) + Expanded( + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: _fetchPosts, + child: InfiniteList( + padding: const EdgeInsets.only(top: 8), + 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 Divider().padding(vertical: 2), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index 52260a7..5d97cff 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -318,7 +318,7 @@ class _RealmPostListWidgetState extends State<_RealmPostListWidget> { }, ); }, - separatorBuilder: (_, __) => const Gap(8), + separatorBuilder: (_, __) => const Divider().padding(vertical: 2), ), ), ).padding(top: 8); diff --git a/lib/theme.dart b/lib/theme.dart index 3ff3392..4152f9b 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -89,6 +89,7 @@ Future createAppTheme( }, ), progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false), + sliderTheme: SliderThemeData(year2023: false), ); } diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index c2f0499..65dad0a 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -76,7 +76,10 @@ class _LoadingIndicatorState extends State const SizedBox( height: 16, width: 16, - child: CircularProgressIndicator(strokeWidth: 2.5), + child: CircularProgressIndicator( + strokeWidth: 2.5, + padding: EdgeInsets.zero, + ), ), const Gap(16), Text('loading').tr(), diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart index 8016487..3cb4beb 100644 --- a/lib/widgets/navigation/app_drawer_navigation.dart +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -10,6 +10,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/channel.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/sn_network.dart'; @@ -42,10 +43,8 @@ class _AppNavigationDrawerState extends State { @override Widget build(BuildContext context) { final ua = context.read(); - final sn = context.read(); final nav = context.watch(); final cfg = context.watch(); - final rel = context.read(); final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null; @@ -77,111 +76,7 @@ class _AppNavigationDrawerState extends State { ), Gap(MediaQuery.of(context).padding.top), Expanded( - child: PageTransitionSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, - Animation primaryAnimation, - Animation secondaryAnimation) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - fillColor: Colors.transparent, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ); - }, - child: nav.focusedRealm == null - ? ListView( - key: const Key('realm-list-view'), - padding: EdgeInsets.zero, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Solar Network').bold(), - AppVersionLabel(), - ], - ).padding( - horizontal: 32, - vertical: 12, - ), - ...rel.availableRealms.map((ele) { - return ListTile( - minTileHeight: 48, - contentPadding: - EdgeInsets.symmetric(horizontal: 24), - leading: AccountImage( - content: ele.avatar, - radius: 16, - ), - title: Text(ele.name), - onTap: () { - if (Scaffold.of(context).isDrawerOpen) { - GoRouter.of(context).goNamed( - 'realmDetail', - pathParameters: { - 'alias': ele.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - } - nav.setFocusedRealm(ele); - }, - ); - }), - ], - ) - : ListView( - key: ValueKey(nav.focusedRealm), - padding: EdgeInsets.zero, - children: [ - if (nav.focusedRealm!.banner != null) - AspectRatio( - aspectRatio: 16 / 9, - child: AutoResizeUniversalImage( - sn.getAttachmentUrl( - nav.focusedRealm!.banner!, - ), - fit: BoxFit.cover, - ), - ), - ListTile( - minTileHeight: 48, - tileColor: Theme.of(context) - .colorScheme - .surfaceContainer, - contentPadding: EdgeInsets.only( - left: 24, - right: 16, - ), - leading: AccountImage( - content: nav.focusedRealm!.avatar, - radius: 16, - ), - trailing: IconButton( - icon: const Icon(Symbols.close), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, - onPressed: () { - nav.setFocusedRealm(null); - }, - ), - title: Text(nav.focusedRealm!.name), - onTap: () { - GoRouter.of(context).pushNamed( - 'realmDetail', - pathParameters: { - 'alias': nav.focusedRealm!.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ), - ], - ), - ), + child: _DrawerContentList(), ), Row( spacing: 8, @@ -249,3 +144,153 @@ class _AppNavigationDrawerState extends State { ); } } + +class _DrawerContentList extends StatelessWidget { + const _DrawerContentList(); + + @override + Widget build(BuildContext context) { + final ct = context.read(); + final sn = context.read(); + final nav = context.watch(); + final rel = context.read(); + + return PageTransitionSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, Animation primaryAnimation, + Animation secondaryAnimation) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + fillColor: Colors.transparent, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + child: nav.focusedRealm == null + ? ListView( + key: const Key('realm-list-view'), + padding: EdgeInsets.zero, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Solar Network').bold(), + AppVersionLabel(), + ], + ).padding( + horizontal: 32, + vertical: 12, + ), + ...rel.availableRealms.map((ele) { + return ListTile( + minTileHeight: 48, + contentPadding: EdgeInsets.symmetric(horizontal: 24), + leading: AccountImage( + content: ele.avatar, + radius: 16, + ), + title: Text(ele.name), + onTap: () { + nav.setFocusedRealm(ele); + }, + ); + }), + ], + ) + : ListView( + key: ValueKey(nav.focusedRealm), + padding: EdgeInsets.zero, + children: [ + if (nav.focusedRealm!.banner != null) + AspectRatio( + aspectRatio: 16 / 9, + child: AutoResizeUniversalImage( + sn.getAttachmentUrl( + nav.focusedRealm!.banner!, + ), + fit: BoxFit.cover, + ), + ), + ListTile( + minTileHeight: 48, + tileColor: Theme.of(context).colorScheme.surfaceContainer, + contentPadding: EdgeInsets.only( + left: 24, + right: 16, + ), + leading: AccountImage( + content: nav.focusedRealm!.avatar, + radius: 16, + ), + trailing: IconButton( + icon: const Icon(Symbols.close), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + onPressed: () { + nav.setFocusedRealm(null); + }, + ), + title: Text(nav.focusedRealm!.name), + onTap: () { + GoRouter.of(context).pushNamed( + 'realmDetail', + pathParameters: { + 'alias': nav.focusedRealm!.alias, + }, + ); + Scaffold.of(context).closeDrawer(); + }, + ), + ListTile( + minTileHeight: 48, + contentPadding: EdgeInsets.only( + left: 28, + right: 8, + ), + leading: const Icon(Symbols.globe), + title: Text('community').tr(), + onTap: () { + GoRouter.of(context).pushNamed( + 'realmCommunity', + pathParameters: { + 'alias': nav.focusedRealm!.alias, + }, + ); + Scaffold.of(context).closeDrawer(); + }, + ), + if (ct.availableChannels + .where((ele) => ele.realmId == nav.focusedRealm?.id) + .isNotEmpty) + const Divider(height: 1), + ...(ct.availableChannels + .where((ele) => ele.realmId == nav.focusedRealm?.id) + .map((ele) { + return ListTile( + minTileHeight: 48, + contentPadding: EdgeInsets.only( + left: 28, + right: 8, + ), + leading: const Icon(Symbols.tag), + title: Text(ele.name), + onTap: () { + GoRouter.of(context).pushNamed( + 'chatRoom', + pathParameters: { + 'scope': ele.realm?.alias ?? 'global', + 'alias': ele.alias, + }, + ); + Scaffold.of(context).closeDrawer(); + }, + ); + })) + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ffe9330..cacffef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: chalkdart - sha256: e7cfcc9a9d9546843304c1ff87fe0696c7eb82ee70e6df63f555f321b15a40d8 + sha256: "82dfa884e3cf97641eb0742a3b9ffd41490666b9ece548b2e32cbfefe540bf86" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" characters: dependency: transitive description: @@ -517,10 +517,10 @@ packages: dependency: "direct main" description: name: fast_rsa - sha256: "205a36c0412b9fabebf3e18ccb5221d819cc28cfb3da988c0bf7b646368d0270" + sha256: a26ad752734dc52fd51abd55248df868d7480e68d8cc8dd12413b0124bba0a7e url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.8.1" ffi: dependency: transitive description: @@ -541,10 +541,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: ee11ce89f8937c39181bc88d57a455972f7545b86150d8f287d0d9cf95bcdf0a + sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.2.1" file_saver: dependency: "direct main" description: @@ -746,10 +746,10 @@ packages: dependency: "direct main" description: name: flutter_expandable_fab - sha256: "85275279d19faf4fbe5639dc1f139b4555b150e079d056f085601a45688af12c" + sha256: b14caf78720a48f650e6e1a38d724e33b1f5348d646fa1c266570c31a7f87ef3 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" flutter_highlight: dependency: "direct main" description: @@ -953,10 +953,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a6274c34d61b3d68082f2b0e9a641a3ec197e525d269f35b82f62d5b2c6d9f75 + sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" freezed_annotation: dependency: "direct main" description: @@ -1177,10 +1177,10 @@ packages: dependency: transitive description: name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_macos: dependency: transitive description: @@ -1385,10 +1385,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: db745002d0323c32097f5bc23711c02b0fb36ef616011a38c8c1798d5684d368 + sha256: "99d5b0e7c65232dfe1247e0ac67eeeee2cab9da2d860748fc495d34f5e9e6397" url: "https://pub.dev" source: hosted - version: "4.2810.0" + version: "4.2811.0" media_kit: dependency: "direct main" description: @@ -2102,10 +2102,10 @@ packages: dependency: transitive description: name: sqlite3_flutter_libs - sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3" + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" url: "https://pub.dev" source: hosted - version: "0.5.31" + version: "0.5.32" sqlparser: dependency: transitive description: @@ -2174,34 +2174,34 @@ packages: dependency: "direct main" description: name: talker - sha256: a074e0a2c1db1b09c1856050c5284a14ff64faea003403409871ab8335738d08 + sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.7.1" talker_dio_logger: dependency: "direct main" description: name: talker_dio_logger - sha256: "6d713d26bdf90c7440222758a4e02ebfbdb3b323cdbdb50b78082db720f69427" + sha256: "52c1b554cccedec6073637a6d4f6a3e267dd4451c1545fe57e1b26897a560ccb" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.7.1" talker_flutter: dependency: "direct main" description: name: talker_flutter - sha256: "25a9faa98a0dcbab8d1ec366f19b3e91d422ede523461ec6e26e198b280cb98d" + sha256: "77458ca11638dfefb651e898a26101ee54e60dc0b168ad7481a05b1c97ce2680" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.7.1" talker_logger: dependency: transitive description: name: talker_logger - sha256: "0a1a295a9d036a3416e89c7155eaaca9ca6e3e0abb5401a40597f98b9cfc8fe6" + sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.7.1" term_glyph: dependency: transitive description: