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