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);