- 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
141 lines
4.0 KiB
Dart
141 lines
4.0 KiB
Dart
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})';
|
|
}
|
|
}
|