diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index d1622e15..94d7314f 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1310,5 +1310,12 @@ "presenceTypeMusic": "Listening to Music", "presenceTypeWorkout": "Working out", "articleCompose": "Compose Article", - "backToHub": "Back to Hub" + "backToHub": "Back to Hub", + "advancedFilters": "Advanced Filters", + "searchPosts": "Search Posts", + "sortBy": "Sort by", + "fromDate": "From Date", + "toDate": "To Date", + "popularity": "Popularity", + "descendingOrder": "Descending Order" } diff --git a/lib/screens/posts/publisher_profile.dart b/lib/screens/posts/publisher_profile.dart index bb7b35d7..11c9778c 100644 --- a/lib/screens/posts/publisher_profile.dart +++ b/lib/screens/posts/publisher_profile.dart @@ -326,21 +326,233 @@ class _PublisherHeatmapWidget extends StatelessWidget { class _PublisherCategoryTabWidget extends StatelessWidget { final TabController categoryTabController; + final ValueNotifier includeReplies; + final ValueNotifier mediaOnly; + final ValueNotifier queryTerm; + final ValueNotifier order; + final ValueNotifier orderDesc; + final ValueNotifier periodStart; + final ValueNotifier periodEnd; + final ValueNotifier showAdvancedFilters; - const _PublisherCategoryTabWidget({required this.categoryTabController}); + 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 Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: TabBar( - controller: categoryTabController, - dividerColor: Colors.transparent, - splashBorderRadius: const BorderRadius.all(Radius.circular(8)), - tabs: [ - Tab(text: 'all'.tr()), - Tab(text: 'postTypePost'.tr()), - Tab(text: 'postArticle'.tr()), + child: Column( + children: [ + TabBar( + controller: categoryTabController, + dividerColor: Colors.transparent, + splashBorderRadius: const BorderRadius.all(Radius.circular(8)), + tabs: [ + Tab(text: 'all'.tr()), + Tab(text: 'postTypePost'.tr()), + Tab(text: 'postArticle'.tr()), + ], + ), + const Divider(height: 1), + Column( + children: [ + Row( + children: [ + Expanded( + child: CheckboxListTile( + title: Text('reply'.tr()), + value: includeReplies.value, + 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; + } else { + includeReplies.value = null; + } + }, + dense: true, + controlAffinity: ListTileControlAffinity.leading, + secondary: const Icon(Symbols.reply), + ), + ), + Expanded( + child: CheckboxListTile( + title: Text('attachments'.tr()), + value: mediaOnly.value, + onChanged: (value) { + if (value != null) { + mediaOnly.value = value; + } + }, + dense: true, + controlAffinity: ListTileControlAffinity.leading, + secondary: const Icon(Symbols.attachment), + ), + ), + ], + ), + CheckboxListTile( + title: Text('descendingOrder'.tr()), + value: orderDesc.value, + onChanged: (value) { + if (value != null) { + orderDesc.value = value; + } + }, + dense: true, + controlAffinity: ListTileControlAffinity.leading, + secondary: const Icon(Symbols.sort), + ), + ], + ), + const Divider(height: 1), + ListTile( + title: Text('advancedFilters'.tr()), + leading: const Icon(Symbols.filter_list), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(const Radius.circular(8)), + ), + trailing: Icon( + showAdvancedFilters.value + ? Symbols.expand_less + : Symbols.expand_more, + ), + onTap: () { + showAdvancedFilters.value = !showAdvancedFilters.value; + }, + ), + if (showAdvancedFilters.value) ...[ + const Divider(height: 1), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + decoration: InputDecoration( + labelText: 'search'.tr(), + hintText: 'searchPosts'.tr(), + prefixIcon: const Icon(Symbols.search), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + onChanged: (value) { + queryTerm.value = value.isEmpty ? null : value; + }, + ), + const Gap(12), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'sortBy'.tr(), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + value: order.value, + items: [ + DropdownMenuItem(value: 'date', child: Text('date'.tr())), + DropdownMenuItem( + value: 'popularity', + child: Text('popularity'.tr()), + ), + ], + onChanged: (value) { + order.value = value; + }, + ), + const Gap(12), + Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: 'fromDate'.tr(), + hintText: 'YYYY-MM-DD', + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + onChanged: (value) { + if (value.isEmpty) { + periodStart.value = null; + } else { + try { + final date = DateTime.parse(value); + periodStart.value = + date.millisecondsSinceEpoch ~/ 1000; + } catch (_) { + periodStart.value = null; + } + } + }, + ), + ), + const Gap(8), + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: 'toDate'.tr(), + hintText: 'YYYY-MM-DD', + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + ), + onChanged: (value) { + if (value.isEmpty) { + periodEnd.value = null; + } else { + try { + final date = DateTime.parse(value); + periodEnd.value = + date.millisecondsSinceEpoch ~/ 1000; + } catch (_) { + periodEnd.value = null; + } + } + }, + ), + ), + ], + ), + ], + ), + ), + ], ], ), ); @@ -423,6 +635,16 @@ class PublisherProfileScreen extends HookConsumerWidget { categoryTab.value = categoryTabController.index; }); + final includeReplies = useState(null); + final mediaOnly = useState(false); + final queryTerm = useState(null); + final order = useState('date'); // 'popularity' or 'date' + final orderDesc = useState( + true, + ); // true for descending, false for ascending + final periodStart = useState(null); + final periodEnd = useState(null); + final showAdvancedFilters = useState(false); final subscribing = useState(false); Future subscribe() async { @@ -498,17 +720,33 @@ class PublisherProfileScreen extends HookConsumerWidget { SliverToBoxAdapter( child: _PublisherCategoryTabWidget( categoryTabController: categoryTabController, + includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + orderDesc: orderDesc, + periodStart: periodStart, + periodEnd: periodEnd, + showAdvancedFilters: showAdvancedFilters, ), ), SliverPostList( - key: ValueKey(categoryTab.value), + key: ValueKey( + '${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}', + ), pubName: name, pinned: false, - type: switch (categoryTab.value) { - 1 => 0, - 2 => 1, - _ => null, - }, + 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, @@ -621,17 +859,33 @@ class PublisherProfileScreen extends HookConsumerWidget { SliverToBoxAdapter( child: _PublisherCategoryTabWidget( categoryTabController: categoryTabController, + includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + orderDesc: orderDesc, + periodStart: periodStart, + periodEnd: periodEnd, + showAdvancedFilters: showAdvancedFilters, ), ), SliverPostList( - key: ValueKey(categoryTab.value), + key: ValueKey( + '${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}', + ), pubName: name, pinned: false, - type: switch (categoryTab.value) { - 1 => 0, - 2 => 1, - _ => null, - }, + 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), ], diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index d85674f5..6f24af87 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -24,6 +24,12 @@ class PostListNotifier extends _$PostListNotifier bool? pinned, bool shuffle = false, bool? includeReplies, + bool? mediaOnly, + String? queryTerm, + String? order, + int? periodStart, + int? periodEnd, + bool orderDesc = true, }) { return fetch(cursor: null); } @@ -36,14 +42,20 @@ class PostListNotifier extends _$PostListNotifier final queryParams = { 'offset': offset, 'take': _pageSize, + 'replies': includeReplies, + 'orderDesc': orderDesc, + if (shuffle) 'shuffle': shuffle, 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 (order != null) 'order': order, + if (periodStart != null) 'periodStart': periodStart, + if (periodEnd != null) 'periodEnd': periodEnd, + if (queryTerm != null) 'query': queryTerm, + if (mediaOnly != null) 'media': mediaOnly, }; final response = await client.get( @@ -82,6 +94,14 @@ class SliverPostList extends HookConsumerWidget { final List? 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 Color? backgroundColor; final EdgeInsets? padding; @@ -99,6 +119,13 @@ class SliverPostList extends HookConsumerWidget { 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.backgroundColor, this.padding, @@ -118,6 +145,13 @@ class SliverPostList extends HookConsumerWidget { tags: tags, shuffle: shuffle, pinned: pinned, + includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + periodStart: periodStart, + periodEnd: periodEnd, + orderDesc: orderDesc ?? true, ); return PagingHelperSliverView( provider: provider, diff --git a/lib/widgets/post/post_list.g.dart b/lib/widgets/post/post_list.g.dart index 262da72d..1e641aa7 100644 --- a/lib/widgets/post/post_list.g.dart +++ b/lib/widgets/post/post_list.g.dart @@ -6,7 +6,7 @@ part of 'post_list.dart'; // RiverpodGenerator // ************************************************************************** -String _$postListNotifierHash() => r'fc139ad4df0deb67bcbb949560319f2f7fbfb503'; +String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97'; /// Copied from Dart SDK class _SystemHash { @@ -39,6 +39,12 @@ abstract class _$PostListNotifier late final bool? pinned; late final bool shuffle; late final bool? includeReplies; + late final bool? mediaOnly; + late final String? queryTerm; + late final String? order; + late final int? periodStart; + late final int? periodEnd; + late final bool orderDesc; FutureOr> build({ String? pubName, @@ -49,6 +55,12 @@ abstract class _$PostListNotifier bool? pinned, bool shuffle = false, bool? includeReplies, + bool? mediaOnly, + String? queryTerm, + String? order, + int? periodStart, + int? periodEnd, + bool orderDesc = true, }); } @@ -72,6 +84,12 @@ class PostListNotifierFamily bool? pinned, bool shuffle = false, bool? includeReplies, + bool? mediaOnly, + String? queryTerm, + String? order, + int? periodStart, + int? periodEnd, + bool orderDesc = true, }) { return PostListNotifierProvider( pubName: pubName, @@ -82,6 +100,12 @@ class PostListNotifierFamily pinned: pinned, shuffle: shuffle, includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + periodStart: periodStart, + periodEnd: periodEnd, + orderDesc: orderDesc, ); } @@ -98,6 +122,12 @@ class PostListNotifierFamily pinned: provider.pinned, shuffle: provider.shuffle, includeReplies: provider.includeReplies, + mediaOnly: provider.mediaOnly, + queryTerm: provider.queryTerm, + order: provider.order, + periodStart: provider.periodStart, + periodEnd: provider.periodEnd, + orderDesc: provider.orderDesc, ); } @@ -133,6 +163,12 @@ class PostListNotifierProvider bool? pinned, bool shuffle = false, bool? includeReplies, + bool? mediaOnly, + String? queryTerm, + String? order, + int? periodStart, + int? periodEnd, + bool orderDesc = true, }) : this._internal( () => PostListNotifier() @@ -143,7 +179,13 @@ class PostListNotifierProvider ..tags = tags ..pinned = pinned ..shuffle = shuffle - ..includeReplies = includeReplies, + ..includeReplies = includeReplies + ..mediaOnly = mediaOnly + ..queryTerm = queryTerm + ..order = order + ..periodStart = periodStart + ..periodEnd = periodEnd + ..orderDesc = orderDesc, from: postListNotifierProvider, name: r'postListNotifierProvider', debugGetCreateSourceHash: @@ -161,6 +203,12 @@ class PostListNotifierProvider pinned: pinned, shuffle: shuffle, includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + periodStart: periodStart, + periodEnd: periodEnd, + orderDesc: orderDesc, ); PostListNotifierProvider._internal( @@ -178,6 +226,12 @@ class PostListNotifierProvider required this.pinned, required this.shuffle, required this.includeReplies, + required this.mediaOnly, + required this.queryTerm, + required this.order, + required this.periodStart, + required this.periodEnd, + required this.orderDesc, }) : super.internal(); final String? pubName; @@ -188,6 +242,12 @@ class PostListNotifierProvider final bool? pinned; final bool shuffle; final bool? includeReplies; + final bool? mediaOnly; + final String? queryTerm; + final String? order; + final int? periodStart; + final int? periodEnd; + final bool orderDesc; @override FutureOr> runNotifierBuild( @@ -202,6 +262,12 @@ class PostListNotifierProvider pinned: pinned, shuffle: shuffle, includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + periodStart: periodStart, + periodEnd: periodEnd, + orderDesc: orderDesc, ); } @@ -219,7 +285,13 @@ class PostListNotifierProvider ..tags = tags ..pinned = pinned ..shuffle = shuffle - ..includeReplies = includeReplies, + ..includeReplies = includeReplies + ..mediaOnly = mediaOnly + ..queryTerm = queryTerm + ..order = order + ..periodStart = periodStart + ..periodEnd = periodEnd + ..orderDesc = orderDesc, from: from, name: null, dependencies: null, @@ -233,6 +305,12 @@ class PostListNotifierProvider pinned: pinned, shuffle: shuffle, includeReplies: includeReplies, + mediaOnly: mediaOnly, + queryTerm: queryTerm, + order: order, + periodStart: periodStart, + periodEnd: periodEnd, + orderDesc: orderDesc, ), ); } @@ -256,7 +334,13 @@ class PostListNotifierProvider other.tags == tags && other.pinned == pinned && other.shuffle == shuffle && - other.includeReplies == includeReplies; + other.includeReplies == includeReplies && + other.mediaOnly == mediaOnly && + other.queryTerm == queryTerm && + other.order == order && + other.periodStart == periodStart && + other.periodEnd == periodEnd && + other.orderDesc == orderDesc; } @override @@ -270,6 +354,12 @@ class PostListNotifierProvider hash = _SystemHash.combine(hash, pinned.hashCode); hash = _SystemHash.combine(hash, shuffle.hashCode); hash = _SystemHash.combine(hash, includeReplies.hashCode); + hash = _SystemHash.combine(hash, mediaOnly.hashCode); + hash = _SystemHash.combine(hash, queryTerm.hashCode); + hash = _SystemHash.combine(hash, order.hashCode); + hash = _SystemHash.combine(hash, periodStart.hashCode); + hash = _SystemHash.combine(hash, periodEnd.hashCode); + hash = _SystemHash.combine(hash, orderDesc.hashCode); return _SystemHash.finish(hash); } @@ -302,6 +392,24 @@ mixin PostListNotifierRef /// The parameter `includeReplies` of this provider. bool? get includeReplies; + + /// The parameter `mediaOnly` of this provider. + bool? get mediaOnly; + + /// The parameter `queryTerm` of this provider. + String? get queryTerm; + + /// The parameter `order` of this provider. + String? get order; + + /// The parameter `periodStart` of this provider. + int? get periodStart; + + /// The parameter `periodEnd` of this provider. + int? get periodEnd; + + /// The parameter `orderDesc` of this provider. + bool get orderDesc; } class _PostListNotifierProviderElement @@ -331,6 +439,18 @@ class _PostListNotifierProviderElement @override bool? get includeReplies => (origin as PostListNotifierProvider).includeReplies; + @override + bool? get mediaOnly => (origin as PostListNotifierProvider).mediaOnly; + @override + String? get queryTerm => (origin as PostListNotifierProvider).queryTerm; + @override + String? get order => (origin as PostListNotifierProvider).order; + @override + int? get periodStart => (origin as PostListNotifierProvider).periodStart; + @override + int? get periodEnd => (origin as PostListNotifierProvider).periodEnd; + @override + bool get orderDesc => (origin as PostListNotifierProvider).orderDesc; } // ignore_for_file: type=lint