v2.56 — UI/Design-Rework: Melo Dark Fusion (neue Startseite, 2-stufige Einstellungen, Design-System)

- 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
This commit is contained in:
Dustin
2026-08-06 19:56:02 +02:00
parent 85615d253c
commit 79b15a3cb8
14 changed files with 1078 additions and 364 deletions
+335 -36
View File
@@ -1,13 +1,140 @@
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(0xFF0D0D0D);
static const Color dunkel1 = Color(0xFF1A1A1A);
static const Color dunkel2 = Color(0xFF2A2A2A);
static const Color textPrimaer = Color(0xFFFFFFFF);
static const Color textSekundaer = Color(0xFF888888);
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].
@@ -15,37 +142,209 @@ class MeloTheme {
static Color get akzent => akzentNotifier.value;
static set akzent(Color farbe) => akzentNotifier.value = farbe;
static ThemeData get theme => ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: schwarz,
colorScheme: ColorScheme.dark(
primary: akzent,
secondary: akzent,
surface: schwarz,
),
appBarTheme: const AppBarTheme(
backgroundColor: schwarz,
foregroundColor: textPrimaer,
elevation: 0,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: schwarz,
selectedItemColor: akzent,
unselectedItemColor: textSekundaer,
),
cardTheme: CardThemeData(
color: dunkel1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
/// 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,
),
),
chipTheme: ChipThemeData(
backgroundColor: dunkel1,
selectedColor: akzent,
labelStyle: const TextStyle(color: textPrimaer),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
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);
}