fix(player): 统一暂停按钮并自动隐藏控制层

This commit is contained in:
Zhou Weidong committed 2026-10-19 16:16:14 +08:00
1 parent 568f051b7a
commit 03e34b3b57
5 files changed
+353 -171

No files matched your search

+228 -170
View File
@@ -24,6 +24,7 @@ import '../../l10n/app_localizations.dart';
import 'player_binding.dart';
import 'player_logic.dart';
import 'widgets/home_feed_overlay.dart';
import 'widgets/player_center_toggle.dart';
import 'widgets/player_featured_comment.dart';
import 'widgets/seek_progress_bar.dart';
import '../../core/data/repositories/watch_history_repository.dart';
@@ -318,23 +319,11 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
playing: logic.session.userWantsPlay,
pip: logic.pip,
))
const Center(
key: Key('home-feed-center-play'),
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
color: AppColors.blue,
shape: BoxShape.circle,
),
child: Padding(
padding: EdgeInsets.all(10),
child: Icon(
Icons.play_arrow_rounded,
size: 48,
color: Colors.white,
),
),
),
Center(
child: PlayerCenterToggle(
key: const Key('home-feed-center-play'),
playing: false,
onTap: () => logic.session.setUserWantsPlay(true),
),
),
if (logic.pip || logic.pipPreparing)
@@ -571,7 +560,7 @@ class _UnlockCard extends StatelessWidget {
}
}
class _PlaybackOverlay extends StatelessWidget {
class _PlaybackOverlay extends StatefulWidget {
const _PlaybackOverlay({
required this.l10n,
required this.drama,
@@ -614,123 +603,185 @@ class _PlaybackOverlay extends StatelessWidget {
final VoidCallback? onScrubStart;
final Future<void> Function(Duration)? onSeek;
@override
State<_PlaybackOverlay> createState() => _PlaybackOverlayState();
}
class _PlaybackOverlayState extends State<_PlaybackOverlay> {
Timer? _hideTimer;
bool _controlsVisible = true;
@override
void initState() {
super.initState();
_scheduleHide();
}
@override
void dispose() {
_hideTimer?.cancel();
super.dispose();
}
void _scheduleHide() {
_hideTimer?.cancel();
_hideTimer = Timer(const Duration(milliseconds: 2500), () {
if (mounted) setState(() => _controlsVisible = false);
});
}
void _reveal() {
setState(() => _controlsVisible = true);
_scheduleHide();
}
void _togglePlay() {
widget.onTogglePlay();
_reveal();
}
void _onVideoTap() {
if (!_controlsVisible) {
_reveal();
} else {
_togglePlay();
}
}
void _run(VoidCallback action) {
action();
_reveal();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
child: Stack(
children: [
_topBar(context),
Expanded(
child: Stack(
children: [
Center(
child: GestureDetector(
key: const Key('player-play-toggle'),
onTap: onTogglePlay,
child: Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Color(0x80000000),
shape: BoxShape.circle,
),
child: Icon(
playing
? Icons.pause_rounded
: Icons.play_arrow_rounded,
color: AppColors.onPrimary,
size: 26,
),
),
),
),
const Positioned(
right: 10,
bottom: 14,
child: Text(
'1.0x',
style: TextStyle(color: Color(0xFFDDDDDD), fontSize: 9),
),
),
],
Positioned.fill(
child: GestureDetector(
key: const Key('player-video-tap'),
behavior: HitTestBehavior.opaque,
onTap: _onVideoTap,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
child: Column(
if (_controlsVisible)
Column(
key: const Key('player-controls-visible'),
children: [
SeekProgressBar(
valueListenable: controller,
semanticsLabel: l10n.seekVideo,
showTimes: false,
onScrubStart: onScrubStart,
onSeek: onSeek,
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: _PlayerAction(
key: const Key('player-episodes'),
icon: Icons.view_list_rounded,
label: l10n.episodes,
onTap: onEpisodes,
_topBar(context),
Expanded(
child: Stack(
children: [
Center(
child: PlayerCenterToggle(
key: const Key('player-play-toggle'),
playing: widget.playing,
onTap: _togglePlay,
),
),
),
const SizedBox(width: 8),
Expanded(
child: _PlayerAction(
key: const Key('player-comments'),
icon: Icons.chat_bubble_outline_rounded,
label: l10n.comments,
onTap: onComments,
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 32,
child: FilledButton(
key: const Key('player-next'),
onPressed: onNext,
style: FilledButton.styleFrom(
backgroundColor: AppColors.blue,
disabledBackgroundColor: AppColors.raised,
disabledForegroundColor: AppColors.subtle,
foregroundColor: AppColors.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
l10n.nextEpisode,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 2),
const Icon(Icons.skip_next_rounded, size: 14),
],
const Positioned(
right: 10,
bottom: 14,
child: Text(
'1.0x',
style: TextStyle(
color: Color(0xFFDDDDDD),
fontSize: 9,
),
),
),
),
],
],
),
),
Padding(
key: const Key('player-controls-bottom'),
padding: const EdgeInsets.fromLTRB(14, 8, 14, 12),
child: Column(
children: [
SeekProgressBar(
key: const Key('player-progress'),
valueListenable: widget.controller,
semanticsLabel: widget.l10n.seekVideo,
showTimes: false,
onScrubStart: widget.onScrubStart,
onSeek: widget.onSeek,
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _PlayerAction(
key: const Key('player-episodes'),
icon: Icons.view_list_rounded,
label: widget.l10n.episodes,
onTap: () => _run(widget.onEpisodes),
),
),
const SizedBox(width: 8),
Expanded(
child: _PlayerAction(
key: const Key('player-comments'),
icon: Icons.chat_bubble_outline_rounded,
label: widget.l10n.comments,
onTap: () => _run(widget.onComments),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 32,
child: FilledButton(
key: const Key('player-next'),
onPressed: widget.onNext == null
? null
: () => _run(widget.onNext!),
style: FilledButton.styleFrom(
backgroundColor: AppColors.blue,
disabledBackgroundColor: AppColors.raised,
disabledForegroundColor: AppColors.subtle,
foregroundColor: AppColors.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
widget.l10n.nextEpisode,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 2),
const Icon(
Icons.skip_next_rounded,
size: 14,
),
],
),
),
),
),
],
),
const SizedBox(height: 12),
PlayerFeaturedComment(
drama: widget.drama,
onViewAll: () => _run(widget.onComments),
),
],
),
),
const SizedBox(height: 10),
PlayerFeaturedComment(drama: drama, onViewAll: onComments),
],
),
),
],
),
);
@@ -738,43 +789,47 @@ class _PlaybackOverlay extends StatelessWidget {
Widget _topBar(BuildContext context) {
return SizedBox(
key: const Key('player-controls-top'),
height: 46,
child: Row(
children: [
if (onBack != null)
_TopAction(
key: const Key('player-back'),
tooltip: l10n.backToSeries,
icon: Icons.chevron_left_rounded,
onPressed: onBack!,
)
else
const SizedBox(width: 28),
const SizedBox(width: 8),
Expanded(
child: Text(
'${localizedDramaTitle(l10n, drama)} · '
'${l10n.episodeLabel(episode.number)}',
key: const Key('player-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w700,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
if (widget.onBack != null)
_TopAction(
key: const Key('player-back'),
tooltip: widget.l10n.backToSeries,
icon: Icons.chevron_left_rounded,
onPressed: () => _run(widget.onBack!),
)
else
const SizedBox(width: 28),
const SizedBox(width: 8),
Expanded(
child: Text(
'${localizedDramaTitle(widget.l10n, widget.drama)} · '
'${widget.l10n.episodeLabel(widget.episode.number)}',
key: const Key('player-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 8),
_TopAction(
key: const Key('player-more'),
tooltip: l10n.more,
icon: Icons.more_horiz_rounded,
onPressed: () => _openMore(context),
),
],
const SizedBox(width: 8),
_TopAction(
key: const Key('player-more'),
tooltip: widget.l10n.more,
icon: Icons.more_horiz_rounded,
onPressed: () => _openMore(context),
),
],
),
),
);
}
@@ -794,62 +849,65 @@ class _PlaybackOverlay extends StatelessWidget {
Icons.download_outlined,
color: AppColors.secondary,
),
title: Text(l10n.downloads),
title: Text(widget.l10n.downloads),
onTap: () {
Navigator.pop(sheetContext);
onDownload();
widget.onDownload();
},
),
if (onPip != null)
if (widget.onPip != null)
ListTile(
key: const Key('player-more-pip'),
leading: const Icon(
Icons.picture_in_picture_alt_outlined,
color: AppColors.secondary,
),
title: Text(l10n.pictureInPicture),
title: Text(widget.l10n.pictureInPicture),
onTap: () {
Navigator.pop(sheetContext);
onPip!();
widget.onPip!();
},
),
ListTile(
key: const Key('player-more-like'),
leading: Icon(
liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: liked ? AppColors.blue : AppColors.muted,
widget.liked
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: widget.liked ? AppColors.blue : AppColors.muted,
),
title: Text(l10n.like),
title: Text(widget.l10n.like),
onTap: () {
Navigator.pop(sheetContext);
onLike();
widget.onLike();
},
),
ListTile(
key: const Key('player-more-favorite'),
leading: Icon(
saved ? Icons.star_rounded : Icons.star_border_rounded,
color: saved ? AppColors.blue : AppColors.muted,
widget.saved ? Icons.star_rounded : Icons.star_border_rounded,
color: widget.saved ? AppColors.blue : AppColors.muted,
),
title: Text(l10n.favorite),
title: Text(widget.l10n.favorite),
onTap: () {
Navigator.pop(sheetContext);
onSave();
widget.onSave();
},
),
ListTile(
key: const Key('player-more-share'),
leading: const Icon(Icons.share_outlined, color: AppColors.muted),
title: Text(l10n.share),
title: Text(widget.l10n.share),
onTap: () {
Navigator.pop(sheetContext);
onShare();
widget.onShare();
},
),
],
),
),
);
if (mounted) _reveal();
}
}
@@ -55,7 +55,7 @@ class HomeFeedOverlay extends StatelessWidget {
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 10, 12),
padding: const EdgeInsets.fromLTRB(0, 0, 12, 16),
child: _rail(),
),
),
@@ -0,0 +1,41 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
/// Shared play/pause control used by the home feed and full-screen player.
class PlayerCenterToggle extends StatelessWidget {
const PlayerCenterToggle({
super.key,
required this.playing,
required this.onTap,
});
final bool playing;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: playing ? 'Pause' : 'Play',
child: GestureDetector(
onTap: onTap,
child: Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Color(0x80000000),
shape: BoxShape.circle,
),
child: Icon(
playing ? Icons.pause_rounded : Icons.play_arrow_rounded,
color: AppColors.onPrimary,
size: 26,
),
),
),
);
}
}