2026-06-02 23:28:39 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2026-06-06 20:41:24 +02:00
|
|
|
import '../models/settings.dart';
|
|
|
|
|
|
|
|
|
|
@immutable
|
|
|
|
|
class AppPalette extends ThemeExtension<AppPalette> {
|
|
|
|
|
final Color panel;
|
|
|
|
|
final Color panelText;
|
|
|
|
|
final Color mutedText;
|
|
|
|
|
|
|
|
|
|
const AppPalette({
|
|
|
|
|
required this.panel,
|
|
|
|
|
required this.panelText,
|
|
|
|
|
required this.mutedText,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
AppPalette copyWith({Color? panel, Color? panelText, Color? mutedText}) {
|
|
|
|
|
return AppPalette(
|
|
|
|
|
panel: panel ?? this.panel,
|
|
|
|
|
panelText: panelText ?? this.panelText,
|
|
|
|
|
mutedText: mutedText ?? this.mutedText,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
AppPalette lerp(covariant AppPalette? other, double t) {
|
|
|
|
|
if (other == null) return this;
|
|
|
|
|
return AppPalette(
|
|
|
|
|
panel: Color.lerp(panel, other.panel, t)!,
|
|
|
|
|
panelText: Color.lerp(panelText, other.panelText, t)!,
|
|
|
|
|
mutedText: Color.lerp(mutedText, other.mutedText, t)!,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-02 23:28:39 +02:00
|
|
|
|
|
|
|
|
class AppTheme {
|
|
|
|
|
// Brand colours
|
|
|
|
|
static const navy = Color(0xFF1C2B47);
|
|
|
|
|
static const teal = Color(0xFF2E7D64);
|
|
|
|
|
static const accent = Color(0xFF2563EB);
|
|
|
|
|
static const surface = Color(0xFFF8F9FA);
|
|
|
|
|
static const panelBg = Color(0xFF1E2028);
|
|
|
|
|
static const panelFg = Color(0xFFE2E8F0);
|
|
|
|
|
|
2026-06-06 20:41:24 +02:00
|
|
|
static Color parseHex(String hex, {Color fallback = Colors.white}) {
|
|
|
|
|
final cleaned = hex.replaceFirst('#', '');
|
|
|
|
|
final value = int.tryParse(
|
|
|
|
|
cleaned.length == 6 ? 'FF$cleaned' : cleaned,
|
|
|
|
|
radix: 16,
|
|
|
|
|
);
|
|
|
|
|
return value == null ? fallback : Color(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static ThemeData fromProfile(AppAppearanceProfile profile) {
|
|
|
|
|
final primary = parseHex(profile.primaryColor, fallback: navy);
|
|
|
|
|
final accentColor = parseHex(profile.accentColor, fallback: accent);
|
|
|
|
|
final background = parseHex(profile.backgroundColor, fallback: surface);
|
|
|
|
|
final surfaceColor = parseHex(profile.surfaceColor);
|
|
|
|
|
final text = parseHex(profile.textColor, fallback: const Color(0xFF1E293B));
|
|
|
|
|
final muted = parseHex(
|
|
|
|
|
profile.mutedTextColor,
|
|
|
|
|
fallback: const Color(0xFF64748B),
|
|
|
|
|
);
|
|
|
|
|
final panel = parseHex(profile.panelColor, fallback: panelBg);
|
|
|
|
|
final panelText = parseHex(profile.panelTextColor, fallback: panelFg);
|
|
|
|
|
final brightness = profile.isDark ? Brightness.dark : Brightness.light;
|
|
|
|
|
final scheme = ColorScheme.fromSeed(
|
|
|
|
|
seedColor: primary,
|
|
|
|
|
brightness: brightness,
|
|
|
|
|
primary: primary,
|
|
|
|
|
secondary: accentColor,
|
|
|
|
|
surface: surfaceColor,
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
return ThemeData(
|
|
|
|
|
useMaterial3: true,
|
2026-06-06 20:41:24 +02:00
|
|
|
brightness: brightness,
|
|
|
|
|
colorScheme: scheme,
|
|
|
|
|
scaffoldBackgroundColor: background,
|
|
|
|
|
canvasColor: surfaceColor,
|
|
|
|
|
cardColor: surfaceColor,
|
|
|
|
|
dialogTheme: DialogThemeData(backgroundColor: surfaceColor),
|
|
|
|
|
textTheme: ThemeData(
|
|
|
|
|
brightness: brightness,
|
|
|
|
|
).textTheme.apply(bodyColor: text, displayColor: text),
|
|
|
|
|
appBarTheme: AppBarTheme(
|
|
|
|
|
backgroundColor: primary,
|
|
|
|
|
foregroundColor: scheme.onPrimary,
|
2026-06-02 23:28:39 +02:00
|
|
|
elevation: 0,
|
|
|
|
|
centerTitle: false,
|
|
|
|
|
titleTextStyle: TextStyle(
|
2026-06-06 20:41:24 +02:00
|
|
|
color: scheme.onPrimary,
|
2026-06-02 23:28:39 +02:00
|
|
|
fontSize: 16,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
letterSpacing: 0.5,
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-06 20:41:24 +02:00
|
|
|
dividerTheme: DividerThemeData(
|
|
|
|
|
color: scheme.outlineVariant,
|
2026-06-02 23:28:39 +02:00
|
|
|
thickness: 1,
|
|
|
|
|
space: 1,
|
|
|
|
|
),
|
|
|
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
|
|
|
filled: true,
|
2026-06-06 20:41:24 +02:00
|
|
|
fillColor: surfaceColor,
|
2026-06-02 23:28:39 +02:00
|
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
|
|
|
horizontal: 12,
|
|
|
|
|
vertical: 10,
|
|
|
|
|
),
|
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
2026-06-06 20:41:24 +02:00
|
|
|
borderSide: BorderSide(color: scheme.outlineVariant),
|
2026-06-02 23:28:39 +02:00
|
|
|
),
|
|
|
|
|
enabledBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
2026-06-06 20:41:24 +02:00
|
|
|
borderSide: BorderSide(color: scheme.outlineVariant),
|
2026-06-02 23:28:39 +02:00
|
|
|
),
|
|
|
|
|
focusedBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
2026-06-06 20:41:24 +02:00
|
|
|
borderSide: BorderSide(color: accentColor, width: 1.5),
|
2026-06-02 23:28:39 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
2026-06-06 20:41:24 +02:00
|
|
|
backgroundColor: accentColor,
|
|
|
|
|
foregroundColor:
|
|
|
|
|
scheme.brightness == Brightness.light &&
|
|
|
|
|
accentColor.computeLuminance() > 0.6
|
|
|
|
|
? Colors.black
|
|
|
|
|
: Colors.white,
|
2026-06-02 23:28:39 +02:00
|
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
|
|
|
textStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-06-06 20:41:24 +02:00
|
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
|
|
|
style: OutlinedButton.styleFrom(foregroundColor: primary),
|
|
|
|
|
),
|
|
|
|
|
iconButtonTheme: IconButtonThemeData(
|
|
|
|
|
style: IconButton.styleFrom(foregroundColor: text),
|
|
|
|
|
),
|
|
|
|
|
extensions: [
|
|
|
|
|
AppPalette(panel: panel, panelText: panelText, mutedText: muted),
|
|
|
|
|
],
|
2026-06-02 23:28:39 +02:00
|
|
|
);
|
|
|
|
|
}
|
2026-06-06 20:41:24 +02:00
|
|
|
|
|
|
|
|
static ThemeData get light => fromProfile(AppAppearanceProfile.basic);
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|