diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings b/composeApp/src/commonMain/composeResources/values-pt/strings.xml similarity index 100% rename from composeApp/src/commonMain/composeResources/values-pt/strings rename to composeApp/src/commonMain/composeResources/values-pt/strings.xml diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt index 2db78246..a99d0be5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt @@ -223,7 +223,6 @@ fun PlayerScreen( activeEpisodeNumber, ) { mutableStateOf(false) } var hasSentCompletionScrobbleForCurrentItem by remember( - activeSourceUrl, activeVideoId, activeSeasonNumber, activeEpisodeNumber, @@ -383,7 +382,6 @@ fun PlayerScreen( val progressPercent = currentPlaybackProgressPercent() if (progressPercent >= 1f && progressPercent < 80f) { emitTraktScrobbleStop(progressPercent) - hasSentCompletionScrobbleForCurrentItem = false return } @@ -1199,15 +1197,20 @@ fun PlayerScreen( pausedOverlayVisible = true } - LaunchedEffect(playbackSnapshot.positionMs, playbackSnapshot.isPlaying, playbackSnapshot.isEnded, playbackSnapshot.durationMs) { + LaunchedEffect( + playbackSnapshot.positionMs, + playbackSnapshot.isPlaying, + playbackSnapshot.isLoading, + playbackSnapshot.isEnded, + playbackSnapshot.durationMs, + ) { if (playbackSnapshot.isEnded) { - hasSentCompletionScrobbleForCurrentItem = false flushWatchProgress() previousIsPlaying = false return@LaunchedEffect } - if (previousIsPlaying && !playbackSnapshot.isPlaying) { + if (previousIsPlaying && !playbackSnapshot.isPlaying && !playbackSnapshot.isLoading) { flushWatchProgress() } @@ -1215,7 +1218,9 @@ fun PlayerScreen( emitTraktScrobbleStart() } - previousIsPlaying = playbackSnapshot.isPlaying + if (!playbackSnapshot.isLoading) { + previousIsPlaying = playbackSnapshot.isPlaying + } if (!playbackSnapshot.isPlaying) { return@LaunchedEffect diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt index 8002195d..f47fb7b8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt @@ -4,7 +4,7 @@ import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.lang_english import nuvio.composeapp.generated.resources.lang_french import nuvio.composeapp.generated.resources.lang_spanish -import nuvio.composeapp.generated.resources.lang_portuguese +import nuvio.composeapp.generated.resources.lang_portuguese_portugal import nuvio.composeapp.generated.resources.lang_turkish import nuvio.composeapp.generated.resources.lang_italian import nuvio.composeapp.generated.resources.lang_greek @@ -16,8 +16,9 @@ enum class AppLanguage( val labelRes: StringResource, ) { ENGLISH("en", Res.string.lang_english), - FRENCH("fr", Res.string.lang_french), + FRENCH("fr", Res.string.lang_french), SPANISH("es", Res.string.lang_spanish), + PORTUGUESE("pt", Res.string.lang_portuguese_portugal), TURKISH("tr", Res.string.lang_turkish), ITALIAN("it", Res.string.lang_italian), GREEK("el", Res.string.lang_greek), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt index 8a2b5241..863dd04f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt @@ -48,8 +48,8 @@ object ThemeSettingsRepository { _selectedTheme.value = theme _amoledEnabled.value = ThemeSettingsStorage.loadAmoledEnabled() ?: false val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage()) - _selectedAppLanguage.value = appLanguage ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code) + _selectedAppLanguage.value = appLanguage } fun setTheme(theme: AppTheme) { @@ -69,8 +69,8 @@ object ThemeSettingsRepository { fun setAppLanguage(language: AppLanguage) { ensureLoaded() if (_selectedAppLanguage.value == language) return - _selectedAppLanguage.value = language ThemeSettingsStorage.saveSelectedAppLanguage(language.code) ThemeSettingsStorage.applySelectedAppLanguage(language.code) + _selectedAppLanguage.value = language } } diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt index a0286b9f..2877b04c 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt @@ -34,180 +34,182 @@ actual fun PlatformPlayerSurface( onError: (String?) -> Unit, ) { sanitizePlaybackResponseHeaders(sourceResponseHeaders) + val latestOnControllerReady = rememberUpdatedState(onControllerReady) val latestOnSnapshot = rememberUpdatedState(onSnapshot) val latestOnError = rememberUpdatedState(onError) - val bridge = remember(sourceUrl) { + val bridge = remember { NuvioPlayerBridgeFactory.create() } if (bridge == null) { LaunchedEffect(Unit) { - onError("MPV player engine not available. Please rebuild the app.") + latestOnError.value("MPV player engine not available. Please rebuild the app.") } return } - // Create controller - LaunchedEffect(bridge) { - onControllerReady( - object : PlayerEngineController { - override fun play() { - bridge.play() - } + val controller = remember(bridge) { + object : PlayerEngineController { + override fun play() { + bridge.play() + } - override fun pause() { - bridge.pause() - } + override fun pause() { + bridge.pause() + } - override fun seekTo(positionMs: Long) { - bridge.seekTo(positionMs) - } + override fun seekTo(positionMs: Long) { + bridge.seekTo(positionMs) + } - override fun seekBy(offsetMs: Long) { - bridge.seekBy(offsetMs) - } + override fun seekBy(offsetMs: Long) { + bridge.seekBy(offsetMs) + } - override fun retry() { - bridge.retry() - } + override fun retry() { + bridge.retry() + } - override fun setPlaybackSpeed(speed: Float) { - bridge.setPlaybackSpeed(speed) - } + override fun setPlaybackSpeed(speed: Float) { + bridge.setPlaybackSpeed(speed) + } - override fun getAudioTracks(): List { - val count = bridge.getAudioTrackCount() - return (0 until count).map { i -> - AudioTrack( - index = bridge.getAudioTrackIndex(i), - id = bridge.getAudioTrackId(i), - label = bridge.getAudioTrackLabel(i), - language = bridge.getAudioTrackLang(i), - isSelected = bridge.isAudioTrackSelected(i), - ) - } + override fun getAudioTracks(): List { + val count = bridge.getAudioTrackCount() + return (0 until count).map { i -> + AudioTrack( + index = bridge.getAudioTrackIndex(i), + id = bridge.getAudioTrackId(i), + label = bridge.getAudioTrackLabel(i), + language = bridge.getAudioTrackLang(i), + isSelected = bridge.isAudioTrackSelected(i), + ) } + } - override fun getSubtitleTracks(): List { - val count = bridge.getSubtitleTrackCount() - val tracks = (0 until count).map { i -> - val trackId = bridge.getSubtitleTrackId(i) - val trackLabel = bridge.getSubtitleTrackLabel(i) - val trackLanguage = bridge.getSubtitleTrackLang(i) - SubtitleTrack( - index = bridge.getSubtitleTrackIndex(i), - id = trackId, + override fun getSubtitleTracks(): List { + val count = bridge.getSubtitleTrackCount() + val tracks = (0 until count).map { i -> + val trackId = bridge.getSubtitleTrackId(i) + val trackLabel = bridge.getSubtitleTrackLabel(i) + val trackLanguage = bridge.getSubtitleTrackLang(i) + SubtitleTrack( + index = bridge.getSubtitleTrackIndex(i), + id = trackId, + label = trackLabel, + language = trackLanguage, + isSelected = bridge.isSubtitleTrackSelected(i), + isForced = inferForcedSubtitleTrack( label = trackLabel, language = trackLanguage, - isSelected = bridge.isSubtitleTrackSelected(i), - isForced = inferForcedSubtitleTrack( - label = trackLabel, - language = trackLanguage, - trackId = trackId, - ), - ) - } - Logger.d(TAG) { "getSubtitleTracks: found ${tracks.size} tracks" } - return tracks + trackId = trackId, + ), + ) } + Logger.d(TAG) { "getSubtitleTracks: found ${tracks.size} tracks" } + return tracks + } - override fun selectAudioTrack(index: Int) { - // Convert from logical track index to mpv track id - val count = bridge.getAudioTrackCount() + override fun selectAudioTrack(index: Int) { + // Convert from logical track index to mpv track id + val count = bridge.getAudioTrackCount() + if (count <= 0) return + + val trackId = (0 until count) + .firstNotNullOfOrNull { at -> + if (bridge.getAudioTrackIndex(at) == index) { + bridge.getAudioTrackId(at).toIntOrNull() + } else { + null + } + } + ?: if (index in 0 until count) { + bridge.getAudioTrackId(index).toIntOrNull() ?: (index + 1) + } else { + null + } + + if (trackId != null) { + bridge.selectAudioTrack(trackId) + } + } + + override fun selectSubtitleTrack(index: Int) { + if (index < 0) { + bridge.selectSubtitleTrack(-1) // disable + } else { + val count = bridge.getSubtitleTrackCount() if (count <= 0) return val trackId = (0 until count) .firstNotNullOfOrNull { at -> - if (bridge.getAudioTrackIndex(at) == index) { - bridge.getAudioTrackId(at).toIntOrNull() + if (bridge.getSubtitleTrackIndex(at) == index) { + bridge.getSubtitleTrackId(at).toIntOrNull() } else { null } } ?: if (index in 0 until count) { - bridge.getAudioTrackId(index).toIntOrNull() ?: (index + 1) + bridge.getSubtitleTrackId(index).toIntOrNull() ?: (index + 1) } else { null } if (trackId != null) { - bridge.selectAudioTrack(trackId) + bridge.selectSubtitleTrack(trackId) } } + } - override fun selectSubtitleTrack(index: Int) { - if (index < 0) { - bridge.selectSubtitleTrack(-1) // disable + override fun setSubtitleUri(url: String) { + Logger.d(TAG) { "setSubtitleUri: $url" } + bridge.setSubtitleUrl(url) + } + + override fun clearExternalSubtitle() { + bridge.clearExternalSubtitle() + } + + override fun clearExternalSubtitleAndSelect(trackIndex: Int) { + val trackId = if (trackIndex < 0) { + -1 + } else { + val count = bridge.getSubtitleTrackCount() + if (count <= 0) { + trackIndex + 1 } else { - val count = bridge.getSubtitleTrackCount() - if (count <= 0) return - - val trackId = (0 until count) + (0 until count) .firstNotNullOfOrNull { at -> - if (bridge.getSubtitleTrackIndex(at) == index) { + if (bridge.getSubtitleTrackIndex(at) == trackIndex) { bridge.getSubtitleTrackId(at).toIntOrNull() } else { null } } - ?: if (index in 0 until count) { - bridge.getSubtitleTrackId(index).toIntOrNull() ?: (index + 1) + ?: if (trackIndex in 0 until count) { + bridge.getSubtitleTrackId(trackIndex).toIntOrNull() ?: (trackIndex + 1) } else { - null + trackIndex + 1 } - - if (trackId != null) { - bridge.selectSubtitleTrack(trackId) - } } } - - override fun setSubtitleUri(url: String) { - Logger.d(TAG) { "setSubtitleUri: $url" } - bridge.setSubtitleUrl(url) - } - - override fun clearExternalSubtitle() { - bridge.clearExternalSubtitle() - } - - override fun clearExternalSubtitleAndSelect(trackIndex: Int) { - val trackId = if (trackIndex < 0) { - -1 - } else { - val count = bridge.getSubtitleTrackCount() - if (count <= 0) { - trackIndex + 1 - } else { - (0 until count) - .firstNotNullOfOrNull { at -> - if (bridge.getSubtitleTrackIndex(at) == trackIndex) { - bridge.getSubtitleTrackId(at).toIntOrNull() - } else { - null - } - } - ?: if (trackIndex in 0 until count) { - bridge.getSubtitleTrackId(trackIndex).toIntOrNull() ?: (trackIndex + 1) - } else { - trackIndex + 1 - } - } - } - bridge.clearExternalSubtitleAndSelect(trackId) - } - - override fun applySubtitleStyle(style: SubtitleStyleState) { - bridge.applySubtitleStyle( - textColor = style.textColor.toMpvColorString(), - outlineSize = if (style.outlineEnabled) 1.65f else 0f, - fontSize = style.toMpvSubtitleFontSize(), - subPos = style.toMpvSubtitlePosition(), - ) - } + bridge.clearExternalSubtitleAndSelect(trackId) } - ) + + override fun applySubtitleStyle(style: SubtitleStyleState) { + bridge.applySubtitleStyle( + textColor = style.textColor.toMpvColorString(), + outlineSize = if (style.outlineEnabled) 1.65f else 0f, + fontSize = style.toMpvSubtitleFontSize(), + subPos = style.toMpvSubtitlePosition(), + ) + } + } + } + + LaunchedEffect(controller, sourceUrl, sourceAudioUrl, sourceHeaders, sourceResponseHeaders) { + latestOnControllerReady.value(controller) } // Load file and set initial state diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt index f71eaaea..c878b4a8 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt @@ -50,7 +50,17 @@ actual object ThemeSettingsStorage { NSUserDefaults.standardUserDefaults.setObject(languageCode, forKey = selectedAppLanguageKey) } - actual fun applySelectedAppLanguage(languageCode: String) = Unit + actual fun applySelectedAppLanguage(languageCode: String) { + val normalizedCode = languageCode + .trim() + .takeIf { it.isNotBlank() } + ?: AppLanguage.ENGLISH.code + NSUserDefaults.standardUserDefaults.setObject( + listOf(normalizedCode), + forKey = "AppleLanguages", + ) + NSUserDefaults.standardUserDefaults.synchronize() + } actual fun exportToSyncPayload(): JsonObject = buildJsonObject { loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) } @@ -69,5 +79,6 @@ actual object ThemeSettingsStorage { payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme) payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled) payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage) + applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code) } } diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig index d7b9fb66..2c747aca 100644 --- a/iosApp/Configuration/Version.xcconfig +++ b/iosApp/Configuration/Version.xcconfig @@ -1,3 +1,3 @@ -CURRENT_PROJECT_VERSION=49 -MARKETING_VERSION=0.1.0 +CURRENT_PROJECT_VERSION=50 +MARKETING_VERSION=0.1.13