Merge branch 'NuvioMedia:cmp-rewrite' into introdb

This commit is contained in:
paregi12 2026-05-02 15:55:17 +05:30 committed by GitHub
commit b0f2767925
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 153 additions and 134 deletions

View file

@ -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

View file

@ -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),

View file

@ -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
}
}

View file

@ -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<AudioTrack> {
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<AudioTrack> {
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<SubtitleTrack> {
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<SubtitleTrack> {
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

View file

@ -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)
}
}

View file

@ -1,3 +1,3 @@
CURRENT_PROJECT_VERSION=49
MARKETING_VERSION=0.1.0
CURRENT_PROJECT_VERSION=50
MARKETING_VERSION=0.1.13