♻️ Refactored post loading
This commit is contained in:
@@ -7,6 +7,7 @@ abstract class PaginationController<T> {
|
||||
int get fetchedCount;
|
||||
|
||||
bool get fetchedAll;
|
||||
bool get isLoading;
|
||||
|
||||
FutureOr<List<T>> fetch();
|
||||
|
||||
@@ -32,11 +33,15 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
|
||||
@override
|
||||
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
|
||||
|
||||
@override
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
FutureOr<List<T>> build() async => fetch();
|
||||
|
||||
@override
|
||||
Future<void> refresh() async {
|
||||
isLoading = true;
|
||||
totalCount = null;
|
||||
state = AsyncData<List<T>>([]);
|
||||
|
||||
@@ -44,12 +49,14 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
|
||||
return await fetch();
|
||||
});
|
||||
state = newState;
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> fetchFurther() async {
|
||||
if (fetchedAll) return;
|
||||
|
||||
isLoading = true;
|
||||
state = AsyncLoading<List<T>>();
|
||||
|
||||
final newState = await AsyncValue.guard<List<T>>(() async {
|
||||
@@ -58,6 +65,7 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
|
||||
});
|
||||
|
||||
state = newState;
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +75,7 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
|
||||
Future<void> applyFilter(F filter) async {
|
||||
if (currentFilter == filter) return;
|
||||
// Reset the data
|
||||
isLoading = true;
|
||||
totalCount = null;
|
||||
state = AsyncData<List<T>>([]);
|
||||
currentFilter = filter;
|
||||
@@ -75,5 +84,6 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
|
||||
return await fetch();
|
||||
});
|
||||
state = newState;
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,22 @@ sealed class PostListQuery with _$PostListQuery {
|
||||
}) = _PostListQuery;
|
||||
}
|
||||
|
||||
final postListNotifierProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<PostListNotifier, List<SnPost>, PostListQuery>(
|
||||
final postListProvider = AsyncNotifierProvider.autoDispose.family(
|
||||
PostListNotifier.new,
|
||||
);
|
||||
);
|
||||
|
||||
class PostListNotifier extends AsyncNotifier<List<SnPost>>
|
||||
with AsyncPaginationController<SnPost> {
|
||||
final PostListQuery arg;
|
||||
PostListNotifier(this.arg);
|
||||
|
||||
with
|
||||
AsyncPaginationController<SnPost>,
|
||||
AsyncPaginationFilter<PostListQuery, SnPost> {
|
||||
static const int pageSize = 20;
|
||||
|
||||
final String? id;
|
||||
PostListNotifier(this.id);
|
||||
|
||||
@override
|
||||
PostListQuery currentFilter = PostListQuery();
|
||||
|
||||
@override
|
||||
Future<List<SnPost>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
@@ -45,20 +49,22 @@ class PostListNotifier extends AsyncNotifier<List<SnPost>>
|
||||
final queryParams = {
|
||||
'offset': fetchedCount,
|
||||
'take': pageSize,
|
||||
'replies': arg.includeReplies,
|
||||
'orderDesc': arg.orderDesc,
|
||||
if (arg.shuffle) 'shuffle': arg.shuffle,
|
||||
if (arg.pubName != null) 'pub': arg.pubName,
|
||||
if (arg.realm != null) 'realm': arg.realm,
|
||||
if (arg.type != null) 'type': arg.type,
|
||||
if (arg.tags != null) 'tags': arg.tags,
|
||||
if (arg.categories != null) 'categories': arg.categories,
|
||||
if (arg.pinned != null) 'pinned': arg.pinned,
|
||||
if (arg.order != null) 'order': arg.order,
|
||||
if (arg.periodStart != null) 'periodStart': arg.periodStart,
|
||||
if (arg.periodEnd != null) 'periodEnd': arg.periodEnd,
|
||||
if (arg.queryTerm != null) 'query': arg.queryTerm,
|
||||
if (arg.mediaOnly != null) 'media': arg.mediaOnly,
|
||||
'replies': currentFilter.includeReplies,
|
||||
'orderDesc': currentFilter.orderDesc,
|
||||
if (currentFilter.shuffle) 'shuffle': currentFilter.shuffle,
|
||||
if (currentFilter.pubName != null) 'pub': currentFilter.pubName,
|
||||
if (currentFilter.realm != null) 'realm': currentFilter.realm,
|
||||
if (currentFilter.type != null) 'type': currentFilter.type,
|
||||
if (currentFilter.tags != null) 'tags': currentFilter.tags,
|
||||
if (currentFilter.categories != null)
|
||||
'categories': currentFilter.categories,
|
||||
if (currentFilter.pinned != null) 'pinned': currentFilter.pinned,
|
||||
if (currentFilter.order != null) 'order': currentFilter.order,
|
||||
if (currentFilter.periodStart != null)
|
||||
'periodStart': currentFilter.periodStart,
|
||||
if (currentFilter.periodEnd != null) 'periodEnd': currentFilter.periodEnd,
|
||||
if (currentFilter.queryTerm != null) 'query': currentFilter.queryTerm,
|
||||
if (currentFilter.mediaOnly != null) 'media': currentFilter.mediaOnly,
|
||||
};
|
||||
|
||||
final response = await client.get(
|
||||
|
||||
@@ -2,6 +2,7 @@ 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/pods/post/post_list.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
|
||||
@@ -20,7 +21,7 @@ class CreatorPostListScreen extends HookConsumerWidget {
|
||||
key: ValueKey(refreshKey.value),
|
||||
slivers: [
|
||||
SliverPostList(
|
||||
pubName: pubName,
|
||||
query: PostListQuery(pubName: pubName),
|
||||
itemType: PostItemType.creator,
|
||||
maxWidth: 640,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
|
||||
@@ -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,15 +83,15 @@ 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
|
||||
final postFilterTitle = isCategory
|
||||
? postCategory?.value?.categoryDisplayTitle ?? 'loading'
|
||||
: postTag?.value?.name ?? postTag?.value?.slug ?? 'loading';
|
||||
|
||||
@@ -108,8 +109,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
child: Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: postCategory!.when(
|
||||
data:
|
||||
(category) => Column(
|
||||
data: (category) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
@@ -118,9 +118,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
Text('A category'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data:
|
||||
(isSubscribed) =>
|
||||
isSubscribed
|
||||
data: (isSubscribed) => isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
@@ -129,9 +127,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
icon: const Icon(Symbols.remove_circle),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
@@ -142,28 +138,20 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
),
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error:
|
||||
(error, _) => Text(
|
||||
'Error loading subscription status',
|
||||
),
|
||||
loading:
|
||||
() =>
|
||||
error: (error, _) =>
|
||||
Text('Error loading subscription status'),
|
||||
loading: () =>
|
||||
CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
postCategoryProvider(slug),
|
||||
),
|
||||
onRetry: () =>
|
||||
ref.invalidate(postCategoryProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
@@ -179,8 +167,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
child: Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: postTag!.when(
|
||||
data:
|
||||
(tag) => Column(
|
||||
data: (tag) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
@@ -189,9 +176,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
Text('A tag'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data:
|
||||
(isSubscribed) =>
|
||||
isSubscribed
|
||||
data: (isSubscribed) => isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
@@ -200,9 +185,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
icon: const Icon(Symbols.remove_circle),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
@@ -213,26 +196,19 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
),
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error:
|
||||
(error, _) => Text(
|
||||
'Error loading subscription status',
|
||||
),
|
||||
loading:
|
||||
() =>
|
||||
error: (error, _) =>
|
||||
Text('Error loading subscription status'),
|
||||
loading: () =>
|
||||
CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(postTagProvider(slug)),
|
||||
onRetry: () => ref.invalidate(postTagProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
@@ -242,8 +218,11 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
const SliverGap(4),
|
||||
SliverPostList(
|
||||
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,17 +116,23 @@ 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: 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,
|
||||
@@ -312,17 +143,20 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
onChanged: onSearchChanged,
|
||||
onSubmitted: (value) {
|
||||
onSearchWithFilters(value);
|
||||
onSearchChanged(value, skipDebounce: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(16),
|
||||
if (showFilters.value && !isWideScreen(context))
|
||||
SliverToBoxAdapter(child: buildFilterPanel()),
|
||||
// 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) {
|
||||
@@ -342,12 +176,17 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
searchController.text.isNotEmpty &&
|
||||
!searchState.isLoading)
|
||||
SliverFillRemaining(
|
||||
child: Center(child: Text('noResultsFound'.tr())),
|
||||
child: Center(
|
||||
child: Text('noResultsFound'.tr()),
|
||||
),
|
||||
),
|
||||
SliverGap(
|
||||
MediaQuery.of(context).padding.bottom + 16,
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
).padding(left: 8),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Align(
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:island/screens/chat/chat.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
@@ -171,11 +172,9 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar:
|
||||
isWideScreen(context)
|
||||
appBar: isWideScreen(context)
|
||||
? realmState.when(
|
||||
data:
|
||||
(realm) => AppBar(
|
||||
data: (realm) => AppBar(
|
||||
foregroundColor: appbarColor.value,
|
||||
leading: PageBackButton(
|
||||
color: appbarColor.value,
|
||||
@@ -184,14 +183,10 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
flexibleSpace: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child:
|
||||
realm!.background?.id != null
|
||||
? CloudImageWidget(
|
||||
fileId: realm.background!.id,
|
||||
)
|
||||
child: realm!.background?.id != null
|
||||
? CloudImageWidget(fileId: realm.background!.id)
|
||||
: Container(
|
||||
color:
|
||||
Theme.of(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).appBarTheme.backgroundColor,
|
||||
),
|
||||
@@ -202,9 +197,7 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
style: TextStyle(
|
||||
color:
|
||||
appbarColor.value ??
|
||||
Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor,
|
||||
Theme.of(context).appBarTheme.foregroundColor,
|
||||
shadows: [iconShadow],
|
||||
),
|
||||
),
|
||||
@@ -219,16 +212,12 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) =>
|
||||
builder: (context) =>
|
||||
_RealmMemberListSheet(realmSlug: slug),
|
||||
);
|
||||
},
|
||||
),
|
||||
_RealmActionMenu(
|
||||
realmSlug: slug,
|
||||
iconShadow: iconShadow,
|
||||
),
|
||||
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
@@ -239,17 +228,19 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
body: realmState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('Error: $error')),
|
||||
data:
|
||||
(realm) =>
|
||||
isWideScreen(context)
|
||||
data: (realm) => isWideScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPostList(realm: slug, pinned: true),
|
||||
SliverPostList(realm: slug, pinned: false),
|
||||
SliverPostList(
|
||||
query: PostListQuery(realm: slug, pinned: true),
|
||||
),
|
||||
SliverPostList(
|
||||
query: PostListQuery(realm: slug, pinned: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -260,14 +251,11 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
realmIdentity.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data:
|
||||
(identity) => Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
data: (identity) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
realmDescriptionWidget(realm!),
|
||||
if (identity == null &&
|
||||
realm.isCommunity)
|
||||
if (identity == null && realm.isCommunity)
|
||||
realmActionWidget(realm)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
@@ -293,14 +281,10 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
flexibleSpace: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child:
|
||||
realm!.background?.id != null
|
||||
? CloudImageWidget(
|
||||
fileId: realm.background!.id,
|
||||
)
|
||||
child: realm!.background?.id != null
|
||||
? CloudImageWidget(fileId: realm.background!.id)
|
||||
: Container(
|
||||
color:
|
||||
Theme.of(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).appBarTheme.backgroundColor,
|
||||
),
|
||||
@@ -311,9 +295,7 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
style: TextStyle(
|
||||
color:
|
||||
appbarColor.value ??
|
||||
Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor,
|
||||
Theme.of(context).appBarTheme.foregroundColor,
|
||||
shadows: [iconShadow],
|
||||
),
|
||||
),
|
||||
@@ -329,17 +311,12 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _RealmMemberListSheet(
|
||||
realmSlug: slug,
|
||||
),
|
||||
builder: (context) =>
|
||||
_RealmMemberListSheet(realmSlug: slug),
|
||||
);
|
||||
},
|
||||
),
|
||||
_RealmActionMenu(
|
||||
realmSlug: slug,
|
||||
iconShadow: iconShadow,
|
||||
),
|
||||
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
@@ -348,10 +325,8 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
child: realmIdentity.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data:
|
||||
(identity) => Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
data: (identity) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
realmDescriptionWidget(realm),
|
||||
if (identity == null && realm.isCommunity)
|
||||
@@ -362,11 +337,13 @@ class RealmDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: realmChatRoomListWidget(realm),
|
||||
SliverToBoxAdapter(child: realmChatRoomListWidget(realm)),
|
||||
SliverPostList(
|
||||
query: PostListQuery(realm: slug, pinned: true),
|
||||
),
|
||||
SliverPostList(
|
||||
query: PostListQuery(realm: slug, pinned: false),
|
||||
),
|
||||
SliverPostList(realm: slug, pinned: true),
|
||||
SliverPostList(realm: slug, pinned: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -391,8 +368,7 @@ class _RealmActionMenu extends HookConsumerWidget {
|
||||
|
||||
return PopupMenuButton(
|
||||
icon: Icon(Icons.more_vert, shadows: [iconShadow]),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
itemBuilder: (context) => [
|
||||
if (isModerator)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
@@ -413,9 +389,7 @@ class _RealmActionMenu extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
realmIdentity.when(
|
||||
data:
|
||||
(identity) =>
|
||||
(identity?.role ?? 0) >= 100
|
||||
data: (identity) => (identity?.role ?? 0) >= 100
|
||||
? PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -478,13 +452,11 @@ class _RealmActionMenu extends HookConsumerWidget {
|
||||
});
|
||||
},
|
||||
),
|
||||
loading:
|
||||
() => const PopupMenuItem(
|
||||
loading: () => const PopupMenuItem(
|
||||
enabled: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(_, _) => PopupMenuItem(
|
||||
error: (_, _) => PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
@@ -494,22 +466,17 @@ class _RealmActionMenu extends HookConsumerWidget {
|
||||
const Gap(12),
|
||||
Text(
|
||||
'leaveRealm',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showConfirmAlert(
|
||||
'leaveRealmHint'.tr(),
|
||||
'leaveRealm'.tr(),
|
||||
).then((confirm) async {
|
||||
showConfirmAlert('leaveRealmHint'.tr(), 'leaveRealm'.tr()).then((
|
||||
confirm,
|
||||
) async {
|
||||
if (confirm) {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.delete(
|
||||
'/pass/realms/$realmSlug/members/me',
|
||||
);
|
||||
await client.delete('/pass/realms/$realmSlug/members/me');
|
||||
ref.invalidate(realmsJoinedProvider);
|
||||
if (context.mounted) {
|
||||
context.pop(true);
|
||||
@@ -684,8 +651,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _RealmMemberRoleSheet(
|
||||
builder: (context) => _RealmMemberRoleSheet(
|
||||
realmSlug: realmSlug,
|
||||
member: member,
|
||||
),
|
||||
@@ -809,12 +775,8 @@ class _RealmMemberRoleSheet extends HookConsumerWidget {
|
||||
onSelected: (int selection) {
|
||||
roleController.text = selection.toString();
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
fieldViewBuilder:
|
||||
(context, controller, focusNode, onFieldSubmitted) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
|
||||
@@ -39,7 +39,7 @@ class PaginationList<T> extends HookConsumerWidget {
|
||||
final data = ref.watch(provider);
|
||||
final noti = ref.watch(notifier);
|
||||
|
||||
if (data.isLoading && data.value?.isEmpty == true) {
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = ResponseLoadingWidget();
|
||||
return isSliver ? SliverFillRemaining(child: content) : content;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
|
||||
final data = ref.watch(provider);
|
||||
final noti = ref.watch(notifier);
|
||||
|
||||
if (data.isLoading && data.value?.isEmpty == true) {
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = ResponseLoadingWidget();
|
||||
return isSliver ? SliverFillRemaining(child: content) : content;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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/post/post_list.dart';
|
||||
@@ -17,21 +18,7 @@ enum PostItemType {
|
||||
}
|
||||
|
||||
class SliverPostList extends HookConsumerWidget {
|
||||
final String? pubName;
|
||||
final String? realm;
|
||||
final int? type;
|
||||
final List<String>? categories;
|
||||
final List<String>? tags;
|
||||
final bool shuffle;
|
||||
final bool? pinned;
|
||||
final bool? includeReplies;
|
||||
final bool? mediaOnly;
|
||||
final String? queryTerm;
|
||||
// Can be "populaurity", other value will be treated as "date"
|
||||
final String? order;
|
||||
final int? periodStart;
|
||||
final int? periodEnd;
|
||||
final bool? orderDesc;
|
||||
final PostListQuery? query;
|
||||
final PostItemType itemType;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
@@ -39,23 +26,11 @@ class SliverPostList extends HookConsumerWidget {
|
||||
final Function? onRefresh;
|
||||
final Function(SnPost)? onUpdate;
|
||||
final double? maxWidth;
|
||||
final String? queryKey;
|
||||
|
||||
const SliverPostList({
|
||||
super.key,
|
||||
this.pubName,
|
||||
this.realm,
|
||||
this.type,
|
||||
this.categories,
|
||||
this.tags,
|
||||
this.shuffle = false,
|
||||
this.pinned,
|
||||
this.includeReplies,
|
||||
this.mediaOnly,
|
||||
this.queryTerm,
|
||||
this.order,
|
||||
this.orderDesc = true,
|
||||
this.periodStart,
|
||||
this.periodEnd,
|
||||
this.query,
|
||||
this.itemType = PostItemType.regular,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
@@ -63,29 +38,19 @@ class SliverPostList extends HookConsumerWidget {
|
||||
this.onRefresh,
|
||||
this.onUpdate,
|
||||
this.maxWidth,
|
||||
this.queryKey,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final params = PostListQuery(
|
||||
pubName: pubName,
|
||||
realm: realm,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
shuffle: shuffle,
|
||||
pinned: pinned,
|
||||
includeReplies: includeReplies,
|
||||
mediaOnly: mediaOnly,
|
||||
queryTerm: queryTerm,
|
||||
order: order,
|
||||
periodStart: periodStart,
|
||||
periodEnd: periodEnd,
|
||||
orderDesc: orderDesc ?? true,
|
||||
);
|
||||
final provider = postListNotifierProvider(params);
|
||||
final provider = postListProvider(queryKey);
|
||||
final notifier = provider.notifier;
|
||||
|
||||
useEffect(() {
|
||||
ref.read(notifier).applyFilter(query!);
|
||||
return null;
|
||||
}, [query]);
|
||||
|
||||
return PaginationList(
|
||||
provider: provider,
|
||||
notifier: notifier,
|
||||
|
||||
@@ -3,26 +3,29 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
const kShufflePostListId = 'shuffle';
|
||||
|
||||
class PostShuffleScreen extends HookConsumerWidget {
|
||||
const PostShuffleScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const params = PostListQuery(shuffle: true);
|
||||
final postListState = ref.watch(postListNotifierProvider(params));
|
||||
const query = PostListQuery(shuffle: true);
|
||||
final postListState = ref.watch(postListProvider(kShufflePostListId));
|
||||
final postListNotifier = ref.watch(
|
||||
postListNotifierProvider(params).notifier,
|
||||
postListProvider(kShufflePostListId).notifier,
|
||||
);
|
||||
|
||||
final cardSwiperController = useMemoized(() => CardSwiperController(), []);
|
||||
|
||||
useEffect(() {
|
||||
postListNotifier.applyFilter(query);
|
||||
return cardSwiperController.dispose;
|
||||
}, []);
|
||||
|
||||
@@ -46,7 +49,8 @@ class PostShuffleScreen extends HookConsumerWidget {
|
||||
controller: cardSwiperController,
|
||||
cardsCount: items.length,
|
||||
isLoop: false,
|
||||
cardBuilder: (
|
||||
cardBuilder:
|
||||
(
|
||||
context,
|
||||
index,
|
||||
horizontalOffsetPercentage,
|
||||
@@ -62,7 +66,9 @@ class PostShuffleScreen extends HookConsumerWidget {
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: PostActionableItem(item: items[index]),
|
||||
child: PostActionableItem(
|
||||
item: items[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -91,8 +97,7 @@ class PostShuffleScreen extends HookConsumerWidget {
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
height: kBottomControlHeight,
|
||||
child:
|
||||
Row(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
|
||||
@@ -1,34 +1,89 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class PostFilterWidget extends StatelessWidget {
|
||||
class PostFilterWidget extends StatefulWidget {
|
||||
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;
|
||||
final PostListQuery initialQuery;
|
||||
final ValueChanged<PostListQuery> onQueryChanged;
|
||||
final bool hideSearch;
|
||||
|
||||
const PostFilterWidget({
|
||||
super.key,
|
||||
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,
|
||||
required this.initialQuery,
|
||||
required this.onQueryChanged,
|
||||
this.hideSearch = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostFilterWidget> createState() => _PostFilterWidgetState();
|
||||
}
|
||||
|
||||
class _PostFilterWidgetState extends State<PostFilterWidget> {
|
||||
late bool? _includeReplies;
|
||||
late bool _mediaOnly;
|
||||
late String? _queryTerm;
|
||||
late String? _order;
|
||||
late bool _orderDesc;
|
||||
late int? _periodStart;
|
||||
late int? _periodEnd;
|
||||
late int? _type;
|
||||
late bool _showAdvancedFilters;
|
||||
late TextEditingController _searchController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_includeReplies = widget.initialQuery.includeReplies;
|
||||
_mediaOnly = widget.initialQuery.mediaOnly ?? false;
|
||||
_queryTerm = widget.initialQuery.queryTerm;
|
||||
_order = widget.initialQuery.order;
|
||||
_orderDesc = widget.initialQuery.orderDesc;
|
||||
_periodStart = widget.initialQuery.periodStart;
|
||||
_periodEnd = widget.initialQuery.periodEnd;
|
||||
_type = widget.initialQuery.type;
|
||||
_showAdvancedFilters = false;
|
||||
_searchController = TextEditingController(text: _queryTerm);
|
||||
|
||||
widget.categoryTabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.categoryTabController.removeListener(_onTabChanged);
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
final tabIndex = widget.categoryTabController.index;
|
||||
setState(() {
|
||||
_type = switch (tabIndex) {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
_ => null,
|
||||
};
|
||||
});
|
||||
_updateQuery();
|
||||
}
|
||||
|
||||
void _updateQuery() {
|
||||
final newQuery = widget.initialQuery.copyWith(
|
||||
includeReplies: _includeReplies,
|
||||
mediaOnly: _mediaOnly,
|
||||
queryTerm: _queryTerm,
|
||||
order: _order,
|
||||
periodStart: _periodStart,
|
||||
periodEnd: _periodEnd,
|
||||
orderDesc: _orderDesc,
|
||||
type: _type,
|
||||
);
|
||||
widget.onQueryChanged(newQuery);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
@@ -36,7 +91,7 @@ class PostFilterWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: categoryTabController,
|
||||
controller: widget.categoryTabController,
|
||||
dividerColor: Colors.transparent,
|
||||
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
tabs: [
|
||||
@@ -53,17 +108,20 @@ class PostFilterWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('reply'.tr()),
|
||||
value: includeReplies.value,
|
||||
value: _includeReplies,
|
||||
tristate: true,
|
||||
onChanged: (value) {
|
||||
// Cycle through: null -> false -> true -> null
|
||||
if (includeReplies.value == null) {
|
||||
includeReplies.value = false;
|
||||
} else if (includeReplies.value == false) {
|
||||
includeReplies.value = true;
|
||||
setState(() {
|
||||
if (_includeReplies == null) {
|
||||
_includeReplies = false;
|
||||
} else if (_includeReplies == false) {
|
||||
_includeReplies = true;
|
||||
} else {
|
||||
includeReplies.value = null;
|
||||
_includeReplies = null;
|
||||
}
|
||||
});
|
||||
_updateQuery();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -73,11 +131,14 @@ class PostFilterWidget extends StatelessWidget {
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('attachments'.tr()),
|
||||
value: mediaOnly.value,
|
||||
value: _mediaOnly,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value != null) {
|
||||
mediaOnly.value = value;
|
||||
_mediaOnly = value;
|
||||
}
|
||||
});
|
||||
_updateQuery();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -88,11 +149,14 @@ class PostFilterWidget extends StatelessWidget {
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('descendingOrder'.tr()),
|
||||
value: orderDesc.value,
|
||||
value: _orderDesc,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value != null) {
|
||||
orderDesc.value = value;
|
||||
_orderDesc = value;
|
||||
}
|
||||
});
|
||||
_updateQuery();
|
||||
},
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -109,23 +173,24 @@ class PostFilterWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.all(const Radius.circular(8)),
|
||||
),
|
||||
trailing: Icon(
|
||||
showAdvancedFilters.value
|
||||
? Symbols.expand_less
|
||||
: Symbols.expand_more,
|
||||
_showAdvancedFilters ? Symbols.expand_less : Symbols.expand_more,
|
||||
),
|
||||
onTap: () {
|
||||
showAdvancedFilters.value = !showAdvancedFilters.value;
|
||||
setState(() {
|
||||
_showAdvancedFilters = !_showAdvancedFilters;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (showAdvancedFilters.value) ...[
|
||||
if (_showAdvancedFilters) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!hideSearch)
|
||||
if (!widget.hideSearch)
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'search'.tr(),
|
||||
hintText: 'searchPosts'.tr(),
|
||||
@@ -139,10 +204,13 @@ class PostFilterWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
queryTerm.value = value.isEmpty ? null : value;
|
||||
setState(() {
|
||||
_queryTerm = value.isEmpty ? null : value;
|
||||
});
|
||||
_updateQuery();
|
||||
},
|
||||
),
|
||||
if (!hideSearch) const Gap(12),
|
||||
if (!widget.hideSearch) const Gap(12),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'sortBy'.tr(),
|
||||
@@ -154,7 +222,7 @@ class PostFilterWidget extends StatelessWidget {
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
value: order.value,
|
||||
value: _order,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
|
||||
DropdownMenuItem(
|
||||
@@ -163,7 +231,10 @@ class PostFilterWidget extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
order.value = value;
|
||||
setState(() {
|
||||
_order = value;
|
||||
});
|
||||
_updateQuery();
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
@@ -174,9 +245,9 @@ class PostFilterWidget extends StatelessWidget {
|
||||
onTap: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: periodStart.value != null
|
||||
initialDate: _periodStart != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodStart.value! * 1000,
|
||||
_periodStart! * 1000,
|
||||
)
|
||||
: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
@@ -185,8 +256,11 @@ class PostFilterWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
periodStart.value =
|
||||
setState(() {
|
||||
_periodStart =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
});
|
||||
_updateQuery();
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
@@ -204,9 +278,9 @@ class PostFilterWidget extends StatelessWidget {
|
||||
suffixIcon: const Icon(Symbols.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
periodStart.value != null
|
||||
_periodStart != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodStart.value! * 1000,
|
||||
_periodStart! * 1000,
|
||||
).toString().split(' ')[0]
|
||||
: 'selectDate'.tr(),
|
||||
),
|
||||
@@ -219,9 +293,9 @@ class PostFilterWidget extends StatelessWidget {
|
||||
onTap: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: periodEnd.value != null
|
||||
initialDate: _periodEnd != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodEnd.value! * 1000,
|
||||
_periodEnd! * 1000,
|
||||
)
|
||||
: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
@@ -230,8 +304,11 @@ class PostFilterWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
periodEnd.value =
|
||||
setState(() {
|
||||
_periodEnd =
|
||||
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||
});
|
||||
_updateQuery();
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
@@ -249,9 +326,9 @@ class PostFilterWidget extends StatelessWidget {
|
||||
suffixIcon: const Icon(Symbols.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
periodEnd.value != null
|
||||
_periodEnd != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
periodEnd.value! * 1000,
|
||||
_periodEnd! * 1000,
|
||||
).toString().split(' ')[0]
|
||||
: 'selectDate'.tr(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user