style(player): 播放页与首页浮层的控件样式统一
播放页顶部按钮与选集条去掉底色、进度条不再画滑块圆点并压到最底部; 剧名与简介收在互动栏左侧,清屏后只留右下角退出按钮。 首页观看条与进度条共用 20pt 页边距,进度槽底色与播放页一致。
This commit is contained in:
1 parent
d5bcda54d3
commit
543d0aadd5
12 files changed
+298
-297
No files matched your search
@@ -4,6 +4,10 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
const List<Shadow> _iconShadows = [
|
||||
Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)),
|
||||
];
|
||||
|
||||
/// Shared floating action used to leave clear-screen mode.
|
||||
class ExitClearScreenButton extends StatelessWidget {
|
||||
const ExitClearScreenButton({
|
||||
@@ -25,11 +29,15 @@ class ExitClearScreenButton extends StatelessWidget {
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: AppColors.controlScrim,
|
||||
backgroundColor: AppColors.transparent,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
icon: const Icon(Icons.fullscreen_exit_rounded, size: 18),
|
||||
icon: const Icon(
|
||||
Icons.fullscreen_exit_rounded,
|
||||
size: 22,
|
||||
shadows: _iconShadows,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,16 @@ import 'scrub_preview_card.dart';
|
||||
part 'home_feed_overlay_parts.dart';
|
||||
|
||||
const double _progressHeight = 2;
|
||||
const double _progressHitHeight = 32;
|
||||
|
||||
/// 进度条触控带高度:没有滑块圆点后收到 16pt,够手指按住这条线拖动。
|
||||
const double _progressHitHeight = 16;
|
||||
const double _pageHorizontal = 20;
|
||||
const double _railRight = 14;
|
||||
const double _railGap = 12;
|
||||
// 观看条高 44;2pt 进度线视觉上沿距观看条顶约 7pt(线中心距进度条盒底 8)。
|
||||
// 信息面板底边取 44+16=60,使简介↔进度线、进度线↔观看条两侧间距均为 7pt。
|
||||
const double _bottomInfoInset = homeFeedWatchAllHeight + 16;
|
||||
// 进度线贴着屏幕底部(线心距底 4pt),观看条底边只留 8pt,视觉上就是线上一根条。
|
||||
const double _watchAllBottomInset = 8;
|
||||
// 信息面板底边直接贴住观看条顶沿,中间不留缝。
|
||||
const double _bottomInfoInset = _watchAllBottomInset + homeFeedWatchAllHeight;
|
||||
const double _railBottomOffset = 40;
|
||||
const Duration _overlayFadeDuration = Duration(milliseconds: 120);
|
||||
const List<Shadow> _boostTextShadows = [
|
||||
@@ -152,7 +155,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: homeFeedWatchAllHeight + 12,
|
||||
bottom: _watchAllBottomInset + homeFeedWatchAllHeight + 12,
|
||||
child: AnimatedOpacity(
|
||||
duration: _overlayFadeDuration,
|
||||
opacity: widget.boosting ? 1 : 0,
|
||||
@@ -162,10 +165,30 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// 观看条在下、进度条在上:进度条 32pt 触控盒要盖住观看条下沿的空白,
|
||||
// 否则拖拽 seek 的起点会被观看条吞掉。两块的可见区域(文字/进度线)互不遮挡。
|
||||
Positioned(
|
||||
left: _pageHorizontal,
|
||||
right: _pageHorizontal,
|
||||
bottom: homeFeedWatchAllHeight,
|
||||
bottom: _watchAllBottomInset,
|
||||
child: AnimatedOpacity(
|
||||
duration: _overlayFadeDuration,
|
||||
opacity: widget.visible && !_scrubbing && !widget.boosting
|
||||
? 1
|
||||
: 0,
|
||||
child: IgnorePointer(
|
||||
ignoring: !widget.visible || _scrubbing || widget.boosting,
|
||||
child: HomeFeedWatchAllBar(
|
||||
drama: widget.drama,
|
||||
onTap: widget.onOpenFullPlayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: _pageHorizontal,
|
||||
right: _pageHorizontal,
|
||||
bottom: 0,
|
||||
child: _FeedProgressBar(
|
||||
controller: widget.controller,
|
||||
locked: widget.locked,
|
||||
@@ -182,27 +205,7 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
onSeek: widget.onSeek,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedOpacity(
|
||||
duration: _overlayFadeDuration,
|
||||
opacity: widget.visible && !_scrubbing && !widget.boosting
|
||||
? 1
|
||||
: 0,
|
||||
child: IgnorePointer(
|
||||
ignoring: !widget.visible || _scrubbing || widget.boosting,
|
||||
child: HomeFeedWatchAllBar(
|
||||
drama: widget.drama,
|
||||
onTap: widget.onOpenFullPlayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 信息面板放在最后(最上层):进度条 32px 触控盒向上伸进底部避让区,
|
||||
// 若面板在前,简介/展开按钮底部的触控留白会被进度条吞掉点击。
|
||||
// 视觉上避让条带为空,提层不改变观感。
|
||||
// 信息面板放在最后(最上层),底部避让区永远优先响应简介/展开按钮的点击。
|
||||
Positioned(
|
||||
left: _pageHorizontal,
|
||||
right: _railRight,
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
part of 'home_feed_overlay.dart';
|
||||
|
||||
const double _thumbSize = 6;
|
||||
const double _thumbActiveSize = 14;
|
||||
const double _trackActiveHeight = 6;
|
||||
const double _lineCenterFromBottom = 8;
|
||||
// 进度线贴住屏幕底部(线心距底 4pt);播放进度只靠蓝色已播段表达,不画滑块圆点。
|
||||
const double _lineCenterFromBottom = 4;
|
||||
const double _timeBubbleWidth = 96;
|
||||
const double _timeBubbleMargin = 12;
|
||||
const Key _inactiveTrackKey = Key('home-feed-progress-inactive');
|
||||
@@ -305,7 +304,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
|
||||
height: widget.height,
|
||||
child: ColoredBox(
|
||||
key: _inactiveTrackKey,
|
||||
color: AppColors.border,
|
||||
color: AppColors.inactiveTrack,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -345,20 +344,11 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
|
||||
Widget _track(double fraction, double width) {
|
||||
final dragging = _preview != null;
|
||||
final trackHeight = dragging ? _trackActiveHeight : widget.height;
|
||||
final size = dragging ? _thumbActiveSize : _thumbSize;
|
||||
final finger = (width * fraction).clamp(0.0, width).toDouble();
|
||||
final thumbSlotLeft = dragging
|
||||
? finger - _thumbActiveSize / 2
|
||||
: (finger - _thumbActiveSize / 2)
|
||||
.clamp(0.0, width - _thumbActiveSize)
|
||||
.toDouble();
|
||||
final playedWidth = dragging ? finger : width * fraction;
|
||||
final playedWidth = (width * fraction).clamp(0.0, width).toDouble();
|
||||
final trackSlotBottom = _lineCenterFromBottom - _trackActiveHeight / 2;
|
||||
final thumbSlotBottom = _lineCenterFromBottom - _thumbActiveSize / 2;
|
||||
return SizedBox(
|
||||
height: widget.hitHeight,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
@@ -374,7 +364,8 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
|
||||
key: const Key('home-feed-progress-track'),
|
||||
width: width,
|
||||
height: trackHeight,
|
||||
color: AppColors.border,
|
||||
// 和剧集播放页进度条共用同一个槽底色,两个入口观感一致。
|
||||
color: AppColors.inactiveTrack,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
@@ -390,24 +381,6 @@ class _FeedProgressBarState extends State<_FeedProgressBar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: thumbSlotLeft,
|
||||
bottom: thumbSlotBottom,
|
||||
child: SizedBox.square(
|
||||
dimension: _thumbActiveSize,
|
||||
child: Center(
|
||||
child: Container(
|
||||
key: const Key('home-feed-progress-thumb'),
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.onPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -13,7 +13,14 @@ import 'interaction_rail.dart';
|
||||
const double _seriesFont = 17;
|
||||
const double _tagFont = 12;
|
||||
const double _synopsisFont = 14;
|
||||
const double _watchAllHeight = 44;
|
||||
const double _watchAllHeight = 38;
|
||||
|
||||
/// 观看条内部左右留白与圆角:外沿由 overlay 按页面边距对齐,内部不再重复内缩。
|
||||
const double _watchAllInset = 12;
|
||||
const double _watchAllRadius = 12;
|
||||
|
||||
/// 标题、标签、简介之间的行间距:底部信息本来就挤,留 4pt 够分层。
|
||||
const double _blockGap = 4;
|
||||
const List<Shadow> _textShadows = [
|
||||
Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)),
|
||||
];
|
||||
@@ -57,6 +64,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
||||
final dramaTitle = localizedDramaTitle(l10n, widget.drama);
|
||||
final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim();
|
||||
final genres = _genreValues(Localizations.localeOf(context).languageCode);
|
||||
final tags = _tagChips(l10n, genres);
|
||||
return Column(
|
||||
key: const Key('home-feed-info-panel'),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -104,18 +112,21 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_tagMetaRow(l10n, genres),
|
||||
// 没有标签就整行跳过,不给标题和简介之间留空档。
|
||||
if (tags.isNotEmpty) ...[
|
||||
const SizedBox(height: _blockGap),
|
||||
_tagRow(tags),
|
||||
],
|
||||
if (synopsis.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: _blockGap),
|
||||
_synopsis(l10n, synopsis),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tagMetaRow(AppLocalizations l10n, List<String> genres) {
|
||||
final chips = <Widget>[
|
||||
List<Widget> _tagChips(AppLocalizations l10n, List<String> genres) {
|
||||
return [
|
||||
if (isHotDrama(widget.drama))
|
||||
_TagChip(
|
||||
key: const Key('home-feed-hot-badge'),
|
||||
@@ -125,6 +136,9 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
||||
for (final genre in genres)
|
||||
_TagChip(key: Key('home-feed-tag-$genre'), label: genre),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _tagRow(List<Widget> chips) {
|
||||
return SizedBox(
|
||||
key: const Key('home-feed-tags'),
|
||||
height: 22,
|
||||
@@ -149,10 +163,7 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
||||
);
|
||||
final span = TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: prefix,
|
||||
style: baseStyle,
|
||||
),
|
||||
TextSpan(text: prefix, style: baseStyle),
|
||||
TextSpan(
|
||||
text: ' | ',
|
||||
style: baseStyle.copyWith(
|
||||
@@ -368,7 +379,7 @@ class _TagChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 底部通栏条:🎬 观看完整剧集 全 N 集。
|
||||
/// 底部「观看完整剧集」条:浮在底部,不再铺满整屏宽度。
|
||||
class HomeFeedWatchAllBar extends StatelessWidget {
|
||||
const HomeFeedWatchAllBar({
|
||||
super.key,
|
||||
@@ -382,42 +393,45 @@ class HomeFeedWatchAllBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Material(
|
||||
key: const Key('home-feed-full-series-bg'),
|
||||
color: AppColors.textPrimary.withValues(alpha: 0.15),
|
||||
child: InkWell(
|
||||
key: const Key('home-feed-full-series'),
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: _watchAllHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.movie_rounded,
|
||||
size: 22,
|
||||
color: AppColors.onPrimary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.watchFullSeries('${drama.episodeCount}'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_watchAllRadius),
|
||||
child: Material(
|
||||
key: const Key('home-feed-full-series-bg'),
|
||||
color: AppColors.textPrimary.withValues(alpha: 0.15),
|
||||
child: InkWell(
|
||||
key: const Key('home-feed-full-series'),
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: _watchAllHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: _watchAllInset),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.movie_rounded,
|
||||
size: 22,
|
||||
color: AppColors.onPrimary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.watchFullSeries('${drama.episodeCount}'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
size: 22,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
],
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
size: 22,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,7 +6,9 @@ import '../../../core/theme/app_colors.dart';
|
||||
|
||||
const double _toggleSize = 68;
|
||||
const double _iconSize = 36;
|
||||
const Color _toggleBackground = AppColors.controlSoftScrim;
|
||||
|
||||
/// 播放/暂停压在画面上,垫一层半透明深色圆底保证亮画面下也能看见。
|
||||
const Color _toggleBackground = Color(0x66000000);
|
||||
const List<Shadow> _iconShadows = [
|
||||
Shadow(color: AppColors.shadow, blurRadius: 12),
|
||||
];
|
||||
|
||||
@@ -19,8 +19,17 @@ import 'seek_progress_bar.dart';
|
||||
|
||||
part 'player_overlay_parts.dart';
|
||||
|
||||
const double _topButtonSize = 36;
|
||||
const double _topButtonSize = 40;
|
||||
const double _topBarHeight = 58;
|
||||
|
||||
/// 底部信息、选集条、进度条之间的统一行距。
|
||||
const double _blockGap = 8;
|
||||
|
||||
/// 底部选集条与右侧清屏按钮的统一高度,两者必须等高。
|
||||
const double _bottomActionHeight = 34;
|
||||
|
||||
/// 选集条内部左右留白。
|
||||
const double _bottomActionInset = 10;
|
||||
const double _boostHintIconSize = 16;
|
||||
const double _boostHintFontSize = 13;
|
||||
const double _synopsisRailInset = 54;
|
||||
@@ -107,11 +116,12 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
key: const Key('player-back'),
|
||||
tooltip: data.l10n.backToSeries,
|
||||
icon: Icons.chevron_left_rounded,
|
||||
iconSize: 30,
|
||||
onPressed: actions.onBack!,
|
||||
)
|
||||
else
|
||||
const SizedBox(width: _topButtonSize),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
data.l10n.episodeLabel(data.episode.number),
|
||||
@@ -135,7 +145,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
_TopButton(
|
||||
key: const Key('player-more'),
|
||||
tooltip: data.l10n.more,
|
||||
icon: Icons.more_vert_rounded,
|
||||
icon: Icons.more_horiz_rounded,
|
||||
onPressed: actions.onMore,
|
||||
),
|
||||
],
|
||||
@@ -226,19 +236,20 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
child: _bottomInfo(context),
|
||||
),
|
||||
if (data.boosting) _boostHint(),
|
||||
_bottomProgress(),
|
||||
// 加速时收起底栏。拖动进度时底栏只隐藏,占位保留,进度条不挪位。
|
||||
// 加速时收起选集条。拖动进度时只隐藏,占位保留,底部不跳动。
|
||||
scrubbing
|
||||
? IgnorePointer(
|
||||
child: Opacity(opacity: 0, child: _bottomActions()),
|
||||
)
|
||||
: Offstage(offstage: data.boosting, child: _bottomActions()),
|
||||
// 进度条压在最底部,和首页 feed 一致。
|
||||
_bottomProgress(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 加速提示:橙色小三角 + 当前倍速文案,悬在进度条上方。
|
||||
/// 加速提示:橙色小三角 + 当前倍速文案,压在底部信息区下方。
|
||||
Widget _boostHint() {
|
||||
return Padding(
|
||||
key: const Key('player-speed-boost-hint'),
|
||||
@@ -270,25 +281,28 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
Widget _bottomInfo(BuildContext context) {
|
||||
final series = localizedDramaTitle(data.l10n, data.drama);
|
||||
final synopsis = localizedDramaSynopsis(data.l10n, data.drama);
|
||||
// 右侧让出互动栏的宽度:剧名和简介一样,收在互动栏左侧,长标题也不会钻到按钮下面。
|
||||
return Padding(
|
||||
key: const Key('player-controls-bottom'),
|
||||
padding: const EdgeInsets.fromLTRB(14, 8, 14, 0),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
14,
|
||||
_blockGap,
|
||||
14 + _synopsisRailInset,
|
||||
0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_detailRow(series),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: _synopsisRailInset),
|
||||
child: _EpisodeSynopsis(
|
||||
key: const Key('player-episode-synopsis'),
|
||||
l10n: data.l10n,
|
||||
drama: data.drama,
|
||||
episodeNumber: data.episode.number,
|
||||
synopsis: synopsis,
|
||||
authorNote: data.authorNote,
|
||||
),
|
||||
_EpisodeSynopsis(
|
||||
key: const Key('player-episode-synopsis'),
|
||||
l10n: data.l10n,
|
||||
drama: data.drama,
|
||||
episodeNumber: data.episode.number,
|
||||
synopsis: synopsis,
|
||||
authorNote: data.authorNote,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -296,9 +310,10 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _bottomProgress() {
|
||||
// 进度条不贴底:底部留 16pt,线心离底边更远,也不再和选集条挤在一起。
|
||||
return Padding(
|
||||
key: const Key('player-progress-container'),
|
||||
padding: EdgeInsets.fromLTRB(14, controlsVisible ? 2 : 8, 14, 0),
|
||||
padding: const EdgeInsets.fromLTRB(14, 2, 14, 16),
|
||||
child: SeekProgressBar(
|
||||
key: const Key('player-progress'),
|
||||
valueListenable: data.controller,
|
||||
@@ -345,10 +360,9 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: AppColors.playerMuted,
|
||||
color: AppColors.onPrimary,
|
||||
size: 20,
|
||||
shadows: _playerTextShadows,
|
||||
),
|
||||
@@ -362,55 +376,63 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
Widget _bottomActions() {
|
||||
return Padding(
|
||||
key: const Key('player-bottom-actions'),
|
||||
padding: const EdgeInsets.fromLTRB(14, 6, 14, 6),
|
||||
padding: const EdgeInsets.fromLTRB(14, 2, 14, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Material(
|
||||
key: const Key('player-episodes-pill'),
|
||||
color: AppColors.playerOverlayPill,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
key: const Key('player-episodes'),
|
||||
onTap: actions.onEpisodes,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${data.l10n.episodes} · '
|
||||
'${data.l10n.allEpisodesBadge(data.totalEpisodes)} · '
|
||||
'${data.l10n.freeWatch}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
// 清屏后只留右下角的退出按钮,左侧选集条一起收起。
|
||||
if (!controlsVisible)
|
||||
const Spacer()
|
||||
else ...[
|
||||
Expanded(
|
||||
// 选集条给一层浅底色:视频画面亮时纯文字不好认,也顺带标出可点区域。
|
||||
child: Material(
|
||||
key: const Key('player-episodes-pill'),
|
||||
color: AppColors.textPrimary.withValues(alpha: 0.14),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
key: const Key('player-episodes'),
|
||||
onTap: actions.onEpisodes,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _bottomActionInset,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _bottomActionHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${data.l10n.episodes} · '
|
||||
'${data.l10n.allEpisodesBadge(data.totalEpisodes)} · '
|
||||
'${data.l10n.freeWatch}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
shadows: _playerTextShadows,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
size: 18,
|
||||
shadows: _playerTextShadows,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
size: 18,
|
||||
shadows: _playerTextShadows,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
_BottomIconButton(
|
||||
key: const Key('player-bottom-clear-screen'),
|
||||
tooltip: controlsVisible
|
||||
@@ -419,6 +441,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
icon: controlsVisible
|
||||
? Icons.fullscreen_rounded
|
||||
: Icons.fullscreen_exit_rounded,
|
||||
height: _bottomActionHeight,
|
||||
onPressed: controlsVisible ? actions.onClear : actions.onExitClear,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
part of 'player_overlay.dart';
|
||||
|
||||
/// 顶部按钮:直接压在画面上,不加底色,靠文字/图标阴影保证可读性。
|
||||
class _TopButton extends StatelessWidget {
|
||||
const _TopButton({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.iconSize = 22,
|
||||
});
|
||||
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -22,11 +25,11 @@ class _TopButton extends StatelessWidget {
|
||||
tooltip: tooltip,
|
||||
padding: EdgeInsets.zero,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: AppColors.controlScrim,
|
||||
backgroundColor: AppColors.transparent,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: 18),
|
||||
icon: Icon(icon, size: iconSize, shadows: _playerTextShadows),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -41,7 +44,7 @@ class _SpeedEntry extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: AppColors.controlScrim,
|
||||
color: AppColors.transparent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
@@ -81,25 +84,30 @@ class _BottomIconButton extends StatelessWidget {
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.height = 40,
|
||||
});
|
||||
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// 与左侧选集条等高,两个控件底部齐平。
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: 40,
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: height,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
padding: EdgeInsets.zero,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: AppColors.playerOverlayPill,
|
||||
backgroundColor: AppColors.transparent,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: 22),
|
||||
icon: Icon(icon, size: 24, shadows: _playerTextShadows),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -242,8 +250,10 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
_toggle(widget.l10n.expandSynopsis,
|
||||
padding: const EdgeInsets.only(left: 6)),
|
||||
_toggle(
|
||||
widget.l10n.expandSynopsis,
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -291,12 +301,12 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> {
|
||||
/// 分隔竖线:同字号 w400 + 45% 透明白,靠两侧空格留白。
|
||||
/// 必须用 TextSpan:WidgetSpan 占位盒的基线对齐会让第n集与简介错位。
|
||||
TextSpan _separatorSpan() => TextSpan(
|
||||
text: ' | ',
|
||||
style: _lineStyle.copyWith(
|
||||
color: AppColors.onPrimary.withValues(alpha: 0.45),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
);
|
||||
text: ' | ',
|
||||
style: _lineStyle.copyWith(
|
||||
color: AppColors.onPrimary.withValues(alpha: 0.45),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _toggle(String label, {required EdgeInsets padding}) {
|
||||
return GestureDetector(
|
||||
|
||||
@@ -321,10 +321,8 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
||||
trackHeight: trackHeight,
|
||||
activeTrackColor: AppColors.blue,
|
||||
inactiveTrackColor: widget.inactiveTrackColor,
|
||||
thumbColor: AppColors.onPrimary,
|
||||
thumbShape: RoundSliderThumbShape(
|
||||
enabledThumbRadius: scrubbing ? 8 : 6,
|
||||
),
|
||||
// 播放页进度条不画滑块圆点:进度只靠蓝色已播段表达。
|
||||
thumbShape: SliderComponentShape.noThumb,
|
||||
overlayShape: RoundSliderOverlayShape(
|
||||
overlayRadius: widget.compact ? 12 : 18,
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user