Equalizer-Fix für Xiaomi/MIUI (Dolby Atmos) + In-App-YouTube-Suche als Extra-Tab

- openSystemPanel(): Xiaomi-Sound-Settings-Intent zuerst, dann
  Settings.ACTION_SOUND_SETTINGS, dann Android-Standard-Intent als
  Fallback. Behebt "kein System-Klangeffekte" trotz Dolby Atmos auf
  MIUI/HyperOS (POCO X7 Pro), wo der Standard-Intent keine Activity findet.
- Neuer Tab "YT-Suche": YouTube-Suche über den bestehenden
  /api/search-Endpunkt des Baka-Proxys, Ergebnisliste mit Thumbnail
  (YouTube-CDN), Titel, Dauer und Download-Knopf pro Treffer — derselbe
  Download-Weg wie im bestehenden Download-Tab.

574 Tests grün, flutter analyze ohne Befund, Kotlin kompiliert sauber
(gradlew :app:compileDebugKotlin). Xiaomi-Fix konnte nicht auf echter
Hardware getestet werden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM2JK5mV7AL1g2Rt6H6h9w
This commit is contained in:
Hermes (Server)
2026-08-25 21:03:46 +02:00
co-authored by Claude Sonnet 5
parent 9e65ddcbb5
commit f0313724d4
9 changed files with 796 additions and 7 deletions
+140
View File
@@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'baka_auth.dart';
import 'yt_download_service.dart';
/// Ein Treffer der YouTube-Suche über den Baka-Proxy.
class YtSuchTreffer {
const YtSuchTreffer(
{required this.title, required this.url, required this.duration});
final String title;
final String url;
final Duration duration;
/// Vorschaubild über YouTubes öffentliches Thumbnail-CDN — braucht keinen
/// eigenen API-Zugriff, nur die Video-ID aus [url].
String? get thumbnailUrl {
final id = youtubeVideoId(url);
return id == null ? null : 'https://i.ytimg.com/vi/$id/mqdefault.jpg';
}
}
/// Video-ID aus einer YouTube-URL — deckt `watch?v=`, `youtu.be/` und
/// `/shorts/` ab. `null`, wenn sich keine ID erkennen lässt.
String? youtubeVideoId(String url) {
final uri = Uri.tryParse(url);
if (uri == null) return null;
final ausQuery = uri.queryParameters['v'];
if (ausQuery != null && ausQuery.isNotEmpty) return ausQuery;
final segmente = uri.pathSegments;
if (segmente.isEmpty) return null;
if (uri.host.contains('youtu.be')) return segmente.first;
final shortsIndex = segmente.indexOf('shorts');
if (shortsIndex != -1 && shortsIndex + 1 < segmente.length) {
return segmente[shortsIndex + 1];
}
return null;
}
/// Durchsucht YouTube über den Baka-Proxy (`/api/search`, yt-dlp-gestützt) —
/// dieselbe Anmeldung wie [YtDownloadService].
class YtSearchService extends ChangeNotifier {
YtSearchService({required this.auth, http.Client? client})
: _client = client ?? http.Client();
static const proxyUrl = YtDownloadService.proxyUrl;
final BakaAuth auth;
final http.Client _client;
bool _laeuft = false;
String? _fehler;
List<YtSuchTreffer> _treffer = const [];
bool get laeuft => _laeuft;
String? get fehler => _fehler;
List<YtSuchTreffer> get treffer => _treffer;
Future<void> suchen(String query) async {
final q = query.trim();
if (q.isEmpty) return;
if (!auth.istAngemeldet) {
_treffer = const [];
_fehler = 'Bitte zuerst beim Baka-Konto anmelden';
notifyListeners();
return;
}
_laeuft = true;
_fehler = null;
notifyListeners();
final uri = Uri.parse('$proxyUrl/api/search')
.replace(queryParameters: {'q': q});
final http.Response antwort;
try {
antwort = await _client
.get(uri, headers: auth.authHeader)
.timeout(const Duration(seconds: 20));
} on TimeoutException {
_scheitere('Die Suche braucht zu lange — bitte später erneut versuchen');
return;
} catch (e) {
debugPrint('YT-Suche nicht erreichbar: $e');
_scheitere('Proxy nicht erreichbar');
return;
}
if (antwort.statusCode == 401) {
_scheitere('Anmeldung abgelaufen — bitte neu anmelden');
return;
}
if (antwort.statusCode != 200) {
_scheitere(_fehlerText(antwort));
return;
}
try {
final roh = jsonDecode(antwort.body) as List;
_treffer = [
for (final eintrag in roh.cast<Map<String, dynamic>>())
YtSuchTreffer(
title: eintrag['title'] as String? ?? '',
url: eintrag['url'] as String? ?? '',
duration:
Duration(seconds: (eintrag['duration'] as num?)?.toInt() ?? 0),
),
];
} catch (e) {
_scheitere('Antwort des Servers nicht lesbar');
return;
}
_laeuft = false;
notifyListeners();
}
void _scheitere(String text) {
_fehler = text;
_treffer = const [];
_laeuft = false;
notifyListeners();
}
String _fehlerText(http.Response antwort) {
try {
final j = jsonDecode(antwort.body) as Map<String, dynamic>;
final text = j['error'] as String?;
if (text != null && text.isNotEmpty) return text;
} catch (_) {
// Kein JSON — dann eben der Statuscode.
}
return 'Proxy-Fehler (${antwort.statusCode})';
}
}