diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 8e7329c..28e8b49 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -128,6 +128,7 @@ "one": "{} social point", "other": "{} social points" }, + "publisherAffiliatedBy": "Affiliated by {}", "publisherRunBy": "Run by {}", "fieldPublisherBelongToRealm": "Belongs to", "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 745cbb2..c152327 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -112,6 +112,7 @@ "one": "{} 点社会信用点", "other": "{} 点社会信用点" }, + "publisherAffiliatedBy": "隶属于 {}", "publisherRunBy": "由 {} 管理", "fieldPublisherBelongToRealm": "所属领域", "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域", diff --git a/lib/screens/post/publisher_page.dart b/lib/screens/post/publisher_page.dart index 676f6a2..f2a7191 100644 --- a/lib/screens/post/publisher_page.dart +++ b/lib/screens/post/publisher_page.dart @@ -14,6 +14,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/types/account.dart'; import 'package:surface/types/post.dart'; +import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/post/post_item.dart'; @@ -22,19 +23,19 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class PostPublisherScreen extends StatefulWidget { final String name; + const PostPublisherScreen({super.key, required this.name}); @override State createState() => _PostPublisherScreenState(); } -class _PostPublisherScreenState extends State - with SingleTickerProviderStateMixin { +class _PostPublisherScreenState extends State with SingleTickerProviderStateMixin { late final ScrollController _scrollController = ScrollController(); - late final TabController _tabController = - TabController(length: 3, vsync: this); + late final TabController _tabController = TabController(length: 3, vsync: this); SnPublisher? _publisher; + SnRealm? _realm; SnAccount? _account; Future _fetchPublisher() async { @@ -45,11 +46,16 @@ class _PostPublisherScreenState extends State if (!mounted) return; _publisher = SnPublisher.fromJson(resp.data); _account = await ud.getAccount(_publisher?.accountId); + if (_publisher?.realmId != null) { + final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}'); + _realm = SnRealm.fromJson(resp.data); + } } catch (err) { if (!mounted) return; context.showErrorDialog(err).then((_) { if (mounted) Navigator.pop(context); }); + rethrow; } finally { setState(() {}); } @@ -114,14 +120,12 @@ class _PostPublisherScreenState extends State double _appBarBlur = 0.0; late final _appBarWidth = MediaQuery.of(context).size.width; - late final _appBarHeight = - (_appBarWidth * kBannerAspectRatio).roundToDouble(); + late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble(); void _updateAppBarBlur() { if (_scrollController.offset > _appBarHeight) return; setState(() { - _appBarBlur = - (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); + _appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0); }); } @@ -215,10 +219,7 @@ class _PostPublisherScreenState extends State text: TextSpan(children: [ TextSpan( text: _publisher!.nick, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( + style: Theme.of(context).textTheme.titleLarge!.copyWith( color: Colors.white, shadows: labelShadows, ), @@ -226,10 +227,7 @@ class _PostPublisherScreenState extends State const TextSpan(text: '\n'), TextSpan( text: '@${_publisher!.name}', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( + style: Theme.of(context).textTheme.bodySmall!.copyWith( color: Colors.white, shadows: labelShadows, ), @@ -241,14 +239,19 @@ class _PostPublisherScreenState extends State ? Stack( fit: StackFit.expand, children: [ - UniversalImage( - sn.getAttachmentUrl(_publisher!.banner), - fit: BoxFit.cover, - height: imageHeight, - width: _appBarWidth, - cacheHeight: imageHeight, - cacheWidth: _appBarWidth, - ), + if (_publisher!.banner.isNotEmpty) + UniversalImage( + sn.getAttachmentUrl(_publisher!.banner), + fit: BoxFit.cover, + height: imageHeight, + width: _appBarWidth, + cacheHeight: imageHeight, + cacheWidth: _appBarWidth, + ) + else + Container( + color: Theme.of(context).colorScheme.surfaceContainer, + ), Positioned( top: 0, left: 0, @@ -288,14 +291,11 @@ class _PostPublisherScreenState extends State const Gap(16), Expanded( child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _publisher!.nick, - style: Theme.of(context) - .textTheme - .titleMedium, + style: Theme.of(context).textTheme.titleMedium, ).bold(), Text('@${_publisher!.name}').fontSize(13), ], @@ -306,9 +306,7 @@ class _PostPublisherScreenState extends State style: ButtonStyle( elevation: WidgetStatePropertyAll(0), ), - onPressed: _isSubscribing - ? null - : _toggleSubscription, + onPressed: _isSubscribing ? null : _toggleSubscription, label: Text('subscribe').tr(), icon: const Icon(Symbols.add), ) @@ -317,17 +315,14 @@ class _PostPublisherScreenState extends State style: ButtonStyle( elevation: WidgetStatePropertyAll(0), ), - onPressed: _isSubscribing - ? null - : _toggleSubscription, + onPressed: _isSubscribing ? null : _toggleSubscription, label: Text('unsubscribe').tr(), icon: const Icon(Symbols.remove), ), ], ).padding(right: 8), const Gap(12), - Text(_publisher!.description) - .padding(horizontal: 8), + Text(_publisher!.description).padding(horizontal: 8), const Gap(12), Column( children: [ @@ -335,10 +330,8 @@ class _PostPublisherScreenState extends State children: [ const Icon(Symbols.calendar_add_on), const Gap(8), - Text('publisherJoinedAt').tr(args: [ - DateFormat('y/M/d') - .format(_publisher!.createdAt) - ]), + Text('publisherJoinedAt') + .tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]), ], ), Row( @@ -346,11 +339,30 @@ class _PostPublisherScreenState extends State const Icon(Symbols.trending_up), const Gap(8), Text('publisherSocialPointTotal').plural( - _publisher!.totalUpvote - - _publisher!.totalDownvote, + _publisher!.totalUpvote - _publisher!.totalDownvote, ), ], ), + if (_realm != null) + Row( + children: [ + const Icon(Symbols.group_work), + const Gap(8), + InkWell( + child: Text('publisherAffiliatedBy').tr(args: [ + '@${_realm?.alias ?? 'unknown'}', + ]), + onTap: () { + GoRouter.of(context).pushNamed( + 'realmDetail', + pathParameters: {'alias': _realm!.alias}, + ); + }, + ), + const Gap(8), + AccountImage(content: _realm?.avatar, radius: 8), + ], + ), Row( children: [ const Icon(Symbols.tools_wrench), @@ -369,8 +381,7 @@ class _PostPublisherScreenState extends State }, ), const Gap(8), - AccountImage( - content: _account?.avatar, radius: 8), + AccountImage(content: _account?.avatar, radius: 8), ], ), ], @@ -447,6 +458,7 @@ class _PublisherPostList extends StatelessWidget { final void Function() fetchPosts; final void Function(int index, SnPost data) onChanged; final void Function() onDeleted; + const _PublisherPostList({ super.key, required this.isBusy, diff --git a/lib/screens/realm.dart b/lib/screens/realm.dart index 6756514..262af89 100644 --- a/lib/screens/realm.dart +++ b/lib/screens/realm.dart @@ -6,16 +6,15 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/universal_image.dart'; -import '../providers/userinfo.dart'; -import '../widgets/unauthorized_hint.dart'; - class RealmScreen extends StatefulWidget { const RealmScreen({super.key}); @@ -101,9 +100,7 @@ class _RealmScreenState extends State { title: Text('screenRealm').tr(), actions: [ IconButton( - icon: !_isCompactView - ? const Icon(Symbols.view_list) - : const Icon(Symbols.view_module), + icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module), onPressed: () { setState(() => _isCompactView = !_isCompactView); }, @@ -129,8 +126,7 @@ class _RealmScreenState extends State { final realm = _realms![idx]; if (_isCompactView) { return ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 16), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), leading: AccountImage( content: realm.avatar, fallbackWidget: const Icon(Symbols.group, size: 20), @@ -201,9 +197,7 @@ class _RealmScreenState extends State { fit: StackFit.expand, children: [ Container( - color: Theme.of(context) - .colorScheme - .surfaceContainer, + color: Theme.of(context).colorScheme.surfaceContainer, child: (realm.banner?.isEmpty ?? true) ? const SizedBox.shrink() : AutoResizeUniversalImage( @@ -217,8 +211,7 @@ class _RealmScreenState extends State { child: AccountImage( content: realm.avatar, radius: 24, - fallbackWidget: - const Icon(Symbols.group, size: 24), + fallbackWidget: const Icon(Symbols.group, size: 24), ), ), ], @@ -228,10 +221,8 @@ class _RealmScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(realm.name).textStyle( - Theme.of(context).textTheme.titleMedium!), - Text(realm.description).textStyle( - Theme.of(context).textTheme.bodySmall!), + Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!), + Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!), ], ).padding(horizontal: 24, bottom: 14), ], diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index a709d98..8368410 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -13,8 +13,11 @@ import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; +import '../../types/post.dart'; + class RealmDetailScreen extends StatefulWidget { final String alias; + const RealmDetailScreen({super.key, required this.alias}); @override @@ -32,6 +35,24 @@ class _RealmDetailScreenState extends State { } catch (err) { if (!mounted) return; context.showErrorDialog(err); + rethrow; + } finally { + setState(() {}); + } + } + + List? _publishers; + + Future _fetchPublishers() async { + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}'); + _publishers = List.from( + resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], + ); + } catch (err) { + if (mounted) context.showErrorDialog(err); + rethrow; } finally { setState(() {}); } @@ -40,7 +61,9 @@ class _RealmDetailScreenState extends State { @override void initState() { super.initState(); - _fetchRealm(); + _fetchRealm().then((_) { + _fetchPublishers(); + }); } @override @@ -60,8 +83,7 @@ class _RealmDetailScreenState extends State { // scroll view thinks it has not been scrolled. // This is not necessary if the "headerSliverBuilder" only builds // widgets that do not overlap the next sliver. - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: Text(_realm?.name ?? 'loading'.tr()), bottom: TabBar( @@ -77,7 +99,7 @@ class _RealmDetailScreenState extends State { }, body: TabBarView( children: [ - _RealmDetailHomeWidget(realm: _realm), + _RealmDetailHomeWidget(realm: _realm, publishers: _publishers), _RealmMemberListWidget(realm: _realm), _RealmSettingsWidget( realm: _realm, @@ -95,7 +117,9 @@ class _RealmDetailScreenState extends State { class _RealmDetailHomeWidget extends StatelessWidget { final SnRealm? realm; - const _RealmDetailHomeWidget({super.key, required this.realm}); + final List? publishers; + + const _RealmDetailHomeWidget({super.key, required this.realm, this.publishers}); @override Widget build(BuildContext context) { @@ -118,6 +142,31 @@ class _RealmDetailHomeWidget extends StatelessWidget { ).padding(horizontal: 24), const Gap(16), const Divider(), + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: publishers?.length ?? 0, + itemBuilder: (context, idx) { + final ele = publishers![idx]; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: AccountImage( + content: ele.avatar, + fallbackWidget: const Icon(Symbols.group, size: 24), + ), + title: Text(ele.nick), + subtitle: Text('@${ele.name}'), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed( + 'postPublisher', + pathParameters: {'name': ele.name}, + ); + }, + ); + }, + ), + ), ], ); } @@ -125,6 +174,7 @@ class _RealmDetailHomeWidget extends StatelessWidget { class _RealmMemberListWidget extends StatefulWidget { final SnRealm? realm; + const _RealmMemberListWidget({super.key, this.realm}); @override @@ -143,12 +193,10 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { try { final ud = context.read(); final sn = context.read(); - final resp = await sn.client.get( - '/cgi/id/realms/${widget.realm!.alias}/members', - queryParameters: { - 'take': 10, - 'offset': 0, - }); + final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: { + 'take': 10, + 'offset': 0, + }); final out = List.from( resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [], @@ -236,12 +284,10 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { fallbackWidget: const Icon(Symbols.group, size: 24), ), title: Text( - ud.getAccountFromCache(member.accountId)?.nick ?? - 'unknown'.tr(), + ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(), ), subtitle: Text( - ud.getAccountFromCache(member.accountId)?.name ?? - 'unknown'.tr(), + ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(), ), trailing: IconButton( icon: const Icon(Symbols.person_remove), @@ -257,6 +303,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { class _NewRealmMemberWidget extends StatefulWidget { final SnRealm realm; + const _NewRealmMemberWidget({super.key, required this.realm}); @override @@ -321,8 +368,7 @@ class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> { child: IconButton( onPressed: _isBusy ? null : () => _performAction(), icon: Icon(Symbols.send), - visualDensity: - const VisualDensity(horizontal: -4, vertical: -4), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), padding: EdgeInsets.zero, ), ), @@ -337,8 +383,8 @@ class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> { class _RealmSettingsWidget extends StatefulWidget { final SnRealm? realm; final Function() onUpdate; - const _RealmSettingsWidget( - {super.key, required this.realm, required this.onUpdate}); + + const _RealmSettingsWidget({super.key, required this.realm, required this.onUpdate}); @override State<_RealmSettingsWidget> createState() => _RealmSettingsWidgetState(); @@ -382,6 +428,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> { return Column( children: [ + const Gap(16), ListTile( leading: const Icon(Symbols.edit), trailing: const Icon(Symbols.chevron_right),