feat(subtitles): optionally anchor text subtitles to the screen bottom
Adds an "Anchor to Screen" toggle under Subtitle Styling (Android + ExoPlayer only, default off). When enabled, the text SubtitleView is sized to the full container instead of the letterboxed video rect, so SRT/VTT/mov_text cues render in the black bars below widescreen video and font size and the position setting become relative to the physical screen height. Bitmap (PGS/VOB) and ASS/libass rendering are unchanged; mpv already places plaintext subtitles in the margins by default. close #1730
This commit is contained in:
@@ -215,6 +215,7 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
@Volatile private var assVideoLatencyFrames = 0
|
@Volatile private var assVideoLatencyFrames = 0
|
||||||
private var subtitlePositionPercent: Int = 100
|
private var subtitlePositionPercent: Int = 100
|
||||||
private var subtitleFontSize: Float = 55f
|
private var subtitleFontSize: Float = 55f
|
||||||
|
private var subtitleAnchorToScreen: Boolean = false
|
||||||
private var lastSubtitleCues: List<Cue> = emptyList()
|
private var lastSubtitleCues: List<Cue> = emptyList()
|
||||||
|
|
||||||
// Tracks whether a text track was selected on the previous onTracksChanged so we
|
// Tracks whether a text track was selected on the previous onTracksChanged so we
|
||||||
@@ -1757,7 +1758,8 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
videoHeight,
|
videoHeight,
|
||||||
pixelRatio,
|
pixelRatio,
|
||||||
resizeMode,
|
resizeMode,
|
||||||
videoZoomScale
|
videoZoomScale,
|
||||||
|
subtitleAnchorToScreen
|
||||||
)
|
)
|
||||||
val bitmapDimensions = SubtitleViewLayout.bitmapDimensions(
|
val bitmapDimensions = SubtitleViewLayout.bitmapDimensions(
|
||||||
containerWidth,
|
containerWidth,
|
||||||
@@ -3911,7 +3913,8 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
bgOpacity: Int,
|
bgOpacity: Int,
|
||||||
subtitlePosition: Int = 100,
|
subtitlePosition: Int = 100,
|
||||||
bold: Boolean = false,
|
bold: Boolean = false,
|
||||||
italic: Boolean = false
|
italic: Boolean = false,
|
||||||
|
anchorToScreen: Boolean = false
|
||||||
) {
|
) {
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
// 1. Non-ASS subtitles: CaptionStyleCompat on SubtitleView
|
// 1. Non-ASS subtitles: CaptionStyleCompat on SubtitleView
|
||||||
@@ -3959,6 +3962,18 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
subtitlePositionPercent = clampedPosition
|
subtitlePositionPercent = clampedPosition
|
||||||
subtitleFontSize = fontSize
|
subtitleFontSize = fontSize
|
||||||
|
|
||||||
|
// Anchor-to-screen (#1730): resize the text SubtitleView to the full
|
||||||
|
// container so default-placed cues land in the letterbox bars.
|
||||||
|
val anchorChanged = subtitleAnchorToScreen != anchorToScreen
|
||||||
|
subtitleAnchorToScreen = anchorToScreen
|
||||||
|
if (anchorChanged) {
|
||||||
|
lastVideoSize?.let { vs ->
|
||||||
|
if (vs.width > 0 && vs.height > 0) {
|
||||||
|
updateSubtitleViewSize(vs.width, vs.height, vs.pixelWidthHeightRatio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cue-level positioning handles default VTT/SRT placement, whose line
|
// Cue-level positioning handles default VTT/SRT placement, whose line
|
||||||
// numbers bypass SubtitleView bottom padding. Authored VTT line positions
|
// numbers bypass SubtitleView bottom padding. Authored VTT line positions
|
||||||
// are preserved in applySubtitlePosition().
|
// are preserved in applySubtitlePosition().
|
||||||
@@ -3980,7 +3995,7 @@ class ExoPlayerCore(private val activity: Activity) :
|
|||||||
Log.w(TAG, "Failed to set ASS font scale: ${e.message}")
|
Log.w(TAG, "Failed to set ASS font scale: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "setSubtitleStyle: fontSize=$fontSize, textColor=$textColor, borderSize=$borderSize, bgOpacity=$bgOpacity, position=$subtitlePosition, bold=$bold, italic=$italic, assScale=$scale")
|
Log.d(TAG, "setSubtitleStyle: fontSize=$fontSize, textColor=$textColor, borderSize=$borderSize, bgOpacity=$bgOpacity, position=$subtitlePosition, bold=$bold, italic=$italic, anchorToScreen=$anchorToScreen, assScale=$scale")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1069,6 +1069,7 @@ class ExoPlayerPlugin :
|
|||||||
val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100
|
val subtitlePosition = call.argument<Number>("subtitlePosition")?.toInt() ?: 100
|
||||||
val bold = call.argument<Boolean>("bold") ?: false
|
val bold = call.argument<Boolean>("bold") ?: false
|
||||||
val italic = call.argument<Boolean>("italic") ?: false
|
val italic = call.argument<Boolean>("italic") ?: false
|
||||||
|
val anchorToScreen = call.argument<Boolean>("anchorToScreen") ?: false
|
||||||
|
|
||||||
if (usingMpvFallback) {
|
if (usingMpvFallback) {
|
||||||
// MPV fallback handles styling via setProperty, no-op here
|
// MPV fallback handles styling via setProperty, no-op here
|
||||||
@@ -1076,7 +1077,7 @@ class ExoPlayerPlugin :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic)
|
playerCore?.setSubtitleStyle(fontSize, textColor, borderSize, borderColor, bgColor, bgOpacity, subtitlePosition, bold, italic, anchorToScreen)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ internal object SubtitleViewLayout {
|
|||||||
videoHeight: Int,
|
videoHeight: Int,
|
||||||
pixelRatio: Float,
|
pixelRatio: Float,
|
||||||
resizeMode: Int,
|
resizeMode: Int,
|
||||||
zoomScale: Float
|
zoomScale: Float,
|
||||||
|
anchorToScreen: Boolean = false
|
||||||
): SubtitleViewDimensions? {
|
): SubtitleViewDimensions? {
|
||||||
val videoAspect = videoAspect(videoWidth, videoHeight, pixelRatio) ?: return null
|
val videoAspect = videoAspect(videoWidth, videoHeight, pixelRatio) ?: return null
|
||||||
if (containerWidth <= 0 || containerHeight <= 0) return null
|
if (containerWidth <= 0 || containerHeight <= 0) return null
|
||||||
|
|
||||||
if (resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT) {
|
// Anchor-to-screen (#1730): size the text view to the full container so
|
||||||
|
// media3's fractional text size and bottom-anchored cue placement compute
|
||||||
|
// against the physical screen, letting subtitles render in the letterbox
|
||||||
|
// bars instead of inside the video rect. Same geometry the non-FIT modes
|
||||||
|
// below already use.
|
||||||
|
if (anchorToScreen || resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT) {
|
||||||
return SubtitleViewDimensions(containerWidth, containerHeight)
|
return SubtitleViewDimensions(containerWidth, containerHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,49 @@ class SubtitleViewLayoutTest {
|
|||||||
assertAspectCloseTo16By9(bitmap!!)
|
assertAspectCloseTo16By9(bitmap!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anchorToScreenSizesTextToContainerAndKeepsBitmapOnVideoRect() {
|
||||||
|
val text = textDimensions(resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT, anchorToScreen = true)
|
||||||
|
val bitmap = bitmapDimensions(resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT)
|
||||||
|
|
||||||
|
assertEquals(SubtitleViewDimensions(2424, 1080), text)
|
||||||
|
assertEquals(SubtitleViewDimensions(1920, 1080), bitmap)
|
||||||
|
assertAspectCloseTo16By9(bitmap!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anchorToScreenReachesBelowLetterboxedWideVideo() {
|
||||||
|
// 2.37:1 video on a 16:9 screen — the #1730 use case. Without the anchor
|
||||||
|
// the text view stops at the bottom letterbox bar; with it the view spans
|
||||||
|
// the physical screen height.
|
||||||
|
val unanchored = SubtitleViewLayout.textDimensions(
|
||||||
|
containerWidth = 1920,
|
||||||
|
containerHeight = 1080,
|
||||||
|
videoWidth = 2560,
|
||||||
|
videoHeight = 1080,
|
||||||
|
pixelRatio = 1f,
|
||||||
|
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT,
|
||||||
|
zoomScale = 1f
|
||||||
|
)
|
||||||
|
val anchored = SubtitleViewLayout.textDimensions(
|
||||||
|
containerWidth = 1920,
|
||||||
|
containerHeight = 1080,
|
||||||
|
videoWidth = 2560,
|
||||||
|
videoHeight = 1080,
|
||||||
|
pixelRatio = 1f,
|
||||||
|
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT,
|
||||||
|
zoomScale = 1f,
|
||||||
|
anchorToScreen = true
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(SubtitleViewDimensions(1920, 810), unanchored)
|
||||||
|
assertEquals(SubtitleViewDimensions(1920, 1080), anchored)
|
||||||
|
}
|
||||||
|
|
||||||
private fun textDimensions(
|
private fun textDimensions(
|
||||||
resizeMode: Int,
|
resizeMode: Int,
|
||||||
zoomScale: Float = 1f
|
zoomScale: Float = 1f,
|
||||||
|
anchorToScreen: Boolean = false
|
||||||
): SubtitleViewDimensions? = SubtitleViewLayout.textDimensions(
|
): SubtitleViewDimensions? = SubtitleViewLayout.textDimensions(
|
||||||
containerWidth = 2424,
|
containerWidth = 2424,
|
||||||
containerHeight = 1080,
|
containerHeight = 1080,
|
||||||
@@ -55,7 +95,8 @@ class SubtitleViewLayoutTest {
|
|||||||
videoHeight = 1080,
|
videoHeight = 1080,
|
||||||
pixelRatio = 1f,
|
pixelRatio = 1f,
|
||||||
resizeMode = resizeMode,
|
resizeMode = resizeMode,
|
||||||
zoomScale = zoomScale
|
zoomScale = zoomScale,
|
||||||
|
anchorToScreen = anchorToScreen
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun bitmapDimensions(
|
private fun bitmapDimensions(
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Formatlaşdırmanı sil",
|
"overrideStrip": "Formatlaşdırmanı sil",
|
||||||
"positionTop": "Yuxarı",
|
"positionTop": "Yuxarı",
|
||||||
"positionBottom": "Aşağı",
|
"positionBottom": "Aşağı",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Qalın",
|
"bold": "Qalın",
|
||||||
"italic": "Kursiv",
|
"italic": "Kursiv",
|
||||||
"renderResolution": "Emal imkanı (Resolution)",
|
"renderResolution": "Emal imkanı (Resolution)",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Премахване на стиловете",
|
"overrideStrip": "Премахване на стиловете",
|
||||||
"positionTop": "Горе",
|
"positionTop": "Горе",
|
||||||
"positionBottom": "Долу",
|
"positionBottom": "Долу",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Получер",
|
"bold": "Получер",
|
||||||
"italic": "Курсив",
|
"italic": "Курсив",
|
||||||
"renderResolution": "Резолюция на изобразяване",
|
"renderResolution": "Резолюция на изобразяване",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Fjern formatering",
|
"overrideStrip": "Fjern formatering",
|
||||||
"positionTop": "Øverst",
|
"positionTop": "Øverst",
|
||||||
"positionBottom": "Nederst",
|
"positionBottom": "Nederst",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Fed",
|
"bold": "Fed",
|
||||||
"italic": "Kursiv",
|
"italic": "Kursiv",
|
||||||
"renderResolution": "Gengivelsesopløsning",
|
"renderResolution": "Gengivelsesopløsning",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Formatierung entfernen",
|
"overrideStrip": "Formatierung entfernen",
|
||||||
"positionTop": "Oben",
|
"positionTop": "Oben",
|
||||||
"positionBottom": "Unten",
|
"positionBottom": "Unten",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Fett",
|
"bold": "Fett",
|
||||||
"italic": "Kursiv",
|
"italic": "Kursiv",
|
||||||
"renderResolution": "Render-Auflösung",
|
"renderResolution": "Render-Auflösung",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Remove styling",
|
"overrideStrip": "Remove styling",
|
||||||
"positionTop": "Top",
|
"positionTop": "Top",
|
||||||
"positionBottom": "Bottom",
|
"positionBottom": "Bottom",
|
||||||
|
"anchorToScreen": "Anchor to Screen",
|
||||||
|
"anchorToScreenDescription": "Show text subtitles in the black bars below widescreen video",
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
"italic": "Italic",
|
"italic": "Italic",
|
||||||
"renderResolution": "Render Resolution",
|
"renderResolution": "Render Resolution",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Quitar estilos",
|
"overrideStrip": "Quitar estilos",
|
||||||
"positionTop": "Arriba",
|
"positionTop": "Arriba",
|
||||||
"positionBottom": "Abajo",
|
"positionBottom": "Abajo",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Negrita",
|
"bold": "Negrita",
|
||||||
"italic": "Cursiva",
|
"italic": "Cursiva",
|
||||||
"renderResolution": "Resolución de renderizado",
|
"renderResolution": "Resolución de renderizado",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Supprimer le style",
|
"overrideStrip": "Supprimer le style",
|
||||||
"positionTop": "Haut",
|
"positionTop": "Haut",
|
||||||
"positionBottom": "Bas",
|
"positionBottom": "Bas",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Gras",
|
"bold": "Gras",
|
||||||
"italic": "Italique",
|
"italic": "Italique",
|
||||||
"renderResolution": "Résolution de rendu",
|
"renderResolution": "Résolution de rendu",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Stílus eltávolítása",
|
"overrideStrip": "Stílus eltávolítása",
|
||||||
"positionTop": "Fent",
|
"positionTop": "Fent",
|
||||||
"positionBottom": "Lent",
|
"positionBottom": "Lent",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Félkövér",
|
"bold": "Félkövér",
|
||||||
"italic": "Dőlt",
|
"italic": "Dőlt",
|
||||||
"renderResolution": "Renderelési felbontás",
|
"renderResolution": "Renderelési felbontás",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Rimuovi stile",
|
"overrideStrip": "Rimuovi stile",
|
||||||
"positionTop": "In alto",
|
"positionTop": "In alto",
|
||||||
"positionBottom": "In basso",
|
"positionBottom": "In basso",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Grassetto",
|
"bold": "Grassetto",
|
||||||
"italic": "Corsivo",
|
"italic": "Corsivo",
|
||||||
"renderResolution": "Risoluzione di rendering",
|
"renderResolution": "Risoluzione di rendering",
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"overrideStrip": "スタイルを削除",
|
"overrideStrip": "スタイルを削除",
|
||||||
"positionTop": "上",
|
"positionTop": "上",
|
||||||
"positionBottom": "下",
|
"positionBottom": "下",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "太字",
|
"bold": "太字",
|
||||||
"italic": "斜体",
|
"italic": "斜体",
|
||||||
"renderResolution": "レンダリング解像度",
|
"renderResolution": "レンダリング解像度",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Форматтауды жою",
|
"overrideStrip": "Форматтауды жою",
|
||||||
"positionTop": "Жоғары",
|
"positionTop": "Жоғары",
|
||||||
"positionBottom": "Төмен",
|
"positionBottom": "Төмен",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Қалың",
|
"bold": "Қалың",
|
||||||
"italic": "Көлбеу",
|
"italic": "Көлбеу",
|
||||||
"renderResolution": "Рендеринг ажыратымдылығы",
|
"renderResolution": "Рендеринг ажыратымдылығы",
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"overrideStrip": "스타일 제거",
|
"overrideStrip": "스타일 제거",
|
||||||
"positionTop": "위",
|
"positionTop": "위",
|
||||||
"positionBottom": "아래",
|
"positionBottom": "아래",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "굵게",
|
"bold": "굵게",
|
||||||
"italic": "기울임꼴",
|
"italic": "기울임꼴",
|
||||||
"renderResolution": "렌더링 해상도",
|
"renderResolution": "렌더링 해상도",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Fjern formatering",
|
"overrideStrip": "Fjern formatering",
|
||||||
"positionTop": "Øverst",
|
"positionTop": "Øverst",
|
||||||
"positionBottom": "Nederst",
|
"positionBottom": "Nederst",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Fet",
|
"bold": "Fet",
|
||||||
"italic": "Kursiv",
|
"italic": "Kursiv",
|
||||||
"renderResolution": "Gjengivelsesoppløsning",
|
"renderResolution": "Gjengivelsesoppløsning",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Opmaak verwijderen",
|
"overrideStrip": "Opmaak verwijderen",
|
||||||
"positionTop": "Bovenaan",
|
"positionTop": "Bovenaan",
|
||||||
"positionBottom": "Onderaan",
|
"positionBottom": "Onderaan",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Vet",
|
"bold": "Vet",
|
||||||
"italic": "Cursief",
|
"italic": "Cursief",
|
||||||
"renderResolution": "Renderresolutie",
|
"renderResolution": "Renderresolutie",
|
||||||
|
|||||||
@@ -752,6 +752,8 @@
|
|||||||
"overrideStrip": "Usuń style",
|
"overrideStrip": "Usuń style",
|
||||||
"positionTop": "Góra",
|
"positionTop": "Góra",
|
||||||
"positionBottom": "Dół",
|
"positionBottom": "Dół",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Pogrubienie",
|
"bold": "Pogrubienie",
|
||||||
"italic": "Kursywa",
|
"italic": "Kursywa",
|
||||||
"renderResolution": "Rozdzielczość renderowania",
|
"renderResolution": "Rozdzielczość renderowania",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Remover estilo",
|
"overrideStrip": "Remover estilo",
|
||||||
"positionTop": "Superior",
|
"positionTop": "Superior",
|
||||||
"positionBottom": "Inferior",
|
"positionBottom": "Inferior",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Negrito",
|
"bold": "Negrito",
|
||||||
"italic": "Itálico",
|
"italic": "Itálico",
|
||||||
"renderResolution": "Resolução de renderização",
|
"renderResolution": "Resolução de renderização",
|
||||||
|
|||||||
@@ -752,6 +752,8 @@
|
|||||||
"overrideStrip": "Удалить стили",
|
"overrideStrip": "Удалить стили",
|
||||||
"positionTop": "Сверху",
|
"positionTop": "Сверху",
|
||||||
"positionBottom": "Снизу",
|
"positionBottom": "Снизу",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Жирный",
|
"bold": "Жирный",
|
||||||
"italic": "Курсив",
|
"italic": "Курсив",
|
||||||
"renderResolution": "Разрешение отрисовки",
|
"renderResolution": "Разрешение отрисовки",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 22
|
/// Locales: 22
|
||||||
/// Strings: 38352 (1743 per locale)
|
/// Strings: 38354 (1743 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -2212,6 +2212,12 @@ class Translations$subtitlingStyling$en {
|
|||||||
/// en: 'Bottom'
|
/// en: 'Bottom'
|
||||||
String get positionBottom => 'Bottom';
|
String get positionBottom => 'Bottom';
|
||||||
|
|
||||||
|
/// en: 'Anchor to Screen'
|
||||||
|
String get anchorToScreen => 'Anchor to Screen';
|
||||||
|
|
||||||
|
/// en: 'Show text subtitles in the black bars below widescreen video'
|
||||||
|
String get anchorToScreenDescription => 'Show text subtitles in the black bars below widescreen video';
|
||||||
|
|
||||||
/// en: 'Bold'
|
/// en: 'Bold'
|
||||||
String get bold => 'Bold';
|
String get bold => 'Bold';
|
||||||
|
|
||||||
@@ -6762,6 +6768,8 @@ extension on Translations {
|
|||||||
'subtitlingStyling.overrideStrip' => 'Remove styling',
|
'subtitlingStyling.overrideStrip' => 'Remove styling',
|
||||||
'subtitlingStyling.positionTop' => 'Top',
|
'subtitlingStyling.positionTop' => 'Top',
|
||||||
'subtitlingStyling.positionBottom' => 'Bottom',
|
'subtitlingStyling.positionBottom' => 'Bottom',
|
||||||
|
'subtitlingStyling.anchorToScreen' => 'Anchor to Screen',
|
||||||
|
'subtitlingStyling.anchorToScreenDescription' => 'Show text subtitles in the black bars below widescreen video',
|
||||||
'subtitlingStyling.bold' => 'Bold',
|
'subtitlingStyling.bold' => 'Bold',
|
||||||
'subtitlingStyling.italic' => 'Italic',
|
'subtitlingStyling.italic' => 'Italic',
|
||||||
'subtitlingStyling.renderResolution' => 'Render Resolution',
|
'subtitlingStyling.renderResolution' => 'Render Resolution',
|
||||||
@@ -7091,10 +7099,10 @@ extension on Translations {
|
|||||||
'explore.stats.favorited' => ({required Object n}) => '${n} favorites',
|
'explore.stats.favorited' => ({required Object n}) => '${n} favorites',
|
||||||
'explore.stats.dropRate' => ({required Object percent}) => '${percent} dropped it',
|
'explore.stats.dropRate' => ({required Object percent}) => '${percent} dropped it',
|
||||||
'explore.stats.comments' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} comment', other: '${n} comments', ),
|
'explore.stats.comments' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} comment', other: '${n} comments', ),
|
||||||
'explore.stats.votes' => ({required Object n}) => '${n} votes',
|
|
||||||
'explore.stats.watching' => ({required Object n}) => '${n} watching it',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.stats.votes' => ({required Object n}) => '${n} votes',
|
||||||
|
'explore.stats.watching' => ({required Object n}) => '${n} watching it',
|
||||||
'explore.stats.completed' => ({required Object n}) => '${n} completed',
|
'explore.stats.completed' => ({required Object n}) => '${n} completed',
|
||||||
'explore.stats.onHold' => ({required Object n}) => '${n} on hold',
|
'explore.stats.onHold' => ({required Object n}) => '${n} on hold',
|
||||||
'explore.stats.dropped' => ({required Object n}) => '${n} dropped',
|
'explore.stats.dropped' => ({required Object n}) => '${n} dropped',
|
||||||
@@ -7605,10 +7613,10 @@ extension on Translations {
|
|||||||
'externalPlayer.useExternalPlayerDescription' => 'Open videos in another app',
|
'externalPlayer.useExternalPlayerDescription' => 'Open videos in another app',
|
||||||
'externalPlayer.selectPlayer' => 'Select Player',
|
'externalPlayer.selectPlayer' => 'Select Player',
|
||||||
'externalPlayer.customPlayers' => 'Custom Players',
|
'externalPlayer.customPlayers' => 'Custom Players',
|
||||||
'externalPlayer.systemDefault' => 'System Default',
|
|
||||||
'externalPlayer.addCustomPlayer' => 'Add Custom Player',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'externalPlayer.systemDefault' => 'System Default',
|
||||||
|
'externalPlayer.addCustomPlayer' => 'Add Custom Player',
|
||||||
'externalPlayer.playerName' => 'Player Name',
|
'externalPlayer.playerName' => 'Player Name',
|
||||||
'externalPlayer.playerNameHint' => 'My Player',
|
'externalPlayer.playerNameHint' => 'My Player',
|
||||||
'externalPlayer.playerCommand' => 'Command',
|
'externalPlayer.playerCommand' => 'Command',
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Ta bort formatering",
|
"overrideStrip": "Ta bort formatering",
|
||||||
"positionTop": "Överst",
|
"positionTop": "Överst",
|
||||||
"positionBottom": "Nederst",
|
"positionBottom": "Nederst",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Fet",
|
"bold": "Fet",
|
||||||
"italic": "Kursiv",
|
"italic": "Kursiv",
|
||||||
"renderResolution": "Renderingsupplösning",
|
"renderResolution": "Renderingsupplösning",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Biçimlendirmeyi kaldır",
|
"overrideStrip": "Biçimlendirmeyi kaldır",
|
||||||
"positionTop": "Üst",
|
"positionTop": "Üst",
|
||||||
"positionBottom": "Alt",
|
"positionBottom": "Alt",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Kalın",
|
"bold": "Kalın",
|
||||||
"italic": "İtalik",
|
"italic": "İtalik",
|
||||||
"renderResolution": "İşleme Çözünürlüğü",
|
"renderResolution": "İşleme Çözünürlüğü",
|
||||||
|
|||||||
@@ -744,6 +744,8 @@
|
|||||||
"overrideStrip": "Formatlashni olib tashlash",
|
"overrideStrip": "Formatlashni olib tashlash",
|
||||||
"positionTop": "Yuqori",
|
"positionTop": "Yuqori",
|
||||||
"positionBottom": "Pastki",
|
"positionBottom": "Pastki",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "Qalin",
|
"bold": "Qalin",
|
||||||
"italic": "Qiya",
|
"italic": "Qiya",
|
||||||
"renderResolution": "Renderlash oʻlchamlari",
|
"renderResolution": "Renderlash oʻlchamlari",
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"overrideStrip": "移除樣式",
|
"overrideStrip": "移除樣式",
|
||||||
"positionTop": "頂部",
|
"positionTop": "頂部",
|
||||||
"positionBottom": "底部",
|
"positionBottom": "底部",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "粗體",
|
"bold": "粗體",
|
||||||
"italic": "斜體",
|
"italic": "斜體",
|
||||||
"renderResolution": "渲染解析度",
|
"renderResolution": "渲染解析度",
|
||||||
|
|||||||
@@ -740,6 +740,8 @@
|
|||||||
"overrideStrip": "移除样式",
|
"overrideStrip": "移除样式",
|
||||||
"positionTop": "顶部",
|
"positionTop": "顶部",
|
||||||
"positionBottom": "底部",
|
"positionBottom": "底部",
|
||||||
|
"anchorToScreen": "",
|
||||||
|
"anchorToScreenDescription": "",
|
||||||
"bold": "粗体",
|
"bold": "粗体",
|
||||||
"italic": "斜体",
|
"italic": "斜体",
|
||||||
"renderResolution": "渲染分辨率",
|
"renderResolution": "渲染分辨率",
|
||||||
|
|||||||
@@ -542,6 +542,7 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
int subtitlePosition = 100,
|
int subtitlePosition = 100,
|
||||||
bool bold = false,
|
bool bold = false,
|
||||||
bool italic = false,
|
bool italic = false,
|
||||||
|
bool anchorToScreen = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (disposed || !initialized) return;
|
if (disposed || !initialized) return;
|
||||||
await invoke('setSubtitleStyle', {
|
await invoke('setSubtitleStyle', {
|
||||||
@@ -554,6 +555,7 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
'subtitlePosition': subtitlePosition,
|
'subtitlePosition': subtitlePosition,
|
||||||
'bold': bold,
|
'bold': bold,
|
||||||
'italic': italic,
|
'italic': italic,
|
||||||
|
'anchorToScreen': anchorToScreen,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ abstract class Player {
|
|||||||
int subtitlePosition = 100,
|
int subtitlePosition = 100,
|
||||||
bool bold = false,
|
bool bold = false,
|
||||||
bool italic = false,
|
bool italic = false,
|
||||||
|
bool anchorToScreen = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Apply the box-fit mode to the native video layer
|
/// Apply the box-fit mode to the native video layer
|
||||||
|
|||||||
@@ -1097,6 +1097,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
|||||||
int subtitlePosition = 100,
|
int subtitlePosition = 100,
|
||||||
bool bold = false,
|
bool bold = false,
|
||||||
bool italic = false,
|
bool italic = false,
|
||||||
|
bool anchorToScreen = false,
|
||||||
}) async {}
|
}) async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class SubtitleStylingScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Anchor-to-screen is an ExoPlayer text-subtitle knob; mpv already places
|
||||||
|
// plaintext subtitles in the letterbox margins by default.
|
||||||
|
final exoActive = Platform.isAndroid && SettingsService.instance.read(SettingsService.useExoPlayer);
|
||||||
return SettingsPage(
|
return SettingsPage(
|
||||||
title: Text(t.screens.subtitleStyling),
|
title: Text(t.screens.subtitleStyling),
|
||||||
children: [
|
children: [
|
||||||
@@ -108,6 +111,13 @@ class SubtitleStylingScreen extends StatelessWidget {
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
),
|
),
|
||||||
|
if (exoActive)
|
||||||
|
SettingSwitchTile(
|
||||||
|
pref: SettingsService.subtitleAnchorToScreen,
|
||||||
|
icon: Symbols.fit_screen_rounded,
|
||||||
|
title: t.subtitlingStyling.anchorToScreen,
|
||||||
|
subtitle: t.subtitlingStyling.anchorToScreenDescription,
|
||||||
|
),
|
||||||
SettingSwitchTile(
|
SettingSwitchTile(
|
||||||
pref: SettingsService.subtitleBold,
|
pref: SettingsService.subtitleBold,
|
||||||
icon: Symbols.format_bold_rounded,
|
icon: Symbols.format_bold_rounded,
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState {
|
|||||||
subtitlePosition: settingsService.read(SettingsService.subtitlePosition),
|
subtitlePosition: settingsService.read(SettingsService.subtitlePosition),
|
||||||
bold: settingsService.read(SettingsService.subtitleBold),
|
bold: settingsService.read(SettingsService.subtitleBold),
|
||||||
italic: settingsService.read(SettingsService.subtitleItalic),
|
italic: settingsService.read(SettingsService.subtitleItalic),
|
||||||
|
anchorToScreen: settingsService.read(SettingsService.subtitleAnchorToScreen),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -377,6 +377,12 @@ class SettingsService extends BaseSharedPreferencesService {
|
|||||||
);
|
);
|
||||||
static const subtitleBold = BoolPref('subtitle_bold');
|
static const subtitleBold = BoolPref('subtitle_bold');
|
||||||
static const subtitleItalic = BoolPref('subtitle_italic');
|
static const subtitleItalic = BoolPref('subtitle_italic');
|
||||||
|
|
||||||
|
/// Render text subtitles (SRT/VTT/mov_text) anchored to the physical screen
|
||||||
|
/// instead of the video rect, so they land in the letterbox bars of
|
||||||
|
/// widescreen video (#1730). ExoPlayer backend only; mpv already places
|
||||||
|
/// plaintext subtitles in the margins by default (sub-use-margins=yes).
|
||||||
|
static const subtitleAnchorToScreen = BoolPref('subtitle_anchor_to_screen');
|
||||||
static const cleanedOldImageCache = BoolPref('cleaned_old_image_cache');
|
static const cleanedOldImageCache = BoolPref('cleaned_old_image_cache');
|
||||||
static const rememberTrackSelections = BoolPref('remember_track_selections', defaultValue: true);
|
static const rememberTrackSelections = BoolPref('remember_track_selections', defaultValue: true);
|
||||||
|
|
||||||
@@ -935,6 +941,7 @@ class SettingsService extends BaseSharedPreferencesService {
|
|||||||
maxVolume,
|
maxVolume,
|
||||||
downmixCenterBoost,
|
downmixCenterBoost,
|
||||||
subtitlePosition,
|
subtitlePosition,
|
||||||
|
subtitleAnchorToScreen,
|
||||||
defaultPlaybackSpeed,
|
defaultPlaybackSpeed,
|
||||||
defaultBoxFitMode,
|
defaultBoxFitMode,
|
||||||
themeMode,
|
themeMode,
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ class FakePlayer implements Player {
|
|||||||
int subtitlePosition = 100,
|
int subtitlePosition = 100,
|
||||||
bool bold = false,
|
bool bold = false,
|
||||||
bool italic = false,
|
bool italic = false,
|
||||||
|
bool anchorToScreen = false,
|
||||||
}) async {}
|
}) async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user