Entwarnung vorweg: es fehlt nichts. Alle vier Bereiche (Meine Musik, Online, Suchen, Favoriten) standen durchgehend in der Bottom-Nav; lib/main.dart wurde von der UI-Politur gar nicht angefasst. Aber die Politur hatte das Menue abgeschwaecht — zwei eigene Fehler: - selectedLabelStyle/unselectedLabelStyle bekamen beide fontSize 12. Damit verlor der aktive Eintrag den Groessenunterschied (Flutter-Vorgabe 14 zu 12), an dem man erkennt, wo man ist. Jetzt keine fontSize mehr. - unselectedItemColor stand auf text3 (Beiwerk-Stufe). Ein Hauptmenue ist kein Beiwerk -> text2. Zusaetzlich deutlicher: Haarlinie ueber der Leiste (auf Schwarz ging sie sonst im Inhalt auf), gefuellte Symbole fuer den aktiven Bereich (zweites Merkmal neben der Farbe) und fetterer aktiver Eintrag. Neu: test/hauptmenue_test.dart — prueft Vorhandensein, Sichtbarkeit auf dem Bildschirm, Position am unteren Rand, Wechsel per Tipp und die Abhebung des aktiven Bereichs. Damit kann keine weitere Politur das Menue unbemerkt abschwaechen. Nebenbei eine Test-Falle geloest: await db.close() nach einem tester.tap laesst den Lauf unbegrenzt haengen (drift plant eine Aufraeum-Aufgabe ein, die der Test-Rahmen nicht mehr abarbeitet). Rezept im Test dokumentiert. Beim bekannten Haenger in song_detail_sheet_test.dart hilft es NICHT — gegengeprueft und als eigene Ursache im BACKLOG vermerkt. 328 Tests gruen (10 neue, 1 uebersprungen), flutter analyze ohne Befund. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
189 lines
6.8 KiB
Dart
189 lines
6.8 KiB
Dart
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)),
|
|
),
|
|
),
|
|
// Das Hauptmenü. Es muss auf den ersten Blick als solches erkennbar
|
|
// sein — deshalb bewusst kräftiger als der übrige Beiwerk-Text:
|
|
// - text2 (72 %) statt der blassen dritten Stufe für die inaktiven
|
|
// Einträge; sie sollen lesbar sein, nicht nur erahnbar.
|
|
// - KEINE fontSize hier: damit gelten Flutters Vorgaben von 14 (aktiv)
|
|
// und 12 (inaktiv). Eine feste Größe für beide nahm dem aktiven
|
|
// Reiter genau den Größenunterschied, an dem man ihn erkennt.
|
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
|
backgroundColor: surface,
|
|
selectedItemColor: red,
|
|
unselectedItemColor: text2,
|
|
type: BottomNavigationBarType.fixed,
|
|
showUnselectedLabels: true,
|
|
selectedLabelStyle: TextStyle(fontWeight: FontWeight.w700),
|
|
unselectedLabelStyle: TextStyle(fontWeight: FontWeight.w500),
|
|
),
|
|
);
|
|
}
|
|
}
|