feat(flutter): 应用浅色暖橙视觉主题

This commit is contained in:
Zhou Weidong committed 2026-10-20 10:15:03 +08:00
1 parent 0d2367431d
commit 634b63e16c
56 files changed
+941 -409

No files matched your search

+4 -4
View File
@@ -251,14 +251,14 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
localeResolutionCallback: (deviceLocale, _) => localeResolutionCallback: (deviceLocale, _) =>
resolveAppLocale(deviceLocale), resolveAppLocale(deviceLocale),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
themeMode: ThemeMode.dark, themeMode: ThemeMode.light,
theme: AppTheme.light(), theme: AppTheme.light(),
darkTheme: AppTheme.dark(), darkTheme: AppTheme.light(),
builder: (context, child) => AnnotatedRegion<SystemUiOverlayStyle>( builder: (context, child) => AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light.copyWith( value: SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
systemNavigationBarColor: AppColors.bar, systemNavigationBarColor: AppColors.bar,
systemNavigationBarIconBrightness: Brightness.light, systemNavigationBarIconBrightness: Brightness.dark,
), ),
child: child ?? const SizedBox.shrink(), child: child ?? const SizedBox.shrink(),
), ),
+72 -49
View File
@@ -2,79 +2,102 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Obsidian Flare palette from `docs/design/README.md` and screens 01/02. /// K28-S 浅色暖橙主题色板。
/// ///
/// Existing semantic names (`blue`, `blueSoft`, `ink`, ...) are kept so page /// 保留既有语义名,避免页面大范围改名;实际颜色统一由本文件控制。
/// code does not need a mass rename; their values now follow the design tokens.
abstract final class AppColors { abstract final class AppColors {
/// Primary CTA, selected navigation, playback progress. /// 主操作、选中态、播放进度。
static const blue = Color(0xFFFF5B3D); static const blue = Color(0xFFFF7A1A);
static const primary = blue; static const primary = blue;
/// Pressed state for the primary CTA. /// 主操作按下态。
static const bluePressed = Color(0xFFFF4321); static const bluePressed = Color(0xFFFF6A00);
/// Secondary accent for tags, locks, and highlighted icons. /// 粉色辅助强调色。
static const blueSoft = Color(0xFF9278EA); static const blueSoft = Color(0xFFFF6A8A);
static const secondary = blueSoft; static const secondary = blueSoft;
static const lavender = blueSoft; static const lavender = blueSoft;
/// Coins and rewards. /// 金币与奖励。
static const gold = Color(0xFFFBBF24); static const gold = Color(0xFFF3B34C);
/// Success and free-state accents. /// 成功状态。
static const success = Color(0xFF34D399); static const success = Color(0xFF2FA873);
/// Liked hearts use the primary accent. /// 收藏和喜欢使用粉色强调。
static const favorite = blue; static const favorite = secondary;
/// Text placed on [gold]. /// 金币底色上的文字。
static const onGold = Color(0xFF2A1810); static const onGold = Color(0xFF5A3511);
/// Text and icons placed directly on [blue]. /// 主色上的文字与图标。
static const onPrimary = Color(0xFFFFFFFF); static const onPrimary = Color(0xFFFFFFFF);
/// Page background. /// 页面背景。
static const ink = Color(0xFF0B0C10); static const ink = Color(0xFFFFF8F0);
/// Bottom navigation background (rendered at 95% with blur). /// 页面顶部暖色渐变起点。
static const bar = Color(0xFF0D0D11); static const surfaceTop = Color(0xFFFFE9D6);
/// Cards lifted off [ink]. /// 底部导航底色。
static const card = Color(0xFF1B1C21); static const bar = Color(0xFFFFFDFB);
/// Alternate card surface from the mobile preview. /// 中央操作按钮渐变。
static const cardAlt = Color(0xFF1C1D21); static const navCenterStart = Color(0xFFFF9A3D);
static const navCenterEnd = Color(0xFFFF6A00);
static const navCenterShadow = Color(0x33FF7A1A);
/// Floating surfaces, inputs, and nested rows. /// 卡片轻阴影。
static const raised = Color(0xFF1C1D21); static const cardShadow = Color(0x14FF8A3D);
static const quick = Color(0xFF202126);
static const input = Color(0xFF25252A);
static const goldCard = Color(0xFF201B1E);
/// Body text on dark surfaces. /// 卡片与弹层。
static const cream = Color(0xFFE6E1E3); static const card = Color(0xFFFFFFFF);
/// Titles and high-emphasis text. /// 次要卡片表面。
static const onSurface = Color(0xFFFFFFFF); static const cardAlt = Color(0xFFFFF4E9);
/// Secondary text. /// 浮层、输入框和嵌套区域。
static const muted = Color(0xFFD2CDD0); static const raised = Color(0xFFFFF1E5);
static const quick = Color(0xFFFFF0E1);
static const input = Color(0xFFFFF4EA);
static const goldCard = Color(0xFFFFF4DF);
/// Weak hints and timestamps. /// 正文颜色。
static const subtle = Color(0xFF9A96A3); static const cream = Color(0xFF1F1B16);
/// Soft orange used for continue labels and secondary times. /// 主文字。
static const accent = Color(0xFFFFA490); static const onSurface = Color(0xFF1F1B16);
/// 8% white border used by cards and inputs. /// 次要文字。
static const border = Color(0x14FFFFFF); static const muted = Color(0xFF8C837B);
/// 14% white border used by glass controls. /// 弱提示与时间。
static const glassBorder = Color(0x24FFFFFF); static const subtle = Color(0xFFB3AAA1);
static const outlineDark = Color(0xFF2A2C36); /// 橙色强调文案。
static const outlineVariantDark = Color(0xFF242329); static const accent = blue;
static const outlineLight = Color(0xFF8E8E93);
static const outlineVariantLight = Color(0xFFE4E4E7); /// 卡片描边。
static const border = Color(0xFFF0E4D6);
/// 玻璃面板描边。
static const glassBorder = Color(0x66FFFFFF);
/// 粉色角标。
static const badgePink = Color(0xFFFFE3EC);
static const badgePinkText = Color(0xFFFF6A8A);
/// 封面锁定数量与视频遮罩。
static const overlayDark = Color(0x33000000);
/// 封面集数胶囊。
static const badgeBeige = Color(0xFFFFF0DF);
/// 导航未选中。
static const navUnselected = muted;
static const outlineDark = border;
static const outlineVariantDark = border;
static const outlineLight = muted;
static const outlineVariantLight = border;
} }
+21 -14
View File
@@ -1,6 +1,6 @@
// Author: fruit.com.cn // Author: fruit.com.cn
/// Cross-page spacing, type, radius, and control sizes. /// 跨页面复用的间距、字号、圆角和控件尺寸。
abstract final class AppDimens { abstract final class AppDimens {
static const double spaceXs = 4; static const double spaceXs = 4;
static const double spaceSm = 8; static const double spaceSm = 8;
@@ -9,17 +9,18 @@ abstract final class AppDimens {
static const double spaceXl = 24; static const double spaceXl = 24;
static const double pageHorizontal = 16; static const double pageHorizontal = 16;
static const double cardRadius = 12; static const double cardRadius = 16;
static const double authCardRadius = 20; static const double authCardRadius = 20;
static const double cardPadding = 16; static const double cardPadding = 16;
static const double innerRadius = 10; static const double innerRadius = 12;
static const double innerPadding = 12; static const double innerPadding = 12;
static const double pillRadius = 20; static const double pillRadius = 999;
static const double glassSmallRadius = 12; static const double glassSmallRadius = 16;
static const double glassRadius = 14; static const double glassRadius = 20;
static const double glassCardRadius = 14; static const double glassCardRadius = 20;
static const double buttonRadius = 9; static const double buttonRadius = 999;
static const double coverRadius = 10; static const double coverRadius = 16;
static const double sheetRadius = 20;
static const double caption = 12; static const double caption = 12;
static const double emptyStateSubtitle = 12.5; static const double emptyStateSubtitle = 12.5;
@@ -33,7 +34,7 @@ abstract final class AppDimens {
static const double previewCardTitle = 13; static const double previewCardTitle = 13;
static const double previewSectionTitle = 14; static const double previewSectionTitle = 14;
static const double chipFont = 10; static const double chipFont = 10;
static const double navLabel = 9; static const double navLabel = 10;
static const double buttonHeight = 48; static const double buttonHeight = 48;
static const double primaryButtonHeight = 52; static const double primaryButtonHeight = 52;
@@ -44,8 +45,10 @@ abstract final class AppDimens {
static const double tapTarget = 44; static const double tapTarget = 44;
static const double contentHeaderHeight = 46; static const double contentHeaderHeight = 46;
static const double navHeight = 54; static const double navHeight = 64;
static const double navIcon = 17; static const double navIcon = 20;
static const double navCenter = 56;
static const double navCenterIcon = 28;
static const double progressHeight = 3; static const double progressHeight = 3;
static const double progressRadius = 2; static const double progressRadius = 2;
static const double progressLineHeight = 3; static const double progressLineHeight = 3;
@@ -60,7 +63,11 @@ abstract final class AppDimens {
static const double watchPlaceholderHeight = 114; static const double watchPlaceholderHeight = 114;
static const double heroBannerHeight = 176; static const double heroBannerHeight = 176;
static const double heroBannerRadius = 20;
static const double cardMeta = 9; static const double cardMeta = 12;
static const double cardViews = 9; static const double cardViews = 11;
static const double cardTitle = 15;
static const double cardShadowBlur = 18;
static const double cardShadowY = 6;
} }
+79 -86
View File
@@ -6,79 +6,62 @@ import 'package:flutter/services.dart';
import 'app_colors.dart'; import 'app_colors.dart';
import 'app_dimens.dart'; import 'app_dimens.dart';
/// Obsidian Flare themes. Dark is the product default; light mirrors the same /// K28-S 浅色暖橙主题。
/// orange primary for system-light devices.
abstract final class AppTheme { abstract final class AppTheme {
static const _lightBackground = Color(0xFFF7F7F8); /// 页面顶部暖色到米白背景的渐变。
static const _darkBackground = AppColors.ink; static const LinearGradient pageGradient = LinearGradient(
begin: Alignment.topCenter,
/// Light theme for system light mode. end: Alignment.bottomCenter,
static ThemeData light() => _build( colors: [AppColors.surfaceTop, AppColors.ink],
brightness: Brightness.light, stops: [0, 0.24],
surface: _lightBackground,
accent: AppColors.blue,
); );
/// Dark theme for system dark mode. /// 卡片轻阴影。
static ThemeData dark() => _build( static const BoxShadow cardShadow = BoxShadow(
brightness: Brightness.dark, color: AppColors.cardShadow,
surface: _darkBackground, blurRadius: AppDimens.cardShadowBlur,
accent: AppColors.blue, offset: Offset(0, AppDimens.cardShadowY),
); );
static ThemeData _build({ /// 全局浅色暖橙主题。
required Brightness brightness, static ThemeData light() {
required Color surface, const scheme = ColorScheme.light(
required Color accent, primary: AppColors.blue,
}) { onPrimary: AppColors.onPrimary,
final dark = brightness == Brightness.dark; secondary: AppColors.secondary,
final scheme = dark onSecondary: AppColors.onPrimary,
? const ColorScheme.dark( primaryContainer: AppColors.surfaceTop,
primary: AppColors.blue, onPrimaryContainer: AppColors.onSurface,
onPrimary: AppColors.onPrimary, secondaryContainer: AppColors.badgePink,
secondary: AppColors.secondary, onSecondaryContainer: AppColors.badgePinkText,
onSecondary: AppColors.onPrimary, surface: AppColors.ink,
primaryContainer: AppColors.bluePressed, onSurface: AppColors.onSurface,
onPrimaryContainer: AppColors.onPrimary, outline: AppColors.border,
secondaryContainer: AppColors.raised, outlineVariant: AppColors.border,
onSecondaryContainer: AppColors.onPrimary, surfaceTint: AppColors.blue,
surface: _darkBackground, inversePrimary: AppColors.bluePressed,
onSurface: AppColors.onSurface, );
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
)
: ColorScheme.light(
primary: AppColors.blue,
onPrimary: AppColors.onPrimary,
secondary: AppColors.secondary,
onSecondary: AppColors.onPrimary,
primaryContainer: AppColors.blue.withValues(alpha: 0.14),
onPrimaryContainer: AppColors.ink,
secondaryContainer: AppColors.blueSoft.withValues(alpha: 0.14),
onSecondaryContainer: AppColors.ink,
surface: _lightBackground,
onSurface: AppColors.ink,
outline: AppColors.outlineLight,
outlineVariant: AppColors.outlineVariantLight,
surfaceTint: AppColors.blue,
inversePrimary: AppColors.blueSoft,
);
final base = ThemeData( final base = ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: brightness, brightness: Brightness.light,
colorScheme: scheme, colorScheme: scheme,
scaffoldBackgroundColor: surface, scaffoldBackgroundColor: AppColors.ink,
fontFamily: 'PlusJakartaSans', fontFamily: 'PlusJakartaSans',
progressIndicatorTheme: ProgressIndicatorThemeData(color: accent), progressIndicatorTheme: const ProgressIndicatorThemeData(
sliderTheme: SliderThemeData( color: AppColors.blue,
activeTrackColor: accent, ),
sliderTheme: const SliderThemeData(
activeTrackColor: AppColors.blue,
inactiveTrackColor: AppColors.border,
thumbColor: AppColors.onPrimary, thumbColor: AppColors.onPrimary,
), ),
tabBarTheme: TabBarThemeData(labelColor: accent, indicatorColor: accent), tabBarTheme: const TabBarThemeData(
labelColor: AppColors.blue,
indicatorColor: AppColors.blue,
unselectedLabelColor: AppColors.muted,
),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: accent), style: TextButton.styleFrom(foregroundColor: AppColors.blue),
), ),
filledButtonTheme: FilledButtonThemeData( filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle( style: ButtonStyle(
@@ -87,25 +70,26 @@ abstract final class AppTheme {
minimumSize: const WidgetStatePropertyAll( minimumSize: const WidgetStatePropertyAll(
Size(0, AppDimens.buttonHeight), Size(0, AppDimens.buttonHeight),
), ),
shape: const WidgetStatePropertyAll(StadiumBorder()),
), ),
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle( style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll( foregroundColor: const WidgetStatePropertyAll(AppColors.blue),
dark ? AppColors.blueSoft : AppColors.bluePressed,
),
minimumSize: const WidgetStatePropertyAll( minimumSize: const WidgetStatePropertyAll(
Size(0, AppDimens.buttonHeight), Size(0, AppDimens.buttonHeight),
), ),
side: WidgetStatePropertyAll( side: const WidgetStatePropertyAll(
BorderSide( BorderSide(color: AppColors.border),
color: dark ? AppColors.outlineDark : AppColors.outlineLight,
),
), ),
shape: const WidgetStatePropertyAll(StadiumBorder()),
), ),
), ),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle.light, backgroundColor: AppColors.ink,
foregroundColor: AppColors.onSurface,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle.dark,
titleTextStyle: TextStyle( titleTextStyle: TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontFamily: 'Outfit', fontFamily: 'Outfit',
@@ -113,6 +97,16 @@ abstract final class AppTheme {
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
), ),
iconTheme: const IconThemeData(color: AppColors.onSurface),
dividerTheme: const DividerThemeData(color: AppColors.border),
cardTheme: const CardThemeData(
color: AppColors.card,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(AppDimens.cardRadius)),
side: BorderSide(color: AppColors.border),
),
),
switchTheme: SwitchThemeData( switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith( thumbColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected) (states) => states.contains(WidgetState.selected)
@@ -120,36 +114,35 @@ abstract final class AppTheme {
: null, : null,
), ),
trackColor: WidgetStateProperty.resolveWith( trackColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected) ? accent : null, (states) =>
states.contains(WidgetState.selected) ? AppColors.blue : null,
), ),
), ),
snackBarTheme: SnackBarThemeData( snackBarTheme: const SnackBarThemeData(
backgroundColor: AppColors.bluePressed, backgroundColor: AppColors.onSurface,
contentTextStyle: const TextStyle(color: AppColors.onPrimary), contentTextStyle: TextStyle(color: AppColors.onPrimary),
), ),
); );
final textTheme = base.textTheme final textTheme = base.textTheme
.apply( .apply(
fontFamily: 'PlusJakartaSans', fontFamily: 'PlusJakartaSans',
bodyColor: dark ? AppColors.cream : AppColors.ink, bodyColor: AppColors.cream,
displayColor: dark ? AppColors.onSurface : AppColors.ink, displayColor: AppColors.onSurface,
) )
.copyWith( .copyWith(
displayLarge: _display(base.textTheme.displayLarge, dark), displayLarge: _display(base.textTheme.displayLarge),
displayMedium: _display(base.textTheme.displayMedium, dark), displayMedium: _display(base.textTheme.displayMedium),
displaySmall: _display(base.textTheme.displaySmall, dark), displaySmall: _display(base.textTheme.displaySmall),
headlineLarge: _display(base.textTheme.headlineLarge, dark), headlineLarge: _display(base.textTheme.headlineLarge),
headlineMedium: _display(base.textTheme.headlineMedium, dark), headlineMedium: _display(base.textTheme.headlineMedium),
headlineSmall: _display(base.textTheme.headlineSmall, dark), headlineSmall: _display(base.textTheme.headlineSmall),
titleLarge: _display(base.textTheme.titleLarge, dark), titleLarge: _display(base.textTheme.titleLarge),
); );
return base.copyWith(textTheme: textTheme); return base.copyWith(textTheme: textTheme);
} }
static TextStyle? _display(TextStyle? style, bool dark) => style?.copyWith( static TextStyle? _display(TextStyle? style) =>
fontFamily: 'Outfit', style?.copyWith(fontFamily: 'Outfit', color: AppColors.onSurface);
color: dark ? AppColors.onSurface : AppColors.ink,
);
static Color _buttonColor(Set<WidgetState> states) { static Color _buttonColor(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed) || if (states.contains(WidgetState.pressed) ||
+100 -22
View File
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import '../theme/app_dimens.dart'; import '../theme/app_dimens.dart';
/// One destination rendered by [AppBottomNav]. /// 底部导航普通目的地。
class AppBottomNavItem { class AppBottomNavItem {
const AppBottomNavItem({ const AppBottomNavItem({
required this.tabKey, required this.tabKey,
@@ -26,41 +26,69 @@ class AppBottomNavItem {
final bool selected; final bool selected;
} }
/// Shared bottom navigation used by the root shell and standalone tabs. /// 底部导航中央操作按钮。
class AppBottomNavCenterAction {
const AppBottomNavCenterAction({
required this.label,
required this.onTap,
this.key = const Key('nav-center-action'),
});
final Key key;
final String label;
final VoidCallback onTap;
}
/// 全局五槽底部导航。
class AppBottomNav extends StatelessWidget { class AppBottomNav extends StatelessWidget {
const AppBottomNav({ const AppBottomNav({
super.key, super.key,
required this.items, required this.items,
this.centerAction,
this.backgroundColor = AppColors.bar, this.backgroundColor = AppColors.bar,
this.barKey, this.barKey,
}); });
final List<AppBottomNavItem> items; final List<AppBottomNavItem> items;
final AppBottomNavCenterAction? centerAction;
final Color backgroundColor; final Color backgroundColor;
final Key? barKey; final Key? barKey;
@override @override
Widget build(BuildContext context) => ClipRect( Widget build(BuildContext context) {
child: BackdropFilter( final action = centerAction;
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), return ClipRect(
child: SafeArea( child: BackdropFilter(
top: false, filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container( child: SafeArea(
key: barKey, top: false,
height: AppDimens.navHeight, child: Container(
decoration: BoxDecoration( key: barKey,
color: backgroundColor.withValues(alpha: 0.95), height: AppDimens.navHeight,
border: const Border( decoration: BoxDecoration(
top: BorderSide(color: AppColors.outlineVariantDark), color: backgroundColor.withValues(alpha: 0.95),
border: const Border(top: BorderSide(color: AppColors.border)),
), ),
), child: action == null
child: Row( ? Row(
children: [for (final item in items) _NavDestination(item: item)], children: [
for (final item in items) _NavDestination(item: item),
],
)
: Row(
children: [
for (final item in items.take(2))
_NavDestination(item: item),
Expanded(child: _CenterDestination(action: action)),
for (final item in items.skip(2))
_NavDestination(item: item),
],
),
), ),
), ),
), ),
), );
); }
} }
class _NavDestination extends StatelessWidget { class _NavDestination extends StatelessWidget {
@@ -78,7 +106,7 @@ class _NavDestination extends StatelessWidget {
key: item.tabKey, key: item.tabKey,
onTap: item.onTap, onTap: item.onTap,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48), constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -91,7 +119,7 @@ class _NavDestination extends StatelessWidget {
size: AppDimens.navIcon, size: AppDimens.navIcon,
color: item.selected color: item.selected
? AppColors.blue ? AppColors.blue
: Colors.white.withValues(alpha: 0.45), : AppColors.navUnselected,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -104,7 +132,7 @@ class _NavDestination extends StatelessWidget {
fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500, fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500,
color: item.selected color: item.selected
? AppColors.blue ? AppColors.blue
: Colors.white.withValues(alpha: 0.45), : AppColors.navUnselected,
letterSpacing: 0, letterSpacing: 0,
), ),
), ),
@@ -115,3 +143,53 @@ class _NavDestination extends StatelessWidget {
), ),
); );
} }
class _CenterDestination extends StatelessWidget {
const _CenterDestination({required this.action});
final AppBottomNavCenterAction action;
@override
Widget build(BuildContext context) {
return Center(
child: Semantics(
button: true,
label: action.label,
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: Ink(
width: AppDimens.navCenter,
height: AppDimens.navCenter,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.navCenterStart, AppColors.navCenterEnd],
),
boxShadow: [
BoxShadow(
color: AppColors.navCenterShadow,
blurRadius: 14,
offset: Offset(0, 5),
),
],
),
child: InkWell(
key: action.key,
customBorder: const CircleBorder(),
onTap: action.onTap,
child: const Icon(
Icons.add_rounded,
color: AppColors.onPrimary,
size: AppDimens.navCenterIcon,
),
),
),
),
),
);
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import '../theme/app_dimens.dart'; import '../theme/app_dimens.dart';
/// Dark glass surface used by the home info card and floating controls. /// 首页信息卡与浮层控件使用的浅色玻璃面板。
class GlassPanel extends StatelessWidget { class GlassPanel extends StatelessWidget {
const GlassPanel({ const GlassPanel({
super.key, super.key,
@@ -38,7 +38,7 @@ class GlassPanel extends StatelessWidget {
const LinearGradient( const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0x24FFFFFF), Color(0x0DFFFFFF)], colors: [Color(0xE6FFFFFF), Color(0xB3FFFDFB)],
), ),
), ),
child: Padding(padding: padding ?? EdgeInsets.zero, child: child), child: Padding(padding: padding ?? EdgeInsets.zero, child: child),
+12
View File
@@ -82,6 +82,18 @@
"count": {"type": "int"} "count": {"type": "int"}
} }
}, },
"allEpisodesBadge": "All {count}",
"@allEpisodesBadge": {
"placeholders": {
"count": {"type": "int"}
}
},
"lockedEpisodeCount": "{count} locked",
"@lockedEpisodeCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"watchedPercent": "Watched {percent}%", "watchedPercent": "Watched {percent}%",
"@watchedPercent": { "@watchedPercent": {
"placeholders": { "placeholders": {
+12
View File
@@ -82,6 +82,18 @@
"count": {"type": "int"} "count": {"type": "int"}
} }
}, },
"allEpisodesBadge": "Semua {count}",
"@allEpisodesBadge": {
"placeholders": {
"count": {"type": "int"}
}
},
"lockedEpisodeCount": "{count} terkunci",
"@lockedEpisodeCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"watchedPercent": "Sudah ditonton {percent}%", "watchedPercent": "Sudah ditonton {percent}%",
"@watchedPercent": { "@watchedPercent": {
"placeholders": { "placeholders": {
+12
View File
@@ -460,6 +460,18 @@ abstract class AppLocalizations {
/// **'{count} views'** /// **'{count} views'**
String viewCountLabel(int count); String viewCountLabel(int count);
/// No description provided for @allEpisodesBadge.
///
/// In en, this message translates to:
/// **'All {count}'**
String allEpisodesBadge(int count);
/// No description provided for @lockedEpisodeCount.
///
/// In en, this message translates to:
/// **'{count} locked'**
String lockedEpisodeCount(int count);
/// No description provided for @watchedPercent. /// No description provided for @watchedPercent.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+10
View File
@@ -201,6 +201,16 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count views'; return '$count views';
} }
@override
String allEpisodesBadge(int count) {
return 'All $count';
}
@override
String lockedEpisodeCount(int count) {
return '$count locked';
}
@override @override
String watchedPercent(int percent) { String watchedPercent(int percent) {
return 'Watched $percent%'; return 'Watched $percent%';
+10
View File
@@ -201,6 +201,16 @@ class AppLocalizationsId extends AppLocalizations {
return '$count tayangan'; return '$count tayangan';
} }
@override
String allEpisodesBadge(int count) {
return 'Semua $count';
}
@override
String lockedEpisodeCount(int count) {
return '$count terkunci';
}
@override @override
String watchedPercent(int percent) { String watchedPercent(int percent) {
return 'Sudah ditonton $percent%'; return 'Sudah ditonton $percent%';
+10
View File
@@ -196,6 +196,16 @@ class AppLocalizationsZh extends AppLocalizations {
return '$count 次播放'; return '$count 次播放';
} }
@override
String allEpisodesBadge(int count) {
return '全 $count 集';
}
@override
String lockedEpisodeCount(int count) {
return '锁 $count';
}
@override @override
String watchedPercent(int percent) { String watchedPercent(int percent) {
return '已观看 $percent%'; return '已观看 $percent%';
+12
View File
@@ -82,6 +82,18 @@
"count": {"type": "int"} "count": {"type": "int"}
} }
}, },
"allEpisodesBadge": "全 {count} 集",
"@allEpisodesBadge": {
"placeholders": {
"count": {"type": "int"}
}
},
"lockedEpisodeCount": "锁 {count}",
"@lockedEpisodeCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"watchedPercent": "已观看 {percent}%", "watchedPercent": "已观看 {percent}%",
"@watchedPercent": { "@watchedPercent": {
"placeholders": { "placeholders": {
@@ -50,7 +50,7 @@ class _AccountDeactivatePageState extends State<AccountDeactivatePage> {
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
final canShow = signedIn || _logic.scheduledAt != null; final canShow = signedIn || _logic.scheduledAt != null;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('account-deactivate-page'), key: const Key('account-deactivate-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+1 -1
View File
@@ -50,7 +50,7 @@ class _AccountDevicesPageState extends State<AccountDevicesPage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('account-devices-page'), key: const Key('account-devices-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+1 -1
View File
@@ -51,7 +51,7 @@ class _AccountExportPageState extends State<AccountExportPage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('account-export-page'), key: const Key('account-export-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+1 -1
View File
@@ -71,7 +71,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('account-profile-page'), key: const Key('account-profile-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+4 -5
View File
@@ -141,7 +141,7 @@ class _AuthPageState extends State<AuthPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
@@ -332,7 +332,7 @@ class _AuthPageState extends State<AuthPage> {
Text( Text(
l10n.authLandingSubtitle, l10n.authLandingSubtitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF9A96A3), fontSize: 11), style: const TextStyle(color: AppColors.subtle, fontSize: 11),
), ),
const SizedBox(height: 22), const SizedBox(height: 22),
_AuthEntryButton( _AuthEntryButton(
@@ -358,7 +358,7 @@ class _AuthPageState extends State<AuthPage> {
key: const Key('auth-legal'), key: const Key('auth-legal'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF6B6772), color: AppColors.muted,
fontSize: 9, fontSize: 9,
height: 1.6, height: 1.6,
), ),
@@ -442,8 +442,7 @@ class _AuthEntryButton extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (icon != null) if (icon != null) Icon(icon, size: 16, color: foreground),
Icon(icon, size: 16, color: foreground),
if (icon != null) const SizedBox(width: 7), if (icon != null) const SizedBox(width: 7),
Flexible( Flexible(
child: Text( child: Text(
+2 -2
View File
@@ -7,7 +7,7 @@ import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
/// K28-A1 placeholder for the future creation workspace. /// K28-A1 创作页占位骨架。
class CreatePage extends StatelessWidget { class CreatePage extends StatelessWidget {
const CreatePage({super.key}); const CreatePage({super.key});
@@ -15,7 +15,7 @@ class CreatePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: SafeArea( child: SafeArea(
bottom: false, bottom: false,
child: Padding( child: Padding(
+1 -1
View File
@@ -27,7 +27,7 @@ class CategoryDramasPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('category-dramas-page'), key: const Key('category-dramas-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+6 -2
View File
@@ -108,9 +108,9 @@ class _DiscoverPageState extends State<DiscoverPage> {
), ),
); );
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, value: SystemUiOverlayStyle.dark,
child: Theme( child: Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: embedded child: embedded
? content ? content
: Scaffold(backgroundColor: AppColors.ink, body: content), : Scaffold(backgroundColor: AppColors.ink, body: content),
@@ -340,6 +340,10 @@ class _DiscoverPageState extends State<DiscoverPage> {
} }
Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( Widget _bottomNav(AppLocalizations l10n) => AppBottomNav(
centerAction: AppBottomNavCenterAction(
label: l10n.createTab,
onTap: () => _act(DiscoverPageAction.create),
),
items: [ items: [
AppBottomNavItem( AppBottomNavItem(
tabKey: const Key('discover-nav-home'), tabKey: const Key('discover-nav-home'),
@@ -88,12 +88,12 @@ class DiscoverHeader extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF6A5CFF), AppColors.blue], colors: [AppColors.secondary, AppColors.blue],
), ),
), ),
child: const Icon( child: const Icon(
Icons.person_rounded, Icons.person_rounded,
color: AppColors.onSurface, color: AppColors.onPrimary,
size: 18, size: 18,
), ),
), ),
@@ -124,7 +124,7 @@ class _HeaderIcon extends StatelessWidget {
onPressed: onTap, onPressed: onTap,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2C2D33), backgroundColor: AppColors.raised,
foregroundColor: AppColors.onSurface, foregroundColor: AppColors.onSurface,
), ),
icon: Icon(icon, size: 18), icon: Icon(icon, size: 18),
+123 -24
View File
@@ -5,11 +5,15 @@ import 'package:flutter/material.dart';
import '../../../core/config/app_config.dart'; import '../../../core/config/app_config.dart';
import '../../../core/data/models/drama.dart'; import '../../../core/data/models/drama.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_theme.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const int _columns = 2; const int _columns = 2;
const double _columnGap = 10; const double _columnGap = 10;
const double _cardGap = 10; const double _cardGap = 10;
const double _addSize = 28;
const double _addIconSize = 18;
/// 剧场/分类页共用的双列剧目网格。 /// 剧场/分类页共用的双列剧目网格。
class DramaGrid extends StatelessWidget { class DramaGrid extends StatelessWidget {
@@ -71,19 +75,33 @@ class DramaGridCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return Container(
key: Key('home-drama-card-${drama.id}'), decoration: BoxDecoration(
onTap: onTap, color: AppColors.card,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(AppDimens.cardRadius),
child: Column( boxShadow: const [AppTheme.cardShadow],
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Material(
_cover(), color: Colors.transparent,
const SizedBox(height: 5), borderRadius: BorderRadius.circular(AppDimens.cardRadius),
_title(), clipBehavior: Clip.antiAlias,
const SizedBox(height: 2), child: InkWell(
_genre(), key: Key('home-drama-card-${drama.id}'),
], onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(AppDimens.spaceSm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_cover(),
const SizedBox(height: AppDimens.spaceSm),
_title(),
const SizedBox(height: AppDimens.spaceXs),
_footer(),
],
),
),
),
), ),
); );
} }
@@ -94,34 +112,90 @@ class DramaGridCard extends StatelessWidget {
drama.episodes.any((episode) => episode.isFree); drama.episodes.any((episode) => episode.isFree);
return ClipRRect( return ClipRRect(
key: Key('home-drama-cover-${drama.id}'), key: Key('home-drama-cover-${drama.id}'),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(AppDimens.coverRadius),
child: Stack( child: Stack(
children: [ children: [
_DiscoverCoverImage(path: drama.posterUrl), _DiscoverCoverImage(path: drama.posterUrl),
if (free) Positioned(top: 6, left: 6, child: _freeBadge()), Positioned(top: 8, left: 8, child: _tagBadge(free)),
if (_lockedCount > 0)
Positioned(top: 8, right: 8, child: _lockedBadge()),
Positioned(right: 8, bottom: 8, child: _episodeBadge()),
], ],
), ),
); );
} }
Widget _freeBadge() { int get _lockedCount {
final explicit = drama.episodes.where((episode) => episode.locked).length;
if (explicit > 0) return explicit;
final inferred = drama.episodeCount - drama.freeEpisodes;
return inferred > 0 ? inferred : 0;
}
Widget _tagBadge(bool free) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), key: Key('drama-tag-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.blue, color: AppColors.badgePink,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(AppDimens.pillRadius),
), ),
child: Text( child: Text(
l10n.free, free ? l10n.free : l10n.originalTag,
style: const TextStyle( style: const TextStyle(
color: AppColors.onPrimary, color: AppColors.badgePinkText,
fontSize: 8, fontSize: AppDimens.cardMeta,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
), ),
); );
} }
Widget _lockedBadge() {
return Container(
key: Key('drama-locked-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: AppColors.overlayDark,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock_rounded, color: AppColors.onPrimary, size: 11),
const SizedBox(width: 2),
Text(
l10n.lockedEpisodeCount(_lockedCount),
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _episodeBadge() {
return Container(
key: Key('drama-episodes-badge-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.badgeBeige,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Text(
l10n.allEpisodesBadge(drama.episodeCount),
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
);
}
Widget _title() { Widget _title() {
return Text( return Text(
drama.title, drama.title,
@@ -130,7 +204,7 @@ class DramaGridCard extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 11, fontSize: AppDimens.cardTitle,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
); );
@@ -143,7 +217,32 @@ class DramaGridCard extends StatelessWidget {
key: Key('home-drama-genre-${drama.id}'), key: Key('home-drama-genre-${drama.id}'),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9), style: const TextStyle(
color: AppColors.muted,
fontSize: AppDimens.cardMeta,
),
);
}
Widget _footer() {
return Row(
children: [
Expanded(child: _genre()),
Container(
key: Key('drama-add-${drama.id}'),
width: _addSize,
height: _addSize,
decoration: const BoxDecoration(
color: AppColors.blue,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add_rounded,
color: AppColors.onPrimary,
size: _addIconSize,
),
),
],
); );
} }
} }
@@ -135,7 +135,7 @@ class _RankingSectionState extends State<RankingSection> {
final selected = logic.board == board; final selected = logic.board == board;
return Material( return Material(
key: key, key: key,
color: selected ? AppColors.blue : AppColors.raised, color: selected ? AppColors.blue : AppColors.card,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
@@ -145,7 +145,7 @@ class _RankingSectionState extends State<RankingSection> {
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.muted, color: selected ? AppColors.onPrimary : AppColors.onSurface,
fontSize: 9, fontSize: 9,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
+3 -6
View File
@@ -54,7 +54,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('downloads-page'), key: const Key('downloads-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
@@ -168,11 +168,8 @@ class _DownloadsBody extends StatelessWidget {
); );
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => PlayerPage( builder: (_) =>
drama: drama, PlayerPage(drama: drama, initialIndex: 0, downloads: downloads),
initialIndex: 0,
downloads: downloads,
),
), ),
); );
} }
@@ -73,7 +73,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: ListenableBuilder( child: ListenableBuilder(
listenable: logic, listenable: logic,
builder: (context, _) => _page(context), builder: (context, _) => _page(context),
@@ -171,7 +171,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0x33000000), Color(0xCC0B0C10)], colors: [Color(0x33000000), AppColors.onSurface],
), ),
), ),
), ),
@@ -363,7 +363,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
? AppColors.blue ? AppColors.blue
: locked : locked
? AppColors.secondary.withValues(alpha: 0.16) ? AppColors.secondary.withValues(alpha: 0.16)
: const Color(0xFF29272B), : AppColors.raised,
borderRadius: BorderRadius.circular(9), borderRadius: BorderRadius.circular(9),
), ),
child: Text( child: Text(
@@ -461,8 +461,8 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
child: Container( child: Container(
width: 36, width: 36,
height: 36, height: 36,
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color(0x80000000), color: AppColors.card.withValues(alpha: 0.92),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
@@ -562,8 +562,8 @@ class _RoundAction extends StatelessWidget {
return Container( return Container(
width: 36, width: 36,
height: 36, height: 36,
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color(0x80000000), color: AppColors.card.withValues(alpha: 0.92),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: IconButton( child: IconButton(
@@ -592,7 +592,7 @@ class _ResumeBar extends StatelessWidget {
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: fraction, value: fraction,
minHeight: 3, minHeight: 3,
backgroundColor: const Color(0xFF474248), backgroundColor: AppColors.border,
valueColor: const AlwaysStoppedAnimation(AppColors.blue), valueColor: const AlwaysStoppedAnimation(AppColors.blue),
), ),
), ),
+1 -1
View File
@@ -51,7 +51,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
appBar: AppBar( appBar: AppBar(
+6 -6
View File
@@ -7,10 +7,10 @@ import 'package:flutter/foundation.dart';
import '../../core/data/models/watch_progress.dart'; import '../../core/data/models/watch_progress.dart';
import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/repositories/watch_history_repository.dart';
/// Home browse channel, backed only by data already present in the catalogue. /// 首页频道,只依赖目录中已有数据排序或过滤。
enum HomeChannel { hot, free, newReleases, ranking } enum HomeChannel { hot, free, newReleases, ranking }
/// Local UI state for the browse-first home page. /// 浏览型首页的本地 UI 状态。
class HomeBrowseLogic extends ChangeNotifier { class HomeBrowseLogic extends ChangeNotifier {
HomeBrowseLogic({required this.history}) { HomeBrowseLogic({required this.history}) {
unawaited(refreshRecent()); unawaited(refreshRecent());
@@ -24,7 +24,7 @@ class HomeBrowseLogic extends ChangeNotifier {
bool loadingRecent = false; bool loadingRecent = false;
bool _disposed = false; bool _disposed = false;
/// First unfinished record with a real position. /// 第一条未完成且存在真实播放位置的记录。
WatchProgress? get primaryRecord { WatchProgress? get primaryRecord {
for (final record in recent) { for (final record in recent) {
if (!record.finished && record.hasPosition) return record; if (!record.finished && record.hasPosition) return record;
@@ -32,14 +32,14 @@ class HomeBrowseLogic extends ChangeNotifier {
return null; return null;
} }
/// Changes the selected browse channel without triggering network work. /// 切换频道,不触发额外网络请求。
void selectChannel(HomeChannel value) { void selectChannel(HomeChannel value) {
if (channel == value) return; if (channel == value) return;
channel = value; channel = value;
_notify(); _notify();
} }
/// Reloads continuation data; signed-out reads simply return an empty list. /// 重新加载续看数据;未登录时返回空列表。
Future<void> refreshRecent() async { Future<void> refreshRecent() async {
loadingRecent = true; loadingRecent = true;
_notify(); _notify();
@@ -53,7 +53,7 @@ class HomeBrowseLogic extends ChangeNotifier {
} }
} }
/// Refreshes continuation data when the home tab becomes visible again. /// 首页重新可见时刷新续看数据。
void syncPage({required bool active, required bool wasActive}) { void syncPage({required bool active, required bool wasActive}) {
if (active && !wasActive) unawaited(refreshRecent()); if (active && !wasActive) unawaited(refreshRecent());
} }
+2 -2
View File
@@ -20,7 +20,7 @@ import 'home_browse_logic.dart';
import 'widgets/home_browse_header.dart'; import 'widgets/home_browse_header.dart';
import 'widgets/home_drama_grid.dart'; import 'widgets/home_drama_grid.dart';
/// Browse-first home page with channels, hero, continuation, and catalogue. /// 浏览型首页:频道、Hero、继续观看与剧目目录。
class HomeBrowseView extends StatefulWidget { class HomeBrowseView extends StatefulWidget {
const HomeBrowseView({ const HomeBrowseView({
super.key, super.key,
@@ -214,7 +214,7 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
color: selected ? AppColors.blue : AppColors.border, color: selected ? AppColors.blue : AppColors.border,
), ),
labelStyle: TextStyle( labelStyle: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.muted, color: selected ? AppColors.onPrimary : AppColors.onSurface,
fontSize: AppDimens.chipFont, fontSize: AppDimens.chipFont,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
+15 -9
View File
@@ -8,7 +8,6 @@ import 'package:provider/provider.dart';
import '../../core/data/models/drama.dart'; import '../../core/data/models/drama.dart';
import '../../core/data/services/catalog_controller.dart'; import '../../core/data/services/catalog_controller.dart';
import '../../core/platform/haptics.dart'; import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/app_bottom_nav.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
@@ -32,8 +31,11 @@ class HomePage extends StatelessWidget {
if (!catalog.settled) return const SplashPage(); if (!catalog.settled) return const SplashPage();
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: _buildHome(context, catalog, l10n), child: DecoratedBox(
decoration: const BoxDecoration(gradient: AppTheme.pageGradient),
child: _buildHome(context, catalog, l10n),
),
); );
} }
@@ -44,19 +46,19 @@ class HomePage extends StatelessWidget {
) { ) {
if (catalog.loading) { if (catalog.loading) {
return const Scaffold( return const Scaffold(
backgroundColor: AppColors.ink, backgroundColor: Colors.transparent,
body: Center(child: CircularProgressIndicator()), body: Center(child: CircularProgressIndicator()),
); );
} }
if (catalog.error != null) { if (catalog.error != null) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.ink, backgroundColor: Colors.transparent,
body: HomeFailureView(onRetry: catalog.load), body: HomeFailureView(onRetry: catalog.load),
); );
} }
if (catalog.dramas.isEmpty) { if (catalog.dramas.isEmpty) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.ink, backgroundColor: Colors.transparent,
body: EmptyState( body: EmptyState(
key: const Key('home-empty'), key: const Key('home-empty'),
icon: Icons.movie_outlined, icon: Icons.movie_outlined,
@@ -75,7 +77,7 @@ class HomePage extends StatelessWidget {
} }
} }
/// Home catalog failure with a single recovery action. /// 首页目录加载失败与重试入口。
class HomeFailureView extends StatelessWidget { class HomeFailureView extends StatelessWidget {
const HomeFailureView({super.key, required this.onRetry}); const HomeFailureView({super.key, required this.onRetry});
@@ -93,7 +95,7 @@ class HomeFailureView extends StatelessWidget {
} }
} }
/// The four primary destinations share a single route and keep their state. /// 四个根页面共享同一路由并保持各自状态。
class _HomeTabs extends StatefulWidget { class _HomeTabs extends StatefulWidget {
const _HomeTabs({super.key, required this.dramas}); const _HomeTabs({super.key, required this.dramas});
@@ -136,7 +138,7 @@ class _HomeTabsState extends State<_HomeTabs> {
}, },
child: Scaffold( child: Scaffold(
key: logic.scaffoldKey, key: logic.scaffoldKey,
backgroundColor: AppColors.ink, backgroundColor: Colors.transparent,
body: _body(context), body: _body(context),
bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n), bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n),
), ),
@@ -193,6 +195,10 @@ class _HomeTabsState extends State<_HomeTabs> {
Widget _nav(BuildContext context, AppLocalizations l10n) { Widget _nav(BuildContext context, AppLocalizations l10n) {
return AppBottomNav( return AppBottomNav(
barKey: const Key('home-tabs-bar'), barKey: const Key('home-tabs-bar'),
centerAction: AppBottomNavCenterAction(
label: l10n.createTab,
onTap: () => _selectTab(context, 2),
),
items: [ items: [
_navItem(context, l10n, 0), _navItem(context, l10n, 0),
_navItem(context, l10n, 1), _navItem(context, l10n, 1),
@@ -8,7 +8,7 @@ import '../../../l10n/app_localizations.dart';
const double _searchHeight = 36; const double _searchHeight = 36;
/// Fixed home header with catalogue search, weekday, and profile entry. /// 首页固定顶栏:搜索、星期与头像入口。
class HomeBrowseHeader extends StatelessWidget { class HomeBrowseHeader extends StatelessWidget {
const HomeBrowseHeader({ const HomeBrowseHeader({
super.key, super.key,
+137 -40
View File
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart'; import '../../../core/data/models/drama.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/remote_image.dart'; import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -12,8 +13,10 @@ const int _columns = 2;
const double _columnGap = 10; const double _columnGap = 10;
const double _cardGap = 10; const double _cardGap = 10;
const double _coverAspectRatio = 3 / 4; const double _coverAspectRatio = 3 / 4;
const double _addSize = 28;
const double _addIconSize = 18;
/// Two-column drama grid used by the browse-first home page. /// 首页浏览态使用的双列剧目网格。
class HomeDramaGrid extends StatelessWidget { class HomeDramaGrid extends StatelessWidget {
const HomeDramaGrid({ const HomeDramaGrid({
super.key, super.key,
@@ -57,7 +60,7 @@ class HomeDramaGrid extends StatelessWidget {
} }
} }
/// Home catalogue card with title, genre, free badge, episode count, and heat. /// 首页剧卡:封面、角标、标题、题材、集数与热度。
class HomeDramaCard extends StatelessWidget { class HomeDramaCard extends StatelessWidget {
const HomeDramaCard({ const HomeDramaCard({
super.key, super.key,
@@ -72,21 +75,35 @@ class HomeDramaCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return Container(
key: Key('home-drama-card-${drama.id}'), decoration: BoxDecoration(
onTap: onTap, color: AppColors.card,
borderRadius: BorderRadius.circular(AppDimens.coverRadius), borderRadius: BorderRadius.circular(AppDimens.cardRadius),
child: Column( boxShadow: const [AppTheme.cardShadow],
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Material(
_cover(), color: Colors.transparent,
const SizedBox(height: AppDimens.spaceXs), borderRadius: BorderRadius.circular(AppDimens.cardRadius),
_title(), clipBehavior: Clip.antiAlias,
const SizedBox(height: 2), child: InkWell(
_genre(), key: Key('home-drama-card-${drama.id}'),
const SizedBox(height: AppDimens.spaceXs), onTap: onTap,
_metadata(), child: Padding(
], padding: const EdgeInsets.all(AppDimens.spaceSm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_cover(),
const SizedBox(height: AppDimens.spaceSm),
_title(),
const SizedBox(height: AppDimens.spaceXs),
_genre(),
const SizedBox(height: AppDimens.spaceXs),
_metadata(),
],
),
),
),
), ),
); );
} }
@@ -104,7 +121,10 @@ class HomeDramaCard extends StatelessWidget {
showFallbackIcon: true, showFallbackIcon: true,
fallbackAspectRatio: _coverAspectRatio, fallbackAspectRatio: _coverAspectRatio,
), ),
if (_isFree) Positioned(top: 6, left: 6, child: _freeBadge()), Positioned(top: 8, left: 8, child: _tagBadge()),
if (_lockedCount > 0)
Positioned(top: 8, right: 8, child: _lockedBadge()),
Positioned(right: 8, bottom: 8, child: _episodeBadge()),
], ],
), ),
); );
@@ -115,17 +135,29 @@ class HomeDramaCard extends StatelessWidget {
drama.episodes.any((episode) => episode.isFree); drama.episodes.any((episode) => episode.isFree);
} }
Widget _freeBadge() { int get _lockedCount {
final explicit = drama.episodes.where((episode) => episode.locked).length;
if (explicit > 0) return explicit;
final inferred = drama.episodeCount - drama.freeEpisodes;
return inferred > 0 ? inferred : 0;
}
Widget _tagBadge() {
final label = _isFree ? l10n.free : l10n.originalTag;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), key: Key('home-drama-tag-${drama.id}'),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.spaceSm,
vertical: 3,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.blue, color: AppColors.badgePink,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(AppDimens.pillRadius),
), ),
child: Text( child: Text(
l10n.free, label,
style: const TextStyle( style: const TextStyle(
color: AppColors.onPrimary, color: AppColors.badgePinkText,
fontSize: AppDimens.cardMeta, fontSize: AppDimens.cardMeta,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
@@ -133,6 +165,51 @@ class HomeDramaCard extends StatelessWidget {
); );
} }
Widget _lockedBadge() {
return Container(
key: Key('home-drama-locked-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: AppColors.overlayDark,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock_rounded, color: AppColors.onPrimary, size: 11),
const SizedBox(width: 2),
Text(
l10n.lockedEpisodeCount(_lockedCount),
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Widget _episodeBadge() {
return Container(
key: Key('home-drama-episodes-badge-${drama.id}'),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.badgeBeige,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
child: Text(
l10n.allEpisodesBadge(drama.episodeCount),
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
);
}
Widget _title() { Widget _title() {
return Text( return Text(
drama.title, drama.title,
@@ -141,7 +218,7 @@ class HomeDramaCard extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: AppDimens.previewSectionTitle, fontSize: AppDimens.cardTitle,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
); );
@@ -162,24 +239,44 @@ class HomeDramaCard extends StatelessWidget {
} }
Widget _metadata() { Widget _metadata() {
return Wrap( return Row(
spacing: AppDimens.spaceSm,
runSpacing: AppDimens.spaceXs,
children: [ children: [
Text( Expanded(
l10n.episodeCount(drama.episodeCount), child: Wrap(
key: Key('home-drama-episode-count-${drama.id}'), spacing: AppDimens.spaceSm,
style: const TextStyle( runSpacing: AppDimens.spaceXs,
color: AppColors.subtle, children: [
fontSize: AppDimens.cardViews, Text(
l10n.episodeCount(drama.episodeCount),
key: Key('home-drama-episode-count-${drama.id}'),
style: const TextStyle(
color: AppColors.subtle,
fontSize: AppDimens.cardViews,
),
),
Text(
l10n.viewCountLabel(drama.viewCount),
key: Key('home-drama-views-${drama.id}'),
style: const TextStyle(
color: AppColors.subtle,
fontSize: AppDimens.cardViews,
),
),
],
), ),
), ),
Text( Container(
l10n.viewCountLabel(drama.viewCount), key: Key('home-drama-add-${drama.id}'),
key: Key('home-drama-views-${drama.id}'), width: _addSize,
style: const TextStyle( height: _addSize,
color: AppColors.subtle, decoration: const BoxDecoration(
fontSize: AppDimens.cardViews, color: AppColors.blue,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add_rounded,
color: AppColors.onPrimary,
size: _addIconSize,
), ),
), ),
], ],
+5 -1
View File
@@ -92,7 +92,7 @@ class _MyPageState extends State<MyPage> {
), ),
); );
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: embedded child: embedded
? content ? content
: Scaffold(backgroundColor: AppColors.ink, body: content), : Scaffold(backgroundColor: AppColors.ink, body: content),
@@ -242,6 +242,10 @@ class _MyPageState extends State<MyPage> {
Widget _bottomNav(AppLocalizations l10n) { Widget _bottomNav(AppLocalizations l10n) {
return AppBottomNav( return AppBottomNav(
centerAction: AppBottomNavCenterAction(
label: l10n.createTab,
onTap: () => _act(MyPageAction.create),
),
items: [ items: [
AppBottomNavItem( AppBottomNavItem(
tabKey: const Key('mine-nav-home'), tabKey: const Key('mine-nav-home'),
@@ -101,7 +101,7 @@ class ContinueWatchingCard extends StatelessWidget {
key: progressKey, key: progressKey,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9), style: const TextStyle(color: AppColors.muted, fontSize: 9),
), ),
], ],
); );
+3 -3
View File
@@ -46,12 +46,12 @@ class MineHeader extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF6A5CFF), AppColors.blue], colors: [AppColors.secondary, AppColors.blue],
), ),
), ),
child: const Icon( child: const Icon(
Icons.person_rounded, Icons.person_rounded,
color: AppColors.onSurface, color: AppColors.onPrimary,
size: 22, size: 22,
), ),
); );
@@ -76,7 +76,7 @@ class MineHeader extends StatelessWidget {
subtitle, subtitle,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 10), style: const TextStyle(color: AppColors.muted, fontSize: 10),
), ),
], ],
); );
+1 -1
View File
@@ -44,7 +44,7 @@ class _NoticesPageState extends State<NoticesPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('notices-page'), key: const Key('notices-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+1 -1
View File
@@ -669,7 +669,7 @@ class PlayerLogic extends ChangeNotifier {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: Color(0xFFB7B3B8), color: AppColors.muted,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
+4 -4
View File
@@ -146,9 +146,9 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
), ),
); );
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, value: SystemUiOverlayStyle.dark,
child: Theme( child: Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: widget.embedded child: widget.embedded
? content ? content
: Scaffold(backgroundColor: AppColors.ink, body: content), : Scaffold(backgroundColor: AppColors.ink, body: content),
@@ -1155,7 +1155,7 @@ class _TopAction extends StatelessWidget {
onPressed: onPressed, onPressed: onPressed,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: const Color(0xFF2C2D33), backgroundColor: AppColors.card.withValues(alpha: 0.94),
foregroundColor: AppColors.onSurface, foregroundColor: AppColors.onSurface,
), ),
icon: Icon(icon, size: 18), icon: Icon(icon, size: 18),
@@ -1179,7 +1179,7 @@ class _PlayerAction extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: const Color(0xFF2A2A30), color: AppColors.card.withValues(alpha: 0.94),
borderRadius: BorderRadius.circular(9), borderRadius: BorderRadius.circular(9),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
@@ -160,12 +160,7 @@ class HomeFeedOverlay extends StatelessWidget {
radius: 14, radius: 14,
blur: 6, blur: 6,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
gradient: const LinearGradient( borderColor: AppColors.border,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xBF0F0F14), Color(0xA60F0F14)],
),
borderColor: Colors.white.withValues(alpha: 0.06),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -184,13 +179,13 @@ class HomeFeedOverlay extends StatelessWidget {
if (drama.category.isNotEmpty && drama.category != genre) if (drama.category.isNotEmpty && drama.category != genre)
_tag( _tag(
drama.category, drama.category,
background: const Color(0xFF333333), background: AppColors.raised,
foreground: const Color(0xFFDDDDDD), foreground: AppColors.onSurface,
), ),
Text( Text(
l10n.episodeOfTotal(episode.number, drama.episodeCount), l10n.episodeOfTotal(episode.number, drama.episodeCount),
key: const Key('home-feed-episode'), key: const Key('home-feed-episode'),
style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9), style: const TextStyle(color: AppColors.muted, fontSize: 9),
), ),
], ],
), ),
@@ -248,7 +243,7 @@ class HomeFeedOverlay extends StatelessWidget {
key: const Key('home-feed-episodes'), key: const Key('home-feed-episodes'),
onPressed: onEpisodes, onPressed: onEpisodes,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF2A2A30), backgroundColor: AppColors.raised,
foregroundColor: AppColors.onSurface, foregroundColor: AppColors.onSurface,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
@@ -362,7 +357,7 @@ class _RoundIcon extends StatelessWidget {
onPressed: onPressed, onPressed: onPressed,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: const Color(0xCC2C2D33), backgroundColor: AppColors.card.withValues(alpha: 0.94),
foregroundColor: AppColors.onSurface, foregroundColor: AppColors.onSurface,
), ),
icon: Icon(icon, size: 18), icon: Icon(icon, size: 18),
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
/// Shared play/pause control used by the home feed and full-screen player. /// 首页沉浸流与全屏播放器共用的播放/暂停按钮。
class PlayerCenterToggle extends StatelessWidget { class PlayerCenterToggle extends StatelessWidget {
const PlayerCenterToggle({ const PlayerCenterToggle({
super.key, super.key,
@@ -26,7 +26,7 @@ class PlayerCenterToggle extends StatelessWidget {
width: 52, width: 52,
height: 52, height: 52,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Color(0x80000000), color: AppColors.blue,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
@@ -117,7 +117,7 @@ class _PlayerFeaturedCommentState extends State<PlayerFeaturedComment> {
Text( Text(
'· $time', '· $time',
style: const TextStyle( style: const TextStyle(
color: Color(0xFF777777), color: AppColors.muted,
fontSize: 9, fontSize: 9,
), ),
), ),
@@ -133,7 +133,7 @@ class _PlayerFeaturedCommentState extends State<PlayerFeaturedComment> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: Color(0xFFDDDDDD), color: AppColors.onSurface,
fontSize: 10, fontSize: 10,
height: 1.5, height: 1.5,
), ),
@@ -127,10 +127,8 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
? (scrubbing ? 3 : 1.5) ? (scrubbing ? 3 : 1.5)
: (scrubbing ? 4 : 3), : (scrubbing ? 4 : 3),
activeTrackColor: AppColors.blue, activeTrackColor: AppColors.blue,
inactiveTrackColor: widget.compact inactiveTrackColor: AppColors.border,
? Colors.white.withValues(alpha: 0.18) thumbColor: AppColors.onPrimary,
: const Color(0xFF474248),
thumbColor: widget.compact ? AppColors.blue : Colors.white,
thumbShape: RoundSliderThumbShape( thumbShape: RoundSliderThumbShape(
enabledThumbRadius: widget.compact enabledThumbRadius: widget.compact
? (scrubbing ? 6 : 4) ? (scrubbing ? 6 : 4)
+1 -1
View File
@@ -22,7 +22,7 @@ class SettingsPage extends StatelessWidget {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final auth = context.watch<AuthController>(); final auth = context.watch<AuthController>();
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('settings-page'), key: const Key('settings-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+1 -1
View File
@@ -56,7 +56,7 @@ class _StorePageState extends State<StorePage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final isVip = widget.type == StoreController.vipType; final isVip = widget.type == StoreController.vipType;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('store-page'), key: const Key('store-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
+2 -2
View File
@@ -44,7 +44,7 @@ class _WalletPageState extends State<WalletPage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final wallet = context.watch<WalletController>(); final wallet = context.watch<WalletController>();
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('wallet-page'), key: const Key('wallet-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
@@ -188,7 +188,7 @@ class _BalanceHero extends StatelessWidget {
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF2B2116), Color(0xFF17171D)], colors: [AppColors.goldCard, AppColors.card],
), ),
border: Border.all(color: AppColors.gold.withValues(alpha: 0.35)), border: Border.all(color: AppColors.gold.withValues(alpha: 0.35)),
boxShadow: [ boxShadow: [
@@ -33,7 +33,7 @@ class WatchRecordsPage extends StatelessWidget {
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Theme( return Theme(
data: AppTheme.dark(), data: AppTheme.light(),
child: Scaffold( child: Scaffold(
key: const Key('watch-records-page'), key: const Key('watch-records-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
@@ -207,7 +207,7 @@ Widget _host(Drama drama) {
ChangeNotifierProvider(create: (_) => CatalogController(catalog)..load()), ChangeNotifierProvider(create: (_) => CatalogController(catalog)..load()),
], ],
child: MaterialApp( child: MaterialApp(
theme: AppTheme.dark(), theme: AppTheme.light(),
locale: const Locale('en'), locale: const Locale('en'),
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
+6 -6
View File
@@ -12,7 +12,7 @@ void main() {
var taps = 0; var taps = 0;
await _pump( await _pump(
tester, tester,
theme: AppTheme.dark(), theme: AppTheme.light(),
child: EmptyState( child: EmptyState(
icon: Icons.movie_outlined, icon: Icons.movie_outlined,
title: 'No stories', title: 'No stories',
@@ -33,7 +33,7 @@ void main() {
); );
expect( expect(
tester.widget<Icon>(find.byKey(const Key('empty-state-icon'))).color, tester.widget<Icon>(find.byKey(const Key('empty-state-icon'))).color,
AppColors.gold, AppColors.blue,
); );
expect( expect(
tester tester
@@ -103,15 +103,15 @@ void main() {
expect(find.byKey(const Key('empty-state-action')), findsNothing); expect(find.byKey(const Key('empty-state-action')), findsNothing);
}); });
testWidgets('should_tint_the_bare_icon_for_light_and_dark', (tester) async { testWidgets('should_tint_the_bare_icon_with_the_light_theme', (tester) async {
await _pump( await _pump(
tester, tester,
theme: AppTheme.dark(), theme: AppTheme.light(),
child: const EmptyState(icon: Icons.history_rounded, title: 'No history'), child: const EmptyState(icon: Icons.history_rounded, title: 'No history'),
); );
expect( expect(
tester.widget<Icon>(find.byKey(const Key('empty-state-icon'))).color, tester.widget<Icon>(find.byKey(const Key('empty-state-icon'))).color,
AppColors.gold, AppColors.blue,
); );
await _pump( await _pump(
@@ -156,7 +156,7 @@ Future<void> _pump(
}) async { }) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme ?? AppTheme.dark(), theme: theme ?? AppTheme.light(),
home: Scaffold(body: child), home: Scaffold(body: child),
), ),
); );
+55 -5
View File
@@ -2,13 +2,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart';
import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; import 'package:fruitshorts/core/widgets/app_bottom_nav.dart';
void main() { void main() {
testWidgets('should_render_64_high_navigation_and_keep_item_keys', ( testWidgets('should_render_five_slots_with_center_action', (tester) async {
tester, var centerTaps = 0;
) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
@@ -23,24 +23,74 @@ void main() {
onTap: () {}, onTap: () {},
), ),
AppBottomNavItem( AppBottomNavItem(
tabKey: const Key('tab-my'), tabKey: const Key('tab-theater'),
icon: Icons.explore_outlined,
selectedIcon: Icons.explore_rounded,
label: 'Theater',
onTap: () {},
),
AppBottomNavItem(
tabKey: const Key('tab-create'),
icon: Icons.add_box_outlined,
selectedIcon: Icons.add_box_rounded,
label: 'Create',
onTap: () {},
),
AppBottomNavItem(
tabKey: const Key('tab-mine'),
icon: Icons.person_outline_rounded, icon: Icons.person_outline_rounded,
selectedIcon: Icons.person_rounded, selectedIcon: Icons.person_rounded,
label: 'Me', label: 'Me',
onTap: () {}, onTap: () {},
), ),
], ],
centerAction: AppBottomNavCenterAction(
label: 'Create',
onTap: () => centerTaps++,
),
), ),
), ),
), ),
); );
expect(find.byKey(const Key('tab-home')), findsOneWidget); expect(find.byKey(const Key('tab-home')), findsOneWidget);
expect(find.byKey(const Key('tab-my')), findsOneWidget); expect(find.byKey(const Key('tab-theater')), findsOneWidget);
expect(find.byKey(const Key('nav-center-action')), findsOneWidget);
expect(find.byKey(const Key('tab-create')), findsOneWidget);
expect(find.byKey(const Key('tab-mine')), findsOneWidget);
expect(find.text('Home'), findsOneWidget); expect(find.text('Home'), findsOneWidget);
final centers = [
Key('tab-home'),
Key('tab-theater'),
Key('nav-center-action'),
Key('tab-create'),
Key('tab-mine'),
].map((key) => tester.getCenter(find.byKey(key)).dx).toList();
expect(centers, orderedEquals(centers.toList()..sort()));
expect( expect(
tester.getSize(find.byType(AppBottomNav)).height, tester.getSize(find.byType(AppBottomNav)).height,
AppDimens.navHeight, AppDimens.navHeight,
); );
expect(
tester.getSize(find.byKey(const Key('nav-center-action'))),
const Size.square(AppDimens.navCenter),
);
final ink = tester.widget<Ink>(
find
.ancestor(
of: find.byKey(const Key('nav-center-action')),
matching: find.byType(Ink),
)
.first,
);
final decoration = ink.decoration! as BoxDecoration;
expect(decoration.shape, BoxShape.circle);
expect((decoration.gradient! as LinearGradient).colors, const [
AppColors.navCenterStart,
AppColors.navCenterEnd,
]);
await tester.tap(find.byKey(const Key('nav-center-action')));
expect(centerTaps, 1);
}); });
} }
+69
View File
@@ -0,0 +1,69 @@
// 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/core/theme/app_colors.dart';
import 'package:fruitshorts/core/theme/app_dimens.dart';
import 'package:fruitshorts/core/theme/app_theme.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/home/widgets/home_drama_grid.dart';
void main() {
testWidgets('should_render_warm_orange_cover_badges_and_plus_action', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
locale: const Locale('en'),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
home: Scaffold(
body: Builder(
builder: (context) => SizedBox(
width: 180,
child: HomeDramaCard(
l10n: AppLocalizations.of(context)!,
drama: _drama,
onTap: () {},
),
),
),
),
),
);
await tester.pumpAndSettle();
final card = find.byKey(const Key('home-drama-card-style-drama'));
final cardContainer = tester.widget<Container>(
find.ancestor(of: card, matching: find.byType(Container)).last,
);
final cardDecoration = cardContainer.decoration! as BoxDecoration;
expect(cardDecoration.color, AppColors.card);
expect(
cardDecoration.borderRadius,
BorderRadius.circular(AppDimens.cardRadius),
);
expect(cardDecoration.boxShadow, const [AppTheme.cardShadow]);
final tag = tester.widget<Container>(
find.byKey(const Key('home-drama-tag-style-drama')),
);
expect((tag.decoration! as BoxDecoration).color, AppColors.badgePink);
expect(find.text('Free'), findsOneWidget);
expect(find.text('18 locked'), findsOneWidget);
expect(find.text('All 24'), findsOneWidget);
expect(find.byKey(const Key('home-drama-add-style-drama')), findsOneWidget);
});
}
const _drama = Drama(
id: 'style-drama',
title: 'Warm Orange Story',
synopsis: '暖橙短剧',
posterUrl: '/files/style.jpg',
genre: 'Romance',
totalEpisodes: 24,
freeEpisodes: 6,
);
+44 -1
View File
@@ -20,6 +20,7 @@ const _keys = [
Key('home-tabs-bar'), Key('home-tabs-bar'),
Key('tab-home'), Key('tab-home'),
Key('tab-theater'), Key('tab-theater'),
Key('nav-center-action'),
Key('tab-create'), Key('tab-create'),
Key('tab-mine'), Key('tab-mine'),
]; ];
@@ -53,7 +54,7 @@ void main() {
final decoration = bar.decoration as BoxDecoration; final decoration = bar.decoration as BoxDecoration;
final border = decoration.border as Border; final border = decoration.border as Border;
expect(decoration.color, AppColors.bar.withValues(alpha: 0.95)); expect(decoration.color, AppColors.bar.withValues(alpha: 0.95));
expect(border.top.color, AppColors.outlineVariantDark); expect(border.top.color, AppColors.border);
expect(border.top.color, isNot(Colors.white)); expect(border.top.color, isNot(Colors.white));
expect( expect(
tester tester
@@ -73,6 +74,17 @@ void main() {
), ),
findsOneWidget, findsOneWidget,
); );
expect(
tester
.widget<Icon>(
find.descendant(
of: find.byKey(const Key('tab-home')),
matching: find.byIcon(Icons.home_rounded),
),
)
.color,
AppColors.blue,
);
await tester.tap(find.byKey(const Key('tab-theater'))); await tester.tap(find.byKey(const Key('tab-theater')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -87,6 +99,30 @@ void main() {
), ),
findsOneWidget, findsOneWidget,
); );
expect(
tester
.widget<Text>(
find.descendant(
of: find.byKey(const Key('tab-theater')),
matching: find.text('Theater'),
),
)
.style
?.color,
AppColors.blue,
);
expect(
tester
.widget<Text>(
find.descendant(
of: find.byKey(const Key('tab-home')),
matching: find.text('Home'),
),
)
.style
?.color,
AppColors.navUnselected,
);
expect( expect(
find.descendant( find.descendant(
of: find.byKey(const Key('tab-home')), of: find.byKey(const Key('tab-home')),
@@ -103,6 +139,7 @@ void main() {
expect(find.byKey(const Key('tab-home')), findsOneWidget); expect(find.byKey(const Key('tab-home')), findsOneWidget);
expect(find.byKey(const Key('tab-theater')), findsOneWidget); expect(find.byKey(const Key('tab-theater')), findsOneWidget);
expect(find.byKey(const Key('nav-center-action')), findsOneWidget);
expect(find.byKey(const Key('tab-create')), findsOneWidget); expect(find.byKey(const Key('tab-create')), findsOneWidget);
expect(find.byKey(const Key('tab-mine')), findsOneWidget); expect(find.byKey(const Key('tab-mine')), findsOneWidget);
expect(find.byKey(const Key('home-browse-page')), findsOneWidget); expect(find.byKey(const Key('home-browse-page')), findsOneWidget);
@@ -111,6 +148,12 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
await tester.tap(find.byKey(const Key('nav-center-action')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('create-page')), findsOneWidget);
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-create'))); await tester.tap(find.byKey(const Key('tab-create')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('create-page')), findsOneWidget); expect(find.byKey(const Key('create-page')), findsOneWidget);
+2 -2
View File
@@ -82,7 +82,7 @@ void main() {
Future<void> pumpEpisode(Episode episode) async { Future<void> pumpEpisode(Episode episode) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: AppTheme.dark(), theme: AppTheme.light(),
locale: const Locale('en'), locale: const Locale('en'),
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
@@ -141,7 +141,7 @@ void main() {
) async { ) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: AppTheme.dark(), theme: AppTheme.light(),
locale: const Locale('en'), locale: const Locale('en'),
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
+13 -1
View File
@@ -8,6 +8,7 @@ import 'package:fruitshorts/core/data/models/watch_progress.dart';
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:fruitshorts/pages/player/player_view.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
@@ -32,8 +33,9 @@ void main() {
for (final key in _tabKeys) { for (final key in _tabKeys) {
expect(find.byKey(key), findsOneWidget); expect(find.byKey(key), findsOneWidget);
} }
expect(find.byKey(const Key('nav-center-action')), findsOneWidget);
await tester.tap(find.byKey(const Key('tab-create'))); await tester.tap(find.byKey(const Key('nav-center-action')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('create-page')), findsOneWidget); expect(find.byKey(const Key('create-page')), findsOneWidget);
@@ -46,12 +48,22 @@ void main() {
expect(_chip(tester, 'home-channel-hot').selected, isTrue); expect(_chip(tester, 'home-channel-hot').selected, isTrue);
expect(_chip(tester, 'home-channel-free').selected, isFalse); expect(_chip(tester, 'home-channel-free').selected, isFalse);
expect(_chip(tester, 'home-channel-hot').selectedColor, AppColors.blue);
expect(_chip(tester, 'home-channel-free').backgroundColor, AppColors.card);
expect(
_chip(tester, 'home-channel-free').labelStyle?.color,
AppColors.onSurface,
);
await tester.tap(find.byKey(const Key('home-channel-free'))); await tester.tap(find.byKey(const Key('home-channel-free')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(_chip(tester, 'home-channel-hot').selected, isFalse); expect(_chip(tester, 'home-channel-hot').selected, isFalse);
expect(_chip(tester, 'home-channel-free').selected, isTrue); expect(_chip(tester, 'home-channel-free').selected, isTrue);
expect(
_chip(tester, 'home-channel-free').labelStyle?.color,
AppColors.onPrimary,
);
}); });
testWidgets('should_open_theater_search_when_home_search_is_tapped', ( testWidgets('should_open_theater_search_when_home_search_is_tapped', (
+2 -1
View File
@@ -2,6 +2,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart'; import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
void main() { void main() {
@@ -36,7 +37,7 @@ void main() {
find.descendant(of: find.byKey(key), matching: find.byType(Container)), find.descendant(of: find.byKey(key), matching: find.byType(Container)),
); );
final decoration = container.decoration! as BoxDecoration; final decoration = container.decoration! as BoxDecoration;
expect(decoration.color, const Color(0x80000000)); expect(decoration.color, AppColors.blue);
expect(decoration.shape, BoxShape.circle); expect(decoration.shape, BoxShape.circle);
final icon = tester.widget<Icon>( final icon = tester.widget<Icon>(
find.descendant(of: find.byKey(key), matching: find.byType(Icon)), find.descendant(of: find.byKey(key), matching: find.byType(Icon)),
+8 -4
View File
@@ -6,18 +6,22 @@ import 'package:fruitshorts/core/theme/app_dimens.dart';
void main() { void main() {
test('should_expose_the_k20_spacing_and_type_scale', () { test('should_expose_the_k20_spacing_and_type_scale', () {
expect(AppDimens.pageHorizontal, 16); expect(AppDimens.pageHorizontal, 16);
expect(AppDimens.cardRadius, 12); expect(AppDimens.cardRadius, 16);
expect(AppDimens.innerRadius, 10); expect(AppDimens.coverRadius, 16);
expect(AppDimens.authCardRadius, 20);
expect(AppDimens.pillRadius, 999);
expect(AppDimens.buttonRadius, 999);
expect(AppDimens.caption, 12); expect(AppDimens.caption, 12);
expect(AppDimens.body, 14); expect(AppDimens.body, 14);
expect(AppDimens.listTitle, 15); expect(AppDimens.listTitle, 15);
expect(AppDimens.sectionTitle, 16); expect(AppDimens.sectionTitle, 16);
expect(AppDimens.pageTitle, 20); expect(AppDimens.pageTitle, 20);
expect(AppDimens.navLabel, 9); expect(AppDimens.navLabel, 10);
expect(AppDimens.buttonHeight, 48); expect(AppDimens.buttonHeight, 48);
expect(AppDimens.primaryButtonHeight, 52); expect(AppDimens.primaryButtonHeight, 52);
expect(AppDimens.tapTarget, 44); expect(AppDimens.tapTarget, 44);
expect(AppDimens.navHeight, 54); expect(AppDimens.navHeight, 64);
expect(AppDimens.navCenter, 56);
expect(AppDimens.progressHeight, 3); expect(AppDimens.progressHeight, 3);
}); });
} }
+35 -60
View File
@@ -2,7 +2,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/theme/app_theme.dart'; import 'package:fruitshorts/core/theme/app_theme.dart';
@@ -14,71 +14,46 @@ double _contrast(Color foreground, Color background) {
} }
void main() { void main() {
test('should_expose_obsidian_flare_brand_tokens', () { test('should_expose_light_warm_orange_tokens', () {
expect(AppColors.blue, const Color(0xFFFF5B3D)); expect(AppColors.blue, const Color(0xFFFF7A1A));
expect(AppColors.bluePressed, const Color(0xFFFF4321)); expect(AppColors.bluePressed, const Color(0xFFFF6A00));
expect(AppColors.blueSoft, const Color(0xFF9278EA)); expect(AppColors.secondary, const Color(0xFFFF6A8A));
expect(AppColors.secondary, const Color(0xFF9278EA)); expect(AppColors.ink, const Color(0xFFFFF8F0));
expect(AppColors.gold, const Color(0xFFFBBF24)); expect(AppColors.surfaceTop, const Color(0xFFFFE9D6));
expect(AppColors.ink, const Color(0xFF0B0C10)); expect(AppColors.card, const Color(0xFFFFFFFF));
expect(AppColors.card, const Color(0xFF1B1C21)); expect(AppColors.onSurface, const Color(0xFF1F1B16));
expect(AppColors.cardAlt, const Color(0xFF1C1D21)); expect(AppColors.muted, const Color(0xFF8C837B));
expect(AppColors.raised, const Color(0xFF1C1D21)); expect(AppColors.subtle, const Color(0xFFB3AAA1));
expect(AppColors.bar, const Color(0xFF0D0D11)); expect(AppColors.border, const Color(0xFFF0E4D6));
expect(AppColors.success, const Color(0xFF34D399)); expect(AppColors.badgePink, const Color(0xFFFFE3EC));
expect(AppColors.onPrimary, const Color(0xFFFFFFFF)); expect(AppColors.badgePinkText, const Color(0xFFFF6A8A));
});
test('should_keep_primary_text_readable_on_warm_background', () {
expect( expect(
_contrast(AppColors.onGold, AppColors.gold), _contrast(AppColors.onSurface, AppColors.ink),
greaterThanOrEqualTo(4.5), greaterThanOrEqualTo(4.5),
); );
expect(_contrast(AppColors.muted, AppColors.card), greaterThanOrEqualTo(3));
}); });
test('should_meet_text_and_icon_contrast_when_using_theme_tokens', () { test('should_use_light_color_scheme_and_dark_system_icons', () {
expect( final theme = AppTheme.light();
_contrast(AppColors.onPrimary, AppColors.blue),
greaterThanOrEqualTo(3), expect(theme.brightness, Brightness.light);
); expect(theme.colorScheme.primary, AppColors.blue);
expect(_contrast(AppColors.blue, Colors.white), greaterThanOrEqualTo(3)); expect(theme.colorScheme.onPrimary, AppColors.onPrimary);
expect( expect(theme.colorScheme.surface, AppColors.ink);
_contrast(AppColors.blueSoft, AppColors.ink), expect(theme.colorScheme.onSurface, AppColors.onSurface);
greaterThanOrEqualTo(3), expect(theme.colorScheme.outline, AppColors.border);
); expect(theme.scaffoldBackgroundColor, AppColors.ink);
expect(theme.appBarTheme.systemOverlayStyle, SystemUiOverlayStyle.dark);
}); });
test('should_use_flare_brand_in_light_and_dark_color_schemes', () { test('should_expose_page_gradient_and_soft_card_shadow', () {
for (final theme in [AppTheme.light(), AppTheme.dark()]) { expect(AppTheme.pageGradient.colors, [AppColors.surfaceTop, AppColors.ink]);
expect(theme.colorScheme.primary, AppColors.blue); expect(AppTheme.cardShadow.color, AppColors.cardShadow);
expect(theme.colorScheme.onPrimary, AppColors.onPrimary); expect(AppTheme.cardShadow.blurRadius, 18);
} expect(AppTheme.cardShadow.offset, const Offset(0, 6));
});
test('should_define_non_white_outline_tokens_in_both_themes', () {
final light = AppTheme.light().colorScheme;
final dark = AppTheme.dark().colorScheme;
expect(light.outline, AppColors.outlineLight);
expect(light.outlineVariant, AppColors.outlineVariantLight);
expect(dark.outline, AppColors.outlineDark);
expect(dark.outlineVariant, AppColors.outlineVariantDark);
expect(dark.outlineVariant.computeLuminance(), lessThan(0.2));
expect(light.outlineVariant.computeLuminance(), lessThan(0.92));
});
testWidgets('should_expose_chili_brand_through_widget_theme', (tester) async {
late ThemeData applied;
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.dark(),
home: Builder(
builder: (context) {
applied = Theme.of(context);
return const SizedBox.shrink();
},
),
),
);
expect(applied.colorScheme.primary, AppColors.blue);
expect(applied.colorScheme.onPrimary, AppColors.onPrimary);
}); });
} }