This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Dustin 79b15a3cb8 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
2026-08-06 19:56:02 +02:00

351 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}