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, activeEpisodeNumber,
) { mutableStateOf(false) } ) { mutableStateOf(false) }
var hasSentCompletionScrobbleForCurrentItem by remember( var hasSentCompletionScrobbleForCurrentItem by remember(
activeSourceUrl,
activeVideoId, activeVideoId,
activeSeasonNumber, activeSeasonNumber,
activeEpisodeNumber, activeEpisodeNumber,
@ -383,7 +382,6 @@ fun PlayerScreen(
val progressPercent = currentPlaybackProgressPercent() val progressPercent = currentPlaybackProgressPercent()
if (progressPercent >= 1f && progressPercent < 80f) { if (progressPercent >= 1f && progressPercent < 80f) {
emitTraktScrobbleStop(progressPercent) emitTraktScrobbleStop(progressPercent)
hasSentCompletionScrobbleForCurrentItem = false
return return
} }
@ -1199,15 +1197,20 @@ fun PlayerScreen(
pausedOverlayVisible = true 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) { if (playbackSnapshot.isEnded) {
hasSentCompletionScrobbleForCurrentItem = false
flushWatchProgress() flushWatchProgress()
previousIsPlaying = false previousIsPlaying = false
return@LaunchedEffect return@LaunchedEffect
} }
if (previousIsPlaying && !playbackSnapshot.isPlaying) { if (previousIsPlaying && !playbackSnapshot.isPlaying && !playbackSnapshot.isLoading) {
flushWatchProgress() flushWatchProgress()
} }
@ -1215,7 +1218,9 @@ fun PlayerScreen(
emitTraktScrobbleStart() emitTraktScrobbleStart()
} }
previousIsPlaying = playbackSnapshot.isPlaying if (!playbackSnapshot.isLoading) {
previousIsPlaying = playbackSnapshot.isPlaying
}
if (!playbackSnapshot.isPlaying) { if (!playbackSnapshot.isPlaying) {
return@LaunchedEffect 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_english
import nuvio.composeapp.generated.resources.lang_french import nuvio.composeapp.generated.resources.lang_french
import nuvio.composeapp.generated.resources.lang_spanish 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_turkish
import nuvio.composeapp.generated.resources.lang_italian import nuvio.composeapp.generated.resources.lang_italian
import nuvio.composeapp.generated.resources.lang_greek import nuvio.composeapp.generated.resources.lang_greek
@ -16,8 +16,9 @@ enum class AppLanguage(
val labelRes: StringResource, val labelRes: StringResource,
) { ) {
ENGLISH("en", Res.string.lang_english), ENGLISH("en", Res.string.lang_english),
FRENCH("fr", Res.string.lang_french), FRENCH("fr", Res.string.lang_french),
SPANISH("es", Res.string.lang_spanish), SPANISH("es", Res.string.lang_spanish),
PORTUGUESE("pt", Res.string.lang_portuguese_portugal),
TURKISH("tr", Res.string.lang_turkish), TURKISH("tr", Res.string.lang_turkish),
ITALIAN("it", Res.string.lang_italian), ITALIAN("it", Res.string.lang_italian),
GREEK("el", Res.string.lang_greek), GREEK("el", Res.string.lang_greek),

View file

@ -48,8 +48,8 @@ object ThemeSettingsRepository {
_selectedTheme.value = theme _selectedTheme.value = theme
_amoledEnabled.value = ThemeSettingsStorage.loadAmoledEnabled() ?: false _amoledEnabled.value = ThemeSettingsStorage.loadAmoledEnabled() ?: false
val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage()) val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage())
_selectedAppLanguage.value = appLanguage
ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code) ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code)
_selectedAppLanguage.value = appLanguage
} }
fun setTheme(theme: AppTheme) { fun setTheme(theme: AppTheme) {
@ -69,8 +69,8 @@ object ThemeSettingsRepository {
fun setAppLanguage(language: AppLanguage) { fun setAppLanguage(language: AppLanguage) {
ensureLoaded() ensureLoaded()
if (_selectedAppLanguage.value == language) return if (_selectedAppLanguage.value == language) return
_selectedAppLanguage.value = language
ThemeSettingsStorage.saveSelectedAppLanguage(language.code) ThemeSettingsStorage.saveSelectedAppLanguage(language.code)
ThemeSettingsStorage.applySelectedAppLanguage(language.code) ThemeSettingsStorage.applySelectedAppLanguage(language.code)
_selectedAppLanguage.value = language
} }
} }

View file

@ -34,180 +34,182 @@ actual fun PlatformPlayerSurface(
onError: (String?) -> Unit, onError: (String?) -> Unit,
) { ) {
sanitizePlaybackResponseHeaders(sourceResponseHeaders) sanitizePlaybackResponseHeaders(sourceResponseHeaders)
val latestOnControllerReady = rememberUpdatedState(onControllerReady)
val latestOnSnapshot = rememberUpdatedState(onSnapshot) val latestOnSnapshot = rememberUpdatedState(onSnapshot)
val latestOnError = rememberUpdatedState(onError) val latestOnError = rememberUpdatedState(onError)
val bridge = remember(sourceUrl) { val bridge = remember {
NuvioPlayerBridgeFactory.create() NuvioPlayerBridgeFactory.create()
} }
if (bridge == null) { if (bridge == null) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
onError("MPV player engine not available. Please rebuild the app.") latestOnError.value("MPV player engine not available. Please rebuild the app.")
} }
return return
} }
// Create controller val controller = remember(bridge) {
LaunchedEffect(bridge) { object : PlayerEngineController {
onControllerReady( override fun play() {
object : PlayerEngineController { bridge.play()
override fun play() { }
bridge.play()
}
override fun pause() { override fun pause() {
bridge.pause() bridge.pause()
} }
override fun seekTo(positionMs: Long) { override fun seekTo(positionMs: Long) {
bridge.seekTo(positionMs) bridge.seekTo(positionMs)
} }
override fun seekBy(offsetMs: Long) { override fun seekBy(offsetMs: Long) {
bridge.seekBy(offsetMs) bridge.seekBy(offsetMs)
} }
override fun retry() { override fun retry() {
bridge.retry() bridge.retry()
} }
override fun setPlaybackSpeed(speed: Float) { override fun setPlaybackSpeed(speed: Float) {
bridge.setPlaybackSpeed(speed) bridge.setPlaybackSpeed(speed)
} }
override fun getAudioTracks(): List<AudioTrack> { override fun getAudioTracks(): List<AudioTrack> {
val count = bridge.getAudioTrackCount() val count = bridge.getAudioTrackCount()
return (0 until count).map { i -> return (0 until count).map { i ->
AudioTrack( AudioTrack(
index = bridge.getAudioTrackIndex(i), index = bridge.getAudioTrackIndex(i),
id = bridge.getAudioTrackId(i), id = bridge.getAudioTrackId(i),
label = bridge.getAudioTrackLabel(i), label = bridge.getAudioTrackLabel(i),
language = bridge.getAudioTrackLang(i), language = bridge.getAudioTrackLang(i),
isSelected = bridge.isAudioTrackSelected(i), isSelected = bridge.isAudioTrackSelected(i),
) )
}
} }
}
override fun getSubtitleTracks(): List<SubtitleTrack> { override fun getSubtitleTracks(): List<SubtitleTrack> {
val count = bridge.getSubtitleTrackCount() val count = bridge.getSubtitleTrackCount()
val tracks = (0 until count).map { i -> val tracks = (0 until count).map { i ->
val trackId = bridge.getSubtitleTrackId(i) val trackId = bridge.getSubtitleTrackId(i)
val trackLabel = bridge.getSubtitleTrackLabel(i) val trackLabel = bridge.getSubtitleTrackLabel(i)
val trackLanguage = bridge.getSubtitleTrackLang(i) val trackLanguage = bridge.getSubtitleTrackLang(i)
SubtitleTrack( SubtitleTrack(
index = bridge.getSubtitleTrackIndex(i), index = bridge.getSubtitleTrackIndex(i),
id = trackId, id = trackId,
label = trackLabel,
language = trackLanguage,
isSelected = bridge.isSubtitleTrackSelected(i),
isForced = inferForcedSubtitleTrack(
label = trackLabel, label = trackLabel,
language = trackLanguage, language = trackLanguage,
isSelected = bridge.isSubtitleTrackSelected(i), trackId = trackId,
isForced = inferForcedSubtitleTrack( ),
label = trackLabel, )
language = trackLanguage,
trackId = trackId,
),
)
}
Logger.d(TAG) { "getSubtitleTracks: found ${tracks.size} tracks" }
return tracks
} }
Logger.d(TAG) { "getSubtitleTracks: found ${tracks.size} tracks" }
return tracks
}
override fun selectAudioTrack(index: Int) { override fun selectAudioTrack(index: Int) {
// Convert from logical track index to mpv track id // Convert from logical track index to mpv track id
val count = bridge.getAudioTrackCount() 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 if (count <= 0) return
val trackId = (0 until count) val trackId = (0 until count)
.firstNotNullOfOrNull { at -> .firstNotNullOfOrNull { at ->
if (bridge.getAudioTrackIndex(at) == index) { if (bridge.getSubtitleTrackIndex(at) == index) {
bridge.getAudioTrackId(at).toIntOrNull() bridge.getSubtitleTrackId(at).toIntOrNull()
} else { } else {
null null
} }
} }
?: if (index in 0 until count) { ?: if (index in 0 until count) {
bridge.getAudioTrackId(index).toIntOrNull() ?: (index + 1) bridge.getSubtitleTrackId(index).toIntOrNull() ?: (index + 1)
} else { } else {
null null
} }
if (trackId != null) { if (trackId != null) {
bridge.selectAudioTrack(trackId) bridge.selectSubtitleTrack(trackId)
} }
} }
}
override fun selectSubtitleTrack(index: Int) { override fun setSubtitleUri(url: String) {
if (index < 0) { Logger.d(TAG) { "setSubtitleUri: $url" }
bridge.selectSubtitleTrack(-1) // disable 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 { } else {
val count = bridge.getSubtitleTrackCount() (0 until count)
if (count <= 0) return
val trackId = (0 until count)
.firstNotNullOfOrNull { at -> .firstNotNullOfOrNull { at ->
if (bridge.getSubtitleTrackIndex(at) == index) { if (bridge.getSubtitleTrackIndex(at) == trackIndex) {
bridge.getSubtitleTrackId(at).toIntOrNull() bridge.getSubtitleTrackId(at).toIntOrNull()
} else { } else {
null null
} }
} }
?: if (index in 0 until count) { ?: if (trackIndex in 0 until count) {
bridge.getSubtitleTrackId(index).toIntOrNull() ?: (index + 1) bridge.getSubtitleTrackId(trackIndex).toIntOrNull() ?: (trackIndex + 1)
} else { } else {
null trackIndex + 1
} }
if (trackId != null) {
bridge.selectSubtitleTrack(trackId)
}
} }
} }
bridge.clearExternalSubtitleAndSelect(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(),
)
}
} }
)
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 // Load file and set initial state

View file

@ -50,7 +50,17 @@ actual object ThemeSettingsStorage {
NSUserDefaults.standardUserDefaults.setObject(languageCode, forKey = selectedAppLanguageKey) 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 { actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) } loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
@ -69,5 +79,6 @@ actual object ThemeSettingsStorage {
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme) payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled) payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage) payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
} }
} }

View file

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