Initial project import
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum JzThemePreference { system, light, dark }
|
||||
|
||||
class ThemeStore extends ChangeNotifier {
|
||||
ThemeStore._();
|
||||
|
||||
static final instance = ThemeStore._();
|
||||
static const _key = 'theme_preference';
|
||||
static const _channel = MethodChannel('com.miaoji/screenshot');
|
||||
|
||||
JzThemePreference _preference = JzThemePreference.system;
|
||||
|
||||
JzThemePreference get preference => _preference;
|
||||
|
||||
ThemeMode get themeMode => switch (_preference) {
|
||||
JzThemePreference.light => ThemeMode.light,
|
||||
JzThemePreference.dark => ThemeMode.dark,
|
||||
JzThemePreference.system => ThemeMode.system,
|
||||
};
|
||||
|
||||
Future<void> initialize() async {
|
||||
final value = (await SharedPreferences.getInstance()).getString(_key);
|
||||
_preference = JzThemePreference.values.firstWhere(
|
||||
(item) => item.name == value,
|
||||
orElse: () => JzThemePreference.system,
|
||||
);
|
||||
await _syncNativeTheme();
|
||||
}
|
||||
|
||||
Future<void> setPreference(JzThemePreference value) async {
|
||||
if (_preference == value) return;
|
||||
_preference = value;
|
||||
notifyListeners();
|
||||
await (await SharedPreferences.getInstance()).setString(_key, value.name);
|
||||
await _syncNativeTheme();
|
||||
}
|
||||
|
||||
Future<void> _syncNativeTheme() async {
|
||||
try {
|
||||
await _channel.invokeMethod<bool>('setThemeMode', {
|
||||
'mode': _preference.name,
|
||||
});
|
||||
} on MissingPluginException {
|
||||
// Native startup themes are Android-only.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user