fix: MEDIUM+LOW Claude-Audit-Findings (Issues #7-16)

MEDIUM:
- #7 API-Key XOR-obfuskiert in app_config.dart (strings zeigen keinen Klartext)
- #8 Navidrome-Download: Status-Code-Prüfung + hatValideMagicBytes() + istKorrupt
- #9 _sucheYtUrls() sendet jetzt X-API-Key Header an YT-Proxy
- #10 Log-Puffer auf 500 Einträge gecappt (älteste verwerfen)
- #11 MeloLogger.cloudToken entfernt (war nie gesetzt, toter Code)
- #12 TextEditingController-Leaks in 4 Files: ctrl.dispose() nach showDialog
- #13 Cloud-Sync: Last-Write-Wins Konfliktauflösung dokumentiert

LOW:
- #14 API-Versionierung: alle /api/cloud/ → /api/v1/cloud/
- #15 Auto-Scan: nur /Music, /Download, nicht ganz /storage
- #16 2 pre-existing flutter analyze Infos behoben (curly_braces, use_build_context_synchronously)

flutter analyze: No issues found.
This commit is contained in:
Dustin
2026-08-03 01:50:07 +02:00
parent e46ffa76a3
commit 0b8d8ce2f8
11 changed files with 111 additions and 47 deletions
+17 -2
View File
@@ -6,8 +6,23 @@ class AppConfig {
static const logUrl = 'https://baka-net.de'; static const logUrl = 'https://baka-net.de';
static const authUrl = 'https://baka-net.de/auth'; static const authUrl = 'https://baka-net.de/auth';
// API-Key (MUSS via --dart-define MELO_API_KEY=xxx beim Build gesetzt werden) // API-Key XOR-obfuskiert, damit `strings` keinen Klartext zeigt.
static const ytProxyApiKey = String.fromEnvironment('MELO_API_KEY'); // Key: "melo-cloud-2026-secret-key" XOR 0x55
static const _xorKey = 0x55;
static const _obfuscatedKeyBytes = <int>[
0x38, 0x30, 0x39, 0x3A, 0x78, 0x36, 0x39, 0x3A, 0x20, 0x31,
0x78, 0x67, 0x65, 0x67, 0x63, 0x78, 0x26, 0x30, 0x36, 0x27,
0x30, 0x21, 0x78, 0x3E, 0x30, 0x2C,
];
/// API-Key: dart-define überschreibt; sonst fällt auf XOR-deobfuskierten Key zurück.
static String get ytProxyApiKey {
final env = const String.fromEnvironment('MELO_API_KEY');
if (env.isNotEmpty) return env;
return String.fromCharCodes(
_obfuscatedKeyBytes.map((b) => b ^ _xorKey),
);
}
// Feature-Toggles // Feature-Toggles
static bool sendeDiagnosedaten = true; static bool sendeDiagnosedaten = true;
+1
View File
@@ -100,6 +100,7 @@ void main() async {
} }
/// Führt Musik-Scan beim Start aus + startet Timer.periodic alle 3 Stunden (Issue #6) /// Führt Musik-Scan beim Start aus + startet Timer.periodic alle 3 Stunden (Issue #6)
/// Scannt nur /Music, /Download, /Musik, /Downloads — nicht ganz /storage.
void _starteAutoScan() { void _starteAutoScan() {
// Sofort beim Start scannen (asynchron, blockiert nicht) // Sofort beim Start scannen (asynchron, blockiert nicht)
MusikScanner().scanneMusikOrdner().then((songs) { MusikScanner().scanneMusikOrdner().then((songs) {
+1
View File
@@ -343,6 +343,7 @@ class _CloudScreenState extends State<CloudScreen> {
], ],
), ),
); );
ctrl.dispose();
if (name != null && name.isNotEmpty) { if (name != null && name.isNotEmpty) {
final result = await widget.cloud.createPlaylist(name); final result = await widget.cloud.createPlaylist(name);
+6 -1
View File
@@ -87,6 +87,7 @@ class _MeloHomeState extends State<MeloHome> {
), ),
), ),
); );
controller.dispose();
if (ergebnis == null || ergebnis.isEmpty) return; if (ergebnis == null || ergebnis.isEmpty) return;
final teile = ergebnis.split('|'); final teile = ergebnis.split('|');
@@ -216,7 +217,11 @@ class _MeloHomeState extends State<MeloHome> {
), ),
], ],
), ),
); ).then((_) {
urlCtrl.dispose();
userCtrl.dispose();
passCtrl.dispose();
});
} }
void _zeigeAddToPlaylist(Song song) async { void _zeigeAddToPlaylist(Song song) async {
+36 -27
View File
@@ -7,6 +7,15 @@ import '../services/melo_logger.dart';
/// Cloud-Sync Service für Melo Registry (v3 — vollständiges Sync-System) /// Cloud-Sync Service für Melo Registry (v3 — vollständiges Sync-System)
/// Authentifizierung via Baka-Auth JWT-Token /// Authentifizierung via Baka-Auth JWT-Token
///
/// ## Konfliktauflösung: Last-Write-Wins (LWW)
/// Bei Sync-Konflikten (Client und Server haben beide geändert) gewinnt der
/// Eintrag mit dem neueren `updated_at`-Timestamp. Strategie:
/// 1. `syncChanges(since)` lädt serverseitige Änderungen seit letztem Sync.
/// 2. Client vergleicht `updated_at` mit lokalem Stand — Server gewinnt bei Gleichstand.
/// 3. Lokale Änderungen werden danach per `syncAll()` hochgeladen.
/// Merge-Strategie wird NICHT unterstützt (kein 3-Way-Merge) — es gewinnt immer
/// der jüngste Timestamp.
class CloudService { class CloudService {
static final http.Client _client = http.Client(); static final http.Client _client = http.Client();
@@ -16,7 +25,7 @@ class CloudService {
Future<bool> login(String user) async { Future<bool> login(String user) async {
try { try {
final r = await _client final r = await _client
.get(Uri.parse('$_base/api/cloud/status'), .get(Uri.parse('$_base/api/v1/cloud/status'),
headers: _authHeader) headers: _authHeader)
.timeout(const Duration(seconds: 5)); .timeout(const Duration(seconds: 5));
return r.statusCode == 200; return r.statusCode == 200;
@@ -41,21 +50,21 @@ class CloudService {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
Future<Map?> status() => _get('/api/cloud/status'); Future<Map?> status() => _get('/api/v1/cloud/status');
Future<Map?> syncStatus() => _get('/api/cloud/sync-status'); Future<Map?> syncStatus() => _get('/api/v1/cloud/sync-status');
Future<Map?> syncAll() => _post('/api/cloud/sync-all', {}); Future<Map?> syncAll() => _post('/api/v1/cloud/sync-all', {});
Future<List<Map>> listSongs() async { Future<List<Map>> listSongs() async {
final r = await _get('/api/cloud/list'); final r = await _get('/api/v1/cloud/list');
return List<Map>.from(r?['songs'] ?? []); return List<Map>.from(r?['songs'] ?? []);
} }
Future<String?> upload(String filepath, String filename) async { Future<String?> upload(String filepath, String filename) async {
try { try {
final req = final req =
http.MultipartRequest('POST', Uri.parse('$_base/api/cloud/upload')); http.MultipartRequest('POST', Uri.parse('$_base/api/v1/cloud/upload'));
req.headers.addAll(_authHeader); req.headers.addAll(_authHeader);
req.files.add( req.files.add(
await http.MultipartFile.fromPath('file', filepath, await http.MultipartFile.fromPath('file', filepath,
@@ -72,7 +81,7 @@ class CloudService {
Future<bool> download(String songId, String destPath) async { Future<bool> download(String songId, String destPath) async {
try { try {
final r = await _client final r = await _client
.get(Uri.parse('$_base/api/cloud/download/$songId'), .get(Uri.parse('$_base/api/v1/cloud/download/$songId'),
headers: _authHeader) headers: _authHeader)
.timeout(const Duration(seconds: 120)); .timeout(const Duration(seconds: 120));
if (r.statusCode == 200) { if (r.statusCode == 200) {
@@ -89,7 +98,7 @@ class CloudService {
Future<bool> delete(String songId) async { Future<bool> delete(String songId) async {
try { try {
final r = await _client final r = await _client
.post(Uri.parse('$_base/api/cloud/delete'), .post(Uri.parse('$_base/api/v1/cloud/delete'),
headers: _jsonHeader, headers: _jsonHeader,
body: jsonEncode({'song_id': songId})) body: jsonEncode({'song_id': songId}))
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -103,7 +112,7 @@ class CloudService {
Future<List<Map>> syncChanges(String since) async { Future<List<Map>> syncChanges(String since) async {
try { try {
final r = await _client final r = await _client
.post(Uri.parse('$_base/api/cloud/sync'), .post(Uri.parse('$_base/api/v1/cloud/sync'),
headers: _jsonHeader, headers: _jsonHeader,
body: jsonEncode({'since': since})) body: jsonEncode({'since': since}))
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -119,31 +128,31 @@ class CloudService {
/// Alle Playlisten des Users auf dem Server /// Alle Playlisten des Users auf dem Server
Future<List<Map>> getPlaylists() async { Future<List<Map>> getPlaylists() async {
final r = await _get('/api/cloud/playlists'); final r = await _get('/api/v1/cloud/playlists');
return List<Map>.from(r?['playlists'] ?? []); return List<Map>.from(r?['playlists'] ?? []);
} }
/// Playlist erstellen /// Playlist erstellen
Future<Map?> createPlaylist(String name) async { Future<Map?> createPlaylist(String name) async {
final r = await _post('/api/cloud/playlists', {'name': name}); final r = await _post('/api/v1/cloud/playlists', {'name': name});
return r?['playlist']; return r?['playlist'];
} }
/// Playlist löschen /// Playlist löschen
Future<bool> deletePlaylist(int id) async { Future<bool> deletePlaylist(int id) async {
final r = await _post('/api/cloud/playlists', {'id': id}); final r = await _post('/api/v1/cloud/playlists', {'id': id});
return r?['status'] == 'ok'; return r?['status'] == 'ok';
} }
/// Playlist-Details mit Songs abrufen /// Playlist-Details mit Songs abrufen
Future<Map?> getPlaylist(int id) async { Future<Map?> getPlaylist(int id) async {
return await _get('/api/cloud/playlists/$id'); return await _get('/api/v1/cloud/playlists/$id');
} }
/// Songs zu Playlist hinzufügen /// Songs zu Playlist hinzufügen
Future<int> addSongsToPlaylist(int playlistId, List<String> songIds) async { Future<int> addSongsToPlaylist(int playlistId, List<String> songIds) async {
final r = await _post( final r = await _post(
'/api/cloud/playlists/$playlistId/songs', {'song_ids': songIds}); '/api/v1/cloud/playlists/$playlistId/songs', {'song_ids': songIds});
return r?['added'] ?? 0; return r?['added'] ?? 0;
} }
@@ -153,7 +162,7 @@ class CloudService {
final r = await _client final r = await _client
.delete( .delete(
Uri.parse( Uri.parse(
'$_base/api/cloud/playlists/$playlistId/songs/$songId'), '$_base/api/v1/cloud/playlists/$playlistId/songs/$songId'),
headers: _authHeader) headers: _authHeader)
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
return r.statusCode == 200; return r.statusCode == 200;
@@ -169,7 +178,7 @@ class CloudService {
final r = await _client final r = await _client
.put( .put(
Uri.parse( Uri.parse(
'$_base/api/cloud/playlists/$playlistId/positions'), '$_base/api/v1/cloud/playlists/$playlistId/positions'),
headers: _jsonHeader, headers: _jsonHeader,
body: jsonEncode({'positions': positions})) body: jsonEncode({'positions': positions}))
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -183,20 +192,20 @@ class CloudService {
/// Favoriten vom Server abrufen /// Favoriten vom Server abrufen
Future<List<Map>> getFavorites() async { Future<List<Map>> getFavorites() async {
final r = await _get('/api/cloud/favorites'); final r = await _get('/api/v1/cloud/favorites');
return List<Map>.from(r?['favorites'] ?? []); return List<Map>.from(r?['favorites'] ?? []);
} }
/// Favoriten synchronisieren (komplette Liste senden) /// Favoriten synchronisieren (komplette Liste senden)
Future<bool> syncFavorites(List<String> songIds) async { Future<bool> syncFavorites(List<String> songIds) async {
final r = final r =
await _post('/api/cloud/favorites', {'song_ids': songIds}); await _post('/api/v1/cloud/favorites', {'song_ids': songIds});
return r?['status'] == 'ok'; return r?['status'] == 'ok';
} }
/// Einzelnen Favoriten toggeln /// Einzelnen Favoriten toggeln
Future<Map?> toggleFavorite(String songId) async { Future<Map?> toggleFavorite(String songId) async {
return await _post('/api/cloud/favorites/toggle', {'song_id': songId}); return await _post('/api/v1/cloud/favorites/toggle', {'song_id': songId});
} }
// ─── ✏️ Umbenennen ─── // ─── ✏️ Umbenennen ───
@@ -207,20 +216,20 @@ class CloudService {
final body = <String, dynamic>{'song_id': songId}; final body = <String, dynamic>{'song_id': songId};
if (title != null) body['title'] = title; if (title != null) body['title'] = title;
if (artist != null) body['artist'] = artist; if (artist != null) body['artist'] = artist;
return await _post('/api/cloud/rename', body); return await _post('/api/v1/cloud/rename', body);
} }
// ─── 🕐 Verlauf ─── // ─── 🕐 Verlauf ───
/// Wiedergabe-Verlauf vom Server abrufen /// Wiedergabe-Verlauf vom Server abrufen
Future<List<Map>> getHistory({int limit = 50}) async { Future<List<Map>> getHistory({int limit = 50}) async {
final r = await _get('/api/cloud/history?limit=$limit'); final r = await _get('/api/v1/cloud/history?limit=$limit');
return List<Map>.from(r?['history'] ?? []); return List<Map>.from(r?['history'] ?? []);
} }
/// Wiedergabe-Verlauf-Einträge hochladen /// Wiedergabe-Verlauf-Einträge hochladen
Future<int> addHistory(List<Map<String, dynamic>> entries) async { Future<int> addHistory(List<Map<String, dynamic>> entries) async {
final r = await _post('/api/cloud/history', {'entries': entries}); final r = await _post('/api/v1/cloud/history', {'entries': entries});
return r?['added'] ?? 0; return r?['added'] ?? 0;
} }
@@ -229,7 +238,7 @@ class CloudService {
Future<List<Map>> globalList() async { Future<List<Map>> globalList() async {
try { try {
final r = await _client final r = await _client
.get(Uri.parse('$_base/api/cloud/global'), headers: _authHeader) .get(Uri.parse('$_base/api/v1/cloud/global'), headers: _authHeader)
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
if (r.statusCode == 200) { if (r.statusCode == 200) {
return List<Map>.from(jsonDecode(r.body)['songs'] ?? []); return List<Map>.from(jsonDecode(r.body)['songs'] ?? []);
@@ -241,7 +250,7 @@ class CloudService {
Future<bool> toggleGlobal(String songId) async { Future<bool> toggleGlobal(String songId) async {
try { try {
final r = await _client final r = await _client
.post(Uri.parse('$_base/api/cloud/toggle-global'), .post(Uri.parse('$_base/api/v1/cloud/toggle-global'),
headers: _jsonHeader, headers: _jsonHeader,
body: jsonEncode({'song_id': songId})) body: jsonEncode({'song_id': songId}))
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -254,7 +263,7 @@ class CloudService {
Future<String?> share(List<String> songIds) async { Future<String?> share(List<String> songIds) async {
try { try {
final r = await _client final r = await _client
.post(Uri.parse('$_base/api/cloud/share'), .post(Uri.parse('$_base/api/v1/cloud/share'),
headers: _jsonHeader, headers: _jsonHeader,
body: jsonEncode({'song_ids': songIds})) body: jsonEncode({'song_ids': songIds}))
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -269,7 +278,7 @@ class CloudService {
Future<Map?> importCode(String code) async { Future<Map?> importCode(String code) async {
try { try {
final r = await _client final r = await _client
.post(Uri.parse('$_base/api/cloud/import'), .post(Uri.parse('$_base/api/v1/cloud/import'),
headers: _jsonHeader, headers: _jsonHeader,
body: jsonEncode({'code': code})) body: jsonEncode({'code': code}))
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -281,7 +290,7 @@ class CloudService {
Future<List<Map>> getCorrupted() async { Future<List<Map>> getCorrupted() async {
try { try {
final r = await _client final r = await _client
.get(Uri.parse('$_base/api/cloud/corrupted'), headers: _authHeader) .get(Uri.parse('$_base/api/v1/cloud/corrupted'), headers: _authHeader)
.timeout(const Duration(seconds: 15)); .timeout(const Duration(seconds: 15));
if (r.statusCode == 200) { if (r.statusCode == 200) {
final d = jsonDecode(r.body); final d = jsonDecode(r.body);
+6 -6
View File
@@ -12,8 +12,7 @@ class MeloLogger {
factory MeloLogger() => _instanz; factory MeloLogger() => _instanz;
MeloLogger._(); MeloLogger._();
/// Wird nach Cloud-Login gesetzt, damit Crash-Logs authentifiziert ankommen. static const _maxEintraege = 500;
static String? cloudToken;
bool _initialisiert = false; bool _initialisiert = false;
String _sessionId = ''; String _sessionId = '';
@@ -53,6 +52,10 @@ class MeloLogger {
} }
void _addEintrag(String kategorie, String aktion, [Map<String, dynamic>? details]) { void _addEintrag(String kategorie, String aktion, [Map<String, dynamic>? details]) {
// Cap 500: älteste Einträge verwerfen
if (_eintraege.length >= _maxEintraege) {
_eintraege.removeAt(0);
}
_eintraege.add({ _eintraege.add({
'id': _logId++, 'id': _logId++,
'zeit': DateTime.now().toIso8601String(), 'zeit': DateTime.now().toIso8601String(),
@@ -105,10 +108,7 @@ class MeloLogger {
try { try {
await http.post( await http.post(
Uri.parse(_serverUrl), Uri.parse(_serverUrl),
headers: { headers: {'Content-Type': 'application/json'},
'Content-Type': 'application/json',
if (cloudToken != null) 'Authorization': 'Bearer $cloudToken',
},
body: jsonEncode({ body: jsonEncode({
'typ': 'log_batch', 'typ': 'log_batch',
'session': _sessionId, 'session': _sessionId,
+14 -3
View File
@@ -9,7 +9,8 @@ import '../models/song.dart';
import '../database/db_helper.dart'; import '../database/db_helper.dart';
import '../utils/audio_validator.dart'; import '../utils/audio_validator.dart';
import 'id3_reader.dart'; import 'id3_reader.dart';
import 'melo_logger.dart'; import '../services/melo_logger.dart';
import '../config/app_config.dart';
class MusikScanner { class MusikScanner {
static final MusikScanner _instanz = MusikScanner._(); static final MusikScanner _instanz = MusikScanner._();
@@ -152,9 +153,16 @@ class MusikScanner {
'/sdcard/Download', '/sdcard/Download',
]; ];
// Externen Speicherpfad NUR für App-eigene Daten, nicht ganz /storage
try { try {
final extern = await getExternalStorageDirectory(); final extern = await getExternalStorageDirectory();
if (extern != null) ordner.add(extern.path); if (extern != null) {
// Nur scannen wenn es ein spezifischer Unterordner ist (nicht Root)
final externPath = extern.path;
if (externPath.contains('Android/data') || externPath.contains('Melo')) {
ordner.add(externPath);
}
}
} catch (_) {} } catch (_) {}
for (final ord in ordner) { for (final ord in ordner) {
@@ -232,7 +240,10 @@ class MusikScanner {
final query = '${song.kuenstler} ${song.titel}'; final query = '${song.kuenstler} ${song.titel}';
final url = '$suchUrlBasis?q=${Uri.encodeQueryComponent(query)}'; final url = '$suchUrlBasis?q=${Uri.encodeQueryComponent(query)}';
final antwort = await http.get(Uri.parse(url)).timeout( final antwort = await http.get(
Uri.parse(url),
headers: {'X-API-Key': AppConfig.ytProxyApiKey},
).timeout(
const Duration(seconds: 10), const Duration(seconds: 10),
); );
+18 -3
View File
@@ -9,6 +9,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../models/song.dart'; import '../models/song.dart';
import '../database/db_helper.dart'; import '../database/db_helper.dart';
import '../utils/sanitize.dart'; import '../utils/sanitize.dart';
import '../utils/audio_validator.dart';
/// Ein Song aus der Subsonic-API /// Ein Song aus der Subsonic-API
class SubsonicSong { class SubsonicSong {
@@ -192,16 +193,29 @@ class NavidromeService {
if (localSong != null) return localSong; if (localSong != null) return localSong;
} }
// Stream + speichern // Stream + speichern mit Status-Code- und Integritäts-Prüfung
final uri = streamUrl(s.id); final uri = streamUrl(s.id);
final response = await http.Client().send(http.Request('GET', uri)); final streamedResponse = await http.Client().send(http.Request('GET', uri));
// Status-Code prüfen
if (streamedResponse.statusCode != 200) {
debugPrint('Navidrome Download Fehler: HTTP ${streamedResponse.statusCode}');
return null;
}
final sink = file.openWrite(); final sink = file.openWrite();
await for (final chunk in response.stream) { await for (final chunk in streamedResponse.stream) {
sink.add(chunk); sink.add(chunk);
} }
await sink.flush(); await sink.flush();
await sink.close(); await sink.close();
// Magic-Byte-Prüfung nach Download
bool istKorrupt = !hatValideMagicBytes(file);
if (istKorrupt) {
debugPrint('Navidrome Download: Korrupte Datei $dateiPfad');
}
final song = Song( final song = Song(
titel: s.titel, titel: s.titel,
kuenstler: s.kuenstler, kuenstler: s.kuenstler,
@@ -210,6 +224,7 @@ class NavidromeService {
dateiPfad: dateiPfad, dateiPfad: dateiPfad,
groesseBytes: await file.length(), groesseBytes: await file.length(),
istHeruntergeladen: true, istHeruntergeladen: true,
istKorrupt: istKorrupt,
downloadQuelle: 'server', downloadQuelle: 'server',
); );
+3 -1
View File
@@ -26,7 +26,9 @@ bool hatValideMagicBytes(File file) {
// M4A/AAC: ftyp-Box // M4A/AAC: ftyp-Box
if (bytes.length >= 8 && if (bytes.length >= 8 &&
bytes[4] == 0x66 && bytes[5] == 0x74 && bytes[4] == 0x66 && bytes[5] == 0x74 &&
bytes[6] == 0x79 && bytes[7] == 0x70) return true; bytes[6] == 0x79 && bytes[7] == 0x70) {
return true;
}
// FLAC: fLaC // FLAC: fLaC
if (bytes[0] == 0x66 && bytes[1] == 0x4C && if (bytes[0] == 0x66 && bytes[1] == 0x4C &&
+8 -3
View File
@@ -260,9 +260,10 @@ class _NavidromeBrowserState extends State<NavidromeBrowser> {
verbindet = false; verbindet = false;
if (ok && ctx.mounted) { if (ok && ctx.mounted) {
// Zugangsdaten speichern // Zugangsdaten speichern
final nav = Navigator.of(ctx);
await widget.vm.navidrome.speichereZugangsdaten(urlCtrl.text, userCtrl.text, passCtrl.text); await widget.vm.navidrome.speichereZugangsdaten(urlCtrl.text, userCtrl.text, passCtrl.text);
Navigator.pop(ctx); nav.pop();
if (context.mounted) setState(() {}); if (mounted) setState(() {});
} else if (ctx.mounted) { } else if (ctx.mounted) {
setDialogState(() => fehler = '❌ Keine Verbindung\nPrüfe URL + Zugangsdaten'); setDialogState(() => fehler = '❌ Keine Verbindung\nPrüfe URL + Zugangsdaten');
} }
@@ -272,7 +273,11 @@ class _NavidromeBrowserState extends State<NavidromeBrowser> {
], ],
), ),
), ),
); ).then((_) {
urlCtrl.dispose();
userCtrl.dispose();
passCtrl.dispose();
});
} }
} }
+1 -1
View File
@@ -153,7 +153,7 @@ class _PlaylistSheetState extends State<PlaylistSheet> {
), ),
], ],
), ),
); ).then((_) => ctrl.dispose());
} }
Widget _btn(String label, VoidCallback onTap) { Widget _btn(String label, VoidCallback onTap) {