diff --git a/.gitignore b/.gitignore
index 5450a36..92b30ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,7 +45,10 @@ app.*.map.json
/android/app/release
# Local demo media and internal planning are absent from the public repo.
-/assets/
+# Launcher/splash source icons are required at build time, so keep them tracked.
+/assets/*
+!/assets/icon/
+!/assets/icon/*.png
/server/
/docs/
/AGENTS.md
diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png
index 2f9eca7..ed4d185 100644
Binary files a/android/app/src/main/res/drawable-hdpi/android12splash.png and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
index a775df4..a88dd38 100644
Binary files a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/splash_icon.png b/android/app/src/main/res/drawable-hdpi/splash_icon.png
index 1b55cc7..f236785 100644
Binary files a/android/app/src/main/res/drawable-hdpi/splash_icon.png and b/android/app/src/main/res/drawable-hdpi/splash_icon.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png
index d55419e..251a44b 100644
Binary files a/android/app/src/main/res/drawable-mdpi/android12splash.png and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
index 689d998..5254b8b 100644
Binary files a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/splash_icon.png b/android/app/src/main/res/drawable-mdpi/splash_icon.png
index 34804ef..99fb840 100644
Binary files a/android/app/src/main/res/drawable-mdpi/splash_icon.png and b/android/app/src/main/res/drawable-mdpi/splash_icon.png differ
diff --git a/android/app/src/main/res/drawable-v31/launch_background.xml b/android/app/src/main/res/drawable-v31/launch_background.xml
new file mode 100644
index 0000000..7c342d1
--- /dev/null
+++ b/android/app/src/main/res/drawable-v31/launch_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png
index 49cf10d..9794446 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/android12splash.png and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
index 7d8eb37..83b9072 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splash_icon.png b/android/app/src/main/res/drawable-xhdpi/splash_icon.png
index af935a6..2828004 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/splash_icon.png and b/android/app/src/main/res/drawable-xhdpi/splash_icon.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png
index 09b431e..c3418c0 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/android12splash.png and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
index c1c47be..871b143 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splash_icon.png b/android/app/src/main/res/drawable-xxhdpi/splash_icon.png
index 78ec1ce..eb60578 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/splash_icon.png and b/android/app/src/main/res/drawable-xxhdpi/splash_icon.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png
index 391fa1a..ac5df74 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
index 4583ed4..98f3b9b 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png b/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png
index 8fde7d4..ab8ca49 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png and b/android/app/src/main/res/drawable-xxxhdpi/splash_icon.png differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index c74b6f9..c79c58a 100644
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,4 @@
-
-
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 4216099..0d3f7bc 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 1807efe..ed7e106 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 7b131e3..d751115 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 4835758..bf85ac7 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 24c7b41..7dd208f 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
index 02d990f..d360f53 100644
--- a/android/app/src/main/res/values-night/styles.xml
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -15,6 +15,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 6a8e712..3462ee2 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -2,6 +2,6 @@
- #FF000000
- #D74A7D
+ #FF100C0A
+ #C4322A
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 3052bbb..5d7602e 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -15,6 +15,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
diff --git a/assets/icon/fruitshorts.png b/assets/icon/fruitshorts.png
new file mode 100644
index 0000000..bed17d0
Binary files /dev/null and b/assets/icon/fruitshorts.png differ
diff --git a/assets/icon/splash_mark.png b/assets/icon/splash_mark.png
new file mode 100644
index 0000000..d6f5dc1
Binary files /dev/null and b/assets/icon/splash_mark.png differ
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 0147af1..946f662 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -10,7 +10,7 @@ final class RunnerFlutterViewController: FlutterViewController {
override func viewDidLoad() {
super.viewDidLoad()
- view.backgroundColor = .black
+ view.backgroundColor = UIColor(red: 16.0 / 255.0, green: 12.0 / 255.0, blue: 10.0 / 255.0, alpha: 1)
}
}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index 4395a4f..3858b43 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 0c177eb..16d0383 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index cd7eb5e..457ec61 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6280968..5822c54 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 9e3a54a..9404627 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index 0bda915..329c73a 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 41f2351..3749006 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index cd7eb5e..457ec61 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 23865da..3072448 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 54ea594..cce1cf8 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
index e408931..d441803 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
index b35d3d8..31ceca6 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
index 7d5894e..9a88887 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
index 72ce271..4d4596f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 54ea594..cce1cf8 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index db3cbb9..7ff6212 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
index 4216099..618814d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
index 4835758..1612bc9 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 3d76f45..a9ce647 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 4e8ed1f..7430443 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 43a5317..d407932 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
index 1244d69..99fb840 100644
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
index 073ff0a..2828004 100644
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
index 20d201f..eb60578 100644
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
index 5f55b1c..d963036 100644
--- a/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -15,12 +15,10 @@
-
+
-
-
-
-
+
+
@@ -30,6 +28,6 @@
-
+
diff --git a/lib/core/data/services/catalog_controller.dart b/lib/core/data/services/catalog_controller.dart
index db5daca..34cccdb 100644
--- a/lib/core/data/services/catalog_controller.dart
+++ b/lib/core/data/services/catalog_controller.dart
@@ -18,6 +18,9 @@ class CatalogController extends ChangeNotifier {
HomeFeed feed = const HomeFeed();
bool loading = false;
+
+ /// 第一次首页请求结束后为 true。之后的重试不再回到开屏。
+ bool settled = false;
String? error;
List get dramas => feed.dramas;
@@ -36,6 +39,7 @@ class CatalogController extends ChangeNotifier {
error = 'catalogError';
} finally {
loading = false;
+ settled = true;
notifyListeners();
}
}
diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart
index bd9aab7..468c587 100644
--- a/lib/core/theme/app_colors.dart
+++ b/lib/core/theme/app_colors.dart
@@ -1,24 +1,49 @@
+// Author: fruit.com.cn
+
import 'package:flutter/material.dart';
-/// Shared colors. Blue is the primary action color.
+/// Evening palette for the Indonesia launch.
+///
+/// Chili is the action color. Gold marks coins, rewards, and playback
+/// progress. Cocoa surfaces keep the app in night-market light instead of
+/// a cold utility screen.
abstract final class AppColors {
- /// Klein Blue primary used by actions and selected states.
- static const blue = Color(0xFF002FA7);
+ /// Chili fill for buttons, selected chips, and brand marks.
+ static const blue = Color(0xFFC4322A);
- /// Pressed/active shade for blue surfaces.
- static const bluePressed = Color(0xFF00227A);
+ /// Pressed chili.
+ static const bluePressed = Color(0xFF8E221C);
- /// High-contrast blue for text and icons on dark surfaces.
- static const blueSoft = Color(0xFF5B7BE0);
+ /// Coral for text and icons sitting on dark surfaces.
+ static const blueSoft = Color(0xFFFF8A72);
+
+ /// Coins, rewards, and the playback progress line.
+ static const gold = Color(0xFFF6C453);
+
+ /// Text placed on [gold].
+ static const onGold = Color(0xFF2A1810);
/// Text and icons placed directly on [blue].
static const onPrimary = Color(0xFFFFFFFF);
- static const ink = Color(0xFF0B0C10);
- static const bar = Color(0xFF101114);
- static const card = Color(0xFF17191F);
- static const muted = Color(0xFFB7BDC9);
- static const outlineDark = Color(0xFF3A3C45);
- static const outlineVariantDark = Color(0xFF242329);
- static const outlineLight = Color(0xFF8A8E99);
- static const outlineVariantLight = Color(0xFFD6D9E2);
+
+ /// Page background.
+ static const ink = Color(0xFF100C0A);
+
+ /// Bottom bar.
+ static const bar = Color(0xFF1A1412);
+
+ /// Cards lifted off [ink].
+ static const card = Color(0xFF241C19);
+
+ /// Chips and nested rows.
+ static const raised = Color(0xFF322824);
+
+ /// Primary text on dark surfaces.
+ static const cream = Color(0xFFF8F1EA);
+
+ static const muted = Color(0xFFB7A59C);
+ static const outlineDark = Color(0xFF4A3C36);
+ static const outlineVariantDark = Color(0xFF2A221F);
+ static const outlineLight = Color(0xFF8C7A72);
+ static const outlineVariantLight = Color(0xFFE4D5CC);
}
diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart
index b49620b..f017d21 100644
--- a/lib/core/theme/app_theme.dart
+++ b/lib/core/theme/app_theme.dart
@@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
import 'app_colors.dart';
-/// App-wide light and dark themes built from the approved Klein Blue palette.
+/// Light paper and warm-cocoa themes for the Indonesia evening palette.
abstract final class AppTheme {
- static const _lightBackground = Color(0xFFF8F7FC);
- static const _darkBackground = Color(0xFF101019);
+ static const _lightBackground = Color(0xFFFFF6F1);
+ static const _darkBackground = AppColors.ink;
/// Light theme for system light mode.
static ThemeData light() => _build(
@@ -40,7 +40,7 @@ abstract final class AppTheme {
secondaryContainer: AppColors.bluePressed,
onSecondaryContainer: AppColors.onPrimary,
surface: _darkBackground,
- onSurface: Colors.white,
+ onSurface: AppColors.cream,
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
surfaceTint: AppColors.blueSoft,
diff --git a/lib/core/widgets/empty_state.dart b/lib/core/widgets/empty_state.dart
index 3aac4f7..6315ba1 100644
--- a/lib/core/widgets/empty_state.dart
+++ b/lib/core/widgets/empty_state.dart
@@ -4,19 +4,17 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
-const double _regularArtSize = 84;
-const double _compactArtSize = 60;
-const double _regularInnerSize = 56;
-const double _compactInnerSize = 40;
-const double _regularIconSize = 27;
-const double _compactIconSize = 21;
+const double _regularIconSize = 48;
+const double _compactIconSize = 32;
const double _maxContentWidth = 260;
+const double _regularHorizontal = 24;
+const double _regularVertical = 28;
+const double _compactPad = 16;
+const double _regularGap = 20;
+const double _compactGap = 12;
const double _actionMinHeight = 44;
-const double _outerArtOpacity = 0.055;
-const double _outerBorderOpacity = 0.11;
-const double _innerArtOpacity = 0.11;
-/// 统一空列表占位:柔和图标、标题、可选说明与操作。
+/// 统一空列表占位:一枚图标、标题、可选说明与操作。
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
@@ -24,6 +22,7 @@ class EmptyState extends StatelessWidget {
required this.title,
this.subtitle,
this.actionLabel,
+ this.actionKey,
this.onAction,
this.compact = false,
});
@@ -32,17 +31,12 @@ class EmptyState extends StatelessWidget {
final String title;
final String? subtitle;
final String? actionLabel;
+ final Key? actionKey;
final VoidCallback? onAction;
final bool compact;
@override
Widget build(BuildContext context) {
- final scheme = Theme.of(context).colorScheme;
- final dark = Theme.of(context).brightness == Brightness.dark;
- final iconColor = dark ? AppColors.blueSoft : AppColors.blue;
- final artSize = compact ? _compactArtSize : _regularArtSize;
- final innerSize = compact ? _compactInnerSize : _regularInnerSize;
- final iconSize = compact ? _compactIconSize : _regularIconSize;
final hasAction = actionLabel != null && onAction != null;
return Center(
child: ConstrainedBox(
@@ -51,8 +45,8 @@ class EmptyState extends StatelessWidget {
child: Padding(
key: const Key('empty-state-padding'),
padding: EdgeInsets.symmetric(
- horizontal: compact ? 16 : 24,
- vertical: compact ? 16 : 28,
+ horizontal: compact ? _compactPad : _regularHorizontal,
+ vertical: compact ? _compactPad : _regularVertical,
),
child: Semantics(
container: true,
@@ -61,88 +55,16 @@ class EmptyState extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
ExcludeSemantics(
- child: Container(
- key: const Key('empty-state-art'),
- width: artSize,
- height: artSize,
- alignment: Alignment.center,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: AppColors.blue.withValues(alpha: _outerArtOpacity),
- border: Border.all(
- color: AppColors.blue.withValues(
- alpha: _outerBorderOpacity,
- ),
- ),
- ),
- child: Container(
- key: const Key('empty-state-art-inner'),
- width: innerSize,
- height: innerSize,
- alignment: Alignment.center,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: AppColors.blue.withValues(
- alpha: _innerArtOpacity,
- ),
- ),
- child: Icon(
- icon,
- key: const Key('empty-state-icon'),
- size: iconSize,
- color: iconColor,
- ),
- ),
- ),
+ child: _EmptyMark(icon: icon, compact: compact),
),
- SizedBox(height: compact ? 10 : 14),
- ExcludeSemantics(
- child: Text(
- title,
- key: const Key('empty-state-title'),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- textAlign: TextAlign.center,
- style: TextStyle(
- color: scheme.onSurface,
- fontSize: 15,
- fontWeight: FontWeight.w600,
- height: 1.35,
- ),
- ),
+ SizedBox(height: compact ? _compactGap : _regularGap),
+ _EmptyCopy(
+ title: title,
+ subtitle: subtitle,
+ actionLabel: hasAction ? actionLabel : null,
+ actionKey: actionKey,
+ onAction: hasAction ? onAction : null,
),
- if (subtitle != null) ...[
- const SizedBox(height: 6),
- ExcludeSemantics(
- child: Text(
- subtitle!,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- textAlign: TextAlign.center,
- style: const TextStyle(
- color: AppColors.muted,
- fontSize: 12.5,
- height: 1.45,
- ),
- ),
- ),
- ],
- if (hasAction) ...[
- const SizedBox(height: 8),
- TextButton(
- key: const Key('empty-state-action'),
- onPressed: onAction,
- style: TextButton.styleFrom(
- foregroundColor: scheme.primary,
- minimumSize: const Size(44, _actionMinHeight),
- padding: const EdgeInsets.symmetric(horizontal: 14),
- ),
- child: Text(
- actionLabel!,
- style: const TextStyle(fontSize: 13),
- ),
- ),
- ],
],
),
),
@@ -151,3 +73,92 @@ class EmptyState extends StatelessWidget {
);
}
}
+
+/// 空状态只留一枚图标,不加水印底和描边。
+class _EmptyMark extends StatelessWidget {
+ const _EmptyMark({required this.icon, required this.compact});
+
+ final IconData icon;
+ final bool compact;
+
+ @override
+ Widget build(BuildContext context) {
+ final dark = Theme.of(context).brightness == Brightness.dark;
+ return Icon(
+ icon,
+ key: const Key('empty-state-icon'),
+ size: compact ? _compactIconSize : _regularIconSize,
+ color: dark ? AppColors.gold : AppColors.blue,
+ );
+ }
+}
+
+class _EmptyCopy extends StatelessWidget {
+ const _EmptyCopy({
+ required this.title,
+ required this.subtitle,
+ required this.actionLabel,
+ required this.actionKey,
+ required this.onAction,
+ });
+
+ final String title;
+ final String? subtitle;
+ final String? actionLabel;
+ final Key? actionKey;
+ final VoidCallback? onAction;
+
+ @override
+ Widget build(BuildContext context) {
+ final scheme = Theme.of(context).colorScheme;
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ExcludeSemantics(
+ child: Text(
+ title,
+ key: const Key('empty-state-title'),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: scheme.onSurface,
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ height: 1.35,
+ ),
+ ),
+ ),
+ if (subtitle != null) ...[
+ const SizedBox(height: 6),
+ ExcludeSemantics(
+ child: Text(
+ subtitle!,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ color: AppColors.muted,
+ fontSize: 12.5,
+ height: 1.45,
+ ),
+ ),
+ ),
+ ],
+ if (actionLabel != null && onAction != null) ...[
+ const SizedBox(height: 8),
+ TextButton(
+ key: actionKey ?? const Key('empty-state-action'),
+ onPressed: onAction,
+ style: TextButton.styleFrom(
+ foregroundColor: scheme.primary,
+ minimumSize: const Size(44, _actionMinHeight),
+ padding: const EdgeInsets.symmetric(horizontal: 14),
+ ),
+ child: Text(actionLabel!, style: const TextStyle(fontSize: 13)),
+ ),
+ ],
+ ],
+ );
+ }
+}
diff --git a/lib/core/widgets/library_drama_tile.dart b/lib/core/widgets/library_drama_tile.dart
index b273fc3..d63ee99 100644
--- a/lib/core/widgets/library_drama_tile.dart
+++ b/lib/core/widgets/library_drama_tile.dart
@@ -22,7 +22,7 @@ class LibraryDramaTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
- color: const Color(0xFF1B1C21),
+ color: AppColors.card,
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.antiAlias,
child: InkWell(
diff --git a/lib/main.dart b/lib/main.dart
index 16769d8..cfc68ce 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,7 +8,9 @@ import 'core/config/server_endpoint.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
- await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
- await ServerEndpoint.load();
+ await Future.wait([
+ SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
+ ServerEndpoint.load(),
+ ]);
runApp(const LumaDramaApp());
}
diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart
index fc8ea7f..7e47800 100644
--- a/lib/pages/discover/discover_view.dart
+++ b/lib/pages/discover/discover_view.dart
@@ -120,7 +120,7 @@ class _DiscoverPageState extends State {
data: AppTheme.dark(),
child: widget.embedded
? content
- : Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
+ : Scaffold(backgroundColor: AppColors.ink, body: content),
),
);
}
@@ -159,7 +159,6 @@ class _DiscoverPageState extends State {
onAction: widget.onAction,
),
),
- const SliverToBoxAdapter(child: SizedBox(height: 24)),
],
);
@@ -176,9 +175,9 @@ class _DiscoverPageState extends State {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
- color: Colors.white,
+ color: AppColors.cream,
fontSize: 20,
- fontWeight: FontWeight.w700,
+ fontWeight: FontWeight.w800,
height: 1.1,
),
),
@@ -262,7 +261,7 @@ class _DiscoverPageState extends State {
];
return SizedBox(
key: const Key('home-category-chips'),
- height: 32,
+ height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: choices.length,
@@ -275,20 +274,18 @@ class _DiscoverPageState extends State {
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
alignment: Alignment.center,
- padding: const EdgeInsets.symmetric(horizontal: 12),
+ padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
- color: selected
- ? AppColors.bluePressed
- : const Color(0xFF1C1E24),
- borderRadius: BorderRadius.circular(16),
+ color: selected ? AppColors.blue : AppColors.raised,
+ borderRadius: BorderRadius.circular(18),
),
child: Text(
choice.label,
maxLines: 1,
style: TextStyle(
- color: selected ? Colors.white : AppColors.muted,
+ color: selected ? AppColors.onPrimary : AppColors.cream,
fontSize: 13,
- fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
+ fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
height: 1.1,
),
),
@@ -302,8 +299,8 @@ class _DiscoverPageState extends State {
Widget _bottomNav(AppLocalizations l10n) => Container(
height: 63,
decoration: const BoxDecoration(
- color: Color(0xFF0D0D11),
- border: Border(top: BorderSide(color: Color(0xFF242329))),
+ color: AppColors.bar,
+ border: Border(top: BorderSide(color: AppColors.outlineVariantDark)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -363,12 +360,12 @@ class _DiscoverNavItem extends StatelessWidget {
Icon(
icon,
size: 21,
- color: selected ? _accent : const Color(0xFFCAC4C7),
+ color: selected ? AppColors.blueSoft : AppColors.muted,
),
Text(
label,
style: TextStyle(
- color: selected ? _accent : const Color(0xFFCAC4C7),
+ color: selected ? AppColors.blueSoft : AppColors.muted,
fontSize: 10,
fontWeight: FontWeight.w700,
),
@@ -379,8 +376,7 @@ class _DiscoverNavItem extends StatelessWidget {
);
}
-const _accent = AppColors.blueSoft;
-const _coin = Color(0xFFF5C451);
+const _coin = AppColors.gold;
class _BrandMark extends StatelessWidget {
const _BrandMark();
@@ -418,8 +414,10 @@ class _HeaderPill extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
- color: const Color(0xFF1C1E24),
- shape: const StadiumBorder(side: BorderSide(color: Color(0x66F5C451))),
+ color: AppColors.raised,
+ shape: StadiumBorder(
+ side: BorderSide(color: AppColors.gold.withValues(alpha: 0.45)),
+ ),
child: InkWell(
customBorder: const StadiumBorder(),
onTap: onTap,
@@ -434,9 +432,9 @@ class _HeaderPill extends StatelessWidget {
label,
maxLines: 1,
style: const TextStyle(
- color: Colors.white,
+ color: AppColors.cream,
fontSize: 12,
- fontWeight: FontWeight.w600,
+ fontWeight: FontWeight.w700,
height: 1.1,
),
),
diff --git a/lib/pages/discover/widgets/home_drama_grid.dart b/lib/pages/discover/widgets/home_drama_grid.dart
index fa1e345..50aabe8 100644
--- a/lib/pages/discover/widgets/home_drama_grid.dart
+++ b/lib/pages/discover/widgets/home_drama_grid.dart
@@ -16,28 +16,20 @@ class _HomeDramaGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (categories.loading && categories.feed.sections.isEmpty) {
- return const SliverPadding(
- padding: EdgeInsets.symmetric(vertical: 24),
- sliver: SliverToBoxAdapter(
- child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
- ),
+ return const SliverFillRemaining(
+ hasScrollBody: false,
+ child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
if (categories.error != null) {
- return SliverPadding(
- padding: const EdgeInsets.fromLTRB(16, 6, 16, 18),
- sliver: SliverToBoxAdapter(
- child: Row(
- children: [
- Expanded(
- child: Text(
- l10n.categoriesError,
- style: const TextStyle(color: AppColors.muted),
- ),
- ),
- TextButton(onPressed: categories.load, child: Text(l10n.retry)),
- ],
- ),
+ return _fill(
+ EmptyState(
+ key: const Key('home-categories-error'),
+ icon: Icons.cloud_off_rounded,
+ title: l10n.categoriesError,
+ subtitle: l10n.emptyHint,
+ actionLabel: l10n.retry,
+ onAction: categories.load,
),
);
}
@@ -45,45 +37,51 @@ class _HomeDramaGrid extends StatelessWidget {
final dramas = categories.visibleDramas;
if (dramas.isEmpty) {
final missingCategory = categories.selectedSectionMissing;
- return SliverPadding(
- padding: const EdgeInsets.fromLTRB(16, 6, 16, 24),
- sliver: SliverToBoxAdapter(
- child: EmptyState(
- key: const Key('home-all-dramas-empty'),
- icon: missingCategory
- ? Icons.grid_view_rounded
- : Icons.search_off_rounded,
- title: missingCategory
- ? l10n.noCategoryDramas
- : l10n.noSearchResults,
- subtitle: l10n.emptyHint,
- ),
+ return _fill(
+ EmptyState(
+ key: const Key('home-all-dramas-empty'),
+ icon: missingCategory
+ ? Icons.grid_view_rounded
+ : Icons.search_off_rounded,
+ title: missingCategory ? l10n.noCategoryDramas : l10n.noSearchResults,
+ subtitle: l10n.emptyHint,
),
);
}
- return SliverPadding(
- key: const Key('home-all-dramas-section'),
- padding: const EdgeInsets.symmetric(horizontal: 16),
- sliver: SliverGrid(
- key: const Key('home-all-dramas-grid'),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3,
- mainAxisSpacing: 14,
- crossAxisSpacing: 10,
- childAspectRatio: 2 / 3,
- ),
- delegate: SliverChildBuilderDelegate(
- (context, index) => _HomeDramaCard(
- l10n: l10n,
- drama: dramas[index],
- onTap: () => onOpen(dramas[index]),
- ),
- childCount: dramas.length,
- ),
- ),
+ return SliverMainAxisGroup(
+ slivers: [
+ _dramaGrid(dramas),
+ const SliverToBoxAdapter(child: SizedBox(height: 24)),
+ ],
);
}
+
+ Widget _dramaGrid(List dramas) => SliverPadding(
+ key: const Key('home-all-dramas-section'),
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ sliver: SliverGrid(
+ key: const Key('home-all-dramas-grid'),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 3,
+ mainAxisSpacing: 14,
+ crossAxisSpacing: 10,
+ childAspectRatio: 2 / 3,
+ ),
+ delegate: SliverChildBuilderDelegate(
+ (context, index) => _HomeDramaCard(
+ l10n: l10n,
+ drama: dramas[index],
+ onTap: () => onOpen(dramas[index]),
+ ),
+ childCount: dramas.length,
+ ),
+ ),
+ );
+
+ Widget _fill(Widget child) {
+ return SliverFillRemaining(hasScrollBody: false, child: child);
+ }
}
class _HomeDramaCard extends StatelessWidget {
@@ -122,11 +120,37 @@ class _HomeDramaCard extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
- colors: [Color(0x00000000), Color(0xE6000000)],
- stops: [0.4, 1],
+ colors: [Color(0x00000000), Color(0xE6100C0A)],
+ stops: [0.42, 1],
),
),
),
+ if (drama.isHot)
+ Positioned(
+ top: 8,
+ left: 8,
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: AppColors.blue,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ child: Text(
+ l10n.hot,
+ style: const TextStyle(
+ color: AppColors.onPrimary,
+ fontSize: 10,
+ fontWeight: FontWeight.w800,
+ height: 1.1,
+ ),
+ ),
+ ),
+ ),
+ ),
Positioned(
left: 10,
right: 10,
@@ -141,10 +165,10 @@ class _HomeDramaCard extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
- color: Colors.white,
- fontSize: 14,
- fontWeight: FontWeight.w700,
- height: 1.2,
+ color: AppColors.cream,
+ fontSize: 13,
+ fontWeight: FontWeight.w800,
+ height: 1.15,
),
),
const SizedBox(height: 4),
diff --git a/lib/pages/downloads/downloads_page.dart b/lib/pages/downloads/downloads_page.dart
index 3064507..8d16604 100644
--- a/lib/pages/downloads/downloads_page.dart
+++ b/lib/pages/downloads/downloads_page.dart
@@ -54,9 +54,9 @@ class _DownloadsPageState extends State {
data: AppTheme.dark(),
child: Scaffold(
key: const Key('downloads-page'),
- backgroundColor: const Color(0xFF0B0C10),
+ backgroundColor: AppColors.ink,
appBar: AppBar(
- backgroundColor: const Color(0xFF0B0C10),
+ backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(l10n.downloads),
),
@@ -79,41 +79,17 @@ class _DownloadsBody extends StatelessWidget {
if (downloads.loading && downloads.policy == null) {
return const Center(child: CircularProgressIndicator());
}
- if (downloads.error != null && downloads.policy == null) {
- return Center(
+ if (downloads.policy == null) {
+ return EmptyState(
key: const Key('downloads-error'),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- errorText(l10n, downloads.error, l10n.downloadsError),
- textAlign: TextAlign.center,
- style: const TextStyle(color: Colors.white70),
- ),
- const SizedBox(height: 14),
- FilledButton(
- key: const Key('downloads-retry'),
- onPressed: () => unawaited(downloads.load()),
- child: Text(l10n.retry),
- ),
- ],
- ),
- ),
- );
- }
- final policy = downloads.policy;
- if (policy == null) {
- return Center(
- key: const Key('downloads-error'),
- child: FilledButton(
- key: const Key('downloads-retry'),
- onPressed: () => unawaited(downloads.load()),
- child: Text(l10n.retry),
- ),
+ icon: Icons.cloud_off_rounded,
+ title: errorText(l10n, downloads.error, l10n.downloadsError),
+ actionLabel: l10n.retry,
+ actionKey: const Key('downloads-retry'),
+ onAction: () => unawaited(downloads.load()),
);
}
+ final policy = downloads.policy!;
return RefreshIndicator(
onRefresh: downloads.load,
child: ListView(
diff --git a/lib/pages/drama_detail/drama_detail_view.dart b/lib/pages/drama_detail/drama_detail_view.dart
index 6dd8bd4..05700c9 100644
--- a/lib/pages/drama_detail/drama_detail_view.dart
+++ b/lib/pages/drama_detail/drama_detail_view.dart
@@ -79,13 +79,13 @@ class _DramaDetailPageState extends State {
final l10n = AppLocalizations.of(context)!;
if (logic.loading) {
return const Scaffold(
- backgroundColor: Color(0xFF111111),
+ backgroundColor: AppColors.ink,
body: Center(child: CircularProgressIndicator()),
);
}
if (logic.failed) {
return Scaffold(
- backgroundColor: const Color(0xFF111111),
+ backgroundColor: AppColors.ink,
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
@@ -109,9 +109,9 @@ class _DramaDetailPageState extends State {
if (drama.episodes.isEmpty) {
// 空态:详情拿到了但一集都没有(还没上传/审核中),不能白屏
return Scaffold(
- backgroundColor: const Color(0xFF111111),
+ backgroundColor: AppColors.ink,
appBar: AppBar(
- backgroundColor: const Color(0xFF111111),
+ backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(localizedDramaTitle(l10n, drama)),
),
@@ -126,12 +126,13 @@ class _DramaDetailPageState extends State {
);
}
return Scaffold(
+ backgroundColor: AppColors.ink,
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 300,
pinned: true,
- backgroundColor: const Color(0xFF111111),
+ backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
actions: [
_reportAction(context, l10n),
diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart
index 032695b..1ebf464 100644
--- a/lib/pages/favorites/favorites_page.dart
+++ b/lib/pages/favorites/favorites_page.dart
@@ -10,6 +10,7 @@ import '../../core/data/services/auth_controller.dart';
import '../../core/data/services/favorite_controller.dart';
import '../../core/formatting/error_text.dart';
import '../../core/platform/haptics.dart';
+import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
@@ -52,9 +53,9 @@ class _FavoritesPageState extends State {
return Theme(
data: AppTheme.dark(),
child: Scaffold(
- backgroundColor: const Color(0xFF0B0C10),
+ backgroundColor: AppColors.ink,
appBar: AppBar(
- backgroundColor: const Color(0xFF0B0C10),
+ backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(l10n.myFavorites),
),
@@ -78,27 +79,13 @@ class _FavoritesBody extends StatelessWidget {
return const Center(child: CircularProgressIndicator());
}
if (favorites.error != null && favorites.cards.isEmpty) {
- return Center(
+ return EmptyState(
key: const Key('favorites-error'),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- errorText(l10n, favorites.error, l10n.favoritesError),
- textAlign: TextAlign.center,
- style: const TextStyle(color: Colors.white70),
- ),
- const SizedBox(height: 14),
- FilledButton(
- key: const Key('favorites-retry'),
- onPressed: () => unawaited(favorites.load()),
- child: Text(l10n.retry),
- ),
- ],
- ),
- ),
+ icon: Icons.cloud_off_rounded,
+ title: errorText(l10n, favorites.error, l10n.favoritesError),
+ actionLabel: l10n.retry,
+ actionKey: const Key('favorites-retry'),
+ onAction: () => unawaited(favorites.load()),
);
}
if (favorites.cards.isEmpty) {
diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart
index 28a67fe..4a309c2 100644
--- a/lib/pages/home/home_view.dart
+++ b/lib/pages/home/home_view.dart
@@ -18,6 +18,7 @@ import '../../core/widgets/empty_state.dart';
import '../../core/widgets/server_endpoint_dialog.dart';
import '../../l10n/app_localizations.dart';
import '../discover/discover_view.dart';
+import '../splash/splash_page.dart';
import '../mine/mine_view.dart';
import '../player/player_view.dart';
import 'home_binding.dart';
@@ -30,6 +31,7 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final catalog = context.watch();
+ if (!catalog.settled) return const SplashPage();
final l10n = AppLocalizations.of(context)!;
return Theme(
data: AppTheme.dark(),
@@ -44,13 +46,13 @@ class HomePage extends StatelessWidget {
) {
if (catalog.loading) {
return const Scaffold(
- backgroundColor: Colors.black,
+ backgroundColor: AppColors.ink,
body: Center(child: CircularProgressIndicator()),
);
}
if (catalog.error != null) {
return Scaffold(
- backgroundColor: Colors.black,
+ backgroundColor: AppColors.ink,
body: HomeFailureView(
onRetry: catalog.load,
showDebugTools: kDebugMode,
@@ -59,7 +61,7 @@ class HomePage extends StatelessWidget {
}
if (catalog.dramas.isEmpty) {
return Scaffold(
- backgroundColor: Colors.black,
+ backgroundColor: AppColors.ink,
body: EmptyState(
key: const Key('home-empty'),
icon: Icons.movie_outlined,
@@ -92,33 +94,37 @@ class HomeFailureView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
- return Center(
+ return Column(
+ children: [
+ Expanded(
+ child: EmptyState(
+ icon: Icons.cloud_off_rounded,
+ title: l10n.catalogError,
+ actionLabel: l10n.retry,
+ onAction: onRetry,
+ ),
+ ),
+ if (showDebugTools) _debugTools(context, l10n),
+ ],
+ );
+ }
+
+ Widget _debugTools(BuildContext context, AppLocalizations l10n) {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
- mainAxisSize: MainAxisSize.min,
children: [
- const Icon(
- Icons.error_outline_rounded,
- color: Colors.white70,
- size: 40,
+ Text(
+ AppConfig.apiBaseUrl,
+ style: const TextStyle(fontSize: 12, color: AppColors.muted),
+ ),
+ const SizedBox(height: 8),
+ OutlinedButton.icon(
+ key: const Key('home-server-settings'),
+ onPressed: () => showServerEndpointDialog(context, l10n),
+ icon: const Icon(Icons.dns_outlined),
+ label: Text(l10n.serverSettings),
),
- const SizedBox(height: 12),
- Text(l10n.catalogError),
- const SizedBox(height: 12),
- FilledButton(onPressed: onRetry, child: Text(l10n.retry)),
- if (showDebugTools) ...[
- const SizedBox(height: 16),
- Text(
- AppConfig.apiBaseUrl,
- style: const TextStyle(fontSize: 12, color: Colors.white54),
- ),
- const SizedBox(height: 8),
- OutlinedButton.icon(
- key: const Key('home-server-settings'),
- onPressed: () => showServerEndpointDialog(context, l10n),
- icon: const Icon(Icons.dns_outlined),
- label: Text(l10n.serverSettings),
- ),
- ],
],
),
);
@@ -340,7 +346,7 @@ class _TabButton extends StatelessWidget {
vertical: 3,
),
decoration: BoxDecoration(
- color: selected ? AppColors.bluePressed : null,
+ color: selected ? AppColors.blue : null,
borderRadius: BorderRadius.circular(14),
),
child: Icon(
diff --git a/lib/pages/home/widgets/home_quick_drawer.dart b/lib/pages/home/widgets/home_quick_drawer.dart
index a4cd825..d419115 100644
--- a/lib/pages/home/widgets/home_quick_drawer.dart
+++ b/lib/pages/home/widgets/home_quick_drawer.dart
@@ -29,8 +29,8 @@ class HomeQuickDrawer extends StatelessWidget {
final VoidCallback onWatchlist;
final VoidCallback onLiked;
- static const _panel = Color(0xFF222225);
- static const _card = Color(0xFF303033);
+ static const _panel = AppColors.card;
+ static const _card = AppColors.raised;
static const _accent = AppColors.blueSoft;
@override
@@ -129,8 +129,8 @@ class HomeQuickDrawer extends StatelessWidget {
key: const Key('drawer-open-series'),
onPressed: onOpenSeries,
style: FilledButton.styleFrom(
- backgroundColor: _accent,
- foregroundColor: Colors.white,
+ backgroundColor: AppColors.blue,
+ foregroundColor: AppColors.onPrimary,
),
icon: const Icon(Icons.play_arrow_rounded),
label: Text(l10n.exploreSeries),
diff --git a/lib/pages/mine/mine_logic.dart b/lib/pages/mine/mine_logic.dart
index 553d35d..3c56aaf 100644
--- a/lib/pages/mine/mine_logic.dart
+++ b/lib/pages/mine/mine_logic.dart
@@ -9,6 +9,7 @@ import '../../core/config/locale_controller.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/models/watch_progress.dart';
import '../../core/data/repositories/watch_history_repository.dart';
+import '../../core/theme/app_colors.dart';
import '../../core/widgets/server_endpoint_dialog.dart';
import '../../l10n/app_localizations.dart';
@@ -120,7 +121,7 @@ class MineLogic extends ChangeNotifier {
final l10n = AppLocalizations.of(context)!;
await showModalBottomSheet(
context: context,
- backgroundColor: const Color(0xFF1C1D22),
+ backgroundColor: AppColors.ink,
showDragHandle: true,
isScrollControlled: true,
builder: (sheetContext) => SafeArea(
diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart
index 9ed63bd..3af591b 100644
--- a/lib/pages/mine/mine_view.dart
+++ b/lib/pages/mine/mine_view.dart
@@ -158,7 +158,7 @@ class _MyPageState extends State {
data: AppTheme.dark(),
child: widget.embedded
? content
- : Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
+ : Scaffold(backgroundColor: AppColors.ink, body: content),
);
}
@@ -188,7 +188,7 @@ class _MyPageState extends State {
const SizedBox(width: 6),
const CircleAvatar(
radius: 14,
- backgroundColor: Color(0xFF2C2D33),
+ backgroundColor: AppColors.raised,
child: Icon(Icons.person_rounded, color: Colors.white, size: 19),
),
],
@@ -229,7 +229,7 @@ class _MyPageState extends State {
children: [
const CircleAvatar(
radius: 28,
- backgroundColor: Color(0xFF32343A),
+ backgroundColor: AppColors.raised,
child: Icon(Icons.person_rounded, color: Colors.white, size: 30),
),
const SizedBox(width: 12),
@@ -269,7 +269,7 @@ class _MyPageState extends State {
children: [
const CircleAvatar(
radius: 28,
- backgroundColor: Color(0xFF32343A),
+ backgroundColor: AppColors.raised,
child: Icon(Icons.person_rounded, color: Colors.white, size: 30),
),
const SizedBox(width: 12),
@@ -297,7 +297,7 @@ class _MyPageState extends State {
TextButton(
onPressed: widget.onSignOut ?? () => _mockSnack(l10n),
style: TextButton.styleFrom(
- backgroundColor: const Color(0xFF2A2C33),
+ backgroundColor: AppColors.raised,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: const StadiumBorder(),
@@ -312,7 +312,7 @@ class _MyPageState extends State {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
- color: const Color(0xFF17191F),
+ color: AppColors.card,
borderRadius: BorderRadius.circular(16),
),
child: Column(
@@ -330,7 +330,7 @@ class _MyPageState extends State {
label: l10n.coins,
value: '\$ 0',
action: l10n.topUpWallet,
- color: AppColors.blue,
+ color: AppColors.gold,
onPressed: () => _mockSnack(l10n),
),
),
@@ -340,7 +340,7 @@ class _MyPageState extends State {
label: l10n.vipMembership,
value: l10n.notActive,
action: l10n.joinVip,
- color: const Color(0xFFF5C451),
+ color: AppColors.blue,
onPressed: () => _mockSnack(l10n),
),
),
@@ -367,7 +367,7 @@ class _MyPageState extends State {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
- color: const Color(0xFF12141A),
+ color: AppColors.ink,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: color.withValues(alpha: 0.45)),
),
@@ -389,7 +389,7 @@ class _MyPageState extends State {
backgroundColor: color,
foregroundColor: color == AppColors.blue
? Colors.white
- : const Color(0xFF2A2110),
+ : AppColors.onGold,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(),
),
@@ -403,7 +403,7 @@ class _MyPageState extends State {
Widget _menuCard(AppLocalizations l10n) {
return Material(
- color: const Color(0xFF17191F),
+ color: AppColors.card,
borderRadius: BorderRadius.circular(16),
clipBehavior: Clip.antiAlias,
child: Column(
@@ -420,7 +420,7 @@ class _MyPageState extends State {
),
onTap: () => _mockSnack(l10n),
),
- const Divider(height: 1, color: Color(0xFF2A2C33)),
+ const Divider(height: 1, color: AppColors.outlineVariantDark),
ListTile(
key: const Key('mine-notices'),
leading: const Icon(
@@ -433,7 +433,7 @@ class _MyPageState extends State {
MaterialPageRoute(builder: (_) => const NoticesPage()),
),
),
- const Divider(height: 1, color: Color(0xFF2A2C33)),
+ const Divider(height: 1, color: AppColors.outlineVariantDark),
ListTile(
leading: const Icon(
Icons.history_rounded,
@@ -445,7 +445,7 @@ class _MyPageState extends State {
MaterialPageRoute(builder: (_) => const WatchRecordsPage()),
),
),
- const Divider(height: 1, color: Color(0xFF2A2C33)),
+ const Divider(height: 1, color: AppColors.outlineVariantDark),
ListTile(
leading: const Icon(
Icons.favorite_outline_rounded,
@@ -457,7 +457,7 @@ class _MyPageState extends State {
MaterialPageRoute(builder: (_) => const FavoritesPage()),
),
),
- const Divider(height: 1, color: Color(0xFF2A2C33)),
+ const Divider(height: 1, color: AppColors.outlineVariantDark),
ListTile(
leading: const Icon(
Icons.download_outlined,
@@ -469,7 +469,7 @@ class _MyPageState extends State {
MaterialPageRoute(builder: (_) => const DownloadsPage()),
),
),
- const Divider(height: 1, color: Color(0xFF2A2C33)),
+ const Divider(height: 1, color: AppColors.outlineVariantDark),
ListTile(
leading: const Icon(
Icons.settings_rounded,
@@ -530,8 +530,8 @@ class _MyPageState extends State {
),
decoration: BoxDecoration(
color: logic.section == index
- ? AppColors.bluePressed
- : const Color(0xFF24262E),
+ ? AppColors.blue
+ : AppColors.raised,
borderRadius: BorderRadius.circular(20),
),
child: Text(
@@ -564,8 +564,8 @@ class _MyPageState extends State {
onSelected: (_) => logic.selectFilter(index),
showCheckmark: false,
visualDensity: VisualDensity.compact,
- backgroundColor: const Color(0xFF242328),
- selectedColor: _accent,
+ backgroundColor: AppColors.raised,
+ selectedColor: AppColors.blue,
labelStyle: TextStyle(
color: logic.filter == index
? Colors.white
@@ -592,7 +592,7 @@ class _MyPageState extends State {
child: Container(
padding: const EdgeInsets.all(9),
decoration: BoxDecoration(
- color: const Color(0xFF1B1C21),
+ color: AppColors.card,
borderRadius: BorderRadius.circular(12),
),
child: Row(
@@ -635,7 +635,7 @@ class _MyPageState extends State {
),
const CircleAvatar(
radius: 15,
- backgroundColor: Color(0xFF313137),
+ backgroundColor: AppColors.raised,
child: Icon(
Icons.play_arrow_rounded,
color: _accent,
@@ -658,21 +658,12 @@ class _MyPageState extends State {
),
)
else if (isWatchlist && favorites.error != null)
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 18),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- errorText(l10n, favorites.error, l10n.favoritesError),
- style: const TextStyle(color: Color(0xFFE6E1E3)),
- ),
- TextButton(
- onPressed: () => unawaited(favorites.load()),
- child: Text(l10n.retry),
- ),
- ],
- ),
+ EmptyState(
+ icon: Icons.cloud_off_rounded,
+ title: errorText(l10n, favorites.error, l10n.favoritesError),
+ actionLabel: l10n.retry,
+ onAction: () => unawaited(favorites.load()),
+ compact: true,
)
else if (logic.section != 0 && markedDramas.isNotEmpty)
for (final drama in markedDramas) ...[
@@ -789,7 +780,7 @@ class _MyPageState extends State {
child: Container(
padding: const EdgeInsets.all(9),
decoration: BoxDecoration(
- color: const Color(0xFF1B1C21),
+ color: AppColors.card,
borderRadius: BorderRadius.circular(12),
),
child: Row(
@@ -832,8 +823,8 @@ class _MyPageState extends State {
Widget _bottomNav(AppLocalizations l10n) => Container(
height: 63,
decoration: const BoxDecoration(
- color: Color(0xFF0D0D11),
- border: Border(top: BorderSide(color: Color(0xFF242329))),
+ color: AppColors.bar,
+ border: Border(top: BorderSide(color: AppColors.outlineVariantDark)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -898,12 +889,12 @@ class _NavItem extends StatelessWidget {
Icon(
icon,
size: 21,
- color: selected ? _accent : const Color(0xFFCAC4C7),
+ color: selected ? _accent : AppColors.muted,
),
Text(
label,
style: TextStyle(
- color: selected ? _accent : const Color(0xFFCAC4C7),
+ color: selected ? _accent : AppColors.muted,
fontSize: 10,
fontWeight: FontWeight.w700,
),
diff --git a/lib/pages/notices/notices_page.dart b/lib/pages/notices/notices_page.dart
index 7d2864b..c828adf 100644
--- a/lib/pages/notices/notices_page.dart
+++ b/lib/pages/notices/notices_page.dart
@@ -74,28 +74,17 @@ class _NoticesPageState extends State {
children: [
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.7,
- child: Center(
+ child: EmptyState(
key: const Key('notices-error'),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- errorText(
- l10n,
- _controller.error,
- l10n.noticesLoadFailed,
- ),
- textAlign: TextAlign.center,
- style: const TextStyle(color: AppColors.muted),
- ),
- const SizedBox(height: 12),
- FilledButton(
- key: const Key('notices-retry'),
- onPressed: _controller.load,
- child: Text(l10n.retry),
- ),
- ],
+ icon: Icons.cloud_off_rounded,
+ title: errorText(
+ l10n,
+ _controller.error,
+ l10n.noticesLoadFailed,
),
+ actionLabel: l10n.retry,
+ actionKey: const Key('notices-retry'),
+ onAction: _controller.load,
),
),
],
diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart
index 31ddc40..aa4e50b 100644
--- a/lib/pages/player/player_logic.dart
+++ b/lib/pages/player/player_logic.dart
@@ -29,6 +29,9 @@ import '../mine/mine_logic.dart';
import '../mine/mine_view.dart';
import 'player_view.dart';
import 'widgets/drama_comments_sheet.dart';
+import 'widgets/episode_picker_cell.dart';
+
+const double _episodeCellAspect = 1.05;
class PlayerLogic extends ChangeNotifier {
PlayerLogic({
@@ -616,7 +619,7 @@ class PlayerLogic extends ChangeNotifier {
try {
await showModalBottomSheet(
context: context,
- backgroundColor: const Color(0xFF16171C),
+ backgroundColor: AppColors.ink,
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: SizedBox(
@@ -655,7 +658,7 @@ class PlayerLogic extends ChangeNotifier {
crossAxisCount: 6,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
- childAspectRatio: 1.45,
+ childAspectRatio: _episodeCellAspect,
),
itemCount: episodes.length,
itemBuilder: (_, index) => _episodeCell(
@@ -706,38 +709,27 @@ class PlayerLogic extends ChangeNotifier {
int currentNumber,
) {
final selected = episode.number == currentNumber;
- return Material(
- color: selected ? AppColors.bluePressed : const Color(0xFF2A2C33),
- borderRadius: BorderRadius.circular(8),
- child: InkWell(
- borderRadius: BorderRadius.circular(8),
- onTap: () {
- Navigator.of(sheetContext).pop();
- if (homeFeed) {
- if (episode.number == currentNumber) return;
- unawaited(openSeriesAt(pageContext, drama, index));
- return;
- }
- if (index == session.currentIndex) return;
- unawaited(
- pages.animateToPage(
- index,
- duration: const Duration(milliseconds: 260),
- curve: Curves.easeOutCubic,
- ),
- );
- },
- child: Center(
- child: Text(
- '${episode.number}',
- style: const TextStyle(
- color: Colors.white,
- fontSize: 15,
- fontWeight: FontWeight.w600,
- ),
+ final l10n = AppLocalizations.of(pageContext)!;
+ return EpisodePickerCell(
+ episode: episode,
+ selected: selected,
+ lockLabel: l10n.locked,
+ onTap: () {
+ Navigator.of(sheetContext).pop();
+ if (homeFeed) {
+ if (episode.number == currentNumber) return;
+ unawaited(openSeriesAt(pageContext, drama, index));
+ return;
+ }
+ if (index == session.currentIndex) return;
+ unawaited(
+ pages.animateToPage(
+ index,
+ duration: const Duration(milliseconds: 260),
+ curve: Curves.easeOutCubic,
),
- ),
- ),
+ );
+ },
);
}
@@ -843,7 +835,7 @@ class PlayerLogic extends ChangeNotifier {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
- backgroundColor: const Color(0xFF232328),
+ backgroundColor: AppColors.ink,
showDragHandle: true,
builder: (_) => DramaCommentsSheet(drama: drama),
);
diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart
index 63fafb8..9b142b2 100644
--- a/lib/pages/player/player_view.dart
+++ b/lib/pages/player/player_view.dart
@@ -136,7 +136,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver {
data: AppTheme.dark(),
child: widget.embedded
? content
- : Scaffold(backgroundColor: Colors.black, body: content),
+ : Scaffold(backgroundColor: AppColors.ink, body: content),
),
);
}
@@ -183,7 +183,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver {
return Stack(
fit: StackFit.expand,
children: [
- const ColoredBox(color: Colors.black),
+ const ColoredBox(color: AppColors.ink),
?video,
if (!logic.chromeHidden)
const IgnorePointer(
@@ -304,7 +304,7 @@ class _PlayerPageState extends State with WidgetsBindingObserver {
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
- color: Color(0x73000000),
+ color: AppColors.blue,
shape: BoxShape.circle,
),
child: Padding(
@@ -494,31 +494,91 @@ class _BlockedNotice extends StatelessWidget {
: l10n.noMediaNotice;
return Center(
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 36),
+ padding: const EdgeInsets.symmetric(horizontal: 28),
+ child: _UnlockCard(
+ locked: needsUnlock,
+ title: title,
+ coinLabel: needsUnlock && unlockCoin > 0
+ ? '$unlockCoin ${l10n.coins}'
+ : null,
+ footnote: needsUnlock ? l10n.unlockComingSoon : null,
+ ),
+ ),
+ );
+ }
+}
+
+/// Coin lock presented as a reward card: gold chip, chili only on the icon.
+class _UnlockCard extends StatelessWidget {
+ const _UnlockCard({
+ required this.locked,
+ required this.title,
+ required this.coinLabel,
+ required this.footnote,
+ });
+
+ final bool locked;
+ final String title;
+ final String? coinLabel;
+ final String? footnote;
+
+ @override
+ Widget build(BuildContext context) {
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ color: AppColors.card,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: AppColors.gold.withValues(alpha: 0.55)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(20, 22, 20, 18),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
- needsUnlock
- ? Icons.lock_outline_rounded
- : Icons.movie_filter_outlined,
- color: Colors.white70,
- size: 36,
+ locked ? Icons.lock_outline_rounded : Icons.movie_filter_outlined,
+ color: locked ? AppColors.gold : AppColors.cream,
+ size: 32,
),
const SizedBox(height: 12),
Text(
title,
textAlign: TextAlign.center,
- style: const TextStyle(color: Colors.white70),
+ style: const TextStyle(
+ color: AppColors.cream,
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
+ ),
),
- if (needsUnlock) ...[
- const SizedBox(height: 6),
+ if (coinLabel != null) ...[
+ const SizedBox(height: 12),
+ DecoratedBox(
+ decoration: BoxDecoration(
+ color: AppColors.gold,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 6,
+ ),
+ child: Text(
+ coinLabel!,
+ style: const TextStyle(
+ color: AppColors.onGold,
+ fontSize: 13,
+ fontWeight: FontWeight.w800,
+ ),
+ ),
+ ),
+ ),
+ ],
+ if (footnote != null) ...[
+ const SizedBox(height: 8),
Text(
- unlockCoin > 0
- ? '${l10n.unlockComingSoon} · $unlockCoin ${l10n.coins}'
- : l10n.unlockComingSoon,
+ footnote!,
textAlign: TextAlign.center,
- style: const TextStyle(color: Colors.white38, fontSize: 12),
+ style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
],
],
@@ -674,14 +734,14 @@ class _PlaybackOverlay extends StatelessWidget {
children: [
_RailAction(
key: const Key('rail-like'),
- icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
+ icon: Icons.favorite_border,
label: l10n.like,
selected: liked,
onTap: onLike,
),
_RailAction(
key: const Key('rail-save'),
- icon: saved ? Icons.bookmark_rounded : Icons.bookmark_border_rounded,
+ icon: Icons.bookmark_border,
label: l10n.save,
selected: saved,
onTap: onSave,
@@ -693,19 +753,19 @@ class _PlaybackOverlay extends StatelessWidget {
onTap: onDownload,
),
_RailAction(
- icon: Icons.grid_view_rounded,
+ icon: Icons.grid_view_outlined,
label: l10n.episodes,
onTap: onEpisodes,
),
_RailAction(
key: const Key('rail-comments'),
- icon: Icons.mode_comment_outlined,
+ icon: Icons.chat_bubble_outline,
label: l10n.comments,
onTap: onComments,
),
_RailAction(
key: const Key('rail-share'),
- icon: Icons.share_rounded,
+ icon: Icons.share_outlined,
label: l10n.share,
onTap: onShare,
),
@@ -713,10 +773,16 @@ class _PlaybackOverlay extends StatelessWidget {
IconButton(
tooltip: l10n.pictureInPicture,
onPressed: onPip,
- visualDensity: VisualDensity.compact,
- iconSize: 20,
- color: Colors.white,
- icon: const Icon(Icons.picture_in_picture_alt_outlined),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints.tightFor(
+ width: _railTapSize,
+ height: _railTapSize,
+ ),
+ color: AppColors.cream,
+ icon: const Icon(
+ Icons.picture_in_picture_alt_outlined,
+ size: _railIconSize,
+ ),
),
],
);
@@ -748,6 +814,10 @@ class _PlaybackOverlay extends StatelessWidget {
);
}
+const double _railIconSize = 28;
+const double _railIconWeight = 300;
+const double _railTapSize = 44;
+
class _RailAction extends StatelessWidget {
const _RailAction({
super.key,
@@ -762,28 +832,16 @@ class _RailAction extends StatelessWidget {
final bool selected;
@override
- Widget build(BuildContext context) => Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- IconButton(
- tooltip: label,
- onPressed: onTap,
- visualDensity: VisualDensity.compact,
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints.tightFor(width: 42, height: 36),
- color: selected ? AppColors.blueSoft : Colors.white,
- icon: Icon(icon, size: 24),
- ),
- Text(
- label,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 11,
- fontWeight: FontWeight.w600,
- shadows: [Shadow(blurRadius: 8, color: Colors.black)],
- ),
- ),
- ],
+ Widget build(BuildContext context) => IconButton(
+ tooltip: label,
+ onPressed: onTap,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints.tightFor(
+ width: _railTapSize,
+ height: _railTapSize,
+ ),
+ color: selected ? AppColors.blueSoft : AppColors.cream,
+ icon: Icon(icon, size: _railIconSize, weight: _railIconWeight, fill: 0),
);
}
diff --git a/lib/pages/player/widgets/comment_tile.dart b/lib/pages/player/widgets/comment_tile.dart
index 5e3145d..52842b5 100644
--- a/lib/pages/player/widgets/comment_tile.dart
+++ b/lib/pages/player/widgets/comment_tile.dart
@@ -52,11 +52,11 @@ class CommentTile extends StatelessWidget {
children: [
CircleAvatar(
radius: isReply ? 12 : 16,
- backgroundColor: AppColors.blue.withValues(alpha: 0.2),
+ backgroundColor: AppColors.raised,
child: Icon(
Icons.person_rounded,
size: isReply ? 14 : 18,
- color: AppColors.blueSoft,
+ color: AppColors.cream,
),
),
const SizedBox(width: 12),
@@ -101,7 +101,7 @@ class CommentTile extends StatelessWidget {
),
Text(
comment.content,
- style: const TextStyle(color: Colors.white, fontSize: 14),
+ style: const TextStyle(color: AppColors.cream, fontSize: 14),
),
if (comment.isPending)
Padding(
diff --git a/lib/pages/player/widgets/drama_comments_sheet.dart b/lib/pages/player/widgets/drama_comments_sheet.dart
index 2926c2a..e95d820 100644
--- a/lib/pages/player/widgets/drama_comments_sheet.dart
+++ b/lib/pages/player/widgets/drama_comments_sheet.dart
@@ -19,9 +19,10 @@ import '../../../l10n/app_localizations.dart';
import '../../interaction/widgets/report_dialog.dart';
import 'comment_tile.dart';
-const double _minSheetHeight = 240;
-const double _maxSheetHeight = 460;
+const double _minSheetHeight = 280;
+const double _maxSheetHeight = 520;
const int _maxCommentLength = 300;
+const double _composerRadius = 22;
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复、点赞和本人删除。
class DramaCommentsSheet extends StatefulWidget {
@@ -164,7 +165,7 @@ class _DramaCommentsSheetState extends State {
child: SafeArea(
top: false,
child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
+ padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) => _content(context),
@@ -182,9 +183,13 @@ class _DramaCommentsSheetState extends State {
children: [
Text(
l10n.comments,
- style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
+ style: const TextStyle(
+ color: AppColors.cream,
+ fontSize: 18,
+ fontWeight: FontWeight.w700,
+ ),
),
- const SizedBox(height: 8),
+ const SizedBox(height: 12),
Expanded(child: _list(l10n)),
if (_replyTo != null) _replyBanner(l10n, _replyTo!),
_composer(l10n),
@@ -199,24 +204,14 @@ class _DramaCommentsSheetState extends State {
);
}
if (_controller.error != null && _controller.comments.isEmpty) {
- return Center(
+ return EmptyState(
key: const Key('comment-list-error'),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- errorText(l10n, _controller.error, l10n.commentsLoadFailed),
- textAlign: TextAlign.center,
- style: const TextStyle(color: AppColors.muted),
- ),
- const SizedBox(height: 8),
- TextButton(
- key: const Key('comment-list-retry'),
- onPressed: _controller.load,
- child: Text(l10n.retry),
- ),
- ],
- ),
+ icon: Icons.cloud_off_rounded,
+ title: errorText(l10n, _controller.error, l10n.commentsLoadFailed),
+ actionLabel: l10n.retry,
+ actionKey: const Key('comment-list-retry'),
+ onAction: _controller.load,
+ compact: true,
);
}
if (_controller.comments.isEmpty) {
@@ -272,7 +267,7 @@ class _DramaCommentsSheetState extends State {
l10n.replyingTo(
replyTo.nickname.isEmpty ? l10n.anonymousUser : replyTo.nickname,
),
- style: const TextStyle(color: AppColors.blueSoft, fontSize: 12),
+ style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
),
IconButton(
@@ -286,6 +281,25 @@ class _DramaCommentsSheetState extends State {
),
);
+ InputDecoration _composerDecoration(String hint) {
+ const radius = BorderRadius.all(Radius.circular(_composerRadius));
+ const clear = OutlineInputBorder(
+ borderRadius: radius,
+ borderSide: BorderSide.none,
+ );
+ return InputDecoration(
+ hintText: hint,
+ hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14),
+ counterText: '',
+ filled: true,
+ fillColor: AppColors.raised,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ border: clear,
+ enabledBorder: clear,
+ focusedBorder: clear,
+ );
+ }
+
Widget _composer(AppLocalizations l10n) => Row(
children: [
Expanded(
@@ -295,16 +309,9 @@ class _DramaCommentsSheetState extends State {
focusNode: _inputFocus,
maxLength: _maxCommentLength,
maxLines: 1,
- decoration: InputDecoration(
- hintText: l10n.writeComment,
- counterText: '',
- filled: true,
- fillColor: const Color(0xFF35353A),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(14),
- borderSide: BorderSide.none,
- ),
- ),
+ style: const TextStyle(color: AppColors.cream, fontSize: 14),
+ cursorColor: AppColors.gold,
+ decoration: _composerDecoration(l10n.writeComment),
onSubmitted: (_) => _post(),
),
),
@@ -387,7 +394,7 @@ class _CommentThread extends StatelessWidget {
],
),
),
- const Divider(height: 1),
+ const Divider(height: 1, color: AppColors.outlineVariantDark),
],
);
}
diff --git a/lib/pages/player/widgets/episode_picker_cell.dart b/lib/pages/player/widgets/episode_picker_cell.dart
new file mode 100644
index 0000000..b5069b7
--- /dev/null
+++ b/lib/pages/player/widgets/episode_picker_cell.dart
@@ -0,0 +1,116 @@
+// Author: fruit.com.cn
+
+import 'package:flutter/material.dart';
+
+import '../../../core/data/models/drama.dart';
+import '../../../core/theme/app_colors.dart';
+
+const double _cellRadius = 8;
+const double _numberSize = 15;
+const double _lockSize = 16;
+const double _lockGap = 4;
+const double _lockWeight = 300;
+const double _coverAlpha = 0.45;
+
+/// 选集格子。需要解锁时铺一层透明底,锁在上,集数在锁下面。
+class EpisodePickerCell extends StatelessWidget {
+ const EpisodePickerCell({
+ required this.episode,
+ required this.selected,
+ required this.lockLabel,
+ required this.onTap,
+ super.key,
+ });
+
+ final Episode episode;
+ final bool selected;
+ final String lockLabel;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ clipBehavior: Clip.antiAlias,
+ color: selected ? AppColors.bluePressed : AppColors.raised,
+ borderRadius: BorderRadius.circular(_cellRadius),
+ child: InkWell(
+ onTap: onTap,
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ if (!episode.locked) Center(child: _EpisodeNumber(episode.number)),
+ if (episode.locked)
+ _LockCover(
+ episodeId: episode.id,
+ number: episode.number,
+ label: lockLabel,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _EpisodeNumber extends StatelessWidget {
+ const _EpisodeNumber(this.number);
+
+ final int number;
+
+ @override
+ Widget build(BuildContext context) {
+ return Text(
+ '$number',
+ style: const TextStyle(
+ color: AppColors.cream,
+ fontSize: _numberSize,
+ fontWeight: FontWeight.w600,
+ height: 1,
+ ),
+ );
+ }
+}
+
+/// 透明底铺满格子,锁在上,集数在锁下面。
+class _LockCover extends StatelessWidget {
+ const _LockCover({
+ required this.episodeId,
+ required this.number,
+ required this.label,
+ });
+
+ final String episodeId;
+ final int number;
+ final String label;
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ fit: StackFit.expand,
+ children: [
+ ColoredBox(
+ key: ValueKey('episode-lock-cover-$episodeId'),
+ color: AppColors.ink.withValues(alpha: _coverAlpha),
+ ),
+ Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.lock_outline,
+ key: ValueKey('episode-lock-$episodeId'),
+ size: _lockSize,
+ weight: _lockWeight,
+ fill: 0,
+ color: AppColors.gold,
+ semanticLabel: label,
+ ),
+ const SizedBox(height: _lockGap),
+ _EpisodeNumber(number),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart
index e66b6ff..1a1cf1a 100644
--- a/lib/pages/player/widgets/seek_progress_bar.dart
+++ b/lib/pages/player/widgets/seek_progress_bar.dart
@@ -120,7 +120,7 @@ class _SeekProgressBarState extends State {
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: scrubbing ? 4 : 3,
- activeTrackColor: AppColors.blueSoft,
+ activeTrackColor: AppColors.gold,
inactiveTrackColor: Colors.white30,
thumbColor: Colors.white,
thumbShape: RoundSliderThumbShape(
diff --git a/lib/pages/splash/splash_page.dart b/lib/pages/splash/splash_page.dart
new file mode 100644
index 0000000..2eac25c
--- /dev/null
+++ b/lib/pages/splash/splash_page.dart
@@ -0,0 +1,53 @@
+// Author: fruit.com.cn
+
+import 'package:flutter/material.dart';
+
+import '../../core/theme/app_colors.dart';
+
+const double _markSize = 84;
+const double _markRadius = 18;
+const double _loaderSize = 22;
+const double _loaderStroke = 2.5;
+const String _markAsset = 'assets/icon/splash_mark.png';
+
+/// 系统开屏已经放好同一块底和图标。这一页接上加载,直到首页数据回来。
+class SplashPage extends StatelessWidget {
+ const SplashPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ColoredBox(
+ key: Key('splash-screen'),
+ color: AppColors.ink,
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Center(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(_markRadius),
+ child: const Image(
+ image: AssetImage(_markAsset),
+ width: _markSize,
+ height: _markSize,
+ filterQuality: FilterQuality.medium,
+ gaplessPlayback: true,
+ ),
+ ),
+ ),
+ Align(
+ alignment: Alignment(0, 0.24),
+ child: SizedBox(
+ width: _loaderSize,
+ height: _loaderSize,
+ child: CircularProgressIndicator(
+ key: Key('splash-loading'),
+ strokeWidth: _loaderStroke,
+ color: AppColors.gold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart
index 130efc6..4ad5fa8 100644
--- a/lib/pages/watch_records/watch_records_page.dart
+++ b/lib/pages/watch_records/watch_records_page.dart
@@ -35,9 +35,9 @@ class WatchRecordsPage extends StatelessWidget {
data: AppTheme.dark(),
child: Scaffold(
key: const Key('watch-records-page'),
- backgroundColor: const Color(0xFF0B0C10),
+ backgroundColor: AppColors.ink,
appBar: AppBar(
- backgroundColor: const Color(0xFF0B0C10),
+ backgroundColor: AppColors.ink,
foregroundColor: Colors.white,
title: Text(l10n.watchHistory),
),
@@ -359,26 +359,12 @@ class _FailureState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
- return Center(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- message,
- textAlign: TextAlign.center,
- style: const TextStyle(color: Colors.white70),
- ),
- const SizedBox(height: 14),
- FilledButton(
- key: const Key('watch-records-retry'),
- onPressed: () => unawaited(onRetry()),
- child: Text(l10n.retry),
- ),
- ],
- ),
- ),
+ return EmptyState(
+ icon: Icons.cloud_off_rounded,
+ title: message,
+ actionLabel: l10n.retry,
+ actionKey: const Key('watch-records-retry'),
+ onAction: () => unawaited(onRetry()),
);
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 02bee69..3c31839 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -79,6 +79,8 @@ flutter:
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
+ assets:
+ - assets/icon/splash_mark.png
# To add assets to your application, add an assets section, like this:
# assets:
@@ -115,6 +117,6 @@ flutter_launcher_icons:
android: true
ios: true
image_path: assets/icon/fruitshorts.png
- adaptive_icon_background: "#D74A7D"
+ adaptive_icon_background: "#C4322A"
adaptive_icon_foreground: assets/icon/fruitshorts.png
remove_alpha_ios: true
diff --git a/test/catalog/catalog_controller_test.dart b/test/catalog/catalog_controller_test.dart
index 9c6b6cf..d5fbf83 100644
--- a/test/catalog/catalog_controller_test.dart
+++ b/test/catalog/catalog_controller_test.dart
@@ -11,7 +11,9 @@ void main() {
final repository = ScriptedCatalogRepository();
final controller = CatalogController(repository);
+ expect(controller.settled, isFalse);
await controller.load();
+ expect(controller.settled, isTrue);
expect(controller.dramas, isEmpty);
expect(controller.error, isNull);
diff --git a/test/comment/drama_comments_sheet_test.dart b/test/comment/drama_comments_sheet_test.dart
index c2b2b21..4c5d436 100644
--- a/test/comment/drama_comments_sheet_test.dart
+++ b/test/comment/drama_comments_sheet_test.dart
@@ -12,6 +12,7 @@ import 'package:fruitshorts/core/data/repositories/interaction_repository.dart';
import 'package:fruitshorts/core/data/services/auth_controller.dart';
import 'package:fruitshorts/core/data/services/auth_session_store.dart';
import 'package:fruitshorts/core/platform/haptics.dart';
+import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/widgets/empty_state.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/player/widgets/drama_comments_sheet.dart';
@@ -352,7 +353,7 @@ Future _pumpSheet(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Scaffold(
- backgroundColor: Color(0xFF232328),
+ backgroundColor: AppColors.ink,
body: DramaCommentsSheet(drama: _drama),
),
),
diff --git a/test/discover/home_all_dramas_grid_test.dart b/test/discover/home_all_dramas_grid_test.dart
index b6d577f..a91ba2d 100644
--- a/test/discover/home_all_dramas_grid_test.dart
+++ b/test/discover/home_all_dramas_grid_test.dart
@@ -148,6 +148,15 @@ void main() {
expect(find.byKey(const Key('home-all-dramas-title')), findsNothing);
expect(find.text('No matching dramas.'), findsOneWidget);
+ final content = tester.getCenter(
+ find.byKey(const Key('empty-state-content')),
+ );
+ final chipsBottom = tester
+ .getBottomLeft(find.byKey(const Key('home-category-chips')))
+ .dy;
+ final feed = tester.getRect(find.byType(CustomScrollView));
+ expect(content.dx, closeTo(feed.center.dx, 1));
+ expect(content.dy, closeTo((chipsBottom + feed.bottom) / 2, 36));
await tester.tap(find.text('Romance'));
await tester.pumpAndSettle();
@@ -168,6 +177,10 @@ void main() {
expect(find.byKey(const Key('home-all-dramas-title')), findsNothing);
expect(find.text('Categories could not be loaded.'), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
+ expect(find.byKey(const Key('empty-state-icon')), findsOneWidget);
+ final icon = tester.getCenter(find.byKey(const Key('empty-state-icon')));
+ final feed = tester.getRect(find.byType(CustomScrollView));
+ expect(icon.dx, closeTo(feed.center.dx, 1));
expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing);
});
diff --git a/test/empty_state/empty_state_test.dart b/test/empty_state/empty_state_test.dart
index 6f9b730..8f1588e 100644
--- a/test/empty_state/empty_state_test.dart
+++ b/test/empty_state/empty_state_test.dart
@@ -22,34 +22,19 @@ void main() {
),
);
- expect(find.byKey(const Key('empty-state-art')), findsOneWidget);
- expect(find.byKey(const Key('empty-state-art-inner')), findsOneWidget);
+ expect(find.byKey(const Key('empty-state-art')), findsNothing);
+ expect(find.byKey(const Key('empty-state-art-inner')), findsNothing);
expect(find.byKey(const Key('empty-state-icon')), findsOneWidget);
expect(find.text('No stories'), findsOneWidget);
expect(find.text('New content will appear here.'), findsOneWidget);
- expect(
- tester.getSize(find.byKey(const Key('empty-state-art'))),
- const Size(84, 84),
- );
- expect(
- tester.getSize(find.byKey(const Key('empty-state-art-inner'))),
- const Size(56, 56),
- );
expect(
tester.getSize(find.byKey(const Key('empty-state-icon'))),
- const Size(27, 27),
+ const Size(48, 48),
);
- final outer = tester.widget(
- find.byKey(const Key('empty-state-art')),
+ expect(
+ tester.widget(find.byKey(const Key('empty-state-icon'))).color,
+ AppColors.gold,
);
- final outerDecoration = outer.decoration! as BoxDecoration;
- expect(outerDecoration.shape, BoxShape.circle);
- expect(outerDecoration.color?.a, closeTo(0.055, 0.001));
- expect(outerDecoration.border!.top.color.a, closeTo(0.11, 0.001));
- final inner = tester.widget(
- find.byKey(const Key('empty-state-art-inner')),
- );
- expect((inner.decoration! as BoxDecoration).color?.a, closeTo(0.11, 0.001));
expect(
tester
.widget(find.byKey(const Key('empty-state-padding')))
@@ -97,18 +82,11 @@ void main() {
),
);
- expect(
- tester.getSize(find.byKey(const Key('empty-state-art'))),
- const Size(60, 60),
- );
- expect(
- tester.getSize(find.byKey(const Key('empty-state-art-inner'))),
- const Size(40, 40),
- );
expect(
tester.getSize(find.byKey(const Key('empty-state-icon'))),
- const Size(21, 21),
+ const Size(32, 32),
);
+ expect(find.byType(DecoratedBox), findsNothing);
expect(
tester
.widget(find.byKey(const Key('empty-state-title')))
@@ -125,7 +103,7 @@ void main() {
expect(find.byKey(const Key('empty-state-action')), findsNothing);
});
- testWidgets('should_use_theme_aware_icon_colors', (tester) async {
+ testWidgets('should_tint_the_bare_icon_for_light_and_dark', (tester) async {
await _pump(
tester,
theme: AppTheme.dark(),
@@ -133,7 +111,7 @@ void main() {
);
expect(
tester.widget(find.byKey(const Key('empty-state-icon'))).color,
- AppColors.blueSoft,
+ AppColors.gold,
);
await _pump(
diff --git a/test/player/episode_picker_cell_test.dart b/test/player/episode_picker_cell_test.dart
new file mode 100644
index 0000000..196feec
--- /dev/null
+++ b/test/player/episode_picker_cell_test.dart
@@ -0,0 +1,73 @@
+// Author: fruit.com.cn
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fruitshorts/core/data/models/drama.dart';
+import 'package:fruitshorts/pages/player/widgets/episode_picker_cell.dart';
+
+void main() {
+ testWidgets('should_show_lock_when_episode_needs_unlock', (tester) async {
+ await tester.pumpWidget(
+ _host(
+ const Episode(
+ id: '12',
+ number: 12,
+ title: 'Paid',
+ isFree: false,
+ locked: true,
+ ),
+ ),
+ );
+
+ expect(find.byIcon(Icons.lock_outline), findsOneWidget);
+ expect(find.text('12'), findsOneWidget);
+ final cell = tester.getRect(find.byType(EpisodePickerCell));
+ final cover = tester.getRect(find.byKey(const ValueKey('episode-lock-cover-12')));
+ final number = tester.getRect(find.text('12'));
+ final lock = tester.getRect(find.byIcon(Icons.lock_outline));
+ expect(number.overlaps(lock), isFalse);
+ expect(number.top, greaterThanOrEqualTo(lock.bottom));
+ final wash = tester.widget(
+ find.byKey(const ValueKey('episode-lock-cover-12')),
+ );
+ expect(cover.width, greaterThanOrEqualTo(cell.width - 1));
+ expect(cover.height, greaterThanOrEqualTo(cell.height - 1));
+ expect(cover.contains(lock.center), isTrue);
+ expect(wash.color.a, greaterThan(0));
+ expect(wash.color.a, lessThan(1));
+ });
+
+ testWidgets('should_hide_lock_when_episode_is_free', (tester) async {
+ await tester.pumpWidget(
+ _host(
+ const Episode(
+ id: '1',
+ number: 1,
+ title: 'Free',
+ isFree: true,
+ ),
+ ),
+ );
+
+ expect(find.byIcon(Icons.lock_outline), findsNothing);
+ expect(find.byKey(const ValueKey('episode-lock-cover-1')), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
+
+Widget _host(Episode episode) {
+ return MaterialApp(
+ home: Scaffold(
+ body: SizedBox(
+ width: 52,
+ height: 50,
+ child: EpisodePickerCell(
+ episode: episode,
+ selected: false,
+ lockLabel: 'Locked',
+ onTap: () {},
+ ),
+ ),
+ ),
+ );
+}
diff --git a/test/splash/splash_gate_test.dart b/test/splash/splash_gate_test.dart
new file mode 100644
index 0000000..0806c82
--- /dev/null
+++ b/test/splash/splash_gate_test.dart
@@ -0,0 +1,59 @@
+// Author: fruit.com.cn
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fruitshorts/core/data/models/drama.dart';
+import 'package:fruitshorts/core/data/models/home_feed.dart';
+import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
+import 'package:fruitshorts/core/data/services/catalog_controller.dart';
+import 'package:fruitshorts/l10n/app_localizations.dart';
+import 'package:fruitshorts/pages/home/home_view.dart';
+import 'package:provider/provider.dart';
+
+void main() {
+ testWidgets(
+ 'should_keep_splash_with_loading_until_home_feed_settles',
+ (tester) async {
+ final repository = _PendingCatalog();
+ await tester.pumpWidget(
+ MaterialApp(
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ home: ChangeNotifierProvider(
+ create: (_) => CatalogController(repository)..load(),
+ child: const HomePage(),
+ ),
+ ),
+ );
+
+ expect(find.byKey(const Key('splash-screen')), findsOneWidget);
+ expect(find.byKey(const Key('splash-loading')), findsOneWidget);
+ expect(find.byKey(const Key('home-empty')), findsNothing);
+ final clip = tester.widget(find.byType(ClipRRect));
+ expect(clip.borderRadius, BorderRadius.circular(18));
+
+ repository.pending.complete(const HomeFeed());
+ await tester.pump();
+ await tester.pump();
+
+ expect(find.byKey(const Key('splash-screen')), findsNothing);
+ expect(find.byKey(const Key('home-empty')), findsOneWidget);
+ },
+ );
+}
+
+class _PendingCatalog implements CatalogRepository {
+ final Completer pending = Completer();
+
+ @override
+ Future fetchHomeFeed() => pending.future;
+
+ @override
+ Future fetchDiscoverFeed() => pending.future;
+
+ @override
+ Future fetchDrama(String dramaId) =>
+ Future.error(StateError('unused'));
+}
diff --git a/test/theme/app_theme_test.dart b/test/theme/app_theme_test.dart
index c556b6b..ba429ff 100644
--- a/test/theme/app_theme_test.dart
+++ b/test/theme/app_theme_test.dart
@@ -14,11 +14,16 @@ double _contrast(Color foreground, Color background) {
}
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));
+ test('should_expose_indonesia_evening_brand_tokens', () {
+ expect(AppColors.blue, const Color(0xFFC4322A));
+ expect(AppColors.bluePressed, const Color(0xFF8E221C));
+ expect(AppColors.blueSoft, const Color(0xFFFF8A72));
+ expect(AppColors.gold, const Color(0xFFF6C453));
expect(AppColors.onPrimary, const Color(0xFFFFFFFF));
+ expect(
+ _contrast(AppColors.onGold, AppColors.gold),
+ greaterThanOrEqualTo(4.5),
+ );
});
test('should_meet_text_and_icon_contrast_when_using_theme_tokens', () {
@@ -33,7 +38,7 @@ void main() {
);
});
- test('should_use_klein_blue_in_light_and_dark_color_schemes', () {
+ test('should_use_chili_brand_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);
@@ -52,7 +57,7 @@ void main() {
expect(light.outlineVariant.computeLuminance(), lessThan(0.92));
});
- testWidgets('should_expose_klein_blue_through_widget_theme', (tester) async {
+ testWidgets('should_expose_chili_brand_through_widget_theme', (tester) async {
late ThemeData applied;
await tester.pumpWidget(
MaterialApp(
diff --git a/test/widget_test.dart b/test/widget_test.dart
index 3efc430..7732b90 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -278,7 +278,7 @@ void main() {
},
);
- testWidgets('should_fit_compact_dark_viewport_when_klein_theme_is_used', (
+ testWidgets('should_fit_compact_dark_viewport_when_evening_theme_is_used', (
tester,
) async {
tester.view.physicalSize = const Size(320, 568);