// Author: fruit.com.cn part of 'player_overlay.dart'; class _TopButton extends StatelessWidget { const _TopButton({ super.key, required this.tooltip, required this.icon, required this.onPressed, }); final String tooltip; final IconData icon; final VoidCallback onPressed; @override Widget build(BuildContext context) { return SizedBox.square( dimension: _topButtonSize, child: IconButton( tooltip: tooltip, padding: EdgeInsets.zero, style: IconButton.styleFrom( backgroundColor: const Color(0x80000000), foregroundColor: AppColors.onPrimary, ), onPressed: onPressed, icon: Icon(icon, size: 18), ), ); } } class _SpeedEntry extends StatelessWidget { const _SpeedEntry({super.key, required this.speed, required this.onTap}); final double speed; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: const Color(0x80000000), borderRadius: BorderRadius.circular(999), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(999), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.speed_rounded, color: AppColors.onPrimary, size: 15, ), const SizedBox(width: 4), Text( '${speed}x', style: const TextStyle( color: AppColors.onPrimary, fontSize: 12, fontWeight: FontWeight.w700, ), ), ], ), ), ), ); } } class _Rail extends StatelessWidget { const _Rail({ required this.l10n, required this.liked, required this.saved, required this.likeCount, required this.favoriteCount, required this.commentCount, required this.onLike, required this.onComments, required this.onSave, required this.onShare, required this.compact, }); final AppLocalizations l10n; final bool liked; final bool saved; final int? likeCount; final int? favoriteCount; final int? commentCount; final VoidCallback onLike; final VoidCallback onComments; final VoidCallback onSave; final VoidCallback onShare; final bool compact; @override Widget build(BuildContext context) { return Column( key: const Key('player-interaction-rail'), children: [ _RailButton( key: const Key('player-rail-save'), icon: saved ? Icons.star_rounded : Icons.star_border_rounded, label: favoriteCount?.toString() ?? l10n.favorite, active: saved, onTap: onSave, compact: compact, ), _RailButton( key: const Key('player-rail-comments'), icon: Icons.chat_bubble_outline_rounded, label: commentCount?.toString() ?? l10n.comments, onTap: onComments, compact: compact, ), _RailButton( key: const Key('player-rail-like'), icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded, label: likeCount?.toString() ?? l10n.like, active: liked, onTap: onLike, compact: compact, ), _RailButton( key: const Key('player-rail-share'), icon: Icons.ios_share_rounded, label: l10n.share, onTap: onShare, compact: compact, ), ], ); } } class _RailButton extends StatelessWidget { const _RailButton({ super.key, required this.icon, required this.label, required this.onTap, required this.compact, this.active = false, }); final IconData icon; final String label; final VoidCallback onTap; final bool compact; final bool active; @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: compact ? 3 : _railGap), child: Column( children: [ SizedBox.square( dimension: _railButtonSize, child: IconButton( onPressed: onTap, padding: EdgeInsets.zero, style: IconButton.styleFrom( backgroundColor: const Color(0x80000000), foregroundColor: active ? AppColors.blue : AppColors.onPrimary, ), icon: Icon(icon, size: 18), ), ), SizedBox(height: compact ? 1 : 3), Text( label, maxLines: 1, style: TextStyle( color: AppColors.onPrimary, fontSize: compact ? 8 : 9, fontWeight: FontWeight.w600, ), ), ], ), ); } } class _EpisodeSynopsis extends StatefulWidget { const _EpisodeSynopsis({ super.key, required this.l10n, required this.episodeNumber, required this.synopsis, required this.textScale, }); final AppLocalizations l10n; final int episodeNumber; final String synopsis; final double textScale; @override State<_EpisodeSynopsis> createState() => _EpisodeSynopsisState(); } class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { bool _expanded = false; @override Widget build(BuildContext context) { final canExpand = widget.synopsis.trim().length > 28; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.l10n.episodeLabel(widget.episodeNumber), key: const Key('player-episode-label'), style: TextStyle( color: AppColors.blue, fontSize: 11 * widget.textScale, fontWeight: FontWeight.w700, ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 6), child: Text('|', style: TextStyle(color: Colors.white38)), ), Expanded( child: Text( widget.synopsis, key: const Key('player-synopsis'), maxLines: _expanded ? 4 : 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white70, fontSize: 11 * widget.textScale, height: 1.45, ), ), ), ], ), if (canExpand) GestureDetector( key: const Key('player-synopsis-toggle'), onTap: () => setState(() => _expanded = !_expanded), child: Padding( padding: const EdgeInsets.only(top: 2), child: Text( _expanded ? widget.l10n.showLess : widget.l10n.showMore, style: const TextStyle( color: Colors.white54, fontSize: 10, fontWeight: FontWeight.w600, ), ), ), ), ], ); } } class _PlayerTimeLabel extends StatelessWidget { const _PlayerTimeLabel({super.key, required this.controller}); final VideoPlayerController? controller; @override Widget build(BuildContext context) { final source = controller; if (source == null) return _label(Duration.zero, Duration.zero); return ValueListenableBuilder( valueListenable: source, builder: (context, value, _) => _label(value.position, value.duration), ); } Widget _label(Duration position, Duration duration) => Align( alignment: Alignment.centerRight, child: Text( '${_format(position)} / ${_format(duration)}', style: const TextStyle( color: Colors.white60, fontSize: 9, fontWeight: FontWeight.w600, ), ), ); String _format(Duration value) { final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0'); return '$minutes:$seconds'; } }