From 5422581f8b2a02fbb51d1451852d79adb259ecc9 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 15:58:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20=E6=B8=85=E7=90=86=20K26=20?= =?UTF-8?q?=E6=97=A7=20UI=20=E4=B8=8E=E6=AD=BB=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 4 - lib/l10n/app_id.arb | 4 - lib/l10n/app_localizations.dart | 24 -- lib/l10n/app_localizations_en.dart | 12 - lib/l10n/app_localizations_id.dart | 12 - lib/l10n/app_localizations_zh.dart | 12 - lib/l10n/app_zh.arb | 4 - .../discover/widgets/cover_carousel.dart | 310 ------------------ lib/pages/home/home_logic.dart | 23 +- lib/pages/home/home_view.dart | 39 ++- lib/pages/home/widgets/home_menu_sheet.dart | 246 ++++++++++++++ lib/pages/home/widgets/home_quick_drawer.dart | 287 ---------------- lib/pages/player/player_logic.dart | 55 +--- lib/pages/player/player_view.dart | 90 +---- .../player/widgets/home_feed_overlay.dart | 14 - test/discover/cover_carousel_test.dart | 193 ----------- test/discover/home_all_dramas_grid_test.dart | 4 - test/discover/home_banner_source_test.dart | 85 ----- test/home/home_logic_test.dart | 18 +- test/home/home_nav_ui_test.dart | 15 - test/player/player_fullscreen_test.dart | 3 - test/player/player_logic_test.dart | 28 +- test/widget_test.dart | 34 +- 23 files changed, 291 insertions(+), 1225 deletions(-) delete mode 100644 lib/pages/discover/widgets/cover_carousel.dart create mode 100644 lib/pages/home/widgets/home_menu_sheet.dart delete mode 100644 lib/pages/home/widgets/home_quick_drawer.dart delete mode 100644 test/discover/cover_carousel_test.dart delete mode 100644 test/discover/home_banner_source_test.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 671ee4f..74e71e8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -142,8 +142,6 @@ "reportFailed": "Could not submit report.", "shareFailed": "Could not open the share sheet.", "nowPlaying": "Playing now", - "watchFullscreen": "Watch in landscape", - "exitFullscreen": "Exit fullscreen", "pictureInPicture": "Picture in picture", "pipUnavailable": "Picture in picture is unavailable on this device", "originalTag": "Original", @@ -272,8 +270,6 @@ "myListTab": "My List", "watchFull": "Watch Full", "swipeForNext": "Swipe up for next", - "clearScreen": "Clear screen", - "exitClearScreen": "Exit clear screen", "logIn": "Log in", "logInHint": "Keep your coins and list", "rewards": "Rewards", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index c75f7c3..66274c6 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -142,8 +142,6 @@ "reportFailed": "Laporan tidak dapat dikirim.", "shareFailed": "Tidak bisa membuka menu berbagi.", "nowPlaying": "Sedang diputar", - "watchFullscreen": "Tonton mode lanskap", - "exitFullscreen": "Keluar dari layar penuh", "pictureInPicture": "Gambar dalam gambar", "pipUnavailable": "Gambar dalam gambar tidak tersedia di perangkat ini", "originalTag": "Original", @@ -272,8 +270,6 @@ "myListTab": "Daftar Saya", "watchFull": "Tonton Penuh", "swipeForNext": "Geser ke atas untuk lanjut", - "clearScreen": "Bersihkan layar", - "exitClearScreen": "Keluar dari mode bersih", "logIn": "Masuk", "logInHint": "Simpan koin dan daftar kamu", "rewards": "Hadiah", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cf14047..9d9ecb0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -772,18 +772,6 @@ abstract class AppLocalizations { /// **'Playing now'** String get nowPlaying; - /// No description provided for @watchFullscreen. - /// - /// In en, this message translates to: - /// **'Watch in landscape'** - String get watchFullscreen; - - /// No description provided for @exitFullscreen. - /// - /// In en, this message translates to: - /// **'Exit fullscreen'** - String get exitFullscreen; - /// No description provided for @pictureInPicture. /// /// In en, this message translates to: @@ -1222,18 +1210,6 @@ abstract class AppLocalizations { /// **'Swipe up for next'** String get swipeForNext; - /// No description provided for @clearScreen. - /// - /// In en, this message translates to: - /// **'Clear screen'** - String get clearScreen; - - /// No description provided for @exitClearScreen. - /// - /// In en, this message translates to: - /// **'Exit clear screen'** - String get exitClearScreen; - /// No description provided for @logIn. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3a00090..0fd6034 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -360,12 +360,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get nowPlaying => 'Playing now'; - @override - String get watchFullscreen => 'Watch in landscape'; - - @override - String get exitFullscreen => 'Exit fullscreen'; - @override String get pictureInPicture => 'Picture in picture'; @@ -604,12 +598,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get swipeForNext => 'Swipe up for next'; - @override - String get clearScreen => 'Clear screen'; - - @override - String get exitClearScreen => 'Exit clear screen'; - @override String get logIn => 'Log in'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 5f2b421..4947298 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -360,12 +360,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get nowPlaying => 'Sedang diputar'; - @override - String get watchFullscreen => 'Tonton mode lanskap'; - - @override - String get exitFullscreen => 'Keluar dari layar penuh'; - @override String get pictureInPicture => 'Gambar dalam gambar'; @@ -606,12 +600,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get swipeForNext => 'Geser ke atas untuk lanjut'; - @override - String get clearScreen => 'Bersihkan layar'; - - @override - String get exitClearScreen => 'Keluar dari mode bersih'; - @override String get logIn => 'Masuk'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b3fb2b3..6010528 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -354,12 +354,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get nowPlaying => '正在播放'; - @override - String get watchFullscreen => '横屏全屏观看'; - - @override - String get exitFullscreen => '退出全屏'; - @override String get pictureInPicture => '画中画'; @@ -593,12 +587,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get swipeForNext => '上滑看下一部'; - @override - String get clearScreen => '清屏'; - - @override - String get exitClearScreen => '退出清屏'; - @override String get logIn => '登录'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0ddd3bb..493979a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -142,8 +142,6 @@ "reportFailed": "举报提交失败,请重试。", "shareFailed": "无法打开系统分享面板。", "nowPlaying": "正在播放", - "watchFullscreen": "横屏全屏观看", - "exitFullscreen": "退出全屏", "pictureInPicture": "画中画", "pipUnavailable": "此设备无法使用画中画", "originalTag": "原创", @@ -234,8 +232,6 @@ "myListTab": "片单", "watchFull": "看全集", "swipeForNext": "上滑看下一部", - "clearScreen": "清屏", - "exitClearScreen": "退出清屏", "logIn": "登录", "logInHint": "登录后保存金币和片单", "rewards": "奖励", diff --git a/lib/pages/discover/widgets/cover_carousel.dart b/lib/pages/discover/widgets/cover_carousel.dart deleted file mode 100644 index 0fdb0b1..0000000 --- a/lib/pages/discover/widgets/cover_carousel.dart +++ /dev/null @@ -1,310 +0,0 @@ -import 'dart:async'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/widgets/remote_image.dart'; -import '../../../l10n/app_localizations.dart'; - -const Duration _autoPlayInterval = Duration(milliseconds: 4500); - -class CoverItem { - const CoverItem({ - required this.title, - required this.subtitle, - required this.image, - required this.onOpen, - }); - - final String title; - final String subtitle; - final String image; - final VoidCallback onOpen; -} - -/// Full-bleed posters that turn as faces of a cube. Titles sit on the image. -class CoverCarousel extends StatefulWidget { - const CoverCarousel({super.key, required this.items, this.active = true}); - - final List items; - final bool active; - - @override - State createState() => _CoverCarouselState(); -} - -class _CoverCarouselState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _snap; - double _page = 0; - double _from = 0; - double _to = 0; - int? _pointer; - double _drag = 0; - double _dragPage = 0; - Timer? _autoTimer; - bool _tickerEnabled = true; - - @override - void initState() { - super.initState(); - _snap = AnimationController(vsync: this)..addListener(_onSnap); - } - - @override - void dispose() { - _autoTimer?.cancel(); - _snap.dispose(); - super.dispose(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final enabled = TickerMode.valuesOf(context).enabled; - if (enabled != _tickerEnabled) { - _tickerEnabled = enabled; - _syncAutoTimer(); - } else if (_autoTimer == null) { - _syncAutoTimer(); - } - } - - @override - void didUpdateWidget(covariant CoverCarousel oldWidget) { - super.didUpdateWidget(oldWidget); - if (_page > _last) _page = _last; - if (oldWidget.items.length != widget.items.length || - oldWidget.active != widget.active) { - _syncAutoTimer(); - } - } - - double get _last => math.max(0, widget.items.length - 1).toDouble(); - - void _onSnap() { - if (!mounted) return; - final t = Curves.easeOutCubic.transform(_snap.value); - setState(() => _page = _from + (_to - _from) * t); - } - - void _turnTo(double target) { - if (!mounted || widget.items.isEmpty) return; - _from = _page; - _to = target.clamp(0, _last); - _snap.duration = const Duration(milliseconds: 420); - _snap.forward(from: 0); - } - - void _syncAutoTimer() { - _autoTimer?.cancel(); - _autoTimer = null; - if (!mounted || - !widget.active || - !_tickerEnabled || - widget.items.length <= 1) { - return; - } - _autoTimer = Timer.periodic(_autoPlayInterval, (_) { - if (!mounted || _pointer != null || !widget.active || !_tickerEnabled) { - return; - } - final current = _page.round().clamp(0, _last.toInt()); - if (current >= _last.toInt()) { - setState(() => _page = 0); - } else { - _turnTo((current + 1).toDouble()); - } - }); - } - - @override - Widget build(BuildContext context) { - if (widget.items.isEmpty) return const SizedBox.shrink(); - final current = _page.round().clamp(0, widget.items.length - 1); - final l10n = AppLocalizations.of(context)!; - return LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth; - final height = math.min(width * 0.52, 210).toDouble(); - return Semantics( - container: true, - label: l10n.carouselSemantics(current + 1, widget.items.length), - child: SizedBox( - width: double.infinity, - height: height, - child: Stack( - children: [ - Positioned.fill(child: _stage(width)), - _indicator(l10n, current), - ], - ), - ), - ); - }, - ); - } - - Widget _stage(double width) { - return Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (event) { - _autoTimer?.cancel(); - _autoTimer = null; - _snap.stop(); - _pointer = event.pointer; - _drag = 0; - _dragPage = _page; - }, - onPointerMove: (event) { - if (event.pointer != _pointer) return; - _drag += event.delta.dx; - setState(() { - _page = (_dragPage - _drag / width).clamp(0.0, _last); - }); - }, - onPointerUp: (event) { - if (event.pointer != _pointer) return; - _pointer = null; - _turnTo(_page.roundToDouble()); - _syncAutoTimer(); - }, - onPointerCancel: (event) { - if (event.pointer != _pointer) return; - _pointer = null; - _turnTo(_page.roundToDouble()); - _syncAutoTimer(); - }, - child: ClipRect( - child: Stack( - fit: StackFit.expand, - children: [for (final index in _visible) _face(index, width)], - ), - ), - ); - } - - Widget _indicator(AppLocalizations l10n, int current) { - return Positioned( - left: 0, - right: 0, - bottom: 8, - child: Semantics( - label: l10n.carouselSemantics(current + 1, widget.items.length), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - for (var index = 0; index < widget.items.length; index++) - Container( - key: Key('carousel-dot-$index'), - width: 6, - height: 6, - margin: const EdgeInsets.symmetric(horizontal: 3), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: index == current - ? AppColors.onPrimary - : AppColors.muted.withValues(alpha: 0.4), - ), - ), - ], - ), - ), - ); - } - - List get _visible { - final first = _page.floor(); - final indexes = [first, first + 1]; - indexes.retainWhere((index) => index >= 0 && index < widget.items.length); - indexes.sort((a, b) => (b - _page).abs().compareTo((a - _page).abs())); - return indexes; - } - - Widget _face(int index, double width) { - final delta = index - _page; - if (delta <= -1 || delta >= 1) return const SizedBox.shrink(); - final item = widget.items[index]; - final angle = delta * math.pi / 2; - // Push the face out to the cube surface, spin the whole cube on its - // vertical center, then bring that center back to the banner. - final face = Matrix4.identity() - ..translateByDouble(0, 0, -width / 2, 1) - ..rotateY(angle) - ..translateByDouble(0, 0, width / 2, 1); - final transform = Matrix4.identity() - ..setEntry(3, 2, 0.0018) - ..multiply(face); - return Positioned.fill( - child: Transform( - alignment: Alignment.center, - filterQuality: FilterQuality.medium, - transform: transform, - child: GestureDetector( - onTap: () { - if ((_page - index).abs() < 0.08) item.onOpen(); - }, - child: _poster(item), - ), - ), - ); - } - - Widget _poster(CoverItem item) { - return Stack( - fit: StackFit.expand, - children: [ - RemoteImage( - item.image, - showFallbackIcon: true, - fallbackIconKey: const Key('carousel-image-fallback'), - ), - const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0x00000000), Color(0x00000000), Color(0xE6000000)], - stops: [0, 0.5, 1], - ), - ), - ), - Positioned( - left: 20, - right: 20, - bottom: 30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w600, - height: 1.15, - shadows: [Shadow(blurRadius: 12, color: Colors.black)], - ), - ), - const SizedBox(height: 6), - Text( - item.subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Color(0xF2FFFFFF), - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.2, - shadows: [Shadow(blurRadius: 8, color: Colors.black)], - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/pages/home/home_logic.dart b/lib/pages/home/home_logic.dart index d4a6e49..307b42c 100644 --- a/lib/pages/home/home_logic.dart +++ b/lib/pages/home/home_logic.dart @@ -49,21 +49,20 @@ class HomeLogic extends ChangeNotifier { notify(); } - void openMenu() { - drawerOpen = true; + void setMenuOpen(bool value) { + if (drawerOpen == value) return; + drawerOpen = value; notify(); - scaffoldKey.currentState?.openDrawer(); } void selectFromMenu(int nextTab, {int? mySection}) { - scaffoldKey.currentState?.closeDrawer(); + drawerOpen = false; select(nextTab, mySection: mySection); } Future openCurrentSeries(BuildContext context) async { detailOpen = true; notify(); - scaffoldKey.currentState?.closeDrawer(); await WidgetsBinding.instance.endOfFrame; if (!context.mounted) return; try { @@ -102,13 +101,11 @@ class HomeLogic extends ChangeNotifier { void openProfile() { profile = true; - scaffoldKey.currentState?.closeDrawer(); select(2, mySection: 0); } void openLibrary({int section = 1}) { profile = false; - scaffoldKey.currentState?.closeDrawer(); select(2, mySection: section); } @@ -139,24 +136,14 @@ class HomeLogic extends ChangeNotifier { } void onSystemBack() { - if (drawerOpen) { - scaffoldKey.currentState?.closeDrawer(); - } else if (videoImmersive) { + if (videoImmersive) { backRequest++; notify(); } else if (tab != 1) { select(1); - } else { - openMenu(); } } - void onDrawerChanged(bool opened) { - if (drawerOpen == opened) return; - drawerOpen = opened; - notify(); - } - void onDramaChanged(Drama drama) { currentDrama = drama; notify(); diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 2bdb917..2c9377c 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -24,7 +24,7 @@ import '../mine/mine_view.dart'; import '../player/player_view.dart'; import 'home_binding.dart'; import 'home_logic.dart'; -import 'widgets/home_quick_drawer.dart'; +import 'widgets/home_menu_sheet.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -173,21 +173,15 @@ class _HomeTabsState extends State<_HomeTabs> { canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; - logic.onSystemBack(); + if (!logic.videoImmersive && logic.tab == 1) { + unawaited(_openMenu(context)); + } else { + logic.onSystemBack(); + } }, child: Scaffold( key: logic.scaffoldKey, backgroundColor: AppColors.ink, - drawer: HomeQuickDrawer( - drama: logic.currentDrama, - onOpenSeries: () => logic.openCurrentSeries(context), - onDiscover: () => logic.selectFromMenu(1), - onHistory: () => logic.openLibrary(section: 0), - onWatchlist: logic.openLibrary, - onLiked: () => logic.selectFromMenu(2, mySection: 2), - ), - drawerEnableOpenDragGesture: false, - onDrawerChanged: logic.onDrawerChanged, body: IndexedStack( index: logic.tab, children: [ @@ -204,7 +198,7 @@ class _HomeTabsState extends State<_HomeTabs> { onDiscover: () => logic.select(1), onMy: logic.openProfile, onSearch: () => logic.select(1, search: true), - onMenu: logic.openMenu, + onMenu: () => _openMenu(context), onImmersiveChanged: logic.onImmersiveChanged, ), DiscoverPage( @@ -284,4 +278,23 @@ class _HomeTabsState extends State<_HomeTabs> { logic.select(tab); } } + + Future _openMenu(BuildContext context) async { + logic.setMenuOpen(true); + await showModalBottomSheet( + context: context, + backgroundColor: AppColors.card, + showDragHandle: true, + isScrollControlled: true, + builder: (_) => HomeMenuSheet( + drama: logic.currentDrama, + onOpenSeries: () => logic.openCurrentSeries(context), + onDiscover: () => logic.selectFromMenu(1), + onHistory: () => logic.openLibrary(section: 0), + onWatchlist: logic.openLibrary, + onLiked: () => logic.selectFromMenu(2, mySection: 2), + ), + ); + logic.setMenuOpen(false); + } } diff --git a/lib/pages/home/widgets/home_menu_sheet.dart b/lib/pages/home/widgets/home_menu_sheet.dart new file mode 100644 index 0000000..e870bd4 --- /dev/null +++ b/lib/pages/home/widgets/home_menu_sheet.dart @@ -0,0 +1,246 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../core/config/locale_controller.dart'; +import '../../../core/data/models/drama.dart'; +import '../../../core/formatting/localized_content.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/remote_image.dart'; +import '../../../l10n/app_localizations.dart'; + +/// Compact navigation menu opened from the preview home header. +class HomeMenuSheet extends StatelessWidget { + const HomeMenuSheet({ + super.key, + required this.drama, + required this.onOpenSeries, + required this.onDiscover, + required this.onHistory, + required this.onWatchlist, + required this.onLiked, + }); + + final Drama drama; + final VoidCallback onOpenSeries; + final VoidCallback onDiscover; + final VoidCallback onHistory; + final VoidCallback onWatchlist; + final VoidCallback onLiked; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final languageCode = Localizations.localeOf(context).languageCode; + void closeThen(VoidCallback action) { + Navigator.pop(context); + action(); + } + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.cardAlt, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: RemoteImage(drama.posterUrl, width: 64, height: 86), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizedDramaTitle(l10n, drama), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 15, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 4), + Text( + l10n.episodeCount(drama.episodeCount), + style: const TextStyle( + color: AppColors.muted, + fontSize: 9, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 34, + child: FilledButton.icon( + key: const Key('drawer-open-series'), + onPressed: () => closeThen(onOpenSeries), + style: FilledButton.styleFrom( + backgroundColor: AppColors.blue, + foregroundColor: AppColors.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + ), + textStyle: const TextStyle(fontSize: 11), + ), + icon: const Icon( + Icons.play_arrow_rounded, + size: 15, + ), + label: Text(l10n.exploreSeries), + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 14), + Text( + l10n.quickAccess, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 13, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _MenuTile( + key: const Key('drawer-discover'), + icon: Icons.explore_outlined, + label: l10n.discoverTab, + onTap: () => closeThen(onDiscover), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _MenuTile( + key: const Key('drawer-history'), + icon: Icons.history_rounded, + label: l10n.history, + onTap: () => closeThen(onHistory), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _MenuTile( + key: const Key('drawer-watchlist'), + icon: Icons.bookmark_border_rounded, + label: l10n.watchlist, + onTap: () => closeThen(onWatchlist), + ), + ), + const SizedBox(width: 8), + Expanded( + child: _MenuTile( + key: const Key('drawer-liked'), + icon: Icons.favorite_border_rounded, + label: l10n.likedTab, + onTap: () => closeThen(onLiked), + ), + ), + ], + ), + const SizedBox(height: 14), + Text( + l10n.appLanguage, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 13, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final (code, label) in [ + ('en', l10n.english), + ('zh', l10n.simplifiedChinese), + ('id', l10n.bahasaIndonesia), + ]) + ChoiceChip( + label: Text(label), + selected: languageCode == code, + onSelected: (_) => + context.read().select(code), + backgroundColor: AppColors.input, + selectedColor: AppColors.blue, + labelStyle: TextStyle( + color: languageCode == code + ? AppColors.onPrimary + : AppColors.muted, + fontSize: 10, + ), + side: BorderSide.none, + showCheckmark: false, + ), + ], + ), + ], + ), + ), + ); + } +} + +class _MenuTile extends StatelessWidget { + const _MenuTile({ + super.key, + required this.icon, + required this.label, + required this.onTap, + }); + + final IconData icon; + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: AppColors.quick, + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: SizedBox( + height: 58, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: AppColors.secondary, size: 16), + const SizedBox(height: 4), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: AppColors.muted, fontSize: 9), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/home/widgets/home_quick_drawer.dart b/lib/pages/home/widgets/home_quick_drawer.dart deleted file mode 100644 index b18575f..0000000 --- a/lib/pages/home/widgets/home_quick_drawer.dart +++ /dev/null @@ -1,287 +0,0 @@ -// Author: fruit.com.cn - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../core/config/locale_controller.dart'; -import '../../../core/data/models/drama.dart'; -import '../../../core/formatting/localized_content.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_dimens.dart'; -import '../../../core/widgets/remote_image.dart'; -import '../../../l10n/app_localizations.dart'; - -/// Shortcuts for the existing root tabs; this widget owns no navigation state. -class HomeQuickDrawer extends StatelessWidget { - const HomeQuickDrawer({ - super.key, - required this.drama, - required this.onOpenSeries, - required this.onDiscover, - required this.onHistory, - required this.onWatchlist, - required this.onLiked, - }); - - final Drama drama; - final VoidCallback onOpenSeries; - final VoidCallback onDiscover; - final VoidCallback onHistory; - final VoidCallback onWatchlist; - final VoidCallback onLiked; - - static const _panel = AppColors.card; - static const _card = AppColors.raised; - static const _accent = AppColors.blueSoft; - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final languageCode = Localizations.localeOf(context).languageCode; - return Drawer( - width: (MediaQuery.sizeOf(context).width * 0.83).clamp(0.0, 360.0), - backgroundColor: _panel, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.horizontal(right: Radius.circular(20)), - ), - child: SafeArea( - child: ListView( - padding: const EdgeInsets.fromLTRB(16, 24, 16, 24), - children: [ - Row( - children: [ - const CircleAvatar( - radius: 30, - backgroundColor: AppColors.raised, - child: Icon( - Icons.person_rounded, - size: 34, - color: Colors.white, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.localViewer, - style: const TextStyle( - color: Colors.white, - fontSize: 19, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 3), - Text( - l10n.localDemoProfile, - style: const TextStyle(color: AppColors.cream), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 26), - _SectionCard( - title: l10n.currentSeries, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - AppDimens.innerRadius, - ), - child: RemoteImage( - drama.posterUrl, - width: 74, - height: 100, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - localizedDramaTitle(l10n, drama), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 16, - ), - ), - const SizedBox(height: 7), - Text( - l10n.episodeCount(drama.episodeCount), - style: const TextStyle(color: AppColors.cream), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 14), - SizedBox( - width: double.infinity, - child: FilledButton.icon( - key: const Key('drawer-open-series'), - onPressed: onOpenSeries, - style: FilledButton.styleFrom( - backgroundColor: AppColors.blue, - foregroundColor: AppColors.onPrimary, - ), - icon: const Icon(Icons.play_arrow_rounded), - label: Text(l10n.exploreSeries), - ), - ), - ], - ), - ), - const SizedBox(height: 14), - _SectionCard( - title: l10n.quickAccess, - child: Column( - children: [ - Row( - children: [ - Expanded( - child: _QuickAction( - key: const Key('drawer-discover'), - icon: Icons.explore_outlined, - label: l10n.discoverTab, - onTap: onDiscover, - ), - ), - const SizedBox(width: 9), - Expanded( - child: _QuickAction( - key: const Key('drawer-history'), - icon: Icons.history_rounded, - label: l10n.history, - onTap: onHistory, - ), - ), - ], - ), - const SizedBox(height: 9), - Row( - children: [ - Expanded( - child: _QuickAction( - key: const Key('drawer-watchlist'), - icon: Icons.bookmark_border_rounded, - label: l10n.watchlist, - onTap: onWatchlist, - ), - ), - const SizedBox(width: 9), - Expanded( - child: _QuickAction( - key: const Key('drawer-liked'), - icon: Icons.favorite_border_rounded, - label: l10n.likedTab, - onTap: onLiked, - ), - ), - ], - ), - ], - ), - ), - const SizedBox(height: 14), - _SectionCard( - title: l10n.appLanguage, - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (final (code, label) in [ - ('en', l10n.english), - ('zh', l10n.simplifiedChinese), - ('id', l10n.bahasaIndonesia), - ]) - ChoiceChip( - label: Text(label, maxLines: 1), - selected: languageCode == code, - onSelected: (_) => - context.read().select(code), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _SectionCard extends StatelessWidget { - const _SectionCard({required this.title, required this.child}); - - final String title; - final Widget child; - - @override - Widget build(BuildContext context) => Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: HomeQuickDrawer._card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700), - ), - const SizedBox(height: 14), - child, - ], - ), - ); -} - -class _QuickAction extends StatelessWidget { - const _QuickAction({ - super.key, - required this.icon, - required this.label, - required this.onTap, - }); - - final IconData icon; - final String label; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) => Material( - color: AppColors.raised, - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 7), - child: Column( - children: [ - Icon(icon, color: HomeQuickDrawer._accent, size: 25), - const SizedBox(height: 6), - Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), - ), - ], - ), - ), - ), - ); -} diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index 64c459e..7362e1d 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/drama.dart'; @@ -71,10 +70,8 @@ class PlayerLogic extends ChangeNotifier { bool muted = false; bool stoppedAtFeedEnd = false; VideoPlayerPort? scrubbingPlayer; - bool fullscreen = false; bool pip = false; bool pipPreparing = false; - bool screenCleared = false; final Map pickedEpisode = {}; bool Function() isMounted = () => false; @@ -98,9 +95,9 @@ class PlayerLogic extends ChangeNotifier { /// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。 bool _started = false; - bool get immersive => fullscreen || pip || pipPreparing; + bool get immersive => pip || pipPreparing; - bool get chromeHidden => immersive || screenCleared; + bool get chromeHidden => immersive; Episode displayEpisode(Drama drama, Episode fallback) { final number = pickedEpisode[drama.id]; @@ -111,13 +108,6 @@ class PlayerLogic extends ChangeNotifier { return fallback; } - void setScreenCleared(bool value) { - if (screenCleared == value) return; - screenCleared = value; - notify(); - notifyImmersive(); - } - void notify() { if (!_disposed) notifyListeners(); } @@ -262,13 +252,7 @@ class PlayerLogic extends ChangeNotifier { final request = backRequest; WidgetsBinding.instance.addPostFrameCallback((_) { if (isMounted() && backRequest == request) { - if (fullscreen) { - unawaited(setFullscreen(false)); - } else if (screenCleared) { - setScreenCleared(false); - } else { - onMenu?.call(); - } + onMenu?.call(); } }); } @@ -295,12 +279,6 @@ class PlayerLogic extends ChangeNotifier { if (_disposed) return; _disposed = true; pictureInPicture.setStateListener(null); - if (fullscreen) { - unawaited( - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]), - ); - unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); - } unawaited(saveCurrentProgress()); if (_started) { session.setRouteVisible(false); @@ -313,32 +291,6 @@ class PlayerLogic extends ChangeNotifier { void notifyImmersive() => onImmersiveChanged?.call(chromeHidden); - Future setFullscreen(bool value) async { - if (fullscreen == value || !isMounted()) return; - fullscreen = value; - notify(); - notifyImmersive(); - try { - await SystemChrome.setPreferredOrientations( - value - ? [ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ] - : [DeviceOrientation.portraitUp], - ); - await SystemChrome.setEnabledSystemUIMode( - value ? SystemUiMode.immersiveSticky : SystemUiMode.edgeToEdge, - ); - } catch (_) { - if (value && isMounted()) { - fullscreen = false; - notify(); - notifyImmersive(); - } - } - } - Future enterPip(BuildContext context) async { if (pip || pipPreparing) return; if (session.active is! VideoPlayerPort) { @@ -347,7 +299,6 @@ class PlayerLogic extends ChangeNotifier { ); return; } - if (fullscreen) await setFullscreen(false); pipPreparing = true; notify(); notifyImmersive(); diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 63a1b63..997cfe6 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -162,7 +162,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { logic.session.preparedPlayerAt(index); final videoPort = port is VideoPlayerPort ? port : null; final ready = videoPort?.controller.value.isInitialized ?? false; - final theater = logic.fullscreen || logic.pip || logic.pipPreparing; + final theater = logic.pip || logic.pipPreparing; Widget? video; if (ready) { video = GestureDetector( @@ -322,20 +322,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ), if (logic.pip || logic.pipPreparing) const SizedBox.expand() - else if (logic.fullscreen) - _FullscreenControls( - l10n: l10n, - controller: ready ? videoPort!.controller : null, - onClose: () => unawaited(logic.setFullscreen(false)), - onPip: logic.pictureInPicture.isPlatformSupported - ? () => unawaited(logic.enterPip(context)) - : null, - onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null, - onSeek: ready - ? (position) => logic.seekTo(context, videoPort!, position) - : null, - ) - else if (!logic.screenCleared && widget.homeMode) + else if (widget.homeMode) HomeFeedOverlay( l10n: l10n, drama: drama, @@ -375,13 +362,12 @@ class _PlayerPageState extends State with WidgetsBindingObserver { onShare: () => logic.shareDrama(context, drama), onEpisodes: () => logic.showEpisodes(context), onDownload: () => requestEpisodeDownload(context, episode.id), - onClear: () => logic.setScreenCleared(true), onSearch: widget.onSearch ?? () => logic.showDiscover(context), onPip: logic.pictureInPicture.isPlatformSupported ? () => unawaited(logic.enterPip(context)) : null, ) - else if (!logic.screenCleared) + else _PlaybackOverlay( l10n: l10n, drama: drama, @@ -441,81 +427,11 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ? (position) => logic.seekTo(context, videoPort!, position) : null, ), - if (logic.screenCleared) - SafeArea( - child: Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(top: 4, right: 8), - child: IconButton( - tooltip: l10n.exitClearScreen, - onPressed: () => logic.setScreenCleared(false), - style: IconButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: const Color(0x59000000), - ), - icon: const Icon(Icons.close_fullscreen_rounded), - ), - ), - ), - ), ], ); } } -class _FullscreenControls extends StatelessWidget { - const _FullscreenControls({ - required this.l10n, - required this.controller, - required this.onClose, - required this.onPip, - required this.onScrubStart, - required this.onSeek, - }); - - final AppLocalizations l10n; - final VideoPlayerController? controller; - final VoidCallback onClose; - final VoidCallback? onPip; - final VoidCallback? onScrubStart; - final Future Function(Duration)? onSeek; - - @override - Widget build(BuildContext context) => SafeArea( - child: Column( - children: [ - Row( - children: [ - IconButton( - tooltip: l10n.exitFullscreen, - onPressed: onClose, - icon: const Icon(Icons.close_fullscreen_rounded), - ), - const Spacer(), - if (onPip != null) - IconButton( - tooltip: l10n.pictureInPicture, - onPressed: onPip, - icon: const Icon(Icons.picture_in_picture_alt_outlined), - ), - ], - ), - const Spacer(), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), - child: SeekProgressBar( - valueListenable: controller, - semanticsLabel: l10n.seekVideo, - onScrubStart: onScrubStart, - onSeek: onSeek, - ), - ), - ], - ), - ); -} - /// 不可播放提示:文案由后端返回的 `accessType` 驱动(契约 §1.4),前端不自行推断。 class _BlockedNotice extends StatelessWidget { const _BlockedNotice({ diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 1e0de71..faca527 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -26,7 +26,6 @@ class HomeFeedOverlay extends StatelessWidget { required this.onShare, required this.onEpisodes, required this.onDownload, - required this.onClear, required this.onSearch, this.onPip, }); @@ -42,7 +41,6 @@ class HomeFeedOverlay extends StatelessWidget { final VoidCallback onShare; final VoidCallback onEpisodes; final VoidCallback onDownload; - final VoidCallback onClear; final VoidCallback onSearch; final VoidCallback? onPip; @@ -332,18 +330,6 @@ class HomeFeedOverlay extends StatelessWidget { onPip!(); }, ), - ListTile( - key: const Key('home-feed-clear'), - leading: const Icon( - Icons.fit_screen_outlined, - color: AppColors.secondary, - ), - title: Text(l10n.clearScreen), - onTap: () { - Navigator.pop(sheetContext); - onClear(); - }, - ), ], ), ), diff --git a/test/discover/cover_carousel_test.dart b/test/discover/cover_carousel_test.dart deleted file mode 100644 index 75040b6..0000000 --- a/test/discover/cover_carousel_test.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fruitshorts/l10n/app_localizations.dart'; -import 'package:fruitshorts/pages/discover/widgets/cover_carousel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - List items(int count) => [ - for (var index = 0; index < count; index++) - CoverItem( - title: 'Banner $index', - subtitle: 'Subtitle $index', - image: '/files/banner-$index.png', - onOpen: _noop, - ), - ]; - - testWidgets('should_turn_cube_and_keep_title_on_poster_when_carousel_pages', ( - tester, - ) async { - await tester.pumpWidget( - _localizedApp( - CoverCarousel( - items: [ - for (final title in ['The Signal', 'City Lights', 'Vengeance']) - CoverItem( - title: title, - subtitle: 'Mock', - image: '/files/poster.png', - onOpen: _noop, - ), - ], - ), - ), - ); - await tester.pump(); - - expect(find.text('The Signal'), findsOneWidget); - expect(find.text('Mock'), findsOneWidget); - - final box = tester.getRect(find.byType(CoverCarousel)); - final gesture = await tester.startGesture( - Offset(box.right - 40, box.center.dy), - ); - await gesture.moveBy(const Offset(-500, 0)); - await tester.pump(); - - final turned = tester - .widgetList(find.byType(Transform)) - .any((transform) => (transform.transform.entry(0, 0) - 1).abs() > 0.15); - expect(turned, isTrue); - - await gesture.moveBy(const Offset(-200, 0)); - await gesture.up(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 450)); - - expect(find.text('City Lights'), findsOneWidget); - expect(tester.takeException(), isNull); - }); - - testWidgets('should_auto_advance_and_show_dots_when_multiple_items_exist', ( - tester, - ) async { - await tester.pumpWidget(_localizedApp(CoverCarousel(items: items(3)))); - await tester.pump(); - - expect(find.text('Banner 0'), findsOneWidget); - expect(find.byKey(const Key('carousel-dot-0')), findsOneWidget); - expect(find.byKey(const Key('carousel-dot-1')), findsOneWidget); - expect(find.byKey(const Key('carousel-dot-2')), findsOneWidget); - - await tester.pump(const Duration(milliseconds: 4500)); - await tester.pump(const Duration(milliseconds: 500)); - - expect(find.text('Banner 1'), findsOneWidget); - await tester.pumpWidget(const SizedBox.shrink()); - }); - - testWidgets('should_not_schedule_auto_advance_when_only_one_item_exists', ( - tester, - ) async { - await tester.pumpWidget(_localizedApp(CoverCarousel(items: items(1)))); - await tester.pump(const Duration(seconds: 12)); - - expect(find.text('Banner 0'), findsOneWidget); - expect(find.byKey(const Key('carousel-dot-0')), findsOneWidget); - expect(find.byKey(const Key('carousel-dot-1')), findsNothing); - }); - - testWidgets('should_pause_auto_advance_while_pointer_is_down', ( - tester, - ) async { - await tester.pumpWidget(_localizedApp(CoverCarousel(items: items(3)))); - await tester.pump(); - final box = tester.getRect(find.byType(CoverCarousel)); - final gesture = await tester.startGesture(box.center); - await tester.pump(const Duration(seconds: 6)); - - expect(find.text('Banner 0'), findsOneWidget); - - await gesture.up(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 4500)); - await tester.pump(const Duration(milliseconds: 500)); - - expect(find.text('Banner 1'), findsOneWidget); - await tester.pumpWidget(const SizedBox.shrink()); - }); - - testWidgets('should_pause_when_inactive_and_resume_when_active_again', ( - tester, - ) async { - final active = ValueNotifier(false); - await tester.pumpWidget( - _localizedApp( - ValueListenableBuilder( - valueListenable: active, - builder: (context, enabled, _) => - CoverCarousel(items: items(2), active: enabled), - ), - ), - ); - await tester.pump(const Duration(seconds: 6)); - expect(find.text('Banner 0'), findsOneWidget); - - active.value = true; - await tester.pump(); - await tester.pump(const Duration(milliseconds: 4500)); - await tester.pump(const Duration(milliseconds: 500)); - expect(find.text('Banner 1'), findsOneWidget); - await tester.pumpWidget(const SizedBox.shrink()); - }); - - testWidgets('should_show_icon_fallback_when_banner_image_is_empty', ( - tester, - ) async { - await tester.pumpWidget( - _localizedApp( - CoverCarousel( - items: const [ - CoverItem( - title: 'Broken image', - subtitle: '', - image: '', - onOpen: _noop, - ), - ], - ), - ), - ); - await tester.pump(); - - expect(find.byKey(const Key('carousel-image-fallback')), findsOneWidget); - }); - - testWidgets( - 'should_keep_existing_open_callback_when_active_poster_is_tapped', - (tester) async { - var opened = 0; - await tester.pumpWidget( - _localizedApp( - CoverCarousel( - items: [ - CoverItem( - title: 'Tap me', - subtitle: '', - image: '/files/banner.png', - onOpen: () => opened++, - ), - ...items(1), - ], - ), - ), - ); - await tester.pump(); - - await tester.tap(find.text('Tap me')); - expect(opened, 1); - await tester.pumpWidget(const SizedBox.shrink()); - }, - ); -} - -void _noop() {} - -Widget _localizedApp(Widget child) => MaterialApp( - locale: const Locale('en'), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: Scaffold(body: child), -); diff --git a/test/discover/home_all_dramas_grid_test.dart b/test/discover/home_all_dramas_grid_test.dart index 3ff02ef..79d4be9 100644 --- a/test/discover/home_all_dramas_grid_test.dart +++ b/test/discover/home_all_dramas_grid_test.dart @@ -38,7 +38,6 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Continue Watching'), findsNothing); - expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('Recommended'), findsOneWidget); expect(find.text('For You'), findsNothing); final grid = tester.widget( @@ -133,7 +132,6 @@ void main() { await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('No matching dramas.'), findsOneWidget); expect(find.byKey(const Key('empty-state-content')), findsOneWidget); @@ -153,7 +151,6 @@ void main() { await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('Categories could not be loaded.'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); expect(find.byKey(const Key('empty-state-icon')), findsOneWidget); @@ -179,7 +176,6 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-all-dramas-title')), findsNothing); expect(find.text('City Lights & Velvet Nights'), findsWidgets); expect(tester.takeException(), isNull); }); diff --git a/test/discover/home_banner_source_test.dart b/test/discover/home_banner_source_test.dart deleted file mode 100644 index 0c5d90c..0000000 --- a/test/discover/home_banner_source_test.dart +++ /dev/null @@ -1,85 +0,0 @@ -// Author: fruit.com.cn - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fruitshorts/app.dart'; -import 'package:fruitshorts/core/data/models/drama.dart'; -import 'package:fruitshorts/core/data/models/home_feed.dart'; -import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; -import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; -import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; -import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; -import 'package:fruitshorts/pages/discover/widgets/cover_carousel.dart'; -import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; - -import '../support/fake_catalog.dart'; -import '../support/fake_category.dart'; -import '../support/fake_interaction_repositories.dart'; -import '../support/fake_notice_repository.dart'; -import '../support/widget_player.dart'; - -void main() { - testWidgets( - 'should_not_mount_carousel_on_discover_even_when_home_banners_exist', - (tester) async { - SharedPreferencesAsyncPlatform.instance = - InMemorySharedPreferencesAsync.empty(); - final catalog = _HomeBannerCatalogRepository(); - await tester.pumpWidget( - LumaDramaApp( - playerFactory: createWidgetPlayer, - catalogRepository: catalog, - categoryRepository: FakeCategoryRepository(catalog: catalog), - watchHistoryRepository: InMemoryWatchHistoryRepository(), - favoriteRepository: InMemoryFavoriteRepository(), - playbackRepository: FakePlaybackRepository(), - commentRepository: FakeCommentRepository(), - interactionRepository: FakeInteractionRepository(), - noticeRepository: FakeNoticeRepository(), - ), - ); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('tab-discover'))); - await tester.pumpAndSettle(); - - expect(find.byType(CoverCarousel), findsNothing); - expect(find.byKey(const Key('discover-search')), findsOneWidget); - expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); - expect(find.byKey(const Key('discover-quick-filter')), findsOneWidget); - expect(find.text('The Signal'), findsWidgets); - await tester.pumpWidget(const SizedBox.shrink()); - }, - ); -} - -class _HomeBannerCatalogRepository implements CatalogRepository { - final _delegate = FakeCatalogRepository(); - - @override - Future fetchHomeFeed() async => HomeFeed( - banners: const [ - FeedBanner( - id: 'banner-1', - imageUrl: '/files/banner-1.png', - linkType: 'DRAMA', - linkValue: 'the-signal-demo', - ), - FeedBanner( - id: 'banner-2', - imageUrl: '/files/banner-2.png', - linkType: 'DRAMA', - linkValue: 'city-lights-demo', - ), - ], - sections: [FeedSection(code: 'hot', title: 'Hot', dramas: demoDramas)], - ); - - @override - Future fetchDiscoverFeed() async => HomeFeed( - sections: [FeedSection(code: 'hot', title: 'Hot', dramas: demoDramas)], - ); - - @override - Future fetchDrama(String dramaId) => _delegate.fetchDrama(dramaId); -} diff --git a/test/home/home_logic_test.dart b/test/home/home_logic_test.dart index 7daa918..85c609c 100644 --- a/test/home/home_logic_test.dart +++ b/test/home/home_logic_test.dart @@ -109,16 +109,15 @@ void main() { }, ); - test('should_return_home_and_open_drawer_when_system_back_is_pressed', () { + test('should_return_to_discover_and_handle_immersive_back', () { final logic = HomeLogic(dramas); // Home is the launch tab; the first back press lands on Discover. logic.onSystemBack(); expect(logic.tab, 1); logic.onSystemBack(); - expect(logic.drawerOpen, isTrue); + expect(logic.tab, 1); expect(logic.backRequest, 0); - logic.onDrawerChanged(false); logic.select(0); logic.onSystemBack(); @@ -131,30 +130,25 @@ void main() { expect(logic.backRequest, 1); expect(logic.tab, 0); - logic.onDrawerChanged(true); - logic.onSystemBack(); - expect(logic.backRequest, 1); - - logic.onDrawerChanged(false); logic.onImmersiveChanged(false); logic.onSystemBack(); expect(logic.tab, 1); logic.dispose(); }); - test('should_notify_only_on_actual_change_when_drawer_state_is_set', () { + test('should_notify_only_on_actual_change_when_menu_state_is_set', () { final logic = HomeLogic(dramas); var notifications = 0; logic.addListener(() => notifications++); - logic.onDrawerChanged(false); + logic.setMenuOpen(false); expect(notifications, 0); - logic.onDrawerChanged(true); + logic.setMenuOpen(true); expect(logic.drawerOpen, isTrue); expect(notifications, 1); - logic.onDrawerChanged(true); + logic.setMenuOpen(true); expect(notifications, 1); logic.dispose(); }); diff --git a/test/home/home_nav_ui_test.dart b/test/home/home_nav_ui_test.dart index 14ceed5..0c9d15a 100644 --- a/test/home/home_nav_ui_test.dart +++ b/test/home/home_nav_ui_test.dart @@ -95,24 +95,9 @@ void main() { ); }); - testWidgets('should_hide_themed_navigation_when_immersive_mode_is_enabled', ( - tester, - ) async { - await _pumpApp(tester); - await tester.tap(find.byKey(const Key('home-feed-more'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('home-feed-clear'))); - await tester.pumpAndSettle(); - - for (final key in _keys) { - expect(find.byKey(key), findsNothing); - } - }); - testWidgets('should_map_three_tabs_to_home_discover_and_me', (tester) async { await _pumpApp(tester); - expect(find.byKey(const Key('tab-my-list')), findsNothing); expect(find.byKey(const Key('tab-home')), findsOneWidget); expect(find.byKey(const Key('tab-discover')), findsOneWidget); expect(find.byKey(const Key('tab-my')), findsOneWidget); diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index e9693bc..1cd1142 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -81,9 +81,6 @@ void _expectFullscreenOverlay() { expect(find.byKey(const Key('rail-like')), findsNothing); expect(find.byKey(const Key('rail-save')), findsNothing); - expect(find.byKey(const Key('card-player-video-frame')), findsNothing); - expect(find.byKey(const Key('card-player-progress')), findsNothing); - expect(find.byKey(const Key('card-player-next')), findsNothing); } Future _pumpApp(WidgetTester tester) async { diff --git a/test/player/player_logic_test.dart b/test/player/player_logic_test.dart index 0b516ea..ff0b5c9 100644 --- a/test/player/player_logic_test.dart +++ b/test/player/player_logic_test.dart @@ -117,33 +117,7 @@ void main() { }); group('surface flags', () { - test('should_hide_chrome_until_restored_when_screen_is_cleared', () { - final logic = _logic(FakeWatchHistory()); - final immersive = []; - logic.onImmersiveChanged = immersive.add; - var notifications = 0; - logic.addListener(() => notifications++); - - expect(logic.chromeHidden, isFalse); - expect(logic.immersive, isFalse); - - logic.setScreenCleared(true); - expect(logic.chromeHidden, isTrue); - expect(notifications, 1); - expect(immersive, [true]); - - // Repeating the same value is not a state change. - logic.setScreenCleared(true); - expect(notifications, 1); - expect(immersive, [true]); - - logic.setScreenCleared(false); - expect(logic.chromeHidden, isFalse); - expect(notifications, 2); - expect(immersive, [true, false]); - }); - - test('should_outrank_cleared_screen_flag_when_picture_in_picture_callback_fires', () async { + test('should_set_immersive_when_picture_in_picture_callback_fires', () async { final factory = _RecordingFactory(); final drama = _drama('signal'); final logic = _logic(FakeWatchHistory()); diff --git a/test/widget_test.dart b/test/widget_test.dart index 10525fd..aa33829 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -122,38 +122,12 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-download')), findsOneWidget); expect(find.byKey(const Key('home-feed-pip')), findsOneWidget); - expect(find.byKey(const Key('home-feed-clear')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Watch Full'), findsNothing); expect(find.byTooltip('Explore series'), findsNothing); }); - testWidgets('should_hide_tabs_and_restore_them_when_clear_screen_is_tapped', ( - tester, - ) async { - tester.view.physicalSize = const Size(390, 844); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - - await tester.pumpWidget(appUnderTest()); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('tab-home'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('home-feed-more'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('home-feed-clear'))); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-tabs-bar')), findsNothing); - expect(find.byTooltip('Exit clear screen'), findsOneWidget); - await tester.tap(find.byTooltip('Exit clear screen')); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget); - expect(find.byKey(const Key('home-feed-more')), findsOneWidget); - expect(tester.takeException(), isNull); - }); - testWidgets( 'should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used', (tester) async { @@ -177,13 +151,7 @@ void main() { ); expect(find.text('Quick access'), findsOneWidget); expect(find.text('Bahasa Indonesia'), findsOneWidget); - expect( - find.descendant( - of: find.byType(Drawer), - matching: find.text('7 EPISODES'), - ), - findsOneWidget, - ); + expect(find.text('7 EPISODES'), findsOneWidget); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('drawer-open-series')));