import 'database.dart'; /// Ergebnis einer Suche in der Bibliothek, nach Art der Treffer getrennt. /// /// Bewusst vier Listen statt einer gemischten: der Nutzer sucht entweder ein /// Lied oder einen Künstler, und eine flache Liste zwingt ihn, das selbst /// auseinanderzuhalten. class SuchTreffer { const SuchTreffer({ this.titel = const [], this.kuenstler = const [], this.kategorien = const [], this.playlisten = const [], }); final List titel; final List kuenstler; final List kategorien; final List playlisten; bool get istLeer => titel.isEmpty && kuenstler.isEmpty && kategorien.isEmpty && playlisten.isEmpty; } /// Normalform eines Namens für den Abgleich. /// /// Die Suche fasst „Neoni", „NEONI" und „Neoni " zu einem Treffer zusammen. /// Wer danach die Titel dieses Namens sucht, muss **dieselbe** Normalform /// verwenden — sonst zeigt der Treffer „0 Titel" und öffnet eine leere Liste, /// weil er auf eine Schreibweise zeigt, die so nirgends gespeichert ist. String suchSchluessel(String name) => name.trim().toLowerCase(); /// Bündelt [songs] unter [suchSchluessel] ihrer Künstler. Nur Titel mit /// Künstlernamen; verschiedene Schreibweisen fallen zusammen. Map> kuenstlerNachSchluessel(List songs) { final gebuendelt = >{}; for (final song in songs) { final name = song.artist?.trim() ?? ''; if (name.isEmpty) continue; gebuendelt.putIfAbsent(suchSchluessel(name), () => []).add(song); } return gebuendelt; } /// Bündelt [songs] unter [suchSchluessel] ihrer Kategorien. [kategorienVon] /// liefert die Kategorien eines Titels (wie `CategoryService.of`). Map> kategorienNachSchluessel( List songs, List Function(Song) kategorienVon, ) { final gebuendelt = >{}; for (final song in songs) { for (final name in kategorienVon(song)) { if (name.trim().isEmpty) continue; gebuendelt.putIfAbsent(suchSchluessel(name), () => []).add(song); } } return gebuendelt; } /// Durchsucht die Bibliothek. Rein — ohne Datenbank und ohne Oberfläche, /// damit die Regeln unten für sich prüfbar sind. /// /// [kategorien] sind alle vergebenen Kategorienamen. Alben stehen dort mit /// drin: der Album-Name ist in Melo die erste Kategorie eines Titels (siehe /// `kategorienAusTags`), ein eigener Alben-Abschnitt wäre also dieselbe Liste /// zweimal. SuchTreffer sucheLokal({ required List songs, required List kategorien, required List playlisten, required String query, }) { final gesucht = query.trim().toLowerCase(); if (gesucht.isEmpty) return const SuchTreffer(); return SuchTreffer( titel: _sortiere( songs.where((s) => _passtSong(s, gesucht)).toList(), gesucht, (s) => s.title, ), kuenstler: _sortiere( _eindeutig(songs.map((s) => s.artist)) .where((name) => _enthaelt(name, gesucht)) .toList(), gesucht, (name) => name, ), kategorien: _sortiere( kategorien.where((name) => _enthaelt(name, gesucht)).toList(), gesucht, (name) => name, ), playlisten: _sortiere( playlisten.where((p) => _enthaelt(p.name, gesucht)).toList(), gesucht, (p) => p.name, ), ); } bool _enthaelt(String? wert, String gesucht) => wert != null && wert.toLowerCase().contains(gesucht); /// Ein Titel passt, wenn Titel, Künstler **oder** Album passen — er steht /// deshalb trotzdem nur einmal in der Trefferliste. bool _passtSong(Song s, String gesucht) => _enthaelt(s.title, gesucht) || _enthaelt(s.artist, gesucht) || _enthaelt(s.album, gesucht); /// Nicht-leere Werte, jeder nur einmal, in der Reihenfolge des ersten /// Auftretens. Groß-/Kleinschreibung entscheidet nicht über Gleichheit. List _eindeutig(Iterable werte) { final gesehen = {}; final ergebnis = []; for (final wert in werte) { final name = wert?.trim() ?? ''; if (name.isEmpty) continue; if (gesehen.add(name.toLowerCase())) ergebnis.add(name); } return ergebnis; } /// Treffer, die mit der Eingabe **beginnen**, zuerst — danach alphabetisch. /// Wer „roses" tippt, meint eher „Roses" als „Wild Roses". List _sortiere(List werte, String gesucht, String Function(T) nameVon) { int rang(T wert) => nameVon(wert).toLowerCase().startsWith(gesucht) ? 0 : 1; return werte ..sort((a, b) { final unterschied = rang(a).compareTo(rang(b)); if (unterschied != 0) return unterschied; return nameVon(a).toLowerCase().compareTo(nameVon(b).toLowerCase()); }); }