From fbfab7cc390078aa8155a3a1ea164edbdb26bd1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?HAL=C4=B0L=20=C4=B0BRAH=C4=B0M=20=C3=96ZDEM=C4=B0R?=
<94225244+halibiram@users.noreply.github.com>
Date: Mon, 30 Mar 2026 23:59:40 +0300
Subject: [PATCH 1/4] Fix pt-BR string resource markup
---
app/src/main/res/values-pt-rBR/strings.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 3746118c..b8858498 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -991,7 +991,7 @@
Isso removerá a lista e todos os seus itens do Trakt.
Excluir
LOCAL
- string name="library_source_nuvio">NUVIO
+ NUVIO
TRAKT
Sincronizando biblioteca…
Todos
From b2ab419b1adeb7ce2428b7d3ab75ddb6cbfb816c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?HAL=C4=B0L=20=C4=B0BRAH=C4=B0M=20=C3=96ZDEM=C4=B0R?=
<94225244+halibiram@users.noreply.github.com>
Date: Mon, 30 Mar 2026 23:59:58 +0300
Subject: [PATCH 2/4] Replace main aspect ratio with typed aspect modes
---
.../screens/player/PlayerAspectScaleUtils.kt | 83 +++++++++++++++++++
.../PlayerRuntimeControllerPlaybackEvents.kt | 13 ++-
.../tv/ui/screens/player/PlayerScreen.kt | 7 +-
.../tv/ui/screens/player/PlayerUiState.kt | 1 +
app/src/main/res/values-tr/strings.xml | 2 +
app/src/main/res/values/strings.xml | 2 +
6 files changed, 98 insertions(+), 10 deletions(-)
create mode 100644 app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt
new file mode 100644
index 00000000..6cb64f0f
--- /dev/null
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt
@@ -0,0 +1,83 @@
+package com.nuvio.tv.ui.screens.player
+
+import androidx.annotation.StringRes
+import androidx.media3.ui.PlayerView
+import com.nuvio.tv.R
+
+enum class AspectMode(@StringRes val labelResId: Int) {
+ ORIGINAL(R.string.player_aspect_fit),
+ FULL_SCREEN(R.string.player_aspect_crop),
+ SLIGHT_ZOOM(R.string.player_aspect_mode_slight_zoom),
+ CINEMA_ZOOM(R.string.player_aspect_mode_cinema_zoom),
+ VERTICAL_STRETCH(R.string.player_aspect_fit_height),
+ HORIZONTAL_STRETCH(R.string.player_aspect_fit_width)
+}
+
+internal fun nextAspectMode(current: AspectMode): AspectMode {
+ val modes = AspectMode.entries
+ val nextIndex = (modes.indexOf(current) + 1) % modes.size
+ return modes[nextIndex]
+}
+
+internal fun aspectModeLabel(mode: AspectMode, getString: (Int) -> String): String =
+ getString(mode.labelResId)
+
+internal fun applyAspectMode(playerView: PlayerView, mode: AspectMode) {
+ when (mode) {
+ AspectMode.ORIGINAL -> {
+ playerView.scaleX = 1.0f
+ playerView.scaleY = 1.0f
+ }
+
+ AspectMode.FULL_SCREEN -> applyCoverAspectScale(playerView)
+
+ AspectMode.SLIGHT_ZOOM -> {
+ playerView.scaleX = 1.15f
+ playerView.scaleY = 1.15f
+ }
+
+ AspectMode.CINEMA_ZOOM -> {
+ playerView.scaleX = 1.33f
+ playerView.scaleY = 1.33f
+ }
+
+ AspectMode.VERTICAL_STRETCH -> {
+ playerView.scaleX = 1.0f
+ playerView.scaleY = 1.33f
+ }
+
+ AspectMode.HORIZONTAL_STRETCH -> {
+ playerView.scaleX = 1.3333f
+ playerView.scaleY = 1.0f
+ }
+ }
+}
+
+private fun applyCoverAspectScale(playerView: PlayerView) {
+ val videoSize = playerView.player?.videoSize
+ val videoAspect = if ((videoSize?.height ?: 0) > 0) {
+ ((videoSize?.width ?: 0).toFloat() * (videoSize?.pixelWidthHeightRatio ?: 1f)) /
+ videoSize!!.height.toFloat()
+ } else {
+ 0f
+ }
+
+ val viewAspect = if (playerView.width > 0 && playerView.height > 0) {
+ playerView.width.toFloat() / playerView.height.toFloat()
+ } else {
+ 0f
+ }
+
+ if (videoAspect > 0f && viewAspect > 0f) {
+ if (videoAspect > viewAspect) {
+ playerView.scaleX = 1.0f
+ playerView.scaleY = videoAspect / viewAspect
+ } else {
+ playerView.scaleX = viewAspect / videoAspect
+ playerView.scaleY = 1.0f
+ }
+ } else {
+ playerView.scaleX = 1.0f
+ playerView.scaleY = 1.0f
+ }
+}
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt
index 9e665021..439c321a 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt
@@ -813,18 +813,17 @@ fun PlayerRuntimeController.onEvent(event: PlayerEvent) {
}
}
PlayerEvent.OnToggleAspectRatio -> {
- val currentMode = _uiState.value.resizeMode
- val newMode = PlayerDisplayModeUtils.nextResizeMode(currentMode)
- val modeText = PlayerDisplayModeUtils.resizeModeLabel(newMode, context)
- Log.d("PlayerViewModel", "Aspect ratio toggled: $currentMode -> $newMode")
+ val state = _uiState.value
+ val newMode = nextAspectMode(state.aspectMode)
+ val label = aspectModeLabel(newMode, context::getString)
+ Log.d("PlayerViewModel", "Aspect mode toggled: ${state.aspectMode} -> $newMode ($label)")
_uiState.update {
it.copy(
- resizeMode = newMode,
+ aspectMode = newMode,
showAspectRatioIndicator = true,
- aspectRatioIndicatorText = modeText
+ aspectRatioIndicatorText = label
)
}
- scope.launch { playerSettingsDataStore.setResizeMode(newMode) }
hideAspectRatioIndicatorJob?.cancel()
hideAspectRatioIndicatorJob = scope.launch {
delay(1500)
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerScreen.kt
index 3f736694..5020dd69 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerScreen.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerScreen.kt
@@ -87,6 +87,7 @@ import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
+import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -489,7 +490,7 @@ fun PlayerScreen(
// Video Player
viewModel.exoPlayer?.let { player ->
val subtitleStyle = uiState.subtitleStyle
- val resizeMode = uiState.resizeMode
+ val aspectMode = uiState.aspectMode
AndroidView(
factory = { context ->
@@ -503,8 +504,8 @@ fun PlayerScreen(
update = { playerView ->
// Keep device awake only while playback is active (or buffering), not when paused.
playerView.keepScreenOn = uiState.isPlaying || uiState.isBuffering
- Log.d("PlayerScreen", "Applying resizeMode: $resizeMode")
- playerView.resizeMode = resizeMode
+ playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
+ applyAspectMode(playerView, aspectMode)
playerView.subtitleView?.apply {
// Calculate font size based on percentage (100% = 24sp base)
val baseFontSize = 24f
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt
index ab5d2ba1..6312957b 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt
@@ -125,6 +125,7 @@ data class PlayerUiState(
val showDisplayModeInfo: Boolean = false,
// Aspect ratio / resize mode
val resizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT,
+ val aspectMode: AspectMode = AspectMode.ORIGINAL,
val showAspectRatioIndicator: Boolean = false,
val aspectRatioIndicatorText: String = "",
// Stream info overlay
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index f1301210..fc4a5ccb 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -847,6 +847,8 @@
Genişliğe Sığdır
Yüksekliğe Sığdır
Kırp
+ Hafif Yakınlaştırma
+ Sinema Yakınlaştırma
Ses
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2ea4fcc5..19c0099a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -850,6 +850,8 @@
Fit Width
Fit Height
Crop
+ Slight Zoom
+ Cinema Zoom
Audio
From 32102b30e5265160565b25f900552050e2cd1068 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?HAL=C4=B0L=20=C4=B0BRAH=C4=B0M=20=C3=96ZDEM=C4=B0R?=
<94225244+halibiram@users.noreply.github.com>
Date: Tue, 31 Mar 2026 00:14:32 +0300
Subject: [PATCH 3/4] Keep subtitles unaffected by aspect scaling
---
.../screens/player/PlayerAspectScaleUtils.kt | 65 ++++++++++++++-----
1 file changed, 47 insertions(+), 18 deletions(-)
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt
index 6cb64f0f..819c8b7c 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerAspectScaleUtils.kt
@@ -1,5 +1,9 @@
package com.nuvio.tv.ui.screens.player
+import android.view.SurfaceView
+import android.view.TextureView
+import android.view.View
+import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.media3.ui.PlayerView
import com.nuvio.tv.R
@@ -23,37 +27,41 @@ internal fun aspectModeLabel(mode: AspectMode, getString: (Int) -> String): Stri
getString(mode.labelResId)
internal fun applyAspectMode(playerView: PlayerView, mode: AspectMode) {
+ val targetView = resolveVideoSurfaceView(playerView) ?: playerView
+ playerView.scaleX = 1.0f
+ playerView.scaleY = 1.0f
+
when (mode) {
AspectMode.ORIGINAL -> {
- playerView.scaleX = 1.0f
- playerView.scaleY = 1.0f
+ targetView.scaleX = 1.0f
+ targetView.scaleY = 1.0f
}
- AspectMode.FULL_SCREEN -> applyCoverAspectScale(playerView)
+ AspectMode.FULL_SCREEN -> applyCoverAspectScale(playerView, targetView)
AspectMode.SLIGHT_ZOOM -> {
- playerView.scaleX = 1.15f
- playerView.scaleY = 1.15f
+ targetView.scaleX = 1.15f
+ targetView.scaleY = 1.15f
}
AspectMode.CINEMA_ZOOM -> {
- playerView.scaleX = 1.33f
- playerView.scaleY = 1.33f
+ targetView.scaleX = 1.33f
+ targetView.scaleY = 1.33f
}
AspectMode.VERTICAL_STRETCH -> {
- playerView.scaleX = 1.0f
- playerView.scaleY = 1.33f
+ targetView.scaleX = 1.0f
+ targetView.scaleY = 1.33f
}
AspectMode.HORIZONTAL_STRETCH -> {
- playerView.scaleX = 1.3333f
- playerView.scaleY = 1.0f
+ targetView.scaleX = 1.3333f
+ targetView.scaleY = 1.0f
}
}
}
-private fun applyCoverAspectScale(playerView: PlayerView) {
+private fun applyCoverAspectScale(playerView: PlayerView, targetView: View) {
val videoSize = playerView.player?.videoSize
val videoAspect = if ((videoSize?.height ?: 0) > 0) {
((videoSize?.width ?: 0).toFloat() * (videoSize?.pixelWidthHeightRatio ?: 1f)) /
@@ -70,14 +78,35 @@ private fun applyCoverAspectScale(playerView: PlayerView) {
if (videoAspect > 0f && viewAspect > 0f) {
if (videoAspect > viewAspect) {
- playerView.scaleX = 1.0f
- playerView.scaleY = videoAspect / viewAspect
+ targetView.scaleX = 1.0f
+ targetView.scaleY = videoAspect / viewAspect
} else {
- playerView.scaleX = viewAspect / videoAspect
- playerView.scaleY = 1.0f
+ targetView.scaleX = viewAspect / videoAspect
+ targetView.scaleY = 1.0f
}
} else {
- playerView.scaleX = 1.0f
- playerView.scaleY = 1.0f
+ targetView.scaleX = 1.0f
+ targetView.scaleY = 1.0f
+ }
+}
+
+private fun resolveVideoSurfaceView(playerView: PlayerView): View? {
+ return findVideoSurfaceView(playerView)
+}
+
+private fun findVideoSurfaceView(view: View): View? {
+ return when (view) {
+ is SurfaceView, is TextureView -> view
+ is ViewGroup -> {
+ for (index in 0 until view.childCount) {
+ val child = findVideoSurfaceView(view.getChildAt(index))
+ if (child != null) {
+ return child
+ }
+ }
+ null
+ }
+
+ else -> null
}
}
From 59dbc6a705f656aeecff74b69cafca53f615f758 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?HAL=C4=B0L=20=C4=B0BRAH=C4=B0M=20=C3=96ZDEM=C4=B0R?=
<94225244+halibiram@users.noreply.github.com>
Date: Tue, 31 Mar 2026 00:19:50 +0300
Subject: [PATCH 4/4] Warn when aspect ratio is unavailable during tunneled
playback
---
.../PlayerRuntimeControllerInitialization.kt | 3 ++-
.../player/PlayerRuntimeControllerObservers.kt | 1 +
.../PlayerRuntimeControllerPlaybackEvents.kt | 14 ++++++++++++++
.../nuvio/tv/ui/screens/player/PlayerUiState.kt | 1 +
app/src/main/res/values-tr/strings.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
6 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerInitialization.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerInitialization.kt
index 15be3503..f73eac01 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerInitialization.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerInitialization.kt
@@ -78,7 +78,8 @@ internal fun PlayerRuntimeController.initializePlayer(url: String, headers: Map<
_uiState.update {
it.copy(
frameRateMatchingMode = playerSettings.frameRateMatchingMode,
- resizeMode = playerSettings.resizeMode
+ resizeMode = playerSettings.resizeMode,
+ tunnelingEnabled = playerSettings.tunnelingEnabled
)
}
val afrJob = async {
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerObservers.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerObservers.kt
index 1f100917..34ac1147 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerObservers.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerObservers.kt
@@ -191,6 +191,7 @@ internal fun PlayerRuntimeController.observeSubtitleSettings() {
pauseOverlayEnabled = settings.pauseOverlayEnabled,
osdClockEnabled = settings.osdClockEnabled,
frameRateMatchingMode = settings.frameRateMatchingMode,
+ tunnelingEnabled = settings.tunnelingEnabled,
persistAudioAmplification = settings.persistAudioAmplification,
audioAmplificationDb = resolvedAudioAmplificationDb
)
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt
index 439c321a..a15388d0 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerRuntimeControllerPlaybackEvents.kt
@@ -814,6 +814,20 @@ fun PlayerRuntimeController.onEvent(event: PlayerEvent) {
}
PlayerEvent.OnToggleAspectRatio -> {
val state = _uiState.value
+ if (state.tunnelingEnabled) {
+ _uiState.update {
+ it.copy(
+ showAspectRatioIndicator = true,
+ aspectRatioIndicatorText = context.getString(R.string.player_aspect_tunneling_unavailable)
+ )
+ }
+ hideAspectRatioIndicatorJob?.cancel()
+ hideAspectRatioIndicatorJob = scope.launch {
+ delay(1500)
+ _uiState.update { it.copy(showAspectRatioIndicator = false) }
+ }
+ return
+ }
val newMode = nextAspectMode(state.aspectMode)
val label = aspectModeLabel(newMode, context::getString)
Log.d("PlayerViewModel", "Aspect mode toggled: ${state.aspectMode} -> $newMode ($label)")
diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt
index 6312957b..db2ccaca 100644
--- a/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt
+++ b/app/src/main/java/com/nuvio/tv/ui/screens/player/PlayerUiState.kt
@@ -126,6 +126,7 @@ data class PlayerUiState(
// Aspect ratio / resize mode
val resizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT,
val aspectMode: AspectMode = AspectMode.ORIGINAL,
+ val tunnelingEnabled: Boolean = false,
val showAspectRatioIndicator: Boolean = false,
val aspectRatioIndicatorText: String = "",
// Stream info overlay
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index fc4a5ccb..4ccea513 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -847,6 +847,7 @@
Genişliğe Sığdır
Yüksekliğe Sığdır
Kırp
+ Tünellenmiş oynatım sırasında kullanılamaz
Hafif Yakınlaştırma
Sinema Yakınlaştırma
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 19c0099a..f3293796 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -850,6 +850,7 @@
Fit Width
Fit Height
Crop
+ Unavailable during tunneled playback
Slight Zoom
Cinema Zoom