Post category details

This commit is contained in:
2025-08-08 23:20:22 +08:00
parent aa9755e6a7
commit a39565f012
7 changed files with 526 additions and 32 deletions

View File

@@ -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/new_app.dart';
import 'package:island/screens/developers/hub.dart'; import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/discovery/articles.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/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart'; import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart'; import 'package:island/screens/tabs.dart';
@@ -357,6 +358,25 @@ final routerProvider = Provider<GoRouter>((ref) {
return PostDetailScreen(id: id); 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( GoRoute(
name: 'publisherProfile', name: 'publisherProfile',
path: '/publishers/:name', path: '/publishers/:name',

View File

@@ -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<SnPostCategory> 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<SnPostTag> 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),
],
),
),
],
),
);
}
}

View File

@@ -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<AsyncValue<SnPostCategory>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'postCategoryProvider';
}
/// See also [postCategory].
class PostCategoryProvider extends AutoDisposeFutureProvider<SnPostCategory> {
/// 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<SnPostCategory> 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<SnPostCategory> 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<SnPostCategory> {
/// The parameter `slug` of this provider.
String get slug;
}
class _PostCategoryProviderElement
extends AutoDisposeFutureProviderElement<SnPostCategory>
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<AsyncValue<SnPostTag>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'postTagProvider';
}
/// See also [postTag].
class PostTagProvider extends AutoDisposeFutureProvider<SnPostTag> {
/// 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<SnPostTag> 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<SnPostTag> 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<SnPostTag> {
/// The parameter `slug` of this provider.
String get slug;
}
class _PostTagProviderElement
extends AutoDisposeFutureProviderElement<SnPostTag>
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

View File

@@ -216,6 +216,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: 'postSettings'.tr(), titleText: 'postSettings'.tr(),
heightFactor: 0.6,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(

View File

@@ -587,7 +587,12 @@ class PostItem extends HookConsumerWidget {
in isFullPost ? item.tags : item.tags.take(3)) in isFullPost ? item.tags : item.tags.take(3))
InkWell( InkWell(
child: Text('#${tag.name ?? tag.slug}'), child: Text('#${tag.name ?? tag.slug}'),
onTap: () {}, onTap: () {
GoRouter.of(context).pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
},
), ),
if (!isFullPost && item.tags.length > 3) if (!isFullPost && item.tags.length > 3)
Text('+${item.tags.length - 3}').opacity(0.6), Text('+${item.tags.length - 3}').opacity(0.6),
@@ -605,7 +610,12 @@ class PostItem extends HookConsumerWidget {
: item.categories.take(2)) : item.categories.take(2))
InkWell( InkWell(
child: Text(category.categoryDisplayTitle), child: Text(category.categoryDisplayTitle),
onTap: () {}, onTap: () {
GoRouter.of(context).pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
},
), ),
if (!isFullPost && item.categories.length > 2) if (!isFullPost && item.categories.length > 2)
Text('+${item.categories.length - 2}').opacity(0.6), Text('+${item.categories.length - 2}').opacity(0.6),

View File

