Code Review Optimierungen: Typsicherheit, Scanner-Performance, DB Upgrade Template, Division Guard
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
# Melo App – Server-Sync & Widgets Implementation Plan
|
||||||
|
|
||||||
|
> **Für Hermes:** Schritt-für-Schritt abarbeiten, keine Tasks springen.
|
||||||
|
|
||||||
|
**Goal:** Melo App um Widgets + Hidden Message "Seit 2008" erweitern, dann Server-Sync auf Aingrad aufsetzen (Musik zentral lagern, von jedem Gerät abrufbar).
|
||||||
|
|
||||||
|
**Architektur:**
|
||||||
|
- Phase 1+2: Reine App-Erweiterungen (Widgets + Easter Egg)
|
||||||
|
- Phase 3: Server als Caddy-File-Server + JSON-API, App holt Song-Liste und lädt runter
|
||||||
|
|
||||||
|
**Tech Stack:** Flutter/Dart, Caddy (läuft auf Aingrad), SQLite (bereits vorhanden)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Widgets
|
||||||
|
|
||||||
|
### Task 1: Home-Widget (Recent Songs)
|
||||||
|
|
||||||
|
**Objective:** Widget zeigt die letzten 5 abgespielten Songs auf dem Home-Screen
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `lib/widgets/recent_widget.dart`
|
||||||
|
- Modify: `lib/screens/home_screen.dart` (Widget einbauen)
|
||||||
|
- Modify: `lib/viewmodels/melo_home_viewmodel.dart` (letzte Songs laden)
|
||||||
|
|
||||||
|
**Schritte:**
|
||||||
|
|
||||||
|
1. `recent_widget.dart` erstellen – kleiner Container mit den letzten 5 Songs, ähnlich wie StatistikCard aufgebaut (schwarz/rot Design)
|
||||||
|
2. ViewModel: `letzteSongs` laden aus `wiedergabe_verlauf`-Tabelle via `db.positionAktualisieren` – die Tabelle existiert bereits
|
||||||
|
3. `home_screen.dart`: Widget unter StatistikCard einbauen
|
||||||
|
4. Nur sichtbar wenn `letzteSongs.isNotEmpty`
|
||||||
|
|
||||||
|
**Code-Skizze recent_widget.dart (~40 Zeilen):**
|
||||||
|
```dart
|
||||||
|
class RecentWidget extends StatelessWidget {
|
||||||
|
final List<Song> songs;
|
||||||
|
final void Function(Song) onPlay;
|
||||||
|
|
||||||
|
// UI: kleiner Container, 5 horizontale Einträge mit Cover + Titel
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Tag-Statistik-Widget
|
||||||
|
|
||||||
|
**Objective:** Widget zeigt wie viele Songs pro Tag existieren (kleine Übersicht)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `lib/widgets/tag_stats_widget.dart`
|
||||||
|
- Modify: `lib/viewmodels/melo_home_viewmodel.dart` (Tag-Counts berechnen)
|
||||||
|
- Modify: `lib/screens/home_screen.dart`
|
||||||
|
|
||||||
|
**Schritte:**
|
||||||
|
|
||||||
|
1. ViewModel: `Map<String, int> tagCounts` – zählt Songs pro Tag-Namen
|
||||||
|
2. `tag_stats_widget.dart` – horizontale Chips mit Tag-Name + Count
|
||||||
|
3. In home_screen unter Tags-Leiste einbauen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Hidden Message "Seit 2008"
|
||||||
|
|
||||||
|
### Task 3: Easter Egg auf dem Home-Screen
|
||||||
|
|
||||||
|
**Objective:** Versteckte Nachricht "Seit 2008" die nach 10 Song-Playbacks erscheint
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `lib/viewmodels/melo_home_viewmodel.dart` (Counter + Logik)
|
||||||
|
- Modify: `lib/screens/home_screen.dart` (Anzeige)
|
||||||
|
|
||||||
|
**Schritte:**
|
||||||
|
|
||||||
|
1. ViewModel: `int _playCount = 0; bool get zeigeBotschaft => _playCount >= 10;`
|
||||||
|
2. Bei jedem `spieleSong()` → `_playCount++`, bei 10 → `zeigeBotschaft = true`
|
||||||
|
3. UI: Wenn `zeigeBotschaft`, zeige ein rotes Herz mit "💌 Seit 2008" zwischen Header und StatistikCard
|
||||||
|
4. Nach einmaligem Anzeigen → `_playCount` zurücksetzen oder ausblendbar
|
||||||
|
|
||||||
|
**Code (~15 Zeilen ViewModel + ~10 Zeilen UI)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Server-Sync
|
||||||
|
|
||||||
|
### Task 4: Caddy-Subdomain einrichten
|
||||||
|
|
||||||
|
**Objective:** `musik.baka-net.de` served Musik-Ordner + JSON-Liste
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `/etc/caddy/Caddyfile` (auf Aingrad)
|
||||||
|
|
||||||
|
**Schritte:**
|
||||||
|
|
||||||
|
1. Ordner erstellen: `mkdir -p /home/dustin/musik/songs`
|
||||||
|
2. Caddyfile-Eintrag:
|
||||||
|
```
|
||||||
|
musik.baka-net.de {
|
||||||
|
root * /home/dustin/musik
|
||||||
|
file_server browse
|
||||||
|
@auth {
|
||||||
|
path /upload*
|
||||||
|
}
|
||||||
|
basicauth @auth {
|
||||||
|
melo HASH_PASSWORT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. JSON-API Script: `/home/dustin/musik/api/songs.json` – generiert Liste aller `.mp3`/`.m4a`/`.flac` Dateien mit Metadaten
|
||||||
|
4. `caddy reload`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: ServerSyncService in der App
|
||||||
|
|
||||||
|
**Objective:** Neuer Service ruft Server-Liste ab und managed Downloads
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `lib/services/server_sync_service.dart`
|
||||||
|
- Create: `lib/models/server_status.dart`
|
||||||
|
- Modify: `lib/screens/home_screen.dart` (neuer Tab)
|
||||||
|
|
||||||
|
**Schritte:**
|
||||||
|
|
||||||
|
1. `ServerSyncService` mit Methoden:
|
||||||
|
- `Future<List<ServerSong>> fetchServerList()` – ruft `musik.baka-net.de/api/songs.json` ab
|
||||||
|
- `Future<Song?> downloadSong(ServerSong s)` – lädt runter, speichert lokal + in DB
|
||||||
|
- `Future<bool> istBereitsLokal(ServerSong s)` – checkt ob Datei existiert
|
||||||
|
|
||||||
|
2. Neuer Screen/Sheet: "📡 Server"-Button im Header öffnet Server-Browser
|
||||||
|
- Zeigt alle Songs auf dem Server
|
||||||
|
- Status: "Downloaded ✓" / "Download ⬇"
|
||||||
|
- Fortschrittsbalken wie beim YouTube-Download
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Auto-Scan von Server-Songs
|
||||||
|
|
||||||
|
**Objective:** Einmal pro Session prüfen ob Server neue Songs hat
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `lib/viewmodels/melo_home_viewmodel.dart`
|
||||||
|
- Modify: `lib/services/server_sync_service.dart`
|
||||||
|
|
||||||
|
**Schritte:**
|
||||||
|
|
||||||
|
1. `ladeSongs()` → zusätzlich Server-Scan starten (im Hintergrund)
|
||||||
|
2. Neue Songs automatisch in DB eintragen (ohne Download – nur Metadaten)
|
||||||
|
3. Badge im Server-Button: "3 neue Songs"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abhängigkeiten
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 (Widgets) → Phase 2 (Hidden Message) → Phase 3 (Server-Sync)
|
||||||
|
↓
|
||||||
|
Alles unabhängig von Server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Widgets: Nach App-Neustart sichtbar, reagieren auf Song-Änderungen
|
||||||
|
- Hidden Message: Nach 10 Playbacks erscheint "Seit 2008"
|
||||||
|
- Server-Sync: `curl https://musik.baka-net.de/api/songs.json` liefert JSON
|
||||||
|
- App: Server-Tab zeigt Songs, Download funktioniert, Musik spielt ab
|
||||||
|
|
||||||
|
## Offene Fragen
|
||||||
|
|
||||||
|
- Soll Hidden Message persistiert werden (einmalig, nie wieder)?
|
||||||
|
- Auth für Download nötig oder öffentlich? (Server intern → öffentlich okay)
|
||||||
|
- Auto-Upload von lokal gescannten Songs zum Server? (Nur Download, kein Upload)
|
||||||
+37
-516
@@ -1,532 +1,53 @@
|
|||||||
# 🔴 Melo App — Vollständiges Code-Review
|
# 🟢 Melo App — Aktuelles Code-Review (v2.5)
|
||||||
|
|
||||||
> **Geprüft:** 20.07.2026 | **Projekt:** ~/Projects/melo_app/ | **Dustin (Baka) — Kiel**
|
> **Geprüft:** 24.07.2026 | **Projekt:** ~/Projects/melo_app/ | **Dustin (Baka) — Kiel**
|
||||||
> **Prinzip:** Ponytail — minimaler Code, maximale Wirkung, max 500 Zeilen pro Datei
|
> **Prinzip:** Ponytail — minimaler Code, maximale Wirkung, max 500 Zeilen pro Datei
|
||||||
|
> **Gesamt-Score:** **A-** (Hervorragend verbessert!)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Kritische Bugs (MÜSSEN SOFORT GEFIXT WERDEN)
|
## ⭐️ Status der Altschulden (Review-Vergleich)
|
||||||
|
Alle kritischen Probleme aus dem vorherigen Review wurden **vollständig und sauber gelöst**:
|
||||||
### 🔴 [CRIT-1] Memory Leak: MiniPlayer-Streams werden nie gecancelled
|
- ✅ **CRIT-1 (Memory Leak im MiniPlayer)**: `StreamSubscriptions` werden jetzt ordnungsgemäß in `dispose()` gecancelled.
|
||||||
|
- ✅ **CRIT-2 (Null-Crash im Metadaten-Editor)**: `widget.song.id!` wurde durch ein sicheres ID-Null-Guard ersetzt.
|
||||||
**Datei:** `widgets/mini_player.dart` · Zeilen 20–32
|
- ✅ **CRIT-3 (Dauer-Sekunden = 0)**: Wird jetzt beim Scannen über den Player ermittelt. (Siehe Performance für Optimierungspotenzial).
|
||||||
|
- ✅ **CRIT-4 (Verlaufs-Flut)**: `positionAktualisieren` führt jetzt saubere Updates/Upserts statt endloser Inserts durch.
|
||||||
```dart
|
- ✅ **CRIT-5 (ErrorBoundary-Deko)**: Der tote `AppWrapper` und `FlutterErrorBoundary` in `main.dart` wurden komplett entfernt (Ponytail-Prinzip!).
|
||||||
// initState abonniert Streams, aber speichert kein StreamSubscription
|
|
||||||
_player.positionStream.listen((pos) { ... });
|
|
||||||
_player.stateStream.listen((state) { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** `initState` abonniert `positionStream` und `stateStream` via `.listen()`, aber es gibt **kein** `dispose()`-Override. Die `StreamSubscription`-Objekte werden nirgends gespeichert, also können sie nie gecancelled werden. Jedes Mal wenn der MiniPlayer neu gebaut wird (z.B. bei setState im Parent), leakt ein neues Paar Subscriptions.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
```dart
|
|
||||||
StreamSubscription? _posSub;
|
|
||||||
StreamSubscription? _stateSub;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_posSub = _player.positionStream.listen((pos) { ... });
|
|
||||||
_stateSub = _player.stateStream.listen((state) { ... });
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_posSub?.cancel();
|
|
||||||
_stateSub?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔴 [CRIT-2] Null-Crash: `widget.song.id!` kann explodieren
|
## 📐 Five-Axis Quality Gate
|
||||||
|
|
||||||
**Datei:** `widgets/metadaten_dialog.dart` · Zeile 88
|
### 1. CORRECTNESS (Korrektheits-Check) — **Score: A-**
|
||||||
|
- **Sicherheits-Guards**: Hervorragende null-safety Guards im Metadaten-Editor und beim Wiedergabestatus.
|
||||||
|
- **Division durch Null (Download)**: In `download_service.dart:103` bei `_fortschritt = downloaded / total;` besteht ein minimales theoretisches Risiko eines `NaN` (Division durch Null), falls `total` (audio.size.totalBytes) jemals `0` zurückgeben sollte.
|
||||||
|
- *Empfehlung:* Ein kurzer Ternary Guard: `_fortschritt = total > 0 ? downloaded / total : 0.0;`
|
||||||
|
- **Substring-Range**: `p.erstelltAm.substring(0, 10)` in `playlist_sheet.dart:103` ist sicher, da `erstelltAm` durch das ISO-Format standardmäßig >= 19 Zeichen lang ist.
|
||||||
|
|
||||||
```dart
|
### 2. READABILITY (Lesbarkeit) — **Score: A**
|
||||||
await _db.metadatenAktualisieren(
|
- **Konsistente Namensgebung**: Großartiger, verständlicher Code. Deutsche Variablennamen (`_aktiverTab`, `_ladeSongs()`) werden konsistent und clean durchgezogen.
|
||||||
widget.song.id!, // ← CRASH wenn id null ist!
|
- **Kein Code-Spam**: Die Klassen sind kompakt, sauber formatiert und verzichten auf unnötige Schachtelungen.
|
||||||
...
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** `Song.id` ist `int?`. Wenn ein Song (z.B. frisch gescannter oder Beispielsong ohne DB-ID) den Metadaten-Editor öffnet, crasht die App mit `null check used on null value`.
|
### 3. ARCHITECTURE (Architektur & Struktur) — **Score: B+**
|
||||||
|
- **`dynamic vm` Anti-Pattern**: In `playlist_sheet.dart` und `navidrome_browser.dart` wird `final dynamic vm` verwendet. Das deaktiviert jegliche Typprüfung, Autovervollständigung und birgt das Risiko von Laufzeitfehlern, falls sich ViewModel-Methoden ändern.
|
||||||
|
- *Empfehlung:* Ersetze `dynamic vm` durch den konkreten Typ `final MeloHomeViewModel vm`.
|
||||||
|
- **Zukünftige DB-Migrationen**: `DbHelper.db` initialisiert die SQLite-Datenbank mit `version: 1` ohne `onUpgrade`-Callback. Wenn du später Tabellen anpasst, crasht die App bei bestehenden Nutzern beim Start.
|
||||||
|
- *Empfehlung:* Bereits jetzt eine leere `onUpgrade` Struktur im `openDatabase` vorsehen.
|
||||||
|
- **Redundanter Aufruf**: In `MeloHomeViewModel.ladeSongs()` wird `tagCounts = _berechneTagCounts();` aufgerufen, während `tags` noch ein leeres Array `[]` ist. Erst danach wird `await ladeTags()` aufgerufen, welches `_berechneTagCounts()` erneut aufruft. Der erste Aufruf ist also redundant.
|
||||||
|
|
||||||
**Fix:**
|
### 4. PERFORMANCE (Laufzeit & Ressourcen) — **Score: B**
|
||||||
```dart
|
- **Granularer Rebuild (Großartig!)**: Die Implementierung des `MiniPlayer` ist **perfekt gelöst**. Er lauscht direkt auf die Streams des Players und aktualisiert seinen State lokal per `setState`. Dadurch wird verhindert, dass bei jedem Millisekunden-Update des Fortschrittsbalkens der gesamte Home-Bildschirm (mit Statistiken, Listen und Covern) neu gerendert wird!
|
||||||
final id = widget.song.id;
|
- **🚨 Der Scanner-Flaschenhals**: In `lib/services/musik_scanner.dart` wird in `_ermittleDauer(pfad)` für **jeden einzelnen** gefundenen Song ein komplett neuer `AudioPlayer` instanziiert, die Datei geöffnet, die Dauer gelesen und der Player wieder disposed.
|
||||||
if (id == null) {
|
- *Problem:* Wenn ein Ordner 200 Songs enthält, wird 200-mal nacheinander ein nativer Player erzeugt und zerstört. Das blockiert den Scanvorgang extrem, zieht viel Akku und kann auf manchen Systemen zu Abstürzen oder "Platform Channel Exception"-Fehlern (Erschöpfung der Audio-Player-Kanäle) führen.
|
||||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
- *Empfehlung:* Entweder eine extrem leichtgewichtige Metadata-Bibliothek (`flutter_media_metadata`) nutzen, oder eine einzige `AudioPlayer`-Instanz für den gesamten Scanvorgang wiederverwenden, anstatt sie in der Schleife ständig neu zu erstellen.
|
||||||
const SnackBar(content: Text('Song-ID fehlt — bitte App neustarten')),
|
|
||||||
);
|
### 5. SECURITY (Sicherheit) — **Score: A**
|
||||||
return;
|
- **Berechtigungen**: Vorbildliche Handhabung von Android Scoped Storage Permissions über `Permission.audio` statt der veralteten `Permission.storage` (deprecating ab Android 13).
|
||||||
}
|
|
||||||
await _db.metadatenAktualisieren(id, ...);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔴 [CRIT-3] `dauerSekunden` ist IMMER 0 (kein Metadaten-Parsing)
|
## 🛠 Top 3 Quick Wins
|
||||||
|
|
||||||
**Datei:** `services/musik_scanner.dart` · Zeilen 34–44
|
1. **Typsicherheit im UI**: Ändere `final dynamic vm` in `playlist_sheet.dart` und `navidrome_browser.dart` zu `final MeloHomeViewModel vm`. Das gibt dir volles Autocomplete und Compile-Safety.
|
||||||
|
2. **Scanner beschleunigen**: Instanziiere den `AudioPlayer` für die Dauer-Ermittlung einmalig außerhalb der Schleife im `MusikScanner` und nutze ihn für alle Dateien, statt ihn pro Song neu zu erstellen und zu disposen.
|
||||||
```dart
|
3. **VM-Cleanup**: Entferne den redundanten Aufruf von `tagCounts = _berechneTagCounts();` aus Zeile 78 in `melo_home_viewmodel.dart`.
|
||||||
final song = Song(
|
|
||||||
titel: _dateiNameOhneEndung(pfad),
|
|
||||||
kuenstler: 'Unbekannt', // ← keinerlei Tag-Parsing
|
|
||||||
dauerSekunden: 0, // ← IMMER 0!
|
|
||||||
...
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Es wird nie die echte Audio-Dauer ausgelesen. Weder ID3-Tags noch MediaStore-Metadaten. Alle gescannten Songs haben `dauerSekunden: 0` → `dauerFormatiert` zeigt `"0:00"`.
|
|
||||||
|
|
||||||
**Fix:** Nutze eine Audio-Metadaten-Bibliothek wie `flutter_media_metadata` oder `audio_metadata_reader`, oder implementiere einen nativen Method-Channel für MediaStore-Query. Minimal-Fix: verwende `just_audio` kurz zum Öffnen der Datei um Duration zu lesen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔴 [CRIT-4] `positionAktualisieren` flutet DB mit immer neuen Einträgen
|
|
||||||
|
|
||||||
**Datei:** `database/db_helper.dart` · Zeilen 126–135
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Future<void> positionAktualisieren(int songId, int position) async {
|
|
||||||
final d = await db;
|
|
||||||
await d.update('songs', {'zuletzt_position': position},
|
|
||||||
where: 'id = ?', whereArgs: [songId]);
|
|
||||||
await d.insert('wiedergabe_verlauf', { // ← IMMER INSERT!
|
|
||||||
'song_id': songId,
|
|
||||||
'position': position,
|
|
||||||
'zuletzt_abgespielt': DateTime.now().toIso8601String(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Bei jedem Positions-Update wird ein NEUER History-Eintrag inserted, ohne den alten zu löschen. Nach 30 Minuten Musik hören mit 1-Sekunden-Takt → 1800 Einträge pro Song.
|
|
||||||
|
|
||||||
**Fix Variante A (einfach):** Lösche alten Eintrag vor neuem Insert:
|
|
||||||
```dart
|
|
||||||
await d.delete('wiedergabe_verlauf', where: 'song_id = ?', whereArgs: [songId]);
|
|
||||||
await d.insert('wiedergabe_verlauf', { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fix Variante B (besser):** Nur bei signifikanten Änderungen speichern (alle 30 Sekunden statt bei jedem Tick).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔴 [CRIT-5] `FlutterErrorBoundary` tut GAR NICHTS
|
|
||||||
|
|
||||||
**Datei:** `main.dart` · Zeilen 48–62
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class _FlutterErrorBoundaryState extends State<FlutterErrorBoundary> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return widget.child; // ← gibt einfach child zurück, kein Error Handling!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Das "Error Boundary" hat keinen Error-Catcher (`FlutterError.onError` oder `ErrorWidget.builder` oder `runZonedGuarded`). Es fängt genau null Fehler. Wird aber zum Glück auch nicht verwendet (der `AppWrapper` wird nie benutzt).
|
|
||||||
|
|
||||||
**Fix:** Entweder korrekt implementieren (mit `runZonedGuarded` oder `ErrorWidget.builder`) oder — besser — den ganzen `AppWrapper` + `FlutterErrorBoundary` rauswerfen (Ponytail!).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Schwere Probleme
|
|
||||||
|
|
||||||
### 🟠 [MAJ-1] Download-Fehlermeldungen werden brutal abgeschnitten
|
|
||||||
|
|
||||||
**Datei:** `services/download_service.dart` · Zeilen 56, 75, 105, 127
|
|
||||||
|
|
||||||
```dart
|
|
||||||
_fehlermeldung = 'Timeout/Fehler bei Video-Info: ${e.toString().substring(0, 80)}';
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** `.substring(0, 80)` und `.substring(0, 100)` zerstören Fehlerdetails. YouTube-Fehler (Rate Limits, Region-Locks, Copyright Claims) werden sinnlos.
|
|
||||||
|
|
||||||
**Fix:** Entweder gar nicht truncaten, oder erst auf UI-Ebene truncaten. Die Fehlermeldung sollte vollständig geloggt werden:
|
|
||||||
```dart
|
|
||||||
debugPrint('Download-Fehler (vollständig): $e');
|
|
||||||
_fehlermeldung = e.toString(); // UI zeigt nur ersten Teil, Log hat alles
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟠 [MAJ-2] MiniPlayer `dispose()` tut nichts (lebt als Memory Leak II)
|
|
||||||
|
|
||||||
**Datei:** `widgets/mini_player.dart` · Zeile 131
|
|
||||||
|
|
||||||
Kein `dispose()`-Override vorhanden! Die Klasse endet mit `Widget _playBtn() { ... }`. Dadurch werden nicht nur Streams nicht gecancelled (CRIT-1), sondern auch keine Ressourcen freigegeben.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟠 [MAJ-3] `_scanneViaMediaStore()` gibt IMMER `[]` zurück
|
|
||||||
|
|
||||||
**Datei:** `services/musik_scanner.dart` · Zeilen 121–126
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Future<List<String>> _scanneViaMediaStore() async {
|
|
||||||
// Nutzt Android's MediaStore Query
|
|
||||||
// Wird über Method Channel in native Android implementiert
|
|
||||||
// Für v1: Fallback auf Dateisystem-Suche
|
|
||||||
return []; // ← TODO seit Version 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Der MediaStore-Pfad ist nie implementiert. Der Fallback auf Dateisystem-Suche ist ineffizient und findet keine Musik in App-spezifischen Verzeichnissen. Auf Android 11+ (API 30+) wird der Dateisystem-Zugriff zudem stark eingeschränkt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟠 [MAJ-4] `_p.positionStream` referenziert möglicherweise alten Player
|
|
||||||
|
|
||||||
**Datei:** `services/player_service.dart` · Zeilen 32–33
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Stream<Duration> get positionStream => _p.positionStream; // _p initiiert lazy
|
|
||||||
Stream<PlayerState> get stateStream => _p.playerStateStream;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** `_p` initialisiert den `AudioPlayer` lazy beim ersten Zugriff. Wenn die Streams vor dem ersten `spiele()`-Aufruf abonniert werden, hängen sie an einem Player, der später ersetzt/reinitialisiert werden könnte. Der getter erzeugt keinen neuen Player, also ist das nur ein Problem wenn `_player` jemals auf null gesetzt wird (was nie passiert). Aber: `_p` wird von diesen Gettern aufgerufen → bei jedem Stream-Zugriff wird der `_player != null` Check gemacht. Das ist ok, aber es ist ein wartungsintensives Pattern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟠 [MAJ-5] `AppWrapper` ist totes Code-Gewebe
|
|
||||||
|
|
||||||
**Datei:** `main.dart` · Zeilen 34–46
|
|
||||||
|
|
||||||
`AppWrapper` wird nirgends verwendet. `MeloApp` geht direkt zu `Scaffold(body: MeloHome())`. Der Wrapper wurde offenbar geplant aber nie aktiviert.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟠 [MAJ-6] Tag-Filter wird in Song-Liste komplett ignoriert
|
|
||||||
|
|
||||||
**Datei:** `screens/home_screen.dart` · Zeilen 374–393 + 431–434
|
|
||||||
|
|
||||||
`_aktiverTag` wird zwar gesetzt (Zeile 382), aber in `_songListe()` (Zeile 431–434) wird die Song-Liste ungefiltert angezeigt:
|
|
||||||
```dart
|
|
||||||
itemCount: _songs.length, // ← immer alle Songs, egal welcher Tag aktiv
|
|
||||||
```
|
|
||||||
|
|
||||||
Die `_beispielTags` sind zudem hardcoded (nicht aus der DB) — das Tag-System hat keinen echten Filter.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Code-Qualität & Anti-Patterns
|
|
||||||
|
|
||||||
### 🟡 [QUAL-1] Singleton-Overkill (5 von 6 Klassen sind Singletons)
|
|
||||||
|
|
||||||
| Klasse | Singleton? | Warum problematisch |
|
|
||||||
|--------|-----------|---------------------|
|
|
||||||
| `DbHelper` | ✅ | Testen unmöglich (kein Mock) |
|
|
||||||
| `PlayerService` | ✅ | State hält zwischen Tests |
|
|
||||||
| `FavoritenService` | ✅ | s.o. |
|
|
||||||
| `DownloadService` | ✅ | s.o. |
|
|
||||||
| `MusikScanner` | ✅ | s.o. |
|
|
||||||
|
|
||||||
**Fix:** Dependency Injection via Konstruktor. Services sollten das DB-Objekt injiziert bekommen statt `DbHelper()` direkt aufzurufen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 [QUAL-2] HomeScreen Monolith — 519 Zeilen in einer Datei
|
|
||||||
|
|
||||||
**Datei:** `screens/home_screen.dart` · 519 Zeilen
|
|
||||||
|
|
||||||
Enthält:
|
|
||||||
- State Management
|
|
||||||
- Header-Widget
|
|
||||||
- Statistik-Widget
|
|
||||||
- Tag-Leiste
|
|
||||||
- Song-Liste
|
|
||||||
- Song-Tile
|
|
||||||
- Bottom Nav
|
|
||||||
- Download-Dialog (komplette UI + Timer-Logik!)
|
|
||||||
- Such-Dialog (komplette UI!)
|
|
||||||
- Scan-Logik
|
|
||||||
- Beispieldaten/Seed-Daten
|
|
||||||
|
|
||||||
**Ponytail-Prinzip verletzt!** Max 500 Zeilen pro Datei fast erreicht, aber die Verantwortlichkeiten sind vermischt.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
- Extrahiere `MeloHeader`, `StatistikCard`, `TagLeiste`, `SongTile` in eigene Widget-Dateien (`widgets/`)
|
|
||||||
- Extrahiere Such- und Download-Dialoge in eigene Methoden oder Dateien
|
|
||||||
- Entferne Beispiel-Song-Seeding (gehört in einen dev-only Seeder)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 [QUAL-3] `loeschen()` löscht in falscher Reihenfolge
|
|
||||||
|
|
||||||
**Datei:** `database/db_helper.dart` · Zeilen 221–229
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await d.delete('wiedergabe_verlauf');
|
|
||||||
await d.delete('song_tags');
|
|
||||||
await d.delete('playlist_songs');
|
|
||||||
await d.delete('playlists');
|
|
||||||
await d.delete('tags');
|
|
||||||
await d.delete('songs');
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Die Tabellen mit `ON DELETE CASCADE` werden vor den Eltern-Tabellen gelöscht. Theoretisch korrekt (CASCADE ist auf FK definiert), aber die Reihenfolge ist inkonsistent: `playlists` wird vor `tags` gelöscht, obwohl beide Eltern sind. Außerdem: **keine Transaktion!** Wenn ein Löschen fehlschlägt, hat man eine korrupte Datenbank.
|
|
||||||
|
|
||||||
**Fix:** Alles in eine Transaktion packen:
|
|
||||||
```dart
|
|
||||||
await d.transaction((txn) async {
|
|
||||||
await txn.delete('wiedergabe_verlauf');
|
|
||||||
await txn.delete('song_tags');
|
|
||||||
await txn.delete('playlist_songs');
|
|
||||||
await txn.delete('playlists');
|
|
||||||
await txn.delete('tags');
|
|
||||||
await txn.delete('songs');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 [QUAL-4] Doppelter try-catch in `musik_scanner.dart`
|
|
||||||
|
|
||||||
**Datei:** `services/musik_scanner.dart` · Zeilen 29–65
|
|
||||||
|
|
||||||
Zwei verschachtelte try-catch Blöcke die fast identischen Code enthalten. Der äußere try-catch fängt alles, und der innere ist ein "vielleicht klappt es beim zweiten Versuch" — ohne ersichtlichen Grund.
|
|
||||||
|
|
||||||
**Fix:** Einfach einen try-catch:
|
|
||||||
```dart
|
|
||||||
try {
|
|
||||||
final file = File(pfad);
|
|
||||||
final stat = await file.stat();
|
|
||||||
gefunden.add(/* Song erstellen */);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Datei nicht lesbar: $pfad — $e');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 [QUAL-5] `favoritenIds()` ist ineffizient (lädt alle Songs nur für IDs)
|
|
||||||
|
|
||||||
**Datei:** `services/favoriten_service.dart` · Zeilen 45–49
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Future<Set<int>> favoritenIds() async {
|
|
||||||
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
|
|
||||||
return songs.where((s) => s.id != null).map((s) => s.id!).toSet();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Lädt komplette Song-Objekte (mit allen Feldern) aus der DB, nur um die IDs zu bekommen. Bei 1000 Songs werden 1000 `Song.fromMap()`-Aufrufe gemacht.
|
|
||||||
|
|
||||||
**Fix:** Dedizierte DB-Query:
|
|
||||||
```dart
|
|
||||||
Future<Set<int>> favoritenIds() async {
|
|
||||||
final d = await _db.db;
|
|
||||||
final rows = await d.rawQuery(
|
|
||||||
'SELECT song_id FROM playlist_songs WHERE playlist_id = ?',
|
|
||||||
[_favoritenPlaylistId],
|
|
||||||
);
|
|
||||||
return rows.map((r) => r['song_id'] as int).toSet();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Überschüssiger Code (Ponytail-Prinzip)
|
|
||||||
|
|
||||||
### 🧹 [PONY-1] 4 redundante Zeilen im `_p` Getter
|
|
||||||
|
|
||||||
**Datei:** `services/player_service.dart` · Zeilen 16–25
|
|
||||||
|
|
||||||
```dart
|
|
||||||
AudioPlayer get _p {
|
|
||||||
if (_player == null) {
|
|
||||||
try {
|
|
||||||
_player = AudioPlayer();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('AudioPlayer Init Fehler: $e');
|
|
||||||
_player = AudioPlayer(); // ← gleicher Code nochmal??
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _player!;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Der doppelte `AudioPlayer()`-Aufruf im catch-Block ist sinnlos. Wenn der erste fehlschlägt, schlägt der zweite auch fehl. Einfach:
|
|
||||||
```dart
|
|
||||||
AudioPlayer get _p => _player ??= AudioPlayer();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🧹 [PONY-2] Beispiel-Songs + Tags aus home_screen.dart raus
|
|
||||||
|
|
||||||
**Datei:** `screens/home_screen.dart` · Zeilen 32–40, 52–67
|
|
||||||
|
|
||||||
Die `_beispielTags` (hardcoded) und die Beispiel-Song-Seeding (Zeilen 53–67) gehören nicht in den produktiven Screen. Das ist Dev-Code.
|
|
||||||
|
|
||||||
**Fix:** Entweder in einen `DevDataSeeder()` auslagern oder per `--dart-define` steuern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🧹 [PONY-3] Download-Dialog als StatefulBuilder im HomeScreen
|
|
||||||
|
|
||||||
**Datei:** `screens/home_screen.dart` · Zeilen 183–245
|
|
||||||
|
|
||||||
Der komplette Download-Dialog mit Timer-Polling ist im HomeScreen vergraben. Das sind ~60 Zeilen UI + Logik.
|
|
||||||
|
|
||||||
**Fix:** Eigene Widget-Datei `widgets/download_dialog.dart` mit `state`-haltendem Widget, das den Timer managed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🧹 [PONY-4] `_header()` + `_statistik()` + `_tagLeiste()` → eigene Widgets
|
|
||||||
|
|
||||||
**Datei:** `screens/home_screen.dart` · Zeilen 280–393
|
|
||||||
|
|
||||||
Diese drei Methoden sind alle UI-only und könnten als eigene Widgets in `widgets/` leben.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Security & Robustheit
|
|
||||||
|
|
||||||
### 🔒 [SEC-1] `Permission.storage` ist auf Android 33+ deprecated
|
|
||||||
|
|
||||||
**Datei:** `services/musik_scanner.dart` · Zeile 19
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final status = await Permission.storage.request();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Ab API 33 (Android 13) gibt es `READ_MEDIA_AUDIO` statt `READ_EXTERNAL_STORAGE`. `Permission.storage` fragt nach der alten Permission, die auf neueren Geräten ignoriert wird.
|
|
||||||
|
|
||||||
**Fix:**
|
|
||||||
```dart
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
// Oder besser über permission_handler Android-specific
|
|
||||||
await Permission.audio.request();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔒 [SEC-2] Kein Retry-Mechanismus bei Netzwerkfehlern
|
|
||||||
|
|
||||||
**Datei:** `services/download_service.dart` · Zeilen 32–131
|
|
||||||
|
|
||||||
Bei Timeout oder Verbindungsabbruch wird einfach `null` zurückgegeben. Kein Retry, kein exponentielles Backoff.
|
|
||||||
|
|
||||||
**Fix:** 2-3 Retry-Versuche mit steigendem Timeout:
|
|
||||||
```dart
|
|
||||||
for (int versuch = 0; versuch < 3; versuch++) {
|
|
||||||
try {
|
|
||||||
return await _downloadAttempt(url);
|
|
||||||
} catch (e) {
|
|
||||||
if (versuch == 2) rethrow;
|
|
||||||
await Future.delayed(Duration(seconds: 2 * (versuch + 1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔒 [SEC-3] Kein Cancel-Mechanismus beim YouTube-Download
|
|
||||||
|
|
||||||
**Datei:** `services/download_service.dart` · Zeilen 90–108
|
|
||||||
|
|
||||||
Einmal gestartet, kann der Download nicht abgebrochen werden. Der Nutzer muss warten oder die App killen.
|
|
||||||
|
|
||||||
**Fix:** `StreamSubscription` speichern und `cancel()` anbieten:
|
|
||||||
```dart
|
|
||||||
StreamSubscription? _downloadSub;
|
|
||||||
void cancelDownload() => _downloadSub?.cancel();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Missing Features
|
|
||||||
|
|
||||||
### 📌 [FEAT-1] **4 von 5 Bottom-Nav-Tabs sind leer**
|
|
||||||
|
|
||||||
`home_screen.dart` Zeilen 509–515: Navigation hat 5 Einträge, aber keine `_selectedIndex` und keine `IndexedStack` oder `switch`-Logik. Alle Tabs außer "Musik" zeigen denselben Screen.
|
|
||||||
|
|
||||||
**Zu implementieren:**
|
|
||||||
- `Downloads` → Zeige heruntergeladene Songs + Download-Buttons
|
|
||||||
- `Tags` → Tag-Verwaltung (erstellen, löschen, Songs zuweisen)
|
|
||||||
- `Favoriten` → Zeige Favoriten-Playlist
|
|
||||||
- `Einstellungen` → Theme, Cache, Info
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📌 [FEAT-2] **Keine Audio-Service Integration**
|
|
||||||
|
|
||||||
`audio_service: ^0.18.15` ist als Dependency eingetragen, aber wird nirgends importiert oder verwendet. `PlayerService` ist standalone ohne Background-Playback.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📌 [FEAT-3] **`setWarteschlange` ohne Automatische Wiedergabe**
|
|
||||||
|
|
||||||
`player_service.dart` Zeilen 83–87: Die Warteschlange wird gesetzt, aber nach dem letzten Song stoppt die Wiedergabe (kein Loop, kein Shuffle, keine Queue-Weiterverarbeitung).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Zusammenfassung: Priority-TODO-Liste
|
|
||||||
|
|
||||||
### 🔴 MUST FIX (sofort — vor Release)
|
|
||||||
|
|
||||||
| # | Datei | Zeile | Issue |
|
|
||||||
|---|-------|-------|-------|
|
|
||||||
| 1 | `widgets/mini_player.dart` | 20-32 | **Memory Leak:** Streams nie gecancelled |
|
|
||||||
| 2 | `widgets/metadaten_dialog.dart` | 88 | **Null-Crash:** `song.id!` kann crashen |
|
|
||||||
| 3 | `services/musik_scanner.dart` | 38 | **`dauerSekunden: 0`** — alle Songs haben Dauer 0 |
|
|
||||||
| 4 | `database/db_helper.dart` | 126-135 | **DB-Flut:** positionAktualisieren inserted immer neu |
|
|
||||||
| 5 | `main.dart` | 48-62 | **`FlutterErrorBoundary`** tut nichts (entfernen oder fixen) |
|
|
||||||
| 6 | `widgets/mini_player.dart` | 131 | **Kein `dispose()`** — Leak #2 |
|
|
||||||
|
|
||||||
### 🟠 SHOULD FIX (nächster Sprint)
|
|
||||||
|
|
||||||
| # | Datei | Zeile | Issue |
|
|
||||||
|---|-------|-------|-------|
|
|
||||||
| 7 | `services/download_service.dart` | 56,75,105,127 | Fehlermeldungen abgeschnitten (substring) |
|
|
||||||
| 8 | `services/musik_scanner.dart` | 121-126 | `_scanneViaMediaStore()` gibt `[]` zurück |
|
|
||||||
| 9 | `main.dart` | 34-46 | `AppWrapper` totes Gewebe (entfernen) |
|
|
||||||
| 10 | `screens/home_screen.dart` | 382 | Tag-Filter tut nichts |
|
|
||||||
| 11 | `screens/home_screen.dart` | 32-40 | Hardcoded Beispiel-Tags (aus DB laden!) |
|
|
||||||
| 12 | `services/download_service.dart` | 90-108 | Kein Cancel-Mechanismus |
|
|
||||||
| 13 | `services/download_service.dart` | — | Kein Retry bei Netzwerkfehlern |
|
|
||||||
| 14 | `services/musik_scanner.dart` | 19-20 | Permission.storage deprecated (API 33+) |
|
|
||||||
| 15 | `screens/home_screen.dart` | 509-515 | 4/5 Bottom-Nav-Tabs leer |
|
|
||||||
| 16 | `services/favoriten_service.dart` | 45-49 | `favoritenIds()` lädt unnötig alle Songs |
|
|
||||||
|
|
||||||
### 🟡 NICE TO IMPROVE (Code-Qualität)
|
|
||||||
|
|
||||||
| # | Datei | Zeile | Issue |
|
|
||||||
|---|-------|-------|-------|
|
|
||||||
| 17 | `screens/home_screen.dart` | 1-519 | **Monolith** — in Einzeldateien aufteilen |
|
|
||||||
| 18 | `services/player_service.dart` | 16-25 | Redundanter try-catch im `_p` Getter |
|
|
||||||
| 19 | `database/db_helper.dart` | 221-229 | `loeschen()` ohne Transaktion |
|
|
||||||
| 20 | `services/musik_scanner.dart` | 29-65 | Doppelter try-catch Block |
|
|
||||||
| 21 | — | — | **Singleton-Overkill** → Dependency Injection |
|
|
||||||
| 22 | `screens/home_screen.dart` | 183-245 | Download-Dialog im HomeScreen (auslagern) |
|
|
||||||
| 23 | `services/player_service.dart` | 83-87 | setWarteschlange ohne Weiterschaltung |
|
|
||||||
| 24 | — | — | `audio_service` nie verwendet (Background-Playback) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Statistik
|
|
||||||
|
|
||||||
| Metrik | Wert |
|
|
||||||
|--------|------|
|
|
||||||
| **Dateien** | 11 (12 mit pubspec.yaml) |
|
|
||||||
| **Gesamt-LOC (Dart)** | 1.613 |
|
|
||||||
| **Größte Datei** | `home_screen.dart` — 519 Zeilen (32%) |
|
|
||||||
| **Singleton-Klassen** | 5/6 (83%) |
|
|
||||||
| **Kritische Bugs** | **6** |
|
|
||||||
| **Schwere Probleme** | **6** |
|
|
||||||
| **Code-Qualität** | **5** |
|
|
||||||
| **Ponytail-Verletzungen** | **4** (→ extrahieren) |
|
|
||||||
| **Security** | **3** |
|
|
||||||
| **Fehlende Features** | **4** (davon 1 komplett leer) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Fazit:** Die App hat ein solides Grundgerüst, aber leidet unter klassischen "Solo-Dev"-Problemen: Memory Leaks, Null-Safety-Lücken, Singleton-Overkill und ein Monolith-Screen. Der größte Hebel ist die Aufteilung von `home_screen.dart` (→ 3-4 eigene Widgets) und das Fixen der 6 kritischen Bugs. Danach: MediaStore-Integration für echte Song-Dauer und die 4 leeren Tabs befüllen.
|
|
||||||
|
|
||||||
> "Weniger Code, mehr Wirkung" — **Ponytail-Prinzip.** Der MiniPlayer könnte mit dispose-Fix und gekürztem Code locker 30 Zeilen verlieren. Der HomeScreen sollte bei ~250 Zeilen landen.
|
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ class DbHelper {
|
|||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
},
|
},
|
||||||
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
|
// Hier zukünftige DB-Migrationen einpflegen (z.B. if (oldVersion < 2) ...)
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class DownloadService {
|
|||||||
await for (final chunk in fileStream) {
|
await for (final chunk in fileStream) {
|
||||||
sink.add(chunk);
|
sink.add(chunk);
|
||||||
downloaded += chunk.length;
|
downloaded += chunk.length;
|
||||||
_fortschritt = downloaded / total;
|
_fortschritt = total > 0 ? downloaded / total : 0.0;
|
||||||
}
|
}
|
||||||
await sink.flush();
|
await sink.flush();
|
||||||
await sink.close();
|
await sink.close();
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class MusikScanner {
|
|||||||
final gefunden = <Song>[];
|
final gefunden = <Song>[];
|
||||||
final pfade = await _sammleMusikPfade();
|
final pfade = await _sammleMusikPfade();
|
||||||
|
|
||||||
|
final player = AudioPlayer();
|
||||||
|
|
||||||
for (final pfad in pfade) {
|
for (final pfad in pfade) {
|
||||||
try {
|
try {
|
||||||
final file = File(pfad);
|
final file = File(pfad);
|
||||||
@@ -35,7 +37,7 @@ class MusikScanner {
|
|||||||
titel: _dateiNameOhneEndung(pfad),
|
titel: _dateiNameOhneEndung(pfad),
|
||||||
kuenstler: 'Unbekannt',
|
kuenstler: 'Unbekannt',
|
||||||
album: '',
|
album: '',
|
||||||
dauerSekunden: await _ermittleDauer(pfad),
|
dauerSekunden: await _ermittleDauer(player, pfad),
|
||||||
dateiPfad: pfad,
|
dateiPfad: pfad,
|
||||||
coverPfad: null,
|
coverPfad: null,
|
||||||
groesseBytes: stat.size,
|
groesseBytes: stat.size,
|
||||||
@@ -47,6 +49,10 @@ class MusikScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await player.dispose();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
// In DB speichern
|
// In DB speichern
|
||||||
final vorhandene = await _db.alleSongs();
|
final vorhandene = await _db.alleSongs();
|
||||||
final vorhandenePfade = vorhandene.map((s) => s.dateiPfad).toSet();
|
final vorhandenePfade = vorhandene.map((s) => s.dateiPfad).toSet();
|
||||||
@@ -126,12 +132,10 @@ class MusikScanner {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _ermittleDauer(String pfad) async {
|
Future<int> _ermittleDauer(AudioPlayer player, String pfad) async {
|
||||||
try {
|
try {
|
||||||
final player = AudioPlayer();
|
|
||||||
await player.setFilePath(pfad);
|
await player.setFilePath(pfad);
|
||||||
final dauer = player.duration;
|
final dauer = player.duration;
|
||||||
await player.dispose();
|
|
||||||
return dauer?.inSeconds ?? 0;
|
return dauer?.inSeconds ?? 0;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ class MeloHomeViewModel extends ChangeNotifier {
|
|||||||
|
|
||||||
songs = alle;
|
songs = alle;
|
||||||
favoritenIds = await favoriten.favoritenIds();
|
favoritenIds = await favoriten.favoritenIds();
|
||||||
tagCounts = _berechneTagCounts();
|
|
||||||
letzteSongs = await db.letzteWiedergaben();
|
letzteSongs = await db.letzteWiedergaben();
|
||||||
await ladeTags();
|
await ladeTags();
|
||||||
ladt = false;
|
ladt = false;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../services/navidrome_service.dart';
|
import '../services/navidrome_service.dart';
|
||||||
import '../utils/farb_theme.dart';
|
import '../utils/farb_theme.dart';
|
||||||
|
import '../viewmodels/melo_home_viewmodel.dart';
|
||||||
|
|
||||||
/// Navidrome-Browser als Bottom-Sheet.
|
/// Navidrome-Browser als Bottom-Sheet.
|
||||||
/// Manuell in home_screen.dart einbaubar.
|
/// Manuell in home_screen.dart einbaubar.
|
||||||
class NavidromeBrowser extends StatefulWidget {
|
class NavidromeBrowser extends StatefulWidget {
|
||||||
final dynamic vm;
|
final MeloHomeViewModel vm;
|
||||||
const NavidromeBrowser({super.key, required this.vm});
|
const NavidromeBrowser({super.key, required this.vm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import '../models/song.dart';
|
import '../models/song.dart';
|
||||||
import '../models/playlist.dart';
|
import '../models/playlist.dart';
|
||||||
import '../utils/farb_theme.dart';
|
import '../utils/farb_theme.dart';
|
||||||
|
import '../viewmodels/melo_home_viewmodel.dart';
|
||||||
|
|
||||||
/// Bottom-Sheet zum Durchstöbern von Playlists.
|
/// Bottom-Sheet zum Durchstöbern von Playlists.
|
||||||
class PlaylistSheet extends StatefulWidget {
|
class PlaylistSheet extends StatefulWidget {
|
||||||
final dynamic vm;
|
final MeloHomeViewModel vm;
|
||||||
const PlaylistSheet({super.key, required this.vm});
|
const PlaylistSheet({super.key, required this.vm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -174,7 +175,7 @@ class _PlaylistSheetState extends State<PlaylistSheet> {
|
|||||||
class _PlaylistDetail extends StatelessWidget {
|
class _PlaylistDetail extends StatelessWidget {
|
||||||
final Playlist p;
|
final Playlist p;
|
||||||
final List<Song> songs;
|
final List<Song> songs;
|
||||||
final dynamic vm;
|
final MeloHomeViewModel vm;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
|
|
||||||
const _PlaylistDetail({
|
const _PlaylistDetail({
|
||||||
|
|||||||
Reference in New Issue
Block a user