merge: 精简底部信息并优化进度拖动

This commit is contained in:
Zhou Weidong committed 2026-10-22 19:55:16 +08:00
commit 76c4f1d411
16 files changed
+129 -151

No files matched your search

+7
View File
@@ -1177,3 +1177,10 @@ $ flutter test
- 新增三语“观看全集”文案;按当前流程未执行 analyze/test。 - 新增三语“观看全集”文案;按当前流程未执行 analyze/test。
- 榜单行支持可选排名:有名次显示 `TOPn·热播榜`,无名次回退“热播榜”;热度值改用 `w` 格式。 - 榜单行支持可选排名:有名次显示 `TOPn·热播榜`,无名次回退“热播榜”;热度值改用 `w` 格式。
- 题材改为空格分隔,集数固定右侧单行;右侧操作栏改为距屏幕边缘 8,底部位置不变。 - 题材改为空格分隔,集数固定右侧单行;右侧操作栏改为距屏幕边缘 8,底部位置不变。
## 2026-10-22 推荐页底部信息二次修正
- 按最新反馈移除整行榜单与热度胶囊,信息顺序收敛为标题、剧名、标签、简介、观看全集。
- 剧名 19/w600,箭头 20 白色并紧邻剧名;标签行改为 13/w600 亮灰内联布局。
- 简介「展开/收起」沿用现有三语文案;进度条热区恢复 24 并接入拖动自动清屏与预览卡片。
- 拖动期间隐藏顶部导航、操作栏、文字及根底栏,松手恢复;按当前流程未执行 analyze/test。
+1
View File
@@ -24,6 +24,7 @@ abstract final class AppColors {
static const overlayMedium = Color(0x99000000); static const overlayMedium = Color(0x99000000);
static const headerScrim = Color(0x59000000); static const headerScrim = Color(0x59000000);
static const overlaySecondary = Color(0xFFC7C7CC); static const overlaySecondary = Color(0xFFC7C7CC);
static const overlayLabel = Color(0xFFE6E6EB);
/// 主操作、选中态、播放进度(品牌橙)。 /// 主操作、选中态、播放进度(品牌橙)。
static const blue = Color(0xFFFF6B2C); static const blue = Color(0xFFFF6B2C);
-3
View File
@@ -749,8 +749,5 @@
"watchedToEpisode": "Watched to Episode {episode}", "watchedToEpisode": "Watched to Episode {episode}",
"@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } },
"homeWatchAll": "Watch all", "homeWatchAll": "Watch all",
"hotChartLabel": "Trending",
"hotChartRanked": "TOP{rank} · Trending",
"@hotChartRanked": { "placeholders": { "rank": { "type": "int" } } },
"confirmRemove": "Remove this item?" "confirmRemove": "Remove this item?"
} }
-3
View File
@@ -706,8 +706,5 @@
"watchedToEpisode": "Ditonton hingga Episode {episode}", "watchedToEpisode": "Ditonton hingga Episode {episode}",
"@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } },
"homeWatchAll": "Tonton semua", "homeWatchAll": "Tonton semua",
"hotChartLabel": "Peringkat populer",
"hotChartRanked": "TOP{rank} · Populer",
"@hotChartRanked": { "placeholders": { "rank": { "type": "int" } } },
"confirmRemove": "Hapus item ini?" "confirmRemove": "Hapus item ini?"
} }
-12
View File
@@ -3262,18 +3262,6 @@ abstract class AppLocalizations {
/// **'Watch all'** /// **'Watch all'**
String get homeWatchAll; String get homeWatchAll;
/// No description provided for @hotChartLabel.
///
/// In en, this message translates to:
/// **'Trending'**
String get hotChartLabel;
/// No description provided for @hotChartRanked.
///
/// In en, this message translates to:
/// **'TOP{rank} · Trending'**
String hotChartRanked(int rank);
/// No description provided for @confirmRemove. /// No description provided for @confirmRemove.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
-8
View File
@@ -1702,14 +1702,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get homeWatchAll => 'Watch all'; String get homeWatchAll => 'Watch all';
@override
String get hotChartLabel => 'Trending';
@override
String hotChartRanked(int rank) {
return 'TOP$rank · Trending';
}
@override @override
String get confirmRemove => 'Remove this item?'; String get confirmRemove => 'Remove this item?';
} }
-8
View File
@@ -1704,14 +1704,6 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get homeWatchAll => 'Tonton semua'; String get homeWatchAll => 'Tonton semua';
@override
String get hotChartLabel => 'Peringkat populer';
@override
String hotChartRanked(int rank) {
return 'TOP$rank · Populer';
}
@override @override
String get confirmRemove => 'Hapus item ini?'; String get confirmRemove => 'Hapus item ini?';
} }
-8
View File
@@ -1678,14 +1678,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get homeWatchAll => '观看全集'; String get homeWatchAll => '观看全集';
@override
String get hotChartLabel => '热播榜';
@override
String hotChartRanked(int rank) {
return 'TOP$rank·热播榜';
}
@override @override
String get confirmRemove => '确定移除这条内容吗?'; String get confirmRemove => '确定移除这条内容吗?';
} }
-3
View File
@@ -668,8 +668,5 @@
"watchedToEpisode": "已看至 第 {episode} 集", "watchedToEpisode": "已看至 第 {episode} 集",
"@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } },
"homeWatchAll": "观看全集", "homeWatchAll": "观看全集",
"hotChartLabel": "热播榜",
"hotChartRanked": "TOP{rank}·热播榜",
"@hotChartRanked": { "placeholders": { "rank": { "type": "int" } } },
"confirmRemove": "确定移除这条内容吗?" "confirmRemove": "确定移除这条内容吗?"
} }
+1
View File
@@ -288,6 +288,7 @@ class _HomeTabsState extends State<_HomeTabs> {
active: logic.tab == 1, active: logic.tab == 1,
onSearch: () => _openSearch(context), onSearch: () => _openSearch(context),
onOpenFilter: () => _pushPage(context, const FilterPage()), onOpenFilter: () => _pushPage(context, const FilterPage()),
onScrubVisibilityChanged: logic.onImmersiveChanged,
); );
} }
+13 -1
View File
@@ -69,6 +69,7 @@ class PlayerPage extends StatefulWidget {
this.showHomeFeedHeader = true, this.showHomeFeedHeader = true,
this.onDramaChanged, this.onDramaChanged,
this.onImmersiveChanged, this.onImmersiveChanged,
this.onScrubVisibilityChanged,
this.onOpenFullPlayer, this.onOpenFullPlayer,
this.onRefreshFeed, this.onRefreshFeed,
this.onLoadMore, this.onLoadMore,
@@ -94,6 +95,7 @@ class PlayerPage extends StatefulWidget {
final bool showHomeFeedHeader; final bool showHomeFeedHeader;
final ValueChanged<Drama>? onDramaChanged; final ValueChanged<Drama>? onDramaChanged;
final ValueChanged<bool>? onImmersiveChanged; final ValueChanged<bool>? onImmersiveChanged;
final ValueChanged<bool>? onScrubVisibilityChanged;
final Future<void> Function(Drama drama, Episode episode)? onOpenFullPlayer; final Future<void> Function(Drama drama, Episode episode)? onOpenFullPlayer;
final Future<bool> Function()? onRefreshFeed; final Future<bool> Function()? onRefreshFeed;
final Future<void> Function()? onLoadMore; final Future<void> Function()? onLoadMore;
@@ -114,6 +116,7 @@ class _PlayerPageState extends State<PlayerPage>
bool _chromeVisible = true; bool _chromeVisible = true;
double _playbackSpeed = 1.0; double _playbackSpeed = 1.0;
bool _boosting = false; bool _boosting = false;
bool _scrubbing = false;
double? _speedBeforeBoost; double? _speedBeforeBoost;
bool? _chromeBeforeBoost; bool? _chromeBeforeBoost;
@@ -296,6 +299,12 @@ class _PlayerPageState extends State<PlayerPage>
unawaited(logic.setPlaybackSpeed(speed)); unawaited(logic.setPlaybackSpeed(speed));
} }
void _setScrubbing(bool value) {
if (_scrubbing == value) return;
setState(() => _scrubbing = value);
widget.onScrubVisibilityChanged?.call(value);
}
/// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。 /// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。
void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) { void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) {
setState(() { setState(() {
@@ -364,6 +373,7 @@ class _PlayerPageState extends State<PlayerPage>
content, content,
if (widget.homeMode && if (widget.homeMode &&
widget.showHomeFeedHeader && widget.showHomeFeedHeader &&
!_scrubbing &&
!_boosting && !_boosting &&
!logic.pip && !logic.pip &&
!logic.pipPreparing) !logic.pipPreparing)
@@ -624,6 +634,7 @@ class _PlayerPageState extends State<PlayerPage>
onEpisodes: () => logic.showEpisodes(context), onEpisodes: () => logic.showEpisodes(context),
onMore: _openCurrentMore, onMore: _openCurrentMore,
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null, onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
onScrubVisibilityChanged: _setScrubbing,
onSeek: ready onSeek: ready
? (position) => logic.seekTo(context, videoPort!, position) ? (position) => logic.seekTo(context, videoPort!, position)
: null, : null,
@@ -695,7 +706,7 @@ class _PlayerPageState extends State<PlayerPage>
? null ? null
: () => _openUnlockSheet(context, index, episode), : () => _openUnlockSheet(context, index, episode),
), ),
if (widget.homeMode && !_chromeVisible && !_boosting) if (widget.homeMode && !_chromeVisible && !_boosting && !_scrubbing)
Positioned( Positioned(
right: 12, right: 12,
bottom: 56, bottom: 56,
@@ -706,6 +717,7 @@ class _PlayerPageState extends State<PlayerPage>
), ),
), ),
if (widget.homeMode && if (widget.homeMode &&
!_scrubbing &&
!logic.chromeHidden && !logic.chromeHidden &&
selected && selected &&
!logic.session.userWantsPlay) !logic.session.userWantsPlay)
@@ -19,7 +19,7 @@ import 'seek_progress_bar.dart';
part 'home_feed_overlay_parts.dart'; part 'home_feed_overlay_parts.dart';
const double _progressHeight = 2; const double _progressHeight = 2;
const double _progressHitHeight = 12; const double _progressHitHeight = 24;
const double _pageHorizontal = 14; const double _pageHorizontal = 14;
const double _railRight = 8; const double _railRight = 8;
const double _railGap = 12; const double _railGap = 12;
@@ -54,10 +54,11 @@ class HomeFeedOverlay extends StatefulWidget {
required this.onMore, required this.onMore,
this.likeCount, this.likeCount,
this.favoriteCount, this.favoriteCount,
this.rankingRank,
this.commentCount, this.commentCount,
this.authorNote = '', this.authorNote = '',
this.onScrubStart, this.onScrubStart,
this.onScrubEnd,
this.onScrubVisibilityChanged,
this.onSeek, this.onSeek,
this.thumbnailService, this.thumbnailService,
this.spriteAxisService, this.spriteAxisService,
@@ -71,7 +72,6 @@ class HomeFeedOverlay extends StatefulWidget {
final bool liked; final bool liked;
final int? likeCount; final int? likeCount;
final int? favoriteCount; final int? favoriteCount;
final int? rankingRank;
final int? commentCount; final int? commentCount;
final bool saved; final bool saved;
final String authorNote; final String authorNote;
@@ -89,6 +89,8 @@ class HomeFeedOverlay extends StatefulWidget {
final VoidCallback onBoostEnd; final VoidCallback onBoostEnd;
final VoidCallback onMore; final VoidCallback onMore;
final VoidCallback? onScrubStart; final VoidCallback? onScrubStart;
final VoidCallback? onScrubEnd;
final ValueChanged<bool>? onScrubVisibilityChanged;
final ValueChanged<Duration>? onSeek; final ValueChanged<Duration>? onSeek;
final PlaybackThumbnailService? thumbnailService; final PlaybackThumbnailService? thumbnailService;
final SpriteThumbnailAxisService? spriteAxisService; final SpriteThumbnailAxisService? spriteAxisService;
@@ -98,6 +100,8 @@ class HomeFeedOverlay extends StatefulWidget {
} }
class _HomeFeedOverlayState extends State<HomeFeedOverlay> { class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
bool _scrubbing = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
@@ -123,7 +127,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
), ),
if (widget.boosting) if (widget.boosting)
Positioned(left: 0, right: 0, bottom: 20, child: _boostHint()) Positioned(left: 0, right: 0, bottom: 20, child: _boostHint())
else if (widget.visible) else if (widget.visible && !_scrubbing)
Positioned( Positioned(
left: _pageHorizontal, left: _pageHorizontal,
right: _railRight, right: _railRight,
@@ -149,7 +153,8 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
locked: widget.locked, locked: widget.locked,
height: _progressHeight, height: _progressHeight,
hitHeight: _progressHitHeight, hitHeight: _progressHitHeight,
onScrubStart: widget.onScrubStart, onScrubStart: _startScrub,
onScrubEnd: _endScrub,
onSeek: widget.onSeek, onSeek: widget.onSeek,
thumbnailService: widget.thumbnailService, thumbnailService: widget.thumbnailService,
spriteAxisService: widget.spriteAxisService, spriteAxisService: widget.spriteAxisService,
@@ -163,6 +168,18 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
); );
} }
void _startScrub() {
if (!_scrubbing) setState(() => _scrubbing = true);
widget.onScrubVisibilityChanged?.call(true);
widget.onScrubStart?.call();
}
void _endScrub() {
if (_scrubbing) setState(() => _scrubbing = false);
widget.onScrubVisibilityChanged?.call(false);
widget.onScrubEnd?.call();
}
void _handleLongPressStart(LongPressStartDetails details, double width) { void _handleLongPressStart(LongPressStartDetails details, double width) {
final third = width / 3; final third = width / 3;
final dx = details.localPosition.dx; final dx = details.localPosition.dx;
@@ -220,7 +237,6 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
episode: widget.episode, episode: widget.episode,
onDetail: widget.onDetail, onDetail: widget.onDetail,
onOpenFullPlayer: widget.onOpenFullPlayer, onOpenFullPlayer: widget.onOpenFullPlayer,
rankingRank: widget.rankingRank,
); );
} }
} }
@@ -16,6 +16,7 @@ class _FeedProgressBar extends StatefulWidget {
required this.height, required this.height,
required this.hitHeight, required this.hitHeight,
this.onScrubStart, this.onScrubStart,
this.onScrubEnd,
this.onSeek, this.onSeek,
this.thumbnailService, this.thumbnailService,
this.spriteAxisService, this.spriteAxisService,
@@ -29,6 +30,7 @@ class _FeedProgressBar extends StatefulWidget {
final double height; final double height;
final double hitHeight; final double hitHeight;
final VoidCallback? onScrubStart; final VoidCallback? onScrubStart;
final VoidCallback? onScrubEnd;
final ValueChanged<Duration>? onSeek; final ValueChanged<Duration>? onSeek;
final PlaybackThumbnailService? thumbnailService; final PlaybackThumbnailService? thumbnailService;
final SpriteThumbnailAxisService? spriteAxisService; final SpriteThumbnailAxisService? spriteAxisService;
@@ -137,8 +139,18 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
onHorizontalDragUpdate: interactive onHorizontalDragUpdate: interactive
? (details) => _update(details.localPosition.dx / width) ? (details) => _update(details.localPosition.dx / width)
: null, : null,
onHorizontalDragEnd: interactive ? (_) => _commit(_preview) : null, onHorizontalDragEnd: interactive
onHorizontalDragCancel: interactive ? _cancelPreview : null, ? (_) {
widget.onScrubEnd?.call();
_commit(_preview);
}
: null,
onHorizontalDragCancel: interactive
? () {
widget.onScrubEnd?.call();
_cancelPreview();
}
: null,
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -10,9 +10,8 @@ import '../../../l10n/app_localizations.dart';
import '../../home/widgets/home_drama_classification.dart'; import '../../home/widgets/home_drama_classification.dart';
const double _headlineFont = 26; const double _headlineFont = 26;
const double _seriesFont = 17; const double _seriesFont = 19;
const double _metaFont = 12; const double _tagFont = 13;
const double _tagFont = 12;
const double _synopsisFont = 13; const double _synopsisFont = 13;
const double _watchAllHeight = 44; const double _watchAllHeight = 44;
const double _actionGap = 28; const double _actionGap = 28;
@@ -30,14 +29,12 @@ class HomeFeedInfoPanel extends StatefulWidget {
required this.episode, required this.episode,
required this.onDetail, required this.onDetail,
required this.onOpenFullPlayer, required this.onOpenFullPlayer,
this.rankingRank,
}); });
final Drama drama; final Drama drama;
final Episode episode; final Episode episode;
final VoidCallback onDetail; final VoidCallback onDetail;
final VoidCallback onOpenFullPlayer; final VoidCallback onOpenFullPlayer;
final int? rankingRank;
@override @override
State<HomeFeedInfoPanel> createState() => _HomeFeedInfoPanelState(); State<HomeFeedInfoPanel> createState() => _HomeFeedInfoPanelState();
@@ -73,15 +70,14 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_metaPills(l10n),
if (_hasMeta()) const SizedBox(height: 8),
GestureDetector( GestureDetector(
key: const Key('home-feed-series-name'), key: const Key('home-feed-series-name'),
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: widget.onDetail, onTap: widget.onDetail,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Flexible(
child: Text( child: Text(
dramaTitle, dramaTitle,
maxLines: 1, maxLines: 1,
@@ -94,10 +90,11 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
), ),
), ),
), ),
const SizedBox(width: 4),
const Icon( const Icon(
Icons.chevron_right_rounded, Icons.chevron_right_rounded,
size: 16, size: 20,
color: AppColors.overlaySecondary, color: AppColors.textPrimary,
), ),
], ],
), ),
@@ -114,81 +111,6 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
); );
} }
bool _hasMeta() {
return isHotDrama(widget.drama) || widget.drama.viewCount > 0;
}
Widget _metaPills(AppLocalizations l10n) {
final hot = isHotDrama(widget.drama);
final views = widget.drama.viewCount;
final rank = widget.rankingRank;
if (!hot && views <= 0) return const SizedBox.shrink();
return Wrap(
key: const Key('home-feed-meta-pills'),
spacing: 6,
runSpacing: 4,
children: [
if (hot)
_pill(
key: const Key('home-feed-ranking-pill'),
background: AppColors.error,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.local_fire_department_rounded,
size: 12,
color: AppColors.textPrimary,
),
const SizedBox(width: 2),
Text(
rank != null && rank > 0
? l10n.hotChartRanked(rank)
: l10n.hotChartLabel,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: _metaFont,
fontWeight: FontWeight.w600,
),
),
],
),
),
if (views > 0)
_pill(
key: const Key('home-feed-view-pill'),
background: AppColors.overlaySoft,
child: Text(
formatPlayCount(views),
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: _metaFont,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _pill({
required Key key,
required Color background,
required Widget child,
}) {
return Container(
key: key,
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 8),
alignment: Alignment.center,
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(11),
),
child: child,
);
}
Widget _tagMetaRow(AppLocalizations l10n, List<String> genres) { Widget _tagMetaRow(AppLocalizations l10n, List<String> genres) {
return Row( return Row(
key: const Key('home-feed-tags'), key: const Key('home-feed-tags'),
@@ -210,20 +132,21 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
), ),
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 8),
], ],
Expanded( Flexible(
fit: FlexFit.loose,
child: Text( child: Text(
genres.join(' '), genres.join(' '),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.overlaySecondary, color: AppColors.overlayLabel,
fontSize: _tagFont, fontSize: _tagFont,
), ),
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 8),
Text( Text(
l10n.favoriteEpisodeProgress( l10n.favoriteEpisodeProgress(
widget.episode.number, widget.episode.number,
@@ -233,8 +156,8 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
maxLines: 1, maxLines: 1,
softWrap: false, softWrap: false,
style: const TextStyle( style: const TextStyle(
color: AppColors.overlaySecondary, color: AppColors.overlayLabel,
fontSize: _metaFont, fontSize: _tagFont,
), ),
), ),
], ],
@@ -298,7 +221,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
bottom: 0, bottom: 0,
child: _synopsisToggle( child: _synopsisToggle(
key: const Key('home-feed-synopsis-more'), key: const Key('home-feed-synopsis-more'),
label: l10n.more, label: l10n.expandSynopsis,
), ),
), ),
], ],
+11
View File
@@ -28,12 +28,14 @@ class RecommendPage extends StatefulWidget {
required this.active, required this.active,
required this.onSearch, required this.onSearch,
required this.onOpenFilter, required this.onOpenFilter,
required this.onScrubVisibilityChanged,
}); });
final HomeFeedController feedController; final HomeFeedController feedController;
final bool active; final bool active;
final VoidCallback onSearch; final VoidCallback onSearch;
final VoidCallback onOpenFilter; final VoidCallback onOpenFilter;
final ValueChanged<bool> onScrubVisibilityChanged;
@override @override
State<RecommendPage> createState() => _RecommendPageState(); State<RecommendPage> createState() => _RecommendPageState();
@@ -41,6 +43,7 @@ class RecommendPage extends StatefulWidget {
class _RecommendPageState extends State<RecommendPage> { class _RecommendPageState extends State<RecommendPage> {
RecommendInnerTab _tab = RecommendInnerTab.feed; RecommendInnerTab _tab = RecommendInnerTab.feed;
bool _scrubbing = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -77,6 +80,7 @@ class _RecommendPageState extends State<RecommendPage> {
), ),
), ),
), ),
if (!_scrubbing)
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
@@ -136,9 +140,16 @@ class _RecommendPageState extends State<RecommendPage> {
_openDrama(drama, episodeId: episode.id), _openDrama(drama, episodeId: episode.id),
onRefreshFeed: widget.feedController.refreshCurrent, onRefreshFeed: widget.feedController.refreshCurrent,
onLoadMore: widget.feedController.loadMore, onLoadMore: widget.feedController.loadMore,
onScrubVisibilityChanged: _setScrubbing,
); );
} }
void _setScrubbing(bool value) {
if (_scrubbing == value) return;
setState(() => _scrubbing = value);
widget.onScrubVisibilityChanged(value);
}
Widget _withTopNavSpace(Widget child) { Widget _withTopNavSpace(Widget child) {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
+45 -5
View File
@@ -23,6 +23,7 @@ import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/home/home_feed_channel.dart'; import 'package:fruitshorts/pages/home/home_feed_channel.dart';
import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart';
import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart';
import 'package:fruitshorts/pages/player/widgets/scrub_preview_card.dart';
import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:fruitshorts/pages/player/player_view.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@@ -355,6 +356,23 @@ void main() {
'Ignored Episode Title', 'Ignored Episode Title',
); );
expect(find.text('Feed Drama'), findsOneWidget); expect(find.text('Feed Drama'), findsOneWidget);
final seriesTitle = tester.widget<Text>(find.text('Feed Drama'));
expect(seriesTitle.style?.fontSize, 19);
expect(seriesTitle.style?.fontWeight, FontWeight.w600);
expect(seriesTitle.style?.color, AppColors.textPrimary);
final seriesChevron = tester.widget<Icon>(
find.descendant(
of: find.byKey(const Key('home-feed-series-name')),
matching: find.byIcon(Icons.chevron_right_rounded),
),
);
expect(seriesChevron.size, 20);
expect(seriesChevron.color, AppColors.textPrimary);
expect(
tester.getTopLeft(find.byIcon(Icons.chevron_right_rounded)).dx -
tester.getTopRight(find.text('Feed Drama')).dx,
4,
);
expect(find.byKey(const Key('home-feed-info-card')), findsNothing); expect(find.byKey(const Key('home-feed-info-card')), findsNothing);
expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget);
expect(find.byKey(const Key('home-feed-tags')), findsOneWidget); expect(find.byKey(const Key('home-feed-tags')), findsOneWidget);
@@ -371,16 +389,27 @@ void main() {
.data, .data,
'Episode 2 / 8 total', 'Episode 2 / 8 total',
); );
expect(find.textContaining('41.9w'), findsOneWidget); expect(find.byKey(const Key('home-feed-meta-pills')), findsNothing);
expect(find.byKey(const Key('home-feed-ranking-pill')), findsOneWidget); expect(find.byKey(const Key('home-feed-ranking-pill')), findsNothing);
expect(find.byKey(const Key('home-feed-view-pill')), findsOneWidget); expect(find.byKey(const Key('home-feed-view-pill')), findsNothing);
expect(find.text('Trending'), findsOneWidget);
expect(find.text('Romance Urban'), findsOneWidget); expect(find.text('Romance Urban'), findsOneWidget);
final genreText = tester.widget<Text>(find.text('Romance Urban'));
expect(genreText.style?.fontSize, 13);
expect(genreText.style?.color, AppColors.overlayLabel);
final episodeProgress = tester.widget<Text>( final episodeProgress = tester.widget<Text>(
find.byKey(const Key('home-feed-episode-progress')), find.byKey(const Key('home-feed-episode-progress')),
); );
expect(episodeProgress.maxLines, 1); expect(episodeProgress.maxLines, 1);
expect(episodeProgress.softWrap, isFalse); expect(episodeProgress.softWrap, isFalse);
expect(episodeProgress.style?.fontSize, 13);
expect(episodeProgress.style?.color, AppColors.overlayLabel);
expect(
tester
.getTopLeft(find.byKey(const Key('home-feed-episode-progress')))
.dx -
tester.getTopRight(find.text('Romance Urban')).dx,
8,
);
expect( expect(
tester.widget<Text>(find.byKey(const Key('rail-like-count'))).data, tester.widget<Text>(find.byKey(const Key('rail-like-count'))).data,
'1006', '1006',
@@ -567,6 +596,9 @@ void main() {
testWidgets('should_seek_and_open_player_from_feed_actions', (tester) async { testWidgets('should_seek_and_open_player_from_feed_actions', (tester) async {
var opened = 0; var opened = 0;
Duration? seeked; Duration? seeked;
final scrubVisibility = <bool>[];
final thumbnailService = _nullThumbnailService();
addTearDown(thumbnailService.dispose);
final controller = ValueNotifier<VideoPlayerValue>( final controller = ValueNotifier<VideoPlayerValue>(
VideoPlayerValue( VideoPlayerValue(
duration: const Duration(seconds: 100), duration: const Duration(seconds: 100),
@@ -616,7 +648,9 @@ void main() {
onOpenFullPlayer: () => opened++, onOpenFullPlayer: () => opened++,
onMore: () {}, onMore: () {},
onScrubStart: () {}, onScrubStart: () {},
onScrubVisibilityChanged: scrubVisibility.add,
onSeek: (value) => seeked = value, onSeek: (value) => seeked = value,
thumbnailService: thumbnailService,
), ),
), ),
), ),
@@ -647,7 +681,7 @@ void main() {
); );
expect( expect(
tester.getSize(find.byKey(const Key('home-feed-progress'))).height, tester.getSize(find.byKey(const Key('home-feed-progress'))).height,
12, greaterThanOrEqualTo(24),
); );
expect( expect(
tester tester
@@ -668,6 +702,10 @@ void main() {
); );
await gesture.moveBy(const Offset(120, 0)); await gesture.moveBy(const Offset(120, 0));
await tester.pump(); await tester.pump();
expect(scrubVisibility.last, isTrue);
expect(find.byKey(const Key('home-feed-title')), findsNothing);
expect(find.byKey(const Key('rail-save')), findsNothing);
expect(find.byType(ScrubPreviewCard), findsOneWidget);
expect( expect(
tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))), tester.getSize(find.byKey(const Key('home-feed-progress-thumb'))),
const Size(14, 14), const Size(14, 14),
@@ -691,6 +729,8 @@ void main() {
); );
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(scrubVisibility.last, isFalse);
expect(find.byKey(const Key('home-feed-title')), findsOneWidget);
expect(seeked, isNotNull); expect(seeked, isNotNull);
expect(seeked!.inSeconds, greaterThan(0)); expect(seeked!.inSeconds, greaterThan(0));
expect( expect(