♻️ Refactored post loading
This commit is contained in:
@@ -5,6 +5,7 @@ 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/post/post_list.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
@@ -82,17 +83,17 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final postCategory =
|
||||
isCategory ? ref.watch(postCategoryProvider(slug)) : null;
|
||||
final postCategory = isCategory
|
||||
? ref.watch(postCategoryProvider(slug))
|
||||
: null;
|
||||
final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
|
||||
final subscriptionStatus = ref.watch(
|
||||
postCategorySubscriptionStatusProvider(slug, isCategory),
|
||||
);
|
||||
|
||||
final postFilterTitle =
|
||||
isCategory
|
||||
? postCategory?.value?.categoryDisplayTitle ?? 'loading'
|
||||
: postTag?.value?.name ?? postTag?.value?.slug ?? 'loading';
|
||||
final postFilterTitle = isCategory
|
||||
? postCategory?.value?.categoryDisplayTitle ?? 'loading'
|
||||
: postTag?.value?.name ?? postTag?.value?.slug ?? 'loading';
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
@@ -108,63 +109,50 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
child: Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: postCategory!.when(
|
||||
data:
|
||||
(category) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
category.categoryDisplayTitle,
|
||||
).bold().fontSize(15),
|
||||
Text('A category'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data:
|
||||
(isSubscribed) =>
|
||||
isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _subscribeToCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error:
|
||||
(error, _) => Text(
|
||||
'Error loading subscription status',
|
||||
),
|
||||
loading:
|
||||
() =>
|
||||
CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
postCategoryProvider(slug),
|
||||
),
|
||||
data: (category) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
category.categoryDisplayTitle,
|
||||
).bold().fontSize(15),
|
||||
Text('A category'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data: (isSubscribed) => isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.remove_circle),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _subscribeToCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error: (error, _) =>
|
||||
Text('Error loading subscription status'),
|
||||
loading: () =>
|
||||
CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () =>
|
||||
ref.invalidate(postCategoryProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
),
|
||||
@@ -179,61 +167,49 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
child: Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: postTag!.when(
|
||||
data:
|
||||
(tag) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tag.name ?? '#${tag.slug}',
|
||||
).bold().fontSize(15),
|
||||
Text('A tag'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data:
|
||||
(isSubscribed) =>
|
||||
isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _subscribeToCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error:
|
||||
(error, _) => Text(
|
||||
'Error loading subscription status',
|
||||
),
|
||||
loading:
|
||||
() =>
|
||||
CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(postTagProvider(slug)),
|
||||
data: (tag) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tag.name ?? '#${tag.slug}',
|
||||
).bold().fontSize(15),
|
||||
Text('A tag'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data: (isSubscribed) => isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.remove_circle),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _subscribeToCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error: (error, _) =>
|
||||
Text('Error loading subscription status'),
|
||||
loading: () =>
|
||||
CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postTagProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
),
|
||||
@@ -242,8 +218,11 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
const SliverGap(4),
|
||||
SliverPostList(
|
||||
categories: isCategory ? [slug] : null,
|
||||
tags: isCategory ? null : [slug],
|
||||
query: PostListQuery(
|
||||
categories: isCategory ? [slug] : null,
|
||||
tags: isCategory ? null : [slug],
|
||||
),
|
||||
|
||||
maxWidth: 540 + 16,
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 8),
|
||||
|
||||
@@ -3,140 +3,18 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/posts/post_filter.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
final postSearchProvider = AsyncNotifierProvider.autoDispose(
|
||||
PostSearchNotifier.new,
|
||||
);
|
||||
|
||||
class PostSearchNotifier extends AsyncNotifier<List<SnPost>>
|
||||
with AsyncPaginationController<SnPost> {
|
||||
static const int _pageSize = 20;
|
||||
String _currentQuery = '';
|
||||
String? _pubName;
|
||||
String? _realm;
|
||||
int? _type;
|
||||
List<String>? _categories;
|
||||
List<String>? _tags;
|
||||
bool _shuffle = false;
|
||||
bool? _pinned;
|
||||
|
||||
@override
|
||||
FutureOr<List<SnPost>> build() async {
|
||||
// Initial state is empty if no query/filters, or fetch if needed
|
||||
// But original logic allowed initial empty state.
|
||||
// Let's replicate original logic: return empty list initially if no query.
|
||||
return [];
|
||||
}
|
||||
|
||||
bool? _includeReplies;
|
||||
bool _mediaOnly = false;
|
||||
String? _queryTerm;
|
||||
String? _order;
|
||||
bool _orderDesc = true;
|
||||
int? _periodStart;
|
||||
int? _periodEnd;
|
||||
|
||||
Future<void> search(
|
||||
String query, {
|
||||
String? pubName,
|
||||
String? realm,
|
||||
int? type,
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
bool shuffle = false,
|
||||
bool? pinned,
|
||||
bool? includeReplies,
|
||||
bool mediaOnly = false,
|
||||
String? queryTerm,
|
||||
String? order,
|
||||
bool orderDesc = true,
|
||||
int? periodStart,
|
||||
int? periodEnd,
|
||||
}) async {
|
||||
_currentQuery = query.trim();
|
||||
_pubName = pubName;
|
||||
_realm = realm;
|
||||
_type = type;
|
||||
_categories = categories;
|
||||
_tags = tags;
|
||||
_shuffle = shuffle;
|
||||
_pinned = pinned;
|
||||
_includeReplies = includeReplies;
|
||||
_mediaOnly = mediaOnly;
|
||||
_queryTerm = queryTerm;
|
||||
_order = order;
|
||||
_orderDesc = orderDesc;
|
||||
_periodStart = periodStart;
|
||||
_periodEnd = periodEnd;
|
||||
|
||||
final hasFilters =
|
||||
pubName != null ||
|
||||
realm != null ||
|
||||
type != null ||
|
||||
categories != null ||
|
||||
tags != null ||
|
||||
shuffle ||
|
||||
pinned != null ||
|
||||
includeReplies != null ||
|
||||
mediaOnly ||
|
||||
queryTerm != null ||
|
||||
order != null ||
|
||||
periodStart != null ||
|
||||
periodEnd != null;
|
||||
|
||||
if (_currentQuery.isEmpty && !hasFilters) {
|
||||
state = const AsyncData([]);
|
||||
totalCount = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SnPost>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts',
|
||||
queryParameters: {
|
||||
'query': _currentQuery,
|
||||
'offset': fetchedCount,
|
||||
'take': _pageSize,
|
||||
'vector': false,
|
||||
if (_pubName != null) 'pub': _pubName,
|
||||
if (_realm != null) 'realm': _realm,
|
||||
if (_type != null) 'type': _type,
|
||||
if (_tags != null) 'tags': _tags,
|
||||
if (_categories != null) 'categories': _categories,
|
||||
if (_shuffle) 'shuffle': true,
|
||||
if (_pinned != null) 'pinned': _pinned,
|
||||
if (_includeReplies != null) 'includeReplies': _includeReplies,
|
||||
if (_mediaOnly) 'mediaOnly': true,
|
||||
if (_queryTerm != null) 'queryTerm': _queryTerm,
|
||||
if (_order != null) 'order': _order,
|
||||
if (_orderDesc) 'orderDesc': true,
|
||||
if (_periodStart != null) 'periodStart': _periodStart,
|
||||
if (_periodEnd != null) 'periodEnd': _periodEnd,
|
||||
},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final data = response.data as List;
|
||||
return data.map((json) => SnPost.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
const kSearchPostListId = 'search';
|
||||
|
||||
class PostSearchScreen extends HookConsumerWidget {
|
||||
const PostSearchScreen({super.key});
|
||||
@@ -149,22 +27,14 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
final showFilters = useState(false);
|
||||
final pubNameController = useTextEditingController();
|
||||
final realmController = useTextEditingController();
|
||||
final typeValue = useState<int?>(null);
|
||||
final selectedCategories = useState<List<String>>([]);
|
||||
final selectedTags = useState<List<String>>([]);
|
||||
final shuffleValue = useState(false);
|
||||
final pinnedValue = useState<bool?>(null);
|
||||
|
||||
// State variables for PostFilterWidget
|
||||
final categoryTabController = useTabController(initialLength: 3);
|
||||
final includeReplies = useState<bool?>(null);
|
||||
final mediaOnly = useState(false);
|
||||
final queryTerm = useState<String?>(null);
|
||||
final order = useState<String?>('date');
|
||||
final orderDesc = useState(true);
|
||||
final periodStart = useState<int?>(null);
|
||||
final periodEnd = useState<int?>(null);
|
||||
final showAdvancedFilters = useState(false);
|
||||
|
||||
// Single query state
|
||||
final queryState = useState(const PostListQuery());
|
||||
|
||||
final noti = ref.read(postListProvider(kSearchPostListId).notifier);
|
||||
|
||||
useEffect(() {
|
||||
return () {
|
||||
@@ -175,77 +45,32 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
};
|
||||
}, []);
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
|
||||
void onSearchChanged(String query, {bool skipDebounce = false}) {
|
||||
queryState.value = queryState.value.copyWith(queryTerm: query);
|
||||
|
||||
if (skipDebounce) {
|
||||
noti.applyFilter(queryState.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
|
||||
debounceTimer.value = Timer(debounce, () {
|
||||
ref
|
||||
.read(postSearchProvider.notifier)
|
||||
.search(
|
||||
query,
|
||||
type: categoryTabController.index == 1
|
||||
? 0
|
||||
: (categoryTabController.index == 2 ? 1 : null),
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
orderDesc: orderDesc.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
);
|
||||
noti.applyFilter(queryState.value);
|
||||
});
|
||||
}
|
||||
|
||||
void onSearchWithFilters(String query) {
|
||||
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
|
||||
|
||||
debounceTimer.value = Timer(debounce, () {
|
||||
ref
|
||||
.read(postSearchProvider.notifier)
|
||||
.search(
|
||||
query,
|
||||
pubName: pubNameController.text.isNotEmpty
|
||||
? pubNameController.text
|
||||
: null,
|
||||
realm: realmController.text.isNotEmpty
|
||||
? realmController.text
|
||||
: null,
|
||||
type: categoryTabController.index == 1
|
||||
? 0
|
||||
: (categoryTabController.index == 2 ? 1 : null),
|
||||
categories: selectedCategories.value.isNotEmpty
|
||||
? selectedCategories.value
|
||||
: null,
|
||||
tags: selectedTags.value.isNotEmpty ? selectedTags.value : null,
|
||||
shuffle: shuffleValue.value,
|
||||
pinned: pinnedValue.value,
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
orderDesc: orderDesc.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void toggleFilters() {
|
||||
void toggleFilterDisplay() {
|
||||
showFilters.value = !showFilters.value;
|
||||
}
|
||||
|
||||
Widget buildFilterPanel() {
|
||||
return PostFilterWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
orderDesc: orderDesc,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
showAdvancedFilters: showAdvancedFilters,
|
||||
initialQuery: queryState.value,
|
||||
onQueryChanged: (newQuery) {
|
||||
queryState.value = newQuery;
|
||||
noti.applyFilter(newQuery);
|
||||
},
|
||||
hideSearch: true,
|
||||
);
|
||||
}
|
||||
@@ -272,7 +97,7 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
onChanged: onSearchChanged,
|
||||
onSubmitted: (value) {
|
||||
onSearchWithFilters(value);
|
||||
onSearchChanged(value, skipDebounce: true);
|
||||
},
|
||||
autofocus: true,
|
||||
),
|
||||
@@ -283,7 +108,7 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
? Icons.filter_alt
|
||||
: Icons.filter_alt_outlined,
|
||||
),
|
||||
onPressed: toggleFilters,
|
||||
onPressed: toggleFilterDisplay,
|
||||
tooltip: 'toggleFilters'.tr(),
|
||||
),
|
||||
],
|
||||
@@ -291,62 +116,76 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
body: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final searchState = ref.watch(postSearchProvider);
|
||||
final searchState = ref.watch(postListProvider(kSearchPostListId));
|
||||
|
||||
return isWideScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 4,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(16),
|
||||
SliverToBoxAdapter(
|
||||
child: SearchBar(
|
||||
elevation: WidgetStateProperty.all(4),
|
||||
controller: searchController,
|
||||
hintText: 'search'.tr(),
|
||||
leading: const Icon(Icons.search),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ExtendedRefreshIndicator(
|
||||
onRefresh: noti.refresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(16),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: SearchBar(
|
||||
elevation: WidgetStateProperty.all(4),
|
||||
controller: searchController,
|
||||
hintText: 'search'.tr(),
|
||||
leading: const Icon(Icons.search),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onChanged: onSearchChanged,
|
||||
onSubmitted: (value) {
|
||||
onSearchChanged(value, skipDebounce: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: onSearchChanged,
|
||||
onSubmitted: (value) {
|
||||
onSearchWithFilters(value);
|
||||
),
|
||||
const SliverGap(16),
|
||||
if (showFilters.value && !isWideScreen(context))
|
||||
SliverToBoxAdapter(child: buildFilterPanel()),
|
||||
// Use PaginationList with isSliver=true
|
||||
PaginationList(
|
||||
provider: postListProvider(kSearchPostListId),
|
||||
notifier: postListProvider(
|
||||
kSearchPostListId,
|
||||
).notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
itemBuilder: (context, index, post) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: PostActionableItem(
|
||||
item: post,
|
||||
borderRadius: 8,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SliverGap(16),
|
||||
if (showFilters.value && !isWideScreen(context))
|
||||
SliverToBoxAdapter(child: buildFilterPanel()),
|
||||
// Use PaginationList with isSliver=true
|
||||
PaginationList(
|
||||
provider: postSearchProvider,
|
||||
notifier: postSearchProvider.notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
itemBuilder: (context, index, post) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
if (searchState.value?.isEmpty == true &&
|
||||
searchController.text.isNotEmpty &&
|
||||
!searchState.isLoading)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text('noResultsFound'.tr()),
|
||||
),
|
||||
child: PostActionableItem(
|
||||
item: post,
|
||||
borderRadius: 8,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (searchState.value?.isEmpty == true &&
|
||||
searchController.text.isNotEmpty &&
|
||||
!searchState.isLoading)
|
||||
SliverFillRemaining(
|
||||
child: Center(child: Text('noResultsFound'.tr())),
|
||||
),
|
||||
SliverGap(
|
||||
MediaQuery.of(context).padding.bottom + 16,
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
).padding(left: 8),
|
||||
],
|
||||
).padding(left: 8),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
@@ -382,7 +221,7 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
Symbols.filter_alt,
|
||||
fill: showFilters.value ? 1 : null,
|
||||
),
|
||||
onPressed: toggleFilters,
|
||||
onPressed: toggleFilterDisplay,
|
||||
tooltip: 'toggleFilters'.tr(),
|
||||
),
|
||||
const Gap(4),
|
||||
@@ -412,8 +251,8 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
// Use PaginationList with isSliver=true
|
||||
PaginationList(
|
||||
provider: postSearchProvider,
|
||||
notifier: postSearchProvider.notifier,
|
||||
provider: postListProvider(kSearchPostListId),
|
||||
notifier: postListProvider(kSearchPostListId).notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
itemBuilder: (context, index, post) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:island/models/account.dart';
|
||||
import 'package:island/models/heatmap.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:island/services/color.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/account/account_name.dart';
|
||||
@@ -344,45 +345,6 @@ class _PublisherHeatmapWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||
final TabController categoryTabController;
|
||||
final ValueNotifier<bool?> includeReplies;
|
||||
final ValueNotifier<bool> mediaOnly;
|
||||
final ValueNotifier<String?> queryTerm;
|
||||
final ValueNotifier<String?> order;
|
||||
final ValueNotifier<bool> orderDesc;
|
||||
final ValueNotifier<int?> periodStart;
|
||||
final ValueNotifier<int?> periodEnd;
|
||||
final ValueNotifier<bool> showAdvancedFilters;
|
||||
|
||||
const _PublisherCategoryTabWidget({
|
||||
required this.categoryTabController,
|
||||
required this.includeReplies,
|
||||
required this.mediaOnly,
|
||||
required this.queryTerm,
|
||||
required this.order,
|
||||
required this.orderDesc,
|
||||
required this.periodStart,
|
||||
required this.periodEnd,
|
||||
required this.showAdvancedFilters,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PostFilterWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
orderDesc: orderDesc,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
showAdvancedFilters: showAdvancedFilters,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnPublisher> publisher(Ref ref, String uname) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
@@ -454,24 +416,22 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final categoryTabController = useTabController(initialLength: 3);
|
||||
final categoryTab = useState(0);
|
||||
categoryTabController.addListener(() {
|
||||
categoryTab.value = categoryTabController.index;
|
||||
});
|
||||
|
||||
final includeReplies = useState<bool?>(null);
|
||||
final mediaOnly = useState(false);
|
||||
final queryTerm = useState<String?>(null);
|
||||
final order = useState<String?>('date'); // 'popularity' or 'date'
|
||||
final orderDesc = useState(
|
||||
true,
|
||||
); // true for descending, false for ascending
|
||||
final periodStart = useState<int?>(null);
|
||||
final periodEnd = useState<int?>(null);
|
||||
final showAdvancedFilters = useState(false);
|
||||
final queryState = useState(PostListQuery(pubName: name));
|
||||
|
||||
final subscribing = useState(false);
|
||||
final isPinnedExpanded = useState(true);
|
||||
|
||||
useEffect(() {
|
||||
final index = switch (queryState.value.type) {
|
||||
0 => 1,
|
||||
1 => 2,
|
||||
_ => 0,
|
||||
};
|
||||
categoryTabController.index = index;
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
Future<void> subscribe() async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
subscribing.value = true;
|
||||
@@ -564,37 +524,22 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
),
|
||||
...[
|
||||
if (isPinnedExpanded.value)
|
||||
SliverPostList(pubName: name, pinned: true),
|
||||
SliverPostList(
|
||||
query: PostListQuery(pubName: name, pinned: true),
|
||||
queryKey: 'publisher-$name-pinned',
|
||||
),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: _PublisherCategoryTabWidget(
|
||||
child: PostFilterWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
orderDesc: orderDesc,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
showAdvancedFilters: showAdvancedFilters,
|
||||
initialQuery: queryState.value,
|
||||
onQueryChanged: (newQuery) =>
|
||||
queryState.value = newQuery,
|
||||
),
|
||||
),
|
||||
SliverPostList(
|
||||
key: ValueKey(
|
||||
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||
),
|
||||
pubName: name,
|
||||
pinned: false,
|
||||
type: categoryTab.value == 1
|
||||
? 0
|
||||
: (categoryTab.value == 2 ? 1 : null),
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
orderDesc: orderDesc.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
query: queryState.value,
|
||||
queryKey: 'publisher-$name',
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
@@ -704,37 +649,22 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
),
|
||||
...[
|
||||
if (isPinnedExpanded.value)
|
||||
SliverPostList(pubName: name, pinned: true),
|
||||
SliverPostList(
|
||||
query: PostListQuery(pubName: name, pinned: true),
|
||||
queryKey: 'publisher-$name-pinned',
|
||||
),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: _PublisherCategoryTabWidget(
|
||||
child: PostFilterWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
orderDesc: orderDesc,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
showAdvancedFilters: showAdvancedFilters,
|
||||
initialQuery: queryState.value,
|
||||
onQueryChanged: (newQuery) => queryState.value = newQuery,
|
||||
),
|
||||
),
|
||||
SliverPostList(
|
||||
key: ValueKey(
|
||||
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||
),
|
||||
pubName: name,
|
||||
pinned: false,
|
||||
type: categoryTab.value == 1
|
||||
? 0
|
||||
: (categoryTab.value == 2 ? 1 : null),
|
||||
includeReplies: includeReplies.value,
|
||||
mediaOnly: mediaOnly.value,
|
||||
queryTerm: queryTerm.value,
|
||||
order: order.value,
|
||||
orderDesc: orderDesc.value,
|
||||
periodStart: periodStart.value,
|
||||
periodEnd: periodEnd.value,
|
||||
key: ValueKey(queryState.value),
|
||||
query: queryState.value,
|
||||
queryKey: 'publisher-$name',
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user