Cover-Farbe, Teilen aus YouTube, Android Auto, geschärfte Typografie
P3-Liste aus dem UX-Review: Player nimmt Cover-Farbe auf, "Teilen" aus YouTube trägt die Adresse in den Downloader ein, Android Auto blättert durch die Bibliothek, Laufweite der Typografie geschärft, Überschriften vereinheitlicht. 533 Tests grün, flutter analyze ohne Befund.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:melo/player/auto_browse.dart';
|
||||
|
||||
/// Im Auto und beim Sprachassistenten blättert man durch die Bibliothek,
|
||||
/// statt die App zu bedienen. Jeder Knoten braucht dafür eine Adresse, die
|
||||
/// hin und zurück eindeutig ist.
|
||||
void main() {
|
||||
group('knotenId und knotenAus', () {
|
||||
test('eine Ordner-Adresse ohne Namen kommt unverändert zurück', () {
|
||||
final id = knotenId(AutoArt.favoriten);
|
||||
|
||||
final knoten = knotenAus(id);
|
||||
|
||||
expect(knoten!.art, AutoArt.favoriten);
|
||||
expect(knoten.name, isNull);
|
||||
});
|
||||
|
||||
test('ein Name übersteht den Weg hin und zurück', () {
|
||||
final id = knotenId(AutoArt.kategorie, 'Trap cool');
|
||||
|
||||
final knoten = knotenAus(id);
|
||||
|
||||
expect(knoten!.art, AutoArt.kategorie);
|
||||
expect(knoten.name, 'Trap cool');
|
||||
});
|
||||
|
||||
test('Doppelpunkte im Namen zerlegen die Adresse nicht', () {
|
||||
// "Intro: Der Anfang" ist ein völlig normaler Albumtitel — und der
|
||||
// Doppelpunkt ist zugleich unser Trennzeichen.
|
||||
final knoten = knotenAus(knotenId(AutoArt.kategorie, 'Intro: Anfang'));
|
||||
|
||||
expect(knoten!.name, 'Intro: Anfang');
|
||||
});
|
||||
|
||||
test('Schrägstriche und Umlaute überstehen es ebenfalls', () {
|
||||
const name = 'AC/DC — Küchenlieder';
|
||||
|
||||
expect(knotenAus(knotenId(AutoArt.kuenstler, name))!.name, name);
|
||||
});
|
||||
|
||||
test('ein Titel trägt seine Song-UUID', () {
|
||||
final knoten = knotenAus(knotenId(AutoArt.titel, 'uuid-1'));
|
||||
|
||||
expect(knoten!.art, AutoArt.titel);
|
||||
expect(knoten.name, 'uuid-1');
|
||||
});
|
||||
|
||||
test('eine fremde Adresse ergibt nichts', () {
|
||||
expect(knotenAus('irgendwas'), isNull);
|
||||
expect(knotenAus(''), isNull);
|
||||
expect(knotenAus('melo:gibtesnicht'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('wurzelArten', () {
|
||||
test('bietet die vier Wege an, die auch die App hat', () {
|
||||
expect(wurzelArten, [
|
||||
AutoArt.favoriten,
|
||||
AutoArt.zuletzt,
|
||||
AutoArt.kategorien,
|
||||
AutoArt.kuenstlerListe,
|
||||
]);
|
||||
});
|
||||
|
||||
test('jede Wurzel hat einen Namen für die Anzeige', () {
|
||||
for (final art in wurzelArten) {
|
||||
expect(art.anzeige, isNotEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
test('die Wurzeln sind Ordner, keine Titel', () {
|
||||
// Ein Ordner darf nicht abspielbar sein, sonst versucht das Auto ihn
|
||||
// als Lied zu starten.
|
||||
for (final art in wurzelArten) {
|
||||
expect(art == AutoArt.titel, isFalse);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:melo/player/cover_farbe.dart';
|
||||
import 'package:melo/shared/theme.dart';
|
||||
|
||||
/// Baut ein RGBA-Bild aus Farben — je Farbe [wiederholungen] Bildpunkte.
|
||||
Uint8List _bild(List<(Color, int)> teile) {
|
||||
final bytes = <int>[];
|
||||
for (final (farbe, anzahl) in teile) {
|
||||
for (var i = 0; i < anzahl; i++) {
|
||||
bytes.addAll([
|
||||
(farbe.r * 255).round(),
|
||||
(farbe.g * 255).round(),
|
||||
(farbe.b * 255).round(),
|
||||
(farbe.a * 255).round(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
||||
/// Der Player war für jeden Titel gleich schwarz. Diese Regel holt die
|
||||
/// prägende Farbe aus dem Coverbild.
|
||||
void main() {
|
||||
group('dominanteFarbe', () {
|
||||
test('ein einfarbiges Bild ergibt seine Farbe', () {
|
||||
const rot = Color(0xFFD32F2F);
|
||||
|
||||
final farbe = dominanteFarbe(_bild([(rot, 100)]));
|
||||
|
||||
// Grob gerundet — die Farben werden in Töpfe einsortiert.
|
||||
expect((farbe.r * 255).round(), closeTo(211, 16));
|
||||
expect((farbe.g * 255).round(), closeTo(47, 16));
|
||||
expect((farbe.b * 255).round(), closeTo(47, 16));
|
||||
});
|
||||
|
||||
test('eine kräftige Farbe schlägt viel Grau', () {
|
||||
// Cover sind oft überwiegend dunkel oder grau. Die häufigste Farbe wäre
|
||||
// dann ein Grauton, und der Player sähe für jeden Titel gleich aus.
|
||||
final farbe = dominanteFarbe(_bild([
|
||||
(const Color(0xFF808080), 900),
|
||||
(const Color(0xFF00A0FF), 100),
|
||||
]));
|
||||
|
||||
expect(farbe.b, greaterThan(farbe.r));
|
||||
});
|
||||
|
||||
test('ein rein schwarzes Bild fällt auf die Markenfarbe zurück', () {
|
||||
expect(dominanteFarbe(_bild([(const Color(0xFF000000), 100)])),
|
||||
MeloTheme.red);
|
||||
});
|
||||
|
||||
test('ein rein weißes Bild fällt ebenfalls zurück', () {
|
||||
expect(dominanteFarbe(_bild([(const Color(0xFFFFFFFF), 100)])),
|
||||
MeloTheme.red);
|
||||
});
|
||||
|
||||
test('durchsichtige Bildpunkte zählen nicht mit', () {
|
||||
final farbe = dominanteFarbe(_bild([
|
||||
(const Color(0x0000FF00), 900),
|
||||
(const Color(0xFFD32F2F), 100),
|
||||
]));
|
||||
|
||||
expect(farbe.r, greaterThan(farbe.g));
|
||||
});
|
||||
|
||||
test('ein leeres Bild fällt auf die Markenfarbe zurück', () {
|
||||
expect(dominanteFarbe(Uint8List(0)), MeloTheme.red);
|
||||
});
|
||||
|
||||
test('unvollständige Bildpunkte am Ende stören nicht', () {
|
||||
// Abgeschnittene Daten dürfen keinen Bereichsfehler auslösen.
|
||||
final bytes = _bild([(const Color(0xFFD32F2F), 10)]).toList()
|
||||
..addAll([12, 34]);
|
||||
|
||||
expect(() => dominanteFarbe(Uint8List.fromList(bytes)), returnsNormally);
|
||||
});
|
||||
});
|
||||
|
||||
group('lesbarAuf', () {
|
||||
test('dunkle Farben bleiben dunkel genug für weiße Schrift', () {
|
||||
final farbe = lesbarAuf(const Color(0xFFFFEB3B));
|
||||
|
||||
// Ein knalliges Gelb muss abgedunkelt werden, sonst ist weiße Schrift
|
||||
// darauf nicht mehr lesbar.
|
||||
expect(farbe.computeLuminance(), lessThan(0.35));
|
||||
});
|
||||
|
||||
test('eine bereits dunkle Farbe bleibt weitgehend erhalten', () {
|
||||
const dunkel = Color(0xFF1A237E);
|
||||
|
||||
expect(lesbarAuf(dunkel).computeLuminance(),
|
||||
closeTo(dunkel.computeLuminance(), 0.1));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user