diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index a09199c..431461b 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -21,7 +21,6 @@ import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/download_action.dart'; import '../../l10n/app_localizations.dart'; -import '../interaction/widgets/report_dialog.dart'; import 'player_binding.dart'; import 'player_logic.dart'; import 'widgets/home_feed_overlay.dart'; @@ -83,7 +82,6 @@ class _PlayerPageState extends State with WidgetsBindingObserver { late final PlayerLogic logic; bool _chromeVisible = true; double _playbackSpeed = 1.0; - double _textScale = 1.0; @override void initState() { @@ -125,10 +123,6 @@ class _PlayerPageState extends State with WidgetsBindingObserver { setState(() => _playbackSpeed = value); } - void _updateTextScale(double value) { - setState(() => _textScale = value); - } - void _hideChrome() { setState(() => _chromeVisible = false); } @@ -389,7 +383,6 @@ class _PlayerPageState extends State with WidgetsBindingObserver { saved: context.watch().isSaved(drama.id), playing: logic.session.userWantsPlay, playbackSpeed: _playbackSpeed, - textScale: _textScale, totalEpisodes: logic.visibleIndices.length, ), actions: PlayerOverlayActions( @@ -408,7 +401,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { unawaited(_toggleLike(context, interactions, drama)), onSave: () => unawaited(_toggleSave(context, drama)), onShare: () => logic.shareDrama(context, drama), - onMore: () => _openMore(context, drama, episode), + onMore: () => _openMore(context, episode), onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null, onSeek: ready ? (position) => logic.seekTo(context, videoPort!, position) diff --git a/lib/pages/player/player_view_actions.dart b/lib/pages/player/player_view_actions.dart index ac3c172..748cc50 100644 --- a/lib/pages/player/player_view_actions.dart +++ b/lib/pages/player/player_view_actions.dart @@ -15,11 +15,7 @@ extension _PlayerViewActions on _PlayerPageState { await logic.setPlaybackSpeed(selected); } - Future _openMore( - BuildContext context, - Drama drama, - Episode episode, - ) async { + Future _openMore(BuildContext context, Episode episode) async { final l10n = AppLocalizations.of(context)!; await showModalBottomSheet( context: context, @@ -31,21 +27,14 @@ extension _PlayerViewActions on _PlayerPageState { state: PlayerMoreState( speed: _playbackSpeed, qualities: logic.qualities, - textScale: _textScale, - pipAvailable: logic.pictureInPicture.isPlatformSupported, ), actions: PlayerMoreActions( onSpeedChanged: (value) { _updatePlaybackSpeed(value); unawaited(logic.setPlaybackSpeed(value)); }, - onTextScaleChanged: _updateTextScale, onClear: _hideChrome, 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; } - Future _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().lightImpact()); - final result = await context.read().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 _toggleLike( BuildContext context, InteractionController interactions, diff --git a/lib/pages/player/widgets/player_more_sheet.dart b/lib/pages/player/widgets/player_more_sheet.dart index 6a885e6..6764b47 100644 --- a/lib/pages/player/widgets/player_more_sheet.dart +++ b/lib/pages/player/widgets/player_more_sheet.dart @@ -4,9 +4,14 @@ 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 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'; @@ -14,24 +19,13 @@ const String _autoQuality = 'AUTO'; /// 更多面板运行状态。 class PlayerMoreState { /// 创建更多面板状态。 - const PlayerMoreState({ - required this.speed, - required this.qualities, - required this.textScale, - required this.pipAvailable, - }); + const PlayerMoreState({required this.speed, required this.qualities}); /// 当前倍速。 final double speed; /// 后端下发的清晰度档位。 final List qualities; - - /// 当前文字缩放比例。 - final double textScale; - - /// 设备是否提供画中画入口。 - final bool pipAvailable; } /// 更多面板动作集合。 @@ -39,33 +33,21 @@ class PlayerMoreActions { /// 创建动作集合。 const PlayerMoreActions({ required this.onSpeedChanged, - required this.onTextScaleChanged, required this.onClear, required this.onDownload, - required this.onReport, - this.onPip, }); /// 倍速变化。 final ValueChanged onSpeedChanged; - /// 字体大小变化。 - final ValueChanged onTextScaleChanged; - /// 进入清屏播放。 final VoidCallback onClear; /// 离线缓存当前集。 final VoidCallback onDownload; - - /// 投诉当前剧。 - final VoidCallback onReport; - - /// 进入画中画;平台不支持时为空。 - final VoidCallback? onPip; } -/// K29 播放页白色更多面板。 +/// K29.6 精简版播放页更多面板。 class PlayerMoreSheet extends StatefulWidget { /// 创建更多面板。 const PlayerMoreSheet({ @@ -90,84 +72,55 @@ class PlayerMoreSheet extends StatefulWidget { 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( + return Material( key: const Key('player-more-sheet'), - constraints: const BoxConstraints(maxHeight: 620), - decoration: const BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.vertical(top: Radius.circular(_sheetRadius)), + color: AppColors.card, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(_sheetRadius), ), + clipBehavior: Clip.antiAlias, child: SafeArea( top: false, child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(18, 14, 18, 18), + 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: 16), + const SizedBox(height: _groupGap), _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( + const SizedBox(height: _groupGap), + _actionRow( 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( + _actionRow( 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), - ), ], ), ), @@ -182,44 +135,47 @@ class _PlayerMoreSheetState extends State { l10n.more, style: const TextStyle( color: AppColors.onSurface, - fontSize: 20, - fontWeight: FontWeight.w800, + fontSize: 15, + fontWeight: FontWeight.w700, ), ), ), - IconButton( - key: const Key('player-more-close'), - tooltip: l10n.close, - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close_rounded), + 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), + padding: const EdgeInsets.only(bottom: 8, top: 2), child: Text( title, style: const TextStyle( - color: AppColors.onSurface, - fontSize: 14, - fontWeight: FontWeight.w800, + color: AppColors.muted, + fontSize: 12, + fontWeight: FontWeight.w600, ), ), ); Widget _speedPicker() => Wrap( key: const Key('player-more-speed'), - spacing: 8, - runSpacing: 8, + spacing: _segmentGap, + runSpacing: _segmentGap, children: [ for (final value in _speedOptions) - ChoiceChip( + _SegmentButton( key: Key('player-more-speed-$value'), - label: Text('${value}x'), + label: '${value}x', selected: (_speed - value).abs() < 0.001, - showCheckmark: false, - onSelected: (_) { + onTap: () { setState(() => _speed = value); widget.actions.onSpeedChanged(value); }, @@ -229,110 +185,69 @@ class _PlayerMoreSheetState extends State { Widget _qualityPicker(AppLocalizations l10n) => Wrap( key: const Key('player-more-quality'), - spacing: 8, - runSpacing: 8, + spacing: _segmentGap, + runSpacing: _segmentGap, 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 selected = quality == _autoQuality && enabled || !widget.state.qualities.contains(_autoQuality) && widget.state.qualities.isNotEmpty && widget.state.qualities.first == quality; - return ChoiceChip( + return _SegmentButton( key: Key('player-more-quality-$quality'), - label: Text(enabled ? quality : '$quality · ${l10n.qualityUnavailable}'), + label: enabled ? quality : '$quality · ${l10n.qualityUnavailable}', selected: selected, - showCheckmark: false, - onSelected: enabled ? (_) {} : null, + enabled: enabled, + onTap: 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({ + Widget _actionRow({ 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), - ); + }) { + 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); @@ -347,56 +262,53 @@ class _PlayerMoreSheetState extends State { } } -class _MoreRow extends StatelessWidget { - const _MoreRow({ +class _SegmentButton extends StatelessWidget { + const _SegmentButton({ super.key, - required this.icon, - required this.title, - required this.enabled, - this.value = '', - this.trailing, + required this.label, + required this.selected, + this.enabled = true, this.onTap, }); - final IconData icon; - final String title; + final String label; + final bool selected; final bool enabled; - final String value; - final Widget? trailing; final VoidCallback? onTap; @override 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( - color: Colors.transparent, - borderRadius: BorderRadius.circular(10), + color: background, + borderRadius: BorderRadius.circular(_segmentRadius), 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, - ), + 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, ), ), - if (value.isNotEmpty) - Text( - value, - style: const TextStyle(color: AppColors.subtle, fontSize: 11), - ), - ?trailing, - ], + ), ), ), ), diff --git a/lib/pages/player/widgets/player_overlay.dart b/lib/pages/player/widgets/player_overlay.dart index c5054cc..bc6773d 100644 --- a/lib/pages/player/widgets/player_overlay.dart +++ b/lib/pages/player/widgets/player_overlay.dart @@ -173,7 +173,6 @@ class PlayerPlaybackOverlay extends StatelessWidget { episodeNumber: data.episode.number, synopsis: synopsis, authorNote: data.authorNote, - textScale: data.textScale, ), ], ), @@ -216,7 +215,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { style: TextStyle( color: AppColors.onPrimary, fontFamily: 'Outfit', - fontSize: 18 * data.textScale, + fontSize: 18, fontWeight: FontWeight.w800, shadows: _playerTextShadows, ), diff --git a/lib/pages/player/widgets/player_overlay_models.dart b/lib/pages/player/widgets/player_overlay_models.dart index 7c43d8c..4849fa3 100644 --- a/lib/pages/player/widgets/player_overlay_models.dart +++ b/lib/pages/player/widgets/player_overlay_models.dart @@ -21,7 +21,6 @@ class PlayerOverlayData { required this.saved, required this.playing, required this.playbackSpeed, - required this.textScale, required this.totalEpisodes, this.authorNote = '', }); @@ -59,9 +58,6 @@ class PlayerOverlayData { /// 当前倍速。 final double playbackSpeed; - /// 信息文字缩放比例。 - final double textScale; - /// 可见集数量。 final int totalEpisodes; diff --git a/lib/pages/player/widgets/player_overlay_parts.dart b/lib/pages/player/widgets/player_overlay_parts.dart index f50403f..034ec5b 100644 --- a/lib/pages/player/widgets/player_overlay_parts.dart +++ b/lib/pages/player/widgets/player_overlay_parts.dart @@ -221,14 +221,12 @@ class _EpisodeSynopsis extends StatefulWidget { required this.episodeNumber, required this.synopsis, required this.authorNote, - required this.textScale, }); final AppLocalizations l10n; final int episodeNumber; final String synopsis; final String authorNote; - final double textScale; @override State<_EpisodeSynopsis> createState() => _EpisodeSynopsisState(); @@ -255,7 +253,7 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { key: const Key('player-episode-label'), style: TextStyle( color: AppColors.onPrimary, - fontSize: 11 * widget.textScale, + fontSize: 11, fontWeight: FontWeight.w700, shadows: _playerTextShadows, ), @@ -278,7 +276,7 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { overflow: TextOverflow.ellipsis, style: TextStyle( color: AppColors.playerMuted, - fontSize: 11 * widget.textScale, + fontSize: 11, height: 1.35, shadows: _playerTextShadows, ), diff --git a/lib/pages/player/widgets/player_text_scale.dart b/lib/pages/player/widgets/player_text_scale.dart deleted file mode 100644 index 1259f53..0000000 --- a/lib/pages/player/widgets/player_text_scale.dart +++ /dev/null @@ -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; -} diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 07ea8f4..4c239f9 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -370,7 +370,7 @@ void main() { 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 _openSeriesPlayer(tester); 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-offline')), 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( - find.byKey(const Key('player-more-danmaku-settings')), - findsOneWidget, + tester.getSize(find.byKey(const Key('player-more-speed-1.0'))).height, + 28, + ); + expect( + tester + .widget( + 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 { @@ -418,14 +431,24 @@ void main() { expect( tester - .widget(find.byKey(const Key('player-more-quality-AUTO'))) - .onSelected, + .widget( + find.descendant( + of: find.byKey(const Key('player-more-quality-AUTO')), + matching: find.byType(InkWell), + ), + ) + .onTap, isNotNull, ); expect( tester - .widget(find.byKey(const Key('player-more-quality-720P'))) - .onSelected, + .widget( + find.descendant( + of: find.byKey(const Key('player-more-quality-720P')), + matching: find.byType(InkWell), + ), + ) + .onTap, isNull, ); expect(find.text('720P · Unavailable'), findsOneWidget);