Files
2026-07-24 23:11:20 +08:00

360 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@immutable
class JzPalette extends ThemeExtension<JzPalette> {
final Color text;
final Color text2;
final Color text3;
final Color background;
final Color card;
final Color line;
final Color primaryBackground;
final Color aiBackground;
final Color expenseBackground;
final Color warningBackground;
const JzPalette({
required this.text,
required this.text2,
required this.text3,
required this.background,
required this.card,
required this.line,
required this.primaryBackground,
required this.aiBackground,
required this.expenseBackground,
required this.warningBackground,
});
@override
JzPalette copyWith({
Color? text,
Color? text2,
Color? text3,
Color? background,
Color? card,
Color? line,
Color? primaryBackground,
Color? aiBackground,
Color? expenseBackground,
Color? warningBackground,
}) => JzPalette(
text: text ?? this.text,
text2: text2 ?? this.text2,
text3: text3 ?? this.text3,
background: background ?? this.background,
card: card ?? this.card,
line: line ?? this.line,
primaryBackground: primaryBackground ?? this.primaryBackground,
aiBackground: aiBackground ?? this.aiBackground,
expenseBackground: expenseBackground ?? this.expenseBackground,
warningBackground: warningBackground ?? this.warningBackground,
);
@override
JzPalette lerp(covariant JzPalette? other, double t) {
if (other == null) return this;
return JzPalette(
text: Color.lerp(text, other.text, t)!,
text2: Color.lerp(text2, other.text2, t)!,
text3: Color.lerp(text3, other.text3, t)!,
background: Color.lerp(background, other.background, t)!,
card: Color.lerp(card, other.card, t)!,
line: Color.lerp(line, other.line, t)!,
primaryBackground: Color.lerp(
primaryBackground,
other.primaryBackground,
t,
)!,
aiBackground: Color.lerp(aiBackground, other.aiBackground, t)!,
expenseBackground: Color.lerp(
expenseBackground,
other.expenseBackground,
t,
)!,
warningBackground: Color.lerp(
warningBackground,
other.warningBackground,
t,
)!,
);
}
}
extension JzThemeContext on BuildContext {
JzPalette get jz => Theme.of(this).extension<JzPalette>()!;
}
class AppTheme {
AppTheme._();
static const Color primary = Color(0xFF00B386);
static const Color primaryDeep = Color(0xFF009973);
static const Color primaryBg = Color(0xFFE6F7F1);
static const Color primaryMid = Color(0xFF4DA6FF);
static const Color primaryLight = Color(0xFF80DCC2);
static const Color ai = Color(0xFF5B6BF5);
static const Color aiBg = Color(0xFFEEF0FE);
static const Color red = Color(0xFFF0642D);
static const Color redBg = Color(0xFFFFF3EC);
static const Color orange = Color(0xFFF5A623);
static const Color orangeBg = Color(0xFFFFF7E8);
static const Color text = Color(0xFF191F26);
static const Color text2 = Color(0xFF5E6772);
static const Color text3 = Color(0xFF9AA3AD);
static const Color bg = Color(0xFFF6F7F9);
static const Color card = Colors.white;
static const Color line = Color(0xFFEEF0F3);
static const _lightPalette = JzPalette(
text: text,
text2: text2,
text3: text3,
background: bg,
card: card,
line: line,
primaryBackground: primaryBg,
aiBackground: aiBg,
expenseBackground: redBg,
warningBackground: orangeBg,
);
static const _darkPalette = JzPalette(
text: Color(0xFFE8EFED),
text2: Color(0xFFAAB6B2),
text3: Color(0xFF74817D),
background: Color(0xFF111615),
card: Color(0xFF1B2220),
line: Color(0xFF2B3532),
primaryBackground: Color(0xFF143A31),
aiBackground: Color(0xFF272B4A),
expenseBackground: Color(0xFF40251F),
warningBackground: Color(0xFF3B2E1B),
);
static ThemeData get light => _build(Brightness.light, _lightPalette);
static ThemeData get dark => _build(Brightness.dark, _darkPalette);
static ThemeData _build(Brightness brightness, JzPalette p) {
final dark = brightness == Brightness.dark;
final scheme = dark
? ColorScheme.dark(
primary: primary,
secondary: ai,
surface: p.card,
error: red,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: p.text,
primaryContainer: p.primaryBackground,
onPrimaryContainer: const Color(0xFF9BE7D1),
secondaryContainer: p.aiBackground,
onSecondaryContainer: const Color(0xFFC5CAFF),
outline: p.line,
outlineVariant: p.line,
)
: ColorScheme.light(
primary: primary,
secondary: ai,
surface: p.card,
error: red,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: p.text,
primaryContainer: p.primaryBackground,
onPrimaryContainer: primaryDeep,
secondaryContainer: p.aiBackground,
onSecondaryContainer: ai,
outline: p.line,
outlineVariant: p.line,
);
final baseText = ThemeData(
brightness: brightness,
).textTheme.apply(bodyColor: p.text, displayColor: p.text);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: p.background,
fontFamily: 'PingFang SC',
textTheme: baseText,
extensions: [p],
dividerColor: p.line,
disabledColor: p.text3,
appBarTheme: AppBarTheme(
backgroundColor: Colors.transparent,
foregroundColor: p.text,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: p.text,
fontSize: 16,
fontWeight: FontWeight.w600,
),
systemOverlayStyle: dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: p.card,
selectedItemColor: primary,
unselectedItemColor: p.text3,
type: BottomNavigationBarType.fixed,
elevation: 0,
),
cardTheme: CardThemeData(
color: p.card,
surfaceTintColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: p.line, width: 0.5),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: p.card,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: p.line),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: p.line),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: primary, width: 1.4),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: red),
),
hintStyle: TextStyle(color: p.text3, fontSize: 13.5),
labelStyle: TextStyle(color: p.text2, fontSize: 13),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 13,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
disabledBackgroundColor: p.line,
disabledForegroundColor: p.text3,
elevation: 0,
minimumSize: const Size(0, 46),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
padding: const EdgeInsets.symmetric(vertical: 14),
textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
disabledBackgroundColor: p.line,
disabledForegroundColor: p.text3,
elevation: 0,
minimumSize: const Size(0, 46),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: p.text,
side: BorderSide(color: p.line),
minimumSize: const Size(0, 46),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: dark ? const Color(0xFF77D7BD) : primaryDeep,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w600,
),
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: p.text2,
highlightColor: p.primaryBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected) ? primary : p.card,
),
side: BorderSide(color: p.text3),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
switchTheme: SwitchThemeData(
trackColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected)
? p.primaryBackground
: p.line,
),
thumbColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.selected) ? primary : p.text3,
),
trackOutlineColor: const WidgetStatePropertyAll(Colors.transparent),
),
dialogTheme: DialogThemeData(
backgroundColor: p.card,
surfaceTintColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
titleTextStyle: TextStyle(
color: p.text,
fontSize: 18,
fontWeight: FontWeight.w800,
),
contentTextStyle: TextStyle(
color: p.text2,
fontSize: 13.5,
height: 1.6,
),
),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: p.card,
modalBackgroundColor: p.card,
surfaceTintColor: Colors.transparent,
showDragHandle: false,
),
snackBarTheme: SnackBarThemeData(
backgroundColor: dark ? const Color(0xFFE8EFED) : p.text,
contentTextStyle: TextStyle(
color: dark ? const Color(0xFF111615) : Colors.white,
fontSize: 13,
),
behavior: SnackBarBehavior.floating,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: p.card,
indicatorColor: p.primaryBackground,
),
);
}
}