💄 Redesign the post tags and categories page

This commit is contained in:
2025-12-06 20:40:28 +08:00
parent 36b0f55a47
commit 3ef2f13dd3
8 changed files with 348 additions and 392 deletions

View File

@@ -618,6 +618,7 @@
"tagsHint": "Enter tags, separated by commas", "tagsHint": "Enter tags, separated by commas",
"categories": "Categories", "categories": "Categories",
"categoriesHint": "Enter categories, separated by commas", "categoriesHint": "Enter categories, separated by commas",
"categoriesAndTags": "Categories & Tags",
"chatNotJoined": "You have not joined this chat yet.", "chatNotJoined": "You have not joined this chat yet.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.", "chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat", "chatJoin": "Join the Chat",

View File

@@ -0,0 +1,52 @@
// Post Categories Notifier
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
final postCategoriesProvider =
AsyncNotifierProvider.autoDispose<
PostCategoriesNotifier,
List<SnPostCategory>
>(PostCategoriesNotifier.new);
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
with AsyncPaginationController<SnPostCategory> {
@override
Future<List<SnPostCategory>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostCategory.fromJson(json)).toList();
}
}
// Post Tags Notifier
final postTagsProvider =
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
PostTagsNotifier.new,
);
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
with AsyncPaginationController<SnPostTag> {
@override
Future<List<SnPostTag>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostTag.fromJson(json)).toList();
}
}

View File

