// Author: fruit.com.cn import 'package:flutter/material.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 'player_center_toggle.dart'; import 'player_overlay_models.dart'; import 'seek_progress_bar.dart'; part 'player_overlay_parts.dart'; const double _topButtonSize = 36; const double _topBarHeight = 58; const double _railButtonSize = 34; const double _railGap = 18; const List _playerTextShadows = [ Shadow(color: Colors.black54, blurRadius: 5, offset: Offset(0, 1)), ]; /// 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, ), ), _top(context), _bottom(context), if (controlsVisible) _rail(context), 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), 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, shadows: _playerTextShadows, ), ), ), _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: MediaQuery.sizeOf(context).height * (compact ? 0.42 : 0.55), 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, ), ); } Widget _bottom(BuildContext context) { return Positioned( left: 0, right: 0, bottom: 0, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (controlsVisible) _bottomInfo(context), _bottomProgress(), _bottomActions(), ], ), ); } 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), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _detailRow(series), const SizedBox(height: 4), _EpisodeSynopsis( key: const Key('player-episode-synopsis'), l10n: data.l10n, episodeNumber: data.episode.number, synopsis: synopsis, authorNote: data.authorNote, ), ], ), ); } Widget _bottomProgress() { return Padding( key: const Key('player-progress-container'), padding: EdgeInsets.fromLTRB(14, controlsVisible ? 2 : 8, 14, 0), child: SeekProgressBar( key: const Key('player-progress'), valueListenable: data.controller, semanticsLabel: data.l10n.seekVideo, showTimes: true, compact: true, inactiveTrackColor: Colors.white24, onScrubStart: actions.onScrubStart, onSeek: actions.onSeek, ), ); } 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: 1), 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: 18, fontWeight: FontWeight.w800, shadows: _playerTextShadows, ), ), ), const SizedBox(width: 6), const Icon( Icons.chevron_right_rounded, color: AppColors.playerMuted, size: 20, shadows: _playerTextShadows, ), ], ), ), ), ); } Widget _bottomActions() { return Padding( key: const Key('player-bottom-actions'), padding: const EdgeInsets.fromLTRB(14, 6, 14, 6), child: Row( children: [ Expanded( child: Material( key: const Key('player-episodes-pill'), color: AppColors.playerOverlayPill, borderRadius: BorderRadius.circular(999), clipBehavior: Clip.antiAlias, child: InkWell( key: const Key('player-episodes'), onTap: actions.onEpisodes, borderRadius: BorderRadius.circular(999), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), 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: 11, fontWeight: FontWeight.w700, shadows: _playerTextShadows, ), ), ), const SizedBox(width: 3), const Icon( Icons.keyboard_arrow_up_rounded, color: AppColors.onPrimary, size: 16, shadows: _playerTextShadows, ), ], ), ), ), ), ), const SizedBox(width: 8), _BottomIconButton( key: const Key('player-bottom-clear-screen'), tooltip: controlsVisible ? data.l10n.clearScreen : data.l10n.exitClearScreen, icon: controlsVisible ? Icons.fullscreen_rounded : Icons.fullscreen_exit_rounded, onPressed: controlsVisible ? actions.onClear : actions.onExitClear, ), ], ), ); } }