- Design-System: MeloDesignTokens ThemeExtension, Outfit-Font, Farben #0B0B10/#14141C/#1C1C26 - Akzentfarbe-User-Einstellung behalten (MeloTheme.akzentNotifier), Gradient aus Akzent - Startseite: Hero-Weiterhören-Karte mit Cover, Titel, Glow-Play-Button - 2-stufige Einstellungen: Standard + Expertenmodus (SharedPreferences Key 'experten_modus') - Komponenten: Pill-Buttons (radius 999), Card-Radius 20, Glow-Shadow, PressScale-Widget - Login: Gradient-Button mit Akzent-Glow - 199 Tests grün (194 bestehend + 5 neu): ThemeExtension, Expertenmodus, Akzent, PressScale, Hero - flutter analyze lib/ + test/ = 0 Issues - KEIN APK
351 lines
12 KiB
Dart
351 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
|
||
/// Melo-Design-Tokens: Zugriff per `Theme.of(context).extension<MeloDesignTokens>()!`
|
||
class MeloDesignTokens extends ThemeExtension<MeloDesignTokens> {
|
||
// ─── Farben ───
|
||
final Color hintergrund; // #0B0B10
|
||
final Color oberflaeche; // #14141C
|
||
final Color karte; // #1C1C26
|
||
final Color textPrimaer; // #F5F5F7
|
||
final Color textSekundaer; // #9E9EAB
|
||
final Color erfolg; // #4CAF50
|
||
final Color warnung; // #FF9800
|
||
final Color fehler; // #F44336
|
||
final Color akzent; // Live per ValueNotifier
|
||
|
||
// ─── Radien ───
|
||
final double cardRadius; // 20
|
||
final double buttonRadius; // 999 (Pill)
|
||
final double sheetRadius; // 28
|
||
final double dialogRadius; // 24
|
||
final double inputRadius; // 14
|
||
|
||
// ─── Abstände / Typo ───
|
||
final double abstandXs;
|
||
final double abstandSm;
|
||
final double abstandMd;
|
||
final double abstandLg;
|
||
|
||
const MeloDesignTokens({
|
||
required this.hintergrund,
|
||
required this.oberflaeche,
|
||
required this.karte,
|
||
required this.textPrimaer,
|
||
required this.textSekundaer,
|
||
required this.erfolg,
|
||
required this.warnung,
|
||
required this.fehler,
|
||
required this.akzent,
|
||
this.cardRadius = 20,
|
||
this.buttonRadius = 999,
|
||
this.sheetRadius = 28,
|
||
this.dialogRadius = 24,
|
||
this.inputRadius = 14,
|
||
this.abstandXs = 4,
|
||
this.abstandSm = 8,
|
||
this.abstandMd = 16,
|
||
this.abstandLg = 24,
|
||
});
|
||
|
||
/// Akzent-Gradient (Akzent → dunklere Variante)
|
||
Gradient get akzentGradient => LinearGradient(
|
||
colors: [akzent, Color.lerp(akzent, Colors.black, 0.5)!],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
);
|
||
|
||
/// Rot-Gradient (hartes Rot, unabhängig vom User-Akzent)
|
||
static const Gradient rotGradient = LinearGradient(
|
||
colors: [Color(0xFFFF3B30), Color(0xFFC62828)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
);
|
||
|
||
/// Glow-Schatten für Akzent-Elemente
|
||
List<BoxShadow> get akzentGlow => [
|
||
BoxShadow(
|
||
color: akzent.withValues(alpha: 0.30),
|
||
blurRadius: 24,
|
||
offset: const Offset(0, 8),
|
||
),
|
||
];
|
||
|
||
@override
|
||
MeloDesignTokens copyWith({
|
||
Color? hintergrund,
|
||
Color? oberflaeche,
|
||
Color? karte,
|
||
Color? textPrimaer,
|
||
Color? textSekundaer,
|
||
Color? erfolg,
|
||
Color? warnung,
|
||
Color? fehler,
|
||
Color? akzent,
|
||
double? cardRadius,
|
||
double? buttonRadius,
|
||
double? sheetRadius,
|
||
double? dialogRadius,
|
||
double? inputRadius,
|
||
}) {
|
||
return MeloDesignTokens(
|
||
hintergrund: hintergrund ?? this.hintergrund,
|
||
oberflaeche: oberflaeche ?? this.oberflaeche,
|
||
karte: karte ?? this.karte,
|
||
textPrimaer: textPrimaer ?? this.textPrimaer,
|
||
textSekundaer: textSekundaer ?? this.textSekundaer,
|
||
erfolg: erfolg ?? this.erfolg,
|
||
warnung: warnung ?? this.warnung,
|
||
fehler: fehler ?? this.fehler,
|
||
akzent: akzent ?? this.akzent,
|
||
cardRadius: cardRadius ?? this.cardRadius,
|
||
buttonRadius: buttonRadius ?? this.buttonRadius,
|
||
sheetRadius: sheetRadius ?? this.sheetRadius,
|
||
dialogRadius: dialogRadius ?? this.dialogRadius,
|
||
inputRadius: inputRadius ?? this.inputRadius,
|
||
);
|
||
}
|
||
|
||
@override
|
||
MeloDesignTokens lerp(ThemeExtension<MeloDesignTokens>? other, double t) {
|
||
if (other is! MeloDesignTokens) return this;
|
||
return MeloDesignTokens(
|
||
hintergrund: Color.lerp(hintergrund, other.hintergrund, t)!,
|
||
oberflaeche: Color.lerp(oberflaeche, other.oberflaeche, t)!,
|
||
karte: Color.lerp(karte, other.karte, t)!,
|
||
textPrimaer: Color.lerp(textPrimaer, other.textPrimaer, t)!,
|
||
textSekundaer: Color.lerp(textSekundaer, other.textSekundaer, t)!,
|
||
erfolg: Color.lerp(erfolg, other.erfolg, t)!,
|
||
warnung: Color.lerp(warnung, other.warnung, t)!,
|
||
fehler: Color.lerp(fehler, other.fehler, t)!,
|
||
akzent: Color.lerp(akzent, other.akzent, t)!,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// MeloTheme – zentrales Design-System für Melo Dark Fusion.
|
||
/// Behälter für Akzentfarbe (per User) und das komplette ThemeData.
|
||
class MeloTheme {
|
||
/// Legacy-Farb-Konstanten (für Rückwärtskompatibilität mit bestehendem Code)
|
||
static const Color rot = Color(0xFFCC0000);
|
||
static const Color rotHell = Color(0x33CC0000);
|
||
static const Color schwarz = Color(0xFF0B0B10);
|
||
static const Color oberflaeche = Color(0xFF14141C);
|
||
static const Color dunkel1 = Color(0xFF14141C);
|
||
static const Color dunkel2 = Color(0xFF1C1C26);
|
||
static const Color textPrimaer = Color(0xFFF5F5F7);
|
||
static const Color textSekundaer = Color(0xFF9E9EAB);
|
||
|
||
/// Akzentfarbe – wechselt pro angemeldeter Person (Login-Effekt).
|
||
/// UI hängt per ValueListenableBuilder an [akzentNotifier].
|
||
static final ValueNotifier<Color> akzentNotifier = ValueNotifier<Color>(rot);
|
||
static Color get akzent => akzentNotifier.value;
|
||
static set akzent(Color farbe) => akzentNotifier.value = farbe;
|
||
|
||
/// Baut das vollständige ThemeData mit allen Komponenten-Themen.
|
||
static ThemeData themeMitAkzent(Color akzent) {
|
||
final tokens = MeloDesignTokens(
|
||
hintergrund: const Color(0xFF0B0B10),
|
||
oberflaeche: const Color(0xFF14141C),
|
||
karte: const Color(0xFF1C1C26),
|
||
textPrimaer: const Color(0xFFF5F5F7),
|
||
textSekundaer: const Color(0xFF9E9EAB),
|
||
erfolg: const Color(0xFF4CAF50),
|
||
warnung: const Color(0xFFFF9800),
|
||
fehler: const Color(0xFFF44336),
|
||
akzent: akzent,
|
||
);
|
||
|
||
// Outfit-TextTheme
|
||
final textTheme = GoogleFonts.outfitTextTheme().apply(
|
||
displayColor: tokens.textPrimaer,
|
||
bodyColor: tokens.textPrimaer,
|
||
);
|
||
|
||
return ThemeData(
|
||
brightness: Brightness.dark,
|
||
scaffoldBackgroundColor: tokens.hintergrund,
|
||
fontFamily: GoogleFonts.outfit().fontFamily,
|
||
colorScheme: ColorScheme.dark(
|
||
primary: akzent,
|
||
secondary: akzent,
|
||
surface: tokens.oberflaeche,
|
||
onPrimary: Colors.white,
|
||
onSecondary: Colors.white,
|
||
onSurface: tokens.textPrimaer,
|
||
error: tokens.fehler,
|
||
),
|
||
textTheme: textTheme,
|
||
|
||
// ─── AppBar ───
|
||
appBarTheme: AppBarTheme(
|
||
backgroundColor: tokens.oberflaeche,
|
||
foregroundColor: tokens.textPrimaer,
|
||
elevation: 0,
|
||
titleTextStyle: TextStyle(
|
||
fontSize: 22,
|
||
fontWeight: FontWeight.w600,
|
||
color: tokens.textPrimaer,
|
||
letterSpacing: -0.5,
|
||
),
|
||
),
|
||
|
||
// ─── BottomNavigationBar ───
|
||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||
backgroundColor: tokens.oberflaeche,
|
||
selectedItemColor: akzent,
|
||
unselectedItemColor: tokens.textSekundaer,
|
||
type: BottomNavigationBarType.fixed,
|
||
elevation: 0,
|
||
selectedLabelStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
|
||
unselectedLabelStyle: const TextStyle(fontSize: 11),
|
||
),
|
||
|
||
// ─── Card ───
|
||
cardTheme: CardThemeData(
|
||
color: tokens.karte,
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(tokens.cardRadius),
|
||
),
|
||
margin: EdgeInsets.zero,
|
||
),
|
||
|
||
// ─── Chip ───
|
||
chipTheme: ChipThemeData(
|
||
backgroundColor: tokens.karte,
|
||
selectedColor: akzent,
|
||
labelStyle: TextStyle(color: tokens.textPrimaer, fontSize: 13),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(tokens.buttonRadius),
|
||
),
|
||
side: BorderSide.none,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
),
|
||
|
||
// ─── FilledButton (Pill, Gradient) ───
|
||
filledButtonTheme: FilledButtonThemeData(
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: akzent,
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(tokens.buttonRadius),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||
textStyle: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
|
||
// ─── ElevatedButton ───
|
||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: akzent,
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(tokens.buttonRadius),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||
textStyle: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
elevation: 0,
|
||
),
|
||
),
|
||
|
||
// ─── OutlinedButton ───
|
||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: akzent,
|
||
side: BorderSide(color: akzent),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(tokens.buttonRadius),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||
),
|
||
),
|
||
|
||
// ─── InputDecoration ───
|
||
inputDecorationTheme: InputDecorationTheme(
|
||
filled: true,
|
||
fillColor: tokens.karte,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(tokens.inputRadius),
|
||
borderSide: BorderSide.none,
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(tokens.inputRadius),
|
||
borderSide: BorderSide(color: akzent, width: 1.5),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(tokens.inputRadius),
|
||
borderSide: BorderSide(color: tokens.karte),
|
||
),
|
||
labelStyle: TextStyle(color: tokens.textSekundaer, fontSize: 13),
|
||
hintStyle: TextStyle(color: tokens.textSekundaer.withValues(alpha: 0.6), fontSize: 13),
|
||
),
|
||
|
||
// ─── Switch ───
|
||
switchTheme: SwitchThemeData(
|
||
thumbColor: WidgetStateProperty.resolveWith((states) {
|
||
if (states.contains(WidgetState.selected)) return akzent;
|
||
return tokens.textSekundaer;
|
||
}),
|
||
trackColor: WidgetStateProperty.resolveWith((states) {
|
||
if (states.contains(WidgetState.selected)) {
|
||
return akzent.withValues(alpha: 0.4);
|
||
}
|
||
return tokens.karte;
|
||
}),
|
||
),
|
||
|
||
// ─── Slider ───
|
||
sliderTheme: SliderThemeData(
|
||
activeTrackColor: akzent,
|
||
inactiveTrackColor: tokens.karte,
|
||
thumbColor: akzent,
|
||
overlayColor: akzent.withValues(alpha: 0.2),
|
||
trackHeight: 4,
|
||
),
|
||
|
||
// ─── Dialog ───
|
||
dialogTheme: DialogThemeData(
|
||
backgroundColor: tokens.oberflaeche,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(tokens.dialogRadius),
|
||
),
|
||
titleTextStyle: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
color: tokens.textPrimaer,
|
||
),
|
||
),
|
||
|
||
// ─── BottomSheet ───
|
||
bottomSheetTheme: BottomSheetThemeData(
|
||
backgroundColor: tokens.oberflaeche,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(tokens.sheetRadius)),
|
||
),
|
||
),
|
||
|
||
// ─── NavigationBar ───
|
||
navigationBarTheme: NavigationBarThemeData(
|
||
backgroundColor: tokens.oberflaeche,
|
||
indicatorColor: akzent.withValues(alpha: 0.25),
|
||
surfaceTintColor: Colors.transparent,
|
||
),
|
||
|
||
// ─── Extensions ───
|
||
extensions: [tokens],
|
||
);
|
||
}
|
||
|
||
/// Legacy-Getter (von main.dart genutzt) — nutzt aktuelle Akzentfarbe.
|
||
static ThemeData get theme => themeMitAkzent(akzent);
|
||
}
|