@@ -105,10 +105,9 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'articleCompose', name: 'articleCompose',
path: '/articles/compose', path: '/articles/compose',
builder: builder: (context, state) => ArticleComposeScreen(
(context, state) => ArticleComposeScreen( initialState: state.extra as PostComposeInitialState?,
initialState: state.extra as PostComposeInitialState?, ),
),
), ),
GoRoute( GoRoute(
name: 'articleEdit', name: 'articleEdit',
@@ -190,12 +189,11 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'explore', name: 'explore',
path: '/', path: '/',
pageBuilder: pageBuilder: (context, state) => CustomTransitionPage(
(context, state) => CustomTransitionPage( key: const ValueKey('explore'),
key: const ValueKey('explore'), child: const ExploreScreen(),
child: const ExploreScreen(), transitionsBuilder: _tabPagesTransitionBuilder,
transitionsBuilder: _tabPagesTransitionBuilder, ),
),
), ),
GoRoute( GoRoute(
name: 'postSearch', name: 'postSearch',
@@ -220,11 +218,6 @@ final routerProvider = Provider<GoRouter>((ref) {
return PostCategoryDetailScreen(slug: slug, isCategory: true); return PostCategoryDetailScreen(slug: slug, isCategory: true);
}, },
), ),
GoRoute(
name: 'postTags',
path: '/posts/tags',
builder: (context, state) => const PostTagsListScreen(),
),
GoRoute( GoRoute(
name: 'postTagDetail', name: 'postTagDetail',
path: '/posts/tags/:slug', path: '/posts/tags/:slug',
@@ -260,12 +253,11 @@ final routerProvider = Provider<GoRouter>((ref) {
// Chat tab // Chat tab
ShellRoute( ShellRoute(
pageBuilder: pageBuilder: (context, state, child) => CustomTransitionPage(
(context, state, child) => CustomTransitionPage( key: const ValueKey('chat'),
key: const ValueKey('chat'), child: ChatShellScreen(child: child),
child: ChatShellScreen(child: child), transitionsBuilder: _tabPagesTransitionBuilder,
transitionsBuilder: _tabPagesTransitionBuilder, ),
),
routes: [ routes: [
GoRoute( GoRoute(
name: 'chatList', name: 'chatList',
@@ -303,12 +295,11 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'realmList', name: 'realmList',
path: '/realms', path: '/realms',
pageBuilder: pageBuilder: (context, state) => CustomTransitionPage(
(context, state) => CustomTransitionPage( key: const ValueKey('realms'),
key: const ValueKey('realms'), child: const RealmListScreen(),
child: const RealmListScreen(), transitionsBuilder: _tabPagesTransitionBuilder,
transitionsBuilder: _tabPagesTransitionBuilder, ),
),
routes: [ routes: [
GoRoute( GoRoute(
name: 'realmNew', name: 'realmNew',
@@ -336,12 +327,11 @@ final routerProvider = Provider<GoRouter>((ref) {
// Account tab // Account tab
ShellRoute( ShellRoute(
pageBuilder: pageBuilder: (context, state, child) => CustomTransitionPage(
(context, state, child) => CustomTransitionPage( key: const ValueKey('account'),
key: const ValueKey('account'), child: AccountShellScreen(child: child),
child: AccountShellScreen(child: child), transitionsBuilder: _tabPagesTransitionBuilder,
transitionsBuilder: _tabPagesTransitionBuilder, ),
),
routes: [ routes: [
GoRoute( GoRoute(
name: 'account', name: 'account',
@@ -352,8 +342,8 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'stickerMarketplace', name: 'stickerMarketplace',
path: '/stickers', path: '/stickers',
builder: builder: (context, state) =>
(context, state) => const MarketplaceStickersScreen(), const MarketplaceStickersScreen(),
routes: [ routes: [
GoRoute( GoRoute(
name: 'stickerPackDetail', name: 'stickerPackDetail',
@@ -368,8 +358,8 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'webFeedMarketplace', name: 'webFeedMarketplace',
path: '/feeds', path: '/feeds',
builder: builder: (context, state) =>
(context, state) => const MarketplaceWebFeedsScreen(), const MarketplaceWebFeedsScreen(),
routes: [ routes: [
GoRoute( GoRoute(
name: 'webFeedDetail', name: 'webFeedDetail',
@@ -516,29 +506,25 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'developerHub', name: 'developerHub',
path: '/developers', path: '/developers',
builder: builder: (context, state) => DeveloperHubScreen(
(context, state) => DeveloperHubScreen( initialPublisherName: state.uri.queryParameters['publisher'],
initialPublisherName: initialProjectId: state.uri.queryParameters['project'],
state.uri.queryParameters['publisher'], ),
initialProjectId: state.uri.queryParameters['project'],
),
routes: [ routes: [
GoRoute( GoRoute(
name: 'developerProjectNew', name: 'developerProjectNew',
path: ':name/projects/new', path: ':name/projects/new',
builder: builder: (context, state) => NewProjectScreen(
(context, state) => NewProjectScreen( publisherName: state.pathParameters['name']!,
publisherName: state.pathParameters['name']!, ),
),
), ),
GoRoute( GoRoute(
name: 'developerProjectEdit', name: 'developerProjectEdit',
path: ':name/projects/:id/edit', path: ':name/projects/:id/edit',
builder: builder: (context, state) => EditProjectScreen(
(context, state) => EditProjectScreen( publisherName: state.pathParameters['name']!,
publisherName: state.pathParameters['name']!, id: state.pathParameters['id']!,
id: state.pathParameters['id']!, ),
),
), ),
GoRoute( GoRoute(
name: 'developerProjectDetail', name: 'developerProjectDetail',
@@ -558,22 +544,20 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'developerAppDetail', name: 'developerAppDetail',
path: 'apps/:appId', path: 'apps/:appId',
builder: builder: (context, state) => AppDetailScreen(
(context, state) => AppDetailScreen( publisherName: state.pathParameters['name']!,
publisherName: state.pathParameters['name']!, projectId: state.pathParameters['projectId']!,
projectId: state.pathParameters['projectId']!, appId: state.pathParameters['appId']!,
appId: state.pathParameters['appId']!, ),
),
), ),
GoRoute( GoRoute(
name: 'developerBotDetail', name: 'developerBotDetail',
path: 'bots/:botId', path: 'bots/:botId',
builder: builder: (context, state) => BotDetailScreen(
(context, state) => BotDetailScreen( publisherName: state.pathParameters['name']!,
publisherName: state.pathParameters['name']!, projectId: state.pathParameters['projectId']!,
projectId: state.pathParameters['projectId']!, botId: state.pathParameters['botId']!,
botId: state.pathParameters['botId']!, ),
),
), ),
], ],
), ),

View File

@@ -102,7 +102,7 @@ class ExploreScreen extends HookConsumerWidget {
// Listen for post creation events to refresh activities // Listen for post creation events to refresh activities
useEffect(() { useEffect(() {
final subscription = eventBus.on<PostCreatedEvent>().listen((event) { final subscription = eventBus.on<PostCreatedEvent>().listen((event) {
ref.invalidate(activityListProvider); ref.read(activityListProvider.notifier).refresh();
}); });
return subscription.cancel; return subscription.cancel;
}, []); }, []);
@@ -183,25 +183,13 @@ class ExploreScreen extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.category), const Icon(Symbols.category),
const Gap(12), const Gap(12),
Text('categories').tr(), Text('categoriesAndTags').tr(),
], ],
), ),
onTap: () { onTap: () {
context.pushNamed('postCategories'); context.pushNamed('postCategories');
}, },
), ),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -490,25 +478,13 @@ class ExploreScreen extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.category), const Icon(Symbols.category),
const Gap(12), const Gap(12),
Text('categories').tr(), Text('categoriesAndTags').tr(),
], ],
), ),
onTap: () { onTap: () {
context.pushNamed('postCategories'); context.pushNamed('postCategories');
}, },
), ),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [

View File

@@ -2,118 +2,113 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart'; import 'package:island/pods/post/post_categories.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
// Post Categories Notifier class PostCategoriesListScreen extends HookConsumerWidget {
final postCategoriesProvider = AsyncNotifierProvider.autoDispose<
PostCategoriesNotifier,
List<SnPostCategory>
>(PostCategoriesNotifier.new);
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
with AsyncPaginationController<SnPostCategory> {
@override
Future<List<SnPostCategory>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostCategory.fromJson(json)).toList();
}
}
// Post Tags Notifier
final postTagsProvider =
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
PostTagsNotifier.new,
);
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
with AsyncPaginationController<SnPostTag> {
@override
Future<List<SnPostTag>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostTag.fromJson(json)).toList();
}
}
class PostCategoriesListScreen extends ConsumerWidget {
const PostCategoriesListScreen({super.key}); const PostCategoriesListScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold( return DefaultTabController(
appBar: AppBar(title: const Text('categories').tr()), length: 2,
body: PaginationList( child: AppScaffold(
provider: postCategoriesProvider, isNoBackground: false,
notifier: postCategoriesProvider.notifier, appBar: AppBar(
padding: EdgeInsets.zero, title: const Text('categoriesAndTags').tr(),
itemBuilder: (context, index, category) { bottom: TabBar(
return ListTile( tabs: [
leading: const Icon(Symbols.category), Tab(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), child: Text(
trailing: const Icon(Symbols.chevron_right), 'categories'.tr(),
title: Text(category.categoryDisplayTitle), style: TextStyle(
subtitle: Text('postCount'.plural(category.usage)), color: Theme.of(context).appBarTheme.foregroundColor,
onTap: () { ),
context.pushNamed( ),
'postCategoryDetail', ),
pathParameters: {'slug': category.slug}, Tab(
); child: Text(
}, 'tags'.tr(),
); style: TextStyle(
}, color: Theme.of(context).appBarTheme.foregroundColor,
),
),
),
],
),
),
body: const TabBarView(children: [_CategoriesTab(), _TagsTab()]),
), ),
); );
} }
} }
class PostTagsListScreen extends ConsumerWidget { class _CategoriesTab extends ConsumerWidget {
const PostTagsListScreen({super.key}); const _CategoriesTab();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold( return PaginationList(
appBar: AppBar(title: const Text('tags').tr()), provider: postCategoriesProvider,
body: PaginationList( notifier: postCategoriesProvider.notifier,
provider: postTagsProvider, footerSkeletonMaxWidth: 640,
notifier: postTagsProvider.notifier, padding: EdgeInsets.zero,
padding: EdgeInsets.zero, itemBuilder: (context, index, category) {
itemBuilder: (context, index, tag) { return Center(
return ListTile( child: ConstrainedBox(
title: Text(tag.name ?? '#${tag.slug}'), constraints: const BoxConstraints(maxWidth: 640),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), child: ListTile(
leading: const Icon(Symbols.label), leading: const Icon(Symbols.category),
trailing: const Icon(Symbols.chevron_right), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
subtitle: Text('postCount'.plural(tag.usage)), trailing: const Icon(Symbols.chevron_right),
onTap: () { title: Text(category.categoryDisplayTitle),
context.pushNamed( subtitle: Text('postCount'.plural(category.usage)),
'postTagDetail', onTap: () {
pathParameters: {'slug': tag.slug}, context.pushNamed(
); 'postCategoryDetail',
}, pathParameters: {'slug': category.slug},
); );
}, },
), ),
),
);
},
);
}
}
class _TagsTab extends ConsumerWidget {
const _TagsTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
return PaginationList(
provider: postTagsProvider,
notifier: postTagsProvider.notifier,
footerSkeletonMaxWidth: 640,
padding: EdgeInsets.zero,
itemBuilder: (context, index, tag) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: ListTile(
title: Text(tag.name ?? '#${tag.slug}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.label),
trailing: const Icon(Symbols.chevron_right),
subtitle: Text('postCount'.plural(tag.usage)),
onTap: () {
context.pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
},
),
),
);
},
); );
} }
} }

View File

@@ -22,6 +22,7 @@ class PaginationList<T> extends HookConsumerWidget {
final bool showDefaultWidgets; final bool showDefaultWidgets;
final EdgeInsets? padding; final EdgeInsets? padding;
final Widget? footerSkeletonChild; final Widget? footerSkeletonChild;
final double? footerSkeletonMaxWidth;
const PaginationList({ const PaginationList({
super.key, super.key,
required this.provider, required this.provider,
@@ -32,6 +33,7 @@ class PaginationList<T> extends HookConsumerWidget {
this.showDefaultWidgets = true, this.showDefaultWidgets = true,
this.padding, this.padding,
this.footerSkeletonChild, this.footerSkeletonChild,
this.footerSkeletonMaxWidth,
}); });
@override @override
@@ -53,7 +55,9 @@ class PaginationList<T> extends HookConsumerWidget {
).colorScheme.surfaceContainerHighest, ).colorScheme.surfaceContainerHighest,
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(), child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
), ),
); );
return SliverList.list(children: content); return SliverList.list(children: content);
@@ -75,6 +79,7 @@ class PaginationList<T> extends HookConsumerWidget {
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
} }
final entry = data.value?[idx]; final entry = data.value?[idx];
@@ -102,7 +107,9 @@ class PaginationList<T> extends HookConsumerWidget {
).colorScheme.surfaceContainerHighest, ).colorScheme.surfaceContainerHighest,
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(), child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
), ),
); );
return SizedBox( return SizedBox(
@@ -128,6 +135,7 @@ class PaginationList<T> extends HookConsumerWidget {
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
} }
final entry = data.value?[idx]; final entry = data.value?[idx];
@@ -159,6 +167,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
final bool isSliver; final bool isSliver;
final bool showDefaultWidgets; final bool showDefaultWidgets;
final Widget? footerSkeletonChild; final Widget? footerSkeletonChild;
final double? footerSkeletonMaxWidth;
const PaginationWidget({ const PaginationWidget({
super.key, super.key,
required this.provider, required this.provider,
@@ -168,6 +177,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
this.isSliver = false, this.isSliver = false,
this.showDefaultWidgets = true, this.showDefaultWidgets = true,
this.footerSkeletonChild, this.footerSkeletonChild,
this.footerSkeletonMaxWidth,
}); });
@override @override
@@ -189,7 +199,9 @@ class PaginationWidget<T> extends HookConsumerWidget {
).colorScheme.surfaceContainerHighest, ).colorScheme.surfaceContainerHighest,
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(), child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
), ),
); );
return SliverList.list(children: content); return SliverList.list(children: content);
@@ -207,6 +219,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
final content = contentBuilder(data.value ?? [], footer); final content = contentBuilder(data.value ?? [], footer);
@@ -229,7 +242,9 @@ class PaginationWidget<T> extends HookConsumerWidget {
).colorScheme.surfaceContainerHighest, ).colorScheme.surfaceContainerHighest,
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(), child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
), ),
); );
return SizedBox( return SizedBox(
@@ -250,6 +265,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
final content = contentBuilder(data.value ?? [], footer); final content = contentBuilder(data.value ?? [], footer);
@@ -272,6 +288,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
final PaginationController<T> noti; final PaginationController<T> noti;
final AsyncValue<List<T>> data; final AsyncValue<List<T>> data;
final Widget? skeletonChild; final Widget? skeletonChild;
final double? skeletonMaxWidth;
final bool isSliver; final bool isSliver;
const PaginationListFooter({ const PaginationListFooter({
@@ -279,6 +296,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
required this.noti, required this.noti,
required this.data, required this.data,
this.skeletonChild, this.skeletonChild,
this.skeletonMaxWidth,
this.isSliver = false, this.isSliver = false,
}); });
@@ -293,7 +311,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest,
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: skeletonChild ?? _DefaultSkeletonChild(), child: skeletonChild ?? _DefaultSkeletonChild(maxWidth: skeletonMaxWidth),
); );
final child = hasBeenVisible.value final child = hasBeenVisible.value
? data.isLoading ? data.isLoading
@@ -322,14 +340,24 @@ class PaginationListFooter<T> extends HookConsumerWidget {
} }
class _DefaultSkeletonChild extends StatelessWidget { class _DefaultSkeletonChild extends StatelessWidget {
const _DefaultSkeletonChild(); final double? maxWidth;
const _DefaultSkeletonChild({this.maxWidth});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( final content = ListTile(
title: Text('Some data'), title: Text('Some data'),
subtitle: const Text('Subtitle here'), subtitle: const Text('Subtitle here'),
trailing: const Icon(Icons.ac_unit), trailing: const Icon(Icons.ac_unit),
); );
if (maxWidth != null) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth!),
child: content,
),
);
}
return content;
} }
} }

