♻️ Refactored post loading

This commit is contained in:
2025-12-06 18:20:47 +08:00
parent 240509ceff
commit 16c7b7e764
11 changed files with 745 additions and 971 deletions

View File

@@ -39,7 +39,7 @@ class PaginationList<T> extends HookConsumerWidget {
final data = ref.watch(provider);
final noti = ref.watch(notifier);
if (data.isLoading && data.value?.isEmpty == true) {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = ResponseLoadingWidget();
return isSliver ? SliverFillRemaining(child: content) : content;
}
@@ -115,7 +115,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
final data = ref.watch(provider);
final noti = ref.watch(notifier);
if (data.isLoading && data.value?.isEmpty == true) {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = ResponseLoadingWidget();
return isSliver ? SliverFillRemaining(child: content) : content;
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/post/post_list.dart';
@@ -17,21 +18,7 @@ enum PostItemType {
}
class SliverPostList extends HookConsumerWidget {
final String? pubName;
final String? realm;
final int? type;
final List<String>? categories;
final List<String>? tags;
final bool shuffle;
final bool? pinned;
final bool? includeReplies;
final bool? mediaOnly;
final String? queryTerm;
// Can be "populaurity", other value will be treated as "date"
final String? order;
final int? periodStart;
final int? periodEnd;
final bool? orderDesc;
final PostListQuery? query;
final PostItemType itemType;
final Color? backgroundColor;
final EdgeInsets? padding;
@@ -39,23 +26,11 @@ class SliverPostList extends HookConsumerWidget {
final Function? onRefresh;
final Function(SnPost)? onUpdate;
final double? maxWidth;
final String? queryKey;
const SliverPostList({
super.key,
this.pubName,
this.realm,
this.type,
this.categories,
this.tags,
this.shuffle = false,
this.pinned,
this.includeReplies,
this.mediaOnly,
this.queryTerm,
this.order,
this.orderDesc = true,
this.periodStart,
this.periodEnd,
this.query,
this.itemType = PostItemType.regular,
this.backgroundColor,
this.padding,
@@ -63,29 +38,19 @@ class SliverPostList extends HookConsumerWidget {
this.onRefresh,
this.onUpdate,
this.maxWidth,
this.queryKey,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final params = PostListQuery(
pubName: pubName,
realm: realm,
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
pinned: pinned,
includeReplies: includeReplies,
mediaOnly: mediaOnly,
queryTerm: queryTerm,
order: order,
periodStart: periodStart,
periodEnd: periodEnd,
orderDesc: orderDesc ?? true,
);
final provider = postListNotifierProvider(params);
final provider = postListProvider(queryKey);
final notifier = provider.notifier;
useEffect(() {
ref.read(notifier).applyFilter(query!);
return null;
}, [query]);
return PaginationList(
provider: provider,
notifier: notifier,

View File

@@ -3,26 +3,29 @@ import 'package:flutter/material.dart';
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
const kShufflePostListId = 'shuffle';
class PostShuffleScreen extends HookConsumerWidget {
const PostShuffleScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
const params = PostListQuery(shuffle: true);
final postListState = ref.watch(postListNotifierProvider(params));
const query = PostListQuery(shuffle: true);
final postListState = ref.watch(postListProvider(kShufflePostListId));
final postListNotifier = ref.watch(
postListNotifierProvider(params).notifier,
postListProvider(kShufflePostListId).notifier,
);
final cardSwiperController = useMemoized(() => CardSwiperController(), []);
useEffect(() {
postListNotifier.applyFilter(query);
return cardSwiperController.dispose;
}, []);
@@ -46,29 +49,32 @@ class PostShuffleScreen extends HookConsumerWidget {
controller: cardSwiperController,
cardsCount: items.length,
isLoop: false,
cardBuilder: (
context,
index,
horizontalOffsetPercentage,
verticalOffsetPercentage,
) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
cardBuilder:
(
context,
index,
horizontalOffsetPercentage,
verticalOffsetPercentage,
) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 540),
child: SingleChildScrollView(
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: PostActionableItem(
item: items[index],
),
),
),
child: PostActionableItem(item: items[index]),
),
),
),
),
);
},
);
},
onEnd: () async {
if (!postListNotifier.fetchedAll) {
postListNotifier.fetchFurther();
@@ -91,24 +97,23 @@ class PostShuffleScreen extends HookConsumerWidget {
bottom: MediaQuery.of(context).padding.bottom,
),
height: kBottomControlHeight,
child:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
cardSwiperController.undo();
},
icon: const Icon(Symbols.arrow_left_alt),
),
IconButton(
onPressed: () {
cardSwiperController.swipe(CardSwiperDirection.right);
},
icon: const Icon(Symbols.arrow_right_alt),
),
],
).padding(all: 8).center(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
cardSwiperController.undo();
},
icon: const Icon(Symbols.arrow_left_alt),
),
IconButton(
onPressed: () {
cardSwiperController.swipe(CardSwiperDirection.right);
},
icon: const Icon(Symbols.arrow_right_alt),
),
],
).padding(all: 8).center(),
),
),
],

