From d25519bed7c3a3a5b4c0f48e33ec099c1abea707 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 11:55:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=E8=B0=83=E6=95=B4=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E9=A1=B5=E5=BA=95=E9=83=A8=E4=BF=A1=E6=81=AF=E6=8E=92?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_id.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_zh.arb | 1 + lib/pages/player/widgets/player_overlay.dart | 18 +-- .../player/widgets/player_overlay_parts.dart | 152 ++++++++++-------- .../player/widgets/seek_progress_bar.dart | 2 + test/player/player_fullscreen_test.dart | 123 +++++++++++++- 11 files changed, 236 insertions(+), 77 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ab59bad..a22ee33 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -41,6 +41,7 @@ "seasonOneOriginalRelease": "Season 1 • Original Release", "startWatching": "Start Watching", "showLess": "Less", + "expandSynopsis": "More", "views": "views", "episodeOfTotal": "Episode {current} / {total}", "@episodeOfTotal": { diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 19aa673..c4deb62 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -41,6 +41,7 @@ "seasonOneOriginalRelease": "Musim 1 • Rilis Orisinal", "startWatching": "Mulai Menonton", "showLess": "Lebih sedikit", + "expandSynopsis": "Selengkapnya", "views": "tayangan", "episodeOfTotal": "Episode {current} / {total}", "@episodeOfTotal": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4a3e024..425f8b8 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -346,6 +346,12 @@ abstract class AppLocalizations { /// **'Less'** String get showLess; + /// No description provided for @expandSynopsis. + /// + /// In en, this message translates to: + /// **'More'** + String get expandSynopsis; + /// No description provided for @views. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d1c882b..f9512b8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -134,6 +134,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get showLess => 'Less'; + @override + String get expandSynopsis => 'More'; + @override String get views => 'views'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 26635f9..fad8f89 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -134,6 +134,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get showLess => 'Lebih sedikit'; + @override + String get expandSynopsis => 'Selengkapnya'; + @override String get views => 'tayangan'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9df426e..1fb5965 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -131,6 +131,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get showLess => '收起'; + @override + String get expandSynopsis => '展开'; + @override String get views => '次播放'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 113d618..65d93b3 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -41,6 +41,7 @@ "seasonOneOriginalRelease": "第 1 季 · 原创上线", "startWatching": "开始观看", "showLess": "收起", + "expandSynopsis": "展开", "views": "次播放", "episodeOfTotal": "第 {current} 集 / 共 {total} 集", "@episodeOfTotal": { diff --git a/lib/pages/player/widgets/player_overlay.dart b/lib/pages/player/widgets/player_overlay.dart index bc6773d..7f1cf7c 100644 --- a/lib/pages/player/widgets/player_overlay.dart +++ b/lib/pages/player/widgets/player_overlay.dart @@ -206,7 +206,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 1), child: Row( children: [ - Expanded( + Flexible( child: Text( series, key: const Key('player-drama-title'), @@ -221,7 +221,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { ), ), ), - const SizedBox(width: 6), + const SizedBox(width: 4), const Icon( Icons.chevron_right_rounded, color: AppColors.playerMuted, @@ -245,16 +245,16 @@ class PlayerPlaybackOverlay extends StatelessWidget { child: Material( key: const Key('player-episodes-pill'), color: AppColors.playerOverlayPill, - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(7), clipBehavior: Clip.antiAlias, child: InkWell( key: const Key('player-episodes'), onTap: actions.onEpisodes, - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(7), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, + horizontal: 14, + vertical: 8, ), child: Row( children: [ @@ -267,8 +267,8 @@ class PlayerPlaybackOverlay extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onPrimary, - fontSize: 11, - fontWeight: FontWeight.w700, + fontSize: 13.5, + fontWeight: FontWeight.w800, shadows: _playerTextShadows, ), ), @@ -277,7 +277,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { const Icon( Icons.keyboard_arrow_up_rounded, color: AppColors.onPrimary, - size: 16, + size: 18, shadows: _playerTextShadows, ), ], diff --git a/lib/pages/player/widgets/player_overlay_parts.dart b/lib/pages/player/widgets/player_overlay_parts.dart index 034ec5b..9d6712b 100644 --- a/lib/pages/player/widgets/player_overlay_parts.dart +++ b/lib/pages/player/widgets/player_overlay_parts.dart @@ -90,7 +90,7 @@ class _BottomIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox.square( - dimension: 30, + dimension: 40, child: IconButton( tooltip: tooltip, padding: EdgeInsets.zero, @@ -99,7 +99,7 @@ class _BottomIconButton extends StatelessWidget { foregroundColor: AppColors.onPrimary, ), onPressed: onPressed, - icon: Icon(icon, size: 17), + icon: Icon(icon, size: 22), ), ); } @@ -235,78 +235,100 @@ class _EpisodeSynopsis extends StatefulWidget { class _EpisodeSynopsisState extends State<_EpisodeSynopsis> { bool _expanded = false; + static const TextStyle _labelStyle = TextStyle( + color: AppColors.onPrimary, + fontSize: 12, + fontWeight: FontWeight.w700, + shadows: _playerTextShadows, + ); + + static const TextStyle _synopsisStyle = TextStyle( + color: AppColors.playerMuted, + fontSize: 12, + height: 1.35, + shadows: _playerTextShadows, + ); + @override Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: InkWell( - key: const Key('player-synopsis-toggle'), - onTap: () => setState(() => _expanded = !_expanded), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.l10n.episodeLabel(widget.episodeNumber), - key: const Key('player-episode-label'), - style: TextStyle( - color: AppColors.onPrimary, - fontSize: 11, - fontWeight: FontWeight.w700, - shadows: _playerTextShadows, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: Text( - '|', - style: TextStyle( - color: Colors.white54, - shadows: _playerTextShadows, - ), - ), - ), - Expanded( - child: Text( - widget.synopsis, - key: const Key('player-synopsis'), - maxLines: _expanded ? 3 : 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: AppColors.playerMuted, - fontSize: 11, - height: 1.35, - shadows: _playerTextShadows, - ), - ), - ), - const SizedBox(width: 4), - Icon( - _expanded - ? Icons.keyboard_arrow_up_rounded - : Icons.keyboard_arrow_down_rounded, - color: AppColors.playerMuted, - size: 16, - shadows: _playerTextShadows, - ), - ], + Text( + widget.l10n.episodeLabel(widget.episodeNumber), + key: const Key('player-episode-label'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: _labelStyle, ), - if (_expanded && widget.authorNote.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - widget.authorNote, - key: const Key('player-author-note'), - style: const TextStyle( - color: AppColors.playerMuted, - fontSize: 10, + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Text( + '|', + maxLines: 1, + style: TextStyle( + color: Colors.white54, + fontSize: 12, shadows: _playerTextShadows, ), ), - ], + ), + Flexible( + child: Text( + widget.synopsis, + key: const Key('player-synopsis'), + maxLines: _expanded ? null : 1, + overflow: _expanded ? TextOverflow.clip : TextOverflow.ellipsis, + style: _synopsisStyle, + ), + ), + if (!_expanded) _toggle(widget.l10n.expandSynopsis), ], ), + if (_expanded) ...[ + if (widget.authorNote.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + widget.authorNote, + key: const Key('player-author-note'), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.playerMuted, + fontSize: 10, + shadows: _playerTextShadows, + ), + ), + ], + _toggle(widget.l10n.showLess), + ], + ], + ); + } + + Widget _toggle(String label) { + return InkWell( + key: const Key('player-synopsis-toggle'), + onTap: () => setState(() => _expanded = !_expanded), + borderRadius: BorderRadius.circular(4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.blue, + fontSize: 12, + fontWeight: FontWeight.w700, + shadows: _playerTextShadows, + ), + ), ), ); } diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index 45195d5..faa2e85 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -144,6 +144,8 @@ class _SeekProgressBarState extends State { label: widget.semanticsLabel, child: Slider( value: fraction, + // 轨道与上方文案共用同一层水平内边距,避免 Slider 默认内缩。 + padding: EdgeInsets.zero, onChangeStart: enabled ? (value) { _previewFraction.value = value; diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 9b9d1ee..5348892 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -129,7 +129,7 @@ void main() { expect(find.byTooltip('Exit clear screen'), findsOneWidget); expect( tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), - const Size(30, 30), + const Size(40, 40), ); await tester.tapAt(const Offset(80, 300)); @@ -204,6 +204,31 @@ void main() { .color, AppColors.playerOverlayPill, ); + expect( + tester + .widget(find.byKey(const Key('player-episodes-pill'))) + .borderRadius, + BorderRadius.circular(7), + ); + expect( + tester + .widget( + find + .descendant( + of: find.byKey(const Key('player-episodes-pill')), + matching: find.byType(Text), + ) + .first, + ) + .style + ?.fontSize, + 13.5, + ); + expect( + tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), + const Size(40, 40), + ); + expect(tester.widget(find.byType(Slider)).padding, EdgeInsets.zero); expect(bottom.height, lessThanOrEqualTo(200)); expect(bottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); @@ -227,8 +252,10 @@ void main() { final expandedBottom = tester.getRect( find.byKey(const Key('player-controls-bottom')), ); - expect(expandedBottom.height, lessThanOrEqualTo(200)); - expect(expandedBottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); + // K29.9 展开态展示简介全文,高度会增长,但仍需留在画面下半区。 + expect(expandedBottom.height, lessThanOrEqualTo(330)); + expect(expandedBottom.top, greaterThanOrEqualTo(screenHeight * 0.5)); + expect(tester.takeException(), isNull); }); testWidgets('should_keep_series_clear_state_when_swiping_next_episode', ( @@ -258,6 +285,96 @@ void main() { expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget); }); + testWidgets('should_toggle_synopsis_with_inline_text_button', (tester) async { + await _pumpApp(tester); + await _openSeriesPlayer(tester); + + final toggle = find.byKey(const Key('player-synopsis-toggle')); + final synopsis = find.byKey(const Key('player-synopsis')); + expect( + find.descendant(of: toggle, matching: find.text('More')), + findsOneWidget, + ); + expect(tester.widget(synopsis).maxLines, 1); + expect( + tester.getRect(toggle).left - tester.getRect(synopsis).right, + lessThanOrEqualTo(4), + ); + + await tester.tap(toggle); + await tester.pumpAndSettle(); + expect( + find.descendant(of: toggle, matching: find.text('Less')), + findsOneWidget, + ); + expect(tester.widget(synopsis).maxLines, isNull); + + await tester.tap(toggle); + await tester.pumpAndSettle(); + expect( + find.descendant(of: toggle, matching: find.text('More')), + findsOneWidget, + ); + expect(tester.widget(synopsis).maxLines, 1); + }); + + testWidgets('should_fit_long_title_and_synopsis_at_320_width', ( + tester, + ) async { + final longCopy = Drama( + id: 'long-copy-demo', + title: + 'An Extremely Long Drama Title That Keeps Going Beyond Any ' + 'Sensible Width On A Narrow Screen', + synopsis: + 'A very long synopsis that keeps describing the story far beyond ' + 'the single line space available in the compact bottom panel of ' + 'the fullscreen player.', + posterUrl: '/files/long-copy-demo.png', + genre: 'Drama', + detailLoaded: true, + episodes: const [ + Episode( + id: 'long-copy-demo-1', + number: 1, + title: 'Episode 1', + isFree: true, + videoUrl: 'https://example.test/long-demo-1.mp4', + ), + ], + ); + await _pumpApp( + tester, + size: const Size(320, 568), + catalog: FakeCatalogRepository(dramas: [longCopy]), + ); + await _openSeriesPlayer( + tester, + cardKey: const Key('home-drama-card-long-copy-demo'), + ); + + expect(tester.takeException(), isNull); + final title = tester.getRect(find.byKey(const Key('player-drama-title'))); + final chevron = tester.getRect( + find.descendant( + of: find.byKey(const Key('player-open-detail')), + matching: find.byIcon(Icons.chevron_right_rounded), + ), + ); + expect(chevron.left - title.right, lessThanOrEqualTo(6)); + expect(chevron.right, lessThanOrEqualTo(320 - 14)); + final synopsis = tester.widget( + find.byKey(const Key('player-synopsis')), + ); + expect(synopsis.maxLines, 1); + expect(synopsis.overflow, TextOverflow.ellipsis); + expect( + tester.getRect(find.byKey(const Key('player-episodes-pill'))).right, + lessThanOrEqualTo(320 - 14 - 40 - 8), + ); + expect(tester.takeException(), isNull); + }); + testWidgets('should_open_speed_sheet_and_apply_selected_speed', ( tester, ) async {