fix(client): 修正 ChangeNotifier 监听生命周期
This commit is contained in:
1 parent
4291031ace
commit
8936c3d6eb
7 files changed
+286
-269
No files matched your search
@@ -105,22 +105,25 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||
walletBadge: walletBadge,
|
||||
),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
key: const Key('home-primary-page-view'),
|
||||
controller: _pageController,
|
||||
itemCount: HomePrimaryTab.values.length,
|
||||
onPageChanged: (index) => setState(
|
||||
() => _selectedPrimary = HomePrimaryTab.values[index],
|
||||
),
|
||||
itemBuilder: (context, index) => _KeepAlivePage(
|
||||
key: PageStorageKey(
|
||||
'home-primary-page-${HomePrimaryTab.values[index].name}',
|
||||
child: ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) => PageView.builder(
|
||||
key: const Key('home-primary-page-view'),
|
||||
controller: _pageController,
|
||||
itemCount: HomePrimaryTab.values.length,
|
||||
onPageChanged: (index) => setState(
|
||||
() => _selectedPrimary = HomePrimaryTab.values[index],
|
||||
),
|
||||
child: _tabGrid(
|
||||
context,
|
||||
l10n,
|
||||
tab: HomePrimaryTab.values[index],
|
||||
shortcuts: shortcuts,
|
||||
itemBuilder: (context, index) => _KeepAlivePage(
|
||||
key: PageStorageKey(
|
||||
'home-primary-page-${HomePrimaryTab.values[index].name}',
|
||||
),
|
||||
child: _tabGrid(
|
||||
context,
|
||||
l10n,
|
||||
tab: HomePrimaryTab.values[index],
|
||||
shortcuts: shortcuts,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -152,90 +155,81 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||
required HomePrimaryTab tab,
|
||||
required List<HomeGridCategoryShortcut> shortcuts,
|
||||
}) {
|
||||
return ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) {
|
||||
if (widget.feedController.loadingFirstPage) {
|
||||
return const Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
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;
|
||||
Episode? trackedEpisode;
|
||||
Drama? trackedDrama;
|
||||
int? _trackedPlayerIndex;
|
||||
bool _completionHandled = false;
|
||||
bool _completionProgressSaved = false;
|
||||
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
/// 进度上报节流:契约建议每 10 秒一次
|
||||
@@ -317,6 +320,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
unawaited(saveCurrentProgress());
|
||||
final previous = session;
|
||||
sessionEpisodes = List<Episode>.of(nextEpisodes);
|
||||
_detachTrackedPlayerListener();
|
||||
trackedPlayer = null;
|
||||
trackedEpisode = null;
|
||||
trackedDrama = null;
|
||||
@@ -601,6 +605,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
_disposed = true;
|
||||
pictureInPicture.setStateListener(null);
|
||||
unawaited(saveCurrentProgress());
|
||||
_detachTrackedPlayerListener();
|
||||
if (_started) {
|
||||
session.setRouteVisible(false);
|
||||
cancelScrub();
|
||||
@@ -696,46 +701,66 @@ class PlayerLogic extends ChangeNotifier {
|
||||
await player.controller.setVolume(muted ? 0 : 1);
|
||||
await player.controller.setPlaybackSpeed(playbackSpeed);
|
||||
if (session.active != player) return;
|
||||
_detachTrackedPlayerListener();
|
||||
trackedPlayer = player;
|
||||
trackedEpisode = episode;
|
||||
trackedDrama = drama;
|
||||
_trackedPlayerIndex = index;
|
||||
_completionHandled = false;
|
||||
_completionProgressSaved = false;
|
||||
lastProgressWrite = DateTime.now();
|
||||
var completionHandled = false;
|
||||
var completionProgressSaved = false;
|
||||
player.controller.addListener(() {
|
||||
if (session.active != player) return;
|
||||
final snapshot = _readPlaybackSnapshot(player);
|
||||
if (snapshot == null) return;
|
||||
_cachePlaybackPosition(drama, episode, snapshot);
|
||||
if (snapshot.completed && !completionProgressSaved) {
|
||||
completionProgressSaved = true;
|
||||
unawaited(
|
||||
queueProgress(
|
||||
drama,
|
||||
episode,
|
||||
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;
|
||||
player.controller.addListener(_handleTrackedPlayerChanged);
|
||||
}
|
||||
|
||||
void _detachTrackedPlayerListener() {
|
||||
trackedPlayer?.controller.removeListener(_handleTrackedPlayerChanged);
|
||||
_trackedPlayerIndex = null;
|
||||
}
|
||||
|
||||
void _handleTrackedPlayerChanged() {
|
||||
if (_disposed) return;
|
||||
final player = trackedPlayer;
|
||||
final episode = trackedEpisode;
|
||||
final drama = trackedDrama;
|
||||
final index = _trackedPlayerIndex;
|
||||
if (player == null ||
|
||||
episode == null ||
|
||||
drama == null ||
|
||||
index == null ||
|
||||
session.active != player) {
|
||||
return;
|
||||
}
|
||||
final snapshot = _readPlaybackSnapshot(player);
|
||||
if (snapshot == null) return;
|
||||
_cachePlaybackPosition(drama, episode, snapshot);
|
||||
if (snapshot.completed && !_completionProgressSaved) {
|
||||
_completionProgressSaved = true;
|
||||
unawaited(
|
||||
queueProgress(
|
||||
drama,
|
||||
episode,
|
||||
snapshot.position,
|
||||
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 {
|
||||
|
||||
@@ -79,6 +79,15 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
|
||||
_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 {
|
||||
if (_isAndroid) {
|
||||
await _keyboardInset.start();
|
||||
|
||||
@@ -56,25 +56,28 @@ class _RecommendPageState extends State<RecommendPage> {
|
||||
children: [
|
||||
_header(l10n),
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: _tab.index,
|
||||
children: [
|
||||
ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) => _recommendFeed(context),
|
||||
),
|
||||
RecommendSectionsView(
|
||||
feedController: widget.feedController,
|
||||
categories: categories,
|
||||
onOpenDrama: _openDrama,
|
||||
onMore: _handleMore,
|
||||
),
|
||||
AllDramasFilterGrid(
|
||||
feedController: widget.feedController,
|
||||
onOpenDrama: _openDrama,
|
||||
onOpenFilter: widget.onOpenFilter,
|
||||
),
|
||||
],
|
||||
child: ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
widget.feedController,
|
||||
categories,
|
||||
]),
|
||||
builder: (context, _) => IndexedStack(
|
||||
index: _tab.index,
|
||||
children: [
|
||||
_recommendFeed(context),
|
||||
RecommendSectionsView(
|
||||
feedController: widget.feedController,
|
||||
categories: categories,
|
||||
onOpenDrama: _openDrama,
|
||||
onMore: _handleMore,
|
||||
),
|
||||
AllDramasFilterGrid(
|
||||
feedController: widget.feedController,
|
||||
onOpenDrama: _openDrama,
|
||||
onOpenFilter: widget.onOpenFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -44,6 +44,7 @@ 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 =
|
||||
@@ -65,69 +66,55 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
key: const Key('all-dramas-scroll'),
|
||||
physics: AppPullToRefresh.physics,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: ListenableBuilder(
|
||||
listenable: widget.feedController,
|
||||
builder: (context, _) => _filters(l10n),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,68 +33,60 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user