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:
co-authored by
Claude Sonnet 5
parent
9e65ddcbb5
commit
f0313724d4
@@ -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})';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user