// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; const double _sheetRadius = 20; const double _rowHeight = 44; const double _groupGap = 12; const double _segmentHeight = 28; const double _segmentRadius = 8; const double _segmentPadding = 10; const double _segmentGap = 6; const List _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; const List _qualityOptions = ['AUTO', '720P', '1080P']; const String _autoQuality = 'AUTO'; /// 更多面板运行状态。 class PlayerMoreState { /// 创建更多面板状态。 const PlayerMoreState({required this.speed, required this.qualities}); /// 当前倍速。 final double speed; /// 后端下发的清晰度档位。 final List qualities; } /// 更多面板动作集合。 class PlayerMoreActions { /// 创建动作集合。 const PlayerMoreActions({ required this.onSpeedChanged, required this.onClear, required this.onDownload, }); /// 倍速变化。 final ValueChanged onSpeedChanged; /// 进入清屏播放。 final VoidCallback onClear; /// 离线缓存当前集。 final VoidCallback onDownload; } /// K29.6 精简版播放页更多面板。 class PlayerMoreSheet extends StatefulWidget { /// 创建更多面板。 const PlayerMoreSheet({ super.key, required this.l10n, required this.state, required this.actions, }); /// 当前语言包。 final AppLocalizations l10n; /// 面板状态。 final PlayerMoreState state; /// 面板动作。 final PlayerMoreActions actions; @override State createState() => _PlayerMoreSheetState(); } class _PlayerMoreSheetState extends State { late double _speed = widget.state.speed; @override Widget build(BuildContext context) { final l10n = widget.l10n; return Material( key: const Key('player-more-sheet'), color: AppColors.card, borderRadius: const BorderRadius.vertical( top: Radius.circular(_sheetRadius), ), clipBehavior: Clip.antiAlias, child: SafeArea( top: false, child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( key: const Key('player-more-drag-handle'), width: 36, height: 4, decoration: BoxDecoration( color: AppColors.border, borderRadius: BorderRadius.circular(999), ), ), ), const SizedBox(height: 8), _header(l10n), _sectionTitle(l10n.playbackSpeed), _speedPicker(), const SizedBox(height: _groupGap), _sectionTitle(l10n.playbackQuality), _qualityPicker(l10n), const SizedBox(height: _groupGap), _actionRow( key: const Key('player-more-clear'), icon: Icons.fullscreen_rounded, title: l10n.clearPlayback, onTap: _clear, ), _actionRow( key: const Key('player-more-offline'), icon: Icons.download_rounded, title: l10n.offlineCache, onTap: () => _closeThen(widget.actions.onDownload), ), ], ), ), ), ); } Widget _header(AppLocalizations l10n) => Row( children: [ Expanded( child: Text( l10n.more, style: const TextStyle( color: AppColors.onSurface, fontSize: 15, fontWeight: FontWeight.w700, ), ), ), SizedBox.square( dimension: 32, child: IconButton( key: const Key('player-more-close'), tooltip: l10n.close, padding: EdgeInsets.zero, onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close_rounded, size: 18), ), ), ], ); Widget _sectionTitle(String title) => Padding( padding: const EdgeInsets.only(bottom: 8, top: 2), child: Text( title, style: const TextStyle( color: AppColors.muted, fontSize: 12, fontWeight: FontWeight.w600, ), ), ); Widget _speedPicker() => Wrap( key: const Key('player-more-speed'), spacing: _segmentGap, runSpacing: _segmentGap, children: [ for (final value in _speedOptions) _SegmentButton( key: Key('player-more-speed-$value'), label: '${value}x', selected: (_speed - value).abs() < 0.001, onTap: () { setState(() => _speed = value); widget.actions.onSpeedChanged(value); }, ), ], ); Widget _qualityPicker(AppLocalizations l10n) => Wrap( key: const Key('player-more-quality'), spacing: _segmentGap, runSpacing: _segmentGap, children: [ for (final quality in _qualityOptions) _qualitySegment(l10n, quality), ], ); Widget _qualitySegment(AppLocalizations l10n, String quality) { final enabled = widget.state.qualities.contains(quality); final selected = quality == _autoQuality && enabled || !widget.state.qualities.contains(_autoQuality) && widget.state.qualities.isNotEmpty && widget.state.qualities.first == quality; return _SegmentButton( key: Key('player-more-quality-$quality'), label: enabled ? quality : '$quality · ${l10n.qualityUnavailable}', selected: selected, enabled: enabled, onTap: enabled ? () {} : null, ); } Widget _actionRow({ required Key key, required IconData icon, required String title, required VoidCallback onTap, }) { return Material( key: key, color: Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: onTap, child: SizedBox( height: _rowHeight, child: Row( children: [ Icon(icon, color: AppColors.muted, size: 20), const SizedBox(width: 12), Expanded( child: Text( title, style: const TextStyle( color: AppColors.onSurface, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), const Icon( Icons.chevron_right_rounded, color: AppColors.subtle, size: 18, ), ], ), ), ), ); } void _clear() { Navigator.pop(context); WidgetsBinding.instance.addPostFrameCallback( (_) => widget.actions.onClear(), ); } void _closeThen(VoidCallback action) { Navigator.pop(context); WidgetsBinding.instance.addPostFrameCallback((_) => action()); } } class _SegmentButton extends StatelessWidget { const _SegmentButton({ super.key, required this.label, required this.selected, this.enabled = true, this.onTap, }); final String label; final bool selected; final bool enabled; final VoidCallback? onTap; @override Widget build(BuildContext context) { final background = selected ? AppColors.blue : enabled ? AppColors.raised : AppColors.cardAlt; final foreground = selected ? AppColors.onPrimary : enabled ? AppColors.onSurface : AppColors.subtle; return Material( color: background, borderRadius: BorderRadius.circular(_segmentRadius), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(_segmentRadius), child: SizedBox( height: _segmentHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: _segmentPadding), child: Center( child: Text( label, maxLines: 1, style: TextStyle( color: foreground, fontSize: 12, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, ), ), ), ), ), ), ); } }