feat(flutter): 重构播放页并增加语言切换
This commit is contained in:
1 parent
f1efbc5be8
commit
8c43e9e385
34 files changed
+1331
-879
No files matched your search
@@ -14,19 +14,14 @@ import 'seek_progress_bar.dart';
|
||||
|
||||
part 'player_overlay_parts.dart';
|
||||
|
||||
const double _topGradientHeight = 230;
|
||||
const double _bottomGradientHeight = 390;
|
||||
const double _topBarHeight = 58;
|
||||
const double _topButtonSize = 36;
|
||||
const double _titleSize = 24;
|
||||
const double _dotSize = 7;
|
||||
const double _railButtonSize = 36;
|
||||
const double _railGap = 9;
|
||||
const double _chipRadius = 999;
|
||||
const double _dotRadius = 3;
|
||||
const double _railTopFactor = 0.28;
|
||||
const double _railTopCompactFactor = 0.18;
|
||||
const double _fullScreenPillBottom = 246;
|
||||
const double _panelRadius = 16;
|
||||
|
||||
/// K28 竖屏播放页浮层。
|
||||
/// K29 竖屏播放页浮层。
|
||||
class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
/// 创建播放浮层。
|
||||
const PlayerPlaybackOverlay({
|
||||
@@ -61,6 +56,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
_top(context),
|
||||
_bottom(context),
|
||||
_rail(context),
|
||||
_fullScreenPill(),
|
||||
],
|
||||
if (!controlsVisible)
|
||||
Positioned(
|
||||
@@ -86,18 +82,14 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _top(BuildContext context) {
|
||||
final title = data.episode.title.trim().isEmpty
|
||||
? data.l10n.episodeLabel(data.episode.number)
|
||||
: data.episode.title;
|
||||
final series = localizedDramaTitle(data.l10n, data.drama);
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
key: const Key('player-controls-top'),
|
||||
height: _topGradientHeight,
|
||||
padding: const EdgeInsets.fromLTRB(14, 4, 14, 20),
|
||||
height: _topBarHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
@@ -105,89 +97,42 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
colors: [Color(0xCC000000), Color(0x00000000)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (actions.onBack != null)
|
||||
_TopButton(
|
||||
key: const Key('player-back'),
|
||||
tooltip: data.l10n.backToSeries,
|
||||
icon: Icons.chevron_left_rounded,
|
||||
onPressed: actions.onBack!,
|
||||
)
|
||||
else
|
||||
const SizedBox(width: _topButtonSize),
|
||||
const Spacer(),
|
||||
_TopButton(
|
||||
key: const Key('player-clear-screen'),
|
||||
tooltip: data.l10n.clearScreen,
|
||||
icon: Icons.fullscreen_rounded,
|
||||
onPressed: actions.onClear,
|
||||
if (actions.onBack != null)
|
||||
_TopButton(
|
||||
key: const Key('player-back'),
|
||||
tooltip: data.l10n.backToSeries,
|
||||
icon: Icons.chevron_left_rounded,
|
||||
onPressed: actions.onBack!,
|
||||
)
|
||||
else
|
||||
const SizedBox(width: _topButtonSize),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
data.l10n.episodeLabel(data.episode.number),
|
||||
key: const Key('player-episode-heading'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
_TopButton(
|
||||
key: const Key('player-more'),
|
||||
tooltip: data.l10n.more,
|
||||
icon: Icons.more_horiz_rounded,
|
||||
onPressed: actions.onMore,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
key: const Key('player-title'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontFamily: 'Outfit',
|
||||
fontSize: _titleSize * data.textScale,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
Container(
|
||||
key: const Key('player-title-dot'),
|
||||
width: _dotSize,
|
||||
height: _dotSize,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.blue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'$series · ${data.l10n.episodeLabel(data.episode.number)}',
|
||||
key: const Key('player-subtitle'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14 * data.textScale,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
if (_genre.isNotEmpty)
|
||||
_infoChip(const Key('player-genre-chip'), _genre),
|
||||
const Spacer(),
|
||||
_PageDots(
|
||||
key: const Key('player-page-dots'),
|
||||
current: data.currentIndex,
|
||||
total: data.totalEpisodes,
|
||||
),
|
||||
],
|
||||
_SpeedEntry(
|
||||
key: const Key('player-speed-entry'),
|
||||
speed: data.playbackSpeed,
|
||||
onTap: actions.onSpeed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_TopButton(
|
||||
key: const Key('player-more'),
|
||||
tooltip: data.l10n.more,
|
||||
icon: Icons.more_vert_rounded,
|
||||
onPressed: actions.onMore,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -196,18 +141,17 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _rail(BuildContext context) {
|
||||
final likeCount = data.likeCount;
|
||||
final compact = MediaQuery.sizeOf(context).height < 700;
|
||||
return Positioned(
|
||||
right: AppDimens.spaceMd,
|
||||
top:
|
||||
MediaQuery.sizeOf(context).height *
|
||||
(compact ? _railTopCompactFactor : _railTopFactor),
|
||||
top: _topBarHeight + (compact ? 18 : 54),
|
||||
child: _Rail(
|
||||
l10n: data.l10n,
|
||||
liked: data.liked,
|
||||
saved: data.saved,
|
||||
likeCount: likeCount,
|
||||
likeCount: data.likeCount,
|
||||
favoriteCount: data.favoriteCount,
|
||||
commentCount: data.commentCount,
|
||||
onLike: actions.onLike,
|
||||
onComments: actions.onComments,
|
||||
onSave: actions.onSave,
|
||||
@@ -224,193 +168,185 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Stack(
|
||||
children: [
|
||||
const Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x00000000),
|
||||
Color(0xDD000000),
|
||||
Color(0xF2000000),
|
||||
],
|
||||
child: Container(
|
||||
key: const Key('player-controls-bottom'),
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xED000000),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(_panelRadius),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_detailRow(series),
|
||||
const SizedBox(height: 6),
|
||||
_EpisodeSynopsis(
|
||||
key: const Key('player-episode-synopsis'),
|
||||
l10n: data.l10n,
|
||||
episodeNumber: data.episode.number,
|
||||
synopsis: synopsis,
|
||||
textScale: data.textScale,
|
||||
),
|
||||
if (data.authorNote.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
data.authorNote,
|
||||
key: const Key('player-author-note'),
|
||||
style: const TextStyle(color: Colors.white60, fontSize: 11),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
_PlayerTimeLabel(
|
||||
key: const Key('player-time-label'),
|
||||
controller: data.controller,
|
||||
),
|
||||
SeekProgressBar(
|
||||
key: const Key('player-progress'),
|
||||
valueListenable: data.controller,
|
||||
semanticsLabel: data.l10n.seekVideo,
|
||||
showTimes: true,
|
||||
compact: true,
|
||||
onScrubStart: actions.onScrubStart,
|
||||
onSeek: actions.onSeek,
|
||||
),
|
||||
_bottomActions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detailRow(String series) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: const Key('player-open-detail'),
|
||||
onTap: actions.onDetail,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
series,
|
||||
key: const Key('player-drama-title'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontFamily: 'Outfit',
|
||||
fontSize: 19 * data.textScale,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Colors.white70,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bottomActions() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: const Key('player-episodes'),
|
||||
onTap: actions.onEpisodes,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
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: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: _bottomGradientHeight),
|
||||
),
|
||||
if (actions.onPip != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_TopButton(
|
||||
key: const Key('player-pip'),
|
||||
tooltip: data.l10n.pictureInPicture,
|
||||
icon: Icons.picture_in_picture_alt_rounded,
|
||||
onPressed: actions.onPip!,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fullScreenPill() {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: _fullScreenPillBottom,
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: const Color(0x99000000),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: InkWell(
|
||||
key: const Key('player-fullscreen-toggle'),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: actions.onClear,
|
||||
child: Padding(
|
||||
key: const Key('player-controls-bottom'),
|
||||
padding: const EdgeInsets.fromLTRB(14, 48, 14, 10),
|
||||
child: Column(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
series,
|
||||
key: const Key('player-drama-title'),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontFamily: 'Outfit',
|
||||
fontSize: 22 * data.textScale,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
data.l10n.episodeLabel(data.episode.number),
|
||||
key: const Key('player-episode-label'),
|
||||
style: TextStyle(
|
||||
color: AppColors.blue,
|
||||
fontSize: 12 * data.textScale,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Icon(
|
||||
Icons.fullscreen_rounded,
|
||||
color: AppColors.onPrimary,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _genre.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _softChip(_genre),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
height: AppDimens.buttonSmall,
|
||||
child: FilledButton(
|
||||
key: const Key('player-follow'),
|
||||
onPressed: actions.onSave,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
child: Text(
|
||||
data.saved
|
||||
? data.l10n.followingTab
|
||||
: data.l10n.followSeries,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
synopsis,
|
||||
key: const Key('player-synopsis'),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12 * data.textScale,
|
||||
height: 1.45,
|
||||
data.l10n.fullScreenWatch,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_TimeLabel(
|
||||
key: const Key('player-time-label'),
|
||||
controller: data.controller,
|
||||
),
|
||||
SeekProgressBar(
|
||||
key: const Key('player-progress'),
|
||||
valueListenable: data.controller,
|
||||
semanticsLabel: data.l10n.seekVideo,
|
||||
showTimes: true,
|
||||
compact: true,
|
||||
onScrubStart: actions.onScrubStart,
|
||||
onSeek: actions.onSeek,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ActionButton(
|
||||
key: const Key('player-episodes'),
|
||||
icon: Icons.grid_view_rounded,
|
||||
label: data.l10n.episodes,
|
||||
onTap: actions.onEpisodes,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _ActionButton(
|
||||
key: const Key('player-next'),
|
||||
icon: Icons.skip_next_rounded,
|
||||
label: data.l10n.nextEpisode,
|
||||
onTap: actions.onNext,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _ActionButton(
|
||||
key: const Key('player-bottom-more'),
|
||||
icon: Icons.more_horiz_rounded,
|
||||
label: data.l10n.more,
|
||||
onTap: actions.onMore,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoChip(Key key, String label) => Container(
|
||||
key: key,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blue,
|
||||
borderRadius: BorderRadius.circular(_chipRadius),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _softChip(String label) => Container(
|
||||
constraints: const BoxConstraints(maxWidth: 180),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.14),
|
||||
borderRadius: BorderRadius.circular(_chipRadius),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String get _genre =>
|
||||
data.drama.genre.isNotEmpty ? data.drama.genre : data.drama.category;
|
||||
}
|
||||
Reference in new issue
Block a user