From 22c2a80650cbc69c3220d011ade840ea7530a79d Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 6 May 2024 20:57:52 +0800 Subject: [PATCH] :sparkles: Complete the realm system --- lib/i18n/app_en.arb | 2 + lib/router.dart | 12 +++ lib/screens/realms/realm.dart | 12 +-- lib/screens/realms/realm_editor.dart | 2 + lib/screens/realms/realm_manage.dart | 119 +++++++++++++++++++++++++ lib/screens/realms/realm_member.dart | 29 +++--- lib/widgets/realms/realm_deletion.dart | 101 +++++++++++++++++++++ 7 files changed, 258 insertions(+), 19 deletions(-) create mode 100644 lib/screens/realms/realm_manage.dart create mode 100644 lib/widgets/realms/realm_deletion.dart diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb index 9c84167..9cd0cf1 100644 --- a/lib/i18n/app_en.arb +++ b/lib/i18n/app_en.arb @@ -87,6 +87,8 @@ "realmDescriptionLabel": "Realm Description", "realmPublicLabel": "It's public", "realmCommunityLabel": "It's community realm", + "realmMember": "Member", + "realmManage": "Realm Manage", "chatNew": "New Chat", "chatNewCreate": "Create a channel", "chatNewJoin": "Join a exists channel", diff --git a/lib/router.dart b/lib/router.dart index 0f6e0e3..44e427b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -21,8 +21,10 @@ import 'package:solian/screens/posts/moment_editor.dart'; import 'package:solian/screens/posts/screen.dart'; import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/realms/realm.dart'; +import 'package:solian/screens/realms/realm_manage.dart'; import 'package:solian/screens/realms/realm_editor.dart'; import 'package:solian/screens/realms/realm_list.dart'; +import 'package:solian/screens/realms/realm_member.dart'; import 'package:solian/screens/users/userinfo.dart'; import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/empty.dart'; @@ -103,6 +105,16 @@ abstract class SolianRouter { realm: state.uri.queryParameters['realm'], ), ), + GoRoute( + path: '/realms/:realm/manage', + name: 'realms.manage', + builder: (context, state) => RealmManageScreen(realm: state.extra as Realm), + ), + GoRoute( + path: '/realms/:realm/member', + name: 'realms.member', + builder: (context, state) => RealmMemberScreen(realm: state.extra as Realm), + ), GoRoute( path: '/realms/:realm/posts/:dataset/:alias', name: 'realms.posts.details', diff --git a/lib/screens/realms/realm.dart b/lib/screens/realms/realm.dart index 2c045b1..c825cd7 100644 --- a/lib/screens/realms/realm.dart +++ b/lib/screens/realms/realm.dart @@ -6,7 +6,6 @@ import 'package:solian/providers/realm.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/chat/chat_list.dart'; import 'package:solian/screens/explore.dart'; -import 'package:solian/screens/realms/realm_member.dart'; import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/scaffold.dart'; @@ -82,7 +81,7 @@ class _RealmWidgetState extends State { } return DefaultTabController( - length: 3, + length: 2, child: Column( children: [ TabBar( @@ -90,7 +89,6 @@ class _RealmWidgetState extends State { tabs: const [ Tab(icon: Icon(Icons.newspaper)), Tab(icon: Icon(Icons.message)), - Tab(icon: Icon(Icons.supervisor_account)) ], ), Expanded( @@ -98,11 +96,6 @@ class _RealmWidgetState extends State { children: [ ExplorePostWidget(realm: widget.alias), ChatListWidget(realm: widget.alias), - _realm.focusRealm != null - ? RealmMemberWidget(realm: _realm.focusRealm!) - : const Center( - child: CircularProgressIndicator(), - ), ], ), ) @@ -127,8 +120,9 @@ class RealmManageAction extends StatelessWidget { return IconButton( onPressed: () async { final did = await SolianRouter.router.pushNamed( - 'realms.editor', + 'realms.manage', extra: realm, + pathParameters: {'realm': realm.alias}, ); if (did == true) onUpdate(); }, diff --git a/lib/screens/realms/realm_editor.dart b/lib/screens/realms/realm_editor.dart index f97c0c3..f10d7dd 100644 --- a/lib/screens/realms/realm_editor.dart +++ b/lib/screens/realms/realm_editor.dart @@ -8,6 +8,7 @@ import 'package:solian/models/realm.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/router.dart'; import 'package:solian/utils/service_url.dart'; +import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/exts.dart'; import 'package:solian/widgets/scaffold.dart'; import 'package:uuid/uuid.dart'; @@ -117,6 +118,7 @@ class _RealmEditorScreenState extends State { child: Text(AppLocalizations.of(context)!.apply.toUpperCase()), ), ], + fixedAppBarColor: SolianTheme.isLargeScreen(context), child: Column( children: [ _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), diff --git a/lib/screens/realms/realm_manage.dart b/lib/screens/realms/realm_manage.dart new file mode 100644 index 0000000..11039d3 --- /dev/null +++ b/lib/screens/realms/realm_manage.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/models/realm.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/router.dart'; +import 'package:solian/widgets/realms/realm_deletion.dart'; +import 'package:solian/widgets/scaffold.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class RealmManageScreen extends StatefulWidget { + final Realm realm; + + const RealmManageScreen({super.key, required this.realm}); + + @override + State createState() => _RealmManageScreenState(); +} + +class _RealmManageScreenState extends State { + bool _isOwned = false; + + void promptLeaveChannel() async { + final did = await showDialog( + context: context, + builder: (context) => RealmDeletion( + realm: widget.realm, + isOwned: _isOwned, + ), + ); + if (did == true && SolianRouter.router.canPop()) { + SolianRouter.router.pop('disposed'); + } + } + + @override + void initState() { + super.initState(); + + Future.delayed(Duration.zero, () async { + final auth = context.read(); + final prof = await auth.getProfiles(); + + setState(() { + _isOwned = prof['id'] == widget.realm.accountId; + }); + }); + } + + @override + Widget build(BuildContext context) { + final authorizedItems = [ + ListTile( + leading: const Icon(Icons.settings), + title: Text(AppLocalizations.of(context)!.settings), + onTap: () async { + SolianRouter.router.pushNamed('realms.editor', extra: widget.realm).then((did) { + if (did == true) { + if (SolianRouter.router.canPop()) SolianRouter.router.pop('refresh'); + } + }); + }, + ), + ]; + + return IndentScaffold( + title: AppLocalizations.of(context)!.realmManage, + hideDrawer: true, + noSafeArea: true, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const CircleAvatar( + radius: 24, + backgroundColor: Colors.teal, + child: Icon(Icons.tag, color: Colors.white), + ), + const SizedBox(width: 16), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(widget.realm.name, style: Theme.of(context).textTheme.bodyLarge), + Text(widget.realm.description, style: Theme.of(context).textTheme.bodySmall), + ]), + ) + ], + ), + ), + const Divider(thickness: 0.3), + Expanded( + child: ListView( + children: [ + ListTile( + leading: const Icon(Icons.supervisor_account), + title: Text(AppLocalizations.of(context)!.chatMember), + onTap: () { + SolianRouter.router.pushNamed( + 'realms.member', + extra: widget.realm, + pathParameters: {'realm': widget.realm.alias}, + ); + }, + ), + ...(_isOwned ? authorizedItems : List.empty()), + const Divider(thickness: 0.3), + ListTile( + leading: _isOwned ? const Icon(Icons.delete) : const Icon(Icons.exit_to_app), + title: Text(_isOwned ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.exit), + onTap: () => promptLeaveChannel(), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/realms/realm_member.dart b/lib/screens/realms/realm_member.dart index 2ef5efb..217ed24 100644 --- a/lib/screens/realms/realm_member.dart +++ b/lib/screens/realms/realm_member.dart @@ -7,20 +7,23 @@ import 'package:solian/models/account.dart'; import 'package:solian/models/realm.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/utils/service_url.dart'; +import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/friend_picker.dart'; import 'package:solian/widgets/exts.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:solian/widgets/scaffold.dart'; -class RealmMemberWidget extends StatefulWidget { +class RealmMemberScreen extends StatefulWidget { final Realm realm; - const RealmMemberWidget({super.key, required this.realm}); + const RealmMemberScreen({super.key, required this.realm}); @override - State createState() => _RealmMemberWidgetState(); + State createState() => _RealmMemberScreenState(); } -class _RealmMemberWidgetState extends State { +class _RealmMemberScreenState extends State { bool _isSubmitting = false; List _members = List.empty(); @@ -136,12 +139,18 @@ class _RealmMemberWidgetState extends State { @override Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () => promptAddMember(), - ), - body: RefreshIndicator( + return IndentScaffold( + title: AppLocalizations.of(context)!.realmMember, + fixedAppBarColor: SolianTheme.isLargeScreen(context), + noSafeArea: true, + hideDrawer: true, + appBarActions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => promptAddMember(), + ), + ], + child: RefreshIndicator( onRefresh: () => fetchMemberships(), child: CustomScrollView( slivers: [ diff --git a/lib/widgets/realms/realm_deletion.dart b/lib/widgets/realms/realm_deletion.dart new file mode 100644 index 0000000..a77e0e0 --- /dev/null +++ b/lib/widgets/realms/realm_deletion.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:solian/models/realm.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/exts.dart'; + +class RealmDeletion extends StatefulWidget { + final Realm realm; + final bool isOwned; + + const RealmDeletion({ + super.key, + required this.realm, + required this.isOwned, + }); + + @override + State createState() => _RealmDeletionState(); +} + +class _RealmDeletionState extends State { + bool _isSubmitting = false; + + Future deleteChannel() async { + setState(() => _isSubmitting = true); + + final auth = context.read(); + if (!await auth.isAuthorized()) { + setState(() => _isSubmitting = false); + return; + } + + var res = await auth.client!.delete( + getRequestUri('passport', '/api/realms/${widget.realm.alias}'), + ); + if (res.statusCode != 200) { + var message = utf8.decode(res.bodyBytes); + context.showErrorDialog(message); + } else if (Navigator.canPop(context)) { + Navigator.pop(context, true); + } + + setState(() => _isSubmitting = false); + } + + Future leaveChannel() async { + setState(() => _isSubmitting = true); + + final auth = context.read(); + if (!await auth.isAuthorized()) { + setState(() => _isSubmitting = false); + return; + } + + var res = await auth.client!.delete( + getRequestUri('passport', '/api/realms/${widget.realm.alias}/members/me'), + ); + if (res.statusCode != 200) { + var message = utf8.decode(res.bodyBytes); + context.showErrorDialog(message); + } else if (Navigator.canPop(context)) { + Navigator.pop(context, true); + } + + setState(() => _isSubmitting = false); + } + + @override + Widget build(BuildContext context) { + final content = widget.isOwned + ? AppLocalizations.of(context)!.chatChannelDeleteConfirm + : AppLocalizations.of(context)!.chatChannelLeaveConfirm; + + return AlertDialog( + title: Text(AppLocalizations.of(context)!.confirmation), + content: Text(content), + actions: [ + TextButton( + onPressed: _isSubmitting ? null : () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.confirmCancel), + ), + TextButton( + onPressed: _isSubmitting + ? null + : () { + if (widget.isOwned) { + deleteChannel(); + } else { + leaveChannel(); + } + }, + child: Text(AppLocalizations.of(context)!.confirmOkay), + ), + ], + ); + } +}