diff --git a/lib/app.dart b/lib/app.dart index 9ff5a7e..f0d10d8 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -33,6 +33,7 @@ import 'core/data/services/favorite_controller.dart'; import 'core/data/services/interaction_controller.dart'; import 'core/data/services/playback_session.dart'; import 'core/data/services/video_player_port.dart'; +import 'core/theme/app_theme.dart'; import 'l10n/app_localizations.dart'; import 'pages/home/home_view.dart'; @@ -193,31 +194,12 @@ class _LumaDramaAppState extends State { locale: locale.locale, supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, - localeResolutionCallback: (deviceLocale, supported) { - final resolved = locale.locale ?? deviceLocale; - if (resolved == null) return supported.first; - for (final item in supported) { - if (item.languageCode == resolved.languageCode) return item; - } - return supported.first; - }, + localeResolutionCallback: (deviceLocale, _) => + resolveAppLocale(locale.locale, deviceLocale), debugShowCheckedModeBanner: false, themeMode: ThemeMode.system, - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF2F7BFF), - ), - scaffoldBackgroundColor: const Color(0xFFF8F7FC), - ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF2F7BFF), - brightness: Brightness.dark, - ), - scaffoldBackgroundColor: const Color(0xFF101019), - ), + theme: AppTheme.light(), + darkTheme: AppTheme.dark(), home: const HomePage(), ), ), diff --git a/lib/core/config/locale_controller.dart b/lib/core/config/locale_controller.dart index c9b3b67..9404b4c 100644 --- a/lib/core/config/locale_controller.dart +++ b/lib/core/config/locale_controller.dart @@ -3,6 +3,19 @@ import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; +/// Resolves the selected or system language to one of the supported app locales. +/// +/// Indonesian is the launch-market fallback when a device uses an unsupported +/// language. The legacy `in` code is treated as Indonesian as well. +Locale resolveAppLocale(Locale? selected, Locale? deviceLocale) { + final code = (selected ?? deviceLocale)?.languageCode; + return switch (code) { + 'zh' => const Locale('zh'), + 'en' => const Locale('en'), + _ => const Locale('id'), + }; +} + class LocaleController extends ChangeNotifier { LocaleController({SharedPreferencesAsync? preferences}) : _preferences = preferences ?? SharedPreferencesAsync(); @@ -17,6 +30,7 @@ class LocaleController extends ChangeNotifier { _locale = switch (code) { 'en' => const Locale('en'), 'zh' => const Locale('zh'), + 'id' => const Locale('id'), _ => null, }; notifyListeners(); @@ -29,6 +43,7 @@ class LocaleController extends ChangeNotifier { _locale = switch (code) { 'en' => const Locale('en'), 'zh' => const Locale('zh'), + 'id' => const Locale('id'), _ => null, }; notifyListeners(); diff --git a/lib/core/formatting/localized_date_time.dart b/lib/core/formatting/localized_date_time.dart new file mode 100644 index 0000000..e5708ad --- /dev/null +++ b/lib/core/formatting/localized_date_time.dart @@ -0,0 +1,15 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +/// Formats a timestamp with the active Material locale and 12/24-hour setting. +String formatLocalizedDateTime(BuildContext context, DateTime value) { + final localizations = MaterialLocalizations.of(context); + final local = value.toLocal(); + final date = localizations.formatCompactDate(local); + final time = localizations.formatTimeOfDay( + TimeOfDay.fromDateTime(local), + alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context), + ); + return '$date $time'; +} diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 56de8ad..6f09ce2 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -2,8 +2,17 @@ import 'package:flutter/material.dart'; /// Shared colors. Blue is the primary action color. abstract final class AppColors { - static const blue = Color(0xFF2F7BFF); - static const blueSoft = Color(0xFF8FB4FF); + /// Klein Blue primary used by actions and selected states. + static const blue = Color(0xFF002FA7); + + /// Pressed/active shade for blue surfaces. + static const bluePressed = Color(0xFF00227A); + + /// High-contrast blue for text and icons on dark surfaces. + static const blueSoft = Color(0xFF5B7BE0); + + /// Text and icons placed directly on [blue]. + static const onPrimary = Color(0xFFFFFFFF); static const ink = Color(0xFF0B0C10); static const bar = Color(0xFF101114); static const card = Color(0xFF17191F); diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..0de77ba --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,106 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import 'app_colors.dart'; + +/// App-wide light and dark themes built from the approved Klein Blue palette. +abstract final class AppTheme { + static const _lightBackground = Color(0xFFF8F7FC); + static const _darkBackground = Color(0xFF101019); + + /// Light theme for system light mode. + static ThemeData light() => _build( + brightness: Brightness.light, + surface: _lightBackground, + accent: AppColors.blue, + ); + + /// Dark theme for system dark mode. + static ThemeData dark() => _build( + brightness: Brightness.dark, + surface: _darkBackground, + accent: AppColors.blueSoft, + ); + + static ThemeData _build({ + required Brightness brightness, + required Color surface, + required Color accent, + }) { + final dark = brightness == Brightness.dark; + final scheme = dark + ? const ColorScheme.dark( + primary: AppColors.blue, + onPrimary: AppColors.onPrimary, + secondary: AppColors.blueSoft, + onSecondary: AppColors.ink, + primaryContainer: AppColors.bluePressed, + onPrimaryContainer: AppColors.onPrimary, + secondaryContainer: AppColors.bluePressed, + onSecondaryContainer: AppColors.onPrimary, + surface: _darkBackground, + onSurface: Colors.white, + surfaceTint: AppColors.blueSoft, + inversePrimary: AppColors.blueSoft, + ) + : ColorScheme.light( + primary: AppColors.blue, + onPrimary: AppColors.onPrimary, + secondary: AppColors.blue, + onSecondary: AppColors.onPrimary, + primaryContainer: AppColors.blueSoft.withValues(alpha: 0.22), + onPrimaryContainer: AppColors.ink, + secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.22), + onSecondaryContainer: AppColors.ink, + surface: _lightBackground, + onSurface: AppColors.ink, + surfaceTint: AppColors.blue, + inversePrimary: AppColors.blueSoft, + ); + return ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: scheme, + scaffoldBackgroundColor: surface, + progressIndicatorTheme: ProgressIndicatorThemeData(color: accent), + sliderTheme: SliderThemeData( + activeTrackColor: accent, + thumbColor: AppColors.onPrimary, + ), + tabBarTheme: TabBarThemeData(labelColor: accent, indicatorColor: accent), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom(foregroundColor: accent), + ), + filledButtonTheme: FilledButtonThemeData( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith(_buttonColor), + foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), + ), + ), + switchTheme: SwitchThemeData( + thumbColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.selected) + ? AppColors.onPrimary + : null, + ), + trackColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.selected) ? accent : null, + ), + ), + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.bluePressed, + contentTextStyle: const TextStyle(color: AppColors.onPrimary), + ), + ); + } + + static Color _buttonColor(Set states) { + if (states.contains(WidgetState.pressed) || + states.contains(WidgetState.hovered) || + states.contains(WidgetState.focused)) { + return AppColors.bluePressed; + } + return AppColors.blue; + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e9e536f..de22c26 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -200,6 +200,7 @@ "systemLanguage": "System", "english": "English", "simplifiedChinese": "简体中文", + "bahasaIndonesia": "Bahasa Indonesia", "episodeOneTitle": "First Light", "episodeTwoTitle": "A New Frequency", "episodeThreeTitle": "Crossing Over", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 39656dc..7d02388 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -200,6 +200,7 @@ "systemLanguage": "Sistem", "english": "English", "simplifiedChinese": "简体中文", + "bahasaIndonesia": "Bahasa Indonesia", "episodeOneTitle": "Cahaya Pertama", "episodeTwoTitle": "Frekuensi Baru", "episodeThreeTitle": "Menyeberang", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d4ca405..1427bb2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -916,6 +916,12 @@ abstract class AppLocalizations { /// **'简体中文'** String get simplifiedChinese; + /// No description provided for @bahasaIndonesia. + /// + /// In en, this message translates to: + /// **'Bahasa Indonesia'** + String get bahasaIndonesia; + /// No description provided for @episodeOneTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e73718c..9243973 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -440,6 +440,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get simplifiedChinese => '简体中文'; + @override + String get bahasaIndonesia => 'Bahasa Indonesia'; + @override String get episodeOneTitle => 'First Light'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index b6ce3be..7d6b7e2 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -442,6 +442,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get simplifiedChinese => '简体中文'; + @override + String get bahasaIndonesia => 'Bahasa Indonesia'; + @override String get episodeOneTitle => 'Cahaya Pertama'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index e6d1590..0ddd0ad 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -434,6 +434,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get simplifiedChinese => '简体中文'; + @override + String get bahasaIndonesia => '印尼语'; + @override String get episodeOneTitle => '初见微光'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index a23e533..d664220 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -162,6 +162,7 @@ "systemLanguage": "跟随系统", "english": "English", "simplifiedChinese": "简体中文", + "bahasaIndonesia": "印尼语", "episodeOneTitle": "初见微光", "episodeTwoTitle": "新的频率", "episodeThreeTitle": "越过边界", diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart index 49dcc70..0b03e76 100644 --- a/lib/pages/auth/auth_view.dart +++ b/lib/pages/auth/auth_view.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../../core/data/remote/api_client.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../l10n/app_localizations.dart'; import 'auth_logic.dart'; @@ -82,102 +83,117 @@ class _AuthPageState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return Scaffold( - backgroundColor: AppColors.ink, - appBar: AppBar( + return Theme( + data: AppTheme.dark(), + child: Scaffold( backgroundColor: AppColors.ink, - elevation: 0, - foregroundColor: Colors.white, - title: Text(_logic.signUpMode ? l10n.signUp : l10n.signIn), - ), - body: SafeArea( - child: ListView( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 28), - children: [ - Text( - _logic.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle, - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(height: 6), - Text( - l10n.authSubtitle, - style: const TextStyle(color: AppColors.muted, fontSize: 13), - ), - const SizedBox(height: 22), - _field( - label: l10n.authEmailLabel, - hint: 'name@example.com', - keyboardType: TextInputType.emailAddress, - onChanged: (value) => _logic.email = value, - autofocus: true, - ), - const SizedBox(height: 14), - _field( - label: l10n.authPasswordLabel, - hint: l10n.authPasswordHint, - obscure: true, - onChanged: (value) => _logic.password = value, - ), - if (_logic.errorMessage != null) ...[ - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.error_outline, color: Color(0xFFFF6B6B), size: 16), - const SizedBox(width: 6), - Expanded( - child: Text( - _logic.errorMessage!, - style: const TextStyle(color: Color(0xFFFF6B6B), fontSize: 13), - ), - ), - ], - ), - ], - const SizedBox(height: 22), - SizedBox( - height: 48, - child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: AppColors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), + appBar: AppBar( + backgroundColor: AppColors.ink, + elevation: 0, + foregroundColor: Colors.white, + title: Text(_logic.signUpMode ? l10n.signUp : l10n.signIn), + ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 28), + children: [ + Text( + _logic.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w800, ), - onPressed: _logic.busy ? null : _submit, - child: _logic.busy - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : Text( - _logic.signUpMode ? l10n.signUp : l10n.signIn, + ), + const SizedBox(height: 6), + Text( + l10n.authSubtitle, + style: const TextStyle(color: AppColors.muted, fontSize: 13), + ), + const SizedBox(height: 22), + _field( + label: l10n.authEmailLabel, + hint: 'name@example.com', + keyboardType: TextInputType.emailAddress, + onChanged: (value) => _logic.email = value, + autofocus: true, + ), + const SizedBox(height: 14), + _field( + label: l10n.authPasswordLabel, + hint: l10n.authPasswordHint, + obscure: true, + onChanged: (value) => _logic.password = value, + ), + if (_logic.errorMessage != null) ...[ + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.error_outline, + color: Color(0xFFFF6B6B), + size: 16, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + _logic.errorMessage!, style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, + color: Color(0xFFFF6B6B), + fontSize: 13, ), ), + ), + ], + ), + ], + const SizedBox(height: 22), + SizedBox( + height: 48, + child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: AppColors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + onPressed: _logic.busy ? null : _submit, + child: _logic.busy + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + _logic.signUpMode ? l10n.signUp : l10n.signIn, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ), ), - ), - const SizedBox(height: 14), - TextButton( - onPressed: _logic.busy - ? null - : () => setState(() => _logic.toggleMode()), - child: Text( - _logic.signUpMode ? l10n.authSwitchToSignIn : l10n.authSwitchToSignUp, - style: const TextStyle(color: AppColors.blueSoft, fontSize: 13), + const SizedBox(height: 14), + TextButton( + onPressed: _logic.busy + ? null + : () => setState(() => _logic.toggleMode()), + child: Text( + _logic.signUpMode + ? l10n.authSwitchToSignIn + : l10n.authSwitchToSignUp, + style: const TextStyle( + color: AppColors.blueSoft, + fontSize: 13, + ), + ), ), - ), - ], + ], + ), ), ), ); @@ -214,7 +230,10 @@ class _AuthPageState extends State { hintStyle: const TextStyle(color: Color(0xFF6C7280), fontSize: 14), filled: true, fillColor: AppColors.card, - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 14, + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index 42bb636..965001d 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -9,6 +9,7 @@ import '../../core/data/models/home_feed.dart'; import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/services/category_controller.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../core/widgets/remote_image.dart'; import '../../l10n/app_localizations.dart'; import 'discover_binding.dart'; @@ -139,7 +140,7 @@ class _DiscoverPageState extends State { return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: Theme( - data: ThemeData.dark(useMaterial3: true), + data: AppTheme.dark(), child: widget.embedded ? content : Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content), @@ -254,7 +255,7 @@ class _DiscoverPageState extends State { Text( l10n.seeAll, style: const TextStyle( - color: AppColors.blue, + color: AppColors.blueSoft, fontSize: 13, fontWeight: FontWeight.w500, ), @@ -417,7 +418,9 @@ class _DiscoverPageState extends State { alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: selected ? AppColors.blue : const Color(0xFF1C1E24), + color: selected + ? AppColors.bluePressed + : const Color(0xFF1C1E24), borderRadius: BorderRadius.circular(16), ), child: Text( @@ -501,12 +504,12 @@ class _DiscoverNavItem extends StatelessWidget { Icon( icon, size: 21, - color: selected ? _orange : const Color(0xFFCAC4C7), + color: selected ? _accent : const Color(0xFFCAC4C7), ), Text( label, style: TextStyle( - color: selected ? _orange : const Color(0xFFCAC4C7), + color: selected ? _accent : const Color(0xFFCAC4C7), fontSize: 10, fontWeight: FontWeight.w700, ), @@ -549,7 +552,7 @@ List<_ShowcaseCard> _cardsFor(List dramas, String query) { ]; } -const _orange = AppColors.blue; +const _accent = AppColors.blueSoft; const _coin = Color(0xFFF5C451); class _BrandMark extends StatelessWidget { diff --git a/lib/pages/downloads/downloads_page.dart b/lib/pages/downloads/downloads_page.dart index f472d30..f1228d9 100644 --- a/lib/pages/downloads/downloads_page.dart +++ b/lib/pages/downloads/downloads_page.dart @@ -10,7 +10,9 @@ import '../../core/data/models/download_policy.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/download_controller.dart'; import '../../core/formatting/error_text.dart'; +import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../l10n/app_localizations.dart'; import 'downloads_binding.dart'; @@ -46,17 +48,20 @@ class _DownloadsPageState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; - return Scaffold( - key: const Key('downloads-page'), - backgroundColor: const Color(0xFF0B0C10), - appBar: AppBar( + return Theme( + data: AppTheme.dark(), + child: Scaffold( + key: const Key('downloads-page'), backgroundColor: const Color(0xFF0B0C10), - foregroundColor: Colors.white, - title: Text(l10n.downloads), + appBar: AppBar( + backgroundColor: const Color(0xFF0B0C10), + foregroundColor: Colors.white, + title: Text(l10n.downloads), + ), + body: signedIn + ? _DownloadsBody(downloads: context.watch()) + : const AuthGatePanel(), ), - body: signedIn - ? _DownloadsBody(downloads: context.watch()) - : const AuthGatePanel(), ); } } @@ -380,7 +385,9 @@ class _LicenseTile extends StatelessWidget { if (license.expireAt != null) ...[ const SizedBox(height: 7), Text( - l10n.downloadExpiresAt(_formatTime(license.expireAt!)), + l10n.downloadExpiresAt( + formatLocalizedDateTime(context, license.expireAt!), + ), style: const TextStyle(color: Color(0xFF8D8990), fontSize: 11), ), ], @@ -398,12 +405,3 @@ List _qualityChoices(String current) { } return choices; } - -String _formatTime(DateTime value) { - final local = value.toLocal(); - final month = local.month.toString().padLeft(2, '0'); - final day = local.day.toString().padLeft(2, '0'); - final hour = local.hour.toString().padLeft(2, '0'); - final minute = local.minute.toString().padLeft(2, '0'); - return '${local.year}-$month-$day $hour:$minute'; -} diff --git a/lib/pages/drama_detail/drama_detail_view.dart b/lib/pages/drama_detail/drama_detail_view.dart index af04e8b..62d0ed8 100644 --- a/lib/pages/drama_detail/drama_detail_view.dart +++ b/lib/pages/drama_detail/drama_detail_view.dart @@ -11,6 +11,8 @@ import '../../core/data/services/favorite_controller.dart'; import '../../core/data/services/interaction_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_content.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../core/widgets/remote_image.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/download_action.dart'; @@ -62,9 +64,12 @@ class _DramaDetailPageState extends State { @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: logic, - builder: (context, _) => _page(context), + return Theme( + data: AppTheme.dark(), + child: ListenableBuilder( + listenable: logic, + builder: (context, _) => _page(context), + ), ); } @@ -158,7 +163,7 @@ class _DramaDetailPageState extends State { l10n.technicalDemoCount(drama.episodes.length), style: Theme.of(context).textTheme.labelSmall?.copyWith( letterSpacing: 1.4, - color: const Color(0xFF2F7BFF), + color: AppColors.blueSoft, fontWeight: FontWeight.bold, ), ), @@ -248,9 +253,7 @@ class _DramaDetailPageState extends State { child: ListTile( minTileHeight: 76, leading: CircleAvatar( - backgroundColor: const Color( - 0xFF2F7BFF, - ).withValues(alpha: 0.18), + backgroundColor: AppColors.blue.withValues(alpha: 0.18), child: Text('${episode.number}'), ), title: Text(l10n.episodeLabel(episode.number)), @@ -303,10 +306,7 @@ class _DramaDetailPageState extends State { tooltip: l10n.more, onSelected: (_) => unawaited(_reportDrama(l10n)), itemBuilder: (_) => [ - PopupMenuItem( - value: _reportMenuValue, - child: Text(l10n.reportDrama), - ), + PopupMenuItem(value: _reportMenuValue, child: Text(l10n.reportDrama)), ], ); } @@ -378,7 +378,7 @@ class _ResumeBar extends StatelessWidget { value: fraction, minHeight: 3, backgroundColor: Colors.white24, - valueColor: const AlwaysStoppedAnimation(Color(0xFF2F7BFF)), + valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft), ), ), if (label != null) ...[ diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index b89bf83..62b7c54 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -9,6 +9,7 @@ import '../../core/data/models/drama.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/favorite_controller.dart'; import '../../core/formatting/error_text.dart'; +import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/library_drama_tile.dart'; import '../../l10n/app_localizations.dart'; @@ -46,16 +47,19 @@ class _FavoritesPageState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; - return Scaffold( - backgroundColor: const Color(0xFF0B0C10), - appBar: AppBar( + return Theme( + data: AppTheme.dark(), + child: Scaffold( backgroundColor: const Color(0xFF0B0C10), - foregroundColor: Colors.white, - title: Text(l10n.myFavorites), + appBar: AppBar( + backgroundColor: const Color(0xFF0B0C10), + foregroundColor: Colors.white, + title: Text(l10n.myFavorites), + ), + body: signedIn + ? _FavoritesBody(favorites: context.watch()) + : const AuthGatePanel(), ), - body: signedIn - ? _FavoritesBody(favorites: context.watch()) - : const AuthGatePanel(), ); } } diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 22f8582..71e1b57 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -8,8 +8,9 @@ import '../../core/config/app_config.dart'; import '../../core/data/models/drama.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/catalog_controller.dart'; -import '../../core/widgets/auth_gate.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; +import '../../core/widgets/auth_gate.dart'; import '../../l10n/app_localizations.dart'; import '../discover/discover_view.dart'; import '../mine/mine_view.dart'; @@ -26,7 +27,7 @@ class HomePage extends StatelessWidget { final catalog = context.watch(); final l10n = AppLocalizations.of(context)!; return Theme( - data: ThemeData.dark(useMaterial3: true), + data: AppTheme.dark(), child: _buildHome(context, catalog, l10n), ); } @@ -258,7 +259,7 @@ class _TabButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3), decoration: selected ? BoxDecoration( - color: AppColors.blue, + color: AppColors.bluePressed, borderRadius: BorderRadius.circular(14), ) : null, @@ -276,7 +277,7 @@ class _TabButton extends StatelessWidget { style: TextStyle( fontSize: 10, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, - color: selected ? AppColors.blue : AppColors.muted, + color: selected ? AppColors.blueSoft : AppColors.muted, ), ), ], diff --git a/lib/pages/home/widgets/home_quick_drawer.dart b/lib/pages/home/widgets/home_quick_drawer.dart index 1deaa31..a4cd825 100644 --- a/lib/pages/home/widgets/home_quick_drawer.dart +++ b/lib/pages/home/widgets/home_quick_drawer.dart @@ -6,6 +6,7 @@ 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'; @@ -30,7 +31,7 @@ class HomeQuickDrawer extends StatelessWidget { static const _panel = Color(0xFF222225); static const _card = Color(0xFF303033); - static const _orange = Color(0xFF2F7BFF); + static const _accent = AppColors.blueSoft; @override Widget build(BuildContext context) { @@ -128,7 +129,7 @@ class HomeQuickDrawer extends StatelessWidget { key: const Key('drawer-open-series'), onPressed: onOpenSeries, style: FilledButton.styleFrom( - backgroundColor: _orange, + backgroundColor: _accent, foregroundColor: Colors.white, ), icon: const Icon(Icons.play_arrow_rounded), @@ -192,22 +193,21 @@ class HomeQuickDrawer extends StatelessWidget { const SizedBox(height: 14), _SectionCard( title: l10n.appLanguage, - child: Row( + child: Wrap( + spacing: 8, + runSpacing: 8, children: [ for (final (code, label) in [ ('en', l10n.english), ('zh', l10n.simplifiedChinese), - ]) ...[ - Expanded( - child: ChoiceChip( - label: Text(label, maxLines: 1), - selected: languageCode == code, - onSelected: (_) => - context.read().select(code), - ), + ('id', l10n.bahasaIndonesia), + ]) + ChoiceChip( + label: Text(label, maxLines: 1), + selected: languageCode == code, + onSelected: (_) => + context.read().select(code), ), - if (code == 'en') const SizedBox(width: 8), - ], ], ), ), @@ -268,7 +268,7 @@ class _QuickAction extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 7), child: Column( children: [ - Icon(icon, color: HomeQuickDrawer._orange, size: 25), + Icon(icon, color: HomeQuickDrawer._accent, size: 25), const SizedBox(height: 6), Text( label, diff --git a/lib/pages/mine/mine_logic.dart b/lib/pages/mine/mine_logic.dart index 3b93e8b..be6faf8 100644 --- a/lib/pages/mine/mine_logic.dart +++ b/lib/pages/mine/mine_logic.dart @@ -26,6 +26,7 @@ class MineLogic extends ChangeNotifier { List recent = const []; bool recentLoading = false; String? recentError; + /// 续看位置读取失败:与"没有记录"区分开,失败要能重试(不能假装成从 0 开始) bool positionFailed = false; int section; @@ -123,6 +124,7 @@ class MineLogic extends ChangeNotifier { for (final (code, label) in [ ('en', l10n.english), ('zh', l10n.simplifiedChinese), + ('id', l10n.bahasaIndonesia), ]) ListTile( title: Text(label), diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index af0a003..b2d425e 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -14,6 +14,7 @@ import '../../core/data/services/favorite_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/widgets/remote_image.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../l10n/app_localizations.dart'; import 'mine_binding.dart'; import 'mine_logic.dart'; @@ -149,7 +150,7 @@ class _MyPageState extends State { ), ); return Theme( - data: ThemeData.dark(useMaterial3: true), + data: AppTheme.dark(), child: widget.embedded ? content : Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content), @@ -162,7 +163,7 @@ class _MyPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ - const Icon(Icons.play_circle_fill_rounded, color: _orange, size: 27), + const Icon(Icons.play_circle_fill_rounded, color: _accent, size: 27), const SizedBox(width: 7), Text( l10n.myListTab, @@ -407,7 +408,7 @@ class _MyPageState extends State { title: Text(l10n.earnRewards), trailing: Text( l10n.freeBonus, - style: const TextStyle(color: AppColors.blue, fontSize: 12), + style: const TextStyle(color: AppColors.blueSoft, fontSize: 12), ), onTap: () => _mockSnack(l10n), ), @@ -505,7 +506,7 @@ class _MyPageState extends State { ), decoration: BoxDecoration( color: logic.section == index - ? AppColors.blue + ? AppColors.bluePressed : const Color(0xFF24262E), borderRadius: BorderRadius.circular(20), ), @@ -540,7 +541,7 @@ class _MyPageState extends State { showCheckmark: false, visualDensity: VisualDensity.compact, backgroundColor: const Color(0xFF242328), - selectedColor: _orange, + selectedColor: _accent, labelStyle: TextStyle( color: logic.filter == index ? Colors.white @@ -613,7 +614,7 @@ class _MyPageState extends State { backgroundColor: Color(0xFF313137), child: Icon( Icons.play_arrow_rounded, - color: _orange, + color: _accent, size: 20, ), ), @@ -872,12 +873,12 @@ class _NavItem extends StatelessWidget { Icon( icon, size: 21, - color: selected ? _orange : const Color(0xFFCAC4C7), + color: selected ? _accent : const Color(0xFFCAC4C7), ), Text( label, style: TextStyle( - color: selected ? _orange : const Color(0xFFCAC4C7), + color: selected ? _accent : const Color(0xFFCAC4C7), fontSize: 10, fontWeight: FontWeight.w700, ), @@ -893,4 +894,4 @@ String _clock(Duration duration) { return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}'; } -const _orange = AppColors.blue; +const _accent = AppColors.blueSoft; diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index f645bea..31ddc40 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -20,6 +20,7 @@ import '../../core/data/services/playback_queue.dart'; import '../../core/data/services/playback_session.dart'; import '../../core/data/services/video_player_port.dart'; import '../../core/formatting/localized_content.dart'; +import '../../core/theme/app_colors.dart'; import '../../l10n/app_localizations.dart'; import '../discover/discover_logic.dart'; import '../discover/discover_view.dart'; @@ -48,12 +49,15 @@ class PlayerLogic extends ChangeNotifier { Episode? trackedEpisode; Drama? trackedDrama; DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0); + /// 进度上报节流:契约建议每 10 秒一次 Duration progressInterval = const Duration(seconds: 10); Future pendingWrite = Future.value(); + /// 服务端续看点:进入播放页时定位到"那一集的哪一秒" ResumePoint? resumePoint; bool resumeLoaded = false; + /// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放 bool ticketLoading = false; String? ticketError; @@ -86,6 +90,7 @@ class PlayerLogic extends ChangeNotifier { ValueChanged? onDramaChanged; ValueChanged? onImmersiveChanged; bool _disposed = false; + /// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化, /// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。 bool _started = false; @@ -702,7 +707,7 @@ class PlayerLogic extends ChangeNotifier { ) { final selected = episode.number == currentNumber; return Material( - color: selected ? const Color(0xFF2F7BFF) : const Color(0xFF2A2C33), + color: selected ? AppColors.bluePressed : const Color(0xFF2A2C33), borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 75941bf..11e4a93 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -15,6 +15,8 @@ import '../../core/data/services/playback_session.dart'; import '../../core/data/services/video_player_port.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_content.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/download_action.dart'; import '../../l10n/app_localizations.dart'; @@ -130,7 +132,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: Theme( - data: ThemeData.dark(useMaterial3: true), + data: AppTheme.dark(), child: widget.embedded ? content : Scaffold(backgroundColor: Colors.black, body: content), @@ -203,7 +205,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { // 票据三态之一:起播前正在向后端要签名地址 if (selected && logic.ticketLoading) const Center( - child: CircularProgressIndicator(color: Color(0xFF2F7BFF)), + child: CircularProgressIndicator(color: AppColors.blueSoft), ), // 票据三态之二:拿票失败(断网/服务异常)→ 可重试,不假装在加载 if (selected && !logic.ticketLoading && logic.ticketError != null) @@ -235,7 +237,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ), if (selected && logic.session.loading) const Center( - child: CircularProgressIndicator(color: Color(0xFF2F7BFF)), + child: CircularProgressIndicator(color: AppColors.blueSoft), ), if (selected && !ready && logic.session.error != null) Center( @@ -764,7 +766,7 @@ class _RailAction extends StatelessWidget { visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints.tightFor(width: 42, height: 36), - color: selected ? const Color(0xFF8FB4FF) : Colors.white, + color: selected ? AppColors.blueSoft : Colors.white, icon: Icon(icon, size: 24), ), Text( @@ -804,16 +806,14 @@ class _BottomAction extends StatelessWidget { children: [ Icon( icon, - color: selected ? const Color(0xFF2F7BFF) : const Color(0xFFF3EEEA), + color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA), ), const SizedBox(height: 2), Text( label, style: TextStyle( fontSize: 11, - color: selected - ? const Color(0xFF2F7BFF) - : const Color(0xFFF3EEEA), + color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA), ), ), ], diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index 04a2b30..e66b6ff 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; +import '../../../core/theme/app_colors.dart'; + /// Keeps seeks within the playable range so dragging to the edge does not /// trigger the player's completion callback before playback resumes. Duration seekPositionForFraction(double fraction, Duration duration) { @@ -118,7 +120,7 @@ class _SeekProgressBarState extends State { child: SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: scrubbing ? 4 : 3, - activeTrackColor: const Color(0xFF2F7BFF), + activeTrackColor: AppColors.blueSoft, inactiveTrackColor: Colors.white30, thumbColor: Colors.white, thumbShape: RoundSliderThumbShape( diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart index 0476274..be98702 100644 --- a/lib/pages/watch_records/watch_records_page.dart +++ b/lib/pages/watch_records/watch_records_page.dart @@ -12,7 +12,9 @@ import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/watch_records_controller.dart'; import '../../core/formatting/error_text.dart'; +import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/remote_image.dart'; import '../../l10n/app_localizations.dart'; @@ -27,15 +29,18 @@ class WatchRecordsPage extends StatelessWidget { Widget build(BuildContext context) { final signedIn = context.watch().isSignedIn; final l10n = AppLocalizations.of(context)!; - return Scaffold( - key: const Key('watch-records-page'), - backgroundColor: const Color(0xFF0B0C10), - appBar: AppBar( + return Theme( + data: AppTheme.dark(), + child: Scaffold( + key: const Key('watch-records-page'), backgroundColor: const Color(0xFF0B0C10), - foregroundColor: Colors.white, - title: Text(l10n.watchHistory), + appBar: AppBar( + backgroundColor: const Color(0xFF0B0C10), + foregroundColor: Colors.white, + title: Text(l10n.watchHistory), + ), + body: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(), ), - body: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(), ); } } @@ -276,7 +281,10 @@ class _RecordTile extends StatelessWidget { if (record.progress.updatedAt != null) Text( l10n.updatedAtLabel( - _formatUpdatedAt(record.progress.updatedAt!), + formatLocalizedDateTime( + context, + record.progress.updatedAt!, + ), ), key: ValueKey('watch-record-updated-${record.dramaId}'), style: const TextStyle( @@ -375,12 +383,3 @@ String _clock(Duration value) { final seconds = value.inSeconds.remainder(60); return '$minutes:${seconds.toString().padLeft(2, '0')}'; } - -String _formatUpdatedAt(DateTime value) { - final local = value.toLocal(); - final month = local.month.toString().padLeft(2, '0'); - final day = local.day.toString().padLeft(2, '0'); - final hour = local.hour.toString().padLeft(2, '0'); - final minute = local.minute.toString().padLeft(2, '0'); - return '${local.year}-$month-$day $hour:$minute'; -} diff --git a/test/config/locale_controller_test.dart b/test/config/locale_controller_test.dart index cd32c01..077b54c 100644 --- a/test/config/locale_controller_test.dart +++ b/test/config/locale_controller_test.dart @@ -32,18 +32,42 @@ void main() { expect(reopened.locale, const Locale('zh')); }); - test('should_clear_stored_chinese_preference_when_english_is_selected', () async { - await LocaleController().select('zh'); + test('should_restore_indonesian_when_selected_and_app_restarts', () async { + final first = LocaleController(); + await first.select('id'); + expect(first.locale, const Locale('id')); final reopened = LocaleController(); await reopened.load(); - await reopened.select('en'); - - final third = LocaleController(); - await third.load(); - expect(third.locale, const Locale('en')); + expect(reopened.locale, const Locale('id')); }); + test('should_fallback_to_indonesian_when_system_language_is_unsupported', () { + expect(resolveAppLocale(null, const Locale('en')), const Locale('en')); + expect(resolveAppLocale(null, const Locale('id')), const Locale('id')); + expect(resolveAppLocale(null, const Locale('fr')), const Locale('id')); + expect(resolveAppLocale(null, const Locale('in')), const Locale('id')); + expect( + resolveAppLocale(const Locale('zh'), const Locale('fr')), + const Locale('zh'), + ); + }); + + test( + 'should_clear_stored_chinese_preference_when_english_is_selected', + () async { + await LocaleController().select('zh'); + + final reopened = LocaleController(); + await reopened.load(); + await reopened.select('en'); + + final third = LocaleController(); + await third.load(); + expect(third.locale, const Locale('en')); + }, + ); + test('should_clear_locale_when_code_is_unsupported', () async { final controller = LocaleController(); await controller.select('zh'); @@ -54,25 +78,31 @@ void main() { expect(controller.locale, isNull); }); - test('should_resolve_to_device_locale_when_stored_code_is_unsupported', () async { - await SharedPreferencesAsync().setString('ui.locale', 'fr'); + test( + 'should_resolve_to_device_locale_when_stored_code_is_unsupported', + () async { + await SharedPreferencesAsync().setString('ui.locale', 'fr'); - final controller = LocaleController(); - await controller.load(); + final controller = LocaleController(); + await controller.load(); - expect(controller.locale, isNull); - }); + expect(controller.locale, isNull); + }, + ); - test('should_survive_load_and_select_when_preference_store_is_broken', () async { - SharedPreferencesAsyncPlatform.instance = _BrokenPreferences(); - final controller = LocaleController(); + test( + 'should_survive_load_and_select_when_preference_store_is_broken', + () async { + SharedPreferencesAsyncPlatform.instance = _BrokenPreferences(); + final controller = LocaleController(); - await controller.load(); - expect(controller.locale, isNull); + await controller.load(); + expect(controller.locale, isNull); - await controller.select('zh'); - expect(controller.locale, const Locale('zh')); - }); + await controller.select('zh'); + expect(controller.locale, const Locale('zh')); + }, + ); } /// Stands in for a platform whose preference store is unavailable. diff --git a/test/formatting/localized_date_time_test.dart b/test/formatting/localized_date_time_test.dart new file mode 100644 index 0000000..2df78be --- /dev/null +++ b/test/formatting/localized_date_time_test.dart @@ -0,0 +1,41 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/formatting/localized_date_time.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; + +void main() { + testWidgets( + 'should_use_indonesian_material_date_and_time_when_locale_is_id', + (tester) async { + final value = DateTime(2026, 10, 17, 12, 30); + late String formatted; + + await tester.pumpWidget( + MaterialApp( + locale: const Locale('id'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) { + formatted = formatLocalizedDateTime(context, value); + final localizations = MaterialLocalizations.of(context); + final expectedDate = localizations.formatCompactDate( + value.toLocal(), + ); + final expectedTime = localizations.formatTimeOfDay( + TimeOfDay.fromDateTime(value.toLocal()), + alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf( + context, + ), + ); + expect(formatted, '$expectedDate $expectedTime'); + return const SizedBox.shrink(); + }, + ), + ), + ); + }, + ); +} diff --git a/test/mine/mine_logic_test.dart b/test/mine/mine_logic_test.dart index 5f12360..5aed167 100644 --- a/test/mine/mine_logic_test.dart +++ b/test/mine/mine_logic_test.dart @@ -32,35 +32,41 @@ Drama _drama(String id, {List numbers = const [1, 2, 3]}) => Drama( void main() { TestWidgetsFlutterBinding.ensureInitialized(); - test('should_keep_requested_section_and_read_episode_one_when_constructed', () async { - final history = FakeWatchHistory()..saved = const Duration(seconds: 7); - final logic = MineLogic( - history: history, - drama: _drama('signal'), - initialSection: 1, - ); + test( + 'should_keep_requested_section_and_read_episode_one_when_constructed', + () async { + final history = FakeWatchHistory()..saved = const Duration(seconds: 7); + final logic = MineLogic( + history: history, + drama: _drama('signal'), + initialSection: 1, + ); - expect(logic.section, 1); - expect(logic.filter, 0); - expect(await logic.position, const Duration(seconds: 7)); - expect(history.reads, ['signal']); - logic.dispose(); - }); + expect(logic.section, 1); + expect(logic.filter, 0); + expect(await logic.position, const Duration(seconds: 7)); + expect(history.reads, ['signal']); + logic.dispose(); + }, + ); - test('should_resume_from_last_watch_when_list_card_has_no_episodes', () async { - final history = FakeWatchHistory() - ..saved = const Duration(seconds: 7) - ..lastEpisode = 'signal-3'; - final logic = MineLogic( - history: history, - drama: _drama('signal', numbers: const []), - initialSection: 1, - ); + test( + 'should_resume_from_last_watch_when_list_card_has_no_episodes', + () async { + final history = FakeWatchHistory() + ..saved = const Duration(seconds: 7) + ..lastEpisode = 'signal-3'; + final logic = MineLogic( + history: history, + drama: _drama('signal', numbers: const []), + initialSection: 1, + ); - expect(await logic.position, const Duration(seconds: 7)); - expect(history.reads, ['signal']); - logic.dispose(); - }); + expect(await logic.position, const Duration(seconds: 7)); + expect(history.reads, ['signal']); + logic.dispose(); + }, + ); test('should_notify_when_section_and_filter_are_selected', () { final logic = MineLogic( @@ -114,7 +120,12 @@ void main() { expect(logic.drama.id, 'city'); history.reads.clear(); - sync(_drama('vengeance'), active: true, wasActive: true, dramaChanged: true); + sync( + _drama('vengeance'), + active: true, + wasActive: true, + dramaChanged: true, + ); expect(logic.drama.id, 'vengeance'); expect(history.reads, ['vengeance']); @@ -125,26 +136,29 @@ void main() { logic.dispose(); }); - test('should_reset_section_when_section_request_arrives_while_page_is_inactive', () { - final logic = MineLogic( - history: FakeWatchHistory(), - drama: _drama('s'), - initialSection: 0, - ); + test( + 'should_reset_section_when_section_request_arrives_while_page_is_inactive', + () { + final logic = MineLogic( + history: FakeWatchHistory(), + drama: _drama('s'), + initialSection: 0, + ); - logic.syncPage( - active: false, - wasActive: false, - nextDrama: logic.drama, - dramaChanged: false, - sectionRequest: 1, - previousSectionRequest: 0, - initialSection: 1, - ); + logic.syncPage( + active: false, + wasActive: false, + nextDrama: logic.drama, + dramaChanged: false, + sectionRequest: 1, + previousSectionRequest: 0, + initialSection: 1, + ); - expect(logic.section, 1); - logic.dispose(); - }); + expect(logic.section, 1); + logic.dispose(); + }, + ); testWidgets('should_return_choice_and_forward_it_when_act_runs_embedded', ( tester, @@ -197,7 +211,9 @@ void main() { logic.dispose(); }); - testWidgets('should_switch_app_locale_when_language_sheet_is_used', (tester) async { + testWidgets('should_switch_app_locale_when_language_sheet_is_used', ( + tester, + ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final locale = LocaleController(); @@ -231,7 +247,14 @@ void main() { final l10n = AppLocalizations.of(context)!; expect(find.text(l10n.english), findsOneWidget); expect(find.text(l10n.simplifiedChinese), findsOneWidget); + expect(find.text('Bahasa Indonesia'), findsOneWidget); + await tester.tap(find.text('Bahasa Indonesia')); + await tester.pumpAndSettle(); + expect(locale.locale, const Locale('id')); + + unawaited(logic.showLanguage(context)); + await tester.pumpAndSettle(); await tester.tap(find.text(l10n.simplifiedChinese)); await tester.pumpAndSettle(); diff --git a/test/theme/app_theme_test.dart b/test/theme/app_theme_test.dart new file mode 100644 index 0000000..bc3229b --- /dev/null +++ b/test/theme/app_theme_test.dart @@ -0,0 +1,60 @@ +// Author: fruit.com.cn + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/theme/app_colors.dart'; +import 'package:fruitshorts/core/theme/app_theme.dart'; + +double _contrast(Color foreground, Color background) { + final first = foreground.computeLuminance(); + final second = background.computeLuminance(); + return (max(first, second) + 0.05) / (min(first, second) + 0.05); +} + +void main() { + test('should_expose_approved_klein_blue_tokens', () { + expect(AppColors.blue, const Color(0xFF002FA7)); + expect(AppColors.bluePressed, const Color(0xFF00227A)); + expect(AppColors.blueSoft, const Color(0xFF5B7BE0)); + expect(AppColors.onPrimary, const Color(0xFFFFFFFF)); + }); + + test('should_meet_text_and_icon_contrast_when_using_theme_tokens', () { + expect( + _contrast(AppColors.onPrimary, AppColors.blue), + greaterThanOrEqualTo(4.5), + ); + expect(_contrast(AppColors.blue, Colors.white), greaterThanOrEqualTo(4.5)); + expect( + _contrast(AppColors.blueSoft, AppColors.ink), + greaterThanOrEqualTo(3), + ); + }); + + test('should_use_klein_blue_in_light_and_dark_color_schemes', () { + for (final theme in [AppTheme.light(), AppTheme.dark()]) { + expect(theme.colorScheme.primary, AppColors.blue); + expect(theme.colorScheme.onPrimary, AppColors.onPrimary); + } + }); + + testWidgets('should_expose_klein_blue_through_widget_theme', (tester) async { + late ThemeData applied; + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.dark(), + home: Builder( + builder: (context) { + applied = Theme.of(context); + return const SizedBox.shrink(); + }, + ), + ), + ); + + expect(applied.colorScheme.primary, AppColors.blue); + expect(applied.colorScheme.onPrimary, AppColors.onPrimary); + }); +} diff --git a/test/theme/hardcoded_blue_scan_test.dart b/test/theme/hardcoded_blue_scan_test.dart new file mode 100644 index 0000000..7e3ddf8 --- /dev/null +++ b/test/theme/hardcoded_blue_scan_test.dart @@ -0,0 +1,25 @@ +// Author: fruit.com.cn + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('should_use_theme_tokens_when_lib_code_is_scanned_for_legacy_blue', () { + final pattern = RegExp( + r'0xFF2F7BFF|0xff2F7BFF|0xFF8FB4FF|0xff8FB4FF|(?[]; + for (final entity in Directory('lib').listSync(recursive: true)) { + if (entity is! File || !entity.path.endsWith('.dart')) continue; + final lines = entity.readAsLinesSync(); + for (var index = 0; index < lines.length; index++) { + if (pattern.hasMatch(lines[index])) { + violations.add('${entity.path}:${index + 1}: ${lines[index].trim()}'); + } + } + } + + expect(violations, isEmpty, reason: violations.join('\n')); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index e9f6c41..c610e38 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -27,13 +27,12 @@ void main() { /// 而是本地会话(这里直接给 shared_preferences 的内存实现塞一份会话)。 /// 昵称/uid 与演示文案保持一致,避免为显示细节改写既有断言。 void seedSignedInSession() { - SharedPreferencesAsyncPlatform.instance = - InMemorySharedPreferencesAsync.withData({ - 'auth.accessToken': 'test-access', - 'auth.refreshToken': 'test-refresh', - 'auth.user': - '{"uid":"M1CZWVYE","nickname":"Vicky sheng","avatarUrl":""}', - }); + SharedPreferencesAsyncPlatform + .instance = InMemorySharedPreferencesAsync.withData({ + 'auth.accessToken': 'test-access', + 'auth.refreshToken': 'test-refresh', + 'auth.user': '{"uid":"M1CZWVYE","nickname":"Vicky sheng","avatarUrl":""}', + }); } void seedSignedOutSession() { @@ -98,15 +97,15 @@ void main() { }, ); - testWidgets('should_open_series_detail_when_immersive_home_is_tapped', (tester) async { + testWidgets('should_open_series_detail_when_immersive_home_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.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); @@ -131,9 +130,7 @@ void main() { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - await tester.pumpWidget( - appUnderTest(), - ); + await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); @@ -148,60 +145,60 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used', ( - tester, - ) async { - tester.view.physicalSize = const Size(360, 720); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); + testWidgets( + 'should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used', + (tester) async { + tester.view.physicalSize = const Size(360, 720); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); - await tester.pumpWidget( - appUnderTest(), - ); - await tester.pumpAndSettle(); - expect(find.text('Continue Watching'), findsOneWidget); - await tester.binding.handlePopRoute(); - await tester.pumpAndSettle(); - expect( - tester - .widget(find.byType(PlayerPage, skipOffstage: false)) - .active, - isFalse, - ); - expect(find.text('Quick access'), findsOneWidget); - expect( - find.descendant( - of: find.byType(Drawer), - matching: find.text('7 EPISODES'), - ), - findsOneWidget, - ); - expect(tester.takeException(), isNull); + await tester.pumpWidget(appUnderTest()); + await tester.pumpAndSettle(); + expect(find.text('Continue Watching'), findsOneWidget); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect( + tester + .widget(find.byType(PlayerPage, skipOffstage: false)) + .active, + isFalse, + ); + 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(tester.takeException(), isNull); - await tester.tap(find.byKey(const Key('drawer-open-series'))); - await tester.pumpAndSettle(); - expect(find.text('Watch episode 1'), findsOneWidget); - await tester.binding.handlePopRoute(); - await tester.pumpAndSettle(); - expect( - tester - .widget(find.byType(PlayerPage, skipOffstage: false)) - .active, - isFalse, - ); - expect(find.text('Continue Watching'), findsOneWidget); + await tester.tap(find.byKey(const Key('drawer-open-series'))); + await tester.pumpAndSettle(); + expect(find.text('Watch episode 1'), findsOneWidget); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect( + tester + .widget(find.byType(PlayerPage, skipOffstage: false)) + .active, + isFalse, + ); + expect(find.text('Continue Watching'), findsOneWidget); - await tester.binding.handlePopRoute(); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('drawer-discover'))); - await tester.pumpAndSettle(); - expect(find.text('Continue Watching'), findsOneWidget); - expect( - Navigator.of(tester.element(find.text('Continue Watching'))).canPop(), - isFalse, - ); - }); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('drawer-discover'))); + await tester.pumpAndSettle(); + expect(find.text('Continue Watching'), findsOneWidget); + expect( + Navigator.of(tester.element(find.text('Continue Watching'))).canPop(), + isFalse, + ); + }, + ); testWidgets('should_change_interface_language_when_locale_is_picked', ( tester, @@ -211,9 +208,7 @@ void main() { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - await tester.pumpWidget( - appUnderTest(), - ); + await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); expect(find.byTooltip('Captions'), findsNothing); await tester.tap(find.byKey(const Key('tab-my'))); @@ -234,51 +229,74 @@ void main() { expect(find.text('热门'), findsOneWidget); }); - testWidgets('should_fit_compact_android_viewport_when_home_controls_are_shown', (tester) async { - tester.view.physicalSize = const Size(360, 720); + testWidgets( + 'should_fit_compact_android_viewport_when_home_controls_are_shown', + (tester) async { + tester.view.physicalSize = const Size(360, 720); + 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(); + expect(find.text('The Signal'), findsOneWidget); + await tester.drag(find.byType(PageView), const Offset(0, -500)); + await tester.pumpAndSettle(); + expect(find.text('City Lights & Velvet Nights'), findsOneWidget); + await tester.tap(find.byKey(const Key('tab-discover'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('City Lights & Velvet Nights')); + await tester.pumpAndSettle(); + expect(find.byType(Slider), findsOneWidget); + expect(tester.takeException(), isNull); + }, + ); + + testWidgets( + 'should_fit_compact_android_viewport_and_return_home_when_my_page_is_shown', + (tester) async { + tester.view.physicalSize = const Size(360, 720); + 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-my'))); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(find.text('Vicky sheng'), findsOneWidget); + expect(find.text('Coins'), findsOneWidget); + expect(tester.takeException(), isNull); + await tester.tap(find.text('Shorts')); + await tester.pumpAndSettle(); + expect(find.text('The Signal'), findsOneWidget); + }, + ); + + testWidgets('should_fit_compact_dark_viewport_when_klein_theme_is_used', ( + tester, + ) async { + tester.view.physicalSize = const Size(320, 568); tester.view.devicePixelRatio = 1; + tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue); - await tester.pumpWidget( - appUnderTest(), - ); + await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); - await tester.drag(find.byType(PageView), const Offset(0, -500)); - await tester.pumpAndSettle(); - expect(find.text('City Lights & Velvet Nights'), findsOneWidget); - await tester.tap(find.byKey(const Key('tab-discover'))); - await tester.pumpAndSettle(); - await tester.tap(find.text('City Lights & Velvet Nights')); - await tester.pumpAndSettle(); - expect(find.byType(Slider), findsOneWidget); expect(tester.takeException(), isNull); - }); - testWidgets('should_fit_compact_android_viewport_and_return_home_when_my_page_is_shown', ( - tester, - ) async { - tester.view.physicalSize = const Size(360, 720); - 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-my'))); await tester.pumpAndSettle(); - expect(tester.takeException(), isNull); - expect(find.text('Vicky sheng'), findsOneWidget); expect(find.text('Coins'), findsOneWidget); expect(tester.takeException(), isNull); - await tester.tap(find.text('Shorts')); - await tester.pumpAndSettle(); - expect(find.text('The Signal'), findsOneWidget); }); testWidgets( @@ -289,9 +307,7 @@ void main() { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - await tester.pumpWidget( - appUnderTest(), - ); + await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.text('Home')); await tester.pumpAndSettle(); @@ -317,15 +333,15 @@ void main() { }, ); - testWidgets('should_stay_on_one_route_when_bottom_tabs_are_used', (tester) async { + testWidgets('should_stay_on_one_route_when_bottom_tabs_are_used', ( + 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.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.text('Home')); await tester.pumpAndSettle(); @@ -375,9 +391,7 @@ void main() { addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - await tester.pumpWidget( - appUnderTest(), - ); + await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); @@ -400,15 +414,15 @@ void main() { }, ); - testWidgets('should_hide_category_navigation_when_shorts_page_is_shown', (tester) async { + testWidgets('should_hide_category_navigation_when_shorts_page_is_shown', ( + 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.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); @@ -422,74 +436,76 @@ void main() { expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(tester.takeException(), isNull); }); - testWidgets('should_save_comment_like_and_share_in_order_when_right_rail_is_used', ( - tester, - ) async { - tester.view.physicalSize = const Size(390, 844); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - String? sharedText; - final interactions = FakeInteractionRepository(); - final comments = FakeCommentRepository( - pages: [const CommentPage()], - ); - const channel = MethodChannel('com.stormg.lumadrama/share'); - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( - call, - ) async { - sharedText = (call.arguments as Map)['text'] as String; - return null; - }); - addTearDown( - () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - null, - ), - ); + testWidgets( + 'should_save_comment_like_and_share_in_order_when_right_rail_is_used', + (tester) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + String? sharedText; + final interactions = FakeInteractionRepository(); + final comments = FakeCommentRepository(pages: [const CommentPage()]); + const channel = MethodChannel('com.stormg.lumadrama/share'); + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( + call, + ) async { + sharedText = (call.arguments as Map)['text'] as String; + return null; + }); + addTearDown( + () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + null, + ), + ); - await tester.pumpWidget( - appUnderTest( - commentRepository: comments, - interactionRepository: interactions, - ), - ); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('tab-home'))); - await tester.pumpAndSettle(); - final keys = [ - const Key('rail-like'), - const Key('rail-save'), - const Key('rail-comments'), - const Key('rail-share'), - ]; - final tops = keys - .map((key) => tester.getTopLeft(find.byKey(key)).dy) - .toList(); - expect(tops, orderedEquals(tops.toList()..sort())); + await tester.pumpWidget( + appUnderTest( + commentRepository: comments, + interactionRepository: interactions, + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-home'))); + await tester.pumpAndSettle(); + final keys = [ + const Key('rail-like'), + const Key('rail-save'), + const Key('rail-comments'), + const Key('rail-share'), + ]; + final tops = keys + .map((key) => tester.getTopLeft(find.byKey(key)).dy) + .toList(); + expect(tops, orderedEquals(tops.toList()..sort())); - await tester.tap(find.byKey(const Key('rail-save'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('rail-comments'))); - await tester.pumpAndSettle(); - expect(find.text('No comments yet.'), findsOneWidget); - await tester.enterText(find.byKey(const Key('comment-input')), 'Nice demo'); - await tester.tap(find.byKey(const Key('comment-post'))); - await tester.pumpAndSettle(); - expect(find.text('Nice demo'), findsOneWidget); - await tester.binding.handlePopRoute(); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('rail-save'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('rail-comments'))); + await tester.pumpAndSettle(); + expect(find.text('No comments yet.'), findsOneWidget); + await tester.enterText( + find.byKey(const Key('comment-input')), + 'Nice demo', + ); + await tester.tap(find.byKey(const Key('comment-post'))); + await tester.pumpAndSettle(); + expect(find.text('Nice demo'), findsOneWidget); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('rail-like'))); - await tester.pumpAndSettle(); - expect(interactions.likeTargets, contains('DRAMA:the-signal-demo')); - await tester.tap(find.byKey(const Key('rail-share'))); - await tester.pumpAndSettle(); - expect(sharedText, contains('The Signal')); - expect(sharedText, contains('A demo series for vertical playback')); - expect(sharedText, contains('FruitShorts')); - expect(tester.takeException(), isNull); - }); + await tester.tap(find.byKey(const Key('rail-like'))); + await tester.pumpAndSettle(); + expect(interactions.likeTargets, contains('DRAMA:the-signal-demo')); + await tester.tap(find.byKey(const Key('rail-share'))); + await tester.pumpAndSettle(); + expect(sharedText, contains('The Signal')); + expect(sharedText, contains('A demo series for vertical playback')); + expect(sharedText, contains('FruitShorts')); + expect(tester.takeException(), isNull); + }, + ); testWidgets('should_open_auth_gate_without_liking_when_signed_out', ( tester, @@ -501,9 +517,7 @@ void main() { seedSignedOutSession(); final interactions = FakeInteractionRepository(); - await tester.pumpWidget( - appUnderTest(interactionRepository: interactions), - ); + await tester.pumpWidget(appUnderTest(interactionRepository: interactions)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); @@ -523,9 +537,7 @@ void main() { addTearDown(tester.view.resetDevicePixelRatio); final interactions = FakeInteractionRepository(); - await tester.pumpWidget( - appUnderTest(interactionRepository: interactions), - ); + await tester.pumpWidget(appUnderTest(interactionRepository: interactions)); await tester.pumpAndSettle(); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); @@ -542,63 +554,63 @@ void main() { expect(find.text('Report submitted. Thank you.'), findsOneWidget); }); - testWidgets('should_open_drawer_and_stay_on_home_when_system_back_is_pressed', (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(); + testWidgets( + 'should_open_drawer_and_stay_on_home_when_system_back_is_pressed', + (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(); - expect(find.text('Continue Watching'), findsOneWidget); - await tester.binding.handlePopRoute(); - await tester.pumpAndSettle(); - expect(find.text('Quick access'), findsOneWidget); - expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget); - await tester.binding.handlePopRoute(); - await tester.pumpAndSettle(); - expect(find.text('Quick access'), findsNothing); - expect(find.text('Continue Watching'), findsOneWidget); - expect( - Navigator.of( - tester.element(find.byKey(const Key('home-tabs-bar'))), - ).canPop(), - isFalse, - ); - }); + expect(find.text('Continue Watching'), findsOneWidget); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect(find.text('Quick access'), findsOneWidget); + expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget); + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect(find.text('Quick access'), findsNothing); + expect(find.text('Continue Watching'), findsOneWidget); + expect( + Navigator.of( + tester.element(find.byKey(const Key('home-tabs-bar'))), + ).canPop(), + isFalse, + ); + }, + ); - testWidgets('should_list_saved_dramas_across_home_feed_when_watchlist_is_opened', ( - tester, - ) async { - tester.view.physicalSize = const Size(390, 844); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - // 收藏要求登录态:沿用本文件已有的"假登录"初始化 - seedSignedInSession(); - await tester.pumpWidget( - appUnderTest(), - ); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('tab-home'))); - await tester.pumpAndSettle(); + testWidgets( + 'should_list_saved_dramas_across_home_feed_when_watchlist_is_opened', + (tester) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + // 收藏要求登录态:沿用本文件已有的"假登录"初始化 + seedSignedInSession(); + 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('rail-save'))); - await tester.pumpAndSettle(); - await tester.drag(find.byType(PageView), const Offset(0, -550)); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('rail-save'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('tab-my-list'))); - await tester.pumpAndSettle(); - await tester.tap(find.text('Watchlist')); - await tester.pumpAndSettle(); - expect(find.text('The Signal'), findsOneWidget); - expect(find.text('City Lights & Velvet Nights'), findsWidgets); - await tester.tap(find.text('The Signal')); - await tester.pumpAndSettle(); - expect(find.text('Watch episode 1'), findsOneWidget); - }); + await tester.tap(find.byKey(const Key('rail-save'))); + await tester.pumpAndSettle(); + await tester.drag(find.byType(PageView), const Offset(0, -550)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('rail-save'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-my-list'))); + await tester.pumpAndSettle(); + await tester.tap(find.text('Watchlist')); + await tester.pumpAndSettle(); + expect(find.text('The Signal'), findsOneWidget); + expect(find.text('City Lights & Velvet Nights'), findsWidgets); + await tester.tap(find.text('The Signal')); + await tester.pumpAndSettle(); + expect(find.text('Watch episode 1'), findsOneWidget); + }, + ); }