@@ -15,7 +15,12 @@ class PostListNotifier extends _$PostListNotifier
static const int _pageSize = 20; static const int _pageSize = 20;
@override @override
Future<CursorPagingData<SnPost>> build(String? pubName, int? type) { Future<CursorPagingData<SnPost>> build(
String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
}) {
return fetch(cursor: null); return fetch(cursor: null);
} }
@@ -29,6 +34,8 @@ class PostListNotifier extends _$PostListNotifier
'take': _pageSize, 'take': _pageSize,
if (pubName != null) 'pub': pubName, if (pubName != null) 'pub': pubName,
if (type != null) 'type': type, if (type != null) 'type': type,
if (tags != null) 'tags': tags,
if (categories != null) 'categories': categories,
}; };
final response = await client.get( final response = await client.get(
@@ -62,6 +69,8 @@ enum PostItemType {
class SliverPostList extends HookConsumerWidget { class SliverPostList extends HookConsumerWidget {
final String? pubName; final String? pubName;
final int? type; final int? type;
final List<String>? categories;
final List<String>? tags;
final PostItemType itemType; final PostItemType itemType;
final Color? backgroundColor; final Color? backgroundColor;
final EdgeInsets? padding; final EdgeInsets? padding;
@@ -73,6 +82,8 @@ class SliverPostList extends HookConsumerWidget {
super.key, super.key,
this.pubName, this.pubName,
this.type, this.type,
this.categories,
this.tags,
this.itemType = PostItemType.regular, this.itemType = PostItemType.regular,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
@@ -84,9 +95,26 @@ class SliverPostList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView( return PagingHelperSliverView(
provider: postListNotifierProvider(pubName, type), provider: postListNotifierProvider(
futureRefreshable: postListNotifierProvider(pubName, type).future, pubName,
notifierRefreshable: postListNotifierProvider(pubName, type).notifier, 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: contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder( (data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount, itemCount: widgetCount,

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$postListNotifierHash() => r'78222d62957f85713d17aecd95af0305b764e86c'; String _$postListNotifierHash() => r'dc57fc6aaff6bfb4e9b4d1185984162b099b8773';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -33,8 +33,15 @@ abstract class _$PostListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> { extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
late final String? pubName; late final String? pubName;
late final int? type; late final int? type;
late final List<String>? categories;
late final List<String>? tags;
FutureOr<CursorPagingData<SnPost>> build(String? pubName, int? type); FutureOr<CursorPagingData<SnPost>> build(
String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
});
} }
/// See also [PostListNotifier]. /// See also [PostListNotifier].
@@ -48,15 +55,30 @@ class PostListNotifierFamily
const PostListNotifierFamily(); const PostListNotifierFamily();
/// See also [PostListNotifier]. /// See also [PostListNotifier].
PostListNotifierProvider call(String? pubName, int? type) { PostListNotifierProvider call(
return PostListNotifierProvider(pubName, type); String? pubName, {
int? type,
List<String>? categories,
List<String>? tags,
}) {
return PostListNotifierProvider(
pubName,
type: type,
categories: categories,
tags: tags,
);
} }
@override @override
PostListNotifierProvider getProviderOverride( PostListNotifierProvider getProviderOverride(
covariant PostListNotifierProvider provider, 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<ProviderOrFamily>? _dependencies = null; static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -82,24 +104,32 @@ class PostListNotifierProvider
CursorPagingData<SnPost> CursorPagingData<SnPost>
> { > {
/// See also [PostListNotifier]. /// See also [PostListNotifier].
PostListNotifierProvider(String? pubName, int? type) PostListNotifierProvider(
: this._internal( String? pubName, {
() => int? type,
PostListNotifier() List<String>? categories,
..pubName = pubName List<String>? tags,
..type = type, }) : this._internal(
from: postListNotifierProvider, () =>
name: r'postListNotifierProvider', PostListNotifier()
debugGetCreateSourceHash: ..pubName = pubName
const bool.fromEnvironment('dart.vm.product') ..type = type
? null ..categories = categories
: _$postListNotifierHash, ..tags = tags,
dependencies: PostListNotifierFamily._dependencies, from: postListNotifierProvider,
allTransitiveDependencies: name: r'postListNotifierProvider',
PostListNotifierFamily._allTransitiveDependencies, debugGetCreateSourceHash:
pubName: pubName, const bool.fromEnvironment('dart.vm.product')
type: type, ? null
); : _$postListNotifierHash,
dependencies: PostListNotifierFamily._dependencies,
allTransitiveDependencies:
PostListNotifierFamily._allTransitiveDependencies,
pubName: pubName,
type: type,
categories: categories,
tags: tags,
);
PostListNotifierProvider._internal( PostListNotifierProvider._internal(
super._createNotifier, { super._createNotifier, {
@@ -110,16 +140,25 @@ class PostListNotifierProvider
required super.from, required super.from,
required this.pubName, required this.pubName,
required this.type, required this.type,
required this.categories,
required this.tags,
}) : super.internal(); }) : super.internal();
final String? pubName; final String? pubName;
final int? type; final int? type;
final List<String>? categories;
final List<String>? tags;
@override @override
FutureOr<CursorPagingData<SnPost>> runNotifierBuild( FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
covariant PostListNotifier notifier, covariant PostListNotifier notifier,
) { ) {
return notifier.build(pubName, type); return notifier.build(
pubName,
type: type,
categories: categories,
tags: tags,
);
} }
@override @override
@@ -130,7 +169,9 @@ class PostListNotifierProvider
() => () =>
create() create()
..pubName = pubName ..pubName = pubName
..type = type, ..type = type
..categories = categories
..tags = tags,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
@@ -138,6 +179,8 @@ class PostListNotifierProvider
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
pubName: pubName, pubName: pubName,
type: type, type: type,
categories: categories,
tags: tags,
), ),
); );
} }
@@ -155,7 +198,9 @@ class PostListNotifierProvider
bool operator ==(Object other) { bool operator ==(Object other) {
return other is PostListNotifierProvider && return other is PostListNotifierProvider &&
other.pubName == pubName && other.pubName == pubName &&
other.type == type; other.type == type &&
other.categories == categories &&
other.tags == tags;
} }
@override @override
@@ -163,6 +208,8 @@ class PostListNotifierProvider
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode); hash = _SystemHash.combine(hash, pubName.hashCode);
hash = _SystemHash.combine(hash, type.hashCode); hash = _SystemHash.combine(hash, type.hashCode);
hash = _SystemHash.combine(hash, categories.hashCode);
hash = _SystemHash.combine(hash, tags.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@@ -177,6 +224,12 @@ mixin PostListNotifierRef
/// The parameter `type` of this provider. /// The parameter `type` of this provider.
int? get type; int? get type;
/// The parameter `categories` of this provider.
List<String>? get categories;
/// The parameter `tags` of this provider.
List<String>? get tags;
} }
class _PostListNotifierProviderElement class _PostListNotifierProviderElement
@@ -192,6 +245,11 @@ class _PostListNotifierProviderElement
String? get pubName => (origin as PostListNotifierProvider).pubName; String? get pubName => (origin as PostListNotifierProvider).pubName;
@override @override
int? get type => (origin as PostListNotifierProvider).type; int? get type => (origin as PostListNotifierProvider).type;
@override
List<String>? get categories =>
(origin as PostListNotifierProvider).categories;
@override
List<String>? get tags => (origin as PostListNotifierProvider).tags;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint