import 'package:flutter/material.dart'; /// Abstände. Alles Sichtbare rastet auf 8 ein (4 nur für Feinheiten /// innerhalb eines Elements) — daher wirken Listen und Karten ruhig, /// obwohl nirgends eine Zahl von Hand gewählt wurde. abstract final class MeloSpace { /// 4 — innerhalb eines Elements (Icon zu Text). static const double xs = 4; /// 8 — zwischen eng zusammengehörenden Elementen. static const double sm = 8; /// 16 — Standard: Seitenrand, Innenabstand einer Karte. static const double md = 16; /// 24 — zwischen Abschnitten. static const double lg = 24; /// 32 — vor einer neuen Überschrift. static const double xl = 32; } /// Eckenradien. Je größer die Fläche, desto größer die Rundung — sonst /// wirken große Karten kantig und kleine Chips aufgeblasen. abstract final class MeloRadius { /// 8 — Coverbilder, kleine Flächen. static const double element = 8; /// 12 — Karten. static const double card = 12; /// 20 — Sheets von unten. static const double sheet = 20; /// Pille — Reiter und Chips. static const double pill = 999; } /// Bewegung. Ruhig und kurz: Übergänge sollen die Verbindung zwischen zwei /// Zuständen zeigen, nicht auf sich aufmerksam machen. abstract final class MeloMotion { /// 120 ms — Reaktion auf eine Berührung (Farbe, Skalierung). static const Duration fast = Duration(milliseconds: 120); /// 220 ms — Wechsel eines Inhalts. static const Duration normal = Duration(milliseconds: 220); static const Curve curve = Curves.easeOutCubic; } /// Melo-Theme: Schwarz + Rot. class MeloTheme { static const Color red = Color(0xFFC0392B); static const Color black = Color(0xFF0B0B10); static const Color surface = Color(0xFF15151C); /// Etwas hellere Fläche für Karten und Chips auf [surface]. static const Color surfaceHigh = Color(0xFF1F1F29); // === Textfarben === // Die Werte sind nicht geschätzt: jede erfüllt auf der hellsten Fläche // ([surfaceHigh]) mindestens 4,5:1 nach WCAG. Siehe theme_kontrast_test.dart, // der die Verhältnisse nachrechnet. /// Titel und alles, was zuerst gelesen wird. static const Color text1 = Colors.white; /// Zweite Ebene: Künstler, Untertitel. static const Color text2 = Color(0xB8FFFFFF); // 72 % /// Dritte Ebene: Anzahl, Zeitangabe, Hinweis. Untere Grenze für Text — /// darunter ist der Kontrast nicht mehr ausreichend. static const Color text3 = Color(0x80FFFFFF); // 50 % /// Trennlinien und Umrisse. Kein Text — hier gilt 4,5:1 nicht. static const Color border = Color(0x14FFFFFF); // 8 % /// Die kleinste Fläche, die sich noch sicher treffen lässt (Material 3). static const double minTouchTarget = 48; /// Umriss statt Schlagschatten: auf Schwarz trennt eine Haarlinie /// Flächen sauberer als ein Schatten, den man ohnehin nicht sieht. static Border get hairline => Border.all(color: border, width: 1); static TextTheme get _textTheme => const TextTheme( // Bildschirm-Überschrift ("Meine Musik", "Online"). displaySmall: TextStyle( fontSize: 28, height: 1.15, fontWeight: FontWeight.w700, color: text1), // Titelleiste, Kopf eines Sheets. headlineSmall: TextStyle( fontSize: 22, height: 1.2, fontWeight: FontWeight.w700, color: text1), // Hervorgehobene Zeile ("Shuffle-Wiedergabe"). titleMedium: TextStyle( fontSize: 17, height: 1.3, fontWeight: FontWeight.w600, color: text1), // Titel in einer Liste. bodyLarge: TextStyle(fontSize: 15, height: 1.4, color: text1), // Untertitel in einer Liste. bodyMedium: TextStyle(fontSize: 13, height: 1.35, color: text2), // Beiwerk: Anzahl, Dauer. bodySmall: TextStyle(fontSize: 12, height: 1.3, color: text3), // Abschnitts-Beschriftung, Knopftext. labelLarge: TextStyle( fontSize: 12, height: 1.2, fontWeight: FontWeight.w600, letterSpacing: 0.4, color: text3), ); static ThemeData get dark { final scheme = ColorScheme.fromSeed( seedColor: red, brightness: Brightness.dark, ).copyWith( primary: red, surface: black, outline: border, ); return ThemeData( useMaterial3: true, colorScheme: scheme, scaffoldBackgroundColor: black, cardColor: surface, textTheme: _textTheme, splashFactory: InkSparkle.splashFactory, appBarTheme: const AppBarTheme( backgroundColor: black, surfaceTintColor: Colors.transparent, centerTitle: false, titleTextStyle: TextStyle( fontSize: 22, height: 1.2, fontWeight: FontWeight.w700, color: text1), ), listTileTheme: const ListTileThemeData( contentPadding: EdgeInsets.symmetric(horizontal: MeloSpace.md), minVerticalPadding: MeloSpace.sm, iconColor: text2, ), dividerTheme: const DividerThemeData( color: border, space: 1, thickness: 1, ), iconTheme: const IconThemeData(color: text2), sliderTheme: const SliderThemeData( activeTrackColor: red, thumbColor: red, inactiveTrackColor: border, trackHeight: 3, ), progressIndicatorTheme: const ProgressIndicatorThemeData( color: red, linearTrackColor: border, ), snackBarTheme: SnackBarThemeData( backgroundColor: surfaceHigh, contentTextStyle: const TextStyle(color: text1, fontSize: 14), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(MeloRadius.element), ), ), bottomSheetTheme: const BottomSheetThemeData( backgroundColor: surface, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(MeloRadius.sheet)), ), ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( backgroundColor: surface, selectedItemColor: red, unselectedItemColor: text3, type: BottomNavigationBarType.fixed, selectedLabelStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), unselectedLabelStyle: TextStyle(fontSize: 12), ), ); } }