feat: 调整播放历史为单列列表

This commit is contained in:
Zhou Weidong committed 2026-10-22 21:18:02 +08:00
1 parent b185200e8c
commit c14d89ffce
8 files changed
+317 -14

No files matched your search

+10
View File
@@ -158,6 +158,16 @@ abstract final class AppDimens {
static const double followingSubtitleFont = 12;
static const double followingSubtitleGap = 4;
static const double followingProgressHeight = 2;
static const double followingHistoryCoverWidth = 96;
static const double followingHistoryCoverHeight = 128;
static const double followingHistoryCoverRadius = 6;
static const double followingHistoryContentGap = 12;
static const double followingHistoryRowGap = 16;
static const double followingHistoryProgressHeight = 3;
static const double followingHistoryProgressRadius = 2;
static const double followingHistoryProgressGap = 8;
static const double followingHistoryLabelGap = 4;
static const double followingHistoryActionHeight = 28;
static const Duration followingTabAnimationDuration = Duration(
milliseconds: 180,
);
+1 -1
View File
@@ -284,7 +284,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String watchedPercent(int percent) {
return '已观看 $percent%';
return '已看 $percent%';
}
@override
+1 -1
View File
@@ -121,7 +121,7 @@
"count": {"type": "int"}
}
},
"watchedPercent": "已观看 {percent}%",
"watchedPercent": "已看 {percent}%",
"@watchedPercent": {
"placeholders": {
"percent": {
@@ -0,0 +1,228 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../core/widgets/cover_play_count.dart';
import '../../core/widgets/remote_image.dart';
import '../../l10n/app_localizations.dart';
class FollowingHistoryCard extends StatelessWidget {
const FollowingHistoryCard({
super.key,
required this.dramaId,
required this.title,
required this.posterUrl,
required this.viewCount,
required this.subtitle,
required this.onTap,
required this.onContinue,
this.progress,
this.progressLabel,
this.editing = false,
this.onRemove,
});
final String dramaId;
final String title;
final String posterUrl;
final int viewCount;
final String subtitle;
final VoidCallback onTap;
final VoidCallback onContinue;
final double? progress;
final String? progressLabel;
final bool editing;
final VoidCallback? onRemove;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return InkWell(
key: Key('following-history-card-$dramaId'),
borderRadius: BorderRadius.circular(
AppDimens.followingHistoryCoverRadius,
),
onTap: onTap,
child: SizedBox(
height: AppDimens.followingHistoryCoverHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_cover(),
const SizedBox(width: AppDimens.followingHistoryContentGap),
Expanded(child: _details(l10n)),
],
),
),
);
}
Widget _cover() {
return SizedBox(
key: Key('following-history-cover-$dramaId'),
width: AppDimens.followingHistoryCoverWidth,
height: AppDimens.followingHistoryCoverHeight,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppDimens.followingHistoryCoverRadius,
),
child: RemoteImage(
posterUrl,
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
),
),
const Positioned(
left: 0,
right: 0,
bottom: 0,
child: CoverBottomScrim(),
),
Positioned(
right: AppDimens.cardPlayCountRight,
bottom: AppDimens.cardPlayCountBottom,
child: CoverPlayCount(
key: Key('following-history-views-$dramaId'),
count: viewCount,
),
),
],
),
);
}
Widget _details(AppLocalizations l10n) {
final hasProgress = progress != null;
return Stack(
children: [
Align(
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(right: editing ? 28 : 0),
child: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.cardTitleFont,
fontWeight: FontWeight.w500,
height: AppDimens.cardTitleLineHeight,
),
),
),
const SizedBox(height: AppDimens.followingSubtitleGap),
Row(
children: [
Expanded(
child: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.followingSubtitleFont,
),
),
),
if (!editing && hasProgress) ...[
const SizedBox(width: AppDimens.spaceSm),
_continueButton(l10n),
],
],
),
if (hasProgress) ...[
const SizedBox(height: AppDimens.followingHistoryProgressGap),
ClipRRect(
borderRadius: BorderRadius.circular(
AppDimens.followingHistoryProgressRadius,
),
child: LinearProgressIndicator(
key: Key('following-progress-$dramaId'),
value: progress!.clamp(0.0, 1.0).toDouble(),
minHeight: AppDimens.followingHistoryProgressHeight,
backgroundColor: AppColors.inactiveTrack,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.primary,
),
),
),
if (progressLabel != null) ...[
const SizedBox(height: AppDimens.followingHistoryLabelGap),
Text(
progressLabel!,
key: Key('following-history-percent-$dramaId'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: AppDimens.followingSubtitleFont,
),
),
],
],
],
),
),
),
if (editing)
Positioned(
top: 0,
right: 0,
child: GestureDetector(
key: Key('following-history-remove-$dramaId'),
onTap: onRemove,
child: const CircleAvatar(
radius: 12,
backgroundColor: AppColors.contentChip,
child: Icon(
Icons.remove_rounded,
color: AppColors.textPrimary,
size: 17,
),
),
),
),
],
);
}
Widget _continueButton(AppLocalizations l10n) {
return SizedBox(
height: AppDimens.followingHistoryActionHeight,
child: OutlinedButton(
key: Key('following-history-continue-$dramaId'),
onPressed: onContinue,
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.textPrimary,
side: const BorderSide(color: AppColors.textPrimary),
padding: const EdgeInsets.symmetric(horizontal: 10),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontSize: AppDimens.followingSubtitleFont,
fontWeight: FontWeight.w400,
),
),
child: Text(l10n.continueWatching),
),
);
}
}
+21 -6
View File
@@ -19,6 +19,7 @@ import '../../core/widgets/empty_state.dart';
import '../../l10n/app_localizations.dart';
import '../player/open_player.dart';
import 'following_grid_card.dart';
import 'following_history_card.dart';
import 'following_paged_grid.dart';
enum FollowingTab { favorites, history }
@@ -94,7 +95,7 @@ class _FollowingPageState extends State<FollowingPage> {
onGoHome: widget.onGoHome,
onListEmptied: _exitEditingIfEmpty,
),
_HistoryGrid(
_HistoryList(
controller: _history,
editing: _editing,
scrollController: _historyScroll,
@@ -301,8 +302,8 @@ class _FavoritesGrid extends StatelessWidget {
}
}
class _HistoryGrid extends StatelessWidget {
const _HistoryGrid({
class _HistoryList extends StatelessWidget {
const _HistoryList({
required this.controller,
required this.editing,
required this.scrollController,
@@ -327,6 +328,7 @@ class _HistoryGrid extends StatelessWidget {
loadingMore: controller.loadingMore,
hasMore: controller.hasMore,
hasItems: controller.records.isNotEmpty,
singleColumn: true,
errorMessage: controller.error == null
? null
: errorText(l10n, controller.error, l10n.progressError),
@@ -345,15 +347,28 @@ class _HistoryGrid extends StatelessWidget {
itemBuilder: (context, index) {
final record = controller.records[index];
final progress = record.progress.fraction;
return FollowingGridCard(
final hasProgress = record.progress.duration > Duration.zero;
final watchedPercent = (progress * 100)
.round()
.clamp(0, 100)
.toInt();
void openRecord() {
unawaited(_openHistory(context, controller, record));
}
return FollowingHistoryCard(
dramaId: record.dramaId,
title: _historyTitle(l10n, record),
posterUrl: record.displayCover,
viewCount: record.drama?.viewCount ?? record.progress.playCount,
progress: progress > 0 ? progress : null,
progress: hasProgress ? progress : null,
progressLabel: hasProgress
? l10n.watchedPercent(watchedPercent)
: null,
editing: editing,
subtitle: _historySubtitle(l10n, record),
onTap: () => unawaited(_openHistory(context, controller, record)),
onTap: openRecord,
onContinue: openRecord,
onRemove: () async {
if (await _confirmRemove(context) &&
await controller.remove(record.dramaId) &&
+15 -6
View File
@@ -25,6 +25,7 @@ class FollowingPagedGrid extends StatelessWidget {
required this.onRefresh,
required this.onLoadMore,
this.showInitialLoading = true,
this.singleColumn = false,
this.errorMessage,
this.loadMoreError,
});
@@ -42,6 +43,7 @@ class FollowingPagedGrid extends StatelessWidget {
final String? errorMessage;
final String? loadMoreError;
final bool showInitialLoading;
final bool singleColumn;
final Future<void> Function() onRefresh;
final Future<void> Function() onLoadMore;
@@ -58,6 +60,18 @@ class FollowingPagedGrid extends StatelessWidget {
final itemExtent =
itemWidth / AppDimens.cardCoverAspectRatio +
AppDimens.followingCardTextExtent;
final gridDelegate = singleColumn
? const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
mainAxisSpacing: AppDimens.followingHistoryRowGap,
mainAxisExtent: AppDimens.followingHistoryCoverHeight,
)
: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: AppDimens.followingGridGap,
mainAxisSpacing: AppDimens.followingGridRowGap,
mainAxisExtent: itemExtent,
);
return NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: AppPullToRefresh(
@@ -106,12 +120,7 @@ class FollowingPagedGrid extends StatelessWidget {
AppDimens.spaceSm,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: AppDimens.followingGridGap,
mainAxisSpacing: AppDimens.followingGridRowGap,
mainAxisExtent: itemExtent,
),
gridDelegate: gridDelegate,
delegate: SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,