From 1d54f947f67d3cb39a7be72185fcddb6c6648e99 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 18 May 2025 18:48:03 +0800 Subject: [PATCH] :sparkles: Realm member manage --- assets/i18n/en-US.json | 2 + lib/screens/realm/detail.dart | 169 ++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index af1fc12..1243374 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -240,6 +240,8 @@ "fileUploadingProgress": "Uploading file #{}: {}%", "removeChatMember": "Remove Chat Room Member", "removeChatMemberHint": "Are you sure to remove this member from the room?", + "removeRealmMember": "Remove Realm Member", + "removeRealmMemberHint": "Are you sure to remove this member from the realm?", "memberRole": "Member Role", "memberRoleHint": "Greater number has higher permission.", "memberRoleEdit": "Edit role for @{}" diff --git a/lib/screens/realm/detail.dart b/lib/screens/realm/detail.dart index 7840117..fca22f7 100644 --- a/lib/screens/realm/detail.dart +++ b/lib/screens/realm/detail.dart @@ -228,6 +228,8 @@ class _RealmMemberListSheet extends HookConsumerWidget { realmMemberStateProvider(realmSlug).notifier, ); + final realmIdentity = ref.watch(realmIdentityProvider(realmSlug)); + useEffect(() { Future(() { memberNotifier.loadMore(); @@ -320,6 +322,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { final member = memberState.members[index]; return ListTile( + contentPadding: EdgeInsets.only(left: 16, right: 12), leading: ProfilePictureWidget( fileId: member.account!.profile.pictureId, ), @@ -344,6 +347,55 @@ class _RealmMemberListSheet extends HookConsumerWidget { Expanded(child: Text("@${member.account!.name}")), ], ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if ((realmIdentity.value?.role ?? 0) >= 50) + IconButton( + icon: const Icon(Symbols.edit), + onPressed: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: + (context) => _RealmMemberRoleSheet( + realmSlug: realmSlug, + member: member, + ), + ).then((value) { + if (value != null) { + memberNotifier.reset(); + memberNotifier.loadMore(); + } + }); + }, + ), + if ((realmIdentity.value?.role ?? 0) >= 50) + IconButton( + icon: const Icon(Symbols.delete), + onPressed: () { + showConfirmAlert( + 'removeRealmMemberHint'.tr(), + 'removeRealmMember'.tr(), + ).then((confirm) async { + if (confirm != true) return; + try { + final apiClient = ref.watch( + apiClientProvider, + ); + await apiClient.delete( + '/realms/$realmSlug/members/${member.accountId}', + ); + memberNotifier.reset(); + memberNotifier.loadMore(); + } catch (err) { + showErrorAlert(err); + } + }); + }, + ), + ], + ), ); }, ), @@ -381,3 +433,120 @@ class RealmMemberState { ); } } + +class _RealmMemberRoleSheet extends HookConsumerWidget { + final String realmSlug; + final SnRealmMember member; + + const _RealmMemberRoleSheet({required this.realmSlug, required this.member}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final roleController = useTextEditingController( + text: member.role.toString(), + ); + + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: EdgeInsets.only( + top: 16, + left: 20, + right: 16, + bottom: 12, + ), + child: Row( + children: [ + Text( + 'memberRoleEdit'.tr(args: [member.account!.name]), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: -0.5, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Symbols.close), + onPressed: () => Navigator.pop(context), + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + ), + ), + ], + ), + ), + const Divider(height: 1), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const [100, 50, 0]; + } + final int? value = int.tryParse(textEditingValue.text); + if (value == null) return const [100, 50, 0]; + return [100, 50, 0].where( + (option) => + option.toString().contains(textEditingValue.text), + ); + }, + onSelected: (int selection) { + roleController.text = selection.toString(); + }, + fieldViewBuilder: ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + return TextField( + controller: controller, + focusNode: focusNode, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'memberRole'.tr(), + helperText: 'memberRoleHint'.tr(), + ), + onTapOutside: (event) => focusNode.unfocus(), + ); + }, + ), + const Gap(16), + FilledButton.icon( + onPressed: () async { + try { + final newRole = int.parse(roleController.text); + if (newRole < 0 || newRole > 100) { + throw 'Role must be between 0 and 100'; + } + + final apiClient = ref.read(apiClientProvider); + await apiClient.patch( + '/realms/$realmSlug/members/${member.accountId}/role', + data: newRole, + ); + + if (context.mounted) Navigator.pop(context, true); + } catch (err) { + showErrorAlert(err); + } + }, + icon: const Icon(Symbols.save), + label: const Text('saveChanges').tr(), + ), + ], + ).padding(vertical: 16, horizontal: 24), + ], + ), + ), + ); + } +}