✨ Post browse by categories, tags
This commit is contained in:
@@ -7,7 +7,7 @@ part of 'room_detail.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatMemberListNotifierHash() =>
|
||||
r'c8fbf4b95df6dae24b1ba21063e9a43351832974';
|
||||
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -148,7 +148,7 @@ class _StickerPackProviderElement
|
||||
}
|
||||
|
||||
String _$stickerPacksNotifierHash() =>
|
||||
r'0a8edcf9c35396c411f1214f5e77b1e8fac6a3e6';
|
||||
r'30024b35235f3085a5b1ec2204d0a974ee907e22';
|
||||
|
||||
abstract class _$StickerPacksNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {
|
||||
|
@@ -173,12 +173,48 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
),
|
||||
tooltip: 'webArticlesStand'.tr(),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushNamed('postSearch');
|
||||
},
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categories').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.label),
|
||||
const Gap(12),
|
||||
Text('tags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postTags');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.search),
|
||||
const Gap(12),
|
||||
Text('search').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postSearch');
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: Icon(
|
||||
Symbols.search,
|
||||
Symbols.action_key,
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
tooltip: 'search'.tr(),
|
||||
|
242
lib/screens/posts/post_categories_list.dart
Normal file
242
lib/screens/posts/post_categories_list.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
import 'dart:async';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.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/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
|
||||
// Post Categories Notifier
|
||||
final postCategoriesNotifierProvider = StateNotifierProvider.autoDispose<
|
||||
PostCategoriesNotifier,
|
||||
AsyncValue<CursorPagingData<SnPostCategory>>
|
||||
>((ref) {
|
||||
return PostCategoriesNotifier(ref);
|
||||
});
|
||||
|
||||
class PostCategoriesNotifier
|
||||
extends StateNotifier<AsyncValue<CursorPagingData<SnPostCategory>>> {
|
||||
final AutoDisposeRef ref;
|
||||
static const int _pageSize = 20;
|
||||
bool _isLoading = false;
|
||||
|
||||
PostCategoriesNotifier(this.ref) : super(const AsyncValue.loading()) {
|
||||
state = const AsyncValue.data(
|
||||
CursorPagingData(items: [], hasMore: false, nextCursor: null),
|
||||
);
|
||||
fetch(cursor: null);
|
||||
}
|
||||
|
||||
Future<void> fetch({String? cursor}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
if (cursor == null) {
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts/categories',
|
||||
queryParameters: {
|
||||
'offset': offset,
|
||||
'take': _pageSize,
|
||||
'order': 'usage',
|
||||
},
|
||||
);
|
||||
|
||||
final data = response.data as List;
|
||||
final categories =
|
||||
data.map((json) => SnPostCategory.fromJson(json)).toList();
|
||||
final hasMore = categories.length == _pageSize;
|
||||
final nextCursor =
|
||||
hasMore ? (offset + categories.length).toString() : null;
|
||||
|
||||
state = AsyncValue.data(
|
||||
CursorPagingData(
|
||||
items: [...(state.value?.items ?? []), ...categories],
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post Tags Notifier
|
||||
final postTagsNotifierProvider = StateNotifierProvider.autoDispose<
|
||||
PostTagsNotifier,
|
||||
AsyncValue<CursorPagingData<SnPostTag>>
|
||||
>((ref) {
|
||||
return PostTagsNotifier(ref);
|
||||
});
|
||||
|
||||
class PostTagsNotifier
|
||||
extends StateNotifier<AsyncValue<CursorPagingData<SnPostTag>>> {
|
||||
final AutoDisposeRef ref;
|
||||
static const int _pageSize = 20;
|
||||
bool _isLoading = false;
|
||||
|
||||
PostTagsNotifier(this.ref) : super(const AsyncValue.loading()) {
|
||||
state = const AsyncValue.data(
|
||||
CursorPagingData(items: [], hasMore: false, nextCursor: null),
|
||||
);
|
||||
fetch(cursor: null);
|
||||
}
|
||||
|
||||
Future<void> fetch({String? cursor}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
if (cursor == null) {
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts/tags',
|
||||
queryParameters: {
|
||||
'offset': offset,
|
||||
'take': _pageSize,
|
||||
'order': 'usage',
|
||||
},
|
||||
);
|
||||
|
||||
final data = response.data as List;
|
||||
final tags = data.map((json) => SnPostTag.fromJson(json)).toList();
|
||||
final hasMore = tags.length == _pageSize;
|
||||
final nextCursor = hasMore ? (offset + tags.length).toString() : null;
|
||||
|
||||
state = AsyncValue.data(
|
||||
CursorPagingData(
|
||||
items: [...(state.value?.items ?? []), ...tags],
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PostCategoriesListScreen extends ConsumerWidget {
|
||||
const PostCategoriesListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final categoriesState = ref.watch(postCategoriesNotifierProvider);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('Categories')),
|
||||
body: categoriesState.when(
|
||||
data: (data) {
|
||||
if (data.items.isEmpty) {
|
||||
return const Center(child: Text('No categories found'));
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.items.length + (data.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= data.items.length) {
|
||||
ref
|
||||
.read(postCategoriesNotifierProvider.notifier)
|
||||
.fetch(cursor: data.nextCursor);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final category = data.items[index];
|
||||
return ListTile(
|
||||
leading: const Icon(Symbols.category),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text(category.categoryDisplayTitle),
|
||||
subtitle: Text('postCount'.plural(category.usage)),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'postCategoryDetail',
|
||||
pathParameters: {'slug': category.slug},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postCategoriesNotifierProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostTagsListScreen extends ConsumerWidget {
|
||||
const PostTagsListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tagsState = ref.watch(postTagsNotifierProvider);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('Tags')),
|
||||
body: tagsState.when(
|
||||
data: (data) {
|
||||
if (data.items.isEmpty) {
|
||||
return const Center(child: Text('No tags found'));
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.items.length + (data.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= data.items.length) {
|
||||
ref
|
||||
.read(postTagsNotifierProvider.notifier)
|
||||
.fetch(cursor: data.nextCursor);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final tag = data.items[index];
|
||||
return ListTile(
|
||||
title: Text(tag.name ?? '#${tag.slug}'),
|
||||
contentPadding: 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},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postTagsNotifierProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -399,7 +399,7 @@ class _RealmChatRoomsProviderElement
|
||||
}
|
||||
|
||||
String _$realmMemberListNotifierHash() =>
|
||||
r'022bcef5a90cbae05ff23b937851afc3ef913d42';
|
||||
r'2f88f803b2e61e7287ed8a43025173e56ff6ca3b';
|
||||
|
||||
abstract class _$RealmMemberListNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> {
|
||||
|
Reference in New Issue
Block a user