mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 16:49:28 +00:00
Merge 002d459b72 into 00cc012193
This commit is contained in:
commit
29fc5ac565
13 changed files with 143 additions and 10 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue