Kein Redesign — Schwarz/Rot, 4 Tabs und Lieder/Kategorie bleiben. Grundlage sind die Design-Skills: ui-ux-pro-max (Barrierefreiheit, Touch, Typografie, Bewegung), ui-design/mobile-android (Material 3) und die Token-Disziplin aus Hue. Behobene Maengel (messbar, nicht Geschmack): - Text in Colors.white38 an 17 Stellen = 3,4:1 Kontrast, WCAG verlangt 4,5:1. Ersetzt durch drei benannte Stufen (text1 15,9:1 / text2 8,8:1 / text3 4,9:1). theme_kontrast_test.dart rechnet die Verhaeltnisse bei jedem Lauf nach, statt sie zu behaupten. - SubTabs waren ~38 dp hoch (Material 3 verlangt 48), ohne Ripple und ohne Uebergang. Jetzt 48 dp, InkWell, AnimatedContainer, und die Auswahl wird Vorlesehilfen als Zustand gemeldet (Semantics.selected). - mini_player.dart: Expanded UM eine feste Hoehe herum — zwei widerspruechliche Angaben. Entschaerft. - Emoji in Bedienelementen der Einstellungen (jeweils neben einem echten Icon) entfernt. Neu in shared/theme.dart: MeloSpace (8er-Raster), MeloRadius, MeloMotion, Farbrollen text1/2/3 + border + hairline, minTouchTarget, vollstaendiges textTheme (7 Stufen) und Themes fuer Listen, Sheets, Snackbars, Fortschritt, Trenner. 318 Tests gruen (23 neue, 1 uebersprungen), flutter analyze ohne Befund. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
182 lines
6.3 KiB
Dart
182 lines
6.3 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)),
|
|
),
|
|
),
|
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
|
backgroundColor: surface,
|
|
selectedItemColor: red,
|
|
unselectedItemColor: text3,
|
|
type: BottomNavigationBarType.fixed,
|
|
selectedLabelStyle:
|
|
TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
|
unselectedLabelStyle: TextStyle(fontSize: 12),
|
|
),
|
|
);
|
|
}
|
|
}
|