refactor: M3 settings screen redesign

This commit is contained in:
edde746
2026-04-02 00:33:25 +02:00
parent 4c534b44d4
commit 339a767304
37 changed files with 2145 additions and 1956 deletions
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Vis navigationsbarlabels",
"showNavBarLabelsDescription": "Vis tekstlabels under navigationsbarikoner",
"liveTvDefaultFavorites": "Standard til favoritkanaler",
"liveTvDefaultFavoritesDescription": "Vis kun favoritkanaler ved åbning af Live TV"
"liveTvDefaultFavoritesDescription": "Vis kun favoritkanaler ved åbning af Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Søg film, serier, musik...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Navigationsleisten-Beschriftungen anzeigen",
"showNavBarLabelsDescription": "Textbeschriftungen unter den Symbolen der Navigationsleiste anzeigen",
"liveTvDefaultFavorites": "Standardmäßig Favoritenkanäle",
"liveTvDefaultFavoritesDescription": "Beim Öffnen von Live TV nur Favoritenkanäle anzeigen"
"liveTvDefaultFavoritesDescription": "Beim Öffnen von Live TV nur Favoritenkanäle anzeigen",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Filme, Serien, Musik suchen...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Show Navigation Bar Labels",
"showNavBarLabelsDescription": "Display text labels under navigation bar icons",
"liveTvDefaultFavorites": "Default to Favorite Channels",
"liveTvDefaultFavoritesDescription": "Show only favorite channels when opening Live TV"
"liveTvDefaultFavoritesDescription": "Show only favorite channels when opening Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Search movies, shows, music...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Mostrar etiquetas de la barra de navegación",
"showNavBarLabelsDescription": "Mostrar etiquetas de texto bajo los iconos de la barra de navegación",
"liveTvDefaultFavorites": "Canales favoritos por defecto",
"liveTvDefaultFavoritesDescription": "Mostrar solo canales favoritos al abrir TV en vivo"
"liveTvDefaultFavoritesDescription": "Mostrar solo canales favoritos al abrir TV en vivo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Buscar películas, series, música...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Afficher les libellés de la barre de navigation",
"showNavBarLabelsDescription": "Afficher les libellés sous les icônes de la barre de navigation",
"liveTvDefaultFavorites": "Chaînes favorites par défaut",
"liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct"
"liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Rechercher des films, des séries, de la musique...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Mostra etichette barra di navigazione",
"showNavBarLabelsDescription": "Mostra le etichette sotto le icone della barra di navigazione",
"liveTvDefaultFavorites": "Canali preferiti predefiniti",
"liveTvDefaultFavoritesDescription": "Mostra solo i canali preferiti all'apertura della TV dal vivo"
"liveTvDefaultFavoritesDescription": "Mostra solo i canali preferiti all'apertura della TV dal vivo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Cerca film. spettacoli, musica...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "ナビゲーションバーラベルを表示",
"showNavBarLabelsDescription": "ナビゲーションバーアイコンの下にテキストラベルを表示",
"liveTvDefaultFavorites": "お気に入りチャンネルをデフォルトに",
"liveTvDefaultFavoritesDescription": "ライブTV を開いたときにお気に入りチャンネルのみ表示"
"liveTvDefaultFavoritesDescription": "ライブTV を開いたときにお気に入りチャンネルのみ表示",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "映画、番組、音楽を検索...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "내비게이션 바 라벨 표시",
"showNavBarLabelsDescription": "내비게이션 바 아이콘 아래에 텍스트 라벨을 표시합니다",
"liveTvDefaultFavorites": "즐겨찾기 채널 기본 설정",
"liveTvDefaultFavoritesDescription": "라이브 TV를 열 때 즐겨찾기 채널만 표시"
"liveTvDefaultFavoritesDescription": "라이브 TV를 열 때 즐겨찾기 채널만 표시",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "영화, 시리즈, 음악 등을 검색하세요...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Vis navigasjonsfeltlabeler",
"showNavBarLabelsDescription": "Vis tekstlabeler under navigasjonsfeltikoner",
"liveTvDefaultFavorites": "Standard til favorittkanaler",
"liveTvDefaultFavoritesDescription": "Vis kun favorittkanaler når du åpner Live TV"
"liveTvDefaultFavoritesDescription": "Vis kun favorittkanaler når du åpner Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Søk i filmer, serier, musikk...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Navigatiebalk labels tonen",
"showNavBarLabelsDescription": "Tekstlabels onder de pictogrammen van de navigatiebalk weergeven",
"liveTvDefaultFavorites": "Standaard favoriete zenders",
"liveTvDefaultFavoritesDescription": "Toon alleen favoriete zenders bij het openen van Live TV"
"liveTvDefaultFavoritesDescription": "Toon alleen favoriete zenders bij het openen van Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Zoek films, series, muziek...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Pokaż etykiety paska nawigacji",
"showNavBarLabelsDescription": "Wyświetl tekstowe etykiety pod ikonami paska nawigacji",
"liveTvDefaultFavorites": "Domyślnie ulubione kanały",
"liveTvDefaultFavoritesDescription": "Pokaż tylko ulubione kanały po otwarciu telewizji na żywo"
"liveTvDefaultFavoritesDescription": "Pokaż tylko ulubione kanały po otwarciu telewizji na żywo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Szukaj filmów, seriali, muzyki...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Mostrar Rótulos da Barra de Navegação",
"showNavBarLabelsDescription": "Exibir rótulos de texto sob os ícones da barra de navegação",
"liveTvDefaultFavorites": "Canais favoritos por padrão",
"liveTvDefaultFavoritesDescription": "Mostrar apenas canais favoritos ao abrir TV ao vivo"
"liveTvDefaultFavoritesDescription": "Mostrar apenas canais favoritos ao abrir TV ao vivo",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Buscar filmes, séries, músicas...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Показывать подписи панели навигации",
"showNavBarLabelsDescription": "Отображать текстовые подписи под иконками панели навигации",
"liveTvDefaultFavorites": "Избранные каналы по умолчанию",
"liveTvDefaultFavoritesDescription": "Показывать только избранные каналы при открытии ТВ"
"liveTvDefaultFavoritesDescription": "Показывать только избранные каналы при открытии ТВ",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Поиск фильмов, сериалов, музыки...",
+2 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 15
/// Strings: 12465 (831 per locale)
/// Strings: 12585 (839 per locale)
///
/// Built on 2026-04-01 at 15:18 UTC
/// Built on 2026-04-01 at 22:20 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsDa implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Vis tekstlabels under navigationsbarikoner';
@override String get liveTvDefaultFavorites => 'Standard til favoritkanaler';
@override String get liveTvDefaultFavoritesDescription => 'Vis kun favoritkanaler ved åbning af Live TV';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsDa {
'settings.showNavBarLabelsDescription' => 'Vis tekstlabels under navigationsbarikoner',
'settings.liveTvDefaultFavorites' => 'Standard til favoritkanaler',
'settings.liveTvDefaultFavoritesDescription' => 'Vis kun favoritkanaler ved åbning af Live TV',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Søg film, serier, musik...',
'search.tryDifferentTerm' => 'Prøv en anden søgning',
'search.searchYourMedia' => 'Søg i dine medier',
@@ -1832,6 +1848,8 @@ extension on TranslationsDa {
'licenses.relatedPackages' => 'Relaterede pakker',
'licenses.license' => 'Licens',
'licenses.licenseNumber' => ({required Object number}) => 'Licens ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licenser',
'navigation.libraries' => 'Biblioteker',
'navigation.downloads' => 'Downloads',
@@ -1840,8 +1858,6 @@ extension on TranslationsDa {
'liveTv.guide' => 'Guide',
'liveTv.noChannels' => 'Ingen kanaler tilgængelige',
'liveTv.noDvr' => 'Ingen DVR konfigureret på nogen server',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Ingen programdata tilgængelig',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Genindlæs guide',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Textbeschriftungen unter den Symbolen der Navigationsleiste anzeigen';
@override String get liveTvDefaultFavorites => 'Standardmäßig Favoritenkanäle';
@override String get liveTvDefaultFavoritesDescription => 'Beim Öffnen von Live TV nur Favoritenkanäle anzeigen';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsDe {
'settings.showNavBarLabelsDescription' => 'Textbeschriftungen unter den Symbolen der Navigationsleiste anzeigen',
'settings.liveTvDefaultFavorites' => 'Standardmäßig Favoritenkanäle',
'settings.liveTvDefaultFavoritesDescription' => 'Beim Öffnen von Live TV nur Favoritenkanäle anzeigen',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Filme, Serien, Musik suchen...',
'search.tryDifferentTerm' => 'Anderen Suchbegriff versuchen',
'search.searchYourMedia' => 'In den eigenen Medien suchen',
@@ -1832,6 +1848,8 @@ extension on TranslationsDe {
'licenses.relatedPackages' => 'Verwandte Pakete',
'licenses.license' => 'Lizenz',
'licenses.licenseNumber' => ({required Object number}) => 'Lizenz ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} Lizenzen',
'navigation.libraries' => 'Medien',
'navigation.downloads' => 'Downloads',
@@ -1840,8 +1858,6 @@ extension on TranslationsDe {
'liveTv.guide' => 'Programmführer',
'liveTv.noChannels' => 'Keine Kanäle verfügbar',
'liveTv.noDvr' => 'Kein DVR auf einem Server konfiguriert',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Keine Programmdaten verfügbar',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Programmführer neu laden',
+34 -2
View File
@@ -765,6 +765,30 @@ class TranslationsSettingsEn {
/// en: 'Show only favorite channels when opening Live TV'
String get liveTvDefaultFavoritesDescription => 'Show only favorite channels when opening Live TV';
/// en: 'Display'
String get display => 'Display';
/// en: 'Home Screen'
String get homeScreen => 'Home Screen';
/// en: 'Navigation'
String get navigation => 'Navigation';
/// en: 'Content'
String get content => 'Content';
/// en: 'Player'
String get player => 'Player';
/// en: 'Subtitles & Configuration'
String get subtitlesAndConfig => 'Subtitles & Configuration';
/// en: 'Seek & Timing'
String get seekAndTiming => 'Seek & Timing';
/// en: 'Behavior'
String get behavior => 'Behavior';
}
// Path: search
@@ -3208,6 +3232,14 @@ extension on Translations {
'settings.showNavBarLabelsDescription' => 'Display text labels under navigation bar icons',
'settings.liveTvDefaultFavorites' => 'Default to Favorite Channels',
'settings.liveTvDefaultFavoritesDescription' => 'Show only favorite channels when opening Live TV',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Search movies, shows, music...',
'search.tryDifferentTerm' => 'Try a different search term',
'search.searchYourMedia' => 'Search your media',
@@ -3501,6 +3533,8 @@ extension on Translations {
'licenses.relatedPackages' => 'Related Packages',
'licenses.license' => 'License',
'licenses.licenseNumber' => ({required Object number}) => 'License ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licenses',
'navigation.libraries' => 'Libraries',
'navigation.downloads' => 'Downloads',
@@ -3509,8 +3543,6 @@ extension on Translations {
'liveTv.guide' => 'Guide',
'liveTv.noChannels' => 'No channels available',
'liveTv.noDvr' => 'No DVR configured on any server',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'No program data available',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Reload Guide',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Mostrar etiquetas de texto bajo los iconos de la barra de navegación';
@override String get liveTvDefaultFavorites => 'Canales favoritos por defecto';
@override String get liveTvDefaultFavoritesDescription => 'Mostrar solo canales favoritos al abrir TV en vivo';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsEs {
'settings.showNavBarLabelsDescription' => 'Mostrar etiquetas de texto bajo los iconos de la barra de navegación',
'settings.liveTvDefaultFavorites' => 'Canales favoritos por defecto',
'settings.liveTvDefaultFavoritesDescription' => 'Mostrar solo canales favoritos al abrir TV en vivo',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Buscar películas, series, música...',
'search.tryDifferentTerm' => 'Prueba con un término de búsqueda diferente',
'search.searchYourMedia' => 'Busca en tu contenido',
@@ -1832,6 +1848,8 @@ extension on TranslationsEs {
'licenses.relatedPackages' => 'Paquetes relacionados',
'licenses.license' => 'Licencia',
'licenses.licenseNumber' => ({required Object number}) => 'Licencia ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licencias',
'navigation.libraries' => 'Medios',
'navigation.downloads' => 'Descargas',
@@ -1840,8 +1858,6 @@ extension on TranslationsEs {
'liveTv.guide' => 'Guía',
'liveTv.noChannels' => 'No hay canales disponibles',
'liveTv.noDvr' => 'No hay DVR configurado en ningún servidor',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'No hay datos de programación disponibles',
'liveTv.live' => 'EN VIVO',
'liveTv.reloadGuide' => 'Recargar guía',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Afficher les libellés sous les icônes de la barre de navigation';
@override String get liveTvDefaultFavorites => 'Chaînes favorites par défaut';
@override String get liveTvDefaultFavoritesDescription => 'Afficher uniquement les chaînes favorites à l\'ouverture de la TV en direct';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsFr {
'settings.showNavBarLabelsDescription' => 'Afficher les libellés sous les icônes de la barre de navigation',
'settings.liveTvDefaultFavorites' => 'Chaînes favorites par défaut',
'settings.liveTvDefaultFavoritesDescription' => 'Afficher uniquement les chaînes favorites à l\'ouverture de la TV en direct',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Rechercher des films, des séries, de la musique...',
'search.tryDifferentTerm' => 'Essayez un autre terme de recherche',
'search.searchYourMedia' => 'Rechercher dans vos médias',
@@ -1832,6 +1848,8 @@ extension on TranslationsFr {
'licenses.relatedPackages' => 'Package associés',
'licenses.license' => 'Licence',
'licenses.licenseNumber' => ({required Object number}) => 'Licence ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licences',
'navigation.libraries' => 'Medias',
'navigation.downloads' => 'Téléch.',
@@ -1840,8 +1858,6 @@ extension on TranslationsFr {
'liveTv.guide' => 'Guide',
'liveTv.noChannels' => 'Aucune chaîne disponible',
'liveTv.noDvr' => 'Aucun DVR configuré sur les serveurs',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Aucune donnée de programme disponible',
'liveTv.live' => 'EN DIRECT',
'liveTv.reloadGuide' => 'Recharger le guide',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Mostra le etichette sotto le icone della barra di navigazione';
@override String get liveTvDefaultFavorites => 'Canali preferiti predefiniti';
@override String get liveTvDefaultFavoritesDescription => 'Mostra solo i canali preferiti all\'apertura della TV dal vivo';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsIt {
'settings.showNavBarLabelsDescription' => 'Mostra le etichette sotto le icone della barra di navigazione',
'settings.liveTvDefaultFavorites' => 'Canali preferiti predefiniti',
'settings.liveTvDefaultFavoritesDescription' => 'Mostra solo i canali preferiti all\'apertura della TV dal vivo',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Cerca film. spettacoli, musica...',
'search.tryDifferentTerm' => 'Prova altri termini di ricerca',
'search.searchYourMedia' => 'Cerca nei tuoi media',
@@ -1832,6 +1848,8 @@ extension on TranslationsIt {
'licenses.relatedPackages' => 'Pacchetti correlati',
'licenses.license' => 'Licenza',
'licenses.licenseNumber' => ({required Object number}) => 'Licenza ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licenze',
'navigation.libraries' => 'Librerie',
'navigation.downloads' => 'Download',
@@ -1840,8 +1858,6 @@ extension on TranslationsIt {
'liveTv.guide' => 'Guida',
'liveTv.noChannels' => 'Nessun canale disponibile',
'liveTv.noDvr' => 'Nessun DVR configurato su nessun server',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Nessun dato di programma disponibile',
'liveTv.live' => 'IN DIRETTA',
'liveTv.reloadGuide' => 'Ricarica guida',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsJa implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'ナビゲーションバーアイコンの下にテキストラベルを表示';
@override String get liveTvDefaultFavorites => 'お気に入りチャンネルをデフォルトに';
@override String get liveTvDefaultFavoritesDescription => 'ライブTV を開いたときにお気に入りチャンネルのみ表示';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsJa {
'settings.showNavBarLabelsDescription' => 'ナビゲーションバーアイコンの下にテキストラベルを表示',
'settings.liveTvDefaultFavorites' => 'お気に入りチャンネルをデフォルトに',
'settings.liveTvDefaultFavoritesDescription' => 'ライブTV を開いたときにお気に入りチャンネルのみ表示',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => '映画、番組、音楽を検索...',
'search.tryDifferentTerm' => '別の検索語をお試しください',
'search.searchYourMedia' => 'メディアを検索',
@@ -1832,6 +1848,8 @@ extension on TranslationsJa {
'licenses.relatedPackages' => '関連パッケージ',
'licenses.license' => 'ライセンス',
'licenses.licenseNumber' => ({required Object number}) => 'ライセンス ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count}件のライセンス',
'navigation.libraries' => 'ライブラリ',
'navigation.downloads' => 'ダウンロード',
@@ -1840,8 +1858,6 @@ extension on TranslationsJa {
'liveTv.guide' => '番組表',
'liveTv.noChannels' => 'チャンネルがありません',
'liveTv.noDvr' => 'どのサーバーにもDVRが設定されていません',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => '番組データがありません',
'liveTv.live' => 'ライブ',
'liveTv.reloadGuide' => '番組表を再読込',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => '내비게이션 바 아이콘 아래에 텍스트 라벨을 표시합니다';
@override String get liveTvDefaultFavorites => '즐겨찾기 채널 기본 설정';
@override String get liveTvDefaultFavoritesDescription => '라이브 TV를 열 때 즐겨찾기 채널만 표시';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsKo {
'settings.showNavBarLabelsDescription' => '내비게이션 바 아이콘 아래에 텍스트 라벨을 표시합니다',
'settings.liveTvDefaultFavorites' => '즐겨찾기 채널 기본 설정',
'settings.liveTvDefaultFavoritesDescription' => '라이브 TV를 열 때 즐겨찾기 채널만 표시',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => '영화, 시리즈, 음악 등을 검색하세요...',
'search.tryDifferentTerm' => '다른 검색어를 시도해 보세요',
'search.searchYourMedia' => '미디어 검색',
@@ -1832,6 +1848,8 @@ extension on TranslationsKo {
'licenses.relatedPackages' => '관련 소프트웨어 패키지',
'licenses.license' => '라이선스',
'licenses.licenseNumber' => ({required Object number}) => '라이선스 ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} 개의 라이선스',
'navigation.libraries' => '미디어 라이브러리',
'navigation.downloads' => '다운로드',
@@ -1840,8 +1858,6 @@ extension on TranslationsKo {
'liveTv.guide' => '편성표',
'liveTv.noChannels' => '사용 가능한 채널이 없습니다',
'liveTv.noDvr' => '서버에 DVR이 구성되어 있지 않습니다',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => '프로그램 데이터가 없습니다',
'liveTv.live' => '실시간',
'liveTv.reloadGuide' => '편성표 새로고침',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsNb implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Vis tekstlabeler under navigasjonsfeltikoner';
@override String get liveTvDefaultFavorites => 'Standard til favorittkanaler';
@override String get liveTvDefaultFavoritesDescription => 'Vis kun favorittkanaler når du åpner Live TV';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsNb {
'settings.showNavBarLabelsDescription' => 'Vis tekstlabeler under navigasjonsfeltikoner',
'settings.liveTvDefaultFavorites' => 'Standard til favorittkanaler',
'settings.liveTvDefaultFavoritesDescription' => 'Vis kun favorittkanaler når du åpner Live TV',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Søk i filmer, serier, musikk...',
'search.tryDifferentTerm' => 'Prøv et annet søkeord',
'search.searchYourMedia' => 'Søk i mediene dine',
@@ -1832,6 +1848,8 @@ extension on TranslationsNb {
'licenses.relatedPackages' => 'Relaterte pakker',
'licenses.license' => 'Lisens',
'licenses.licenseNumber' => ({required Object number}) => 'Lisens ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} lisenser',
'navigation.libraries' => 'Biblioteker',
'navigation.downloads' => 'Nedlastinger',
@@ -1840,8 +1858,6 @@ extension on TranslationsNb {
'liveTv.guide' => 'Programguide',
'liveTv.noChannels' => 'Ingen kanaler tilgjengelig',
'liveTv.noDvr' => 'Ingen DVR konfigurert på noen server',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Ingen programdata tilgjengelig',
'liveTv.live' => 'DIREKTE',
'liveTv.reloadGuide' => 'Last inn programguide på nytt',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Tekstlabels onder de pictogrammen van de navigatiebalk weergeven';
@override String get liveTvDefaultFavorites => 'Standaard favoriete zenders';
@override String get liveTvDefaultFavoritesDescription => 'Toon alleen favoriete zenders bij het openen van Live TV';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsNl {
'settings.showNavBarLabelsDescription' => 'Tekstlabels onder de pictogrammen van de navigatiebalk weergeven',
'settings.liveTvDefaultFavorites' => 'Standaard favoriete zenders',
'settings.liveTvDefaultFavoritesDescription' => 'Toon alleen favoriete zenders bij het openen van Live TV',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Zoek films, series, muziek...',
'search.tryDifferentTerm' => 'Probeer een andere zoekterm',
'search.searchYourMedia' => 'Zoek in je media',
@@ -1832,6 +1848,8 @@ extension on TranslationsNl {
'licenses.relatedPackages' => 'Gerelateerde pakketten',
'licenses.license' => 'Licentie',
'licenses.licenseNumber' => ({required Object number}) => 'Licentie ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licenties',
'navigation.libraries' => 'Media',
'navigation.downloads' => 'Downloads',
@@ -1840,8 +1858,6 @@ extension on TranslationsNl {
'liveTv.guide' => 'Gids',
'liveTv.noChannels' => 'Geen zenders beschikbaar',
'liveTv.noDvr' => 'Geen DVR geconfigureerd op een server',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Geen programmagegevens beschikbaar',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Gids herladen',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsPl implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Wyświetl tekstowe etykiety pod ikonami paska nawigacji';
@override String get liveTvDefaultFavorites => 'Domyślnie ulubione kanały';
@override String get liveTvDefaultFavoritesDescription => 'Pokaż tylko ulubione kanały po otwarciu telewizji na żywo';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsPl {
'settings.showNavBarLabelsDescription' => 'Wyświetl tekstowe etykiety pod ikonami paska nawigacji',
'settings.liveTvDefaultFavorites' => 'Domyślnie ulubione kanały',
'settings.liveTvDefaultFavoritesDescription' => 'Pokaż tylko ulubione kanały po otwarciu telewizji na żywo',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Szukaj filmów, seriali, muzyki...',
'search.tryDifferentTerm' => 'Spróbuj innego wyszukiwania',
'search.searchYourMedia' => 'Przeszukaj swoje media',
@@ -1832,6 +1848,8 @@ extension on TranslationsPl {
'licenses.relatedPackages' => 'Powiązane pakiety',
'licenses.license' => 'Licencja',
'licenses.licenseNumber' => ({required Object number}) => 'Licencja ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licencji',
'navigation.libraries' => 'Biblioteki',
'navigation.downloads' => 'Pobrania',
@@ -1840,8 +1858,6 @@ extension on TranslationsPl {
'liveTv.guide' => 'Przewodnik',
'liveTv.noChannels' => 'Brak dostępnych kanałów',
'liveTv.noDvr' => 'Brak skonfigurowanego DVR na żadnym serwerze',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Brak danych o programach',
'liveTv.live' => 'NA ŻYWO',
'liveTv.reloadGuide' => 'Odśwież przewodnik',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsPt implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Exibir rótulos de texto sob os ícones da barra de navegação';
@override String get liveTvDefaultFavorites => 'Canais favoritos por padrão';
@override String get liveTvDefaultFavoritesDescription => 'Mostrar apenas canais favoritos ao abrir TV ao vivo';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsPt {
'settings.showNavBarLabelsDescription' => 'Exibir rótulos de texto sob os ícones da barra de navegação',
'settings.liveTvDefaultFavorites' => 'Canais favoritos por padrão',
'settings.liveTvDefaultFavoritesDescription' => 'Mostrar apenas canais favoritos ao abrir TV ao vivo',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Buscar filmes, séries, músicas...',
'search.tryDifferentTerm' => 'Tente um termo de busca diferente',
'search.searchYourMedia' => 'Buscar suas mídias',
@@ -1832,6 +1848,8 @@ extension on TranslationsPt {
'licenses.relatedPackages' => 'Pacotes Relacionados',
'licenses.license' => 'Licença',
'licenses.licenseNumber' => ({required Object number}) => 'Licença ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licenças',
'navigation.libraries' => 'Bibliotecas',
'navigation.downloads' => 'Downloads',
@@ -1840,8 +1858,6 @@ extension on TranslationsPt {
'liveTv.guide' => 'Guia',
'liveTv.noChannels' => 'Nenhum canal disponível',
'liveTv.noDvr' => 'Nenhum DVR configurado em nenhum servidor',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Nenhum dado de programação disponível',
'liveTv.live' => 'AO VIVO',
'liveTv.reloadGuide' => 'Recarregar Guia',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsRu implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Отображать текстовые подписи под иконками панели навигации';
@override String get liveTvDefaultFavorites => 'Избранные каналы по умолчанию';
@override String get liveTvDefaultFavoritesDescription => 'Показывать только избранные каналы при открытии ТВ';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsRu {
'settings.showNavBarLabelsDescription' => 'Отображать текстовые подписи под иконками панели навигации',
'settings.liveTvDefaultFavorites' => 'Избранные каналы по умолчанию',
'settings.liveTvDefaultFavoritesDescription' => 'Показывать только избранные каналы при открытии ТВ',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Поиск фильмов, сериалов, музыки...',
'search.tryDifferentTerm' => 'Попробуйте другой запрос',
'search.searchYourMedia' => 'Поиск в вашей медиатеке',
@@ -1832,6 +1848,8 @@ extension on TranslationsRu {
'licenses.relatedPackages' => 'Связанные пакеты',
'licenses.license' => 'Лицензия',
'licenses.licenseNumber' => ({required Object number}) => 'Лицензия ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} лицензий',
'navigation.libraries' => 'Библиотеки',
'navigation.downloads' => 'Загрузки',
@@ -1840,8 +1858,6 @@ extension on TranslationsRu {
'liveTv.guide' => 'Программа',
'liveTv.noChannels' => 'Нет доступных каналов',
'liveTv.noDvr' => 'DVR не настроен ни на одном сервере',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Нет данных о программах',
'liveTv.live' => 'ЭФИР',
'liveTv.reloadGuide' => 'Перезагрузить программу',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => 'Visa textetiketter under navigeringsfältets ikoner';
@override String get liveTvDefaultFavorites => 'Standard till favoritkanaler';
@override String get liveTvDefaultFavoritesDescription => 'Visa bara favoritkanaler när du öppnar Live TV';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsSv {
'settings.showNavBarLabelsDescription' => 'Visa textetiketter under navigeringsfältets ikoner',
'settings.liveTvDefaultFavorites' => 'Standard till favoritkanaler',
'settings.liveTvDefaultFavoritesDescription' => 'Visa bara favoritkanaler när du öppnar Live TV',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => 'Sök filmer, serier, musik...',
'search.tryDifferentTerm' => 'Prova en annan sökterm',
'search.searchYourMedia' => 'Sök i dina media',
@@ -1832,6 +1848,8 @@ extension on TranslationsSv {
'licenses.relatedPackages' => 'Relaterade paket',
'licenses.license' => 'Licens',
'licenses.licenseNumber' => ({required Object number}) => 'Licens ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} licenser',
'navigation.libraries' => 'Bibliotek',
'navigation.downloads' => 'Nerladdat',
@@ -1840,8 +1858,6 @@ extension on TranslationsSv {
'liveTv.guide' => 'Programguide',
'liveTv.noChannels' => 'Inga kanaler tillgängliga',
'liveTv.noDvr' => 'Ingen DVR konfigurerad på någon server',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => 'Ingen programdata tillgänglig',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Ladda om programguide',
+18 -2
View File
@@ -340,6 +340,14 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn {
@override String get showNavBarLabelsDescription => '在导航栏图标下方显示文字标签';
@override String get liveTvDefaultFavorites => '默认显示收藏频道';
@override String get liveTvDefaultFavoritesDescription => '打开直播电视时仅显示收藏频道';
@override String get display => 'Display';
@override String get homeScreen => 'Home Screen';
@override String get navigation => 'Navigation';
@override String get content => 'Content';
@override String get player => 'Player';
@override String get subtitlesAndConfig => 'Subtitles & Configuration';
@override String get seekAndTiming => 'Seek & Timing';
@override String get behavior => 'Behavior';
}
// Path: search
@@ -1539,6 +1547,14 @@ extension on TranslationsZh {
'settings.showNavBarLabelsDescription' => '在导航栏图标下方显示文字标签',
'settings.liveTvDefaultFavorites' => '默认显示收藏频道',
'settings.liveTvDefaultFavoritesDescription' => '打开直播电视时仅显示收藏频道',
'settings.display' => 'Display',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.content' => 'Content',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'search.hint' => '搜索电影、系列、音乐...',
'search.tryDifferentTerm' => '尝试不同的搜索词',
'search.searchYourMedia' => '搜索媒体',
@@ -1832,6 +1848,8 @@ extension on TranslationsZh {
'licenses.relatedPackages' => '相关软件包',
'licenses.license' => '许可证',
'licenses.licenseNumber' => ({required Object number}) => '许可证 ${number}',
_ => null,
} ?? switch (path) {
'licenses.licensesCount' => ({required Object count}) => '${count} 个许可证',
'navigation.libraries' => '媒体库',
'navigation.downloads' => '下载',
@@ -1840,8 +1858,6 @@ extension on TranslationsZh {
'liveTv.guide' => '节目指南',
'liveTv.noChannels' => '没有可用的频道',
'liveTv.noDvr' => '没有服务器配置了DVR',
_ => null,
} ?? switch (path) {
'liveTv.noPrograms' => '没有可用的节目数据',
'liveTv.live' => '直播',
'liveTv.reloadGuide' => '重新加载节目指南',
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "Visa navigeringsfältets etiketter",
"showNavBarLabelsDescription": "Visa textetiketter under navigeringsfältets ikoner",
"liveTvDefaultFavorites": "Standard till favoritkanaler",
"liveTvDefaultFavoritesDescription": "Visa bara favoritkanaler när du öppnar Live TV"
"liveTvDefaultFavoritesDescription": "Visa bara favoritkanaler när du öppnar Live TV",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "Sök filmer, serier, musik...",
+9 -1
View File
@@ -220,7 +220,15 @@
"showNavBarLabels": "显示导航栏标签",
"showNavBarLabelsDescription": "在导航栏图标下方显示文字标签",
"liveTvDefaultFavorites": "默认显示收藏频道",
"liveTvDefaultFavoritesDescription": "打开直播电视时仅显示收藏频道"
"liveTvDefaultFavoritesDescription": "打开直播电视时仅显示收藏频道",
"display": "Display",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"content": "Content",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior"
},
"search": {
"hint": "搜索电影、系列、音乐...",
@@ -0,0 +1,396 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../i18n/strings.g.dart';
import '../../providers/settings_provider.dart';
import '../../providers/theme_provider.dart';
import '../../providers/user_profile_provider.dart';
import '../../services/settings_service.dart' as settings;
import '../../utils/platform_detector.dart';
import '../../widgets/app_icon.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import '../../widgets/settings_section.dart';
import 'settings_utils.dart';
class AppearanceSettingsScreen extends StatefulWidget {
const AppearanceSettingsScreen({super.key});
@override
State<AppearanceSettingsScreen> createState() => _AppearanceSettingsScreenState();
}
class _AppearanceSettingsScreenState extends State<AppearanceSettingsScreen> {
late settings.SettingsService _settingsService;
bool _isLoading = true;
bool _requireProfileSelectionOnOpen = false;
bool _confirmExitOnBack = true;
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
_settingsService = await settings.SettingsService.getInstance();
if (!mounted) return;
setState(() {
_requireProfileSelectionOnOpen = _settingsService.getRequireProfileSelectionOnOpen();
_confirmExitOnBack = _settingsService.getConfirmExitOnBack();
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return FocusedScrollScaffold(
title: Text(t.settings.appearance),
slivers: [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))],
);
}
return FocusedScrollScaffold(
title: Text(t.settings.appearance),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// --- Display ---
SettingsSectionHeader(t.settings.display),
_buildThemeSelector(),
_buildLanguageSelector(),
_buildDensitySelector(),
_buildViewModeSelector(),
_buildEpisodePosterModeSelector(),
// --- Home Screen ---
SettingsSectionHeader(t.settings.homeScreen),
_buildShowHeroSection(),
_buildUseGlobalHubs(),
_buildShowServerNameOnHubs(),
// --- Navigation ---
SettingsSectionHeader(t.settings.navigation),
if (PlatformDetector.shouldUseSideNavigation(context)) _buildAlwaysKeepSidebarOpen(),
if (!PlatformDetector.shouldUseSideNavigation(context)) _buildShowNavBarLabels(),
_buildShowUnwatchedCount(),
// --- Content ---
SettingsSectionHeader(t.settings.content),
_buildLiveTvDefaultFavorites(),
_buildHideSpoilers(),
_buildRequireProfileSelection(),
if (PlatformDetector.isTV()) _buildConfirmExitOnBack(),
const SizedBox(height: 24),
]),
),
],
);
}
// --- Display section ---
Widget _buildThemeSelector() {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return SegmentedSetting<settings.ThemeMode>(
icon: themeProvider.themeModeIcon,
title: t.settings.theme,
segments: [
ButtonSegment(value: settings.ThemeMode.system, label: Text(t.settings.systemTheme)),
ButtonSegment(value: settings.ThemeMode.light, label: Text(t.settings.lightTheme)),
ButtonSegment(value: settings.ThemeMode.dark, label: Text(t.settings.darkTheme)),
ButtonSegment(value: settings.ThemeMode.oled, label: Text(t.settings.oledTheme)),
],
selected: themeProvider.themeMode,
onChanged: (value) => themeProvider.setThemeMode(value),
);
},
);
}
Widget _buildLanguageSelector() {
return ListTile(
leading: const AppIcon(Symbols.language_rounded, fill: 1),
title: Text(t.settings.language),
subtitle: Text(_getLanguageDisplayName(LocaleSettings.currentLocale)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () async {
final value = await showSelectionDialog<AppLocale>(
context: context,
title: t.settings.language,
options: AppLocale.values
.map((locale) => DialogOption(value: locale, title: _getLanguageDisplayName(locale)))
.toList(),
currentValue: LocaleSettings.currentLocale,
);
if (value != null) {
await _settingsService.setAppLocale(value);
LocaleSettings.setLocale(value);
_restartApp();
}
},
);
}
Widget _buildDensitySelector() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SegmentedSetting<settings.LibraryDensity>(
icon: Symbols.grid_view_rounded,
title: t.settings.libraryDensity,
segments: [
ButtonSegment(value: settings.LibraryDensity.compact, label: Text(t.settings.compact)),
ButtonSegment(value: settings.LibraryDensity.normal, label: Text(t.settings.normal)),
ButtonSegment(value: settings.LibraryDensity.comfortable, label: Text(t.settings.comfortable)),
],
selected: settingsProvider.libraryDensity,
onChanged: (value) => settingsProvider.setLibraryDensity(value),
);
},
);
}
Widget _buildViewModeSelector() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SegmentedSetting<settings.ViewMode>(
icon: Symbols.view_list_rounded,
title: t.settings.viewMode,
segments: [
ButtonSegment(value: settings.ViewMode.grid, label: Text(t.settings.gridView)),
ButtonSegment(value: settings.ViewMode.list, label: Text(t.settings.listView)),
],
selected: settingsProvider.viewMode,
onChanged: (value) => settingsProvider.setViewMode(value),
);
},
);
}
Widget _buildEpisodePosterModeSelector() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SegmentedSetting<settings.EpisodePosterMode>(
icon: Symbols.image_rounded,
title: t.settings.episodePosterMode,
segments: [
ButtonSegment(value: settings.EpisodePosterMode.seriesPoster, label: Text(t.settings.seriesPoster)),
ButtonSegment(value: settings.EpisodePosterMode.seasonPoster, label: Text(t.settings.seasonPoster)),
ButtonSegment(
value: settings.EpisodePosterMode.episodeThumbnail, label: Text(t.settings.episodeThumbnail)),
],
selected: settingsProvider.episodePosterMode,
onChanged: (value) => settingsProvider.setEpisodePosterMode(value),
);
},
);
}
// --- Home Screen section ---
Widget _buildShowHeroSection() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.featured_play_list_rounded, fill: 1),
title: Text(t.settings.showHeroSection),
subtitle: Text(t.settings.showHeroSectionDescription),
value: settingsProvider.showHeroSection,
onChanged: (value) async {
await settingsProvider.setShowHeroSection(value);
},
);
},
);
}
Widget _buildUseGlobalHubs() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.home_rounded, fill: 1),
title: Text(t.settings.useGlobalHubs),
subtitle: Text(t.settings.useGlobalHubsDescription),
value: settingsProvider.useGlobalHubs,
onChanged: (value) async {
await settingsProvider.setUseGlobalHubs(value);
},
);
},
);
}
Widget _buildShowServerNameOnHubs() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.dns_rounded, fill: 1),
title: Text(t.settings.showServerNameOnHubs),
subtitle: Text(t.settings.showServerNameOnHubsDescription),
value: settingsProvider.showServerNameOnHubs,
onChanged: (value) async {
await settingsProvider.setShowServerNameOnHubs(value);
},
);
},
);
}
// --- Navigation section ---
Widget _buildAlwaysKeepSidebarOpen() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.dock_to_left_rounded, fill: 1),
title: Text(t.settings.alwaysKeepSidebarOpen),
subtitle: Text(t.settings.alwaysKeepSidebarOpenDescription),
value: settingsProvider.alwaysKeepSidebarOpen,
onChanged: (value) async {
await settingsProvider.setAlwaysKeepSidebarOpen(value);
},
);
},
);
}
Widget _buildShowNavBarLabels() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.label_rounded, fill: 1),
title: Text(t.settings.showNavBarLabels),
subtitle: Text(t.settings.showNavBarLabelsDescription),
value: settingsProvider.showNavBarLabels,
onChanged: (value) async {
await settingsProvider.setShowNavBarLabels(value);
},
);
},
);
}
Widget _buildShowUnwatchedCount() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.counter_1_rounded, fill: 1),
title: Text(t.settings.showUnwatchedCount),
subtitle: Text(t.settings.showUnwatchedCountDescription),
value: settingsProvider.showUnwatchedCount,
onChanged: (value) async {
await settingsProvider.setShowUnwatchedCount(value);
},
);
},
);
}
// --- Content section ---
Widget _buildLiveTvDefaultFavorites() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.star_rounded, fill: 1),
title: Text(t.settings.liveTvDefaultFavorites),
subtitle: Text(t.settings.liveTvDefaultFavoritesDescription),
value: settingsProvider.liveTvDefaultFavorites,
onChanged: (value) async {
await settingsProvider.setLiveTvDefaultFavorites(value);
},
);
},
);
}
Widget _buildHideSpoilers() {
return Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const AppIcon(Symbols.visibility_off_rounded, fill: 1),
title: Text(t.settings.hideSpoilers),
subtitle: Text(t.settings.hideSpoilersDescription),
value: settingsProvider.hideSpoilers,
onChanged: (value) async {
await settingsProvider.setHideSpoilers(value);
},
);
},
);
}
Widget _buildRequireProfileSelection() {
return Consumer<UserProfileProvider>(
builder: (context, userProfileProvider, child) {
if (!userProfileProvider.hasMultipleUsers) return const SizedBox.shrink();
return SwitchListTile(
secondary: const AppIcon(Symbols.person_rounded, fill: 1),
title: Text(t.settings.requireProfileSelectionOnOpen),
subtitle: Text(t.settings.requireProfileSelectionOnOpenDescription),
value: _requireProfileSelectionOnOpen,
onChanged: (value) async {
setState(() => _requireProfileSelectionOnOpen = value);
await _settingsService.setRequireProfileSelectionOnOpen(value);
},
);
},
);
}
Widget _buildConfirmExitOnBack() {
return SwitchListTile(
secondary: const AppIcon(Symbols.exit_to_app_rounded, fill: 1),
title: Text(t.settings.confirmExitOnBack),
subtitle: Text(t.settings.confirmExitOnBackDescription),
value: _confirmExitOnBack,
onChanged: (value) async {
setState(() => _confirmExitOnBack = value);
await _settingsService.setConfirmExitOnBack(value);
},
);
}
// --- Helpers ---
String _getLanguageDisplayName(AppLocale locale) {
switch (locale) {
case AppLocale.en:
return 'English';
case AppLocale.sv:
return 'Svenska';
case AppLocale.fr:
return 'Français';
case AppLocale.it:
return 'Italiano';
case AppLocale.nl:
return 'Nederlands';
case AppLocale.de:
return 'Deutsch';
case AppLocale.zh:
return '中文';
case AppLocale.ko:
return '한국어';
case AppLocale.es:
return 'Español';
case AppLocale.pt:
return 'Português';
case AppLocale.ja:
return '日本語';
case AppLocale.ru:
return 'Русский';
case AppLocale.pl:
return 'Polski';
case AppLocale.da:
return 'Dansk';
case AppLocale.nb:
return 'Norsk bokmål';
}
}
void _restartApp() {
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
}
}
@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import '../../i18n/strings.g.dart';
import '../../models/hotkey_model.dart';
import '../../services/keyboard_shortcuts_service.dart';
import '../../utils/snackbar_helper.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import 'hotkey_recorder_widget.dart';
class KeyboardShortcutsScreen extends StatefulWidget {
final KeyboardShortcutsService keyboardService;
const KeyboardShortcutsScreen({super.key, required this.keyboardService});
@override
State<KeyboardShortcutsScreen> createState() => _KeyboardShortcutsScreenState();
}
class _KeyboardShortcutsScreenState extends State<KeyboardShortcutsScreen> {
Map<String, HotKey> _hotkeys = {};
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadHotkeys();
}
Future<void> _loadHotkeys() async {
await widget.keyboardService.refreshFromStorage();
if (!mounted) return;
setState(() {
_hotkeys = widget.keyboardService.hotkeys;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return FocusedScrollScaffold(
title: Text(t.settings.keyboardShortcuts),
actions: [
TextButton(
onPressed: () async {
await widget.keyboardService.resetToDefaults();
await _loadHotkeys();
if (mounted) {
showSuccessSnackBar(this.context, t.settings.shortcutsReset);
}
},
child: Text(t.common.reset),
),
],
slivers: _isLoading
? [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))]
: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final actions = _hotkeys.keys.toList();
final action = actions[index];
final hotkey = _hotkeys[action]!;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(widget.keyboardService.getActionDisplayName(action)),
subtitle: Text(action),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.fromBorderSide(BorderSide(color: Theme.of(context).dividerColor)),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
child: Text(
widget.keyboardService.formatHotkey(hotkey),
style: const TextStyle(fontFamily: 'monospace'),
),
),
onTap: () => _editHotkey(action, hotkey),
),
);
}, childCount: _hotkeys.length),
),
),
],
);
}
void _editHotkey(String action, HotKey currentHotkey) {
showDialog(
context: context,
builder: (BuildContext context) {
return HotKeyRecorderWidget(
actionName: widget.keyboardService.getActionDisplayName(action),
currentHotKey: currentHotkey,
onHotKeyRecorded: (newHotkey) async {
final navigator = Navigator.of(context);
// Check for conflicts
final existingAction = widget.keyboardService.getActionForHotkey(newHotkey);
if (existingAction != null && existingAction != action) {
navigator.pop();
showErrorSnackBar(
context,
t.settings.shortcutAlreadyAssigned(action: widget.keyboardService.getActionDisplayName(existingAction)),
);
return;
}
// Save the new hotkey
await widget.keyboardService.setHotkey(action, newHotkey);
if (mounted) {
setState(() {
_hotkeys[action] = newHotkey;
});
navigator.pop();
showSuccessSnackBar(
this.context,
t.settings.shortcutUpdated(action: widget.keyboardService.getActionDisplayName(action)),
);
}
},
onCancel: () => Navigator.pop(context),
);
},
);
}
}
@@ -0,0 +1,553 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../i18n/strings.g.dart';
import '../../mpv/player/platform/player_android.dart';
import '../../services/discord_rpc_service.dart';
import '../../services/keyboard_shortcuts_service.dart';
import '../../services/settings_service.dart' as settings;
import '../../utils/platform_detector.dart';
import '../../utils/snackbar_helper.dart';
import '../../widgets/app_icon.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import '../../widgets/settings_section.dart';
import 'external_player_screen.dart';
import 'mpv_config_screen.dart';
import 'settings_utils.dart';
import 'subtitle_styling_screen.dart';
class PlaybackSettingsScreen extends StatefulWidget {
const PlaybackSettingsScreen({super.key});
@override
State<PlaybackSettingsScreen> createState() => _PlaybackSettingsScreenState();
}
class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
late settings.SettingsService _settingsService;
KeyboardShortcutsService? _keyboardService;
bool _isLoading = true;
bool _enableHardwareDecoding = true;
int _bufferSize = 0;
int _seekTimeSmall = 10;
int _seekTimeLarge = 30;
int _rewindOnResume = 0;
int _sleepTimerDuration = 30;
bool _rememberTrackSelections = true;
bool _clickVideoTogglesPlayback = false;
bool _autoSkipIntro = false;
bool _autoSkipCredits = false;
int _autoSkipDelay = 5;
String _introPattern = settings.SettingsService.defaultIntroPattern;
String _creditsPattern = settings.SettingsService.defaultCreditsPattern;
int _maxVolume = 100;
bool _enableDiscordRPC = false;
bool _autoPip = true;
bool _matchContentFrameRate = false;
bool _tunneledPlayback = true;
bool _useExoPlayer = true;
bool _useExternalPlayer = false;
String _selectedExternalPlayerName = '';
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
_settingsService = await settings.SettingsService.getInstance();
if (KeyboardShortcutsService.isPlatformSupported()) {
_keyboardService = await KeyboardShortcutsService.getInstance();
}
if (!mounted) return;
setState(() {
_enableHardwareDecoding = _settingsService.getEnableHardwareDecoding();
_bufferSize = _settingsService.getBufferSize();
_seekTimeSmall = _settingsService.getSeekTimeSmall();
_seekTimeLarge = _settingsService.getSeekTimeLarge();
_rewindOnResume = _settingsService.getRewindOnResume();
_sleepTimerDuration = _settingsService.getSleepTimerDuration();
_rememberTrackSelections = _settingsService.getRememberTrackSelections();
_clickVideoTogglesPlayback = _settingsService.getClickVideoTogglesPlayback();
_autoSkipIntro = _settingsService.getAutoSkipIntro();
_autoSkipCredits = _settingsService.getAutoSkipCredits();
_autoSkipDelay = _settingsService.getAutoSkipDelay();
_introPattern = _settingsService.getIntroPattern();
_creditsPattern = _settingsService.getCreditsPattern();
_maxVolume = _settingsService.getMaxVolume();
_enableDiscordRPC = _settingsService.getEnableDiscordRPC();
_autoPip = _settingsService.getAutoPip();
_matchContentFrameRate = _settingsService.getMatchContentFrameRate();
_tunneledPlayback = _settingsService.getTunneledPlayback();
_useExoPlayer = _settingsService.getUseExoPlayer();
_useExternalPlayer = _settingsService.getUseExternalPlayer();
_selectedExternalPlayerName = _settingsService.getSelectedExternalPlayer().name;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return FocusedScrollScaffold(
title: Text(t.settings.videoPlayback),
slivers: [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))],
);
}
final isMobile = PlatformDetector.isMobile(context);
return FocusedScrollScaffold(
title: Text(t.settings.videoPlayback),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// --- Player ---
SettingsSectionHeader(t.settings.player),
if (Platform.isAndroid) _buildPlayerBackendSelector(),
_buildExternalPlayerTile(),
_buildHardwareDecoding(),
if ((Platform.isAndroid && !PlatformDetector.isTV()) || Platform.isIOS || Platform.isMacOS) _buildAutoPip(),
if (Platform.isAndroid) _buildMatchContentFrameRate(),
if (Platform.isAndroid && _useExoPlayer) _buildTunneledPlayback(),
_buildBufferSizeSelector(),
// --- Subtitles & Config ---
SettingsSectionHeader(t.settings.subtitlesAndConfig),
_buildSubtitleStylingTile(),
if (!Platform.isAndroid || !_useExoPlayer) _buildMpvConfigTile(),
// --- Seek & Timing ---
SettingsSectionHeader(t.settings.seekAndTiming),
_buildSmallSkipDuration(),
_buildLargeSkipDuration(),
_buildRewindOnResume(),
_buildDefaultSleepTimer(),
_buildMaxVolume(),
// --- Behavior ---
SettingsSectionHeader(t.settings.behavior),
if (DiscordRPCService.isAvailable) _buildDiscordRPC(),
_buildRememberTrackSelections(),
if (!isMobile) _buildClickVideoTogglesPlayback(),
// --- Auto-Skip ---
SettingsSectionHeader(t.settings.autoSkip),
_buildAutoSkipIntro(),
_buildAutoSkipCredits(),
_buildAutoSkipDelay(),
_buildIntroPattern(),
_buildCreditsPattern(),
const SizedBox(height: 24),
]),
),
],
);
}
// --- Player section ---
Widget _buildPlayerBackendSelector() {
return SegmentedSetting<bool>(
icon: Symbols.play_circle_rounded,
title: t.settings.playerBackend,
segments: [
ButtonSegment(value: true, label: Text(t.settings.exoPlayer)),
ButtonSegment(value: false, label: Text(t.settings.mpv)),
],
selected: _useExoPlayer,
onChanged: (value) async {
setState(() => _useExoPlayer = value);
await _settingsService.setUseExoPlayer(value);
},
);
}
Widget _buildExternalPlayerTile() {
return ListTile(
leading: const AppIcon(Symbols.open_in_new_rounded, fill: 1),
title: Text(t.externalPlayer.title),
subtitle: Text(_useExternalPlayer ? _selectedExternalPlayerName : t.externalPlayer.off),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () async {
await Navigator.push(context, MaterialPageRoute(builder: (context) => const ExternalPlayerScreen()));
final s = await settings.SettingsService.getInstance();
if (!mounted) return;
setState(() {
_useExternalPlayer = s.getUseExternalPlayer();
_selectedExternalPlayerName = s.getSelectedExternalPlayer().name;
});
},
);
}
Widget _buildHardwareDecoding() {
return SwitchListTile(
secondary: const AppIcon(Symbols.hardware_rounded, fill: 1),
title: Text(t.settings.hardwareDecoding),
subtitle: Text(t.settings.hardwareDecodingDescription),
value: _enableHardwareDecoding,
onChanged: (value) async {
setState(() => _enableHardwareDecoding = value);
await _settingsService.setEnableHardwareDecoding(value);
},
);
}
Widget _buildAutoPip() {
return SwitchListTile(
secondary: const AppIcon(Symbols.picture_in_picture_alt_rounded, fill: 1),
title: Text(t.settings.autoPip),
subtitle: Text(t.settings.autoPipDescription),
value: _autoPip,
onChanged: (value) async {
setState(() => _autoPip = value);
await _settingsService.setAutoPip(value);
},
);
}
Widget _buildMatchContentFrameRate() {
return SwitchListTile(
secondary: const AppIcon(Symbols.display_settings_rounded, fill: 1),
title: Text(t.settings.matchContentFrameRate),
subtitle: Text(t.settings.matchContentFrameRateDescription),
value: _matchContentFrameRate,
onChanged: (value) async {
setState(() => _matchContentFrameRate = value);
await _settingsService.setMatchContentFrameRate(value);
},
);
}
Widget _buildTunneledPlayback() {
return SwitchListTile(
secondary: const AppIcon(Symbols.tv_options_input_settings_rounded, fill: 1),
title: Text(t.settings.tunneledPlayback),
subtitle: Text(t.settings.tunneledPlaybackDescription),
value: _tunneledPlayback,
onChanged: (value) async {
setState(() => _tunneledPlayback = value);
await _settingsService.setTunneledPlayback(value);
},
);
}
Widget _buildBufferSizeSelector() {
return ListTile(
leading: const AppIcon(Symbols.memory_rounded, fill: 1),
title: Text(t.settings.bufferSize),
subtitle: Text(
_bufferSize == 0 ? t.settings.bufferSizeAuto : t.settings.bufferSizeMB(size: _bufferSize.toString()),
),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () async {
final bufferOptions = [0, 64, 128, 256, 512, 1024];
final value = await showSelectionDialog<int>(
context: context,
title: t.settings.bufferSize,
options: bufferOptions
.map((size) => DialogOption(value: size, title: size == 0 ? t.settings.bufferSizeAuto : '${size}MB'))
.toList(),
currentValue: _bufferSize,
);
if (value != null) {
setState(() {
_bufferSize = value;
_settingsService.setBufferSize(value);
});
if (Platform.isAndroid && value > 0) {
final heapMB = await PlayerAndroid.getHeapSize();
if (heapMB > 0 && value > heapMB ~/ 4 && mounted) {
showAppSnackBar(
context,
t.settings.bufferSizeWarning(heap: heapMB.toString(), size: value.toString()),
);
}
}
}
},
);
}
// --- Subtitles & Config section ---
Widget _buildSubtitleStylingTile() {
return ListTile(
leading: const AppIcon(Symbols.subtitles_rounded, fill: 1),
title: Text(t.settings.subtitleStyling),
subtitle: Text(t.settings.subtitleStylingDescription),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const SubtitleStylingScreen()));
},
);
}
Widget _buildMpvConfigTile() {
return ListTile(
leading: const AppIcon(Symbols.tune_rounded, fill: 1),
title: Text(t.mpvConfig.title),
subtitle: Text(t.mpvConfig.description),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const MpvConfigScreen()));
},
);
}
// --- Seek & Timing section ---
Widget _buildSmallSkipDuration() {
return ListTile(
leading: const AppIcon(Symbols.replay_10_rounded, fill: 1),
title: Text(t.settings.smallSkipDuration),
subtitle: Text(t.settings.secondsUnit(seconds: _seekTimeSmall.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.settings.smallSkipDuration,
labelText: t.settings.secondsLabel,
suffixText: t.settings.secondsShort,
min: 1,
max: 120,
currentValue: _seekTimeSmall,
onSave: (value) async {
setState(() {
_seekTimeSmall = value;
_settingsService.setSeekTimeSmall(value);
});
await _keyboardService?.refreshFromStorage();
},
),
);
}
Widget _buildLargeSkipDuration() {
return ListTile(
leading: const AppIcon(Symbols.replay_30_rounded, fill: 1),
title: Text(t.settings.largeSkipDuration),
subtitle: Text(t.settings.secondsUnit(seconds: _seekTimeLarge.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.settings.largeSkipDuration,
labelText: t.settings.secondsLabel,
suffixText: t.settings.secondsShort,
min: 1,
max: 120,
currentValue: _seekTimeLarge,
onSave: (value) async {
setState(() {
_seekTimeLarge = value;
_settingsService.setSeekTimeLarge(value);
});
await _keyboardService?.refreshFromStorage();
},
),
);
}
Widget _buildRewindOnResume() {
return ListTile(
leading: const AppIcon(Symbols.replay_rounded, fill: 1),
title: Text(t.settings.rewindOnResume),
subtitle: Text(t.settings.secondsUnit(seconds: _rewindOnResume.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.settings.rewindOnResume,
labelText: t.settings.secondsLabel,
suffixText: t.settings.secondsShort,
min: 0,
max: 10,
currentValue: _rewindOnResume,
onSave: (value) async {
setState(() {
_rewindOnResume = value;
_settingsService.setRewindOnResume(value);
});
},
),
);
}
Widget _buildDefaultSleepTimer() {
return ListTile(
leading: const AppIcon(Symbols.bedtime_rounded, fill: 1),
title: Text(t.settings.defaultSleepTimer),
subtitle: Text(t.settings.minutesUnit(minutes: _sleepTimerDuration.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.settings.defaultSleepTimer,
labelText: t.settings.minutesLabel,
suffixText: t.settings.minutesShort,
min: 5,
max: 240,
currentValue: _sleepTimerDuration,
onSave: (value) async {
setState(() => _sleepTimerDuration = value);
await _settingsService.setSleepTimerDuration(value);
},
),
);
}
Widget _buildMaxVolume() {
return ListTile(
leading: const AppIcon(Symbols.volume_up_rounded, fill: 1),
title: Text(t.settings.maxVolume),
subtitle: Text(t.settings.maxVolumePercent(percent: _maxVolume.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.settings.maxVolume,
labelText: t.settings.maxVolumeDescription,
suffixText: '%',
min: 100,
max: 300,
currentValue: _maxVolume,
onSave: (value) async {
setState(() => _maxVolume = value);
await _settingsService.setMaxVolume(value);
},
),
);
}
// --- Behavior section ---
Widget _buildDiscordRPC() {
return SwitchListTile(
secondary: const AppIcon(Symbols.chat_rounded, fill: 1),
title: Text(t.settings.discordRichPresence),
subtitle: Text(t.settings.discordRichPresenceDescription),
value: _enableDiscordRPC,
onChanged: (value) async {
setState(() => _enableDiscordRPC = value);
await _settingsService.setEnableDiscordRPC(value);
await DiscordRPCService.instance.setEnabled(value);
},
);
}
Widget _buildRememberTrackSelections() {
return SwitchListTile(
secondary: const AppIcon(Symbols.bookmark_rounded, fill: 1),
title: Text(t.settings.rememberTrackSelections),
subtitle: Text(t.settings.rememberTrackSelectionsDescription),
value: _rememberTrackSelections,
onChanged: (value) async {
setState(() => _rememberTrackSelections = value);
await _settingsService.setRememberTrackSelections(value);
},
);
}
Widget _buildClickVideoTogglesPlayback() {
return SwitchListTile(
secondary: const AppIcon(Symbols.play_pause_rounded, fill: 1),
title: Text(t.settings.clickVideoTogglesPlayback),
subtitle: Text(t.settings.clickVideoTogglesPlaybackDescription),
value: _clickVideoTogglesPlayback,
onChanged: (value) async {
setState(() => _clickVideoTogglesPlayback = value);
await _settingsService.setClickVideoTogglesPlayback(value);
},
);
}
// --- Auto-Skip section ---
Widget _buildAutoSkipIntro() {
return SwitchListTile(
secondary: const AppIcon(Symbols.fast_forward_rounded, fill: 1),
title: Text(t.settings.autoSkipIntro),
subtitle: Text(t.settings.autoSkipIntroDescription),
value: _autoSkipIntro,
onChanged: (value) async {
setState(() => _autoSkipIntro = value);
await _settingsService.setAutoSkipIntro(value);
},
);
}
Widget _buildAutoSkipCredits() {
return SwitchListTile(
secondary: const AppIcon(Symbols.skip_next_rounded, fill: 1),
title: Text(t.settings.autoSkipCredits),
subtitle: Text(t.settings.autoSkipCreditsDescription),
value: _autoSkipCredits,
onChanged: (value) async {
setState(() => _autoSkipCredits = value);
await _settingsService.setAutoSkipCredits(value);
},
);
}
Widget _buildAutoSkipDelay() {
return ListTile(
leading: const AppIcon(Symbols.timer_rounded, fill: 1),
title: Text(t.settings.autoSkipDelay),
subtitle: Text(t.settings.autoSkipDelayDescription(seconds: _autoSkipDelay.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.settings.autoSkipDelay,
labelText: t.settings.secondsLabel,
suffixText: t.settings.secondsShort,
min: 1,
max: 30,
currentValue: _autoSkipDelay,
onSave: (value) async {
setState(() => _autoSkipDelay = value);
await _settingsService.setAutoSkipDelay(value);
},
),
);
}
Widget _buildIntroPattern() {
return ListTile(
leading: const AppIcon(Symbols.match_case_rounded, fill: 1),
title: Text(t.settings.introPattern),
subtitle: Text(t.settings.introPatternDescription),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showTextInputDialog(
context: context,
title: t.settings.introPattern,
currentValue: _introPattern,
defaultValue: settings.SettingsService.defaultIntroPattern,
onSave: (value) async {
setState(() => _introPattern = value);
await _settingsService.setIntroPattern(value);
},
),
);
}
Widget _buildCreditsPattern() {
return ListTile(
leading: const AppIcon(Symbols.match_case_rounded, fill: 1),
title: Text(t.settings.creditsPattern),
subtitle: Text(t.settings.creditsPatternDescription),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showTextInputDialog(
context: context,
title: t.settings.creditsPattern,
currentValue: _creditsPattern,
defaultValue: settings.SettingsService.defaultCreditsPattern,
onSave: (value) async {
setState(() => _creditsPattern = value);
await _settingsService.setCreditsPattern(value);
},
),
);
}
}
File diff suppressed because it is too large Load Diff
+290
View File
@@ -0,0 +1,290 @@
import 'package:flutter/material.dart';
import '../../focus/input_mode_tracker.dart';
import '../../i18n/strings.g.dart';
import '../../widgets/tv_number_spinner.dart';
/// Model for option selection dialogs.
class DialogOption<T> {
final T value;
final String title;
final String? subtitle;
const DialogOption({required this.value, required this.title, this.subtitle});
}
/// Shows an M3-conformant AlertDialog with RadioListTiles for multi-option selection.
/// Used for settings with 5+ options (language, buffer size, etc.).
Future<T?> showSelectionDialog<T>({
required BuildContext context,
required String title,
required List<DialogOption<T>> options,
required T currentValue,
}) {
return showDialog<T>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(title),
contentPadding: const EdgeInsets.only(top: 12, bottom: 24),
content: SingleChildScrollView(
child: RadioGroup<T>(
groupValue: currentValue,
onChanged: (value) => Navigator.pop(dialogContext, value),
child: Column(
mainAxisSize: MainAxisSize.min,
children: options
.map((option) => RadioListTile<T>(
title: Text(option.title),
subtitle: option.subtitle != null ? Text(option.subtitle!) : null,
value: option.value,
))
.toList(),
),
),
),
),
);
}
/// Generic numeric input dialog.
/// On TV/keyboard mode, uses a spinner widget with +/- buttons for D-pad navigation.
/// On other platforms, uses a TextField with focus management.
void showNumericInputDialog({
required BuildContext context,
required String title,
required String labelText,
required String suffixText,
required int min,
required int max,
required int currentValue,
required Future<void> Function(int value) onSave,
}) {
final useDpadControls = InputModeTracker.isKeyboardMode(context);
if (useDpadControls) {
_showNumericInputDialogTV(
context: context,
title: title,
suffixText: suffixText,
min: min,
max: max,
currentValue: currentValue,
onSave: onSave,
);
} else {
_showNumericInputDialogStandard(
context: context,
title: title,
labelText: labelText,
suffixText: suffixText,
min: min,
max: max,
currentValue: currentValue,
onSave: onSave,
);
}
}
void _showNumericInputDialogTV({
required BuildContext context,
required String title,
required String suffixText,
required int min,
required int max,
required int currentValue,
required Future<void> Function(int value) onSave,
}) {
int spinnerValue = currentValue;
final saveFocusNode = FocusNode();
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TvNumberSpinner(
value: spinnerValue,
min: min,
max: max,
suffix: suffixText,
autofocus: true,
onChanged: (value) {
setDialogState(() {
spinnerValue = value;
});
},
onConfirm: () => saveFocusNode.requestFocus(),
onCancel: () => Navigator.pop(dialogContext),
),
const SizedBox(height: 8),
Text(
t.settings.durationHint(min: min, max: max),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
TextButton(
focusNode: saveFocusNode,
onPressed: () async {
await onSave(spinnerValue);
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
},
child: Text(t.common.save),
),
],
);
},
);
},
).then((_) => saveFocusNode.dispose());
}
void _showNumericInputDialogStandard({
required BuildContext context,
required String title,
required String labelText,
required String suffixText,
required int min,
required int max,
required int currentValue,
required Future<void> Function(int value) onSave,
}) {
final controller = TextEditingController(text: currentValue.toString());
String? errorText;
final saveFocusNode = FocusNode();
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Text(title),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: labelText,
hintText: t.settings.durationHint(min: min, max: max),
errorText: errorText,
suffixText: suffixText,
),
autofocus: true,
textInputAction: TextInputAction.done,
onEditingComplete: () {
saveFocusNode.requestFocus();
},
onChanged: (value) {
final parsed = int.tryParse(value);
setDialogState(() {
if (parsed == null) {
errorText = t.settings.validationErrorEnterNumber;
} else if (parsed < min || parsed > max) {
errorText = t.settings.validationErrorDuration(min: min, max: max, unit: labelText.toLowerCase());
} else {
errorText = null;
}
});
},
),
actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
TextButton(
focusNode: saveFocusNode,
onPressed: () async {
final parsed = int.tryParse(controller.text);
if (parsed != null && parsed >= min && parsed <= max) {
await onSave(parsed);
if (dialogContext.mounted) {
Navigator.pop(dialogContext);
}
}
},
child: Text(t.common.save),
),
],
);
},
);
},
).then((_) {
saveFocusNode.dispose();
});
}
/// Shows a text input dialog with regex validation and reset-to-default support.
void showTextInputDialog({
required BuildContext context,
required String title,
required String currentValue,
required String defaultValue,
required Future<void> Function(String value) onSave,
}) {
final controller = TextEditingController(text: currentValue);
String? errorText;
final saveFocusNode = FocusNode();
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Text(title),
content: TextField(
controller: controller,
decoration: InputDecoration(
labelText: 'Regex',
errorText: errorText,
),
autofocus: true,
textInputAction: TextInputAction.done,
onEditingComplete: () => saveFocusNode.requestFocus(),
onChanged: (value) {
setDialogState(() {
try {
RegExp(value, caseSensitive: false);
errorText = null;
} catch (_) {
errorText = t.settings.invalidRegex;
}
});
},
),
actions: [
TextButton(
onPressed: () {
controller.text = defaultValue;
setDialogState(() => errorText = null);
},
child: Text(t.settings.resetToDefault),
),
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
TextButton(
focusNode: saveFocusNode,
onPressed: () async {
if (errorText != null) return;
await onSave(controller.text);
if (dialogContext.mounted) Navigator.pop(dialogContext);
},
child: Text(t.common.save),
),
],
);
},
);
},
).then((_) => saveFocusNode.dispose());
}
+73
View File
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import '../theme/mono_tokens.dart';
import 'app_icon.dart';
class SettingsSectionHeader extends StatelessWidget {
final String title;
const SettingsSectionHeader(this.title, {super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: tokens(context).textMuted,
fontWeight: FontWeight.w600,
),
),
);
}
}
/// A setting with a label + icon row and a full-width SegmentedButton below.
/// Used for settings with 2-4 short options.
class SegmentedSetting<T> extends StatelessWidget {
final IconData icon;
final String title;
final List<ButtonSegment<T>> segments;
final T selected;
final ValueChanged<T> onChanged;
const SegmentedSetting({
super.key,
required this.icon,
required this.title,
required this.segments,
required this.selected,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
AppIcon(icon, fill: 1),
const SizedBox(width: 16),
Text(title, style: Theme.of(context).textTheme.bodyLarge),
],
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: SegmentedButton<T>(
segments: segments,
selected: {selected},
onSelectionChanged: (Set<T> newSelection) {
onChanged(newSelection.first);
},
showSelectedIcon: false,
),
),
],
),
);
}
}