From a39565f0129b0ca36c6f7439d9cfd4bb8e75a331 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 8 Aug 2025 23:20:22 +0800 Subject: [PATCH] :sparkles: Post category details --- lib/route.dart | 20 ++ lib/screens/posts/post_category_detail.dart | 107 +++++++ lib/screens/posts/post_category_detail.g.dart | 270 ++++++++++++++++++ lib/widgets/post/compose_settings_sheet.dart | 1 + lib/widgets/post/post_item.dart | 14 +- lib/widgets/post/post_list.dart | 36 ++- lib/widgets/post/post_list.g.dart | 110 +++++-- 7 files changed, 526 insertions(+), 32 deletions(-) create mode 100644 lib/screens/posts/post_category_detail.dart create mode 100644 lib/screens/posts/post_category_detail.g.dart diff --git a/lib/route.dart b/lib/route.dart index cc7f743..8222ea8 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -7,6 +7,7 @@ import 'package:island/screens/developers/edit_app.dart'; import 'package:island/screens/developers/new_app.dart'; import 'package:island/screens/developers/hub.dart'; import 'package:island/screens/discovery/articles.dart'; +import 'package:island/screens/posts/post_category_detail.dart'; import 'package:island/screens/posts/post_search.dart'; import 'package:island/widgets/app_wrapper.dart'; import 'package:island/screens/tabs.dart'; @@ -357,6 +358,25 @@ final routerProvider = Provider((ref) { return PostDetailScreen(id: id); }, ), + GoRoute( + name: 'postCategoryDetail', + path: '/posts/categories/:slug', + builder: (context, state) { + final slug = state.pathParameters['slug']!; + return PostCategoryDetailScreen(slug: slug, isCategory: true); + }, + ), + GoRoute( + name: 'postTagDetail', + path: '/posts/tags/:slug', + builder: (context, state) { + final slug = state.pathParameters['slug']!; + return PostCategoryDetailScreen( + slug: slug, + isCategory: false, + ); + }, + ), GoRoute( name: 'publisherProfile', path: '/publishers/:name', diff --git a/lib/screens/posts/post_category_detail.dart b/lib/screens/posts/post_category_detail.dart new file mode 100644 index 0000000..7100450 --- /dev/null +++ b/lib/screens/posts/post_category_detail.dart @@ -0,0 +1,107 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +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/widgets/app_scaffold.dart'; +import 'package:island/widgets/post/post_list.dart'; +import 'package:island/widgets/response.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:styled_widget/styled_widget.dart'; + +part 'post_category_detail.g.dart'; + +@riverpod +Future postCategory(Ref ref, String slug) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get('/sphere/posts/categories/$slug'); + return SnPostCategory.fromJson(resp.data); +} + +@riverpod +Future postTag(Ref ref, String slug) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get('/sphere/posts/tags/$slug'); + return SnPostTag.fromJson(resp.data); +} + +class PostCategoryDetailScreen extends HookConsumerWidget { + final String slug; + final bool isCategory; + const PostCategoryDetailScreen({ + super.key, + required this.slug, + required this.isCategory, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final postCategory = + isCategory ? ref.watch(postCategoryProvider(slug)) : null; + final postTag = isCategory ? null : ref.watch(postTagProvider(slug)); + + final postFilterTitle = + isCategory + ? postCategory?.value?.categoryDisplayTitle ?? 'loading' + : postTag?.value?.name ?? postTag?.value?.slug ?? 'loading'; + + return AppScaffold( + isNoBackground: false, + appBar: AppBar(title: Text(postFilterTitle).tr()), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isCategory) + postCategory!.when( + data: + (category) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(category.categoryDisplayTitle).bold().fontSize(15), + Text('A category'), + ], + ).padding(horizontal: 24, vertical: 16), + error: + (error, _) => ResponseErrorWidget( + error: error, + onRetry: () => ref.invalidate(postCategoryProvider(slug)), + ), + loading: () => ResponseLoadingWidget(), + ) + else + postTag!.when( + data: + (tag) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(tag.name ?? '#${tag.slug}').bold().fontSize(15), + Text('A tag'), + ], + ).padding(horizontal: 24, vertical: 16), + error: + (error, _) => ResponseErrorWidget( + error: error, + onRetry: () => ref.invalidate(postTagProvider(slug)), + ), + loading: () => ResponseLoadingWidget(), + ), + const Divider(height: 1), + Expanded( + child: CustomScrollView( + slivers: [ + const SliverGap(4), + SliverPostList( + categories: isCategory ? [slug] : null, + tags: isCategory ? null : [slug], + ), + SliverGap(MediaQuery.of(context).padding.bottom + 8), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/posts/post_category_detail.g.dart b/lib/screens/posts/post_category_detail.g.dart new file mode 100644 index 0000000..156b962 --- /dev/null +++ b/lib/screens/posts/post_category_detail.g.dart @@ -0,0 +1,270 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'post_category_detail.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$postCategoryHash() => r'0df2de729ba96819ee37377314615abef0c99547'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [postCategory]. +@ProviderFor(postCategory) +const postCategoryProvider = PostCategoryFamily(); + +/// See also [postCategory]. +class PostCategoryFamily extends Family> { + /// See also [postCategory]. + const PostCategoryFamily(); + + /// See also [postCategory]. + PostCategoryProvider call(String slug) { + return PostCategoryProvider(slug); + } + + @override + PostCategoryProvider getProviderOverride( + covariant PostCategoryProvider provider, + ) { + return call(provider.slug); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'postCategoryProvider'; +} + +/// See also [postCategory]. +class PostCategoryProvider extends AutoDisposeFutureProvider { + /// See also [postCategory]. + PostCategoryProvider(String slug) + : this._internal( + (ref) => postCategory(ref as PostCategoryRef, slug), + from: postCategoryProvider, + name: r'postCategoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$postCategoryHash, + dependencies: PostCategoryFamily._dependencies, + allTransitiveDependencies: + PostCategoryFamily._allTransitiveDependencies, + slug: slug, + ); + + PostCategoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.slug, + }) : super.internal(); + + final String slug; + + @override + Override overrideWith( + FutureOr Function(PostCategoryRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: PostCategoryProvider._internal( + (ref) => create(ref as PostCategoryRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + slug: slug, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _PostCategoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PostCategoryProvider && other.slug == slug; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, slug.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin PostCategoryRef on AutoDisposeFutureProviderRef { + /// The parameter `slug` of this provider. + String get slug; +} + +class _PostCategoryProviderElement + extends AutoDisposeFutureProviderElement + with PostCategoryRef { + _PostCategoryProviderElement(super.provider); + + @override + String get slug => (origin as PostCategoryProvider).slug; +} + +String _$postTagHash() => r'e050fdf9af81a843a9abd9cf979dd2672e0a2b93'; + +/// See also [postTag]. +@ProviderFor(postTag) +const postTagProvider = PostTagFamily(); + +/// See also [postTag]. +class PostTagFamily extends Family> { + /// See also [postTag]. + const PostTagFamily(); + + /// See also [postTag]. + PostTagProvider call(String slug) { + return PostTagProvider(slug); + } + + @override + PostTagProvider getProviderOverride(covariant PostTagProvider provider) { + return call(provider.slug); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'postTagProvider'; +} + +/// See also [postTag]. +class PostTagProvider extends AutoDisposeFutureProvider { + /// See also [postTag]. + PostTagProvider(String slug) + : this._internal( + (ref) => postTag(ref as PostTagRef, slug), + from: postTagProvider, + name: r'postTagProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$postTagHash, + dependencies: PostTagFamily._dependencies, + allTransitiveDependencies: PostTagFamily._allTransitiveDependencies, + slug: slug, + ); + + PostTagProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.slug, + }) : super.internal(); + + final String slug; + + @override + Override overrideWith( + FutureOr Function(PostTagRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: PostTagProvider._internal( + (ref) => create(ref as PostTagRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + slug: slug, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _PostTagProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PostTagProvider && other.slug == slug; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, slug.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin PostTagRef on AutoDisposeFutureProviderRef { + /// The parameter `slug` of this provider. + String get slug; +} + +class _PostTagProviderElement + extends AutoDisposeFutureProviderElement + with PostTagRef { + _PostTagProviderElement(super.provider); + + @override + String get slug => (origin as PostTagProvider).slug; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/compose_settings_sheet.dart b/lib/widgets/post/compose_settings_sheet.dart index 192ce6a..9609e64 100644 --- a/lib/widgets/post/compose_settings_sheet.dart +++ b/lib/widgets/post/compose_settings_sheet.dart @@ -216,6 +216,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { return SheetScaffold( titleText: 'postSettings'.tr(), + heightFactor: 0.6, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index bbc6537..d105a85 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -587,7 +587,12 @@ class PostItem extends HookConsumerWidget { in isFullPost ? item.tags : item.tags.take(3)) InkWell( child: Text('#${tag.name ?? tag.slug}'), - onTap: () {}, + onTap: () { + GoRouter.of(context).pushNamed( + 'postTagDetail', + pathParameters: {'slug': tag.slug}, + ); + }, ), if (!isFullPost && item.tags.length > 3) Text('+${item.tags.length - 3}').opacity(0.6), @@ -605,7 +610,12 @@ class PostItem extends HookConsumerWidget { : item.categories.take(2)) InkWell( child: Text(category.categoryDisplayTitle), - onTap: () {}, + onTap: () { + GoRouter.of(context).pushNamed( + 'postCategoryDetail', + pathParameters: {'slug': category.slug}, + ); + }, ), if (!isFullPost && item.categories.length > 2) Text('+${item.categories.length - 2}').opacity(0.6), diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index eea3b54..a632686 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -15,7 +15,12 @@ class PostListNotifier extends _$PostListNotifier static const int _pageSize = 20; @override - Future> build(String? pubName, int? type) { + Future> build( + String? pubName, { + int? type, + List? categories, + List? tags, + }) { return fetch(cursor: null); } @@ -29,6 +34,8 @@ class PostListNotifier extends _$PostListNotifier 'take': _pageSize, if (pubName != null) 'pub': pubName, if (type != null) 'type': type, + if (tags != null) 'tags': tags, + if (categories != null) 'categories': categories, }; final response = await client.get( @@ -62,6 +69,8 @@ enum PostItemType { class SliverPostList extends HookConsumerWidget { final String? pubName; final int? type; + final List? categories; + final List? tags; final PostItemType itemType; final Color? backgroundColor; final EdgeInsets? padding; @@ -73,6 +82,8 @@ class SliverPostList extends HookConsumerWidget { super.key, this.pubName, this.type, + this.categories, + this.tags, this.itemType = PostItemType.regular, this.backgroundColor, this.padding, @@ -84,9 +95,26 @@ class SliverPostList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return PagingHelperSliverView( - provider: postListNotifierProvider(pubName, type), - futureRefreshable: postListNotifierProvider(pubName, type).future, - notifierRefreshable: postListNotifierProvider(pubName, type).notifier, + provider: postListNotifierProvider( + pubName, + type: type, + categories: categories, + tags: tags, + ), + futureRefreshable: + postListNotifierProvider( + pubName, + type: type, + categories: categories, + tags: tags, + ).future, + notifierRefreshable: + postListNotifierProvider( + pubName, + type: type, + categories: categories, + tags: tags, + ).notifier, contentBuilder: (data, widgetCount, endItemView) => SliverList.builder( itemCount: widgetCount, diff --git a/lib/widgets/post/post_list.g.dart b/lib/widgets/post/post_list.g.dart index 58dc750..01ec70d 100644 --- a/lib/widgets/post/post_list.g.dart +++ b/lib/widgets/post/post_list.g.dart @@ -6,7 +6,7 @@ part of 'post_list.dart'; // RiverpodGenerator // ************************************************************************** -String _$postListNotifierHash() => r'78222d62957f85713d17aecd95af0305b764e86c'; +String _$postListNotifierHash() => r'dc57fc6aaff6bfb4e9b4d1185984162b099b8773'; /// Copied from Dart SDK class _SystemHash { @@ -33,8 +33,15 @@ abstract class _$PostListNotifier extends BuildlessAutoDisposeAsyncNotifier> { late final String? pubName; late final int? type; + late final List? categories; + late final List? tags; - FutureOr> build(String? pubName, int? type); + FutureOr> build( + String? pubName, { + int? type, + List? categories, + List? tags, + }); } /// See also [PostListNotifier]. @@ -48,15 +55,30 @@ class PostListNotifierFamily const PostListNotifierFamily(); /// See also [PostListNotifier]. - PostListNotifierProvider call(String? pubName, int? type) { - return PostListNotifierProvider(pubName, type); + PostListNotifierProvider call( + String? pubName, { + int? type, + List? categories, + List? tags, + }) { + return PostListNotifierProvider( + pubName, + type: type, + categories: categories, + tags: tags, + ); } @override PostListNotifierProvider getProviderOverride( covariant PostListNotifierProvider provider, ) { - return call(provider.pubName, provider.type); + return call( + provider.pubName, + type: provider.type, + categories: provider.categories, + tags: provider.tags, + ); } static const Iterable? _dependencies = null; @@ -82,24 +104,32 @@ class PostListNotifierProvider CursorPagingData > { /// See also [PostListNotifier]. - PostListNotifierProvider(String? pubName, int? type) - : this._internal( - () => - PostListNotifier() - ..pubName = pubName - ..type = type, - from: postListNotifierProvider, - name: r'postListNotifierProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$postListNotifierHash, - dependencies: PostListNotifierFamily._dependencies, - allTransitiveDependencies: - PostListNotifierFamily._allTransitiveDependencies, - pubName: pubName, - type: type, - ); + PostListNotifierProvider( + String? pubName, { + int? type, + List? categories, + List? tags, + }) : this._internal( + () => + PostListNotifier() + ..pubName = pubName + ..type = type + ..categories = categories + ..tags = tags, + from: postListNotifierProvider, + name: r'postListNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$postListNotifierHash, + dependencies: PostListNotifierFamily._dependencies, + allTransitiveDependencies: + PostListNotifierFamily._allTransitiveDependencies, + pubName: pubName, + type: type, + categories: categories, + tags: tags, + ); PostListNotifierProvider._internal( super._createNotifier, { @@ -110,16 +140,25 @@ class PostListNotifierProvider required super.from, required this.pubName, required this.type, + required this.categories, + required this.tags, }) : super.internal(); final String? pubName; final int? type; + final List? categories; + final List? tags; @override FutureOr> runNotifierBuild( covariant PostListNotifier notifier, ) { - return notifier.build(pubName, type); + return notifier.build( + pubName, + type: type, + categories: categories, + tags: tags, + ); } @override @@ -130,7 +169,9 @@ class PostListNotifierProvider () => create() ..pubName = pubName - ..type = type, + ..type = type + ..categories = categories + ..tags = tags, from: from, name: null, dependencies: null, @@ -138,6 +179,8 @@ class PostListNotifierProvider debugGetCreateSourceHash: null, pubName: pubName, type: type, + categories: categories, + tags: tags, ), ); } @@ -155,7 +198,9 @@ class PostListNotifierProvider bool operator ==(Object other) { return other is PostListNotifierProvider && other.pubName == pubName && - other.type == type; + other.type == type && + other.categories == categories && + other.tags == tags; } @override @@ -163,6 +208,8 @@ class PostListNotifierProvider var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, pubName.hashCode); hash = _SystemHash.combine(hash, type.hashCode); + hash = _SystemHash.combine(hash, categories.hashCode); + hash = _SystemHash.combine(hash, tags.hashCode); return _SystemHash.finish(hash); } @@ -177,6 +224,12 @@ mixin PostListNotifierRef /// The parameter `type` of this provider. int? get type; + + /// The parameter `categories` of this provider. + List? get categories; + + /// The parameter `tags` of this provider. + List? get tags; } class _PostListNotifierProviderElement @@ -192,6 +245,11 @@ class _PostListNotifierProviderElement String? get pubName => (origin as PostListNotifierProvider).pubName; @override int? get type => (origin as PostListNotifierProvider).type; + @override + List? get categories => + (origin as PostListNotifierProvider).categories; + @override + List? get tags => (origin as PostListNotifierProvider).tags; } // ignore_for_file: type=lint