From 634b63e16c5fc3ded0e0ca7865743f4dc71d9cf0 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 10:15:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(flutter):=20=E5=BA=94=E7=94=A8=E6=B5=85?= =?UTF-8?q?=E8=89=B2=E6=9A=96=E6=A9=99=E8=A7=86=E8=A7=89=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 8 +- lib/core/theme/app_colors.dart | 121 +++++++----- lib/core/theme/app_dimens.dart | 35 ++-- lib/core/theme/app_theme.dart | 165 ++++++++-------- lib/core/widgets/app_bottom_nav.dart | 122 +++++++++--- lib/core/widgets/glass_panel.dart | 4 +- lib/l10n/app_en.arb | 12 ++ lib/l10n/app_id.arb | 12 ++ lib/l10n/app_localizations.dart | 12 ++ lib/l10n/app_localizations_en.dart | 10 + lib/l10n/app_localizations_id.dart | 10 + lib/l10n/app_localizations_zh.dart | 10 + lib/l10n/app_zh.arb | 12 ++ .../account/account_deactivate_page.dart | 2 +- lib/pages/account/account_devices_page.dart | 2 +- lib/pages/account/account_export_page.dart | 2 +- lib/pages/account/account_profile_page.dart | 2 +- lib/pages/auth/auth_view.dart | 9 +- lib/pages/create/create_view.dart | 4 +- lib/pages/discover/category_dramas_page.dart | 2 +- lib/pages/discover/discover_view.dart | 8 +- .../discover/widgets/discover_header.dart | 6 +- lib/pages/discover/widgets/drama_grid.dart | 147 ++++++++++++--- .../discover/widgets/ranking_section.dart | 4 +- lib/pages/downloads/downloads_page.dart | 9 +- lib/pages/drama_detail/drama_detail_view.dart | 16 +- lib/pages/favorites/favorites_page.dart | 2 +- lib/pages/home/home_browse_logic.dart | 12 +- lib/pages/home/home_browse_view.dart | 4 +- lib/pages/home/home_view.dart | 24 ++- .../home/widgets/home_browse_header.dart | 2 +- lib/pages/home/widgets/home_drama_grid.dart | 177 ++++++++++++++---- lib/pages/mine/mine_view.dart | 6 +- .../mine/widgets/continue_watching_card.dart | 2 +- lib/pages/mine/widgets/mine_header.dart | 6 +- lib/pages/notices/notices_page.dart | 2 +- lib/pages/player/player_logic.dart | 2 +- lib/pages/player/player_view.dart | 8 +- .../player/widgets/home_feed_overlay.dart | 17 +- .../player/widgets/player_center_toggle.dart | 4 +- .../widgets/player_featured_comment.dart | 4 +- .../player/widgets/seek_progress_bar.dart | 6 +- lib/pages/settings/settings_page.dart | 2 +- lib/pages/store/store_page.dart | 2 +- lib/pages/wallet/wallet_page.dart | 4 +- .../watch_records/watch_records_page.dart | 2 +- test/drama_detail/drama_detail_view_test.dart | 2 +- test/empty_state/empty_state_test.dart | 12 +- test/home/app_bottom_nav_test.dart | 60 +++++- test/home/home_card_style_test.dart | 69 +++++++ test/home/home_nav_ui_test.dart | 45 ++++- test/home/home_preview_layout_test.dart | 4 +- test/home/k28_a_navigation_test.dart | 14 +- test/player/player_center_toggle_test.dart | 3 +- test/theme/app_dimens_test.dart | 12 +- test/theme/app_theme_test.dart | 95 ++++------ 56 files changed, 941 insertions(+), 409 deletions(-) create mode 100644 test/home/home_card_style_test.dart diff --git a/lib/app.dart b/lib/app.dart index b697138..e51d6ba 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -251,14 +251,14 @@ class _LumaDramaAppState extends State { localeResolutionCallback: (deviceLocale, _) => resolveAppLocale(deviceLocale), debugShowCheckedModeBanner: false, - themeMode: ThemeMode.dark, + themeMode: ThemeMode.light, theme: AppTheme.light(), - darkTheme: AppTheme.dark(), + darkTheme: AppTheme.light(), builder: (context, child) => AnnotatedRegion( - value: SystemUiOverlayStyle.light.copyWith( + value: SystemUiOverlayStyle.dark.copyWith( statusBarColor: Colors.transparent, systemNavigationBarColor: AppColors.bar, - systemNavigationBarIconBrightness: Brightness.light, + systemNavigationBarIconBrightness: Brightness.dark, ), child: child ?? const SizedBox.shrink(), ), diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index da46392..a71d739 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -2,79 +2,102 @@ 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 { - /// Primary CTA, selected navigation, playback progress. - static const blue = Color(0xFFFF5B3D); + /// 主操作、选中态、播放进度。 + static const blue = Color(0xFFFF7A1A); 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 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); - /// 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 quick = Color(0xFF202126); - static const input = Color(0xFF25252A); - static const goldCard = Color(0xFF201B1E); + /// 卡片轻阴影。 + static const cardShadow = Color(0x14FF8A3D); - /// 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 outlineLight = Color(0xFF8E8E93); - static const outlineVariantLight = Color(0xFFE4E4E7); + /// 橙色强调文案。 + static const accent = blue; + + /// 卡片描边。 + 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; } diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index c482578..3e5f95c 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -1,6 +1,6 @@ // Author: fruit.com.cn -/// Cross-page spacing, type, radius, and control sizes. +/// 跨页面复用的间距、字号、圆角和控件尺寸。 abstract final class AppDimens { static const double spaceXs = 4; static const double spaceSm = 8; @@ -9,17 +9,18 @@ abstract final class AppDimens { static const double spaceXl = 24; static const double pageHorizontal = 16; - static const double cardRadius = 12; + static const double cardRadius = 16; static const double authCardRadius = 20; static const double cardPadding = 16; - static const double innerRadius = 10; + static const double innerRadius = 12; static const double innerPadding = 12; - static const double pillRadius = 20; - static const double glassSmallRadius = 12; - static const double glassRadius = 14; - static const double glassCardRadius = 14; - static const double buttonRadius = 9; - static const double coverRadius = 10; + static const double pillRadius = 999; + static const double glassSmallRadius = 16; + static const double glassRadius = 20; + static const double glassCardRadius = 20; + static const double buttonRadius = 999; + static const double coverRadius = 16; + static const double sheetRadius = 20; static const double caption = 12; static const double emptyStateSubtitle = 12.5; @@ -33,7 +34,7 @@ abstract final class AppDimens { static const double previewCardTitle = 13; static const double previewSectionTitle = 14; static const double chipFont = 10; - static const double navLabel = 9; + static const double navLabel = 10; static const double buttonHeight = 48; static const double primaryButtonHeight = 52; @@ -44,8 +45,10 @@ abstract final class AppDimens { static const double tapTarget = 44; static const double contentHeaderHeight = 46; - static const double navHeight = 54; - static const double navIcon = 17; + static const double navHeight = 64; + static const double navIcon = 20; + static const double navCenter = 56; + static const double navCenterIcon = 28; static const double progressHeight = 3; static const double progressRadius = 2; static const double progressLineHeight = 3; @@ -60,7 +63,11 @@ abstract final class AppDimens { static const double watchPlaceholderHeight = 114; static const double heroBannerHeight = 176; + static const double heroBannerRadius = 20; - static const double cardMeta = 9; - static const double cardViews = 9; + static const double cardMeta = 12; + static const double cardViews = 11; + static const double cardTitle = 15; + static const double cardShadowBlur = 18; + static const double cardShadowY = 6; } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 324e540..07f446e 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -6,79 +6,62 @@ import 'package:flutter/services.dart'; import 'app_colors.dart'; import 'app_dimens.dart'; -/// Obsidian Flare themes. Dark is the product default; light mirrors the same -/// orange primary for system-light devices. +/// K28-S 浅色暖橙主题。 abstract final class AppTheme { - static const _lightBackground = Color(0xFFF7F7F8); - static const _darkBackground = AppColors.ink; - - /// Light theme for system light mode. - static ThemeData light() => _build( - brightness: Brightness.light, - surface: _lightBackground, - accent: AppColors.blue, + /// 页面顶部暖色到米白背景的渐变。 + static const LinearGradient pageGradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.surfaceTop, AppColors.ink], + stops: [0, 0.24], ); - /// Dark theme for system dark mode. - static ThemeData dark() => _build( - brightness: Brightness.dark, - surface: _darkBackground, - accent: AppColors.blue, + /// 卡片轻阴影。 + static const BoxShadow cardShadow = BoxShadow( + color: AppColors.cardShadow, + blurRadius: AppDimens.cardShadowBlur, + offset: Offset(0, AppDimens.cardShadowY), ); - static ThemeData _build({ - required Brightness brightness, - required Color surface, - required Color accent, - }) { - final dark = brightness == Brightness.dark; - final scheme = dark - ? const ColorScheme.dark( - primary: AppColors.blue, - onPrimary: AppColors.onPrimary, - secondary: AppColors.secondary, - onSecondary: AppColors.onPrimary, - primaryContainer: AppColors.bluePressed, - onPrimaryContainer: AppColors.onPrimary, - secondaryContainer: AppColors.raised, - onSecondaryContainer: AppColors.onPrimary, - surface: _darkBackground, - 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, - ); + /// 全局浅色暖橙主题。 + static ThemeData light() { + const scheme = ColorScheme.light( + primary: AppColors.blue, + onPrimary: AppColors.onPrimary, + secondary: AppColors.secondary, + onSecondary: AppColors.onPrimary, + primaryContainer: AppColors.surfaceTop, + onPrimaryContainer: AppColors.onSurface, + secondaryContainer: AppColors.badgePink, + onSecondaryContainer: AppColors.badgePinkText, + surface: AppColors.ink, + onSurface: AppColors.onSurface, + outline: AppColors.border, + outlineVariant: AppColors.border, + surfaceTint: AppColors.blue, + inversePrimary: AppColors.bluePressed, + ); final base = ThemeData( useMaterial3: true, - brightness: brightness, + brightness: Brightness.light, colorScheme: scheme, - scaffoldBackgroundColor: surface, + scaffoldBackgroundColor: AppColors.ink, fontFamily: 'PlusJakartaSans', - progressIndicatorTheme: ProgressIndicatorThemeData(color: accent), - sliderTheme: SliderThemeData( - activeTrackColor: accent, + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: AppColors.blue, + ), + sliderTheme: const SliderThemeData( + activeTrackColor: AppColors.blue, + inactiveTrackColor: AppColors.border, thumbColor: AppColors.onPrimary, ), - tabBarTheme: TabBarThemeData(labelColor: accent, indicatorColor: accent), + tabBarTheme: const TabBarThemeData( + labelColor: AppColors.blue, + indicatorColor: AppColors.blue, + unselectedLabelColor: AppColors.muted, + ), textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom(foregroundColor: accent), + style: TextButton.styleFrom(foregroundColor: AppColors.blue), ), filledButtonTheme: FilledButtonThemeData( style: ButtonStyle( @@ -87,25 +70,26 @@ abstract final class AppTheme { minimumSize: const WidgetStatePropertyAll( Size(0, AppDimens.buttonHeight), ), + shape: const WidgetStatePropertyAll(StadiumBorder()), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( - foregroundColor: WidgetStatePropertyAll( - dark ? AppColors.blueSoft : AppColors.bluePressed, - ), + foregroundColor: const WidgetStatePropertyAll(AppColors.blue), minimumSize: const WidgetStatePropertyAll( Size(0, AppDimens.buttonHeight), ), - side: WidgetStatePropertyAll( - BorderSide( - color: dark ? AppColors.outlineDark : AppColors.outlineLight, - ), + side: const WidgetStatePropertyAll( + BorderSide(color: AppColors.border), ), + shape: const WidgetStatePropertyAll(StadiumBorder()), ), ), appBarTheme: const AppBarTheme( - systemOverlayStyle: SystemUiOverlayStyle.light, + backgroundColor: AppColors.ink, + foregroundColor: AppColors.onSurface, + elevation: 0, + systemOverlayStyle: SystemUiOverlayStyle.dark, titleTextStyle: TextStyle( color: AppColors.onSurface, fontFamily: 'Outfit', @@ -113,6 +97,16 @@ abstract final class AppTheme { 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( thumbColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) @@ -120,36 +114,35 @@ abstract final class AppTheme { : null, ), trackColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.selected) ? accent : null, + (states) => + states.contains(WidgetState.selected) ? AppColors.blue : null, ), ), - snackBarTheme: SnackBarThemeData( - backgroundColor: AppColors.bluePressed, - contentTextStyle: const TextStyle(color: AppColors.onPrimary), + snackBarTheme: const SnackBarThemeData( + backgroundColor: AppColors.onSurface, + contentTextStyle: TextStyle(color: AppColors.onPrimary), ), ); final textTheme = base.textTheme .apply( fontFamily: 'PlusJakartaSans', - bodyColor: dark ? AppColors.cream : AppColors.ink, - displayColor: dark ? AppColors.onSurface : AppColors.ink, + bodyColor: AppColors.cream, + displayColor: AppColors.onSurface, ) .copyWith( - displayLarge: _display(base.textTheme.displayLarge, dark), - displayMedium: _display(base.textTheme.displayMedium, dark), - displaySmall: _display(base.textTheme.displaySmall, dark), - headlineLarge: _display(base.textTheme.headlineLarge, dark), - headlineMedium: _display(base.textTheme.headlineMedium, dark), - headlineSmall: _display(base.textTheme.headlineSmall, dark), - titleLarge: _display(base.textTheme.titleLarge, dark), + displayLarge: _display(base.textTheme.displayLarge), + displayMedium: _display(base.textTheme.displayMedium), + displaySmall: _display(base.textTheme.displaySmall), + headlineLarge: _display(base.textTheme.headlineLarge), + headlineMedium: _display(base.textTheme.headlineMedium), + headlineSmall: _display(base.textTheme.headlineSmall), + titleLarge: _display(base.textTheme.titleLarge), ); return base.copyWith(textTheme: textTheme); } - static TextStyle? _display(TextStyle? style, bool dark) => style?.copyWith( - fontFamily: 'Outfit', - color: dark ? AppColors.onSurface : AppColors.ink, - ); + static TextStyle? _display(TextStyle? style) => + style?.copyWith(fontFamily: 'Outfit', color: AppColors.onSurface); static Color _buttonColor(Set states) { if (states.contains(WidgetState.pressed) || diff --git a/lib/core/widgets/app_bottom_nav.dart b/lib/core/widgets/app_bottom_nav.dart index 880ae8f..0a7a72f 100644 --- a/lib/core/widgets/app_bottom_nav.dart +++ b/lib/core/widgets/app_bottom_nav.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import '../theme/app_colors.dart'; import '../theme/app_dimens.dart'; -/// One destination rendered by [AppBottomNav]. +/// 底部导航普通目的地。 class AppBottomNavItem { const AppBottomNavItem({ required this.tabKey, @@ -26,41 +26,69 @@ class AppBottomNavItem { 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 { const AppBottomNav({ super.key, required this.items, + this.centerAction, this.backgroundColor = AppColors.bar, this.barKey, }); final List items; + final AppBottomNavCenterAction? centerAction; final Color backgroundColor; final Key? barKey; @override - Widget build(BuildContext context) => ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: SafeArea( - top: false, - child: Container( - key: barKey, - height: AppDimens.navHeight, - decoration: BoxDecoration( - color: backgroundColor.withValues(alpha: 0.95), - border: const Border( - top: BorderSide(color: AppColors.outlineVariantDark), + Widget build(BuildContext context) { + final action = centerAction; + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: SafeArea( + top: false, + child: Container( + key: barKey, + height: AppDimens.navHeight, + decoration: BoxDecoration( + color: backgroundColor.withValues(alpha: 0.95), + border: const Border(top: BorderSide(color: AppColors.border)), ), - ), - child: Row( - children: [for (final item in items) _NavDestination(item: item)], + child: action == null + ? Row( + 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 { @@ -78,7 +106,7 @@ class _NavDestination extends StatelessWidget { key: item.tabKey, onTap: item.onTap, child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 48), + constraints: const BoxConstraints(minHeight: AppDimens.navHeight), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -91,7 +119,7 @@ class _NavDestination extends StatelessWidget { size: AppDimens.navIcon, color: item.selected ? AppColors.blue - : Colors.white.withValues(alpha: 0.45), + : AppColors.navUnselected, ), ), const SizedBox(height: 2), @@ -104,7 +132,7 @@ class _NavDestination extends StatelessWidget { fontWeight: item.selected ? FontWeight.w700 : FontWeight.w500, color: item.selected ? AppColors.blue - : Colors.white.withValues(alpha: 0.45), + : AppColors.navUnselected, 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, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/glass_panel.dart b/lib/core/widgets/glass_panel.dart index 02d144c..1edd1fb 100644 --- a/lib/core/widgets/glass_panel.dart +++ b/lib/core/widgets/glass_panel.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import '../theme/app_colors.dart'; import '../theme/app_dimens.dart'; -/// Dark glass surface used by the home info card and floating controls. +/// 首页信息卡与浮层控件使用的浅色玻璃面板。 class GlassPanel extends StatelessWidget { const GlassPanel({ super.key, @@ -38,7 +38,7 @@ class GlassPanel extends StatelessWidget { const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0x24FFFFFF), Color(0x0DFFFFFF)], + colors: [Color(0xE6FFFFFF), Color(0xB3FFFDFB)], ), ), child: Padding(padding: padding ?? EdgeInsets.zero, child: child), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 30c0b29..c1077e2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -82,6 +82,18 @@ "count": {"type": "int"} } }, + "allEpisodesBadge": "All {count}", + "@allEpisodesBadge": { + "placeholders": { + "count": {"type": "int"} + } + }, + "lockedEpisodeCount": "{count} locked", + "@lockedEpisodeCount": { + "placeholders": { + "count": {"type": "int"} + } + }, "watchedPercent": "Watched {percent}%", "@watchedPercent": { "placeholders": { diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 6ec3a23..f431983 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -82,6 +82,18 @@ "count": {"type": "int"} } }, + "allEpisodesBadge": "Semua {count}", + "@allEpisodesBadge": { + "placeholders": { + "count": {"type": "int"} + } + }, + "lockedEpisodeCount": "{count} terkunci", + "@lockedEpisodeCount": { + "placeholders": { + "count": {"type": "int"} + } + }, "watchedPercent": "Sudah ditonton {percent}%", "@watchedPercent": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 44dbabc..298338c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -460,6 +460,18 @@ abstract class AppLocalizations { /// **'{count} views'** 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. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0195af7..18d075b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -201,6 +201,16 @@ class AppLocalizationsEn extends AppLocalizations { return '$count views'; } + @override + String allEpisodesBadge(int count) { + return 'All $count'; + } + + @override + String lockedEpisodeCount(int count) { + return '$count locked'; + } + @override String watchedPercent(int percent) { return 'Watched $percent%'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 2a36cc2..fcb7857 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -201,6 +201,16 @@ class AppLocalizationsId extends AppLocalizations { return '$count tayangan'; } + @override + String allEpisodesBadge(int count) { + return 'Semua $count'; + } + + @override + String lockedEpisodeCount(int count) { + return '$count terkunci'; + } + @override String watchedPercent(int percent) { return 'Sudah ditonton $percent%'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 1cf3021..4990bb9 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -196,6 +196,16 @@ class AppLocalizationsZh extends AppLocalizations { return '$count 次播放'; } + @override + String allEpisodesBadge(int count) { + return '全 $count 集'; + } + + @override + String lockedEpisodeCount(int count) { + return '锁 $count'; + } + @override String watchedPercent(int percent) { return '已观看 $percent%'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8bbee93..111cb7c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -82,6 +82,18 @@ "count": {"type": "int"} } }, + "allEpisodesBadge": "全 {count} 集", + "@allEpisodesBadge": { + "placeholders": { + "count": {"type": "int"} + } + }, + "lockedEpisodeCount": "锁 {count}", + "@lockedEpisodeCount": { + "placeholders": { + "count": {"type": "int"} + } + }, "watchedPercent": "已观看 {percent}%", "@watchedPercent": { "placeholders": { diff --git a/lib/pages/account/account_deactivate_page.dart b/lib/pages/account/account_deactivate_page.dart index cf1799f..4b04d21 100644 --- a/lib/pages/account/account_deactivate_page.dart +++ b/lib/pages/account/account_deactivate_page.dart @@ -50,7 +50,7 @@ class _AccountDeactivatePageState extends State { final signedIn = context.watch().isSignedIn; final canShow = signedIn || _logic.scheduledAt != null; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('account-deactivate-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/account/account_devices_page.dart b/lib/pages/account/account_devices_page.dart index b64be9c..804a192 100644 --- a/lib/pages/account/account_devices_page.dart +++ b/lib/pages/account/account_devices_page.dart @@ -50,7 +50,7 @@ class _AccountDevicesPageState extends State { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('account-devices-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/account/account_export_page.dart b/lib/pages/account/account_export_page.dart index d17cde7..1bc0a9d 100644 --- a/lib/pages/account/account_export_page.dart +++ b/lib/pages/account/account_export_page.dart @@ -51,7 +51,7 @@ class _AccountExportPageState extends State { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('account-export-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/account/account_profile_page.dart b/lib/pages/account/account_profile_page.dart index d3aeac9..6b9ef9e 100644 --- a/lib/pages/account/account_profile_page.dart +++ b/lib/pages/account/account_profile_page.dart @@ -71,7 +71,7 @@ class _AccountProfilePageState extends State { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('account-profile-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart index 0edfe59..18345a7 100644 --- a/lib/pages/auth/auth_view.dart +++ b/lib/pages/auth/auth_view.dart @@ -141,7 +141,7 @@ class _AuthPageState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( resizeToAvoidBottomInset: true, backgroundColor: AppColors.ink, @@ -332,7 +332,7 @@ class _AuthPageState extends State { Text( l10n.authLandingSubtitle, textAlign: TextAlign.center, - style: const TextStyle(color: Color(0xFF9A96A3), fontSize: 11), + style: const TextStyle(color: AppColors.subtle, fontSize: 11), ), const SizedBox(height: 22), _AuthEntryButton( @@ -358,7 +358,7 @@ class _AuthPageState extends State { key: const Key('auth-legal'), textAlign: TextAlign.center, style: const TextStyle( - color: Color(0xFF6B6772), + color: AppColors.muted, fontSize: 9, height: 1.6, ), @@ -442,8 +442,7 @@ class _AuthEntryButton extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (icon != null) - Icon(icon, size: 16, color: foreground), + if (icon != null) Icon(icon, size: 16, color: foreground), if (icon != null) const SizedBox(width: 7), Flexible( child: Text( diff --git a/lib/pages/create/create_view.dart b/lib/pages/create/create_view.dart index 4e0a414..313beb0 100644 --- a/lib/pages/create/create_view.dart +++ b/lib/pages/create/create_view.dart @@ -7,7 +7,7 @@ import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../l10n/app_localizations.dart'; -/// K28-A1 placeholder for the future creation workspace. +/// K28-A1 创作页占位骨架。 class CreatePage extends StatelessWidget { const CreatePage({super.key}); @@ -15,7 +15,7 @@ class CreatePage extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: SafeArea( bottom: false, child: Padding( diff --git a/lib/pages/discover/category_dramas_page.dart b/lib/pages/discover/category_dramas_page.dart index 385a52c..0687f49 100644 --- a/lib/pages/discover/category_dramas_page.dart +++ b/lib/pages/discover/category_dramas_page.dart @@ -27,7 +27,7 @@ class CategoryDramasPage extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('category-dramas-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index 277177c..71b06dd 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -108,9 +108,9 @@ class _DiscoverPageState extends State { ), ); return AnnotatedRegion( - value: SystemUiOverlayStyle.light, + value: SystemUiOverlayStyle.dark, child: Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: embedded ? content : Scaffold(backgroundColor: AppColors.ink, body: content), @@ -340,6 +340,10 @@ class _DiscoverPageState extends State { } Widget _bottomNav(AppLocalizations l10n) => AppBottomNav( + centerAction: AppBottomNavCenterAction( + label: l10n.createTab, + onTap: () => _act(DiscoverPageAction.create), + ), items: [ AppBottomNavItem( tabKey: const Key('discover-nav-home'), diff --git a/lib/pages/discover/widgets/discover_header.dart b/lib/pages/discover/widgets/discover_header.dart index 1d20061..c55adb5 100644 --- a/lib/pages/discover/widgets/discover_header.dart +++ b/lib/pages/discover/widgets/discover_header.dart @@ -88,12 +88,12 @@ class DiscoverHeader extends StatelessWidget { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF6A5CFF), AppColors.blue], + colors: [AppColors.secondary, AppColors.blue], ), ), child: const Icon( Icons.person_rounded, - color: AppColors.onSurface, + color: AppColors.onPrimary, size: 18, ), ), @@ -124,7 +124,7 @@ class _HeaderIcon extends StatelessWidget { onPressed: onTap, padding: EdgeInsets.zero, style: IconButton.styleFrom( - backgroundColor: const Color(0xFF2C2D33), + backgroundColor: AppColors.raised, foregroundColor: AppColors.onSurface, ), icon: Icon(icon, size: 18), diff --git a/lib/pages/discover/widgets/drama_grid.dart b/lib/pages/discover/widgets/drama_grid.dart index 7974b5d..437f827 100644 --- a/lib/pages/discover/widgets/drama_grid.dart +++ b/lib/pages/discover/widgets/drama_grid.dart @@ -5,11 +5,15 @@ import 'package:flutter/material.dart'; import '../../../core/config/app_config.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/theme/app_dimens.dart'; +import '../../../core/theme/app_theme.dart'; import '../../../l10n/app_localizations.dart'; const int _columns = 2; const double _columnGap = 10; const double _cardGap = 10; +const double _addSize = 28; +const double _addIconSize = 18; /// 剧场/分类页共用的双列剧目网格。 class DramaGrid extends StatelessWidget { @@ -71,19 +75,33 @@ class DramaGridCard extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - key: Key('home-drama-card-${drama.id}'), - onTap: onTap, - borderRadius: BorderRadius.circular(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _cover(), - const SizedBox(height: 5), - _title(), - const SizedBox(height: 2), - _genre(), - ], + return Container( + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(AppDimens.cardRadius), + boxShadow: const [AppTheme.cardShadow], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(AppDimens.cardRadius), + clipBehavior: Clip.antiAlias, + child: InkWell( + 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); return ClipRRect( key: Key('home-drama-cover-${drama.id}'), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppDimens.coverRadius), child: Stack( children: [ _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( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + key: Key('drama-tag-${drama.id}'), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( - color: AppColors.blue, - borderRadius: BorderRadius.circular(6), + color: AppColors.badgePink, + borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( - l10n.free, + free ? l10n.free : l10n.originalTag, style: const TextStyle( - color: AppColors.onPrimary, - fontSize: 8, + color: AppColors.badgePinkText, + fontSize: AppDimens.cardMeta, 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() { return Text( drama.title, @@ -130,7 +204,7 @@ class DramaGridCard extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, - fontSize: 11, + fontSize: AppDimens.cardTitle, fontWeight: FontWeight.w800, ), ); @@ -143,7 +217,32 @@ class DramaGridCard extends StatelessWidget { key: Key('home-drama-genre-${drama.id}'), maxLines: 1, 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, + ), + ), + ], ); } } diff --git a/lib/pages/discover/widgets/ranking_section.dart b/lib/pages/discover/widgets/ranking_section.dart index 9dbe2ef..e3ade9d 100644 --- a/lib/pages/discover/widgets/ranking_section.dart +++ b/lib/pages/discover/widgets/ranking_section.dart @@ -135,7 +135,7 @@ class _RankingSectionState extends State { final selected = logic.board == board; return Material( key: key, - color: selected ? AppColors.blue : AppColors.raised, + color: selected ? AppColors.blue : AppColors.card, borderRadius: BorderRadius.circular(999), clipBehavior: Clip.antiAlias, child: InkWell( @@ -145,7 +145,7 @@ class _RankingSectionState extends State { child: Text( label, style: TextStyle( - color: selected ? AppColors.onPrimary : AppColors.muted, + color: selected ? AppColors.onPrimary : AppColors.onSurface, fontSize: 9, fontWeight: FontWeight.w700, ), diff --git a/lib/pages/downloads/downloads_page.dart b/lib/pages/downloads/downloads_page.dart index ea53383..46e87f4 100644 --- a/lib/pages/downloads/downloads_page.dart +++ b/lib/pages/downloads/downloads_page.dart @@ -54,7 +54,7 @@ class _DownloadsPageState extends State { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('downloads-page'), backgroundColor: AppColors.ink, @@ -168,11 +168,8 @@ class _DownloadsBody extends StatelessWidget { ); Navigator.of(context).push( MaterialPageRoute( - builder: (_) => PlayerPage( - drama: drama, - initialIndex: 0, - downloads: downloads, - ), + builder: (_) => + PlayerPage(drama: drama, initialIndex: 0, downloads: downloads), ), ); } diff --git a/lib/pages/drama_detail/drama_detail_view.dart b/lib/pages/drama_detail/drama_detail_view.dart index 952dda7..0f9ef45 100644 --- a/lib/pages/drama_detail/drama_detail_view.dart +++ b/lib/pages/drama_detail/drama_detail_view.dart @@ -73,7 +73,7 @@ class _DramaDetailPageState extends State { @override Widget build(BuildContext context) { return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: ListenableBuilder( listenable: logic, builder: (context, _) => _page(context), @@ -171,7 +171,7 @@ class _DramaDetailPageState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Color(0x33000000), Color(0xCC0B0C10)], + colors: [Color(0x33000000), AppColors.onSurface], ), ), ), @@ -363,7 +363,7 @@ class _DramaDetailPageState extends State { ? AppColors.blue : locked ? AppColors.secondary.withValues(alpha: 0.16) - : const Color(0xFF29272B), + : AppColors.raised, borderRadius: BorderRadius.circular(9), ), child: Text( @@ -461,8 +461,8 @@ class _DramaDetailPageState extends State { child: Container( width: 36, height: 36, - decoration: const BoxDecoration( - color: Color(0x80000000), + decoration: BoxDecoration( + color: AppColors.card.withValues(alpha: 0.92), shape: BoxShape.circle, ), child: const Icon( @@ -562,8 +562,8 @@ class _RoundAction extends StatelessWidget { return Container( width: 36, height: 36, - decoration: const BoxDecoration( - color: Color(0x80000000), + decoration: BoxDecoration( + color: AppColors.card.withValues(alpha: 0.92), shape: BoxShape.circle, ), child: IconButton( @@ -592,7 +592,7 @@ class _ResumeBar extends StatelessWidget { child: LinearProgressIndicator( value: fraction, minHeight: 3, - backgroundColor: const Color(0xFF474248), + backgroundColor: AppColors.border, valueColor: const AlwaysStoppedAnimation(AppColors.blue), ), ), diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index d1626fb..2e5fa8a 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -51,7 +51,7 @@ class _FavoritesPageState extends State { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( backgroundColor: AppColors.ink, appBar: AppBar( diff --git a/lib/pages/home/home_browse_logic.dart b/lib/pages/home/home_browse_logic.dart index 5e16f7f..907c614 100644 --- a/lib/pages/home/home_browse_logic.dart +++ b/lib/pages/home/home_browse_logic.dart @@ -7,10 +7,10 @@ import 'package:flutter/foundation.dart'; import '../../core/data/models/watch_progress.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 } -/// Local UI state for the browse-first home page. +/// 浏览型首页的本地 UI 状态。 class HomeBrowseLogic extends ChangeNotifier { HomeBrowseLogic({required this.history}) { unawaited(refreshRecent()); @@ -24,7 +24,7 @@ class HomeBrowseLogic extends ChangeNotifier { bool loadingRecent = false; bool _disposed = false; - /// First unfinished record with a real position. + /// 第一条未完成且存在真实播放位置的记录。 WatchProgress? get primaryRecord { for (final record in recent) { if (!record.finished && record.hasPosition) return record; @@ -32,14 +32,14 @@ class HomeBrowseLogic extends ChangeNotifier { return null; } - /// Changes the selected browse channel without triggering network work. + /// 切换频道,不触发额外网络请求。 void selectChannel(HomeChannel value) { if (channel == value) return; channel = value; _notify(); } - /// Reloads continuation data; signed-out reads simply return an empty list. + /// 重新加载续看数据;未登录时返回空列表。 Future refreshRecent() async { loadingRecent = true; _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}) { if (active && !wasActive) unawaited(refreshRecent()); } diff --git a/lib/pages/home/home_browse_view.dart b/lib/pages/home/home_browse_view.dart index 724e43c..6782f33 100644 --- a/lib/pages/home/home_browse_view.dart +++ b/lib/pages/home/home_browse_view.dart @@ -20,7 +20,7 @@ import 'home_browse_logic.dart'; import 'widgets/home_browse_header.dart'; import 'widgets/home_drama_grid.dart'; -/// Browse-first home page with channels, hero, continuation, and catalogue. +/// 浏览型首页:频道、Hero、继续观看与剧目目录。 class HomeBrowseView extends StatefulWidget { const HomeBrowseView({ super.key, @@ -214,7 +214,7 @@ class _HomeBrowseViewState extends State { color: selected ? AppColors.blue : AppColors.border, ), labelStyle: TextStyle( - color: selected ? AppColors.onPrimary : AppColors.muted, + color: selected ? AppColors.onPrimary : AppColors.onSurface, fontSize: AppDimens.chipFont, fontWeight: FontWeight.w700, ), diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index c4e4e37..b5901e1 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -8,7 +8,6 @@ import 'package:provider/provider.dart'; import '../../core/data/models/drama.dart'; import '../../core/data/services/catalog_controller.dart'; import '../../core/platform/haptics.dart'; -import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/empty_state.dart'; @@ -32,8 +31,11 @@ class HomePage extends StatelessWidget { if (!catalog.settled) return const SplashPage(); final l10n = AppLocalizations.of(context)!; return Theme( - data: AppTheme.dark(), - child: _buildHome(context, catalog, l10n), + data: AppTheme.light(), + child: DecoratedBox( + decoration: const BoxDecoration(gradient: AppTheme.pageGradient), + child: _buildHome(context, catalog, l10n), + ), ); } @@ -44,19 +46,19 @@ class HomePage extends StatelessWidget { ) { if (catalog.loading) { return const Scaffold( - backgroundColor: AppColors.ink, + backgroundColor: Colors.transparent, body: Center(child: CircularProgressIndicator()), ); } if (catalog.error != null) { return Scaffold( - backgroundColor: AppColors.ink, + backgroundColor: Colors.transparent, body: HomeFailureView(onRetry: catalog.load), ); } if (catalog.dramas.isEmpty) { return Scaffold( - backgroundColor: AppColors.ink, + backgroundColor: Colors.transparent, body: EmptyState( key: const Key('home-empty'), icon: Icons.movie_outlined, @@ -75,7 +77,7 @@ class HomePage extends StatelessWidget { } } -/// Home catalog failure with a single recovery action. +/// 首页目录加载失败与重试入口。 class HomeFailureView extends StatelessWidget { 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 { const _HomeTabs({super.key, required this.dramas}); @@ -136,7 +138,7 @@ class _HomeTabsState extends State<_HomeTabs> { }, child: Scaffold( key: logic.scaffoldKey, - backgroundColor: AppColors.ink, + backgroundColor: Colors.transparent, body: _body(context), bottomNavigationBar: logic.videoImmersive ? null : _nav(context, l10n), ), @@ -193,6 +195,10 @@ class _HomeTabsState extends State<_HomeTabs> { Widget _nav(BuildContext context, AppLocalizations l10n) { return AppBottomNav( barKey: const Key('home-tabs-bar'), + centerAction: AppBottomNavCenterAction( + label: l10n.createTab, + onTap: () => _selectTab(context, 2), + ), items: [ _navItem(context, l10n, 0), _navItem(context, l10n, 1), diff --git a/lib/pages/home/widgets/home_browse_header.dart b/lib/pages/home/widgets/home_browse_header.dart index 91406db..71f6208 100644 --- a/lib/pages/home/widgets/home_browse_header.dart +++ b/lib/pages/home/widgets/home_browse_header.dart @@ -8,7 +8,7 @@ import '../../../l10n/app_localizations.dart'; const double _searchHeight = 36; -/// Fixed home header with catalogue search, weekday, and profile entry. +/// 首页固定顶栏:搜索、星期与头像入口。 class HomeBrowseHeader extends StatelessWidget { const HomeBrowseHeader({ super.key, diff --git a/lib/pages/home/widgets/home_drama_grid.dart b/lib/pages/home/widgets/home_drama_grid.dart index 3a884a8..ce650a8 100644 --- a/lib/pages/home/widgets/home_drama_grid.dart +++ b/lib/pages/home/widgets/home_drama_grid.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; +import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; @@ -12,8 +13,10 @@ const int _columns = 2; const double _columnGap = 10; const double _cardGap = 10; 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 { const HomeDramaGrid({ 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 { const HomeDramaCard({ super.key, @@ -72,21 +75,35 @@ class HomeDramaCard extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - key: Key('home-drama-card-${drama.id}'), - onTap: onTap, - borderRadius: BorderRadius.circular(AppDimens.coverRadius), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _cover(), - const SizedBox(height: AppDimens.spaceXs), - _title(), - const SizedBox(height: 2), - _genre(), - const SizedBox(height: AppDimens.spaceXs), - _metadata(), - ], + return Container( + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(AppDimens.cardRadius), + boxShadow: const [AppTheme.cardShadow], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(AppDimens.cardRadius), + clipBehavior: Clip.antiAlias, + child: InkWell( + 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), + _genre(), + const SizedBox(height: AppDimens.spaceXs), + _metadata(), + ], + ), + ), + ), ), ); } @@ -104,7 +121,10 @@ class HomeDramaCard extends StatelessWidget { showFallbackIcon: true, 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); } - 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( - 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( - color: AppColors.blue, - borderRadius: BorderRadius.circular(6), + color: AppColors.badgePink, + borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( - l10n.free, + label, style: const TextStyle( - color: AppColors.onPrimary, + color: AppColors.badgePinkText, fontSize: AppDimens.cardMeta, 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() { return Text( drama.title, @@ -141,7 +218,7 @@ class HomeDramaCard extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, - fontSize: AppDimens.previewSectionTitle, + fontSize: AppDimens.cardTitle, fontWeight: FontWeight.w800, ), ); @@ -162,24 +239,44 @@ class HomeDramaCard extends StatelessWidget { } Widget _metadata() { - return Wrap( - spacing: AppDimens.spaceSm, - runSpacing: AppDimens.spaceXs, + return Row( children: [ - Text( - l10n.episodeCount(drama.episodeCount), - key: Key('home-drama-episode-count-${drama.id}'), - style: const TextStyle( - color: AppColors.subtle, - fontSize: AppDimens.cardViews, + Expanded( + child: Wrap( + spacing: AppDimens.spaceSm, + runSpacing: AppDimens.spaceXs, + children: [ + 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( - l10n.viewCountLabel(drama.viewCount), - key: Key('home-drama-views-${drama.id}'), - style: const TextStyle( - color: AppColors.subtle, - fontSize: AppDimens.cardViews, + Container( + key: Key('home-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, ), ), ], diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index 0beee47..b52bf77 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -92,7 +92,7 @@ class _MyPageState extends State { ), ); return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: embedded ? content : Scaffold(backgroundColor: AppColors.ink, body: content), @@ -242,6 +242,10 @@ class _MyPageState extends State { Widget _bottomNav(AppLocalizations l10n) { return AppBottomNav( + centerAction: AppBottomNavCenterAction( + label: l10n.createTab, + onTap: () => _act(MyPageAction.create), + ), items: [ AppBottomNavItem( tabKey: const Key('mine-nav-home'), diff --git a/lib/pages/mine/widgets/continue_watching_card.dart b/lib/pages/mine/widgets/continue_watching_card.dart index e849a8d..6e17981 100644 --- a/lib/pages/mine/widgets/continue_watching_card.dart +++ b/lib/pages/mine/widgets/continue_watching_card.dart @@ -101,7 +101,7 @@ class ContinueWatchingCard extends StatelessWidget { key: progressKey, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 9), + style: const TextStyle(color: AppColors.muted, fontSize: 9), ), ], ); diff --git a/lib/pages/mine/widgets/mine_header.dart b/lib/pages/mine/widgets/mine_header.dart index f2559c2..a51cc36 100644 --- a/lib/pages/mine/widgets/mine_header.dart +++ b/lib/pages/mine/widgets/mine_header.dart @@ -46,12 +46,12 @@ class MineHeader extends StatelessWidget { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF6A5CFF), AppColors.blue], + colors: [AppColors.secondary, AppColors.blue], ), ), child: const Icon( Icons.person_rounded, - color: AppColors.onSurface, + color: AppColors.onPrimary, size: 22, ), ); @@ -76,7 +76,7 @@ class MineHeader extends StatelessWidget { subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 10), + style: const TextStyle(color: AppColors.muted, fontSize: 10), ), ], ); diff --git a/lib/pages/notices/notices_page.dart b/lib/pages/notices/notices_page.dart index 0b16bac..a8e2f27 100644 --- a/lib/pages/notices/notices_page.dart +++ b/lib/pages/notices/notices_page.dart @@ -44,7 +44,7 @@ class _NoticesPageState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('notices-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index 5d47ab1..14f3bc0 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -669,7 +669,7 @@ class PlayerLogic extends ChangeNotifier { maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: Color(0xFFB7B3B8), + color: AppColors.muted, fontSize: 14, fontWeight: FontWeight.w400, ), diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 2f462d3..95935ea 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -146,9 +146,9 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ), ); return AnnotatedRegion( - value: SystemUiOverlayStyle.light, + value: SystemUiOverlayStyle.dark, child: Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: widget.embedded ? content : Scaffold(backgroundColor: AppColors.ink, body: content), @@ -1155,7 +1155,7 @@ class _TopAction extends StatelessWidget { onPressed: onPressed, padding: EdgeInsets.zero, style: IconButton.styleFrom( - backgroundColor: const Color(0xFF2C2D33), + backgroundColor: AppColors.card.withValues(alpha: 0.94), foregroundColor: AppColors.onSurface, ), icon: Icon(icon, size: 18), @@ -1179,7 +1179,7 @@ class _PlayerAction extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - color: const Color(0xFF2A2A30), + color: AppColors.card.withValues(alpha: 0.94), borderRadius: BorderRadius.circular(9), clipBehavior: Clip.antiAlias, child: InkWell( diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 81dc343..220d1ab 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -160,12 +160,7 @@ class HomeFeedOverlay extends StatelessWidget { radius: 14, blur: 6, padding: const EdgeInsets.all(12), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xBF0F0F14), Color(0xA60F0F14)], - ), - borderColor: Colors.white.withValues(alpha: 0.06), + borderColor: AppColors.border, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -184,13 +179,13 @@ class HomeFeedOverlay extends StatelessWidget { if (drama.category.isNotEmpty && drama.category != genre) _tag( drama.category, - background: const Color(0xFF333333), - foreground: const Color(0xFFDDDDDD), + background: AppColors.raised, + foreground: AppColors.onSurface, ), Text( l10n.episodeOfTotal(episode.number, drama.episodeCount), 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'), onPressed: onEpisodes, style: FilledButton.styleFrom( - backgroundColor: const Color(0xFF2A2A30), + backgroundColor: AppColors.raised, foregroundColor: AppColors.onSurface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -362,7 +357,7 @@ class _RoundIcon extends StatelessWidget { onPressed: onPressed, padding: EdgeInsets.zero, style: IconButton.styleFrom( - backgroundColor: const Color(0xCC2C2D33), + backgroundColor: AppColors.card.withValues(alpha: 0.94), foregroundColor: AppColors.onSurface, ), icon: Icon(icon, size: 18), diff --git a/lib/pages/player/widgets/player_center_toggle.dart b/lib/pages/player/widgets/player_center_toggle.dart index b15e355..c939301 100644 --- a/lib/pages/player/widgets/player_center_toggle.dart +++ b/lib/pages/player/widgets/player_center_toggle.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; -/// Shared play/pause control used by the home feed and full-screen player. +/// 首页沉浸流与全屏播放器共用的播放/暂停按钮。 class PlayerCenterToggle extends StatelessWidget { const PlayerCenterToggle({ super.key, @@ -26,7 +26,7 @@ class PlayerCenterToggle extends StatelessWidget { width: 52, height: 52, decoration: const BoxDecoration( - color: Color(0x80000000), + color: AppColors.blue, shape: BoxShape.circle, ), child: Icon( diff --git a/lib/pages/player/widgets/player_featured_comment.dart b/lib/pages/player/widgets/player_featured_comment.dart index e888ba8..1f1381f 100644 --- a/lib/pages/player/widgets/player_featured_comment.dart +++ b/lib/pages/player/widgets/player_featured_comment.dart @@ -117,7 +117,7 @@ class _PlayerFeaturedCommentState extends State { Text( '· $time', style: const TextStyle( - color: Color(0xFF777777), + color: AppColors.muted, fontSize: 9, ), ), @@ -133,7 +133,7 @@ class _PlayerFeaturedCommentState extends State { maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: Color(0xFFDDDDDD), + color: AppColors.onSurface, fontSize: 10, height: 1.5, ), diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index 6680fa1..57f7503 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -127,10 +127,8 @@ class _SeekProgressBarState extends State { ? (scrubbing ? 3 : 1.5) : (scrubbing ? 4 : 3), activeTrackColor: AppColors.blue, - inactiveTrackColor: widget.compact - ? Colors.white.withValues(alpha: 0.18) - : const Color(0xFF474248), - thumbColor: widget.compact ? AppColors.blue : Colors.white, + inactiveTrackColor: AppColors.border, + thumbColor: AppColors.onPrimary, thumbShape: RoundSliderThumbShape( enabledThumbRadius: widget.compact ? (scrubbing ? 6 : 4) diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index b1f1ec8..8d06c0a 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -22,7 +22,7 @@ class SettingsPage extends StatelessWidget { final l10n = AppLocalizations.of(context)!; final auth = context.watch(); return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('settings-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/store/store_page.dart b/lib/pages/store/store_page.dart index e5501bd..9fe52d6 100644 --- a/lib/pages/store/store_page.dart +++ b/lib/pages/store/store_page.dart @@ -56,7 +56,7 @@ class _StorePageState extends State { final l10n = AppLocalizations.of(context)!; final isVip = widget.type == StoreController.vipType; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('store-page'), backgroundColor: AppColors.ink, diff --git a/lib/pages/wallet/wallet_page.dart b/lib/pages/wallet/wallet_page.dart index b9361de..7748314 100644 --- a/lib/pages/wallet/wallet_page.dart +++ b/lib/pages/wallet/wallet_page.dart @@ -44,7 +44,7 @@ class _WalletPageState extends State { final l10n = AppLocalizations.of(context)!; final wallet = context.watch(); return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('wallet-page'), backgroundColor: AppColors.ink, @@ -188,7 +188,7 @@ class _BalanceHero extends StatelessWidget { gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF2B2116), Color(0xFF17171D)], + colors: [AppColors.goldCard, AppColors.card], ), border: Border.all(color: AppColors.gold.withValues(alpha: 0.35)), boxShadow: [ diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart index d5250c5..48b46b5 100644 --- a/lib/pages/watch_records/watch_records_page.dart +++ b/lib/pages/watch_records/watch_records_page.dart @@ -33,7 +33,7 @@ class WatchRecordsPage extends StatelessWidget { final signedIn = context.watch().isSignedIn; final l10n = AppLocalizations.of(context)!; return Theme( - data: AppTheme.dark(), + data: AppTheme.light(), child: Scaffold( key: const Key('watch-records-page'), backgroundColor: AppColors.ink, diff --git a/test/drama_detail/drama_detail_view_test.dart b/test/drama_detail/drama_detail_view_test.dart index a57e9e5..ac4bd22 100644 --- a/test/drama_detail/drama_detail_view_test.dart +++ b/test/drama_detail/drama_detail_view_test.dart @@ -207,7 +207,7 @@ Widget _host(Drama drama) { ChangeNotifierProvider(create: (_) => CatalogController(catalog)..load()), ], child: MaterialApp( - theme: AppTheme.dark(), + theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/test/empty_state/empty_state_test.dart b/test/empty_state/empty_state_test.dart index 8f1588e..cfac0bd 100644 --- a/test/empty_state/empty_state_test.dart +++ b/test/empty_state/empty_state_test.dart @@ -12,7 +12,7 @@ void main() { var taps = 0; await _pump( tester, - theme: AppTheme.dark(), + theme: AppTheme.light(), child: EmptyState( icon: Icons.movie_outlined, title: 'No stories', @@ -33,7 +33,7 @@ void main() { ); expect( tester.widget(find.byKey(const Key('empty-state-icon'))).color, - AppColors.gold, + AppColors.blue, ); expect( tester @@ -103,15 +103,15 @@ void main() { 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( tester, - theme: AppTheme.dark(), + theme: AppTheme.light(), child: const EmptyState(icon: Icons.history_rounded, title: 'No history'), ); expect( tester.widget(find.byKey(const Key('empty-state-icon'))).color, - AppColors.gold, + AppColors.blue, ); await _pump( @@ -156,7 +156,7 @@ Future _pump( }) async { await tester.pumpWidget( MaterialApp( - theme: theme ?? AppTheme.dark(), + theme: theme ?? AppTheme.light(), home: Scaffold(body: child), ), ); diff --git a/test/home/app_bottom_nav_test.dart b/test/home/app_bottom_nav_test.dart index 6e0d181..3b8ae91 100644 --- a/test/home/app_bottom_nav_test.dart +++ b/test/home/app_bottom_nav_test.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; void main() { - testWidgets('should_render_64_high_navigation_and_keep_item_keys', ( - tester, - ) async { + testWidgets('should_render_five_slots_with_center_action', (tester) async { + var centerTaps = 0; await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -23,24 +23,74 @@ void main() { onTap: () {}, ), 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, selectedIcon: Icons.person_rounded, label: 'Me', onTap: () {}, ), ], + centerAction: AppBottomNavCenterAction( + label: 'Create', + onTap: () => centerTaps++, + ), ), ), ), ); 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); + 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( tester.getSize(find.byType(AppBottomNav)).height, AppDimens.navHeight, ); + expect( + tester.getSize(find.byKey(const Key('nav-center-action'))), + const Size.square(AppDimens.navCenter), + ); + final ink = tester.widget( + 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); }); } diff --git a/test/home/home_card_style_test.dart b/test/home/home_card_style_test.dart new file mode 100644 index 0000000..1859799 --- /dev/null +++ b/test/home/home_card_style_test.dart @@ -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( + 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( + 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, +); diff --git a/test/home/home_nav_ui_test.dart b/test/home/home_nav_ui_test.dart index d5f2563..7da4606 100644 --- a/test/home/home_nav_ui_test.dart +++ b/test/home/home_nav_ui_test.dart @@ -20,6 +20,7 @@ const _keys = [ Key('home-tabs-bar'), Key('tab-home'), Key('tab-theater'), + Key('nav-center-action'), Key('tab-create'), Key('tab-mine'), ]; @@ -53,7 +54,7 @@ void main() { final decoration = bar.decoration as BoxDecoration; final border = decoration.border as Border; 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( tester @@ -73,6 +74,17 @@ void main() { ), findsOneWidget, ); + expect( + tester + .widget( + 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.pumpAndSettle(); @@ -87,6 +99,30 @@ void main() { ), findsOneWidget, ); + expect( + tester + .widget( + find.descendant( + of: find.byKey(const Key('tab-theater')), + matching: find.text('Theater'), + ), + ) + .style + ?.color, + AppColors.blue, + ); + expect( + tester + .widget( + find.descendant( + of: find.byKey(const Key('tab-home')), + matching: find.text('Home'), + ), + ) + .style + ?.color, + AppColors.navUnselected, + ); expect( find.descendant( 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-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.byKey(const Key('home-browse-page')), findsOneWidget); @@ -111,6 +148,12 @@ void main() { await tester.pumpAndSettle(); 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.pumpAndSettle(); expect(find.byKey(const Key('create-page')), findsOneWidget); diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index dccae62..279abd3 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -82,7 +82,7 @@ void main() { Future pumpEpisode(Episode episode) async { await tester.pumpWidget( MaterialApp( - theme: AppTheme.dark(), + theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -141,7 +141,7 @@ void main() { ) async { await tester.pumpWidget( MaterialApp( - theme: AppTheme.dark(), + theme: AppTheme.light(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/test/home/k28_a_navigation_test.dart b/test/home/k28_a_navigation_test.dart index fd6b428..41002e4 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -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/playback_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:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; @@ -32,8 +33,9 @@ void main() { for (final key in _tabKeys) { 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(); 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-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.pumpAndSettle(); expect(_chip(tester, 'home-channel-hot').selected, isFalse); 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', ( diff --git a/test/player/player_center_toggle_test.dart b/test/player/player_center_toggle_test.dart index e6912cd..dec5deb 100644 --- a/test/player/player_center_toggle_test.dart +++ b/test/player/player_center_toggle_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.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'; void main() { @@ -36,7 +37,7 @@ void main() { find.descendant(of: find.byKey(key), matching: find.byType(Container)), ); final decoration = container.decoration! as BoxDecoration; - expect(decoration.color, const Color(0x80000000)); + expect(decoration.color, AppColors.blue); expect(decoration.shape, BoxShape.circle); final icon = tester.widget( find.descendant(of: find.byKey(key), matching: find.byType(Icon)), diff --git a/test/theme/app_dimens_test.dart b/test/theme/app_dimens_test.dart index f4402d4..485442d 100644 --- a/test/theme/app_dimens_test.dart +++ b/test/theme/app_dimens_test.dart @@ -6,18 +6,22 @@ import 'package:fruitshorts/core/theme/app_dimens.dart'; void main() { test('should_expose_the_k20_spacing_and_type_scale', () { expect(AppDimens.pageHorizontal, 16); - expect(AppDimens.cardRadius, 12); - expect(AppDimens.innerRadius, 10); + expect(AppDimens.cardRadius, 16); + expect(AppDimens.coverRadius, 16); + expect(AppDimens.authCardRadius, 20); + expect(AppDimens.pillRadius, 999); + expect(AppDimens.buttonRadius, 999); expect(AppDimens.caption, 12); expect(AppDimens.body, 14); expect(AppDimens.listTitle, 15); expect(AppDimens.sectionTitle, 16); expect(AppDimens.pageTitle, 20); - expect(AppDimens.navLabel, 9); + expect(AppDimens.navLabel, 10); expect(AppDimens.buttonHeight, 48); expect(AppDimens.primaryButtonHeight, 52); expect(AppDimens.tapTarget, 44); - expect(AppDimens.navHeight, 54); + expect(AppDimens.navHeight, 64); + expect(AppDimens.navCenter, 56); expect(AppDimens.progressHeight, 3); }); } diff --git a/test/theme/app_theme_test.dart b/test/theme/app_theme_test.dart index 4a7eb85..62ad6ef 100644 --- a/test/theme/app_theme_test.dart +++ b/test/theme/app_theme_test.dart @@ -2,7 +2,7 @@ import 'dart:math'; -import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_theme.dart'; @@ -14,71 +14,46 @@ double _contrast(Color foreground, Color background) { } void main() { - test('should_expose_obsidian_flare_brand_tokens', () { - expect(AppColors.blue, const Color(0xFFFF5B3D)); - expect(AppColors.bluePressed, const Color(0xFFFF4321)); - expect(AppColors.blueSoft, const Color(0xFF9278EA)); - expect(AppColors.secondary, const Color(0xFF9278EA)); - expect(AppColors.gold, const Color(0xFFFBBF24)); - expect(AppColors.ink, const Color(0xFF0B0C10)); - expect(AppColors.card, const Color(0xFF1B1C21)); - expect(AppColors.cardAlt, const Color(0xFF1C1D21)); - expect(AppColors.raised, const Color(0xFF1C1D21)); - expect(AppColors.bar, const Color(0xFF0D0D11)); - expect(AppColors.success, const Color(0xFF34D399)); - expect(AppColors.onPrimary, const Color(0xFFFFFFFF)); + test('should_expose_light_warm_orange_tokens', () { + expect(AppColors.blue, const Color(0xFFFF7A1A)); + expect(AppColors.bluePressed, const Color(0xFFFF6A00)); + expect(AppColors.secondary, const Color(0xFFFF6A8A)); + expect(AppColors.ink, const Color(0xFFFFF8F0)); + expect(AppColors.surfaceTop, const Color(0xFFFFE9D6)); + expect(AppColors.card, const Color(0xFFFFFFFF)); + expect(AppColors.onSurface, const Color(0xFF1F1B16)); + expect(AppColors.muted, const Color(0xFF8C837B)); + expect(AppColors.subtle, const Color(0xFFB3AAA1)); + expect(AppColors.border, const Color(0xFFF0E4D6)); + expect(AppColors.badgePink, const Color(0xFFFFE3EC)); + expect(AppColors.badgePinkText, const Color(0xFFFF6A8A)); + }); + + test('should_keep_primary_text_readable_on_warm_background', () { expect( - _contrast(AppColors.onGold, AppColors.gold), + _contrast(AppColors.onSurface, AppColors.ink), greaterThanOrEqualTo(4.5), ); + expect(_contrast(AppColors.muted, AppColors.card), greaterThanOrEqualTo(3)); }); - test('should_meet_text_and_icon_contrast_when_using_theme_tokens', () { - expect( - _contrast(AppColors.onPrimary, AppColors.blue), - greaterThanOrEqualTo(3), - ); - expect(_contrast(AppColors.blue, Colors.white), greaterThanOrEqualTo(3)); - expect( - _contrast(AppColors.blueSoft, AppColors.ink), - greaterThanOrEqualTo(3), - ); + test('should_use_light_color_scheme_and_dark_system_icons', () { + final theme = AppTheme.light(); + + expect(theme.brightness, Brightness.light); + expect(theme.colorScheme.primary, AppColors.blue); + expect(theme.colorScheme.onPrimary, AppColors.onPrimary); + expect(theme.colorScheme.surface, AppColors.ink); + expect(theme.colorScheme.onSurface, AppColors.onSurface); + 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', () { - for (final theme in [AppTheme.light(), AppTheme.dark()]) { - expect(theme.colorScheme.primary, AppColors.blue); - expect(theme.colorScheme.onPrimary, AppColors.onPrimary); - } - }); - - 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); + test('should_expose_page_gradient_and_soft_card_shadow', () { + expect(AppTheme.pageGradient.colors, [AppColors.surfaceTop, AppColors.ink]); + expect(AppTheme.cardShadow.color, AppColors.cardShadow); + expect(AppTheme.cardShadow.blurRadius, 18); + expect(AppTheme.cardShadow.offset, const Offset(0, 6)); }); }