merge: 合并 K29.6 更多面板精简
This commit is contained in:
commit
1e3e5b64dc
8 files changed
+175
-310
No files matched your search
@@ -21,7 +21,6 @@ import '../../core/theme/app_theme.dart';
|
|||||||
import '../../core/widgets/auth_gate.dart';
|
import '../../core/widgets/auth_gate.dart';
|
||||||
import '../../core/widgets/download_action.dart';
|
import '../../core/widgets/download_action.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../interaction/widgets/report_dialog.dart';
|
|
||||||
import 'player_binding.dart';
|
import 'player_binding.dart';
|
||||||
import 'player_logic.dart';
|
import 'player_logic.dart';
|
||||||
import 'widgets/home_feed_overlay.dart';
|
import 'widgets/home_feed_overlay.dart';
|
||||||
@@ -83,7 +82,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
late final PlayerLogic logic;
|
late final PlayerLogic logic;
|
||||||
bool _chromeVisible = true;
|
bool _chromeVisible = true;
|
||||||
double _playbackSpeed = 1.0;
|
double _playbackSpeed = 1.0;
|
||||||
double _textScale = 1.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -125,10 +123,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
setState(() => _playbackSpeed = value);
|
setState(() => _playbackSpeed = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateTextScale(double value) {
|
|
||||||
setState(() => _textScale = value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _hideChrome() {
|
void _hideChrome() {
|
||||||
setState(() => _chromeVisible = false);
|
setState(() => _chromeVisible = false);
|
||||||
}
|
}
|
||||||
@@ -389,7 +383,6 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
||||||
playing: logic.session.userWantsPlay,
|
playing: logic.session.userWantsPlay,
|
||||||
playbackSpeed: _playbackSpeed,
|
playbackSpeed: _playbackSpeed,
|
||||||
textScale: _textScale,
|
|
||||||
totalEpisodes: logic.visibleIndices.length,
|
totalEpisodes: logic.visibleIndices.length,
|
||||||
),
|
),
|
||||||
actions: PlayerOverlayActions(
|
actions: PlayerOverlayActions(
|
||||||
@@ -408,7 +401,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
unawaited(_toggleLike(context, interactions, drama)),
|
unawaited(_toggleLike(context, interactions, drama)),
|
||||||
onSave: () => unawaited(_toggleSave(context, drama)),
|
onSave: () => unawaited(_toggleSave(context, drama)),
|
||||||
onShare: () => logic.shareDrama(context, drama),
|
onShare: () => logic.shareDrama(context, drama),
|
||||||
onMore: () => _openMore(context, drama, episode),
|
onMore: () => _openMore(context, episode),
|
||||||
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
||||||
onSeek: ready
|
onSeek: ready
|
||||||
? (position) => logic.seekTo(context, videoPort!, position)
|
? (position) => logic.seekTo(context, videoPort!, position)
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ extension _PlayerViewActions on _PlayerPageState {
|
|||||||
await logic.setPlaybackSpeed(selected);
|
await logic.setPlaybackSpeed(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openMore(
|
Future<void> _openMore(BuildContext context, Episode episode) async {
|
||||||
BuildContext context,
|
|
||||||
Drama drama,
|
|
||||||
Episode episode,
|
|
||||||
) async {
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -31,21 +27,14 @@ extension _PlayerViewActions on _PlayerPageState {
|
|||||||
state: PlayerMoreState(
|
state: PlayerMoreState(
|
||||||
speed: _playbackSpeed,
|
speed: _playbackSpeed,
|
||||||
qualities: logic.qualities,
|
qualities: logic.qualities,
|
||||||
textScale: _textScale,
|
|
||||||
pipAvailable: logic.pictureInPicture.isPlatformSupported,
|
|
||||||
),
|
),
|
||||||
actions: PlayerMoreActions(
|
actions: PlayerMoreActions(
|
||||||
onSpeedChanged: (value) {
|
onSpeedChanged: (value) {
|
||||||
_updatePlaybackSpeed(value);
|
_updatePlaybackSpeed(value);
|
||||||
unawaited(logic.setPlaybackSpeed(value));
|
unawaited(logic.setPlaybackSpeed(value));
|
||||||
},
|
},
|
||||||
onTextScaleChanged: _updateTextScale,
|
|
||||||
onClear: _hideChrome,
|
onClear: _hideChrome,
|
||||||
onDownload: () => requestEpisodeDownload(context, episode.id),
|
onDownload: () => requestEpisodeDownload(context, episode.id),
|
||||||
onReport: () => unawaited(_reportDrama(context, drama, l10n)),
|
|
||||||
onPip: logic.pictureInPicture.isPlatformSupported
|
|
||||||
? () => unawaited(logic.enterPip(context))
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -86,32 +75,6 @@ extension _PlayerViewActions on _PlayerPageState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reportDrama(
|
|
||||||
BuildContext context,
|
|
||||||
Drama drama,
|
|
||||||
AppLocalizations l10n,
|
|
||||||
) async {
|
|
||||||
if (!await ensureSignedIn(context) || !context.mounted) return;
|
|
||||||
final draft = await showReportDialog(context, title: l10n.reportDrama);
|
|
||||||
if (draft == null || !context.mounted) return;
|
|
||||||
unawaited(context.read<Haptics>().lightImpact());
|
|
||||||
final result = await context.read<InteractionController>().reportDrama(
|
|
||||||
drama.id,
|
|
||||||
reason: draft.reason,
|
|
||||||
description: draft.description,
|
|
||||||
);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
result.isSuccess
|
|
||||||
? l10n.reportSubmitted
|
|
||||||
: result.errorMessage ?? l10n.reportFailed,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _toggleLike(
|
Future<void> _toggleLike(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
InteractionController interactions,
|
InteractionController interactions,
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import 'player_text_scale.dart';
|
|
||||||
|
|
||||||
const double _sheetRadius = 20;
|
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<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0];
|
const List<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0];
|
||||||
const List<String> _qualityOptions = ['AUTO', '720P', '1080P'];
|
const List<String> _qualityOptions = ['AUTO', '720P', '1080P'];
|
||||||
const String _autoQuality = 'AUTO';
|
const String _autoQuality = 'AUTO';
|
||||||
@@ -14,24 +19,13 @@ const String _autoQuality = 'AUTO';
|
|||||||
/// 更多面板运行状态。
|
/// 更多面板运行状态。
|
||||||
class PlayerMoreState {
|
class PlayerMoreState {
|
||||||
/// 创建更多面板状态。
|
/// 创建更多面板状态。
|
||||||
const PlayerMoreState({
|
const PlayerMoreState({required this.speed, required this.qualities});
|
||||||
required this.speed,
|
|
||||||
required this.qualities,
|
|
||||||
required this.textScale,
|
|
||||||
required this.pipAvailable,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 当前倍速。
|
/// 当前倍速。
|
||||||
final double speed;
|
final double speed;
|
||||||
|
|
||||||
/// 后端下发的清晰度档位。
|
/// 后端下发的清晰度档位。
|
||||||
final List<String> qualities;
|
final List<String> qualities;
|
||||||
|
|
||||||
/// 当前文字缩放比例。
|
|
||||||
final double textScale;
|
|
||||||
|
|
||||||
/// 设备是否提供画中画入口。
|
|
||||||
final bool pipAvailable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更多面板动作集合。
|
/// 更多面板动作集合。
|
||||||
@@ -39,33 +33,21 @@ class PlayerMoreActions {
|
|||||||
/// 创建动作集合。
|
/// 创建动作集合。
|
||||||
const PlayerMoreActions({
|
const PlayerMoreActions({
|
||||||
required this.onSpeedChanged,
|
required this.onSpeedChanged,
|
||||||
required this.onTextScaleChanged,
|
|
||||||
required this.onClear,
|
required this.onClear,
|
||||||
required this.onDownload,
|
required this.onDownload,
|
||||||
required this.onReport,
|
|
||||||
this.onPip,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 倍速变化。
|
/// 倍速变化。
|
||||||
final ValueChanged<double> onSpeedChanged;
|
final ValueChanged<double> onSpeedChanged;
|
||||||
|
|
||||||
/// 字体大小变化。
|
|
||||||
final ValueChanged<double> onTextScaleChanged;
|
|
||||||
|
|
||||||
/// 进入清屏播放。
|
/// 进入清屏播放。
|
||||||
final VoidCallback onClear;
|
final VoidCallback onClear;
|
||||||
|
|
||||||
/// 离线缓存当前集。
|
/// 离线缓存当前集。
|
||||||
final VoidCallback onDownload;
|
final VoidCallback onDownload;
|
||||||
|
|
||||||
/// 投诉当前剧。
|
|
||||||
final VoidCallback onReport;
|
|
||||||
|
|
||||||
/// 进入画中画;平台不支持时为空。
|
|
||||||
final VoidCallback? onPip;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// K29 播放页白色更多面板。
|
/// K29.6 精简版播放页更多面板。
|
||||||
class PlayerMoreSheet extends StatefulWidget {
|
class PlayerMoreSheet extends StatefulWidget {
|
||||||
/// 创建更多面板。
|
/// 创建更多面板。
|
||||||
const PlayerMoreSheet({
|
const PlayerMoreSheet({
|
||||||
@@ -90,84 +72,55 @@ class PlayerMoreSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
|
class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
|
||||||
late double _speed = widget.state.speed;
|
late double _speed = widget.state.speed;
|
||||||
late double _textScale = widget.state.textScale;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = widget.l10n;
|
final l10n = widget.l10n;
|
||||||
return Container(
|
return Material(
|
||||||
key: const Key('player-more-sheet'),
|
key: const Key('player-more-sheet'),
|
||||||
constraints: const BoxConstraints(maxHeight: 620),
|
color: AppColors.card,
|
||||||
decoration: const BoxDecoration(
|
borderRadius: const BorderRadius.vertical(
|
||||||
color: AppColors.card,
|
top: Radius.circular(_sheetRadius),
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(_sheetRadius)),
|
|
||||||
),
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(18, 14, 18, 18),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
_header(l10n),
|
||||||
_sectionTitle(l10n.playbackSpeed),
|
_sectionTitle(l10n.playbackSpeed),
|
||||||
_speedPicker(),
|
_speedPicker(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: _groupGap),
|
||||||
_sectionTitle(l10n.playbackQuality),
|
_sectionTitle(l10n.playbackQuality),
|
||||||
_qualityPicker(l10n),
|
_qualityPicker(l10n),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: _groupGap),
|
||||||
_disabledRow(
|
_actionRow(
|
||||||
key: const Key('player-more-not-interested'),
|
|
||||||
icon: Icons.thumb_down_alt_outlined,
|
|
||||||
title: l10n.notInterested,
|
|
||||||
),
|
|
||||||
_enabledRow(
|
|
||||||
key: const Key('player-more-clear'),
|
key: const Key('player-more-clear'),
|
||||||
icon: Icons.fullscreen_rounded,
|
icon: Icons.fullscreen_rounded,
|
||||||
title: l10n.clearPlayback,
|
title: l10n.clearPlayback,
|
||||||
onTap: _clear,
|
onTap: _clear,
|
||||||
),
|
),
|
||||||
_disabledSwitchRow(
|
_actionRow(
|
||||||
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'),
|
key: const Key('player-more-offline'),
|
||||||
icon: Icons.download_rounded,
|
icon: Icons.download_rounded,
|
||||||
title: l10n.offlineCache,
|
title: l10n.offlineCache,
|
||||||
onTap: () => _closeThen(widget.actions.onDownload),
|
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),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -182,44 +135,47 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
|
|||||||
l10n.more,
|
l10n.more,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.onSurface,
|
color: AppColors.onSurface,
|
||||||
fontSize: 20,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
SizedBox.square(
|
||||||
key: const Key('player-more-close'),
|
dimension: 32,
|
||||||
tooltip: l10n.close,
|
child: IconButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
key: const Key('player-more-close'),
|
||||||
icon: const Icon(Icons.close_rounded),
|
tooltip: l10n.close,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
icon: const Icon(Icons.close_rounded, size: 18),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _sectionTitle(String title) => Padding(
|
Widget _sectionTitle(String title) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8, top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.onSurface,
|
color: AppColors.muted,
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _speedPicker() => Wrap(
|
Widget _speedPicker() => Wrap(
|
||||||
key: const Key('player-more-speed'),
|
key: const Key('player-more-speed'),
|
||||||
spacing: 8,
|
spacing: _segmentGap,
|
||||||
runSpacing: 8,
|
runSpacing: _segmentGap,
|
||||||
children: [
|
children: [
|
||||||
for (final value in _speedOptions)
|
for (final value in _speedOptions)
|
||||||
ChoiceChip(
|
_SegmentButton(
|
||||||
key: Key('player-more-speed-$value'),
|
key: Key('player-more-speed-$value'),
|
||||||
label: Text('${value}x'),
|
label: '${value}x',
|
||||||
selected: (_speed - value).abs() < 0.001,
|
selected: (_speed - value).abs() < 0.001,
|
||||||
showCheckmark: false,
|
onTap: () {
|
||||||
onSelected: (_) {
|
|
||||||
setState(() => _speed = value);
|
setState(() => _speed = value);
|
||||||
widget.actions.onSpeedChanged(value);
|
widget.actions.onSpeedChanged(value);
|
||||||
},
|
},
|
||||||
@@ -229,110 +185,69 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
|
|||||||
|
|
||||||
Widget _qualityPicker(AppLocalizations l10n) => Wrap(
|
Widget _qualityPicker(AppLocalizations l10n) => Wrap(
|
||||||
key: const Key('player-more-quality'),
|
key: const Key('player-more-quality'),
|
||||||
spacing: 8,
|
spacing: _segmentGap,
|
||||||
runSpacing: 8,
|
runSpacing: _segmentGap,
|
||||||
children: [
|
children: [
|
||||||
for (final quality in _qualityOptions) _qualityChip(l10n, quality),
|
for (final quality in _qualityOptions) _qualitySegment(l10n, quality),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _qualityChip(AppLocalizations l10n, String quality) {
|
Widget _qualitySegment(AppLocalizations l10n, String quality) {
|
||||||
final enabled = widget.state.qualities.contains(quality);
|
final enabled = widget.state.qualities.contains(quality);
|
||||||
final selected =
|
final selected =
|
||||||
quality == _autoQuality && enabled ||
|
quality == _autoQuality && enabled ||
|
||||||
!widget.state.qualities.contains(_autoQuality) &&
|
!widget.state.qualities.contains(_autoQuality) &&
|
||||||
widget.state.qualities.isNotEmpty &&
|
widget.state.qualities.isNotEmpty &&
|
||||||
widget.state.qualities.first == quality;
|
widget.state.qualities.first == quality;
|
||||||
return ChoiceChip(
|
return _SegmentButton(
|
||||||
key: Key('player-more-quality-$quality'),
|
key: Key('player-more-quality-$quality'),
|
||||||
label: Text(enabled ? quality : '$quality · ${l10n.qualityUnavailable}'),
|
label: enabled ? quality : '$quality · ${l10n.qualityUnavailable}',
|
||||||
selected: selected,
|
selected: selected,
|
||||||
showCheckmark: false,
|
enabled: enabled,
|
||||||
onSelected: enabled ? (_) {} : null,
|
onTap: enabled ? () {} : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _textSizeRow(AppLocalizations l10n) => Padding(
|
Widget _actionRow({
|
||||||
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 Key key,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) =>
|
}) {
|
||||||
_MoreRow(key: key, icon: icon, title: title, enabled: true, onTap: onTap);
|
return Material(
|
||||||
|
key: key,
|
||||||
Widget _disabledRow({
|
color: Colors.transparent,
|
||||||
required Key key,
|
borderRadius: BorderRadius.circular(8),
|
||||||
required IconData icon,
|
child: InkWell(
|
||||||
required String title,
|
borderRadius: BorderRadius.circular(8),
|
||||||
}) => _MoreRow(key: key, icon: icon, title: title, enabled: false);
|
onTap: onTap,
|
||||||
|
child: SizedBox(
|
||||||
Widget _disabledSwitchRow({
|
height: _rowHeight,
|
||||||
required Key key,
|
child: Row(
|
||||||
required IconData icon,
|
children: [
|
||||||
required String title,
|
Icon(icon, color: AppColors.muted, size: 20),
|
||||||
required String unavailable,
|
const SizedBox(width: 12),
|
||||||
}) => _MoreRow(
|
Expanded(
|
||||||
key: key,
|
child: Text(
|
||||||
icon: icon,
|
title,
|
||||||
title: title,
|
style: const TextStyle(
|
||||||
enabled: false,
|
color: AppColors.onSurface,
|
||||||
value: unavailable,
|
fontSize: 14,
|
||||||
trailing: const Switch(value: false, onChanged: null),
|
fontWeight: FontWeight.w600,
|
||||||
);
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
color: AppColors.subtle,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _clear() {
|
void _clear() {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@@ -347,56 +262,53 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MoreRow extends StatelessWidget {
|
class _SegmentButton extends StatelessWidget {
|
||||||
const _MoreRow({
|
const _SegmentButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.label,
|
||||||
required this.title,
|
required this.selected,
|
||||||
required this.enabled,
|
this.enabled = true,
|
||||||
this.value = '',
|
|
||||||
this.trailing,
|
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final String label;
|
||||||
final String title;
|
final bool selected;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final String value;
|
|
||||||
final Widget? trailing;
|
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = enabled ? AppColors.onSurface : AppColors.subtle;
|
final background = selected
|
||||||
|
? AppColors.blue
|
||||||
|
: enabled
|
||||||
|
? AppColors.raised
|
||||||
|
: AppColors.cardAlt;
|
||||||
|
final foreground = selected
|
||||||
|
? AppColors.onPrimary
|
||||||
|
: enabled
|
||||||
|
? AppColors.onSurface
|
||||||
|
: AppColors.subtle;
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: background,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(_segmentRadius),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(10),
|
onTap: onTap,
|
||||||
onTap: enabled ? onTap : null,
|
borderRadius: BorderRadius.circular(_segmentRadius),
|
||||||
child: Padding(
|
child: SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 11),
|
height: _segmentHeight,
|
||||||
child: Row(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: _segmentPadding),
|
||||||
Icon(icon, color: color, size: 21),
|
child: Center(
|
||||||
const SizedBox(width: 12),
|
child: Text(
|
||||||
Expanded(
|
label,
|
||||||
child: Text(
|
maxLines: 1,
|
||||||
title,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: foreground,
|
||||||
color: color,
|
fontSize: 12,
|
||||||
fontSize: 14,
|
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (value.isNotEmpty)
|
),
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: const TextStyle(color: AppColors.subtle, fontSize: 11),
|
|
||||||
),
|
|
||||||
?trailing,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
|||||||
episodeNumber: data.episode.number,
|
episodeNumber: data.episode.number,
|
||||||
synopsis: synopsis,
|
synopsis: synopsis,
|
||||||
authorNote: data.authorNote,
|
authorNote: data.authorNote,
|
||||||
textScale: data.textScale,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -216,7 +215,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.onPrimary,
|
color: AppColors.onPrimary,
|
||||||
fontFamily: 'Outfit',
|
fontFamily: 'Outfit',
|
||||||
fontSize: 18 * data.textScale,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
shadows: _playerTextShadows,
|
shadows: _playerTextShadows,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class PlayerOverlayData {
|
|||||||
required this.saved,
|
required this.saved,
|
||||||
required this.playing,
|
required this.playing,
|
||||||
required this.playbackSpeed,
|
required this.playbackSpeed,
|
||||||
required this.textScale,
|
|
||||||
required this.totalEpisodes,
|
required this.totalEpisodes,
|
||||||
this.authorNote = '',
|
this.authorNote = '',
|
||||||
});
|
});
|
||||||
@@ -59,9 +58,6 @@ class PlayerOverlayData {
|
|||||||
/// 当前倍速。
|
/// 当前倍速。
|
||||||
final double playbackSpeed;
|
final double playbackSpeed;
|
||||||
|
|
||||||
/// 信息文字缩放比例。
|
|
||||||
final double textScale;
|
|
||||||
|
|
||||||
/// 可见集数量。
|
/// 可见集数量。
|
||||||
final int totalEpisodes;
|
final int totalEpisodes;
|
||||||
|
|
||||||
|
|||||||
@@ -221,14 +221,12 @@ class _EpisodeSynopsis extends StatefulWidget {
|
|||||||
required this.episodeNumber,
|
required this.episodeNumber,
|
||||||
required this.synopsis,
|
required this.synopsis,
|
||||||
required this.authorNote,
|
required this.authorNote,
|
||||||
required this.textScale,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final AppLocalizations l10n;
|
final AppLocalizations l10n;
|
||||||
final int episodeNumber;
|
final int episodeNumber;
|
||||||
final String synopsis;
|
final String synopsis;
|
||||||
final String authorNote;
|
final String authorNote;
|
||||||
final double textScale;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_EpisodeSynopsis> createState() => _EpisodeSynopsisState();
|
State<_EpisodeSynopsis> createState() => _EpisodeSynopsisState();
|
||||||
@@ -255,7 +253,7 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> {
|
|||||||
key: const Key('player-episode-label'),
|
key: const Key('player-episode-label'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.onPrimary,
|
color: AppColors.onPrimary,
|
||||||
fontSize: 11 * widget.textScale,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
shadows: _playerTextShadows,
|
shadows: _playerTextShadows,
|
||||||
),
|
),
|
||||||
@@ -278,7 +276,7 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.playerMuted,
|
color: AppColors.playerMuted,
|
||||||
fontSize: 11 * widget.textScale,
|
fontSize: 11,
|
||||||
height: 1.35,
|
height: 1.35,
|
||||||
shadows: _playerTextShadows,
|
shadows: _playerTextShadows,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
// Author: fruit.com.cn
|
|
||||||
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
/// 播放信息小号文字比例。
|
|
||||||
const double playerTextSizeSmall = 0.9;
|
|
||||||
|
|
||||||
/// 播放信息标准文字比例。
|
|
||||||
const double playerTextSizeStandard = 1.0;
|
|
||||||
|
|
||||||
/// 播放信息大号文字比例。
|
|
||||||
const double playerTextSizeLarge = 1.15;
|
|
||||||
|
|
||||||
/// 返回播放信息文字档位的本地化名称。
|
|
||||||
String playerTextSizeLabel(AppLocalizations l10n, double scale) {
|
|
||||||
if (scale <= playerTextSizeSmall) return l10n.textSizeSmall;
|
|
||||||
if (scale >= playerTextSizeLarge) return l10n.textSizeLarge;
|
|
||||||
return l10n.textSizeStandard;
|
|
||||||
}
|
|
||||||
@@ -370,7 +370,7 @@ void main() {
|
|||||||
expect(find.byKey(const Key('player-episode-2')), findsOneWidget);
|
expect(find.byKey(const Key('player-episode-2')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_open_more_actions_and_change_text_size', (tester) async {
|
testWidgets('should_open_compact_more_actions', (tester) async {
|
||||||
await _pumpApp(tester);
|
await _pumpApp(tester);
|
||||||
await _openSeriesPlayer(tester);
|
await _openSeriesPlayer(tester);
|
||||||
await tester.tap(find.byKey(const Key('player-more')));
|
await tester.tap(find.byKey(const Key('player-more')));
|
||||||
@@ -381,22 +381,35 @@ void main() {
|
|||||||
expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget);
|
expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
|
expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
|
expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('player-more-danmaku')), findsOneWidget);
|
expect(find.byKey(const Key('player-more-drag-handle')), findsOneWidget);
|
||||||
|
for (final removed in const [
|
||||||
|
Key('player-more-not-interested'),
|
||||||
|
Key('player-more-danmaku'),
|
||||||
|
Key('player-more-danmaku-settings'),
|
||||||
|
Key('player-more-text-size'),
|
||||||
|
Key('player-more-cast'),
|
||||||
|
Key('player-more-timer'),
|
||||||
|
Key('player-more-pip'),
|
||||||
|
Key('player-more-report'),
|
||||||
|
]) {
|
||||||
|
expect(find.byKey(removed), findsNothing);
|
||||||
|
}
|
||||||
expect(
|
expect(
|
||||||
find.byKey(const Key('player-more-danmaku-settings')),
|
tester.getSize(find.byKey(const Key('player-more-speed-1.0'))).height,
|
||||||
findsOneWidget,
|
28,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester
|
||||||
|
.widget<Text>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('player-more-speed-1.0')),
|
||||||
|
matching: find.text('1.0x'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.style
|
||||||
|
?.fontSize,
|
||||||
|
12,
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('player-more-cast')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('player-more-timer')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('player-more-text-size')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('player-more-report')), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.tap(find.byTooltip('Text size'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byKey(const Key('player-text-size-large')), findsOneWidget);
|
|
||||||
await tester.tap(find.byKey(const Key('player-text-size-large')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('Large'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_disable_quality_not_supplied_by_ticket', (tester) async {
|
testWidgets('should_disable_quality_not_supplied_by_ticket', (tester) async {
|
||||||
@@ -418,14 +431,24 @@ void main() {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<ChoiceChip>(find.byKey(const Key('player-more-quality-AUTO')))
|
.widget<InkWell>(
|
||||||
.onSelected,
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('player-more-quality-AUTO')),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.onTap,
|
||||||
isNotNull,
|
isNotNull,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<ChoiceChip>(find.byKey(const Key('player-more-quality-720P')))
|
.widget<InkWell>(
|
||||||
.onSelected,
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('player-more-quality-720P')),
|
||||||
|
matching: find.byType(InkWell),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.onTap,
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
expect(find.text('720P · Unavailable'), findsOneWidget);
|
expect(find.text('720P · Unavailable'), findsOneWidget);
|
||||||
|
|||||||
Reference in new issue
Block a user