From cf93d8d794db1bff2cafc576956cf1fe17596514 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 11:12:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=E6=94=B6=E7=B4=A7=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E9=A1=B5=E5=BA=95=E9=83=A8=E4=B8=8E=E4=BA=92=E5=8A=A8?= =?UTF-8?q?=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 2 - lib/l10n/app_id.arb | 2 - lib/l10n/app_localizations.dart | 12 - lib/l10n/app_localizations_en.dart | 6 - lib/l10n/app_localizations_id.dart | 6 - lib/l10n/app_localizations_zh.dart | 6 - lib/l10n/app_zh.arb | 2 - lib/pages/player/widgets/player_overlay.dart | 86 ++----- .../player/widgets/player_overlay_parts.dart | 216 +++++++++--------- test/player/player_fullscreen_test.dart | 69 ++++-- 10 files changed, 167 insertions(+), 240 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e377d84..ab59bad 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -592,9 +592,7 @@ "unlockMethodsVary": "Available unlock methods vary by title.", "unlockEpisode": "Unlock this episode", "language": "Language", - "fullScreenWatch": "Full screen", "freeWatch": "Free watch", - "showMore": "More", "qualityUnavailable": "Unavailable", "notInterested": "Not interested", "clearPlayback": "Clear-screen playback", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index d80f862..19aa673 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -590,9 +590,7 @@ "unlockMethodsVary": "Metode pembukaan tersedia dapat berbeda per judul.", "unlockEpisode": "Buka episode ini", "language": "Bahasa", - "fullScreenWatch": "Layar penuh", "freeWatch": "Tonton gratis", - "showMore": "Selengkapnya", "qualityUnavailable": "Tidak tersedia", "notInterested": "Tidak tertarik", "clearPlayback": "Putar tanpa kontrol", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 41f5f46..4a3e024 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2638,24 +2638,12 @@ abstract class AppLocalizations { /// **'Language'** String get language; - /// No description provided for @fullScreenWatch. - /// - /// In en, this message translates to: - /// **'Full screen'** - String get fullScreenWatch; - /// No description provided for @freeWatch. /// /// In en, this message translates to: /// **'Free watch'** String get freeWatch; - /// No description provided for @showMore. - /// - /// In en, this message translates to: - /// **'More'** - String get showMore; - /// No description provided for @qualityUnavailable. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9f8819a..d1c882b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1368,15 +1368,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get language => 'Language'; - @override - String get fullScreenWatch => 'Full screen'; - @override String get freeWatch => 'Free watch'; - @override - String get showMore => 'More'; - @override String get qualityUnavailable => 'Unavailable'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 518dad1..26635f9 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1368,15 +1368,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get language => 'Bahasa'; - @override - String get fullScreenWatch => 'Layar penuh'; - @override String get freeWatch => 'Tonton gratis'; - @override - String get showMore => 'Selengkapnya'; - @override String get qualityUnavailable => 'Tidak tersedia'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7d344c5..9df426e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1344,15 +1344,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get language => '语言'; - @override - String get fullScreenWatch => '全屏观看'; - @override String get freeWatch => '免费观看'; - @override - String get showMore => '展开'; - @override String get qualityUnavailable => '暂不可用'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 05aa597..113d618 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -552,9 +552,7 @@ "unlockMethodsVary": "具体解锁方式以作品而定。", "unlockEpisode": "解锁本集", "language": "语言", - "fullScreenWatch": "全屏观看", "freeWatch": "免费观看", - "showMore": "展开", "qualityUnavailable": "暂不可用", "notInterested": "不感兴趣", "clearPlayback": "清屏播放", diff --git a/lib/pages/player/widgets/player_overlay.dart b/lib/pages/player/widgets/player_overlay.dart index ab3de69..54e8f1f 100644 --- a/lib/pages/player/widgets/player_overlay.dart +++ b/lib/pages/player/widgets/player_overlay.dart @@ -1,8 +1,6 @@ // Author: fruit.com.cn import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; - import '../../../core/formatting/localized_content.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; @@ -14,11 +12,10 @@ import 'seek_progress_bar.dart'; part 'player_overlay_parts.dart'; -const double _topBarHeight = 58; const double _topButtonSize = 36; -const double _railButtonSize = 36; -const double _railGap = 9; -const double _fullScreenPillBottom = 246; +const double _topBarHeight = 58; +const double _railButtonSize = 34; +const double _railGap = 18; const double _panelRadius = 16; /// K29 竖屏播放页浮层。 @@ -57,7 +54,6 @@ class PlayerPlaybackOverlay extends StatelessWidget { _bottom(context), _rail(context), ], - _fullScreenPill(), if (!controlsVisible) Positioned( top: AppDimens.spaceSm, @@ -144,7 +140,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { final compact = MediaQuery.sizeOf(context).height < 700; return Positioned( right: AppDimens.spaceMd, - top: _topBarHeight + (compact ? 18 : 54), + top: MediaQuery.sizeOf(context).height * (compact ? 0.42 : 0.55), child: _Rail( l10n: data.l10n, liked: data.liked, @@ -156,7 +152,6 @@ class PlayerPlaybackOverlay extends StatelessWidget { onComments: actions.onComments, onSave: actions.onSave, onShare: actions.onShare, - compact: compact, ), ); } @@ -170,7 +165,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { bottom: 0, child: Container( key: const Key('player-controls-bottom'), - padding: const EdgeInsets.fromLTRB(14, 10, 14, 10), + padding: const EdgeInsets.fromLTRB(14, 8, 14, 6), decoration: const BoxDecoration( color: Color(0xED000000), borderRadius: BorderRadius.vertical( @@ -182,27 +177,16 @@ class PlayerPlaybackOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _detailRow(series), - const SizedBox(height: 6), + const SizedBox(height: 4), _EpisodeSynopsis( key: const Key('player-episode-synopsis'), l10n: data.l10n, episodeNumber: data.episode.number, synopsis: synopsis, + authorNote: data.authorNote, textScale: data.textScale, ), - if (data.authorNote.isNotEmpty) ...[ - const SizedBox(height: 6), - Text( - data.authorNote, - key: const Key('player-author-note'), - style: const TextStyle(color: Colors.white60, fontSize: 11), - ), - ], - const SizedBox(height: 6), - _PlayerTimeLabel( - key: const Key('player-time-label'), - controller: data.controller, - ), + const SizedBox(height: 2), SeekProgressBar( key: const Key('player-progress'), valueListenable: data.controller, @@ -226,7 +210,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { key: const Key('player-open-detail'), onTap: actions.onDetail, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(vertical: 1), child: Row( children: [ Expanded( @@ -238,7 +222,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { style: TextStyle( color: AppColors.onPrimary, fontFamily: 'Outfit', - fontSize: 19 * data.textScale, + fontSize: 18 * data.textScale, fontWeight: FontWeight.w800, ), ), @@ -267,7 +251,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { onTap: actions.onEpisodes, borderRadius: BorderRadius.circular(8), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Flexible( @@ -288,7 +272,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { const Icon( Icons.keyboard_arrow_up_rounded, color: AppColors.onPrimary, - size: 18, + size: 16, ), ], ), @@ -298,57 +282,13 @@ class PlayerPlaybackOverlay extends StatelessWidget { ), if (actions.onPip != null) ...[ const SizedBox(width: 8), - _TopButton( + _PipButton( key: const Key('player-pip'), tooltip: data.l10n.pictureInPicture, - icon: Icons.picture_in_picture_alt_rounded, onPressed: actions.onPip!, ), ], ], ); } - - Widget _fullScreenPill() { - return Positioned( - left: 0, - right: 0, - bottom: _fullScreenPillBottom, - child: Center( - child: Material( - color: const Color(0x99000000), - borderRadius: BorderRadius.circular(999), - child: InkWell( - key: const Key('player-fullscreen-toggle'), - borderRadius: BorderRadius.circular(999), - onTap: controlsVisible ? actions.onClear : actions.onExitClear, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.fullscreen_rounded, - color: AppColors.onPrimary, - size: 16, - ), - const SizedBox(width: 5), - Text( - controlsVisible - ? data.l10n.fullScreenWatch - : data.l10n.exitClearScreen, - style: const TextStyle( - color: AppColors.onPrimary, - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } } diff --git a/lib/pages/player/widgets/player_overlay_parts.dart b/lib/pages/player/widgets/player_overlay_parts.dart index f472365..99c1452 100644 --- a/lib/pages/player/widgets/player_overlay_parts.dart +++ b/lib/pages/player/widgets/player_overlay_parts.dart @@ -73,6 +73,30 @@ class _SpeedEntry extends StatelessWidget { } } +class _PipButton extends StatelessWidget { + const _PipButton({super.key, required this.tooltip, required this.onPressed}); + + final String tooltip; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + tooltip: tooltip, + padding: EdgeInsets.zero, + style: IconButton.styleFrom( + backgroundColor: const Color(0x80000000), + foregroundColor: AppColors.onPrimary, + ), + onPressed: onPressed, + icon: const Icon(Icons.picture_in_picture_alt_rounded, size: 17), + ), + ); + } +} + class _Rail extends StatelessWidget { const _Rail({ required this.l10n, @@ -85,7 +109,6 @@ class _Rail extends StatelessWidget { required this.onComments, required this.onSave, required this.onShare, - required this.compact, }); final AppLocalizations l10n; @@ -98,7 +121,6 @@ class _Rail extends StatelessWidget { final VoidCallback onComments; final VoidCallback onSave; final VoidCallback onShare; - final bool compact; @override Widget build(BuildContext context) { @@ -111,29 +133,28 @@ class _Rail extends StatelessWidget { label: favoriteCount?.toString() ?? l10n.favorite, active: saved, onTap: onSave, - compact: compact, ), + const SizedBox(height: _railGap), _RailButton( key: const Key('player-rail-comments'), icon: Icons.chat_bubble_outline_rounded, label: commentCount?.toString() ?? l10n.comments, onTap: onComments, - compact: compact, ), + const SizedBox(height: _railGap), _RailButton( key: const Key('player-rail-like'), icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded, label: likeCount?.toString() ?? l10n.like, active: liked, onTap: onLike, - compact: compact, ), + const SizedBox(height: _railGap), _RailButton( key: const Key('player-rail-share'), icon: Icons.ios_share_rounded, label: l10n.share, onTap: onShare, - compact: compact, ), ], ); @@ -146,46 +167,41 @@ class _RailButton extends StatelessWidget { required this.icon, required this.label, required this.onTap, - required this.compact, this.active = false, }); final IconData icon; final String label; final VoidCallback onTap; - final bool compact; final bool active; @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(bottom: compact ? 3 : _railGap), - child: Column( - children: [ - SizedBox.square( - dimension: _railButtonSize, - child: IconButton( - onPressed: onTap, - padding: EdgeInsets.zero, - style: IconButton.styleFrom( - backgroundColor: const Color(0x80000000), - foregroundColor: active ? AppColors.blue : AppColors.onPrimary, - ), - icon: Icon(icon, size: 18), + return Column( + children: [ + SizedBox.square( + dimension: _railButtonSize, + child: IconButton( + onPressed: onTap, + padding: EdgeInsets.zero, + style: IconButton.styleFrom( + backgroundColor: const Color(0x80000000), + foregroundColor: active ? AppColors.blue : AppColors.onPrimary, ), + icon: Icon(icon, size: 24), ), - SizedBox(height: compact ? 1 : 3), - Text( - label, - maxLines: 1, - style: TextStyle( - color: AppColors.onPrimary, - fontSize: compact ? 8 : 9, - fontWeight: FontWeight.w600, - ), + ), + Text( + label, + maxLines: 1, + style: const TextStyle( + color: AppColors.onPrimary, + fontSize: 11, + height: 1, + fontWeight: FontWeight.w600, ), - ], - ), + ), + ], ); } } @@ -196,12 +212,14 @@ class _EpisodeSynopsis extends StatefulWidget { required this.l10n, 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 @@ -213,92 +231,64 @@ class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { @override Widget build(BuildContext context) { - final canExpand = widget.synopsis.trim().length > 28; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + return Material( + color: Colors.transparent, + child: InkWell( + key: const Key('player-synopsis-toggle'), + onTap: () => setState(() => _expanded = !_expanded), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.l10n.episodeLabel(widget.episodeNumber), - key: const Key('player-episode-label'), - style: TextStyle( - color: AppColors.blue, - fontSize: 11 * widget.textScale, - fontWeight: FontWeight.w700, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: Text('|', style: TextStyle(color: Colors.white38)), - ), - Expanded( - child: Text( - widget.synopsis, - key: const Key('player-synopsis'), - maxLines: _expanded ? 4 : 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white70, - fontSize: 11 * widget.textScale, - height: 1.45, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.l10n.episodeLabel(widget.episodeNumber), + key: const Key('player-episode-label'), + style: TextStyle( + color: AppColors.blue, + fontSize: 11 * widget.textScale, + fontWeight: FontWeight.w700, + ), ), - ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Text('|', style: TextStyle(color: Colors.white38)), + ), + Expanded( + child: Text( + widget.synopsis, + key: const Key('player-synopsis'), + maxLines: _expanded ? 3 : 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white70, + fontSize: 11 * widget.textScale, + height: 1.35, + ), + ), + ), + const SizedBox(width: 4), + Icon( + _expanded + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.white54, + size: 16, + ), + ], ), + if (_expanded && widget.authorNote.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + widget.authorNote, + key: const Key('player-author-note'), + style: const TextStyle(color: Colors.white54, fontSize: 10), + ), + ], ], ), - if (canExpand) - GestureDetector( - key: const Key('player-synopsis-toggle'), - onTap: () => setState(() => _expanded = !_expanded), - child: Padding( - padding: const EdgeInsets.only(top: 2), - child: Text( - _expanded ? widget.l10n.showLess : widget.l10n.showMore, - style: const TextStyle( - color: Colors.white54, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ); - } -} - -class _PlayerTimeLabel extends StatelessWidget { - const _PlayerTimeLabel({super.key, required this.controller}); - - final VideoPlayerController? controller; - - @override - Widget build(BuildContext context) { - final source = controller; - if (source == null) return _label(Duration.zero, Duration.zero); - return ValueListenableBuilder( - valueListenable: source, - builder: (context, value, _) => _label(value.position, value.duration), - ); - } - - Widget _label(Duration position, Duration duration) => Align( - alignment: Alignment.centerRight, - child: Text( - '${_format(position)} / ${_format(duration)}', - style: const TextStyle( - color: Colors.white60, - fontSize: 9, - fontWeight: FontWeight.w600, ), - ), - ); - - String _format(Duration value) { - final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0'); - return '$minutes:$seconds'; + ); } } diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index bc3108d..ec41d51 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -106,7 +106,7 @@ void main() { expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); expect(find.byType(PlayerCenterToggle), findsOneWidget); - await tester.tap(find.byKey(const Key('player-fullscreen-toggle'))); + await _clearFromMore(tester); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); @@ -173,13 +173,53 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('should_keep_compact_bottom_panel_and_lower_rail', ( + tester, + ) async { + await _pumpApp(tester); + await _openSeriesPlayer(tester); + + final screenHeight = tester.view.physicalSize.height; + final bottom = tester.getRect( + find.byKey(const Key('player-controls-bottom')), + ); + final rail = tester.getRect( + find.byKey(const Key('player-interaction-rail')), + ); + + expect(bottom.height, lessThanOrEqualTo(200)); + expect(bottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); + expect(rail.top, greaterThanOrEqualTo(screenHeight * 0.55 - 2)); + expect(rail.bottom, lessThanOrEqualTo(bottom.top)); + expect(find.byKey(const Key('player-author-note')), findsNothing); + expect( + tester + .getSize( + find.descendant( + of: find.byKey(const Key('player-rail-comments')), + matching: find.byIcon(Icons.chat_bubble_outline_rounded), + ), + ) + .width, + 24, + ); + + await tester.tap(find.byKey(const Key('player-synopsis-toggle'))); + await tester.pumpAndSettle(); + final expandedBottom = tester.getRect( + find.byKey(const Key('player-controls-bottom')), + ); + expect(expandedBottom.height, lessThanOrEqualTo(200)); + expect(expandedBottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); + }); + testWidgets('should_keep_series_clear_state_when_swiping_next_episode', ( tester, ) async { await _pumpApp(tester); await _openSeriesPlayer(tester); - await tester.tap(find.byKey(const Key('player-fullscreen-toggle'))); + await _clearFromMore(tester); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); @@ -237,7 +277,7 @@ void main() { await tester.tap(find.byKey(const Key('player-speed-1.5'))); await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('player-fullscreen-toggle'))); + await _clearFromMore(tester); await tester.pump(); expect(find.byKey(const Key('player-more')), findsNothing); @@ -383,19 +423,6 @@ void main() { expect(find.byKey(const Key('player-exit-clear-screen')), findsOneWidget); }); - testWidgets('should_toggle_fullscreen_from_the_same_pill', (tester) async { - await _pumpApp(tester); - await _openSeriesPlayer(tester); - await tester.tap(find.byKey(const Key('player-fullscreen-toggle'))); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('player-controls-top')), findsNothing); - - await tester.tap(find.byKey(const Key('player-fullscreen-toggle'))); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('player-controls-top')), findsOneWidget); - expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); - }); - testWidgets('should_open_unlock_panel_with_honest_method_states', ( tester, ) async { @@ -434,7 +461,6 @@ void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-drama-title')), findsOneWidget); expect(find.byKey(const Key('player-episode-label')), findsOneWidget); expect(find.byKey(const Key('player-synopsis')), findsOneWidget); - expect(find.byKey(const Key('player-time-label')), findsOneWidget); expect(find.byKey(const Key('player-more')), findsOneWidget); for (final key in const [Key('player-back'), Key('player-more')]) { expect(tester.getSize(find.byKey(key)), const Size(36, 36)); @@ -442,7 +468,7 @@ void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-play-toggle')), findsNothing); expect(find.byKey(const Key('player-episodes')), findsOneWidget); expect(find.byKey(const Key('player-rail-comments')), findsOneWidget); - expect(find.byKey(const Key('player-fullscreen-toggle')), findsOneWidget); + expect(find.byKey(const Key('player-fullscreen-toggle')), findsNothing); expect(find.byType(Slider), findsOneWidget); expect(find.byKey(const Key('player-title')), findsNothing); expect(find.byKey(const Key('player-subtitle')), findsNothing); @@ -457,6 +483,13 @@ void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-rail-share')), findsOneWidget); } +Future _clearFromMore(WidgetTester tester) async { + await tester.tap(find.byKey(const Key('player-more'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('player-more-clear'))); + await tester.pumpAndSettle(); +} + Future _openSeriesPlayer( WidgetTester tester, { Key cardKey = const Key('home-drama-card-city-lights-demo'),