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:
+335
-36
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user