View File

@@ -1,34 +1,89 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
class PostFilterWidget extends StatelessWidget {
class PostFilterWidget extends StatefulWidget {
final TabController categoryTabController;
final ValueNotifier<bool?> includeReplies;
final ValueNotifier<bool> mediaOnly;
final ValueNotifier<String?> queryTerm;
final ValueNotifier<String?> order;
final ValueNotifier<bool> orderDesc;
final ValueNotifier<int?> periodStart;
final ValueNotifier<int?> periodEnd;
final ValueNotifier<bool> showAdvancedFilters;
final PostListQuery initialQuery;
final ValueChanged<PostListQuery> onQueryChanged;
final bool hideSearch;
const PostFilterWidget({
super.key,
required this.categoryTabController,
required this.includeReplies,
required this.mediaOnly,
required this.queryTerm,
required this.order,
required this.orderDesc,
required this.periodStart,
required this.periodEnd,
required this.showAdvancedFilters,
required this.initialQuery,
required this.onQueryChanged,
this.hideSearch = false,
});
@override
State<PostFilterWidget> createState() => _PostFilterWidgetState();
}
class _PostFilterWidgetState extends State<PostFilterWidget> {
late bool? _includeReplies;
late bool _mediaOnly;
late String? _queryTerm;
late String? _order;
late bool _orderDesc;
late int? _periodStart;
late int? _periodEnd;
late int? _type;
late bool _showAdvancedFilters;
late TextEditingController _searchController;
@override
void initState() {
super.initState();
_includeReplies = widget.initialQuery.includeReplies;
_mediaOnly = widget.initialQuery.mediaOnly ?? false;
_queryTerm = widget.initialQuery.queryTerm;
_order = widget.initialQuery.order;
_orderDesc = widget.initialQuery.orderDesc;
_periodStart = widget.initialQuery.periodStart;
_periodEnd = widget.initialQuery.periodEnd;
_type = widget.initialQuery.type;
_showAdvancedFilters = false;
_searchController = TextEditingController(text: _queryTerm);
widget.categoryTabController.addListener(_onTabChanged);
}
@override
void dispose() {
widget.categoryTabController.removeListener(_onTabChanged);
_searchController.dispose();
super.dispose();
}
void _onTabChanged() {
final tabIndex = widget.categoryTabController.index;
setState(() {
_type = switch (tabIndex) {
1 => 0,
2 => 1,
_ => null,
};
});
_updateQuery();
}
void _updateQuery() {
final newQuery = widget.initialQuery.copyWith(
includeReplies: _includeReplies,
mediaOnly: _mediaOnly,
queryTerm: _queryTerm,
order: _order,
periodStart: _periodStart,
periodEnd: _periodEnd,
orderDesc: _orderDesc,
type: _type,
);
widget.onQueryChanged(newQuery);
}
@override
Widget build(BuildContext context) {
return Card(
@@ -36,7 +91,7 @@ class PostFilterWidget extends StatelessWidget {
child: Column(
children: [
TabBar(
controller: categoryTabController,
controller: widget.categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [
@@ -53,17 +108,20 @@ class PostFilterWidget extends StatelessWidget {
Expanded(
child: CheckboxListTile(
title: Text('reply'.tr()),
value: includeReplies.value,
value: _includeReplies,
tristate: true,
onChanged: (value) {
// Cycle through: null -> false -> true -> null
if (includeReplies.value == null) {
includeReplies.value = false;
} else if (includeReplies.value == false) {
includeReplies.value = true;
} else {
includeReplies.value = null;
}
setState(() {
if (_includeReplies == null) {
_includeReplies = false;
} else if (_includeReplies == false) {
_includeReplies = true;
} else {
_includeReplies = null;
}
});
_updateQuery();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
@@ -73,11 +131,14 @@ class PostFilterWidget extends StatelessWidget {
Expanded(
child: CheckboxListTile(
title: Text('attachments'.tr()),
value: mediaOnly.value,
value: _mediaOnly,
onChanged: (value) {
if (value != null) {
mediaOnly.value = value;
}
setState(() {
if (value != null) {
_mediaOnly = value;
}
});
_updateQuery();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
@@ -88,11 +149,14 @@ class PostFilterWidget extends StatelessWidget {
),
CheckboxListTile(
title: Text('descendingOrder'.tr()),
value: orderDesc.value,
value: _orderDesc,
onChanged: (value) {
if (value != null) {
orderDesc.value = value;
}
setState(() {
if (value != null) {
_orderDesc = value;
}
});
_updateQuery();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
@@ -109,23 +173,24 @@ class PostFilterWidget extends StatelessWidget {
borderRadius: BorderRadius.all(const Radius.circular(8)),
),
trailing: Icon(
showAdvancedFilters.value
? Symbols.expand_less
: Symbols.expand_more,
_showAdvancedFilters ? Symbols.expand_less : Symbols.expand_more,
),
onTap: () {
showAdvancedFilters.value = !showAdvancedFilters.value;
setState(() {
_showAdvancedFilters = !_showAdvancedFilters;
});
},
),
if (showAdvancedFilters.value) ...[
if (_showAdvancedFilters) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!hideSearch)
if (!widget.hideSearch)
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'search'.tr(),
hintText: 'searchPosts'.tr(),
@@ -139,10 +204,13 @@ class PostFilterWidget extends StatelessWidget {
),
),
onChanged: (value) {
queryTerm.value = value.isEmpty ? null : value;
setState(() {
_queryTerm = value.isEmpty ? null : value;
});
_updateQuery();
},
),
if (!hideSearch) const Gap(12),
if (!widget.hideSearch) const Gap(12),
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: 'sortBy'.tr(),
@@ -154,7 +222,7 @@ class PostFilterWidget extends StatelessWidget {
vertical: 8,
),
),
value: order.value,
value: _order,
items: [
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
DropdownMenuItem(
@@ -163,7 +231,10 @@ class PostFilterWidget extends StatelessWidget {
),
],
onChanged: (value) {
order.value = value;
setState(() {
_order = value;
});
_updateQuery();
},
),
const Gap(12),
@@ -174,9 +245,9 @@ class PostFilterWidget extends StatelessWidget {
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: periodStart.value != null
initialDate: _periodStart != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
_periodStart! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
@@ -185,8 +256,11 @@ class PostFilterWidget extends StatelessWidget {
),
);
if (pickedDate != null) {
periodStart.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
setState(() {
_periodStart =
pickedDate.millisecondsSinceEpoch ~/ 1000;
});
_updateQuery();
}
},
child: InputDecorator(
@@ -204,9 +278,9 @@ class PostFilterWidget extends StatelessWidget {
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodStart.value != null
_periodStart != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
_periodStart! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
@@ -219,9 +293,9 @@ class PostFilterWidget extends StatelessWidget {
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: periodEnd.value != null
initialDate: _periodEnd != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
_periodEnd! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
@@ -230,8 +304,11 @@ class PostFilterWidget extends StatelessWidget {
),
);
if (pickedDate != null) {
periodEnd.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
setState(() {
_periodEnd =
pickedDate.millisecondsSinceEpoch ~/ 1000;
});
_updateQuery();
}
},
child: InputDecorator(
@@ -249,9 +326,9 @@ class PostFilterWidget extends StatelessWidget {
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodEnd.value != null
_periodEnd != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
_periodEnd! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),