Light theme default + yt-dlp search fallback

This commit is contained in:
iliasmouhcine
2026-08-22 13:02:32 +01:00
commit 7704cae4a0
282 changed files with 52324 additions and 0 deletions
@@ -0,0 +1,244 @@
import '../models/video_item.dart';
class PreferredLanguageOption {
const PreferredLanguageOption({
required this.code,
required this.label,
});
final String code;
final String label;
}
class PreferredLanguageService {
PreferredLanguageService._();
static const List<PreferredLanguageOption> options =
<PreferredLanguageOption>[
PreferredLanguageOption(code: 'auto', label: 'Auto'),
PreferredLanguageOption(code: 'fr', label: 'Français (fr)'),
PreferredLanguageOption(code: 'en', label: 'English (en)'),
PreferredLanguageOption(code: 'ar', label: 'العربية (ar)'),
PreferredLanguageOption(code: 'de', label: 'Deutsch (de)'),
PreferredLanguageOption(code: 'es', label: 'Español (es)'),
];
static bool isSupported(String code) {
return options.any((option) => option.code == code);
}
static String labelFor(String code) {
for (final option in options) {
if (option.code == code) {
return option.label;
}
}
return 'Auto';
}
static String nativeSearchHint(String code) {
return switch (code) {
'fr' => 'français',
'en' => 'English',
'ar' => 'عربي',
'de' => 'Deutsch',
'es' => 'español',
_ => '',
};
}
static String dubbingSearchHint(String code) {
return switch (code) {
'fr' => 'VF doublage français version française',
'en' => 'English dub dubbed',
'ar' => 'مدبلج عربي دبلجة عربية',
'de' => 'deutsche Synchronisation Deutsch',
'es' => 'doblaje español versión española',
_ => '',
};
}
/// Returns a positive local boost. This is deliberately a preference,
/// never a hard filter: videos in other languages remain eligible.
static int scoreVideo(VideoItem video, String preferredLanguage) {
if (preferredLanguage == 'auto') {
return 0;
}
final raw = '${video.title} ${video.description} ${video.author}';
final text = _normalize(raw);
var score = 0;
if (_hasDubbingMarker(text, preferredLanguage)) {
score += 18;
score += _popularityBonus(video.viewCount);
}
if (_looksLikeLanguage(text, preferredLanguage)) {
score += 12;
}
return score.clamp(0, 32).toInt();
}
static bool isLikelyDubbed(VideoItem video, String preferredLanguage) {
if (preferredLanguage == 'auto') {
return false;
}
final text = _normalize(
'${video.title} ${video.description} ${video.author}',
);
return _hasDubbingMarker(text, preferredLanguage);
}
static List<VideoItem> rankVideos(
List<VideoItem> videos,
String preferredLanguage,
) {
if (preferredLanguage == 'auto' || videos.length < 2) {
return videos;
}
final indexed = videos.asMap().entries.toList(growable: false);
final sorted = [...indexed]
..sort((a, b) {
final aScore = scoreVideo(a.value, preferredLanguage);
final bScore = scoreVideo(b.value, preferredLanguage);
final scoreCompare = bScore.compareTo(aScore);
if (scoreCompare != 0) {
return scoreCompare;
}
return a.key.compareTo(b.key);
});
return sorted.map((entry) => entry.value).toList(growable: false);
}
static int _popularityBonus(int? viewCount) {
if (viewCount == null || viewCount <= 0) {
return 0;
}
if (viewCount >= 10000000) {
return 8;
}
if (viewCount >= 1000000) {
return 6;
}
if (viewCount >= 100000) {
return 4;
}
if (viewCount >= 10000) {
return 2;
}
return 1;
}
static bool _hasDubbingMarker(String text, String code) {
final markers = switch (code) {
'fr' => const <String>[
' vf ',
'[vf]',
'(vf)',
'version francaise',
'version française',
'double en francais',
'doublé en français',
'doublage francais',
'doublage français',
'french dub',
'french dubbed',
'audio francais',
'audio français',
],
'en' => const <String>[
'english dub',
'english dubbed',
'dubbed in english',
'english audio',
],
'ar' => const <String>[
'مدبلج عربي',
'مدبلجة عربية',
'دبلجة عربية',
'باللغة العربية',
],
'de' => const <String>[
'deutsche synchronisation',
'deutsch synchronisiert',
'deutsche synchro',
'german dub',
],
'es' => const <String>[
'doblaje espanol',
'doblaje español',
'version espanola',
'versión española',
'spanish dub',
'audio espanol',
'audio español',
],
_ => const <String>[],
};
return markers.any(text.contains);
}
static bool _looksLikeLanguage(String text, String code) {
if (code == 'ar') {
final arabic = RegExp(r'[\u0600-\u06FF]');
final matches = arabic.allMatches(text).length;
return matches >= 5;
}
final words = text
.split(RegExp(r'\s+'))
.where((word) => word.length >= 2)
.toList(growable: false);
final markers = switch (code) {
'fr' => const <String>{
'le', 'la', 'les', 'un', 'une', 'des', 'du', 'de', 'dans', 'avec',
'pour', 'sur', 'est', 'sont', 'comment', 'pourquoi', 'nouveau',
'nouvelle', 'francais', 'français', 'voici', 'tout', 'tous',
},
'en' => const <String>{
'the', 'and', 'with', 'for', 'from', 'this', 'that', 'how', 'why',
'what', 'new', 'best', 'review', 'explained', 'english',
},
'de' => const <String>{
'der', 'die', 'das', 'und', 'mit', 'für', 'fur', 'von', 'ist', 'wie',
'warum', 'neu', 'neue', 'deutsch', 'erklart', 'erklärt',
},
'es' => const <String>{
'el', 'la', 'los', 'las', 'un', 'una', 'con', 'para', 'por', 'como',
'cómo', 'nuevo', 'nueva', 'espanol', 'español', 'explicado',
},
_ => const <String>{},
};
var hits = 0;
for (final word in words) {
if (markers.contains(word)) {
hits++;
}
}
return hits >= 2;
}
static String _normalize(String value) {
return ' ${value.toLowerCase()}'
.replaceAll(RegExp(r'[^a-z0-9à-ÿ\u0600-\u06ff\[\]\(\)]+'), ' ')
.replaceAll(RegExp(r'\s+'), ' ')
.trimRight() +
' ';
}
}