merge: 精简底部信息并优化进度拖动
This commit is contained in:
commit
76c4f1d411
16 files changed
+129
-151
No files matched your search
@@ -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。
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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?"
|
||||||
}
|
}
|
||||||
@@ -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?"
|
||||||
}
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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?';
|
||||||
}
|
}
|
||||||
@@ -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?';
|
||||||
}
|
}
|
||||||
@@ -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 => '确定移除这条内容吗?';
|
||||||
}
|
}
|
||||||
@@ -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": "确定移除这条内容吗?"
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in new issue
Block a user