feat(player): 倍速改为底部弹窗选择
This commit is contained in:
1 parent
5ca5846a26
commit
3a7e0bc4b7
10 files changed
+194
-6
No files matched your search
@@ -48,6 +48,7 @@
|
||||
},
|
||||
"clearScreen": "Clear screen",
|
||||
"exitClearScreen": "Exit clear screen",
|
||||
"playbackSpeed": "Playback speed",
|
||||
"minutesAgo": "{count} min ago",
|
||||
"@minutesAgo": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
},
|
||||
"clearScreen": "Bersihkan layar",
|
||||
"exitClearScreen": "Keluar dari layar bersih",
|
||||
"playbackSpeed": "Kecepatan putar",
|
||||
"minutesAgo": "{count} mnt lalu",
|
||||
"@minutesAgo": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -352,6 +352,12 @@ abstract class AppLocalizations {
|
||||
/// **'Exit clear screen'**
|
||||
String get exitClearScreen;
|
||||
|
||||
/// No description provided for @playbackSpeed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Playback speed'**
|
||||
String get playbackSpeed;
|
||||
|
||||
/// No description provided for @minutesAgo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -139,6 +139,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get exitClearScreen => 'Exit clear screen';
|
||||
|
||||
@override
|
||||
String get playbackSpeed => 'Playback speed';
|
||||
|
||||
@override
|
||||
String minutesAgo(int count) {
|
||||
return '$count min ago';
|
||||
|
||||
@@ -139,6 +139,9 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get exitClearScreen => 'Keluar dari layar bersih';
|
||||
|
||||
@override
|
||||
String get playbackSpeed => 'Kecepatan putar';
|
||||
|
||||
@override
|
||||
String minutesAgo(int count) {
|
||||
return '$count mnt lalu';
|
||||
|
||||
@@ -136,6 +136,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get exitClearScreen => '退出清屏';
|
||||
|
||||
@override
|
||||
String get playbackSpeed => '倍速';
|
||||
|
||||
@override
|
||||
String minutesAgo(int count) {
|
||||
return '$count 分钟前';
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
},
|
||||
"clearScreen": "清屏",
|
||||
"exitClearScreen": "退出清屏",
|
||||
"playbackSpeed": "倍速",
|
||||
"minutesAgo": "{count} 分钟前",
|
||||
"@minutesAgo": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -68,6 +68,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
List<Episode> sessionEpisodes = const [];
|
||||
int pageRequest = 0;
|
||||
bool muted = false;
|
||||
double playbackSpeed = 1.0;
|
||||
bool stoppedAtFeedEnd = false;
|
||||
VideoPlayerPort? scrubbingPlayer;
|
||||
bool pip = false;
|
||||
@@ -341,6 +342,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
Future<void> preparePlayer(Episode episode, PlayerPort player) async {
|
||||
if (player is! VideoPlayerPort) return;
|
||||
try {
|
||||
await player.controller.setPlaybackSpeed(playbackSpeed);
|
||||
await pendingWrite;
|
||||
// 服务端每剧只记一条:只有"记录里的那一集"才定位,其它集从头播
|
||||
if (resumePoint == null) await loadResumePoint();
|
||||
@@ -360,6 +362,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final index = session.currentIndex;
|
||||
final drama = queue.entryAt(index).drama;
|
||||
await player.controller.setVolume(muted ? 0 : 1);
|
||||
await player.controller.setPlaybackSpeed(playbackSpeed);
|
||||
if (session.active != player) return;
|
||||
trackedPlayer = player;
|
||||
trackedEpisode = episode;
|
||||
@@ -787,6 +790,15 @@ class PlayerLogic extends ChangeNotifier {
|
||||
notify();
|
||||
}
|
||||
|
||||
Future<void> setPlaybackSpeed(double value) async {
|
||||
playbackSpeed = value;
|
||||
final active = session.active;
|
||||
if (active is VideoPlayerPort && active.controller.value.isInitialized) {
|
||||
await active.controller.setPlaybackSpeed(value);
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
Future<void> showComments(BuildContext context, Drama drama) async {
|
||||
session.setOverlayOpen(true);
|
||||
await session.playbackSettled;
|
||||
|
||||
@@ -72,6 +72,7 @@ class PlayerPage extends StatefulWidget {
|
||||
class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
late final PlayerLogic logic;
|
||||
bool _chromeVisible = true;
|
||||
double _playbackSpeed = 1.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -384,6 +385,8 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
? () => unawaited(logic.playNextEpisode())
|
||||
: null,
|
||||
playing: logic.session.userWantsPlay,
|
||||
playbackSpeed: _playbackSpeed,
|
||||
onSpeedTap: () => unawaited(_openSpeedSheet(context)),
|
||||
controlsVisible: _chromeVisible,
|
||||
onClear: () => setState(() => _chromeVisible = false),
|
||||
onExitClear: () => setState(() => _chromeVisible = true),
|
||||
@@ -469,6 +472,18 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openSpeedSheet(BuildContext context) async {
|
||||
final selected = await showModalBottomSheet<double>(
|
||||
context: context,
|
||||
backgroundColor: AppColors.card,
|
||||
showDragHandle: true,
|
||||
builder: (_) => _PlaybackSpeedSheet(selected: _playbackSpeed),
|
||||
);
|
||||
if (selected == null || !mounted) return;
|
||||
setState(() => _playbackSpeed = selected);
|
||||
await logic.setPlaybackSpeed(selected);
|
||||
}
|
||||
}
|
||||
|
||||
/// 不可播放提示:文案由后端返回的 `accessType` 驱动(契约 §1.4),前端不自行推断。
|
||||
@@ -602,6 +617,8 @@ class _PlaybackOverlay extends StatefulWidget {
|
||||
required this.liked,
|
||||
required this.saved,
|
||||
required this.playing,
|
||||
required this.playbackSpeed,
|
||||
required this.onSpeedTap,
|
||||
required this.controlsVisible,
|
||||
required this.onTogglePlay,
|
||||
required this.onClear,
|
||||
@@ -626,6 +643,8 @@ class _PlaybackOverlay extends StatefulWidget {
|
||||
final bool liked;
|
||||
final bool saved;
|
||||
final bool playing;
|
||||
final double playbackSpeed;
|
||||
final VoidCallback onSpeedTap;
|
||||
final bool controlsVisible;
|
||||
final VoidCallback onTogglePlay;
|
||||
final VoidCallback onClear;
|
||||
@@ -667,14 +686,28 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> {
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
const Positioned(
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 14,
|
||||
child: Text(
|
||||
'1.0x',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFDDDDDD),
|
||||
fontSize: 9,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: const Key('player-speed'),
|
||||
onTap: widget.onSpeedTap,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
child: Text(
|
||||
_speedLabel(widget.playbackSpeed),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFDDDDDD),
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -920,6 +953,69 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> {
|
||||
}
|
||||
}
|
||||
|
||||
const List<double> _speedOptions = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
|
||||
|
||||
String _speedLabel(double value) => '${value.toString()}x';
|
||||
|
||||
class _PlaybackSpeedSheet extends StatelessWidget {
|
||||
const _PlaybackSpeedSheet({required this.selected});
|
||||
|
||||
final double selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
key: const Key('player-speed-sheet'),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 2, 20, 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
l10n.playbackSpeed,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final value in _speedOptions)
|
||||
ListTile(
|
||||
key: Key('player-speed-${value.toString()}'),
|
||||
dense: true,
|
||||
tileColor: value == selected
|
||||
? AppColors.blue.withValues(alpha: 0.12)
|
||||
: null,
|
||||
title: Text(
|
||||
_speedLabel(value),
|
||||
style: TextStyle(
|
||||
color: value == selected ? AppColors.blue : AppColors.cream,
|
||||
fontSize: 13,
|
||||
fontWeight: value == selected
|
||||
? FontWeight.w700
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
trailing: value == selected
|
||||
? const Icon(
|
||||
Icons.check_rounded,
|
||||
color: AppColors.blue,
|
||||
size: 18,
|
||||
)
|
||||
: null,
|
||||
onTap: () => Navigator.pop(context, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TopAction extends StatelessWidget {
|
||||
const _TopAction({
|
||||
super.key,
|
||||
|
||||
Reference in new issue
Block a user