merge: 修正 ChangeNotifier 监听生命周期
This commit is contained in:
commit
2cfa655750
7 files changed
+286
-269
No files matched your search
@@ -1131,3 +1131,10 @@ $ flutter test
|
|||||||
- 首页移除父级 feed 监听,网格、剧单和全部短剧改为局部监听,刷新时保留旧数据及原滚动位置。
|
- 首页移除父级 feed 监听,网格、剧单和全部短剧改为局部监听,刷新时保留旧数据及原滚动位置。
|
||||||
- 剧单、全部短剧、追剧沿用相同刷新策略;首帧加载与刷新指示器分离。
|
- 剧单、全部短剧、追剧沿用相同刷新策略;首帧加载与刷新指示器分离。
|
||||||
- 按当前流程未执行 analyze/test。
|
- 按当前流程未执行 analyze/test。
|
||||||
|
|
||||||
|
## 2026-10-22 ChangeNotifier 监听生命周期排查
|
||||||
|
|
||||||
|
- 全仓核对手动 add/remove;首页、推荐和追剧改为内容区单实例 ListenableBuilder,避免重复订阅。
|
||||||
|
- 播放器进度监听改为具名回调,在切换、重建会话和 dispose 时解绑;评论输入弹窗补 controller 更换迁移。
|
||||||
|
- `_onFeedChanged` 已无源码调用链,报错来源为热重载前注册的旧 tear-off;需完全关闭 App 后冷启动。
|
||||||
|
- 按当前流程未执行 analyze/test。
|
||||||
@@ -105,22 +105,25 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
walletBadge: walletBadge,
|
walletBadge: walletBadge,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PageView.builder(
|
child: ListenableBuilder(
|
||||||
key: const Key('home-primary-page-view'),
|
listenable: widget.feedController,
|
||||||
controller: _pageController,
|
builder: (context, _) => PageView.builder(
|
||||||
itemCount: HomePrimaryTab.values.length,
|
key: const Key('home-primary-page-view'),
|
||||||
onPageChanged: (index) => setState(
|
controller: _pageController,
|
||||||
() => _selectedPrimary = HomePrimaryTab.values[index],
|
itemCount: HomePrimaryTab.values.length,
|
||||||
),
|
onPageChanged: (index) => setState(
|
||||||
itemBuilder: (context, index) => _KeepAlivePage(
|
() => _selectedPrimary = HomePrimaryTab.values[index],
|
||||||
key: PageStorageKey(
|
|
||||||
'home-primary-page-${HomePrimaryTab.values[index].name}',
|
|
||||||
),
|
),
|
||||||
child: _tabGrid(
|
itemBuilder: (context, index) => _KeepAlivePage(
|
||||||
context,
|
key: PageStorageKey(
|
||||||
l10n,
|
'home-primary-page-${HomePrimaryTab.values[index].name}',
|
||||||
tab: HomePrimaryTab.values[index],
|
),
|
||||||
shortcuts: shortcuts,
|
child: _tabGrid(
|
||||||
|
context,
|
||||||
|
l10n,
|
||||||
|
tab: HomePrimaryTab.values[index],
|
||||||
|
shortcuts: shortcuts,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -152,90 +155,81 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
required HomePrimaryTab tab,
|
required HomePrimaryTab tab,
|
||||||
required List<HomeGridCategoryShortcut> shortcuts,
|
required List<HomeGridCategoryShortcut> shortcuts,
|
||||||
}) {
|
}) {
|
||||||
return ListenableBuilder(
|
if (widget.feedController.loadingFirstPage) {
|
||||||
listenable: widget.feedController,
|
return const Center(
|
||||||
builder: (context, _) {
|
child: SizedBox.square(
|
||||||
if (widget.feedController.loadingFirstPage) {
|
dimension: 24,
|
||||||
return const Center(
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
VideoPlayerPort? trackedPlayer;
|
VideoPlayerPort? trackedPlayer;
|
||||||
Episode? trackedEpisode;
|
Episode? trackedEpisode;
|
||||||
Drama? trackedDrama;
|
Drama? trackedDrama;
|
||||||
|
int? _trackedPlayerIndex;
|
||||||
|
bool _completionHandled = false;
|
||||||
|
bool _completionProgressSaved = false;
|
||||||
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
|
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
|
||||||
/// 进度上报节流:契约建议每 10 秒一次
|
/// 进度上报节流:契约建议每 10 秒一次
|
||||||
@@ -317,6 +320,7 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
unawaited(saveCurrentProgress());
|
unawaited(saveCurrentProgress());
|
||||||
final previous = session;
|
final previous = session;
|
||||||
sessionEpisodes = List<Episode>.of(nextEpisodes);
|
sessionEpisodes = List<Episode>.of(nextEpisodes);
|
||||||
|
_detachTrackedPlayerListener();
|
||||||
trackedPlayer = null;
|
trackedPlayer = null;
|
||||||
trackedEpisode = null;
|
trackedEpisode = null;
|
||||||
trackedDrama = null;
|
trackedDrama = null;
|
||||||
@@ -601,6 +605,7 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
pictureInPicture.setStateListener(null);
|
pictureInPicture.setStateListener(null);
|
||||||
unawaited(saveCurrentProgress());
|
unawaited(saveCurrentProgress());
|
||||||
|
_detachTrackedPlayerListener();
|
||||||
if (_started) {
|
if (_started) {
|
||||||
session.setRouteVisible(false);
|
session.setRouteVisible(false);
|
||||||
cancelScrub();
|
cancelScrub();
|
||||||
@@ -696,46 +701,66 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
await player.controller.setVolume(muted ? 0 : 1);
|
await player.controller.setVolume(muted ? 0 : 1);
|
||||||
await player.controller.setPlaybackSpeed(playbackSpeed);
|
await player.controller.setPlaybackSpeed(playbackSpeed);
|
||||||
if (session.active != player) return;
|
if (session.active != player) return;
|
||||||
|
_detachTrackedPlayerListener();
|
||||||
trackedPlayer = player;
|
trackedPlayer = player;
|
||||||
trackedEpisode = episode;
|
trackedEpisode = episode;
|
||||||
trackedDrama = drama;
|
trackedDrama = drama;
|
||||||
|
_trackedPlayerIndex = index;
|
||||||
|
_completionHandled = false;
|
||||||
|
_completionProgressSaved = false;
|
||||||
lastProgressWrite = DateTime.now();
|
lastProgressWrite = DateTime.now();
|
||||||
var completionHandled = false;
|
player.controller.addListener(_handleTrackedPlayerChanged);
|
||||||
var completionProgressSaved = false;
|
}
|
||||||
player.controller.addListener(() {
|
|
||||||
if (session.active != player) return;
|
void _detachTrackedPlayerListener() {
|
||||||
final snapshot = _readPlaybackSnapshot(player);
|
trackedPlayer?.controller.removeListener(_handleTrackedPlayerChanged);
|
||||||
if (snapshot == null) return;
|
_trackedPlayerIndex = null;
|
||||||
_cachePlaybackPosition(drama, episode, snapshot);
|
}
|
||||||
if (snapshot.completed && !completionProgressSaved) {
|
|
||||||
completionProgressSaved = true;
|
void _handleTrackedPlayerChanged() {
|
||||||
unawaited(
|
if (_disposed) return;
|
||||||
queueProgress(
|
final player = trackedPlayer;
|
||||||
drama,
|
final episode = trackedEpisode;
|
||||||
episode,
|
final drama = trackedDrama;
|
||||||
snapshot.duration,
|
final index = _trackedPlayerIndex;
|
||||||
duration: snapshot.duration,
|
if (player == null ||
|
||||||
finished: true,
|
episode == null ||
|
||||||
),
|
drama == null ||
|
||||||
);
|
index == null ||
|
||||||
}
|
session.active != player) {
|
||||||
if (!completionHandled &&
|
return;
|
||||||
snapshot.completed &&
|
}
|
||||||
session.canAutoAdvance &&
|
final snapshot = _readPlaybackSnapshot(player);
|
||||||
session.currentIndex == index) {
|
if (snapshot == null) return;
|
||||||
completionHandled = true;
|
_cachePlaybackPosition(drama, episode, snapshot);
|
||||||
unawaited(advanceAfterEnd(player, index));
|
if (snapshot.completed && !_completionProgressSaved) {
|
||||||
}
|
_completionProgressSaved = true;
|
||||||
if (!shouldReportProgress(DateTime.now())) return;
|
|
||||||
unawaited(
|
unawaited(
|
||||||
queueProgress(
|
queueProgress(
|
||||||
drama,
|
drama,
|
||||||
episode,
|
episode,
|
||||||
snapshot.position,
|
snapshot.duration,
|
||||||
duration: snapshot.duration,
|
duration: snapshot.duration,
|
||||||
|
finished: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
if (!_completionHandled &&
|
||||||
|
snapshot.completed &&
|
||||||
|
session.canAutoAdvance &&
|
||||||
|
session.currentIndex == index) {
|
||||||
|
_completionHandled = true;
|
||||||
|
unawaited(advanceAfterEnd(player, index));
|
||||||
|
}
|
||||||
|
if (!shouldReportProgress(DateTime.now())) return;
|
||||||
|
unawaited(
|
||||||
|
queueProgress(
|
||||||
|
drama,
|
||||||
|
episode,
|
||||||
|
snapshot.position,
|
||||||
|
duration: snapshot.duration,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> advanceAfterEnd(VideoPlayerPort player, int index) async {
|
Future<void> advanceAfterEnd(VideoPlayerPort player, int index) async {
|
||||||
|
|||||||
@@ -79,6 +79,15 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
|
|||||||
_trackKeyboardInset(_keyboardHeight(context));
|
_trackKeyboardInset(_keyboardHeight(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant CommentComposerSheet oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.controller != widget.controller) {
|
||||||
|
oldWidget.controller.removeListener(_onControllerChanged);
|
||||||
|
widget.controller.addListener(_onControllerChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _prepareInput() async {
|
Future<void> _prepareInput() async {
|
||||||
if (_isAndroid) {
|
if (_isAndroid) {
|
||||||
await _keyboardInset.start();
|
await _keyboardInset.start();
|
||||||
|
|||||||
@@ -56,25 +56,28 @@ class _RecommendPageState extends State<RecommendPage> {
|
|||||||
children: [
|
children: [
|
||||||
_header(l10n),
|
_header(l10n),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: IndexedStack(
|
child: ListenableBuilder(
|
||||||
index: _tab.index,
|
listenable: Listenable.merge([
|
||||||
children: [
|
widget.feedController,
|
||||||
ListenableBuilder(
|
categories,
|
||||||
listenable: widget.feedController,
|
]),
|
||||||
builder: (context, _) => _recommendFeed(context),
|
builder: (context, _) => IndexedStack(
|
||||||
),
|
index: _tab.index,
|
||||||
RecommendSectionsView(
|
children: [
|
||||||
feedController: widget.feedController,
|
_recommendFeed(context),
|
||||||
categories: categories,
|
RecommendSectionsView(
|
||||||
onOpenDrama: _openDrama,
|
feedController: widget.feedController,
|
||||||
onMore: _handleMore,
|
categories: categories,
|
||||||
),
|
onOpenDrama: _openDrama,
|
||||||
AllDramasFilterGrid(
|
onMore: _handleMore,
|
||||||
feedController: widget.feedController,
|
),
|
||||||
onOpenDrama: _openDrama,
|
AllDramasFilterGrid(
|
||||||
onOpenFilter: widget.onOpenFilter,
|
feedController: widget.feedController,
|
||||||
),
|
onOpenDrama: _openDrama,
|
||||||
],
|
onOpenFilter: widget.onOpenFilter,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final dramas = _filtered();
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final itemWidth =
|
final itemWidth =
|
||||||
@@ -65,69 +66,55 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
|||||||
key: const Key('all-dramas-scroll'),
|
key: const Key('all-dramas-scroll'),
|
||||||
physics: AppPullToRefresh.physics,
|
physics: AppPullToRefresh.physics,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(child: _filters(l10n)),
|
||||||
child: ListenableBuilder(
|
if (dramas.isEmpty)
|
||||||
listenable: widget.feedController,
|
SliverFillRemaining(
|
||||||
builder: (context, _) => _filters(l10n),
|
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(
|
||||||
ListenableBuilder(
|
child: Padding(
|
||||||
listenable: widget.feedController,
|
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||||
builder: (context, _) {
|
child: PagedListFooter(
|
||||||
final dramas = _filtered();
|
loading: widget.feedController.loadingMore,
|
||||||
if (dramas.isEmpty) {
|
hasMore: widget.feedController.hasMore,
|
||||||
return SliverFillRemaining(
|
hasItems: dramas.isNotEmpty,
|
||||||
hasScrollBody: false,
|
errorMessage: widget.feedController.lastError == null
|
||||||
child: EmptyState(
|
? null
|
||||||
key: const Key('all-dramas-empty'),
|
: l10n.catalogError,
|
||||||
icon: Icons.movie_filter_outlined,
|
onRetry: widget.feedController.loadMore,
|
||||||
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,68 +33,60 @@ class RecommendSectionsView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListenableBuilder(
|
final l10n = AppLocalizations.of(context)!;
|
||||||
listenable: Listenable.merge([feedController, categories]),
|
final source = feedController.dramas;
|
||||||
builder: (context, _) {
|
final hits = [...source]
|
||||||
final l10n = AppLocalizations.of(context)!;
|
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
||||||
final source = feedController.dramas;
|
final newest = [...source]
|
||||||
final hits = [...source]
|
..sort((a, b) {
|
||||||
..sort((a, b) => b.viewCount.compareTo(a.viewCount));
|
final left = publishedAtForDrama(a);
|
||||||
final newest = [...source]
|
final right = publishedAtForDrama(b);
|
||||||
..sort((a, b) {
|
if (left == null && right == null) return b.id.compareTo(a.id);
|
||||||
final left = publishedAtForDrama(a);
|
if (left == null) return 1;
|
||||||
final right = publishedAtForDrama(b);
|
if (right == null) return -1;
|
||||||
if (left == null && right == null) return b.id.compareTo(a.id);
|
return right.compareTo(left);
|
||||||
if (left == null) return 1;
|
});
|
||||||
if (right == null) return -1;
|
return AppPullToRefresh(
|
||||||
return right.compareTo(left);
|
key: const Key('recommend-sections-refresh'),
|
||||||
});
|
onRefresh: () async {
|
||||||
return AppPullToRefresh(
|
await Future.wait([feedController.refreshCurrent(), categories.load()]);
|
||||||
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