// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import '../../../core/formatting/localized_content.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../l10n/app_localizations.dart'; import 'exit_clear_screen_button.dart'; import 'player_center_toggle.dart'; import 'player_overlay_models.dart'; import 'seek_progress_bar.dart'; part 'player_overlay_parts.dart'; const double _topBarHeight = 58; const double _topButtonSize = 36; const double _railButtonSize = 36; const double _railGap = 9; const double _fullScreenPillBottom = 246; const double _panelRadius = 16; /// K29 竖屏播放页浮层。 class PlayerPlaybackOverlay extends StatelessWidget { /// 创建播放浮层。 const PlayerPlaybackOverlay({ super.key, required this.data, required this.actions, required this.controlsVisible, }); /// 展示数据。 final PlayerOverlayData data; /// 交互动作。 final PlayerOverlayActions actions; /// 控制层是否可见。 final bool controlsVisible; @override Widget build(BuildContext context) { return SafeArea( child: Stack( children: [ Positioned.fill( child: GestureDetector( key: const Key('player-video-tap'), behavior: HitTestBehavior.opaque, onTap: actions.onTogglePlay, ), ), if (controlsVisible) ...[ _top(context), _bottom(context), _rail(context), ], _fullScreenPill(), if (!controlsVisible) Positioned( top: AppDimens.spaceSm, right: AppDimens.spaceMd, child: ExitClearScreenButton( key: const Key('player-exit-clear-screen'), tooltip: data.l10n.exitClearScreen, onPressed: actions.onExitClear, ), ), if (!data.playing) Center( child: PlayerCenterToggle( key: const Key('player-play-toggle'), playing: false, onTap: actions.onTogglePlay, ), ), ], ), ); } Widget _top(BuildContext context) { return Positioned( top: 0, left: 0, right: 0, child: Container( key: const Key('player-controls-top'), height: _topBarHeight, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xCC000000), Color(0x00000000)], ), ), child: 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 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, ), ), ), _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, ), ], ), ), ); } Widget _rail(BuildContext context) { final compact = MediaQuery.sizeOf(context).height < 700; return Positioned( right: AppDimens.spaceMd, top: _topBarHeight + (compact ? 18 : 54), child: _Rail( l10n: data.l10n, liked: data.liked, saved: data.saved, likeCount: data.likeCount, favoriteCount: data.favoriteCount, commentCount: data.commentCount, onLike: actions.onLike, onComments: actions.onComments, onSave: actions.onSave, onShare: actions.onShare, compact: compact, ), ); } Widget _bottom(BuildContext context) { final series = localizedDramaTitle(data.l10n, data.drama); final synopsis = localizedDramaSynopsis(data.l10n, data.drama); return Positioned( left: 0, right: 0, bottom: 0, 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, ), ], ), ), ), ), ), 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: controlsVisible ? actions.onClear : actions.onExitClear, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.fullscreen_rounded, color: AppColors.onPrimary, size: 16, ), const SizedBox(width: 5), Text( controlsVisible ? data.l10n.fullScreenWatch : data.l10n.exitClearScreen, style: const TextStyle( color: AppColors.onPrimary, fontSize: 12, fontWeight: FontWeight.w700, ), ), ], ), ), ), ), ), ); } }