import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.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/sn_network.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.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/account/account_select.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class RealmDetailScreen extends StatefulWidget { final String alias; const RealmDetailScreen({super.key, required this.alias}); @override State createState() => _RealmDetailScreenState(); } class _RealmDetailScreenState 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(() {}); } } 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(() {}); } } @override void initState() { super.initState(); _fetchRealm().then((_) { _fetchPublishers(); }); } @override Widget build(BuildContext context) { return DefaultTabController( length: 3, child: AppScaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: Text(_realm?.name ?? 'loading'.tr()), bottom: TabBar( tabs: [ Tab(icon: Icon(Symbols.home, 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)), ], ), ), ), ]; }, body: TabBarView( children: [ _RealmDetailHomeWidget(realm: _realm, publishers: _publishers), _RealmMemberListWidget(realm: _realm), _RealmSettingsWidget( realm: _realm, onUpdate: () { _fetchRealm(); }, ), ], ), ), ), ); } } class _RealmDetailHomeWidget extends StatelessWidget { final SnRealm? realm; final List? publishers; const _RealmDetailHomeWidget({required this.realm, this.publishers}); @override Widget build(BuildContext context) { return Column( children: [ const Gap(24), if (realm != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( realm!.name, style: Theme.of(context).textTheme.titleMedium, ), Text( realm!.description, style: Theme.of(context).textTheme.bodyMedium, ), ], ).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: 20), 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}, ); }, ); }, ), ), ], ); } } class _RealmMemberListWidget extends StatefulWidget { final SnRealm? realm; const _RealmMemberListWidget({this.realm}); @override State<_RealmMemberListWidget> createState() => _RealmMemberListWidgetState(); } class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { bool _isBusy = false; int? _totalCount; final List _members = List.empty(growable: true); Future _fetchMembers() async { setState(() => _isBusy = true); 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 out = List.from( resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [], ); await ud.listAccount(out.map((ele) => ele.accountId).toSet()); _totalCount = resp.data['count']; _members.addAll(out); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } bool _isUpdating = false; Future _deleteMember(SnRealmMember member) async { if (_isUpdating) return; setState(() => _isUpdating = true); try { final sn = context.read(); await sn.client.delete( '/cgi/id/realms/${widget.realm!.alias}/members/${member.id}', ); if (!mounted) return; _members.clear(); _fetchMembers(); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isUpdating = false); } } Future _addMember(SnAccount related) async { setState(() => _isBusy = true); try { final sn = context.read(); await sn.client.post( '/cgi/id/realms/${widget.realm!.alias}/members', data: {'related': related.name}, ); if (!mounted) return; context.showSnackbar('realmMemberAdded'.tr()); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } void _showMemberAdd() async { final user = await showModalBottomSheet( context: context, builder: (context) => AccountSelect( title: 'realmMemberAdd'.tr(), ), ); if (!mounted) return; if (user == null) return; _addMember(user); } @override void initState() { super.initState(); _fetchMembers(); } @override Widget build(BuildContext context) { final ud = context.read(); return CustomScrollView( slivers: [ SliverToBoxAdapter( child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.group_add), trailing: const Icon(Symbols.chevron_right), title: Text('realmMemberAdd').tr(), subtitle: Text('realmMemberAddDescription').tr(), onTap: _showMemberAdd, ), ), SliverToBoxAdapter(child: const Divider(height: 1)), SliverInfiniteList( // padding: EdgeInsets.zero, itemCount: _members.length, isLoading: _isBusy, hasReachedMax: _totalCount != null && _members.length >= _totalCount!, onFetchData: _fetchMembers, itemBuilder: (context, index) { final member = _members[index]; return ListTile( contentPadding: const EdgeInsets.only(right: 24, left: 16), leading: AccountImage( content: ud.getAccountFromCache(member.accountId)?.avatar, fallbackWidget: const Icon(Symbols.group, size: 24), ), title: Text( ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(), ), subtitle: Text( ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(), ), trailing: IconButton( icon: const Icon(Symbols.person_remove), onPressed: _isUpdating ? null : () => _deleteMember(member), ), ); }, ), ], ); } } class _RealmSettingsWidget extends StatefulWidget { final SnRealm? realm; final Function() onUpdate; const _RealmSettingsWidget({required this.realm, required this.onUpdate}); @override State<_RealmSettingsWidget> createState() => _RealmSettingsWidgetState(); } class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> { bool _isBusy = false; Future _deleteRealm() async { final confirm = await context.showConfirmDialog( 'realmDelete'.tr(args: ['#${widget.realm!.alias}']), 'realmDeleteDescription'.tr(), ); if (!confirm) return; if (!mounted) return; final sn = context.read(); setState(() => _isBusy = true); try { await sn.client.delete('/cgi/id/realms/${widget.realm!.id}'); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _leaveRealm() async { final confirm = await context.showConfirmDialog( 'realmLeave'.tr(), 'realmLeaveDescription'.tr(), ); if (!confirm) return; if (!mounted) return; final sn = context.read(); try { await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me'); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } @override Widget build(BuildContext context) { final ua = context.read(); final isOwned = ua.isAuthorized && widget.realm?.accountId == ua.user?.id; return Column( children: [ const Gap(8), ListTile( leading: const Icon(Symbols.logout), trailing: const Icon(Symbols.chevron_right), title: Text('realmLeave').tr(), subtitle: Text('realmLeaveDescription').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), onTap: _isBusy ? null : () => _leaveRealm(), ), if (isOwned) ListTile( leading: const Icon(Symbols.edit), trailing: const Icon(Symbols.chevron_right), title: Text('realmEdit').tr(), subtitle: Text('realmEditDescription').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), onTap: () { GoRouter.of(context).pushNamed( 'realmManage', queryParameters: {'editing': widget.realm!.alias}, ).then((value) { if (value != null) { widget.onUpdate(); } }); }, ), if (isOwned) ListTile( leading: const Icon(Symbols.delete), trailing: const Icon(Symbols.chevron_right), title: Text('realmActionDelete').tr(), subtitle: Text('realmActionDeleteDescription').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), onTap: _isBusy ? null : _deleteRealm, ), ], ); } }