feat(library): 收藏与观看历史支持分页

This commit is contained in:
Zhou Weidong committed 2026-10-22 12:34:27 +08:00
1 parent 63d3b0f7ca
commit 235b50598a
24 files changed
+1015 -105

No files matched your search

+38 -11
View File
@@ -17,6 +17,7 @@ import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_pull_to_refresh.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/paged_list_footer.dart';
import '../../core/widgets/subpage_header.dart';
import '../../l10n/app_localizations.dart';
import '../player/open_player.dart';
@@ -26,6 +27,7 @@ import 'widgets/watch_record_card.dart';
const double _pageHorizontal = 14;
const double _statsFontSize = 12;
const double _emptyListHeight = 360;
const double _loadMoreThreshold = 240;
/// 独立观看记录页。
class WatchRecordsPage extends StatelessWidget {
@@ -141,31 +143,56 @@ class _WatchRecordsBodyState extends State<_WatchRecordsBody> {
onOpen: record.drama == null ? null : () => _open(record),
),
],
PagedListFooter(
loading: controller.loadingMore,
hasMore: controller.hasMore,
hasItems: controller.records.isNotEmpty,
errorMessage: controller.loadMoreError == null
? null
: errorText(l10n, controller.loadMoreError, l10n.progressError),
onRetry: controller.loadMore,
),
],
);
}
Widget _refreshable({required List<Widget> children}) {
return AppPullToRefresh(
onRefresh: controller.load,
child: ListView(
physics: AppPullToRefresh.physics,
padding: const EdgeInsets.fromLTRB(
_pageHorizontal,
4,
_pageHorizontal,
28,
return NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: AppPullToRefresh(
onRefresh: controller.load,
child: ListView(
physics: AppPullToRefresh.physics,
padding: const EdgeInsets.fromLTRB(
_pageHorizontal,
4,
_pageHorizontal,
28,
),
children: children,
),
children: children,
),
);
}
bool _onScroll(ScrollNotification notification) {
if (notification.depth != 0) return false;
final reachedBottom =
notification.metrics.extentAfter <= _loadMoreThreshold;
if (reachedBottom &&
(notification is ScrollUpdateNotification ||
notification is ScrollEndNotification ||
notification is OverscrollNotification)) {
unawaited(controller.loadMore());
}
return false;
}
Widget _summary(AppLocalizations l10n) {
return Row(
children: [
Text(
l10n.watchRecordsCount(controller.records.length),
l10n.watchRecordsCount(controller.total),
key: const Key('watch-records-count'),
style: const TextStyle(
color: AppColors.muted,
@@ -39,17 +39,22 @@ class WatchRecordCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (record.detailFailed) {
final hasMetadata = record.hasDisplayMetadata;
if (record.detailFailed && !hasMetadata) {
return _FailedRecordTile(record: record, onRetry: onRetry);
}
final drama = record.drama;
if (drama == null || record.detailLoading) {
if (!hasMetadata && (drama == null || record.detailLoading)) {
return _loadingCard();
}
final l10n = AppLocalizations.of(context)!;
final episodeLabel = record.episodeNumber > 0
final episodeNumber = record.episodeNumber;
final episodeLabel = episodeNumber > 0
? l10n.episodeLabel(record.episodeNumber)
: l10n.episodeUnavailable;
final title = record.displayTitle.isEmpty
? episodeLabel
: record.displayTitle;
return _card(
onTap: onOpen,
child: Stack(
@@ -60,10 +65,14 @@ class WatchRecordCard extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(_coverRadius),
child: RemoteImage(
drama.posterUrl,
record.displayCover,
key: ValueKey('watch-record-cover-${record.dramaId}'),
width: _coverWidth,
height: _coverHeight,
showFallbackIcon: true,
fallbackIconKey: ValueKey(
'watch-record-cover-placeholder-${record.dramaId}',
),
),
),
const SizedBox(width: 12),
@@ -74,7 +83,7 @@ class WatchRecordCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
drama.title,
title,
key: ValueKey('watch-record-title-${record.dramaId}'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -85,14 +94,18 @@ class WatchRecordCard extends StatelessWidget {
),
),
const SizedBox(height: 4),
Text(
episodeLabel,
key: ValueKey('watch-record-episode-${record.dramaId}'),
style: const TextStyle(
color: AppColors.muted,
fontSize: _statsFontSize,
if (title != episodeLabel) ...[
Text(
episodeLabel,
key: ValueKey(
'watch-record-episode-${record.dramaId}',
),
style: const TextStyle(
color: AppColors.muted,
fontSize: _statsFontSize,
),
),
),
],
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(_progressRadius),