feat(flutter): 应用克莱因蓝主题与印尼语适配
This commit is contained in:
1 parent
e1147cfcba
commit
10d448c8ba
31 files changed
+891
-521
No files matched your search
+5
-23
@@ -33,6 +33,7 @@ import 'core/data/services/favorite_controller.dart';
|
|||||||
import 'core/data/services/interaction_controller.dart';
|
import 'core/data/services/interaction_controller.dart';
|
||||||
import 'core/data/services/playback_session.dart';
|
import 'core/data/services/playback_session.dart';
|
||||||
import 'core/data/services/video_player_port.dart';
|
import 'core/data/services/video_player_port.dart';
|
||||||
|
import 'core/theme/app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'pages/home/home_view.dart';
|
import 'pages/home/home_view.dart';
|
||||||
|
|
||||||
@@ -193,31 +194,12 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
locale: locale.locale,
|
locale: locale.locale,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
localeResolutionCallback: (deviceLocale, supported) {
|
localeResolutionCallback: (deviceLocale, _) =>
|
||||||
final resolved = locale.locale ?? deviceLocale;
|
resolveAppLocale(locale.locale, deviceLocale),
|
||||||
if (resolved == null) return supported.first;
|
|
||||||
for (final item in supported) {
|
|
||||||
if (item.languageCode == resolved.languageCode) return item;
|
|
||||||
}
|
|
||||||
return supported.first;
|
|
||||||
},
|
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
theme: ThemeData(
|
theme: AppTheme.light(),
|
||||||
useMaterial3: true,
|
darkTheme: AppTheme.dark(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
home: const HomePage(),
|
home: const HomePage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,19 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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 {
|
class LocaleController extends ChangeNotifier {
|
||||||
LocaleController({SharedPreferencesAsync? preferences})
|
LocaleController({SharedPreferencesAsync? preferences})
|
||||||
: _preferences = preferences ?? SharedPreferencesAsync();
|
: _preferences = preferences ?? SharedPreferencesAsync();
|
||||||
@@ -17,6 +30,7 @@ class LocaleController extends ChangeNotifier {
|
|||||||
_locale = switch (code) {
|
_locale = switch (code) {
|
||||||
'en' => const Locale('en'),
|
'en' => const Locale('en'),
|
||||||
'zh' => const Locale('zh'),
|
'zh' => const Locale('zh'),
|
||||||
|
'id' => const Locale('id'),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -29,6 +43,7 @@ class LocaleController extends ChangeNotifier {
|
|||||||
_locale = switch (code) {
|
_locale = switch (code) {
|
||||||
'en' => const Locale('en'),
|
'en' => const Locale('en'),
|
||||||
'zh' => const Locale('zh'),
|
'zh' => const Locale('zh'),
|
||||||
|
'id' => const Locale('id'),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -2,8 +2,17 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// Shared colors. Blue is the primary action color.
|
/// Shared colors. Blue is the primary action color.
|
||||||
abstract final class AppColors {
|
abstract final class AppColors {
|
||||||
static const blue = Color(0xFF2F7BFF);
|
/// Klein Blue primary used by actions and selected states.
|
||||||
static const blueSoft = Color(0xFF8FB4FF);
|
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 ink = Color(0xFF0B0C10);
|
||||||
static const bar = Color(0xFF101114);
|
static const bar = Color(0xFF101114);
|
||||||
static const card = Color(0xFF17191F);
|
static const card = Color(0xFF17191F);
|
||||||
|
|||||||
@@ -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<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed) ||
|
||||||
|
states.contains(WidgetState.hovered) ||
|
||||||
|
states.contains(WidgetState.focused)) {
|
||||||
|
return AppColors.bluePressed;
|
||||||
|
}
|
||||||
|
return AppColors.blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -200,6 +200,7 @@
|
|||||||
"systemLanguage": "System",
|
"systemLanguage": "System",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"simplifiedChinese": "简体中文",
|
"simplifiedChinese": "简体中文",
|
||||||
|
"bahasaIndonesia": "Bahasa Indonesia",
|
||||||
"episodeOneTitle": "First Light",
|
"episodeOneTitle": "First Light",
|
||||||
"episodeTwoTitle": "A New Frequency",
|
"episodeTwoTitle": "A New Frequency",
|
||||||
"episodeThreeTitle": "Crossing Over",
|
"episodeThreeTitle": "Crossing Over",
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
"systemLanguage": "Sistem",
|
"systemLanguage": "Sistem",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"simplifiedChinese": "简体中文",
|
"simplifiedChinese": "简体中文",
|
||||||
|
"bahasaIndonesia": "Bahasa Indonesia",
|
||||||
"episodeOneTitle": "Cahaya Pertama",
|
"episodeOneTitle": "Cahaya Pertama",
|
||||||
"episodeTwoTitle": "Frekuensi Baru",
|
"episodeTwoTitle": "Frekuensi Baru",
|
||||||
"episodeThreeTitle": "Menyeberang",
|
"episodeThreeTitle": "Menyeberang",
|
||||||
|
|||||||
@@ -916,6 +916,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'简体中文'**
|
/// **'简体中文'**
|
||||||
String get simplifiedChinese;
|
String get simplifiedChinese;
|
||||||
|
|
||||||
|
/// No description provided for @bahasaIndonesia.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Bahasa Indonesia'**
|
||||||
|
String get bahasaIndonesia;
|
||||||
|
|
||||||
/// No description provided for @episodeOneTitle.
|
/// No description provided for @episodeOneTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -440,6 +440,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get simplifiedChinese => '简体中文';
|
String get simplifiedChinese => '简体中文';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bahasaIndonesia => 'Bahasa Indonesia';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get episodeOneTitle => 'First Light';
|
String get episodeOneTitle => 'First Light';
|
||||||
|
|
||||||
|
|||||||
@@ -442,6 +442,9 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get simplifiedChinese => '简体中文';
|
String get simplifiedChinese => '简体中文';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bahasaIndonesia => 'Bahasa Indonesia';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get episodeOneTitle => 'Cahaya Pertama';
|
String get episodeOneTitle => 'Cahaya Pertama';
|
||||||
|
|
||||||
|
|||||||
@@ -434,6 +434,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get simplifiedChinese => '简体中文';
|
String get simplifiedChinese => '简体中文';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bahasaIndonesia => '印尼语';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get episodeOneTitle => '初见微光';
|
String get episodeOneTitle => '初见微光';
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@
|
|||||||
"systemLanguage": "跟随系统",
|
"systemLanguage": "跟随系统",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"simplifiedChinese": "简体中文",
|
"simplifiedChinese": "简体中文",
|
||||||
|
"bahasaIndonesia": "印尼语",
|
||||||
"episodeOneTitle": "初见微光",
|
"episodeOneTitle": "初见微光",
|
||||||
"episodeTwoTitle": "新的频率",
|
"episodeTwoTitle": "新的频率",
|
||||||
"episodeThreeTitle": "越过边界",
|
"episodeThreeTitle": "越过边界",
|
||||||
|
|||||||
+109
-90
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../core/data/remote/api_client.dart';
|
import '../../core/data/remote/api_client.dart';
|
||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import 'auth_logic.dart';
|
import 'auth_logic.dart';
|
||||||
|
|
||||||
@@ -82,102 +83,117 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Scaffold(
|
return Theme(
|
||||||
backgroundColor: AppColors.ink,
|
data: AppTheme.dark(),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
backgroundColor: AppColors.ink,
|
backgroundColor: AppColors.ink,
|
||||||
elevation: 0,
|
appBar: AppBar(
|
||||||
foregroundColor: Colors.white,
|
backgroundColor: AppColors.ink,
|
||||||
title: Text(_logic.signUpMode ? l10n.signUp : l10n.signIn),
|
elevation: 0,
|
||||||
),
|
foregroundColor: Colors.white,
|
||||||
body: SafeArea(
|
title: Text(_logic.signUpMode ? l10n.signUp : l10n.signIn),
|
||||||
child: ListView(
|
),
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
body: SafeArea(
|
||||||
children: [
|
child: ListView(
|
||||||
Text(
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
||||||
_logic.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
|
children: [
|
||||||
style: const TextStyle(
|
Text(
|
||||||
color: Colors.white,
|
_logic.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
|
||||||
fontSize: 22,
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w800,
|
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onPressed: _logic.busy ? null : _submit,
|
),
|
||||||
child: _logic.busy
|
const SizedBox(height: 6),
|
||||||
? const SizedBox(
|
Text(
|
||||||
width: 18,
|
l10n.authSubtitle,
|
||||||
height: 18,
|
style: const TextStyle(color: AppColors.muted, fontSize: 13),
|
||||||
child: CircularProgressIndicator(
|
),
|
||||||
strokeWidth: 2,
|
const SizedBox(height: 22),
|
||||||
color: Colors.white,
|
_field(
|
||||||
),
|
label: l10n.authEmailLabel,
|
||||||
)
|
hint: 'name@example.com',
|
||||||
: Text(
|
keyboardType: TextInputType.emailAddress,
|
||||||
_logic.signUpMode ? l10n.signUp : l10n.signIn,
|
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(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
color: Color(0xFFFF6B6B),
|
||||||
fontWeight: FontWeight.w700,
|
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),
|
||||||
const SizedBox(height: 14),
|
TextButton(
|
||||||
TextButton(
|
onPressed: _logic.busy
|
||||||
onPressed: _logic.busy
|
? null
|
||||||
? null
|
: () => setState(() => _logic.toggleMode()),
|
||||||
: () => setState(() => _logic.toggleMode()),
|
child: Text(
|
||||||
child: Text(
|
_logic.signUpMode
|
||||||
_logic.signUpMode ? l10n.authSwitchToSignIn : l10n.authSwitchToSignUp,
|
? l10n.authSwitchToSignIn
|
||||||
style: const TextStyle(color: AppColors.blueSoft, fontSize: 13),
|
: l10n.authSwitchToSignUp,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.blueSoft,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -214,7 +230,10 @@ class _AuthPageState extends State<AuthPage> {
|
|||||||
hintStyle: const TextStyle(color: Color(0xFF6C7280), fontSize: 14),
|
hintStyle: const TextStyle(color: Color(0xFF6C7280), fontSize: 14),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.card,
|
fillColor: AppColors.card,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../core/data/models/home_feed.dart';
|
|||||||
import '../../core/data/repositories/watch_history_repository.dart';
|
import '../../core/data/repositories/watch_history_repository.dart';
|
||||||
import '../../core/data/services/category_controller.dart';
|
import '../../core/data/services/category_controller.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../core/widgets/remote_image.dart';
|
import '../../core/widgets/remote_image.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import 'discover_binding.dart';
|
import 'discover_binding.dart';
|
||||||
@@ -139,7 +140,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
|||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle.light,
|
value: SystemUiOverlayStyle.light,
|
||||||
child: Theme(
|
child: Theme(
|
||||||
data: ThemeData.dark(useMaterial3: true),
|
data: AppTheme.dark(),
|
||||||
child: widget.embedded
|
child: widget.embedded
|
||||||
? content
|
? content
|
||||||
: Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
|
: Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
|
||||||
@@ -254,7 +255,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
|||||||
Text(
|
Text(
|
||||||
l10n.seeAll,
|
l10n.seeAll,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.blue,
|
color: AppColors.blueSoft,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -417,7 +418,9 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selected ? AppColors.blue : const Color(0xFF1C1E24),
|
color: selected
|
||||||
|
? AppColors.bluePressed
|
||||||
|
: const Color(0xFF1C1E24),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -501,12 +504,12 @@ class _DiscoverNavItem extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 21,
|
size: 21,
|
||||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
@@ -549,7 +552,7 @@ List<_ShowcaseCard> _cardsFor(List<Drama> dramas, String query) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const _orange = AppColors.blue;
|
const _accent = AppColors.blueSoft;
|
||||||
const _coin = Color(0xFFF5C451);
|
const _coin = Color(0xFFF5C451);
|
||||||
|
|
||||||
class _BrandMark extends StatelessWidget {
|
class _BrandMark extends StatelessWidget {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import '../../core/data/models/download_policy.dart';
|
|||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/data/services/download_controller.dart';
|
import '../../core/data/services/download_controller.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
|
import '../../core/formatting/localized_date_time.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../core/widgets/auth_gate.dart';
|
import '../../core/widgets/auth_gate.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import 'downloads_binding.dart';
|
import 'downloads_binding.dart';
|
||||||
@@ -46,17 +48,20 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||||
return Scaffold(
|
return Theme(
|
||||||
key: const Key('downloads-page'),
|
data: AppTheme.dark(),
|
||||||
backgroundColor: const Color(0xFF0B0C10),
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
key: const Key('downloads-page'),
|
||||||
backgroundColor: const Color(0xFF0B0C10),
|
backgroundColor: const Color(0xFF0B0C10),
|
||||||
foregroundColor: Colors.white,
|
appBar: AppBar(
|
||||||
title: Text(l10n.downloads),
|
backgroundColor: const Color(0xFF0B0C10),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
title: Text(l10n.downloads),
|
||||||
|
),
|
||||||
|
body: signedIn
|
||||||
|
? _DownloadsBody(downloads: context.watch<DownloadController>())
|
||||||
|
: const AuthGatePanel(),
|
||||||
),
|
),
|
||||||
body: signedIn
|
|
||||||
? _DownloadsBody(downloads: context.watch<DownloadController>())
|
|
||||||
: const AuthGatePanel(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +385,9 @@ class _LicenseTile extends StatelessWidget {
|
|||||||
if (license.expireAt != null) ...[
|
if (license.expireAt != null) ...[
|
||||||
const SizedBox(height: 7),
|
const SizedBox(height: 7),
|
||||||
Text(
|
Text(
|
||||||
l10n.downloadExpiresAt(_formatTime(license.expireAt!)),
|
l10n.downloadExpiresAt(
|
||||||
|
formatLocalizedDateTime(context, license.expireAt!),
|
||||||
|
),
|
||||||
style: const TextStyle(color: Color(0xFF8D8990), fontSize: 11),
|
style: const TextStyle(color: Color(0xFF8D8990), fontSize: 11),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -398,12 +405,3 @@ List<String> _qualityChoices(String current) {
|
|||||||
}
|
}
|
||||||
return choices;
|
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';
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,8 @@ import '../../core/data/services/favorite_controller.dart';
|
|||||||
import '../../core/data/services/interaction_controller.dart';
|
import '../../core/data/services/interaction_controller.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
import '../../core/formatting/localized_content.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/remote_image.dart';
|
||||||
import '../../core/widgets/auth_gate.dart';
|
import '../../core/widgets/auth_gate.dart';
|
||||||
import '../../core/widgets/download_action.dart';
|
import '../../core/widgets/download_action.dart';
|
||||||
@@ -62,9 +64,12 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListenableBuilder(
|
return Theme(
|
||||||
listenable: logic,
|
data: AppTheme.dark(),
|
||||||
builder: (context, _) => _page(context),
|
child: ListenableBuilder(
|
||||||
|
listenable: logic,
|
||||||
|
builder: (context, _) => _page(context),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +163,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
|||||||
l10n.technicalDemoCount(drama.episodes.length),
|
l10n.technicalDemoCount(drama.episodes.length),
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
letterSpacing: 1.4,
|
letterSpacing: 1.4,
|
||||||
color: const Color(0xFF2F7BFF),
|
color: AppColors.blueSoft,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -248,9 +253,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
minTileHeight: 76,
|
minTileHeight: 76,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: const Color(
|
backgroundColor: AppColors.blue.withValues(alpha: 0.18),
|
||||||
0xFF2F7BFF,
|
|
||||||
).withValues(alpha: 0.18),
|
|
||||||
child: Text('${episode.number}'),
|
child: Text('${episode.number}'),
|
||||||
),
|
),
|
||||||
title: Text(l10n.episodeLabel(episode.number)),
|
title: Text(l10n.episodeLabel(episode.number)),
|
||||||
@@ -303,10 +306,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
|||||||
tooltip: l10n.more,
|
tooltip: l10n.more,
|
||||||
onSelected: (_) => unawaited(_reportDrama(l10n)),
|
onSelected: (_) => unawaited(_reportDrama(l10n)),
|
||||||
itemBuilder: (_) => [
|
itemBuilder: (_) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(value: _reportMenuValue, child: Text(l10n.reportDrama)),
|
||||||
value: _reportMenuValue,
|
|
||||||
child: Text(l10n.reportDrama),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -378,7 +378,7 @@ class _ResumeBar extends StatelessWidget {
|
|||||||
value: fraction,
|
value: fraction,
|
||||||
minHeight: 3,
|
minHeight: 3,
|
||||||
backgroundColor: Colors.white24,
|
backgroundColor: Colors.white24,
|
||||||
valueColor: const AlwaysStoppedAnimation(Color(0xFF2F7BFF)),
|
valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (label != null) ...[
|
if (label != null) ...[
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../core/data/models/drama.dart';
|
|||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/data/services/favorite_controller.dart';
|
import '../../core/data/services/favorite_controller.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../core/widgets/auth_gate.dart';
|
import '../../core/widgets/auth_gate.dart';
|
||||||
import '../../core/widgets/library_drama_tile.dart';
|
import '../../core/widgets/library_drama_tile.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
@@ -46,16 +47,19 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||||
return Scaffold(
|
return Theme(
|
||||||
backgroundColor: const Color(0xFF0B0C10),
|
data: AppTheme.dark(),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
backgroundColor: const Color(0xFF0B0C10),
|
backgroundColor: const Color(0xFF0B0C10),
|
||||||
foregroundColor: Colors.white,
|
appBar: AppBar(
|
||||||
title: Text(l10n.myFavorites),
|
backgroundColor: const Color(0xFF0B0C10),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
title: Text(l10n.myFavorites),
|
||||||
|
),
|
||||||
|
body: signedIn
|
||||||
|
? _FavoritesBody(favorites: context.watch<FavoriteController>())
|
||||||
|
: const AuthGatePanel(),
|
||||||
),
|
),
|
||||||
body: signedIn
|
|
||||||
? _FavoritesBody(favorites: context.watch<FavoriteController>())
|
|
||||||
: const AuthGatePanel(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import '../../core/config/app_config.dart';
|
|||||||
import '../../core/data/models/drama.dart';
|
import '../../core/data/models/drama.dart';
|
||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/data/services/catalog_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_colors.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
|
import '../../core/widgets/auth_gate.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../discover/discover_view.dart';
|
import '../discover/discover_view.dart';
|
||||||
import '../mine/mine_view.dart';
|
import '../mine/mine_view.dart';
|
||||||
@@ -26,7 +27,7 @@ class HomePage extends StatelessWidget {
|
|||||||
final catalog = context.watch<CatalogController>();
|
final catalog = context.watch<CatalogController>();
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Theme(
|
return Theme(
|
||||||
data: ThemeData.dark(useMaterial3: true),
|
data: AppTheme.dark(),
|
||||||
child: _buildHome(context, catalog, l10n),
|
child: _buildHome(context, catalog, l10n),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,7 +259,7 @@ class _TabButton extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3),
|
||||||
decoration: selected
|
decoration: selected
|
||||||
? BoxDecoration(
|
? BoxDecoration(
|
||||||
color: AppColors.blue,
|
color: AppColors.bluePressed,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -276,7 +277,7 @@ class _TabButton extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||||
color: selected ? AppColors.blue : AppColors.muted,
|
color: selected ? AppColors.blueSoft : AppColors.muted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../../core/config/locale_controller.dart';
|
import '../../../core/config/locale_controller.dart';
|
||||||
import '../../../core/data/models/drama.dart';
|
import '../../../core/data/models/drama.dart';
|
||||||
import '../../../core/formatting/localized_content.dart';
|
import '../../../core/formatting/localized_content.dart';
|
||||||
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/widgets/remote_image.dart';
|
import '../../../core/widgets/remote_image.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class HomeQuickDrawer extends StatelessWidget {
|
|||||||
|
|
||||||
static const _panel = Color(0xFF222225);
|
static const _panel = Color(0xFF222225);
|
||||||
static const _card = Color(0xFF303033);
|
static const _card = Color(0xFF303033);
|
||||||
static const _orange = Color(0xFF2F7BFF);
|
static const _accent = AppColors.blueSoft;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -128,7 +129,7 @@ class HomeQuickDrawer extends StatelessWidget {
|
|||||||
key: const Key('drawer-open-series'),
|
key: const Key('drawer-open-series'),
|
||||||
onPressed: onOpenSeries,
|
onPressed: onOpenSeries,
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: _orange,
|
backgroundColor: _accent,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.play_arrow_rounded),
|
icon: const Icon(Icons.play_arrow_rounded),
|
||||||
@@ -192,22 +193,21 @@ class HomeQuickDrawer extends StatelessWidget {
|
|||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_SectionCard(
|
_SectionCard(
|
||||||
title: l10n.appLanguage,
|
title: l10n.appLanguage,
|
||||||
child: Row(
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final (code, label) in [
|
for (final (code, label) in [
|
||||||
('en', l10n.english),
|
('en', l10n.english),
|
||||||
('zh', l10n.simplifiedChinese),
|
('zh', l10n.simplifiedChinese),
|
||||||
]) ...[
|
('id', l10n.bahasaIndonesia),
|
||||||
Expanded(
|
])
|
||||||
child: ChoiceChip(
|
ChoiceChip(
|
||||||
label: Text(label, maxLines: 1),
|
label: Text(label, maxLines: 1),
|
||||||
selected: languageCode == code,
|
selected: languageCode == code,
|
||||||
onSelected: (_) =>
|
onSelected: (_) =>
|
||||||
context.read<LocaleController>().select(code),
|
context.read<LocaleController>().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),
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 7),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: HomeQuickDrawer._orange, size: 25),
|
Icon(icon, color: HomeQuickDrawer._accent, size: 25),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class MineLogic extends ChangeNotifier {
|
|||||||
List<WatchProgress> recent = const [];
|
List<WatchProgress> recent = const [];
|
||||||
bool recentLoading = false;
|
bool recentLoading = false;
|
||||||
String? recentError;
|
String? recentError;
|
||||||
|
|
||||||
/// 续看位置读取失败:与"没有记录"区分开,失败要能重试(不能假装成从 0 开始)
|
/// 续看位置读取失败:与"没有记录"区分开,失败要能重试(不能假装成从 0 开始)
|
||||||
bool positionFailed = false;
|
bool positionFailed = false;
|
||||||
int section;
|
int section;
|
||||||
@@ -123,6 +124,7 @@ class MineLogic extends ChangeNotifier {
|
|||||||
for (final (code, label) in [
|
for (final (code, label) in [
|
||||||
('en', l10n.english),
|
('en', l10n.english),
|
||||||
('zh', l10n.simplifiedChinese),
|
('zh', l10n.simplifiedChinese),
|
||||||
|
('id', l10n.bahasaIndonesia),
|
||||||
])
|
])
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(label),
|
title: Text(label),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import '../../core/data/services/favorite_controller.dart';
|
|||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
import '../../core/widgets/remote_image.dart';
|
import '../../core/widgets/remote_image.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import 'mine_binding.dart';
|
import 'mine_binding.dart';
|
||||||
import 'mine_logic.dart';
|
import 'mine_logic.dart';
|
||||||
@@ -149,7 +150,7 @@ class _MyPageState extends State<MyPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Theme(
|
return Theme(
|
||||||
data: ThemeData.dark(useMaterial3: true),
|
data: AppTheme.dark(),
|
||||||
child: widget.embedded
|
child: widget.embedded
|
||||||
? content
|
? content
|
||||||
: Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
|
: Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
|
||||||
@@ -162,7 +163,7 @@ class _MyPageState extends State<MyPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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),
|
const SizedBox(width: 7),
|
||||||
Text(
|
Text(
|
||||||
l10n.myListTab,
|
l10n.myListTab,
|
||||||
@@ -407,7 +408,7 @@ class _MyPageState extends State<MyPage> {
|
|||||||
title: Text(l10n.earnRewards),
|
title: Text(l10n.earnRewards),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
l10n.freeBonus,
|
l10n.freeBonus,
|
||||||
style: const TextStyle(color: AppColors.blue, fontSize: 12),
|
style: const TextStyle(color: AppColors.blueSoft, fontSize: 12),
|
||||||
),
|
),
|
||||||
onTap: () => _mockSnack(l10n),
|
onTap: () => _mockSnack(l10n),
|
||||||
),
|
),
|
||||||
@@ -505,7 +506,7 @@ class _MyPageState extends State<MyPage> {
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: logic.section == index
|
color: logic.section == index
|
||||||
? AppColors.blue
|
? AppColors.bluePressed
|
||||||
: const Color(0xFF24262E),
|
: const Color(0xFF24262E),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
@@ -540,7 +541,7 @@ class _MyPageState extends State<MyPage> {
|
|||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
backgroundColor: const Color(0xFF242328),
|
backgroundColor: const Color(0xFF242328),
|
||||||
selectedColor: _orange,
|
selectedColor: _accent,
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: logic.filter == index
|
color: logic.filter == index
|
||||||
? Colors.white
|
? Colors.white
|
||||||
@@ -613,7 +614,7 @@ class _MyPageState extends State<MyPage> {
|
|||||||
backgroundColor: Color(0xFF313137),
|
backgroundColor: Color(0xFF313137),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.play_arrow_rounded,
|
Icons.play_arrow_rounded,
|
||||||
color: _orange,
|
color: _accent,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -872,12 +873,12 @@ class _NavItem extends StatelessWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 21,
|
size: 21,
|
||||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
@@ -893,4 +894,4 @@ String _clock(Duration duration) {
|
|||||||
return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}';
|
return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
const _orange = AppColors.blue;
|
const _accent = AppColors.blueSoft;
|
||||||
@@ -20,6 +20,7 @@ import '../../core/data/services/playback_queue.dart';
|
|||||||
import '../../core/data/services/playback_session.dart';
|
import '../../core/data/services/playback_session.dart';
|
||||||
import '../../core/data/services/video_player_port.dart';
|
import '../../core/data/services/video_player_port.dart';
|
||||||
import '../../core/formatting/localized_content.dart';
|
import '../../core/formatting/localized_content.dart';
|
||||||
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../discover/discover_logic.dart';
|
import '../discover/discover_logic.dart';
|
||||||
import '../discover/discover_view.dart';
|
import '../discover/discover_view.dart';
|
||||||
@@ -48,12 +49,15 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
Episode? trackedEpisode;
|
Episode? trackedEpisode;
|
||||||
Drama? trackedDrama;
|
Drama? trackedDrama;
|
||||||
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
|
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
|
||||||
/// 进度上报节流:契约建议每 10 秒一次
|
/// 进度上报节流:契约建议每 10 秒一次
|
||||||
Duration progressInterval = const Duration(seconds: 10);
|
Duration progressInterval = const Duration(seconds: 10);
|
||||||
Future<void> pendingWrite = Future<void>.value();
|
Future<void> pendingWrite = Future<void>.value();
|
||||||
|
|
||||||
/// 服务端续看点:进入播放页时定位到"那一集的哪一秒"
|
/// 服务端续看点:进入播放页时定位到"那一集的哪一秒"
|
||||||
ResumePoint? resumePoint;
|
ResumePoint? resumePoint;
|
||||||
bool resumeLoaded = false;
|
bool resumeLoaded = false;
|
||||||
|
|
||||||
/// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放
|
/// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放
|
||||||
bool ticketLoading = false;
|
bool ticketLoading = false;
|
||||||
String? ticketError;
|
String? ticketError;
|
||||||
@@ -86,6 +90,7 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
ValueChanged<Drama>? onDramaChanged;
|
ValueChanged<Drama>? onDramaChanged;
|
||||||
ValueChanged<bool>? onImmersiveChanged;
|
ValueChanged<bool>? onImmersiveChanged;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
/// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化,
|
/// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化,
|
||||||
/// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。
|
/// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。
|
||||||
bool _started = false;
|
bool _started = false;
|
||||||
@@ -702,7 +707,7 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
) {
|
) {
|
||||||
final selected = episode.number == currentNumber;
|
final selected = episode.number == currentNumber;
|
||||||
return Material(
|
return Material(
|
||||||
color: selected ? const Color(0xFF2F7BFF) : const Color(0xFF2A2C33),
|
color: selected ? AppColors.bluePressed : const Color(0xFF2A2C33),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import '../../core/data/services/playback_session.dart';
|
|||||||
import '../../core/data/services/video_player_port.dart';
|
import '../../core/data/services/video_player_port.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
import '../../core/formatting/localized_content.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/auth_gate.dart';
|
||||||
import '../../core/widgets/download_action.dart';
|
import '../../core/widgets/download_action.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
@@ -130,7 +132,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle.light,
|
value: SystemUiOverlayStyle.light,
|
||||||
child: Theme(
|
child: Theme(
|
||||||
data: ThemeData.dark(useMaterial3: true),
|
data: AppTheme.dark(),
|
||||||
child: widget.embedded
|
child: widget.embedded
|
||||||
? content
|
? content
|
||||||
: Scaffold(backgroundColor: Colors.black, body: content),
|
: Scaffold(backgroundColor: Colors.black, body: content),
|
||||||
@@ -203,7 +205,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
// 票据三态之一:起播前正在向后端要签名地址
|
// 票据三态之一:起播前正在向后端要签名地址
|
||||||
if (selected && logic.ticketLoading)
|
if (selected && logic.ticketLoading)
|
||||||
const Center(
|
const Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF2F7BFF)),
|
child: CircularProgressIndicator(color: AppColors.blueSoft),
|
||||||
),
|
),
|
||||||
// 票据三态之二:拿票失败(断网/服务异常)→ 可重试,不假装在加载
|
// 票据三态之二:拿票失败(断网/服务异常)→ 可重试,不假装在加载
|
||||||
if (selected && !logic.ticketLoading && logic.ticketError != null)
|
if (selected && !logic.ticketLoading && logic.ticketError != null)
|
||||||
@@ -235,7 +237,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
if (selected && logic.session.loading)
|
if (selected && logic.session.loading)
|
||||||
const Center(
|
const Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF2F7BFF)),
|
child: CircularProgressIndicator(color: AppColors.blueSoft),
|
||||||
),
|
),
|
||||||
if (selected && !ready && logic.session.error != null)
|
if (selected && !ready && logic.session.error != null)
|
||||||
Center(
|
Center(
|
||||||
@@ -764,7 +766,7 @@ class _RailAction extends StatelessWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints.tightFor(width: 42, height: 36),
|
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),
|
icon: Icon(icon, size: 24),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -804,16 +806,14 @@ class _BottomAction extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
color: selected ? const Color(0xFF2F7BFF) : const Color(0xFFF3EEEA),
|
color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: selected
|
color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA),
|
||||||
? const Color(0xFF2F7BFF)
|
|
||||||
: const Color(0xFFF3EEEA),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.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
|
/// Keeps seeks within the playable range so dragging to the edge does not
|
||||||
/// trigger the player's completion callback before playback resumes.
|
/// trigger the player's completion callback before playback resumes.
|
||||||
Duration seekPositionForFraction(double fraction, Duration duration) {
|
Duration seekPositionForFraction(double fraction, Duration duration) {
|
||||||
@@ -118,7 +120,7 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
|||||||
child: SliderTheme(
|
child: SliderTheme(
|
||||||
data: SliderTheme.of(context).copyWith(
|
data: SliderTheme.of(context).copyWith(
|
||||||
trackHeight: scrubbing ? 4 : 3,
|
trackHeight: scrubbing ? 4 : 3,
|
||||||
activeTrackColor: const Color(0xFF2F7BFF),
|
activeTrackColor: AppColors.blueSoft,
|
||||||
inactiveTrackColor: Colors.white30,
|
inactiveTrackColor: Colors.white30,
|
||||||
thumbColor: Colors.white,
|
thumbColor: Colors.white,
|
||||||
thumbShape: RoundSliderThumbShape(
|
thumbShape: RoundSliderThumbShape(
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import '../../core/data/repositories/watch_history_repository.dart';
|
|||||||
import '../../core/data/services/auth_controller.dart';
|
import '../../core/data/services/auth_controller.dart';
|
||||||
import '../../core/data/services/watch_records_controller.dart';
|
import '../../core/data/services/watch_records_controller.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
|
import '../../core/formatting/localized_date_time.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../core/widgets/auth_gate.dart';
|
import '../../core/widgets/auth_gate.dart';
|
||||||
import '../../core/widgets/remote_image.dart';
|
import '../../core/widgets/remote_image.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
@@ -27,15 +29,18 @@ class WatchRecordsPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
return Scaffold(
|
return Theme(
|
||||||
key: const Key('watch-records-page'),
|
data: AppTheme.dark(),
|
||||||
backgroundColor: const Color(0xFF0B0C10),
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
key: const Key('watch-records-page'),
|
||||||
backgroundColor: const Color(0xFF0B0C10),
|
backgroundColor: const Color(0xFF0B0C10),
|
||||||
foregroundColor: Colors.white,
|
appBar: AppBar(
|
||||||
title: Text(l10n.watchHistory),
|
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)
|
if (record.progress.updatedAt != null)
|
||||||
Text(
|
Text(
|
||||||
l10n.updatedAtLabel(
|
l10n.updatedAtLabel(
|
||||||
_formatUpdatedAt(record.progress.updatedAt!),
|
formatLocalizedDateTime(
|
||||||
|
context,
|
||||||
|
record.progress.updatedAt!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
key: ValueKey('watch-record-updated-${record.dramaId}'),
|
key: ValueKey('watch-record-updated-${record.dramaId}'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -375,12 +383,3 @@ String _clock(Duration value) {
|
|||||||
final seconds = value.inSeconds.remainder(60);
|
final seconds = value.inSeconds.remainder(60);
|
||||||
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
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';
|
|
||||||
}
|
|
||||||
@@ -32,18 +32,42 @@ void main() {
|
|||||||
expect(reopened.locale, const Locale('zh'));
|
expect(reopened.locale, const Locale('zh'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should_clear_stored_chinese_preference_when_english_is_selected', () async {
|
test('should_restore_indonesian_when_selected_and_app_restarts', () async {
|
||||||
await LocaleController().select('zh');
|
final first = LocaleController();
|
||||||
|
await first.select('id');
|
||||||
|
expect(first.locale, const Locale('id'));
|
||||||
|
|
||||||
final reopened = LocaleController();
|
final reopened = LocaleController();
|
||||||
await reopened.load();
|
await reopened.load();
|
||||||
await reopened.select('en');
|
expect(reopened.locale, const Locale('id'));
|
||||||
|
|
||||||
final third = LocaleController();
|
|
||||||
await third.load();
|
|
||||||
expect(third.locale, const Locale('en'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
test('should_clear_locale_when_code_is_unsupported', () async {
|
||||||
final controller = LocaleController();
|
final controller = LocaleController();
|
||||||
await controller.select('zh');
|
await controller.select('zh');
|
||||||
@@ -54,25 +78,31 @@ void main() {
|
|||||||
expect(controller.locale, isNull);
|
expect(controller.locale, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should_resolve_to_device_locale_when_stored_code_is_unsupported', () async {
|
test(
|
||||||
await SharedPreferencesAsync().setString('ui.locale', 'fr');
|
'should_resolve_to_device_locale_when_stored_code_is_unsupported',
|
||||||
|
() async {
|
||||||
|
await SharedPreferencesAsync().setString('ui.locale', 'fr');
|
||||||
|
|
||||||
final controller = LocaleController();
|
final controller = LocaleController();
|
||||||
await controller.load();
|
await controller.load();
|
||||||
|
|
||||||
expect(controller.locale, isNull);
|
expect(controller.locale, isNull);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should_survive_load_and_select_when_preference_store_is_broken', () async {
|
test(
|
||||||
SharedPreferencesAsyncPlatform.instance = _BrokenPreferences();
|
'should_survive_load_and_select_when_preference_store_is_broken',
|
||||||
final controller = LocaleController();
|
() async {
|
||||||
|
SharedPreferencesAsyncPlatform.instance = _BrokenPreferences();
|
||||||
|
final controller = LocaleController();
|
||||||
|
|
||||||
await controller.load();
|
await controller.load();
|
||||||
expect(controller.locale, isNull);
|
expect(controller.locale, isNull);
|
||||||
|
|
||||||
await controller.select('zh');
|
await controller.select('zh');
|
||||||
expect(controller.locale, const Locale('zh'));
|
expect(controller.locale, const Locale('zh'));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stands in for a platform whose preference store is unavailable.
|
/// Stands in for a platform whose preference store is unavailable.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,35 +32,41 @@ Drama _drama(String id, {List<int> numbers = const [1, 2, 3]}) => Drama(
|
|||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
test('should_keep_requested_section_and_read_episode_one_when_constructed', () async {
|
test(
|
||||||
final history = FakeWatchHistory()..saved = const Duration(seconds: 7);
|
'should_keep_requested_section_and_read_episode_one_when_constructed',
|
||||||
final logic = MineLogic(
|
() async {
|
||||||
history: history,
|
final history = FakeWatchHistory()..saved = const Duration(seconds: 7);
|
||||||
drama: _drama('signal'),
|
final logic = MineLogic(
|
||||||
initialSection: 1,
|
history: history,
|
||||||
);
|
drama: _drama('signal'),
|
||||||
|
initialSection: 1,
|
||||||
|
);
|
||||||
|
|
||||||
expect(logic.section, 1);
|
expect(logic.section, 1);
|
||||||
expect(logic.filter, 0);
|
expect(logic.filter, 0);
|
||||||
expect(await logic.position, const Duration(seconds: 7));
|
expect(await logic.position, const Duration(seconds: 7));
|
||||||
expect(history.reads, ['signal']);
|
expect(history.reads, ['signal']);
|
||||||
logic.dispose();
|
logic.dispose();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should_resume_from_last_watch_when_list_card_has_no_episodes', () async {
|
test(
|
||||||
final history = FakeWatchHistory()
|
'should_resume_from_last_watch_when_list_card_has_no_episodes',
|
||||||
..saved = const Duration(seconds: 7)
|
() async {
|
||||||
..lastEpisode = 'signal-3';
|
final history = FakeWatchHistory()
|
||||||
final logic = MineLogic(
|
..saved = const Duration(seconds: 7)
|
||||||
history: history,
|
..lastEpisode = 'signal-3';
|
||||||
drama: _drama('signal', numbers: const []),
|
final logic = MineLogic(
|
||||||
initialSection: 1,
|
history: history,
|
||||||
);
|
drama: _drama('signal', numbers: const []),
|
||||||
|
initialSection: 1,
|
||||||
|
);
|
||||||
|
|
||||||
expect(await logic.position, const Duration(seconds: 7));
|
expect(await logic.position, const Duration(seconds: 7));
|
||||||
expect(history.reads, ['signal']);
|
expect(history.reads, ['signal']);
|
||||||
logic.dispose();
|
logic.dispose();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('should_notify_when_section_and_filter_are_selected', () {
|
test('should_notify_when_section_and_filter_are_selected', () {
|
||||||
final logic = MineLogic(
|
final logic = MineLogic(
|
||||||
@@ -114,7 +120,12 @@ void main() {
|
|||||||
expect(logic.drama.id, 'city');
|
expect(logic.drama.id, 'city');
|
||||||
|
|
||||||
history.reads.clear();
|
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(logic.drama.id, 'vengeance');
|
||||||
expect(history.reads, ['vengeance']);
|
expect(history.reads, ['vengeance']);
|
||||||
|
|
||||||
@@ -125,26 +136,29 @@ void main() {
|
|||||||
logic.dispose();
|
logic.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should_reset_section_when_section_request_arrives_while_page_is_inactive', () {
|
test(
|
||||||
final logic = MineLogic(
|
'should_reset_section_when_section_request_arrives_while_page_is_inactive',
|
||||||
history: FakeWatchHistory(),
|
() {
|
||||||
drama: _drama('s'),
|
final logic = MineLogic(
|
||||||
initialSection: 0,
|
history: FakeWatchHistory(),
|
||||||
);
|
drama: _drama('s'),
|
||||||
|
initialSection: 0,
|
||||||
|
);
|
||||||
|
|
||||||
logic.syncPage(
|
logic.syncPage(
|
||||||
active: false,
|
active: false,
|
||||||
wasActive: false,
|
wasActive: false,
|
||||||
nextDrama: logic.drama,
|
nextDrama: logic.drama,
|
||||||
dramaChanged: false,
|
dramaChanged: false,
|
||||||
sectionRequest: 1,
|
sectionRequest: 1,
|
||||||
previousSectionRequest: 0,
|
previousSectionRequest: 0,
|
||||||
initialSection: 1,
|
initialSection: 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(logic.section, 1);
|
expect(logic.section, 1);
|
||||||
logic.dispose();
|
logic.dispose();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('should_return_choice_and_forward_it_when_act_runs_embedded', (
|
testWidgets('should_return_choice_and_forward_it_when_act_runs_embedded', (
|
||||||
tester,
|
tester,
|
||||||
@@ -197,7 +211,9 @@ void main() {
|
|||||||
logic.dispose();
|
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 =
|
SharedPreferencesAsyncPlatform.instance =
|
||||||
InMemorySharedPreferencesAsync.empty();
|
InMemorySharedPreferencesAsync.empty();
|
||||||
final locale = LocaleController();
|
final locale = LocaleController();
|
||||||
@@ -231,7 +247,14 @@ void main() {
|
|||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
expect(find.text(l10n.english), findsOneWidget);
|
expect(find.text(l10n.english), findsOneWidget);
|
||||||
expect(find.text(l10n.simplifiedChinese), 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.tap(find.text(l10n.simplifiedChinese));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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|(?<!App)Colors\.blue\b',
|
||||||
|
);
|
||||||
|
final violations = <String>[];
|
||||||
|
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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
+252
-240
@@ -27,13 +27,12 @@ void main() {
|
|||||||
/// 而是本地会话(这里直接给 shared_preferences 的内存实现塞一份会话)。
|
/// 而是本地会话(这里直接给 shared_preferences 的内存实现塞一份会话)。
|
||||||
/// 昵称/uid 与演示文案保持一致,避免为显示细节改写既有断言。
|
/// 昵称/uid 与演示文案保持一致,避免为显示细节改写既有断言。
|
||||||
void seedSignedInSession() {
|
void seedSignedInSession() {
|
||||||
SharedPreferencesAsyncPlatform.instance =
|
SharedPreferencesAsyncPlatform
|
||||||
InMemorySharedPreferencesAsync.withData({
|
.instance = InMemorySharedPreferencesAsync.withData({
|
||||||
'auth.accessToken': 'test-access',
|
'auth.accessToken': 'test-access',
|
||||||
'auth.refreshToken': 'test-refresh',
|
'auth.refreshToken': 'test-refresh',
|
||||||
'auth.user':
|
'auth.user': '{"uid":"M1CZWVYE","nickname":"Vicky sheng","avatarUrl":""}',
|
||||||
'{"uid":"M1CZWVYE","nickname":"Vicky sheng","avatarUrl":""}',
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void seedSignedOutSession() {
|
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.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -131,9 +130,7 @@ void main() {
|
|||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -148,60 +145,60 @@ void main() {
|
|||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used', (
|
testWidgets(
|
||||||
tester,
|
'should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used',
|
||||||
) async {
|
(tester) async {
|
||||||
tester.view.physicalSize = const Size(360, 720);
|
tester.view.physicalSize = const Size(360, 720);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
await tester.pumpAndSettle();
|
||||||
);
|
expect(find.text('Continue Watching'), findsOneWidget);
|
||||||
await tester.pumpAndSettle();
|
await tester.binding.handlePopRoute();
|
||||||
expect(find.text('Continue Watching'), findsOneWidget);
|
await tester.pumpAndSettle();
|
||||||
await tester.binding.handlePopRoute();
|
expect(
|
||||||
await tester.pumpAndSettle();
|
tester
|
||||||
expect(
|
.widget<PlayerPage>(find.byType(PlayerPage, skipOffstage: false))
|
||||||
tester
|
.active,
|
||||||
.widget<PlayerPage>(find.byType(PlayerPage, skipOffstage: false))
|
isFalse,
|
||||||
.active,
|
);
|
||||||
isFalse,
|
expect(find.text('Quick access'), findsOneWidget);
|
||||||
);
|
expect(find.text('Bahasa Indonesia'), findsOneWidget);
|
||||||
expect(find.text('Quick access'), findsOneWidget);
|
expect(
|
||||||
expect(
|
find.descendant(
|
||||||
find.descendant(
|
of: find.byType(Drawer),
|
||||||
of: find.byType(Drawer),
|
matching: find.text('7 EPISODES'),
|
||||||
matching: find.text('7 EPISODES'),
|
),
|
||||||
),
|
findsOneWidget,
|
||||||
findsOneWidget,
|
);
|
||||||
);
|
expect(tester.takeException(), isNull);
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('drawer-open-series')));
|
await tester.tap(find.byKey(const Key('drawer-open-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Watch episode 1'), findsOneWidget);
|
expect(find.text('Watch episode 1'), findsOneWidget);
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<PlayerPage>(find.byType(PlayerPage, skipOffstage: false))
|
.widget<PlayerPage>(find.byType(PlayerPage, skipOffstage: false))
|
||||||
.active,
|
.active,
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
expect(find.text('Continue Watching'), findsOneWidget);
|
expect(find.text('Continue Watching'), findsOneWidget);
|
||||||
|
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('drawer-discover')));
|
await tester.tap(find.byKey(const Key('drawer-discover')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Continue Watching'), findsOneWidget);
|
expect(find.text('Continue Watching'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
Navigator.of(tester.element(find.text('Continue Watching'))).canPop(),
|
Navigator.of(tester.element(find.text('Continue Watching'))).canPop(),
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('should_change_interface_language_when_locale_is_picked', (
|
testWidgets('should_change_interface_language_when_locale_is_picked', (
|
||||||
tester,
|
tester,
|
||||||
@@ -211,9 +208,7 @@ void main() {
|
|||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byTooltip('Captions'), findsNothing);
|
expect(find.byTooltip('Captions'), findsNothing);
|
||||||
await tester.tap(find.byKey(const Key('tab-my')));
|
await tester.tap(find.byKey(const Key('tab-my')));
|
||||||
@@ -234,51 +229,74 @@ void main() {
|
|||||||
expect(find.text('热门'), findsOneWidget);
|
expect(find.text('热门'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_fit_compact_android_viewport_when_home_controls_are_shown', (tester) async {
|
testWidgets(
|
||||||
tester.view.physicalSize = const Size(360, 720);
|
'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.view.devicePixelRatio = 1;
|
||||||
|
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('The Signal'), findsOneWidget);
|
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);
|
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.tap(find.byKey(const Key('tab-my')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
expect(find.text('Vicky sheng'), findsOneWidget);
|
|
||||||
expect(find.text('Coins'), findsOneWidget);
|
expect(find.text('Coins'), findsOneWidget);
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
await tester.tap(find.text('Shorts'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('The Signal'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
@@ -289,9 +307,7 @@ void main() {
|
|||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Home'));
|
await tester.tap(find.text('Home'));
|
||||||
await tester.pumpAndSettle();
|
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.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Home'));
|
await tester.tap(find.text('Home'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -375,9 +391,7 @@ void main() {
|
|||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
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.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -422,74 +436,76 @@ void main() {
|
|||||||
expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
|
expect(find.text('City Lights & Velvet Nights'), findsOneWidget);
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
testWidgets('should_save_comment_like_and_share_in_order_when_right_rail_is_used', (
|
testWidgets(
|
||||||
tester,
|
'should_save_comment_like_and_share_in_order_when_right_rail_is_used',
|
||||||
) async {
|
(tester) async {
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
String? sharedText;
|
String? sharedText;
|
||||||
final interactions = FakeInteractionRepository();
|
final interactions = FakeInteractionRepository();
|
||||||
final comments = FakeCommentRepository(
|
final comments = FakeCommentRepository(pages: [const CommentPage()]);
|
||||||
pages: [const CommentPage()],
|
const channel = MethodChannel('com.stormg.lumadrama/share');
|
||||||
);
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
||||||
const channel = MethodChannel('com.stormg.lumadrama/share');
|
call,
|
||||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (
|
) async {
|
||||||
call,
|
sharedText = (call.arguments as Map)['text'] as String;
|
||||||
) async {
|
return null;
|
||||||
sharedText = (call.arguments as Map)['text'] as String;
|
});
|
||||||
return null;
|
addTearDown(
|
||||||
});
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
addTearDown(
|
channel,
|
||||||
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
null,
|
||||||
channel,
|
),
|
||||||
null,
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
appUnderTest(
|
appUnderTest(
|
||||||
commentRepository: comments,
|
commentRepository: comments,
|
||||||
interactionRepository: interactions,
|
interactionRepository: interactions,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
final keys = [
|
final keys = [
|
||||||
const Key('rail-like'),
|
const Key('rail-like'),
|
||||||
const Key('rail-save'),
|
const Key('rail-save'),
|
||||||
const Key('rail-comments'),
|
const Key('rail-comments'),
|
||||||
const Key('rail-share'),
|
const Key('rail-share'),
|
||||||
];
|
];
|
||||||
final tops = keys
|
final tops = keys
|
||||||
.map((key) => tester.getTopLeft(find.byKey(key)).dy)
|
.map((key) => tester.getTopLeft(find.byKey(key)).dy)
|
||||||
.toList();
|
.toList();
|
||||||
expect(tops, orderedEquals(tops.toList()..sort()));
|
expect(tops, orderedEquals(tops.toList()..sort()));
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('rail-save')));
|
await tester.tap(find.byKey(const Key('rail-save')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('rail-comments')));
|
await tester.tap(find.byKey(const Key('rail-comments')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('No comments yet.'), findsOneWidget);
|
expect(find.text('No comments yet.'), findsOneWidget);
|
||||||
await tester.enterText(find.byKey(const Key('comment-input')), 'Nice demo');
|
await tester.enterText(
|
||||||
await tester.tap(find.byKey(const Key('comment-post')));
|
find.byKey(const Key('comment-input')),
|
||||||
await tester.pumpAndSettle();
|
'Nice demo',
|
||||||
expect(find.text('Nice demo'), findsOneWidget);
|
);
|
||||||
await tester.binding.handlePopRoute();
|
await tester.tap(find.byKey(const Key('comment-post')));
|
||||||
await tester.pumpAndSettle();
|
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.tap(find.byKey(const Key('rail-like')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(interactions.likeTargets, contains('DRAMA:the-signal-demo'));
|
expect(interactions.likeTargets, contains('DRAMA:the-signal-demo'));
|
||||||
await tester.tap(find.byKey(const Key('rail-share')));
|
await tester.tap(find.byKey(const Key('rail-share')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(sharedText, contains('The Signal'));
|
expect(sharedText, contains('The Signal'));
|
||||||
expect(sharedText, contains('A demo series for vertical playback'));
|
expect(sharedText, contains('A demo series for vertical playback'));
|
||||||
expect(sharedText, contains('FruitShorts'));
|
expect(sharedText, contains('FruitShorts'));
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('should_open_auth_gate_without_liking_when_signed_out', (
|
testWidgets('should_open_auth_gate_without_liking_when_signed_out', (
|
||||||
tester,
|
tester,
|
||||||
@@ -501,9 +517,7 @@ void main() {
|
|||||||
seedSignedOutSession();
|
seedSignedOutSession();
|
||||||
final interactions = FakeInteractionRepository();
|
final interactions = FakeInteractionRepository();
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest(interactionRepository: interactions));
|
||||||
appUnderTest(interactionRepository: interactions),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-home')));
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -523,9 +537,7 @@ void main() {
|
|||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
final interactions = FakeInteractionRepository();
|
final interactions = FakeInteractionRepository();
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest(interactionRepository: interactions));
|
||||||
appUnderTest(interactionRepository: interactions),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -542,63 +554,63 @@ void main() {
|
|||||||
expect(find.text('Report submitted. Thank you.'), findsOneWidget);
|
expect(find.text('Report submitted. Thank you.'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_open_drawer_and_stay_on_home_when_system_back_is_pressed', (tester) async {
|
testWidgets(
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
'should_open_drawer_and_stay_on_home_when_system_back_is_pressed',
|
||||||
tester.view.devicePixelRatio = 1;
|
(tester) async {
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
tester.view.devicePixelRatio = 1;
|
||||||
await tester.pumpWidget(
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
appUnderTest(),
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
);
|
await tester.pumpWidget(appUnderTest());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Continue Watching'), findsOneWidget);
|
expect(find.text('Continue Watching'), findsOneWidget);
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Quick access'), findsOneWidget);
|
expect(find.text('Quick access'), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Quick access'), findsNothing);
|
expect(find.text('Quick access'), findsNothing);
|
||||||
expect(find.text('Continue Watching'), findsOneWidget);
|
expect(find.text('Continue Watching'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
tester.element(find.byKey(const Key('home-tabs-bar'))),
|
tester.element(find.byKey(const Key('home-tabs-bar'))),
|
||||||
).canPop(),
|
).canPop(),
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('should_list_saved_dramas_across_home_feed_when_watchlist_is_opened', (
|
testWidgets(
|
||||||
tester,
|
'should_list_saved_dramas_across_home_feed_when_watchlist_is_opened',
|
||||||
) async {
|
(tester) async {
|
||||||
tester.view.physicalSize = const Size(390, 844);
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
// 收藏要求登录态:沿用本文件已有的"假登录"初始化
|
// 收藏要求登录态:沿用本文件已有的"假登录"初始化
|
||||||
seedSignedInSession();
|
seedSignedInSession();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(appUnderTest());
|
||||||
appUnderTest(),
|
await tester.pumpAndSettle();
|
||||||
);
|
await tester.tap(find.byKey(const Key('tab-home')));
|
||||||
await tester.pumpAndSettle();
|
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.tap(find.byKey(const Key('rail-save')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.drag(find.byType(PageView), const Offset(0, -550));
|
await tester.drag(find.byType(PageView), const Offset(0, -550));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('rail-save')));
|
await tester.tap(find.byKey(const Key('rail-save')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('tab-my-list')));
|
await tester.tap(find.byKey(const Key('tab-my-list')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text('Watchlist'));
|
await tester.tap(find.text('Watchlist'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('The Signal'), findsOneWidget);
|
expect(find.text('The Signal'), findsOneWidget);
|
||||||
expect(find.text('City Lights & Velvet Nights'), findsWidgets);
|
expect(find.text('City Lights & Velvet Nights'), findsWidgets);
|
||||||
await tester.tap(find.text('The Signal'));
|
await tester.tap(find.text('The Signal'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Watch episode 1'), findsOneWidget);
|
expect(find.text('Watch episode 1'), findsOneWidget);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user