// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; import 'player_text_scale.dart'; const double _sheetRadius = 20; 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, required this.textScale, }); /// 当前倍速。 final double speed; /// 后端下发的清晰度档位。 final List qualities; /// 当前文字缩放比例。 final double textScale; } /// 更多面板动作集合。 class PlayerMoreActions { /// 创建动作集合。 const PlayerMoreActions({ required this.onSpeedChanged, required this.onTextScaleChanged, required this.onClear, required this.onDownload, required this.onReport, }); /// 倍速变化。 final ValueChanged onSpeedChanged; /// 字体大小变化。 final ValueChanged onTextScaleChanged; /// 进入清屏播放。 final VoidCallback onClear; /// 离线缓存当前集。 final VoidCallback onDownload; /// 投诉当前剧。 final VoidCallback onReport; } /// K29 播放页白色更多面板。 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; late double _textScale = widget.state.textScale; @override Widget build(BuildContext context) { final l10n = widget.l10n; return Container( key: const Key('player-more-sheet'), constraints: const BoxConstraints(maxHeight: 620), decoration: const BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.vertical(top: Radius.circular(_sheetRadius)), ), child: SafeArea( top: false, child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(18, 14, 18, 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _header(l10n), _sectionTitle(l10n.playbackSpeed), _speedPicker(), const SizedBox(height: 16), _sectionTitle(l10n.playbackQuality), _qualityPicker(l10n), const SizedBox(height: 12), _disabledRow( key: const Key('player-more-not-interested'), icon: Icons.thumb_down_alt_outlined, title: l10n.notInterested, ), _enabledRow( key: const Key('player-more-clear'), icon: Icons.fullscreen_rounded, title: l10n.clearPlayback, onTap: _clear, ), _disabledSwitchRow( key: const Key('player-more-danmaku'), icon: Icons.subtitles_rounded, title: l10n.danmaku, unavailable: l10n.danmakuUnavailable, ), _disabledRow( key: const Key('player-more-danmaku-settings'), icon: Icons.tune_rounded, title: l10n.danmakuSettings, ), _enabledRow( key: const Key('player-more-offline'), icon: Icons.download_rounded, title: l10n.offlineCache, onTap: () => _closeThen(widget.actions.onDownload), ), _textSizeRow(l10n), _disabledRow( key: const Key('player-more-cast'), icon: Icons.cast_rounded, title: l10n.screenCast, ), _disabledRow( key: const Key('player-more-timer'), icon: Icons.timer_outlined, title: l10n.sleepTimer, ), _enabledRow( key: const Key('player-more-report'), icon: Icons.flag_outlined, title: l10n.reportDrama, onTap: () => _closeThen(widget.actions.onReport), ), ], ), ), ), ); } Widget _header(AppLocalizations l10n) => Row( children: [ Expanded( child: Text( l10n.more, style: const TextStyle( color: AppColors.onSurface, fontSize: 20, fontWeight: FontWeight.w800, ), ), ), IconButton( key: const Key('player-more-close'), tooltip: l10n.close, onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close_rounded), ), ], ); Widget _sectionTitle(String title) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( title, style: const TextStyle( color: AppColors.onSurface, fontSize: 14, fontWeight: FontWeight.w800, ), ), ); Widget _speedPicker() => Wrap( key: const Key('player-more-speed'), spacing: 8, runSpacing: 8, children: [ for (final value in _speedOptions) ChoiceChip( key: Key('player-more-speed-$value'), label: Text('${value}x'), selected: (_speed - value).abs() < 0.001, showCheckmark: false, onSelected: (_) { setState(() => _speed = value); widget.actions.onSpeedChanged(value); }, ), ], ); Widget _qualityPicker(AppLocalizations l10n) => Wrap( key: const Key('player-more-quality'), spacing: 8, runSpacing: 8, children: [ for (final quality in _qualityOptions) _qualityChip(l10n, quality), ], ); Widget _qualityChip(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 ChoiceChip( key: Key('player-more-quality-$quality'), label: Text(enabled ? quality : '$quality · ${l10n.qualityUnavailable}'), selected: selected, showCheckmark: false, onSelected: enabled ? (_) {} : null, ); } Widget _textSizeRow(AppLocalizations l10n) => Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( key: const Key('player-more-text-size'), children: [ const Icon(Icons.format_size_rounded, color: AppColors.muted, size: 21), const SizedBox(width: 12), Expanded( child: Text( l10n.textSize, style: const TextStyle( color: AppColors.onSurface, fontSize: 14, fontWeight: FontWeight.w700, ), ), ), PopupMenuButton( tooltip: l10n.textSize, initialValue: _textScale, onSelected: (value) { setState(() => _textScale = value); widget.actions.onTextScaleChanged(value); }, itemBuilder: (_) => [ PopupMenuItem( key: const Key('player-text-size-small'), value: playerTextSizeSmall, child: Text(l10n.textSizeSmall), ), PopupMenuItem( key: const Key('player-text-size-standard'), value: playerTextSizeStandard, child: Text(l10n.textSizeStandard), ), PopupMenuItem( key: const Key('player-text-size-large'), value: playerTextSizeLarge, child: Text(l10n.textSizeLarge), ), ], child: Text( playerTextSizeLabel(l10n, _textScale), style: const TextStyle( color: AppColors.blue, fontSize: 12, fontWeight: FontWeight.w700, ), ), ), ], ), ); Widget _enabledRow({ required Key key, required IconData icon, required String title, required VoidCallback onTap, }) => _MoreRow(key: key, icon: icon, title: title, enabled: true, onTap: onTap); Widget _disabledRow({ required Key key, required IconData icon, required String title, }) => _MoreRow(key: key, icon: icon, title: title, enabled: false); Widget _disabledSwitchRow({ required Key key, required IconData icon, required String title, required String unavailable, }) => _MoreRow( key: key, icon: icon, title: title, enabled: false, value: unavailable, trailing: const Switch(value: false, onChanged: null), ); void _clear() { Navigator.pop(context); WidgetsBinding.instance.addPostFrameCallback( (_) => widget.actions.onClear(), ); } void _closeThen(VoidCallback action) { Navigator.pop(context); WidgetsBinding.instance.addPostFrameCallback((_) => action()); } } class _MoreRow extends StatelessWidget { const _MoreRow({ super.key, required this.icon, required this.title, required this.enabled, this.value = '', this.trailing, this.onTap, }); final IconData icon; final String title; final bool enabled; final String value; final Widget? trailing; final VoidCallback? onTap; @override Widget build(BuildContext context) { final color = enabled ? AppColors.onSurface : AppColors.subtle; return Material( color: Colors.transparent, borderRadius: BorderRadius.circular(10), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: enabled ? onTap : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 11), child: Row( children: [ Icon(icon, color: color, size: 21), const SizedBox(width: 12), Expanded( child: Text( title, style: TextStyle( color: color, fontSize: 14, fontWeight: FontWeight.w600, ), ), ), if (value.isNotEmpty) Text( value, style: const TextStyle(color: AppColors.subtle, fontSize: 11), ), ?trailing, ], ), ), ), ); } }