mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: Implement PlayerLaunch and PlayerLaunchStore for managing player launch data; update MainAppContent to utilize new structures
This commit is contained in:
parent
e139a39b11
commit
258538ac8b
7 changed files with 113 additions and 34 deletions
|
|
@ -68,6 +68,8 @@ import com.nuvio.app.features.library.LibraryItem
|
|||
import com.nuvio.app.features.library.LibraryRepository
|
||||
import com.nuvio.app.features.library.LibraryScreen
|
||||
import com.nuvio.app.features.library.toLibraryItem
|
||||
import com.nuvio.app.features.player.PlayerLaunch
|
||||
import com.nuvio.app.features.player.PlayerLaunchStore
|
||||
import com.nuvio.app.features.player.PlayerRoute
|
||||
import com.nuvio.app.features.player.PlayerScreen
|
||||
import com.nuvio.app.features.profiles.NuvioProfile
|
||||
|
|
@ -428,8 +430,8 @@ private fun MainAppContent(
|
|||
onStreamSelected = { stream ->
|
||||
val sourceUrl = stream.directPlaybackUrl
|
||||
if (sourceUrl != null) {
|
||||
navController.navigate(
|
||||
PlayerRoute(
|
||||
val launchId = PlayerLaunchStore.put(
|
||||
PlayerLaunch(
|
||||
title = route.title,
|
||||
sourceUrl = sourceUrl,
|
||||
logo = route.logo,
|
||||
|
|
@ -450,6 +452,9 @@ private fun MainAppContent(
|
|||
initialPositionMs = route.resumePositionMs ?: 0L,
|
||||
)
|
||||
)
|
||||
navController.navigate(
|
||||
PlayerRoute(launchId = launchId)
|
||||
)
|
||||
}
|
||||
},
|
||||
onBack = {
|
||||
|
|
@ -461,26 +466,37 @@ private fun MainAppContent(
|
|||
}
|
||||
composable<PlayerRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<PlayerRoute>()
|
||||
val launch = remember(route.launchId) { PlayerLaunchStore.get(route.launchId) }
|
||||
if (launch == null) {
|
||||
LaunchedEffect(route.launchId) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
return@composable
|
||||
}
|
||||
PlayerScreen(
|
||||
title = route.title,
|
||||
sourceUrl = route.sourceUrl,
|
||||
logo = route.logo,
|
||||
poster = route.poster,
|
||||
background = route.background,
|
||||
seasonNumber = route.seasonNumber,
|
||||
episodeNumber = route.episodeNumber,
|
||||
episodeTitle = route.episodeTitle,
|
||||
episodeThumbnail = route.episodeThumbnail,
|
||||
streamTitle = route.streamTitle,
|
||||
streamSubtitle = route.streamSubtitle,
|
||||
providerName = route.providerName,
|
||||
providerAddonId = route.providerAddonId,
|
||||
contentType = route.contentType,
|
||||
videoId = route.videoId,
|
||||
parentMetaId = route.parentMetaId,
|
||||
parentMetaType = route.parentMetaType,
|
||||
initialPositionMs = route.initialPositionMs,
|
||||
onBack = { navController.popBackStack() },
|
||||
title = launch.title,
|
||||
sourceUrl = launch.sourceUrl,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
seasonNumber = launch.seasonNumber,
|
||||
episodeNumber = launch.episodeNumber,
|
||||
episodeTitle = launch.episodeTitle,
|
||||
episodeThumbnail = launch.episodeThumbnail,
|
||||
streamTitle = launch.streamTitle,
|
||||
streamSubtitle = launch.streamSubtitle,
|
||||
providerName = launch.providerName,
|
||||
providerAddonId = launch.providerAddonId,
|
||||
contentType = launch.contentType,
|
||||
videoId = launch.videoId,
|
||||
parentMetaId = launch.parentMetaId,
|
||||
parentMetaType = launch.parentMetaType,
|
||||
initialPositionMs = launch.initialPositionMs,
|
||||
onBack = {
|
||||
PlayerLaunchStore.remove(route.launchId)
|
||||
navController.popBackStack()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,22 @@ package com.nuvio.app.features.details
|
|||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
internal object MetaDetailsParser {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun parse(payload: String): MetaDetails {
|
||||
val root = json.parseToJsonElement(payload).jsonObject
|
||||
val meta = root["meta"]?.jsonObject ?: error("Missing 'meta' in response")
|
||||
val root = json.parseToJsonElement(payload).asJsonObjectOrNull()
|
||||
?: error("Expected top-level JSON object in response")
|
||||
val meta = root["meta"].asJsonObjectOrNull()
|
||||
?: error("Missing or invalid 'meta' object in response")
|
||||
val links = meta.links()
|
||||
|
||||
return MetaDetails(
|
||||
|
|
@ -190,3 +192,5 @@ internal object MetaDetailsParser {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonElement?.asJsonObjectOrNull(): JsonObject? = this as? JsonObject
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
@Serializable
|
||||
data class PlayerRoute(
|
||||
val launchId: Long,
|
||||
)
|
||||
|
||||
data class PlayerLaunch(
|
||||
val title: String,
|
||||
val sourceUrl: String,
|
||||
val logo: String? = null,
|
||||
|
|
@ -24,6 +28,23 @@ data class PlayerRoute(
|
|||
val initialPositionMs: Long = 0L,
|
||||
)
|
||||
|
||||
object PlayerLaunchStore {
|
||||
private var nextLaunchId = 1L
|
||||
private val launches = mutableMapOf<Long, PlayerLaunch>()
|
||||
|
||||
fun put(launch: PlayerLaunch): Long {
|
||||
val launchId = nextLaunchId++
|
||||
launches[launchId] = launch
|
||||
return launchId
|
||||
}
|
||||
|
||||
fun get(launchId: Long): PlayerLaunch? = launches[launchId]
|
||||
|
||||
fun remove(launchId: Long) {
|
||||
launches.remove(launchId)
|
||||
}
|
||||
}
|
||||
|
||||
enum class PlayerResizeMode {
|
||||
Fit,
|
||||
Fill,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.details
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class MetaDetailsParserTest {
|
||||
|
||||
@Test
|
||||
fun `parse rejects null meta object without json object cast crash`() {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
MetaDetailsParser.parse("""{"meta":null}""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class PlayerLaunchStoreTest {
|
||||
|
||||
@Test
|
||||
fun storesAndRemovesLaunchesById() {
|
||||
val launch = PlayerLaunch(
|
||||
title = "Title",
|
||||
sourceUrl = "https://example.com/video.m3u8?token=a/b:c",
|
||||
streamTitle = "Source",
|
||||
providerName = "Provider",
|
||||
parentMetaId = "tt1234567",
|
||||
parentMetaType = "movie",
|
||||
)
|
||||
|
||||
val launchId = PlayerLaunchStore.put(launch)
|
||||
|
||||
assertEquals(launch, PlayerLaunchStore.get(launchId))
|
||||
|
||||
PlayerLaunchStore.remove(launchId)
|
||||
|
||||
assertNull(PlayerLaunchStore.get(launchId))
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
|
||||
shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\nif [ -z \"$JAVA_HOME\" ] && [ -x \"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/java\" ]; then\n export JAVA_HOME=\"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home\"\nfi\nif [ -n \"$JAVA_HOME\" ]; then\n export PATH=\"$JAVA_HOME/bin:/opt/homebrew/bin:$PATH\"\nelse\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
|
|
|
|||
|
|
@ -60,15 +60,6 @@ final class OrientationLockCoordinator {
|
|||
print("[OrientationLockCoordinator] Geometry update failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if forceRotate {
|
||||
UIDevice.current.setValue(preferredLandscapeOrientation.rawValue, forKey: "orientation")
|
||||
} else if UIDevice.current.orientation.isPortrait {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
}
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap(\.windows)
|
||||
|
|
@ -76,6 +67,11 @@ final class OrientationLockCoordinator {
|
|||
window.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
|
||||
}
|
||||
} else {
|
||||
if forceRotate {
|
||||
UIDevice.current.setValue(preferredLandscapeOrientation.rawValue, forKey: "orientation")
|
||||
} else if UIDevice.current.orientation.isPortrait {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
}
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue