fix(home): 优化短剧列表下拉刷新体验
This commit is contained in:
1 parent
f69a27bb80
commit
7945f3ffed
8 files changed
+247
-201
No files matched your search
@@ -210,20 +210,14 @@ class _HomeTabsState extends State<_HomeTabs> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
logic = HomeBinding().create(widget.feedController.dramas);
|
||||
widget.feedController.addListener(_onFeedChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.feedController.removeListener(_onFeedChanged);
|
||||
logic.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFeedChanged() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
|
||||
@@ -152,83 +152,90 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||
required HomePrimaryTab tab,
|
||||
required List<HomeGridCategoryShortcut> shortcuts,
|
||||
}) {
|
||||
final tag = _selectedTags[tab];
|
||||
final dramas = _dramasFor(tab, tag, shortcuts);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final itemWidth =
|
||||
(constraints.maxWidth -
|
||||
AppDimens.feedPageHorizontal * 2 -
|
||||
AppDimens.homeGridGap * 2) /
|
||||
3;
|
||||
final itemExtent =
|
||||
itemWidth / AppDimens.cardCoverAspectRatio +
|
||||
AppDimens.homeCardBodyExtent;
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScroll,
|
||||
child: AppPullToRefresh(
|
||||
key: const Key('home-grid-refresh'),
|
||||
onRefresh: _refresh,
|
||||
child: CustomScrollView(
|
||||
key: const Key('home-grid-scroll'),
|
||||
controller: _scrollControllers[tab],
|
||||
physics: AppPullToRefresh.physics,
|
||||
slivers: [
|
||||
if (widget.feedController.loadingFirstPage)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (dramas.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: _emptyState(l10n, tab, tag),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('home-grid-cards'),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => HomeFeedCard(
|
||||
drama: dramas[index],
|
||||
onTap: () => widget.onOpenDrama(dramas[index]),
|
||||
),
|
||||
childCount: dramas.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (dramas.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||
child: PagedListFooter(
|
||||
loading: widget.feedController.loadingMore,
|
||||
hasMore: widget.feedController.hasMore,
|
||||
hasItems: dramas.isNotEmpty,
|
||||
errorMessage: widget.feedController.lastError == null
|
||||
? null
|
||||
: l10n.catalogError,
|
||||
onRetry: widget.feedController.loadMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) {
|
||||
if (widget.feedController.loadingFirstPage) {
|
||||
return const Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final tag = _selectedTags[tab];
|
||||
final dramas = _dramasFor(tab, tag, shortcuts);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final itemWidth =
|
||||
(constraints.maxWidth -
|
||||
AppDimens.feedPageHorizontal * 2 -
|
||||
AppDimens.homeGridGap * 2) /
|
||||
3;
|
||||
final itemExtent =
|
||||
itemWidth / AppDimens.cardCoverAspectRatio +
|
||||
AppDimens.homeCardBodyExtent;
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScroll,
|
||||
child: AppPullToRefresh(
|
||||
key: const Key('home-grid-refresh'),
|
||||
onRefresh: _refresh,
|
||||
child: CustomScrollView(
|
||||
key: const Key('home-grid-scroll'),
|
||||
controller: _scrollControllers[tab],
|
||||
physics: AppPullToRefresh.physics,
|
||||
slivers: [
|
||||
if (dramas.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: _emptyState(l10n, tab, tag),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('home-grid-cards'),
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => HomeFeedCard(
|
||||
drama: dramas[index],
|
||||
onTap: () => widget.onOpenDrama(dramas[index]),
|
||||
),
|
||||
childCount: dramas.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (dramas.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: AppDimens.spaceXl,
|
||||
),
|
||||
child: PagedListFooter(
|
||||
loading: widget.feedController.loadingMore,
|
||||
hasMore: widget.feedController.hasMore,
|
||||
hasItems: dramas.isNotEmpty,
|
||||
errorMessage:
|
||||
widget.feedController.lastError == null
|
||||
? null
|
||||
: l10n.catalogError,
|
||||
onRetry: widget.feedController.loadMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ class _RecommendPageState extends State<RecommendPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final categories = context.watch<CategoryController>();
|
||||
final categories = context.read<CategoryController>();
|
||||
return ColoredBox(
|
||||
key: const Key('recommend-page'),
|
||||
color: AppColors.appBackground,
|
||||
@@ -59,7 +59,10 @@ class _RecommendPageState extends State<RecommendPage> {
|
||||
child: IndexedStack(
|
||||
index: _tab.index,
|
||||
children: [
|
||||
_recommendFeed(context),
|
||||
ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) => _recommendFeed(context),
|
||||
),
|
||||
RecommendSectionsView(
|
||||
feedController: widget.feedController,
|
||||
categories: categories,
|
||||
|
||||
@@ -44,7 +44,6 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final dramas = _filtered();
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final itemWidth =
|
||||
@@ -66,55 +65,69 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
key: const Key('all-dramas-scroll'),
|
||||
physics: AppPullToRefresh.physics,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: _filters(l10n)),
|
||||
if (dramas.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyState(
|
||||
key: const Key('all-dramas-empty'),
|
||||
icon: Icons.movie_filter_outlined,
|
||||
title: l10n.homeGridFilterEmpty,
|
||||
actionLabel: l10n.homeGridClearFilter,
|
||||
onAction: () => setState(() {
|
||||
_region = 'all';
|
||||
_audio = 'all';
|
||||
_genre = null;
|
||||
}),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('all-dramas-grid'),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => HomeFeedCard(
|
||||
drama: dramas[index],
|
||||
onTap: () => widget.onOpenDrama(dramas[index]),
|
||||
),
|
||||
childCount: dramas.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||
child: PagedListFooter(
|
||||
loading: widget.feedController.loadingMore,
|
||||
hasMore: widget.feedController.hasMore,
|
||||
hasItems: dramas.isNotEmpty,
|
||||
errorMessage: widget.feedController.lastError == null
|
||||
? null
|
||||
: l10n.catalogError,
|
||||
onRetry: widget.feedController.loadMore,
|
||||
child: ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) => _filters(l10n),
|
||||
),
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) {
|
||||
final dramas = _filtered();
|
||||
if (dramas.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: EmptyState(
|
||||
key: const Key('all-dramas-empty'),
|
||||
icon: Icons.movie_filter_outlined,
|
||||
title: l10n.homeGridFilterEmpty,
|
||||
actionLabel: l10n.homeGridClearFilter,
|
||||
onAction: () => setState(() {
|
||||
_region = 'all';
|
||||
_audio = 'all';
|
||||
_genre = null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('all-dramas-grid'),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => HomeFeedCard(
|
||||
drama: dramas[index],
|
||||
onTap: () => widget.onOpenDrama(dramas[index]),
|
||||
),
|
||||
childCount: dramas.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) => SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||
child: PagedListFooter(
|
||||
loading: widget.feedController.loadingMore,
|
||||
hasMore: widget.feedController.hasMore,
|
||||
hasItems: _filtered().isNotEmpty,
|
||||
errorMessage: widget.feedController.lastError == null
|
||||
? null
|
||||
: l10n.catalogError,
|
||||
onRetry: widget.feedController.loadMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,60 +33,68 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final source = feedController.dramas;
|
||||
final hits = [...source]
|
||||
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
||||
final newest = [...source]
|
||||
..sort((a, b) {
|
||||
final left = publishedAtForDrama(a);
|
||||
final right = publishedAtForDrama(b);
|
||||
if (left == null && right == null) return b.id.compareTo(a.id);
|
||||
if (left == null) return 1;
|
||||
if (right == null) return -1;
|
||||
return right.compareTo(left);
|
||||
});
|
||||
return AppPullToRefresh(
|
||||
key: const Key('recommend-sections-refresh'),
|
||||
onRefresh: () async {
|
||||
await Future.wait([feedController.refreshCurrent(), categories.load()]);
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge([feedController, categories]),
|
||||
builder: (context, _) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final source = feedController.dramas;
|
||||
final hits = [...source]
|
||||
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
||||
final newest = [...source]
|
||||
..sort((a, b) {
|
||||
final left = publishedAtForDrama(a);
|
||||
final right = publishedAtForDrama(b);
|
||||
if (left == null && right == null) return b.id.compareTo(a.id);
|
||||
if (left == null) return 1;
|
||||
if (right == null) return -1;
|
||||
return right.compareTo(left);
|
||||
});
|
||||
return AppPullToRefresh(
|
||||
key: const Key('recommend-sections-refresh'),
|
||||
onRefresh: () async {
|
||||
await Future.wait([
|
||||
feedController.refreshCurrent(),
|
||||
categories.load(),
|
||||
]);
|
||||
},
|
||||
child: ListView(
|
||||
key: const Key('recommend-sections-scroll'),
|
||||
physics: AppPullToRefresh.physics,
|
||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||
children: [
|
||||
_section(
|
||||
context,
|
||||
l10n,
|
||||
key: 'hits',
|
||||
title: l10n.weeklyHitsTitle,
|
||||
subtitle: l10n.weeklyHitsSubtitle,
|
||||
dramas: hits.take(10).toList(),
|
||||
onMore: () => onMore(RecommendSectionKind.hits),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXl),
|
||||
_section(
|
||||
context,
|
||||
l10n,
|
||||
key: 'new',
|
||||
title: l10n.weeklyNewTitle,
|
||||
subtitle: l10n.weeklyNewSubtitle,
|
||||
dramas: newest.take(10).toList(),
|
||||
onMore: () => onMore(RecommendSectionKind.newReleases),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXl),
|
||||
_section(
|
||||
context,
|
||||
l10n,
|
||||
key: 'costume',
|
||||
title: l10n.costumeAwardsTitle,
|
||||
subtitle: l10n.costumeAwardsSubtitle,
|
||||
dramas: _costumeDramas(source),
|
||||
onMore: () => onMore(RecommendSectionKind.costume),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListView(
|
||||
key: const Key('recommend-sections-scroll'),
|
||||
physics: AppPullToRefresh.physics,
|
||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||
children: [
|
||||
_section(
|
||||
context,
|
||||
l10n,
|
||||
key: 'hits',
|
||||
title: l10n.weeklyHitsTitle,
|
||||
subtitle: l10n.weeklyHitsSubtitle,
|
||||
dramas: hits.take(10).toList(),
|
||||
onMore: () => onMore(RecommendSectionKind.hits),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXl),
|
||||
_section(
|
||||
context,
|
||||
l10n,
|
||||
key: 'new',
|
||||
title: l10n.weeklyNewTitle,
|
||||
subtitle: l10n.weeklyNewSubtitle,
|
||||
dramas: newest.take(10).toList(),
|
||||
onMore: () => onMore(RecommendSectionKind.newReleases),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXl),
|
||||
_section(
|
||||
context,
|
||||
l10n,
|
||||
key: 'costume',
|
||||
title: l10n.costumeAwardsTitle,
|
||||
subtitle: l10n.costumeAwardsSubtitle,
|
||||
dramas: _costumeDramas(source),
|
||||
onMore: () => onMore(RecommendSectionKind.costume),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user