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.
99 lines
3.2 KiB
Dart
99 lines
3.2 KiB
Dart
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));
|
|
});
|
|
});
|
|
}
|