This commit is contained in:
i7xre 2026-07-26 21:43:12 +02:00 committed by GitHub
commit 29fc5ac565
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 143 additions and 10 deletions

View file

@ -9,6 +9,7 @@ internal actual object PlatformLocalAccountDataCleaner {
"nuvio_library_display_settings",
"nuvio_home_catalog_settings",
"nuvio_player_settings",
"nuvio_player_track_preferences",
"torrent_settings",
"nuvio_profile_cache",
"nuvio_avatar_cache",

View file

@ -1350,11 +1350,25 @@ private class NuvioLibmpvView(
}
override fun clearExternalSubtitle() {
extractLibmpvTracks(context, type = "sub")
.filter { it.isExternal }
.forEach { track -> mpv.command("sub-remove", track.id.toString()) }
mpv.setPropertyString("sid", "no")
}
override fun clearExternalSubtitleAndSelect(trackIndex: Int) {
selectSubtitleTrack(trackIndex)
val tracks = extractLibmpvTracks(context, type = "sub")
val targetTrackId = tracks.getOrNull(trackIndex)
?.takeUnless { it.isExternal }
?.id
tracks
.filter { it.isExternal }
.forEach { track -> mpv.command("sub-remove", track.id.toString()) }
if (targetTrackId == null) {
mpv.setPropertyString("sid", "no")
} else {
mpv.setPropertyInt("sid", targetTrackId)
}
}
override fun applySubtitleStyle(style: SubtitleStyleState) {
@ -1408,6 +1422,8 @@ private class NuvioLibmpvView(
label = label,
language = language,
isSelected = node.nodeBoolean("selected") ?: false,
isExternal = node.nodeBoolean("external") == true ||
node.nodeString("external-filename") != null,
isForced = inferForcedSubtitleTrack(
label = label,
language = language,
@ -1424,6 +1440,7 @@ private data class LibmpvTrack(
val label: String,
val language: String?,
val isSelected: Boolean,
val isExternal: Boolean,
val isForced: Boolean,
)

View file

@ -13,6 +13,7 @@ internal actual object PlayerTrackPreferenceStorage {
private const val addonSubtitleIdKey = "addon_subtitle_id"
private const val addonSubtitleUrlKey = "addon_subtitle_url"
private const val addonSubtitleAddonNameKey = "addon_subtitle_addon_name"
private const val addonSubtitleVideoIdKey = "addon_subtitle_video_id"
private const val audioLanguageKey = "audio_language"
private const val audioNameKey = "audio_name"
private const val audioTrackIdKey = "audio_track_id"
@ -34,6 +35,7 @@ internal actual object PlayerTrackPreferenceStorage {
addonSubtitleId = loadString(addonSubtitleIdKey, id),
addonSubtitleUrl = loadString(addonSubtitleUrlKey, id),
addonSubtitleAddonName = loadString(addonSubtitleAddonNameKey, id),
addonSubtitleVideoId = loadString(addonSubtitleVideoIdKey, id),
audioLanguage = loadString(audioLanguageKey, id),
audioName = loadString(audioNameKey, id),
audioTrackId = loadString(audioTrackIdKey, id),
@ -47,6 +49,7 @@ internal actual object PlayerTrackPreferenceStorage {
it.addonSubtitleId,
it.addonSubtitleUrl,
it.addonSubtitleAddonName,
it.addonSubtitleVideoId,
it.audioLanguage,
it.audioName,
it.audioTrackId,
@ -64,6 +67,7 @@ internal actual object PlayerTrackPreferenceStorage {
putOptionalString(addonSubtitleIdKey, id, preference.addonSubtitleId)
putOptionalString(addonSubtitleUrlKey, id, preference.addonSubtitleUrl)
putOptionalString(addonSubtitleAddonNameKey, id, preference.addonSubtitleAddonName)
putOptionalString(addonSubtitleVideoIdKey, id, preference.addonSubtitleVideoId)
putOptionalString(audioLanguageKey, id, preference.audioLanguage)
putOptionalString(audioNameKey, id, preference.audioName)
putOptionalString(audioTrackIdKey, id, preference.audioTrackId)

View file

@ -65,6 +65,12 @@ internal fun PlayerScreenRuntime.resetIdentityStateIfNeeded() {
val videoIdentity = "$identity:$activeVideoId:$activeSeasonNumber:$activeEpisodeNumber"
if (lastResetVideoIdentity != videoIdentity) {
lastResetVideoIdentity = videoIdentity
showSubtitleModal = false
subtitleModalVideoId = null
trackPreferenceRestoreApplied = false
preferredSubtitleSelectionApplied = false
selectedAddonSubtitleId = null
useCustomSubtitles = false
hasRequestedScrobbleStartForCurrentItem = false
scrobbleStartRequestGeneration = 0L
pendingScrobbleStartAfterSeek = false

View file

@ -426,6 +426,8 @@ private data class EpisodeResume(val positionMs: Long, val fraction: Float?)
private fun PlayerScreenRuntime.resetEpisodePanelAndNextEpisodeState() {
showNextEpisodeCard = false
showSubtitleModal = false
subtitleModalVideoId = null
showSourcesPanel = false
showEpisodesPanel = false
episodeStreamsPanelState = EpisodeStreamsPanelState()

View file

@ -180,6 +180,7 @@ internal class PlayerScreenRuntime(
var showAudioModal by mutableStateOf(false)
var showSubtitleModal by mutableStateOf(false)
var subtitleModalVideoId by mutableStateOf<String?>(null)
var showVideoSettingsModal by mutableStateOf(false)
var audioTracks by mutableStateOf<List<AudioTrack>>(emptyList())
var subtitleTracks by mutableStateOf<List<SubtitleTrack>>(emptyList())

View file

@ -56,6 +56,7 @@ internal fun PlayerScreenRuntime.persistInternalSubtitlePreference(track: Subtit
addonSubtitleId = null,
addonSubtitleUrl = null,
addonSubtitleAddonName = null,
addonSubtitleVideoId = null,
)
}
}
@ -70,17 +71,19 @@ internal fun PlayerScreenRuntime.persistAddonSubtitlePreference(subtitle: AddonS
addonSubtitleId = subtitle.id,
addonSubtitleUrl = subtitle.url,
addonSubtitleAddonName = subtitle.addonName,
addonSubtitleVideoId = playbackSession.videoId,
)
}
}
internal fun PlayerScreenRuntime.restorePersistedTrackPreferenceIfNeeded() {
if (trackPreferenceRestoreApplied) return
internal fun PlayerScreenRuntime.restorePersistedTrackPreferenceIfNeeded(): Boolean {
if (trackPreferenceRestoreApplied) return false
val preference = PlayerTrackPreferenceStorage.load(parentMetaId)
if (preference == null) {
trackPreferenceRestoreApplied = true
return
return false
}
var subtitleTracksInvalidated = false
if (
audioTracks.isNotEmpty() &&
@ -98,11 +101,12 @@ internal fun PlayerScreenRuntime.restorePersistedTrackPreferenceIfNeeded() {
when (preference.subtitleType) {
PersistedSubtitleSelectionType.DISABLED -> {
playerController?.selectSubtitleTrack(-1)
playerController?.clearExternalSubtitle()
selectedSubtitleIndex = -1
selectedAddonSubtitleId = null
useCustomSubtitles = false
preferredSubtitleSelectionApplied = true
subtitleTracksInvalidated = true
}
PersistedSubtitleSelectionType.INTERNAL -> {
if (subtitleTracks.isNotEmpty()) {
@ -110,6 +114,7 @@ internal fun PlayerScreenRuntime.restorePersistedTrackPreferenceIfNeeded() {
if (restoredSubtitleIndex >= 0) {
if (useCustomSubtitles) {
playerController?.clearExternalSubtitleAndSelect(restoredSubtitleIndex)
subtitleTracksInvalidated = true
} else {
playerController?.selectSubtitleTrack(restoredSubtitleIndex)
}
@ -121,18 +126,28 @@ internal fun PlayerScreenRuntime.restorePersistedTrackPreferenceIfNeeded() {
}
}
PersistedSubtitleSelectionType.ADDON -> {
val url = preference.addonSubtitleUrl?.takeIf { it.isNotBlank() }
val url = persistedAddonSubtitleUrlForVideo(
preference = preference,
videoId = playbackSession.videoId,
)
if (url != null) {
selectedAddonSubtitleId = preference.addonSubtitleId ?: url
selectedSubtitleIndex = -1
useCustomSubtitles = true
playerController?.setSubtitleUri(url)
preferredSubtitleSelectionApplied = true
} else {
playerController?.clearExternalSubtitle()
selectedAddonSubtitleId = null
selectedSubtitleIndex = -1
useCustomSubtitles = false
subtitleTracksInvalidated = true
}
}
}
trackPreferenceRestoreApplied = true
return subtitleTracksInvalidated
}
internal fun PlayerScreenRuntime.refreshTracks() {
@ -144,7 +159,10 @@ internal fun PlayerScreenRuntime.refreshTracks() {
val selectedSub = subtitleTracks.firstOrNull { it.isSelected }
if (selectedSub != null && !useCustomSubtitles) selectedSubtitleIndex = selectedSub.index
restorePersistedTrackPreferenceIfNeeded()
if (restorePersistedTrackPreferenceIfNeeded()) {
subtitleTracks = ctrl.getSubtitleTracks()
selectedSubtitleIndex = subtitleTracks.firstOrNull { it.isSelected }?.index ?: -1
}
val preferredAudioTargets = resolvePreferredAudioLanguageTargets(
preferredAudioLanguage = playerSettingsUiState.preferredAudioLanguage,

View file

@ -270,6 +270,7 @@ private fun PlayerScreenRuntime.RenderPlayerControls(displayedPositionMs: Long,
onSpeedClick = { cyclePlaybackSpeed() },
onSubtitleClick = {
refreshTracks()
subtitleModalVideoId = playbackSession.videoId
showSubtitleModal = true
},
onAudioClick = {
@ -464,7 +465,10 @@ private fun PlayerScreenRuntime.RenderPlayerModals(displayedPositionMs: Long) {
playerController?.selectSubtitleTrack(index)
}
},
onAddonSubtitleSelected = { addon ->
onAddonSubtitleSelected = selection@{ addon ->
if (!isSubtitleModalSelectionCurrent(subtitleModalVideoId, playbackSession.videoId)) {
return@selection
}
selectedAddonSubtitleId = addon.id
selectedSubtitleIndex = -1
useCustomSubtitles = true
@ -478,7 +482,10 @@ private fun PlayerScreenRuntime.RenderPlayerModals(displayedPositionMs: Long) {
onAutoSyncCapture = { captureSubtitleAutoSyncTime() },
onAutoSyncCueSelected = { cue -> applySubtitleAutoSyncCue(cue) },
onAutoSyncReload = { loadSubtitleAutoSyncCues(force = true) },
onSubtitleModalDismissed = { showSubtitleModal = false },
onSubtitleModalDismissed = {
showSubtitleModal = false
subtitleModalVideoId = null
},
showVideoSettingsModal = showVideoSettingsModal,
playerSettings = playerSettingsUiState,
onVideoSettingsChanged = {

View file

@ -8,6 +8,7 @@ data class PersistedPlayerTrackPreference(
val addonSubtitleId: String? = null,
val addonSubtitleUrl: String? = null,
val addonSubtitleAddonName: String? = null,
val addonSubtitleVideoId: String? = null,
val audioLanguage: String? = null,
val audioName: String? = null,
val audioTrackId: String? = null,

View file

@ -219,3 +219,17 @@ internal fun findPersistedSubtitleTrackIndex(
}
return -1
}
internal fun persistedAddonSubtitleUrlForVideo(
preference: PersistedPlayerTrackPreference,
videoId: String,
): String? {
val persistedVideoId = preference.addonSubtitleVideoId?.takeIf { it.isNotBlank() } ?: return null
if (persistedVideoId != videoId.takeIf { it.isNotBlank() }) return null
return preference.addonSubtitleUrl?.takeIf { it.isNotBlank() }
}
internal fun isSubtitleModalSelectionCurrent(
modalVideoId: String?,
currentVideoId: String,
): Boolean = !modalVideoId.isNullOrBlank() && modalVideoId == currentVideoId

View file

@ -234,6 +234,49 @@ class PlayerTrackSelectionTest {
assertEquals(listOf("french", "english"), visibleSubtitles.map { it.id })
}
@Test
fun addonSubtitleUrlIsRestoredForTheSameEpisode() {
val preference = PersistedPlayerTrackPreference(
addonSubtitleUrl = "https://example.com/episode-1.srt",
addonSubtitleVideoId = "series:1:1",
)
assertEquals(
"https://example.com/episode-1.srt",
persistedAddonSubtitleUrlForVideo(preference, "series:1:1"),
)
}
@Test
fun addonSubtitleUrlIsNotReusedForAnotherEpisode() {
val preference = PersistedPlayerTrackPreference(
addonSubtitleUrl = "https://example.com/episode-1.srt",
addonSubtitleVideoId = "series:1:1",
)
assertEquals(null, persistedAddonSubtitleUrlForVideo(preference, "series:1:2"))
}
@Test
fun legacyUnscopedAddonSubtitleUrlIsNotRestored() {
val preference = PersistedPlayerTrackPreference(
addonSubtitleUrl = "https://example.com/episode-1.srt",
)
assertEquals(null, persistedAddonSubtitleUrlForVideo(preference, "series:1:1"))
}
@Test
fun subtitleModalSelectionIsAcceptedForTheSameEpisode() {
assertEquals(true, isSubtitleModalSelectionCurrent("series:1:1", "series:1:1"))
}
@Test
fun subtitleModalSelectionIsRejectedAfterEpisodeChanges() {
assertEquals(false, isSubtitleModalSelectionCurrent("series:1:1", "series:1:2"))
assertEquals(false, isSubtitleModalSelectionCurrent(null, "series:1:2"))
}
private fun subtitleTrack(
index: Int,
language: String?,

View file

@ -60,6 +60,20 @@ internal actual object PlatformLocalAccountDataCleaner {
"collection_mobile_settings_payload",
"collections_payload",
)
private val playerTrackPreferencePrefixes = listOf(
"subtitle_type|",
"subtitle_language|",
"subtitle_name|",
"subtitle_track_id|",
"addon_subtitle_id|",
"addon_subtitle_url|",
"addon_subtitle_addon_name|",
"addon_subtitle_video_id|",
"audio_language|",
"audio_name|",
"audio_track_id|",
"subtitle_delay_ms|",
)
actual fun wipe() {
val defaults = NSUserDefaults.standardUserDefaults
@ -82,7 +96,8 @@ internal actual object PlatformLocalAccountDataCleaner {
val keyString = key as? String ?: continue
if (
keyString.startsWith("stream_link_") ||
keyString.startsWith("cw_enrichment_cache_")
keyString.startsWith("cw_enrichment_cache_") ||
playerTrackPreferencePrefixes.any(keyString::startsWith)
) {
defaults.removeObjectForKey(keyString)
}

View file

@ -11,6 +11,7 @@ internal actual object PlayerTrackPreferenceStorage {
private const val addonSubtitleIdKey = "addon_subtitle_id"
private const val addonSubtitleUrlKey = "addon_subtitle_url"
private const val addonSubtitleAddonNameKey = "addon_subtitle_addon_name"
private const val addonSubtitleVideoIdKey = "addon_subtitle_video_id"
private const val audioLanguageKey = "audio_language"
private const val audioNameKey = "audio_name"
private const val audioTrackIdKey = "audio_track_id"
@ -26,6 +27,7 @@ internal actual object PlayerTrackPreferenceStorage {
addonSubtitleId = loadString(addonSubtitleIdKey, id),
addonSubtitleUrl = loadString(addonSubtitleUrlKey, id),
addonSubtitleAddonName = loadString(addonSubtitleAddonNameKey, id),
addonSubtitleVideoId = loadString(addonSubtitleVideoIdKey, id),
audioLanguage = loadString(audioLanguageKey, id),
audioName = loadString(audioNameKey, id),
audioTrackId = loadString(audioTrackIdKey, id),
@ -39,6 +41,7 @@ internal actual object PlayerTrackPreferenceStorage {
it.addonSubtitleId,
it.addonSubtitleUrl,
it.addonSubtitleAddonName,
it.addonSubtitleVideoId,
it.audioLanguage,
it.audioName,
it.audioTrackId,
@ -55,6 +58,7 @@ internal actual object PlayerTrackPreferenceStorage {
saveOptionalString(addonSubtitleIdKey, id, preference.addonSubtitleId)
saveOptionalString(addonSubtitleUrlKey, id, preference.addonSubtitleUrl)
saveOptionalString(addonSubtitleAddonNameKey, id, preference.addonSubtitleAddonName)
saveOptionalString(addonSubtitleVideoIdKey, id, preference.addonSubtitleVideoId)
saveOptionalString(audioLanguageKey, id, preference.audioLanguage)
saveOptionalString(audioNameKey, id, preference.audioName)
saveOptionalString(audioTrackIdKey, id, preference.audioTrackId)