// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'app_colors.dart'; import 'app_dimens.dart'; /// 深色视觉主题(默认且唯一)。 abstract final class AppTheme { /// 页面背景:顶部暖黑、底部纯黑,避免旧深蓝渐变。 static const LinearGradient pageGradient = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.mineGradientTop, AppColors.mineGradientMiddle, AppColors.mineGradientBottom, ], stops: [0, 0.4, 0.75], ); /// 卡片阴影关闭,改用底色与分割线。 static const BoxShadow cardShadow = BoxShadow( color: AppColors.cardShadow, blurRadius: AppDimens.cardShadowBlur, offset: Offset(0, AppDimens.cardShadowY), ); static final BorderRadius _buttonRadius = BorderRadius.circular( AppDimens.buttonRadius, ); static final BorderRadius _cardRadius = BorderRadius.circular( AppDimens.cardRadius, ); static final BorderRadius _inputRadius = BorderRadius.circular( AppDimens.inputRadius, ); /// 全局深色主题。 static ThemeData dark() { const scheme = ColorScheme.dark( primary: AppColors.brandRed, onPrimary: AppColors.onPrimary, secondary: AppColors.secondary, onSecondary: AppColors.ink, primaryContainer: AppColors.raised, onPrimaryContainer: AppColors.onSurface, secondaryContainer: AppColors.badgePink, onSecondaryContainer: AppColors.badgePinkText, surface: AppColors.ink, onSurface: AppColors.onSurface, surfaceContainerHighest: AppColors.raised, outline: AppColors.border, outlineVariant: AppColors.border, surfaceTint: AppColors.transparent, inversePrimary: AppColors.brandRedPressed, error: AppColors.error, onError: AppColors.onPrimary, ); final base = ThemeData( useMaterial3: true, brightness: Brightness.dark, colorScheme: scheme, scaffoldBackgroundColor: AppColors.ink, canvasColor: AppColors.ink, fontFamily: 'PlusJakartaSans', progressIndicatorTheme: const ProgressIndicatorThemeData( color: AppColors.brandRed, ), sliderTheme: const SliderThemeData( activeTrackColor: AppColors.brandRed, inactiveTrackColor: AppColors.border, thumbColor: AppColors.onPrimary, ), tabBarTheme: const TabBarThemeData( labelColor: AppColors.brandRed, indicatorColor: AppColors.brandRed, unselectedLabelColor: AppColors.muted, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: AppColors.primary, shape: RoundedRectangleBorder(borderRadius: _buttonRadius), ), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith(_buttonColor), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), elevation: const WidgetStatePropertyAll(0), minimumSize: const WidgetStatePropertyAll( Size(0, AppDimens.buttonHeight), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: _buttonRadius), ), ), ), filledButtonTheme: FilledButtonThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith(_buttonColor), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), minimumSize: const WidgetStatePropertyAll( Size(0, AppDimens.buttonHeight), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: _buttonRadius), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( foregroundColor: const WidgetStatePropertyAll(AppColors.textPrimary), minimumSize: const WidgetStatePropertyAll( Size(0, AppDimens.buttonHeight), ), side: const WidgetStatePropertyAll( BorderSide(color: AppColors.mineOutline), ), shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: _buttonRadius), ), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: AppColors.input, hintStyle: const TextStyle(color: AppColors.subtle), labelStyle: const TextStyle(color: AppColors.muted), border: OutlineInputBorder( borderRadius: _inputRadius, borderSide: const BorderSide(color: AppColors.border), ), enabledBorder: OutlineInputBorder( borderRadius: _inputRadius, borderSide: const BorderSide(color: AppColors.border), ), focusedBorder: OutlineInputBorder( borderRadius: _inputRadius, borderSide: const BorderSide(color: AppColors.brandRed), ), ), chipTheme: ChipThemeData( backgroundColor: AppColors.raised, selectedColor: AppColors.brandRed.withValues(alpha: 0.22), labelStyle: const TextStyle(color: AppColors.onSurface), secondaryLabelStyle: const TextStyle(color: AppColors.muted), side: const BorderSide(color: AppColors.border), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimens.chipRadius), ), ), appBarTheme: const AppBarTheme( backgroundColor: AppColors.transparent, foregroundColor: AppColors.textPrimary, elevation: 0, scrolledUnderElevation: 0, surfaceTintColor: AppColors.transparent, systemOverlayStyle: SystemUiOverlayStyle.light, titleTextStyle: TextStyle( color: AppColors.textPrimary, fontFamily: 'Outfit', fontSize: AppDimens.pageTitle, fontWeight: FontWeight.w800, ), ), iconTheme: const IconThemeData(color: AppColors.textPrimary), dividerTheme: const DividerThemeData( color: AppColors.border, thickness: 1, space: 1, ), cardTheme: CardThemeData( color: AppColors.card, elevation: 0, shadowColor: AppColors.transparent, surfaceTintColor: AppColors.transparent, shape: RoundedRectangleBorder( borderRadius: _cardRadius, side: const BorderSide(color: AppColors.border), ), ), dialogTheme: DialogThemeData( backgroundColor: AppColors.raised, surfaceTintColor: AppColors.transparent, shape: RoundedRectangleBorder(borderRadius: _cardRadius), titleTextStyle: const TextStyle( color: AppColors.onSurface, fontFamily: 'Outfit', fontSize: AppDimens.sectionTitle, fontWeight: FontWeight.w700, ), contentTextStyle: const TextStyle( color: AppColors.muted, fontSize: AppDimens.body, ), ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( backgroundColor: AppColors.navBackground, selectedItemColor: AppColors.navSelected, unselectedItemColor: AppColors.navUnselected, elevation: 0, type: BottomNavigationBarType.fixed, ), bottomSheetTheme: const BottomSheetThemeData( backgroundColor: AppColors.transparent, modalBackgroundColor: AppColors.transparent, surfaceTintColor: AppColors.transparent, ), switchTheme: SwitchThemeData( thumbColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) ? AppColors.onPrimary : null, ), trackColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) ? AppColors.brandRed : null, ), ), snackBarTheme: const SnackBarThemeData( backgroundColor: AppColors.raised, contentTextStyle: TextStyle(color: AppColors.onSurface), actionTextColor: AppColors.secondary, ), listTileTheme: const ListTileThemeData( iconColor: AppColors.onSurface, textColor: AppColors.onSurface, ), ); final textTheme = base.textTheme .apply( fontFamily: 'PlusJakartaSans', bodyColor: AppColors.cream, displayColor: AppColors.onSurface, ) .copyWith( 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), bodyLarge: base.textTheme.bodyLarge?.copyWith( color: AppColors.onSurface, ), bodyMedium: base.textTheme.bodyMedium?.copyWith( color: AppColors.onSurface, ), bodySmall: base.textTheme.bodySmall?.copyWith(color: AppColors.muted), labelLarge: base.textTheme.labelLarge?.copyWith( color: AppColors.onSurface, ), labelMedium: base.textTheme.labelMedium?.copyWith( color: AppColors.muted, ), labelSmall: base.textTheme.labelSmall?.copyWith( color: AppColors.subtle, ), ); return base.copyWith(textTheme: textTheme); } /// 兼容旧调用点,统一落到深色主题。 static ThemeData light() => dark(); static TextStyle? _display(TextStyle? style) => style?.copyWith(fontFamily: 'Outfit', color: AppColors.onSurface); static Color _buttonColor(Set states) { if (states.contains(WidgetState.pressed) || states.contains(WidgetState.hovered) || states.contains(WidgetState.focused)) { return AppColors.brandRedPressed; } return AppColors.brandRed; } }