🎨 Use feature based folder structure
This commit is contained in:
925
lib/realms/realm/realm_detail.dart
Normal file
925
lib/realms/realm/realm_detail.dart
Normal file
@@ -0,0 +1,925 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:island/accounts/accounts_widgets/account/account_pfc.dart';
|
||||
import 'package:island/accounts/accounts_widgets/account/account_picker.dart';
|
||||
import 'package:island/accounts/accounts_widgets/account/status.dart';
|
||||
import 'package:island/chat/chat_widgets/chat_room_list_tile.dart';
|
||||
import 'package:island/pagination/pagination.dart';
|
||||
import 'package:island/posts/post/post_list.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_item.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_list.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/chat/chat_models/chat.dart';
|
||||
import 'package:island/core/services/color.dart';
|
||||
import 'package:island/core/services/responsive.dart';
|
||||
import 'package:island/core/services/color_extraction.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/realms/realm/realms.dart';
|
||||
import 'package:island/realms/realms_models/realm.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/core/config.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:island/shared/widgets/pagination_list.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class _RealmPinnedPostsPageView extends HookConsumerWidget {
|
||||
final String realmSlug;
|
||||
|
||||
const _RealmPinnedPostsPageView({required this.realmSlug});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final provider = postListProvider(
|
||||
PostListQueryConfig(
|
||||
id: 'realm-$realmSlug-pinned',
|
||||
initialFilter: PostListQuery(realm: realmSlug, pinned: true),
|
||||
),
|
||||
);
|
||||
final pinnedPosts = ref.watch(provider);
|
||||
final pageController = usePageController();
|
||||
final currentPage = useState(0);
|
||||
|
||||
useEffect(() {
|
||||
void listener() {
|
||||
currentPage.value = pageController.page?.round() ?? 0;
|
||||
}
|
||||
|
||||
pageController.addListener(listener);
|
||||
return () => pageController.removeListener(listener);
|
||||
}, [pageController]);
|
||||
|
||||
return pinnedPosts.when(
|
||||
data: (data) {
|
||||
if (data.items.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
leading: const Icon(Symbols.push_pin),
|
||||
title: Text('pinnedPosts'.tr()),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
collapsedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: pageController,
|
||||
itemCount: data.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: SingleChildScrollView(
|
||||
child: Card(
|
||||
child: PostActionableItem(
|
||||
item: data.items[index],
|
||||
borderRadius: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
data.items.length,
|
||||
(index) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: EdgeInsets.symmetric(horizontal: 4),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: index == currentPage.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final realmAppbarForegroundColorProvider = FutureProvider.autoDispose
|
||||
.family<Color?, String>((ref, realmSlug) async {
|
||||
final realm = await ref.watch(realmProvider(realmSlug).future);
|
||||
if (realm?.background == null) return null;
|
||||
final colors = await ColorExtractionService.getColorsFromImage(
|
||||
CloudImageWidget.provider(
|
||||
file: realm!.background!,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
),
|
||||
);
|
||||
if (colors.isEmpty) return null;
|
||||
final dominantColor = colors.first;
|
||||
return dominantColor.computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white;
|
||||
});
|
||||
|
||||
final realmIdentityProvider = FutureProvider.autoDispose
|
||||
.family<SnRealmMember?, String>((ref, realmSlug) async {
|
||||
try {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final response = await apiClient.get(
|
||||
'/pass/realms/$realmSlug/members/me',
|
||||
);
|
||||
return SnRealmMember.fromJson(response.data);
|
||||
} catch (err) {
|
||||
if (err is DioException && err.response?.statusCode == 404) {
|
||||
return null; // No identity found, user is not a member
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
|
||||
final realmChatRoomsProvider = FutureProvider.autoDispose
|
||||
.family<List<SnChatRoom>, String>((ref, realmSlug) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final response = await apiClient.get('/messager/realms/$realmSlug/chat');
|
||||
return (response.data as List)
|
||||
.map((e) => SnChatRoom.fromJson(e))
|
||||
.toList();
|
||||
});
|
||||
|
||||
class RealmDetailScreen extends HookConsumerWidget {
|
||||
final String slug;
|
||||
|
||||
const RealmDetailScreen({super.key, required this.slug});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final realmState = ref.watch(realmProvider(slug));
|
||||
final appbarColor = ref.watch(realmAppbarForegroundColorProvider(slug));
|
||||
|
||||
final iconShadow = Shadow(
|
||||
color: appbarColor.value?.invert ?? Colors.black54,
|
||||
blurRadius: 5.0,
|
||||
offset: Offset(1.0, 1.0),
|
||||
);
|
||||
|
||||
final realmIdentity = ref.watch(realmIdentityProvider(slug));
|
||||
final realmChatRooms = ref.watch(realmChatRoomsProvider(slug));
|
||||
|
||||
Widget realmDescriptionWidget(SnRealm realm) => Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
collapsedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
title: const Text('description').tr(),
|
||||
initiallyExpanded:
|
||||
realmIdentity.hasValue && realmIdentity.value == null,
|
||||
tilePadding: EdgeInsets.only(left: 24, right: 20),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
realm.description,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
).padding(horizontal: 20, bottom: 16, top: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget realmActionWidget(SnRealm realm) => Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post('/pass/realms/$slug/members/me');
|
||||
ref.invalidate(realmIdentityProvider(slug));
|
||||
ref.invalidate(realmsJoinedProvider);
|
||||
showSnackBar('realmJoinSuccess'.tr());
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: const Text('realmJoin').tr(),
|
||||
).padding(all: 16),
|
||||
);
|
||||
|
||||
Widget realmChatRoomListWidget(SnRealm realm) => Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'chatTabGroup',
|
||||
).tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
|
||||
realmChatRooms.when(
|
||||
loading: () => Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('Error: $error')),
|
||||
data: (rooms) {
|
||||
if (rooms.isEmpty) {
|
||||
return Text(
|
||||
'dataEmpty',
|
||||
).tr().padding(horizontal: 24, bottom: 12);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
for (final room in rooms)
|
||||
ChatRoomListTile(
|
||||
room: room,
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': room.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: isWideScreen(context)
|
||||
? realmState.when(
|
||||
data: (realm) => AppBar(
|
||||
foregroundColor: appbarColor.value,
|
||||
leading: PageBackButton(
|
||||
color: appbarColor.value,
|
||||
shadows: [iconShadow],
|
||||
),
|
||||
flexibleSpace: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: realm!.background != null
|
||||
? CloudImageWidget(file: realm.background!)
|
||||
: Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).appBarTheme.backgroundColor,
|
||||
),
|
||||
),
|
||||
FlexibleSpaceBar(
|
||||
title: Text(
|
||||
realm.name,
|
||||
style: TextStyle(
|
||||
color:
|
||||
appbarColor.value ??
|
||||
Theme.of(context).appBarTheme.foregroundColor,
|
||||
shadows: [iconShadow],
|
||||
),
|
||||
),
|
||||
background: Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.people, shadows: [iconShadow]),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
_RealmMemberListSheet(realmSlug: slug),
|
||||
);
|
||||
},
|
||||
),
|
||||
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
error: (_, _) => AppBar(leading: PageBackButton()),
|
||||
loading: () => AppBar(leading: PageBackButton()),
|
||||
)
|
||||
: null,
|
||||
body: realmState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('Error: $error')),
|
||||
data: (realm) => isWideScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _RealmPinnedPostsPageView(realmSlug: slug),
|
||||
),
|
||||
SliverPostList(
|
||||
query: PostListQuery(realm: slug, pinned: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
realmIdentity.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data: (identity) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
realmDescriptionWidget(realm!),
|
||||
if (identity == null && realm.isCommunity)
|
||||
realmActionWidget(realm)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
realmChatRoomListWidget(realm!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8, top: 8)
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 180,
|
||||
pinned: true,
|
||||
foregroundColor: appbarColor.value,
|
||||
leading: PageBackButton(
|
||||
color: appbarColor.value,
|
||||
shadows: [iconShadow],
|
||||
),
|
||||
flexibleSpace: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: realm!.background != null
|
||||
? CloudImageWidget(file: realm.background!)
|
||||
: Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).appBarTheme.backgroundColor,
|
||||
),
|
||||
),
|
||||
FlexibleSpaceBar(
|
||||
title: Text(
|
||||
realm.name,
|
||||
style: TextStyle(
|
||||
color:
|
||||
appbarColor.value ??
|
||||
Theme.of(context).appBarTheme.foregroundColor,
|
||||
shadows: [iconShadow],
|
||||
),
|
||||
),
|
||||
background:
|
||||
Container(), // Empty container since background is handled by Stack
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.people, shadows: [iconShadow]),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
_RealmMemberListSheet(realmSlug: slug),
|
||||
);
|
||||
},
|
||||
),
|
||||
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
SliverGap(4),
|
||||
SliverToBoxAdapter(
|
||||
child: realmIdentity.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data: (identity) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
realmDescriptionWidget(realm),
|
||||
if (identity == null && realm.isCommunity)
|
||||
realmActionWidget(realm)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: realmChatRoomListWidget(realm)),
|
||||
SliverToBoxAdapter(
|
||||
child: _RealmPinnedPostsPageView(realmSlug: slug),
|
||||
),
|
||||
SliverPostList(
|
||||
query: PostListQuery(realm: slug, pinned: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmActionMenu extends HookConsumerWidget {
|
||||
final String realmSlug;
|
||||
final Shadow iconShadow;
|
||||
|
||||
const _RealmActionMenu({required this.realmSlug, required this.iconShadow});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
|
||||
final isModerator = realmIdentity.when(
|
||||
data: (identity) => (identity?.role ?? 0) >= 50,
|
||||
loading: () => false,
|
||||
error: (_, _) => false,
|
||||
);
|
||||
|
||||
return PopupMenuButton(
|
||||
icon: Icon(Icons.more_vert, shadows: [iconShadow]),
|
||||
itemBuilder: (context) => [
|
||||
if (isModerator)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
context.pushReplacementNamed(
|
||||
'realmEdit',
|
||||
pathParameters: {'slug': realmSlug},
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const Gap(12),
|
||||
const Text('editRealm').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
realmIdentity.when(
|
||||
data: (identity) => (identity?.role ?? 0) >= 100
|
||||
? PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red),
|
||||
const Gap(12),
|
||||
const Text(
|
||||
'deleteRealm',
|
||||
style: TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showConfirmAlert(
|
||||
'deleteRealmHint'.tr(),
|
||||
'deleteRealm'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
client.delete('/pass/realms/$realmSlug');
|
||||
ref.invalidate(realmsJoinedProvider);
|
||||
if (context.mounted) {
|
||||
context.pop(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
: PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.exit_to_app,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'leaveRealm',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showConfirmAlert(
|
||||
'leaveRealmHint'.tr(),
|
||||
'leaveRealm'.tr(),
|
||||
).then((confirm) async {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/pass/realms/$realmSlug/members/me',
|
||||
);
|
||||
ref.invalidate(realmsJoinedProvider);
|
||||
if (context.mounted) {
|
||||
context.pop(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
loading: () => const PopupMenuItem(
|
||||
enabled: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (_, _) => PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.exit_to_app,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'leaveRealm',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showConfirmAlert('leaveRealmHint'.tr(), 'leaveRealm'.tr()).then((
|
||||
confirm,
|
||||
) async {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete('/pass/realms/$realmSlug/members/me');
|
||||
ref.invalidate(realmsJoinedProvider);
|
||||
if (context.mounted) {
|
||||
context.pop(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final realmMemberListNotifierProvider = AsyncNotifierProvider.autoDispose
|
||||
.family(RealmMemberListNotifier.new);
|
||||
|
||||
class RealmMemberListNotifier
|
||||
extends AsyncNotifier<PaginationState<SnRealmMember>>
|
||||
with AsyncPaginationController<SnRealmMember> {
|
||||
String arg;
|
||||
RealmMemberListNotifier(this.arg);
|
||||
|
||||
static const int pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<List<SnRealmMember>> fetch() async {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
|
||||
final response = await apiClient.get(
|
||||
'/pass/realms/$arg/members',
|
||||
queryParameters: {
|
||||
'offset': fetchedCount,
|
||||
'take': pageSize,
|
||||
'withStatus': true,
|
||||
},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => SnRealmMember.fromJson(e)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmMemberListSheet extends HookConsumerWidget {
|
||||
final String realmSlug;
|
||||
const _RealmMemberListSheet({required this.realmSlug});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final memberListProvider = realmMemberListNotifierProvider(realmSlug);
|
||||
|
||||
final memberListState = ref.watch(memberListProvider);
|
||||
final memberListNotifier = ref.watch(memberListProvider.notifier);
|
||||
final realmIdentity = ref.watch(realmIdentityProvider(realmSlug));
|
||||
|
||||
Future<void> invitePerson() async {
|
||||
final result = await showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => const AccountPickerSheet(),
|
||||
);
|
||||
if (result == null) return;
|
||||
try {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.post(
|
||||
'/pass/realms/invites/$realmSlug',
|
||||
data: {'related_user_id': result.id, 'role': 0},
|
||||
);
|
||||
// Refresh the provider
|
||||
memberListNotifier.refresh();
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildMemberListHeader() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return Text(
|
||||
'members'.plural(memberListState.value?.totalCount ?? 0),
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.person_add),
|
||||
onPressed: invitePerson,
|
||||
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.refresh),
|
||||
onPressed: () {
|
||||
// Refresh the provider
|
||||
ref.invalidate(memberListProvider);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMemberListContent() {
|
||||
return Expanded(
|
||||
child: PaginationList(
|
||||
provider: memberListProvider,
|
||||
notifier: memberListProvider.notifier,
|
||||
itemBuilder: (context, index, member) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.only(left: 16, right: 12),
|
||||
leading: AccountPfcRegion(
|
||||
uname: member.account!.name,
|
||||
child: ProfilePictureWidget(
|
||||
file: member.account!.profile.picture,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
member.account!.nick,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (member.status != null)
|
||||
AccountStatusLabel(status: member.status!),
|
||||
if (member.joinedAt == null)
|
||||
const Icon(Symbols.pending_actions, size: 20),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
member.role >= 100
|
||||
? 'permissionOwner'
|
||||
: member.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
Text('·').bold().padding(horizontal: 6),
|
||||
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) {
|
||||
// Refresh the provider
|
||||
ref.invalidate(memberListProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
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(
|
||||
'/pass/realms/$realmSlug/members/${member.accountId}',
|
||||
);
|
||||
// Refresh the provider
|
||||
ref.invalidate(memberListProvider);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
buildMemberListHeader(),
|
||||
const Divider(height: 1),
|
||||
buildMemberListContent(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<int>(
|
||||
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(
|
||||
'/pass/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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
271
lib/realms/realm/realm_form.dart
Normal file
271
lib/realms/realm/realm_form.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
import 'package:croppy/croppy.dart' show CropAspectRatio;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/core/services/image.dart';
|
||||
import 'package:island/drive/drive_models/file.dart';
|
||||
import 'package:island/realms/realm/realms.dart';
|
||||
import 'package:island/realms/realms_models/realm.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/drive/drive_service.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class NewRealmScreen extends StatelessWidget {
|
||||
const NewRealmScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const EditRealmScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class EditRealmScreen extends HookConsumerWidget {
|
||||
final String? slug;
|
||||
const EditRealmScreen({super.key, this.slug});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final submitting = useState(false);
|
||||
|
||||
final picture = useState<SnCloudFile?>(null);
|
||||
final background = useState<SnCloudFile?>(null);
|
||||
final isPublic = useState(true);
|
||||
final isCommunity = useState(false);
|
||||
|
||||
final slugController = useTextEditingController();
|
||||
final nameController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
|
||||
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
|
||||
|
||||
final realm = ref.watch(realmProvider(slug));
|
||||
|
||||
useEffect(() {
|
||||
if (realm.value != null) {
|
||||
picture.value = realm.value!.picture;
|
||||
background.value = realm.value!.background;
|
||||
slugController.text = realm.value!.slug;
|
||||
nameController.text = realm.value!.name;
|
||||
descriptionController.text = realm.value!.description;
|
||||
isPublic.value = realm.value!.isPublic;
|
||||
isCommunity.value = realm.value!.isCommunity;
|
||||
}
|
||||
return null;
|
||||
}, [realm]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
const CropAspectRatio(height: 7, width: 16)
|
||||
else
|
||||
const CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showLoadingModal(context);
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile = await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(data: result, type: UniversalFileType.image),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
switch (position) {
|
||||
case 'picture':
|
||||
picture.value = cloudFile;
|
||||
case 'background':
|
||||
background.value = cloudFile;
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performAction() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.request(
|
||||
'/pass${slug == null ? '/realms' : '/realms/$slug'}',
|
||||
data: {
|
||||
'slug': slugController.text,
|
||||
'name': nameController.text,
|
||||
'description': descriptionController.text,
|
||||
'background_id': background.value?.id,
|
||||
'picture_id': picture.value?.id,
|
||||
'is_public': isPublic.value,
|
||||
'is_community': isCommunity.value,
|
||||
},
|
||||
options: Options(method: slug == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.pop(SnRealm.fromJson(resp.data));
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
file: picture.value,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.public),
|
||||
title: Text('publicRealm').tr(),
|
||||
subtitle: Text('publicRealmDescription').tr(),
|
||||
value: isPublic.value,
|
||||
onChanged: (value) {
|
||||
isPublic.value = value ?? true;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.travel_explore),
|
||||
title: Text('communityRealm').tr(),
|
||||
subtitle: Text('communityRealmDescription').tr(),
|
||||
value: isCommunity.value,
|
||||
onChanged: (value) {
|
||||
isCommunity.value = value ?? false;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
254
lib/realms/realm/realms.dart
Normal file
254
lib/realms/realm/realms.dart
Normal file
@@ -0,0 +1,254 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/realms/realms_models/realm.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/accounts/accounts_pod.dart';
|
||||
import 'package:island/realms/realms_widgets/realm/realm_list_tile.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/shared/widgets/extended_refresh_indicator.dart';
|
||||
|
||||
part 'realms.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnRealm>> realmsJoined(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/pass/realms');
|
||||
return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnRealm?> realm(Ref ref, String? identifier) async {
|
||||
if (identifier == null) return null;
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/pass/realms/$identifier');
|
||||
return SnRealm.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class RealmListScreen extends HookConsumerWidget {
|
||||
const RealmListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final realms = ref.watch(realmsJoinedProvider);
|
||||
final realmInvites = ref.watch(realmInvitesProvider);
|
||||
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('realms').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.travel_explore),
|
||||
onPressed: () => context.pushNamed(
|
||||
'universalSearch',
|
||||
queryParameters: {'tab': 'realms'},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Badge(
|
||||
label: Text(
|
||||
realmInvites.when(
|
||||
data: (invites) => invites.length.toString(),
|
||||
error: (_, _) => '0',
|
||||
loading: () => '0',
|
||||
),
|
||||
),
|
||||
isLabelVisible: realmInvites.when(
|
||||
data: (invites) => invites.isNotEmpty,
|
||||
error: (_, _) => false,
|
||||
loading: () => false,
|
||||
),
|
||||
child: const Icon(Symbols.email),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (_) => const _RealmInviteSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
floatingActionButton: userInfo.value != null
|
||||
? FloatingActionButton(
|
||||
child: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Gap(40),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
leading: const Icon(Symbols.group_add),
|
||||
title: Text('createRealm').tr(),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
context.pushNamed('realmNew').then((value) {
|
||||
if (value != null) {
|
||||
// Fire realm refresh event if needed
|
||||
// eventBus.fire(const RealmsRefreshEvent());
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
).padding(bottom: MediaQuery.of(context).padding.bottom)
|
||||
: null,
|
||||
body: userInfo.value == null
|
||||
? const ResponseUnauthorizedWidget()
|
||||
: ExtendedRefreshIndicator(
|
||||
child: realms.when(
|
||||
data: (value) => Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.only(
|
||||
top: 8,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
itemCount: value.length,
|
||||
itemBuilder: (context, item) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
child: RealmListTile(realm: value[item]),
|
||||
).padding(horizontal: 8).center();
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => ResponseErrorWidget(
|
||||
error: e,
|
||||
onRetry: () => ref.invalidate(realmsJoinedProvider),
|
||||
),
|
||||
),
|
||||
onRefresh: () => ref.refresh(realmsJoinedProvider.future),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SnRealmMember>> realmInvites(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/pass/realms/invites');
|
||||
return resp.data
|
||||
.map((e) => SnRealmMember.fromJson(e))
|
||||
.cast<SnRealmMember>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class _RealmInviteSheet extends HookConsumerWidget {
|
||||
const _RealmInviteSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final invites = ref.watch(realmInvitesProvider);
|
||||
|
||||
Future<void> acceptInvite(SnRealmMember invite) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/pass/realms/invites/${invite.realm!.slug}/accept');
|
||||
ref.invalidate(realmInvitesProvider);
|
||||
ref.invalidate(realmsJoinedProvider);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> declineInvite(SnRealmMember invite) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/pass/realms/invites/${invite.realm!.slug}/decline');
|
||||
ref.invalidate(realmInvitesProvider);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'invites'.tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.refresh),
|
||||
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
|
||||
onPressed: () {
|
||||
ref.invalidate(realmInvitesProvider);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: invites.when(
|
||||
data: (items) => items.isEmpty
|
||||
? Center(
|
||||
child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invite = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
file: invite.realm!.picture,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(invite.realm!.name),
|
||||
subtitle: Text(
|
||||
invite.role >= 100
|
||||
? 'permissionOwner'
|
||||
: invite.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
onPressed: () => acceptInvite(invite),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => declineInvite(invite),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(realmInvitesProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
160
lib/realms/realm/realms.g.dart
Normal file
160
lib/realms/realm/realms.g.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'realms.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(realmsJoined)
|
||||
final realmsJoinedProvider = RealmsJoinedProvider._();
|
||||
|
||||
final class RealmsJoinedProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnRealm>>,
|
||||
List<SnRealm>,
|
||||
FutureOr<List<SnRealm>>
|
||||
>
|
||||
with $FutureModifier<List<SnRealm>>, $FutureProvider<List<SnRealm>> {
|
||||
RealmsJoinedProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'realmsJoinedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$realmsJoinedHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnRealm>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnRealm>> create(Ref ref) {
|
||||
return realmsJoined(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$realmsJoinedHash() => r'b15029acd38f03bbbb8708adb78f25ac357a0421';
|
||||
|
||||
@ProviderFor(realm)
|
||||
final realmProvider = RealmFamily._();
|
||||
|
||||
final class RealmProvider
|
||||
extends
|
||||
$FunctionalProvider<AsyncValue<SnRealm?>, SnRealm?, FutureOr<SnRealm?>>
|
||||
with $FutureModifier<SnRealm?>, $FutureProvider<SnRealm?> {
|
||||
RealmProvider._({
|
||||
required RealmFamily super.from,
|
||||
required String? super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'realmProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$realmHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'realmProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<SnRealm?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<SnRealm?> create(Ref ref) {
|
||||
final argument = this.argument as String?;
|
||||
return realm(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is RealmProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$realmHash() => r'71a126ab2810566646e1629290c1ce9ffa0839e3';
|
||||
|
||||
final class RealmFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<SnRealm?>, String?> {
|
||||
RealmFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'realmProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
RealmProvider call(String? identifier) =>
|
||||
RealmProvider._(argument: identifier, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'realmProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(realmInvites)
|
||||
final realmInvitesProvider = RealmInvitesProvider._();
|
||||
|
||||
final class RealmInvitesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnRealmMember>>,
|
||||
List<SnRealmMember>,
|
||||
FutureOr<List<SnRealmMember>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SnRealmMember>>,
|
||||
$FutureProvider<List<SnRealmMember>> {
|
||||
RealmInvitesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'realmInvitesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$realmInvitesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnRealmMember>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnRealmMember>> create(Ref ref) {
|
||||
return realmInvites(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$realmInvitesHash() => r'92cce0978c7ca8813e27ae42fc6f3a93a09a8962';
|
||||
Reference in New Issue
Block a user