View File

@@ -9,33 +9,14 @@ import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart'; import 'package:island/models/post_tag.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/post/post_categories.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart'; import 'package:island/widgets/post/compose_shared.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'compose_settings_sheet.g.dart';
@riverpod
Future<List<SnPostCategory>> postCategories(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/categories');
final categories =
resp.data
.map((e) => SnPostCategory.fromJson(e))
.cast<SnPostCategory>()
.toList();
// Remove duplicates based on id
final uniqueCategories = <String, SnPostCategory>{};
for (final category in categories) {
uniqueCategories[category.id] = category;
}
return uniqueCategories.values.toList();
}
class ComposeSettingsSheet extends HookConsumerWidget { class ComposeSettingsSheet extends HookConsumerWidget {
final ComposeState state; final ComposeState state;
@@ -121,39 +102,38 @@ class ComposeSettingsSheet extends HookConsumerWidget {
void showVisibilitySheet() { void showVisibilitySheet() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'postVisibility'.tr(),
titleText: 'postVisibility'.tr(), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ buildVisibilityOption(
buildVisibilityOption( context,
context, 0,
0, Symbols.public,
Symbols.public, 'postVisibilityPublic',
'postVisibilityPublic',
),
buildVisibilityOption(
context,
1,
Symbols.group,
'postVisibilityFriends',
),
buildVisibilityOption(
context,
2,
Symbols.link_off,
'postVisibilityUnlisted',
),
buildVisibilityOption(
context,
3,
Symbols.lock,
'postVisibilityPrivate',
),
],
), ),
), buildVisibilityOption(
context,
1,
Symbols.group,
'postVisibilityFriends',
),
buildVisibilityOption(
context,
2,
Symbols.link_off,
'postVisibilityUnlisted',
),
buildVisibilityOption(
context,
3,
Symbols.lock,
'postVisibilityPrivate',
),
],
),
),
); );
} }
@@ -182,8 +162,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
// Tags field // Tags field
@@ -209,51 +189,48 @@ class ComposeSettingsSheet extends HookConsumerWidget {
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: children: currentTags.map((tag) {
currentTags.map((tag) { return Container(
return Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16), ),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'#$tag',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onPrimary,
fontSize: 14,
),
), ),
padding: const EdgeInsets.symmetric( const Gap(4),
horizontal: 12, InkWell(
vertical: 6, onTap: () {
final newTags = List<String>.from(
state.tags.value,
)..remove(tag);
state.tags.value = newTags;
},
child: Icon(
Icons.close,
size: 16,
color: Theme.of(
context,
).colorScheme.onPrimary,
),
), ),
child: Row( ],
mainAxisSize: MainAxisSize.min, ),
children: [ );
Text( }).toList(),
'#$tag',
style: TextStyle(
color:
Theme.of(
context,
).colorScheme.onPrimary,
fontSize: 14,
),
),
const Gap(4),
InkWell(
onTap: () {
final newTags = List<String>.from(
state.tags.value,
)..remove(tag);
state.tags.value = newTags;
},
child: Icon(
Icons.close,
size: 16,
color:
Theme.of(
context,
).colorScheme.onPrimary,
),
),
],
),
);
}).toList(),
), ),
// Tag input with autocomplete // Tag input with autocomplete
TypeAheadField<SnPostTag>( TypeAheadField<SnPostTag>(
@@ -274,8 +251,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
}, },
); );
}, },
suggestionsCallback: suggestionsCallback: (pattern) =>
(pattern) => _fetchTagSuggestions(pattern, ref), _fetchTagSuggestions(pattern, ref),
itemBuilder: (context, suggestion) { itemBuilder: (context, suggestion) {
return ListTile( return ListTile(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@@ -314,55 +291,49 @@ class ComposeSettingsSheet extends HookConsumerWidget {
), ),
), ),
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)), hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
items: items: (postCategories.value ?? <SnPostCategory>[]).map((item) {
(postCategories.value ?? <SnPostCategory>[]).map((item) { return DropdownMenuItem(
return DropdownMenuItem( value: item,
value: item, enabled: false,
enabled: false, child: StatefulBuilder(
child: StatefulBuilder( builder: (context, menuSetState) {
builder: (context, menuSetState) { final isSelected = state.categories.value.contains(item);
final isSelected = state.categories.value.contains( return InkWell(
item, onTap: () {
); isSelected
return InkWell( ? state.categories.value = state.categories.value
onTap: () { .where((e) => e != item)
isSelected .toList()
? state.categories.value = : state.categories.value = [
state.categories.value ...state.categories.value,
.where((e) => e != item) item,
.toList() ];
: state.categories.value = [ menuSetState(() {});
...state.categories.value,
item,
];
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
item.categoryDisplayTitle,
style: const TextStyle(fontSize: 14),
),
),
],
),
),
);
}, },
), child: Container(
); height: double.infinity,
}).toList(), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
item.categoryDisplayTitle,
style: const TextStyle(fontSize: 14),
),
),
],
),
),
);
},
),
);
}).toList(),
value: currentCategories.isEmpty ? null : currentCategories.last, value: currentCategories.isEmpty ? null : currentCategories.last,
onChanged: (_) {}, onChanged: (_) {},
selectedItemBuilder: (context) { selectedItemBuilder: (context) {

View File

@@ -1,51 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'compose_settings_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(postCategories)
const postCategoriesProvider = PostCategoriesProvider._();
final class PostCategoriesProvider
extends
$FunctionalProvider<
AsyncValue<List<SnPostCategory>>,
List<SnPostCategory>,
FutureOr<List<SnPostCategory>>
>
with
$FutureModifier<List<SnPostCategory>>,
$FutureProvider<List<SnPostCategory>> {
const PostCategoriesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'postCategoriesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$postCategoriesHash();
@$internal
@override
$FutureProviderElement<List<SnPostCategory>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnPostCategory>> create(Ref ref) {
return postCategories(ref);
}
}
String _$postCategoriesHash() => r'8799c10eb91cf8c8c7ea72eff3475e1eaa7b9a2b';