From 2de92e660d5b786c897dc8df7fd8109255685c01 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:22:19 +0100 Subject: [PATCH] feat: adjustable subtitle position close #426 --- .../edde746/plezy/exoplayer/ExoPlayerCore.kt | 24 ++++++++++- .../plezy/exoplayer/ExoPlayerPlugin.kt | 3 +- lib/i18n/de.i18n.json | 3 +- lib/i18n/en.i18n.json | 3 +- lib/i18n/es.i18n.json | 3 +- lib/i18n/fr.i18n.json | 3 +- lib/i18n/it.i18n.json | 3 +- lib/i18n/ko.i18n.json | 3 +- lib/i18n/nl.i18n.json | 3 +- lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_de.g.dart | 4 +- lib/i18n/strings_en.g.dart | 6 ++- lib/i18n/strings_es.g.dart | 4 +- lib/i18n/strings_fr.g.dart | 4 +- lib/i18n/strings_it.g.dart | 4 +- lib/i18n/strings_ko.g.dart | 4 +- lib/i18n/strings_nl.g.dart | 4 +- lib/i18n/strings_sv.g.dart | 4 +- lib/i18n/strings_zh.g.dart | 4 +- lib/i18n/sv.i18n.json | 3 +- lib/i18n/zh.i18n.json | 3 +- lib/mpv/player/player_android.dart | 2 + .../settings/subtitle_styling_screen.dart | 41 +++++++++++++++++++ lib/screens/video_player_screen.dart | 2 + lib/services/settings_service.dart | 11 +++++ 25 files changed, 129 insertions(+), 23 deletions(-) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 524bfe49..456c359c 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -858,7 +858,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { borderSize: Float, borderColor: String, bgColor: String, - bgOpacity: Int + bgOpacity: Int, + subtitlePosition: Int = 100 ) { activity.runOnUiThread { // 1. Non-ASS subtitles: CaptionStyleCompat on SubtitleView @@ -885,6 +886,25 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { val fraction = fontSize / 720f subtitleView?.setFractionalTextSize(fraction) + // Subtitle position: adjust gravity and bottom padding + val clampedPosition = subtitlePosition.coerceIn(0, 100) + val gravity = when { + clampedPosition <= 33 -> Gravity.TOP + clampedPosition <= 66 -> Gravity.CENTER + else -> Gravity.BOTTOM + } + (subtitleView?.layoutParams as? FrameLayout.LayoutParams)?.let { params -> + params.gravity = gravity or Gravity.CENTER_HORIZONTAL + subtitleView?.layoutParams = params + } + // Fine-grained positioning within bottom region via bottom padding fraction + if (clampedPosition > 66) { + val bottomFraction = (100 - clampedPosition) / 100f + subtitleView?.setBottomPaddingFraction(bottomFraction) + } else { + subtitleView?.setBottomPaddingFraction(0f) + } + // 2. ASS subtitles: font scale via libass // MPV default sub-font-size is 38 val defaultSize = 38f @@ -895,7 +915,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { Log.w(TAG, "Failed to set ASS font scale: ${e.message}") } - Log.d(TAG, "setSubtitleStyle: fontSize=$fontSize, textColor=$textColor, borderSize=$borderSize, bgOpacity=$bgOpacity, assScale=$scale") + Log.d(TAG, "setSubtitleStyle: fontSize=$fontSize, textColor=$textColor, borderSize=$borderSize, bgOpacity=$bgOpacity, position=$subtitlePosition, assScale=$scale") } } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index eebd6a1b..947f7d0f 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -419,6 +419,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, val borderColor = call.argument("borderColor") ?: "#000000" val bgColor = call.argument("bgColor") ?: "#000000" val bgOpacity = call.argument("bgOpacity")?.toInt() ?: 0 + val subtitlePosition = call.argument("subtitlePosition")?.toInt() ?: 100 if (usingMpvFallback) { // MPV fallback handles styling via setProperty, no-op here @@ -426,7 +427,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, return } - playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity) + playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition) result.success(null) } diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 51b57488..199c02a2 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Rahmengröße", "borderColor": "Rahmenfarbe", "backgroundOpacity": "Hintergrunddeckkraft", - "backgroundColor": "Hintergrundfarbe" + "backgroundColor": "Hintergrundfarbe", + "position": "Position" }, "mpvConfig": { "title": "MPV-Konfiguration", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index de1b761c..33e3037f 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Border Size", "borderColor": "Border Color", "backgroundOpacity": "Background Opacity", - "backgroundColor": "Background Color" + "backgroundColor": "Background Color", + "position": "Position" }, "mpvConfig": { "title": "MPV Configuration", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 3751319c..8ceeb82e 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Tamaño de Borde", "borderColor": "Color de Borde", "backgroundOpacity": "Opacidad de Fondo", - "backgroundColor": "Color de Fondo" + "backgroundColor": "Color de Fondo", + "position": "Position" }, "mpvConfig": { "title": "Configuración de MPV", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index b1db909b..ea85b3c3 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Taille de la bordure", "borderColor": "Couleur de la bordure", "backgroundOpacity": "Opacité d'arrière-plan", - "backgroundColor": "Couleur d'arrière-plan" + "backgroundColor": "Couleur d'arrière-plan", + "position": "Position" }, "mpvConfig": { "title": "Configuration MPV", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 66d95807..50acca45 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Dimensione bordo", "borderColor": "Colore bordo", "backgroundOpacity": "Opacità sfondo", - "backgroundColor": "Colore sfondo" + "backgroundColor": "Colore sfondo", + "position": "Position" }, "mpvConfig": { "title": "Configurazione MPV", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 79f6a243..e77259df 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -365,7 +365,8 @@ "borderSize": "테두리 크기", "borderColor": "테두리 색상", "backgroundOpacity": "배경 불투명도", - "backgroundColor": "배경색" + "backgroundColor": "배경색", + "position": "Position" }, "mpvConfig": { "title": "MPV 설정", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 4cf9ada8..c4c74807 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Rand grootte", "borderColor": "Randkleur", "backgroundOpacity": "Achtergrond transparantie", - "backgroundColor": "Achtergrondkleur" + "backgroundColor": "Achtergrondkleur", + "position": "Position" }, "mpvConfig": { "title": "MPV-configuratie", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 8445caca..3f2d88ef 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 9 -/// Strings: 5121 (569 per locale) +/// Strings: 5130 (570 per locale) /// -/// Built on 2026-02-08 at 11:03 UTC +/// Built on 2026-02-09 at 17:54 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 00bcdbfc..fccb7592 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingDe implements TranslationsSubtitlingStylingE @override String get borderColor => 'Rahmenfarbe'; @override String get backgroundOpacity => 'Hintergrunddeckkraft'; @override String get backgroundColor => 'Hintergrundfarbe'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsDe { 'subtitlingStyling.borderColor' => 'Rahmenfarbe', 'subtitlingStyling.backgroundOpacity' => 'Hintergrunddeckkraft', 'subtitlingStyling.backgroundColor' => 'Hintergrundfarbe', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'MPV-Konfiguration', 'mpvConfig.description' => 'Erweiterte Videoplayer-Einstellungen', 'mpvConfig.properties' => 'Eigenschaften', @@ -1497,9 +1499,9 @@ extension on TranslationsDe { 'collections.failedToLoadItems' => ({required Object error}) => 'Sammlungselemente konnten nicht geladen werden: ${error}', 'collections.selectCollection' => 'Sammlung auswählen', 'collections.createNewCollection' => 'Neue Sammlung erstellen', - 'collections.collectionName' => 'Sammlungsname', _ => null, } ?? switch (path) { + 'collections.collectionName' => 'Sammlungsname', 'collections.enterCollectionName' => 'Sammlungsnamen eingeben', 'collections.addedToCollection' => 'Zur Sammlung hinzugefügt', 'collections.errorAddingToCollection' => 'Fehler beim Hinzufügen zur Sammlung', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 4e321619..9eeb5a1f 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -1151,6 +1151,9 @@ class TranslationsSubtitlingStylingEn { /// en: 'Background Color' String get backgroundColor => 'Background Color'; + + /// en: 'Position' + String get position => 'Position'; } // Path: mpvConfig @@ -2460,6 +2463,7 @@ extension on Translations { 'subtitlingStyling.borderColor' => 'Border Color', 'subtitlingStyling.backgroundOpacity' => 'Background Opacity', 'subtitlingStyling.backgroundColor' => 'Background Color', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'MPV Configuration', 'mpvConfig.description' => 'Advanced video player settings', 'mpvConfig.properties' => 'Properties', @@ -2641,9 +2645,9 @@ extension on Translations { 'watchTogether.description' => 'Watch content in sync with friends and family', 'watchTogether.createSession' => 'Create Session', 'watchTogether.creating' => 'Creating...', - 'watchTogether.joinSession' => 'Join Session', _ => null, } ?? switch (path) { + 'watchTogether.joinSession' => 'Join Session', 'watchTogether.joining' => 'Joining...', 'watchTogether.controlMode' => 'Control Mode', 'watchTogether.controlModeQuestion' => 'Who can control playback?', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index e80bbf6c..b3f93d63 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingEs implements TranslationsSubtitlingStylingE @override String get borderColor => 'Color de Borde'; @override String get backgroundOpacity => 'Opacidad de Fondo'; @override String get backgroundColor => 'Color de Fondo'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsEs { 'subtitlingStyling.borderColor' => 'Color de Borde', 'subtitlingStyling.backgroundOpacity' => 'Opacidad de Fondo', 'subtitlingStyling.backgroundColor' => 'Color de Fondo', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'Configuración de MPV', 'mpvConfig.description' => 'Ajustes avanzados del reproductor de video', 'mpvConfig.properties' => 'Propiedades', @@ -1497,9 +1499,9 @@ extension on TranslationsEs { 'watchTogether.description' => 'Mira contenido en sincronía con amigos y familiares', 'watchTogether.createSession' => 'Crear Sesión', 'watchTogether.creating' => 'Creando...', - 'watchTogether.joinSession' => 'Unirse a Sesión', _ => null, } ?? switch (path) { + 'watchTogether.joinSession' => 'Unirse a Sesión', 'watchTogether.joining' => 'Uniendo...', 'watchTogether.controlMode' => 'Modo de Control', 'watchTogether.controlModeQuestion' => '¿Quién puede controlar la reproducción?', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 966798a4..f0d26140 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingFr implements TranslationsSubtitlingStylingE @override String get borderColor => 'Couleur de la bordure'; @override String get backgroundOpacity => 'Opacité d\'arrière-plan'; @override String get backgroundColor => 'Couleur d\'arrière-plan'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsFr { 'subtitlingStyling.borderColor' => 'Couleur de la bordure', 'subtitlingStyling.backgroundOpacity' => 'Opacité d\'arrière-plan', 'subtitlingStyling.backgroundColor' => 'Couleur d\'arrière-plan', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'Configuration MPV', 'mpvConfig.description' => 'Paramètres avancés du lecteur vidéo', 'mpvConfig.properties' => 'Propriétés', @@ -1497,9 +1499,9 @@ extension on TranslationsFr { 'watchTogether.description' => 'Regardez du contenu en synchronisation avec vos amis et votre famille', 'watchTogether.createSession' => 'Créer une session', 'watchTogether.creating' => 'Création...', - 'watchTogether.joinSession' => 'Rejoindre la session', _ => null, } ?? switch (path) { + 'watchTogether.joinSession' => 'Rejoindre la session', 'watchTogether.joining' => 'Rejoindre...', 'watchTogether.controlMode' => 'Mode de contrôle', 'watchTogether.controlModeQuestion' => 'Qui peut contrôler la lecture ?', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index aa563644..e2539916 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingIt implements TranslationsSubtitlingStylingE @override String get borderColor => 'Colore bordo'; @override String get backgroundOpacity => 'Opacità sfondo'; @override String get backgroundColor => 'Colore sfondo'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsIt { 'subtitlingStyling.borderColor' => 'Colore bordo', 'subtitlingStyling.backgroundOpacity' => 'Opacità sfondo', 'subtitlingStyling.backgroundColor' => 'Colore sfondo', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'Configurazione MPV', 'mpvConfig.description' => 'Impostazioni avanzate del lettore video', 'mpvConfig.properties' => 'Proprietà', @@ -1497,9 +1499,9 @@ extension on TranslationsIt { 'collections.failedToLoadItems' => ({required Object error}) => 'Impossibile caricare gli elementi della raccolta: ${error}', 'collections.selectCollection' => 'Seleziona raccolta', 'collections.createNewCollection' => 'Crea nuova raccolta', - 'collections.collectionName' => 'Nome raccolta', _ => null, } ?? switch (path) { + 'collections.collectionName' => 'Nome raccolta', 'collections.enterCollectionName' => 'Inserisci nome raccolta', 'collections.addedToCollection' => 'Aggiunto alla raccolta', 'collections.errorAddingToCollection' => 'Errore nell\'aggiunta alla raccolta', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index d83efca9..f7779277 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingKo implements TranslationsSubtitlingStylingE @override String get borderColor => '테두리 색상'; @override String get backgroundOpacity => '배경 불투명도'; @override String get backgroundColor => '배경색'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsKo { 'subtitlingStyling.borderColor' => '테두리 색상', 'subtitlingStyling.backgroundOpacity' => '배경 불투명도', 'subtitlingStyling.backgroundColor' => '배경색', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'MPV 설정', 'mpvConfig.description' => '고급 비디오 플레이어 설정', 'mpvConfig.properties' => '속성', @@ -1497,9 +1499,9 @@ extension on TranslationsKo { 'watchTogether.description' => '친구 및 가족과 콘텐츠를 동시에 시청하세요', 'watchTogether.createSession' => '세션 생성', 'watchTogether.creating' => '생성 중...', - 'watchTogether.joinSession' => '세션 참여', _ => null, } ?? switch (path) { + 'watchTogether.joinSession' => '세션 참여', 'watchTogether.joining' => '참가 중...', 'watchTogether.controlMode' => '제어 모드', 'watchTogether.controlModeQuestion' => '누가 재생을 제어할 수 있나요?', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 72c61e84..c8c844f7 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingNl implements TranslationsSubtitlingStylingE @override String get borderColor => 'Randkleur'; @override String get backgroundOpacity => 'Achtergrond transparantie'; @override String get backgroundColor => 'Achtergrondkleur'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsNl { 'subtitlingStyling.borderColor' => 'Randkleur', 'subtitlingStyling.backgroundOpacity' => 'Achtergrond transparantie', 'subtitlingStyling.backgroundColor' => 'Achtergrondkleur', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'MPV-configuratie', 'mpvConfig.description' => 'Geavanceerde videospeler-instellingen', 'mpvConfig.properties' => 'Eigenschappen', @@ -1497,9 +1499,9 @@ extension on TranslationsNl { 'collections.failedToLoadItems' => ({required Object error}) => 'Collectie-items laden mislukt: ${error}', 'collections.selectCollection' => 'Selecteer collectie', 'collections.createNewCollection' => 'Nieuwe collectie maken', - 'collections.collectionName' => 'Collectienaam', _ => null, } ?? switch (path) { + 'collections.collectionName' => 'Collectienaam', 'collections.enterCollectionName' => 'Voer collectienaam in', 'collections.addedToCollection' => 'Toegevoegd aan collectie', 'collections.errorAddingToCollection' => 'Fout bij toevoegen aan collectie', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index af03f7a3..c4e37017 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingSv implements TranslationsSubtitlingStylingE @override String get borderColor => 'Kantfärg'; @override String get backgroundOpacity => 'Bakgrundsopacitet'; @override String get backgroundColor => 'Bakgrundsfärg'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsSv { 'subtitlingStyling.borderColor' => 'Kantfärg', 'subtitlingStyling.backgroundOpacity' => 'Bakgrundsopacitet', 'subtitlingStyling.backgroundColor' => 'Bakgrundsfärg', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'MPV-konfiguration', 'mpvConfig.description' => 'Avancerade videospelares inställningar', 'mpvConfig.properties' => 'Egenskaper', @@ -1497,9 +1499,9 @@ extension on TranslationsSv { 'collections.failedToLoadItems' => ({required Object error}) => 'Det gick inte att läsa in samlingsobjekt: ${error}', 'collections.selectCollection' => 'Välj samling', 'collections.createNewCollection' => 'Skapa ny samling', - 'collections.collectionName' => 'Samlingsnamn', _ => null, } ?? switch (path) { + 'collections.collectionName' => 'Samlingsnamn', 'collections.enterCollectionName' => 'Ange samlingsnamn', 'collections.addedToCollection' => 'Tillagd i samling', 'collections.errorAddingToCollection' => 'Fel vid tillägg i samling', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 1cbddee6..9313eebc 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -532,6 +532,7 @@ class _TranslationsSubtitlingStylingZh implements TranslationsSubtitlingStylingE @override String get borderColor => '边框颜色'; @override String get backgroundOpacity => '背景不透明度'; @override String get backgroundColor => '背景颜色'; + @override String get position => 'Position'; } // Path: mpvConfig @@ -1316,6 +1317,7 @@ extension on TranslationsZh { 'subtitlingStyling.borderColor' => '边框颜色', 'subtitlingStyling.backgroundOpacity' => '背景不透明度', 'subtitlingStyling.backgroundColor' => '背景颜色', + 'subtitlingStyling.position' => 'Position', 'mpvConfig.title' => 'MPV 配置', 'mpvConfig.description' => '高级视频播放器设置', 'mpvConfig.properties' => '属性', @@ -1497,9 +1499,9 @@ extension on TranslationsZh { 'collections.failedToLoadItems' => ({required Object error}) => '加载合集项目失败:${error}', 'collections.selectCollection' => '选择合集', 'collections.createNewCollection' => '创建新合集', - 'collections.collectionName' => '合集名称', _ => null, } ?? switch (path) { + 'collections.collectionName' => '合集名称', 'collections.enterCollectionName' => '输入合集名称', 'collections.addedToCollection' => '已添加到合集', 'collections.errorAddingToCollection' => '添加到合集失败', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 07eca9fd..d4be883d 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -365,7 +365,8 @@ "borderSize": "Kantstorlek", "borderColor": "Kantfärg", "backgroundOpacity": "Bakgrundsopacitet", - "backgroundColor": "Bakgrundsfärg" + "backgroundColor": "Bakgrundsfärg", + "position": "Position" }, "mpvConfig": { "title": "MPV-konfiguration", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 5ccec267..3a16bef5 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -365,7 +365,8 @@ "borderSize": "边框大小", "borderColor": "边框颜色", "backgroundOpacity": "背景不透明度", - "backgroundColor": "背景颜色" + "backgroundColor": "背景颜色", + "position": "Position" }, "mpvConfig": { "title": "MPV 配置", diff --git a/lib/mpv/player/player_android.dart b/lib/mpv/player/player_android.dart index be63d810..f25fc605 100644 --- a/lib/mpv/player/player_android.dart +++ b/lib/mpv/player/player_android.dart @@ -276,6 +276,7 @@ class PlayerAndroid extends PlayerBase { required String borderColor, required String bgColor, required int bgOpacity, + int subtitlePosition = 100, }) async { checkDisposed(); if (!initialized) return; @@ -286,6 +287,7 @@ class PlayerAndroid extends PlayerBase { 'borderColor': borderColor, 'bgColor': bgColor, 'bgOpacity': bgOpacity, + 'subtitlePosition': subtitlePosition, }); } diff --git a/lib/screens/settings/subtitle_styling_screen.dart b/lib/screens/settings/subtitle_styling_screen.dart index c96002cc..adec3b9d 100644 --- a/lib/screens/settings/subtitle_styling_screen.dart +++ b/lib/screens/settings/subtitle_styling_screen.dart @@ -121,6 +121,7 @@ class _SubtitleStylingScreenState extends State { String _borderColor = '#000000'; String _backgroundColor = '#000000'; int _backgroundOpacity = 0; + int _subtitlePosition = 100; @override void initState() { @@ -138,6 +139,7 @@ class _SubtitleStylingScreenState extends State { _borderColor = _settingsService.getSubtitleBorderColor(); _backgroundColor = _settingsService.getSubtitleBackgroundColor(); _backgroundOpacity = _settingsService.getSubtitleBackgroundOpacity(); + _subtitlePosition = _settingsService.getSubtitlePosition(); _isLoading = false; }); } @@ -340,6 +342,45 @@ class _SubtitleStylingScreenState extends State { }, ), const Divider(), + // Subtitle Position Slider + if (PlatformDetector.isTV()) + ListTile( + title: Text(t.subtitlingStyling.position), + trailing: Text(_subtitlePosition == 0 ? 'Top' : _subtitlePosition == 100 ? 'Bottom' : '$_subtitlePosition%'), + onTap: () => _showTvSpinnerDialog( + title: t.subtitlingStyling.position, + currentValue: _subtitlePosition, + min: 0, + max: 100, + step: 5, + onSave: (value) { + setState(() => _subtitlePosition = value); + _settingsService.setSubtitlePosition(value); + }, + ), + ) + else + _StylingSliderSection( + label: t.subtitlingStyling.position, + value: _subtitlePosition, + min: 0, + max: 100, + divisions: 20, + valueFormatter: (value) { + if (value == 0) return 'Top'; + if (value == 100) return 'Bottom'; + return '$value%'; + }, + onChanged: (value) { + setState(() { + _subtitlePosition = value.toInt(); + }); + }, + onChangeEnd: (value) { + _settingsService.setSubtitlePosition(_subtitlePosition); + }, + ), + const Divider(), // Text Color _ColorSettingTile( label: t.subtitlingStyling.textColor, diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 1ccd18ce..365c2c5b 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -343,6 +343,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin '#${bgOpacity.toRadixString(16).padLeft(2, '0').toUpperCase()}$bgColor', ); await player!.setProperty('sub-ass-override', 'no'); + await player!.setProperty('sub-pos', settingsService.getSubtitlePosition().toString()); // Platform-specific settings if (Platform.isIOS) { @@ -858,6 +859,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin borderColor: settingsService.getSubtitleBorderColor(), bgColor: settingsService.getSubtitleBackgroundColor(), bgOpacity: settingsService.getSubtitleBackgroundOpacity(), + subtitlePosition: settingsService.getSubtitlePosition(), ); } diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 3fd2b561..3ee69dd9 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -46,6 +46,7 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keySubtitleBorderColor = 'subtitle_border_color'; static const String _keySubtitleBackgroundColor = 'subtitle_background_color'; static const String _keySubtitleBackgroundOpacity = 'subtitle_background_opacity'; + static const String _keySubtitlePosition = 'subtitle_position'; static const String _keyAppLocale = 'app_locale'; static const String _keyRememberTrackSelections = 'remember_track_selections'; static const String _keyClickVideoTogglesPlayback = 'click_video_toggles_playback'; @@ -345,6 +346,15 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getInt(_keySubtitleBackgroundOpacity) ?? 0; } + // Subtitle Position (0 = top, 100 = bottom, default 100) + Future setSubtitlePosition(int position) async { + await prefs.setInt(_keySubtitlePosition, position.clamp(0, 100)); + } + + int getSubtitlePosition() { + return prefs.getInt(_keySubtitlePosition) ?? 100; // Default: bottom + } + // Keyboard Shortcuts (Legacy String-based) Map getDefaultKeyboardShortcuts() { return { @@ -1068,6 +1078,7 @@ class SettingsService extends BaseSharedPreferencesService { prefs.remove(_keySubtitleBorderColor), prefs.remove(_keySubtitleBackgroundColor), prefs.remove(_keySubtitleBackgroundOpacity), + prefs.remove(_keySubtitlePosition), prefs.remove(_keyAppLocale), prefs.remove(_keyRememberTrackSelections), prefs.remove(_keyCustomDownloadPath),