406 lines
11 KiB
Dart
406 lines
11 KiB
Dart
// 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<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0];
|
|
const List<String> _qualityOptions = ['AUTO', '720P', '1080P'];
|
|
const String _autoQuality = 'AUTO';
|
|
|
|
/// 更多面板运行状态。
|
|
class PlayerMoreState {
|
|
/// 创建更多面板状态。
|
|
const PlayerMoreState({
|
|
required this.speed,
|
|
required this.qualities,
|
|
required this.textScale,
|
|
required this.pipAvailable,
|
|
});
|
|
|
|
/// 当前倍速。
|
|
final double speed;
|
|
|
|
/// 后端下发的清晰度档位。
|
|
final List<String> qualities;
|
|
|
|
/// 当前文字缩放比例。
|
|
final double textScale;
|
|
|
|
/// 设备是否提供画中画入口。
|
|
final bool pipAvailable;
|
|
}
|
|
|
|
/// 更多面板动作集合。
|
|
class PlayerMoreActions {
|
|
/// 创建动作集合。
|
|
const PlayerMoreActions({
|
|
required this.onSpeedChanged,
|
|
required this.onTextScaleChanged,
|
|
required this.onClear,
|
|
required this.onDownload,
|
|
required this.onReport,
|
|
this.onPip,
|
|
});
|
|
|
|
/// 倍速变化。
|
|
final ValueChanged<double> onSpeedChanged;
|
|
|
|
/// 字体大小变化。
|
|
final ValueChanged<double> onTextScaleChanged;
|
|
|
|
/// 进入清屏播放。
|
|
final VoidCallback onClear;
|
|
|
|
/// 离线缓存当前集。
|
|
final VoidCallback onDownload;
|
|
|
|
/// 投诉当前剧。
|
|
final VoidCallback onReport;
|
|
|
|
/// 进入画中画;平台不支持时为空。
|
|
final VoidCallback? onPip;
|
|
}
|
|
|
|
/// 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<PlayerMoreSheet> createState() => _PlayerMoreSheetState();
|
|
}
|
|
|
|
class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
|
|
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),
|
|
),
|
|
if (widget.state.pipAvailable && widget.actions.onPip != null)
|
|
_enabledRow(
|
|
key: const Key('player-more-pip'),
|
|
icon: Icons.picture_in_picture_alt_rounded,
|
|
title: l10n.pictureInPicture,
|
|
onTap: () => _closeThen(widget.actions.onPip!),
|
|
),
|
|
_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<double>(
|
|
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,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|