merge: 清理 K26 旧 UI 与死代码
This commit is contained in:
commit
c82502559b
23 files changed
+291
-1225
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => '登录';
|
||||
|
||||
|
||||
@@ -142,8 +142,6 @@
|
||||
"reportFailed": "举报提交失败,请重试。",
|
||||
"shareFailed": "无法打开系统分享面板。",
|
||||
"nowPlaying": "正在播放",
|
||||
"watchFullscreen": "横屏全屏观看",
|
||||
"exitFullscreen": "退出全屏",
|
||||
"pictureInPicture": "画中画",
|
||||
"pipUnavailable": "此设备无法使用画中画",
|
||||
"originalTag": "原创",
|
||||
@@ -234,8 +232,6 @@
|
||||
"myListTab": "片单",
|
||||
"watchFull": "看全集",
|
||||
"swipeForNext": "上滑看下一部",
|
||||
"clearScreen": "清屏",
|
||||
"exitClearScreen": "退出清屏",
|
||||
"logIn": "登录",
|
||||
"logInHint": "登录后保存金币和片单",
|
||||
"rewards": "奖励",
|
||||
|
||||
@@ -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<CoverItem> items;
|
||||
final bool active;
|
||||
|
||||
@override
|
||||
State<CoverCarousel> createState() => _CoverCarouselState();
|
||||
}
|
||||
|
||||
class _CoverCarouselState extends State<CoverCarousel>
|
||||
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<int> get _visible {
|
||||
final first = _page.floor();
|
||||
final indexes = <int>[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)],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> 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();
|
||||
|
||||
@@ -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<void> _openMenu(BuildContext context) async {
|
||||
logic.setMenuOpen(true);
|
||||
await showModalBottomSheet<void>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<LocaleController>().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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<LocaleController>().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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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<String, int> 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<void> 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<void> 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();
|
||||
|
||||
@@ -162,7 +162,7 @@ class _PlayerPageState extends State<PlayerPage> 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<PlayerPage> 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<PlayerPage> 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<PlayerPage> 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<void> 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({
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<CoverItem> 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<Transform>(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<bool>(
|
||||
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),
|
||||
);
|
||||
@@ -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<SliverGrid>(
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<HomeFeed> 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<HomeFeed> fetchDiscoverFeed() async => HomeFeed(
|
||||
sections: [FeedSection(code: 'hot', title: 'Hot', dramas: demoDramas)],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) => _delegate.fetchDrama(dramaId);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void> _pumpApp(WidgetTester tester) async {
|
||||
|
||||
@@ -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 = <bool>[];
|
||||
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());
|
||||
|
||||
+1
-33
@@ -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')));
|
||||
|
||||
Reference in new issue
Block a user