mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-01 09:09:29 +00:00
Merge branch 'NuvioMedia:cmp-rewrite' into cmp
This commit is contained in:
commit
f468be5542
5 changed files with 439 additions and 307 deletions
117
README.md
117
README.md
|
|
@ -4,89 +4,63 @@
|
|||
<br />
|
||||
<br />
|
||||
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![License][license-shield]][license-url]
|
||||
|
||||
<p>
|
||||
Nuvio Desktop is a desktop media hub built with Kotlin Multiplatform and Compose Multiplatform.
|
||||
A modern media hub for Android and iOS built with Kotlin Multiplatform and Compose Multiplatform.
|
||||
<br />
|
||||
Desktop app in development
|
||||
Stremio addon ecosystem • Cross-platform
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
## About
|
||||
|
||||
Nuvio Desktop brings the Nuvio media experience to desktop. It keeps the playback-focused browsing, collection, watch progress, downloads, and Stremio addon ecosystem integration from Nuvio while adapting the app for desktop input, desktop storage, and native desktop playback.
|
||||
Nuvio is the current Kotlin Multiplatform rewrite of the original React Native app. It delivers a shared Compose UI for Android and iOS while keeping the playback-focused experience, collection tools, watch progress flows, downloads, and Stremio addon ecosystem integration that shaped the earlier app.
|
||||
|
||||
The desktop app is built from the shared Kotlin Multiplatform codebase in [composeApp](./composeApp), with desktop-specific code in [composeApp/src/desktopMain](./composeApp/src/desktopMain). Desktop packaging is configured through Gradle, with development builds active for desktop hosts and broader platform coverage continuing over time.
|
||||
|
||||
## Platform Status
|
||||
|
||||
Current desktop builds are actively being developed. Linux support is planned for a later phase, and public release targets are not finalized yet.
|
||||
The mobile app is built from a single shared codebase in [composeApp](./composeApp), with native platform entry points for Android and iOS.
|
||||
|
||||
## Installation
|
||||
|
||||
Public desktop releases are not available yet. Current builds are development builds.
|
||||
### Android
|
||||
|
||||
When releases are ready, desktop builds will be published from the Nuvio Desktop repository.
|
||||
Download the latest Android build from [GitHub Releases](https://github.com/NuvioMedia/NuvioMobile/releases/latest).
|
||||
|
||||
### iOS
|
||||
|
||||
- [TestFlight](https://testflight.apple.com/join/u4y7MHK9)
|
||||
|
||||
## Development
|
||||
|
||||
Desktop development checkout:
|
||||
|
||||
```bash
|
||||
git clone --branch Dev --recurse-submodules https://github.com/NuvioMedia/NuvioDesktop.git
|
||||
cd NuvioDesktop
|
||||
git submodule update --init --recursive MPVKit
|
||||
git -C MPVKit fetch origin Nuvio
|
||||
git -C MPVKit switch Nuvio
|
||||
git -C MPVKit pull --ff-only
|
||||
./gradlew :composeApp:run
|
||||
git clone https://github.com/NuvioMedia/NuvioMobile.git
|
||||
cd NuvioMobile
|
||||
./scripts/run-mobile.sh android
|
||||
# or
|
||||
./scripts/run-mobile.sh ios
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `composeApp/` contains the shared Kotlin Multiplatform and Compose Multiplatform app code.
|
||||
- `composeApp/src/commonMain/` contains shared UI, features, repositories, and platform-agnostic logic.
|
||||
- `composeApp/src/androidMain/` contains Android-specific integrations.
|
||||
- `composeApp/src/iosMain/` contains iOS-specific integrations.
|
||||
- `iosApp/` contains the native Xcode project and iOS entry point.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
./gradlew :composeApp:compileKotlinDesktop
|
||||
./gradlew :composeApp:packageDistributionForCurrentOS
|
||||
./gradlew :composeApp:assembleDebug
|
||||
./gradlew :composeApp:compileKotlinIosSimulatorArm64
|
||||
./scripts/build-distribution.sh
|
||||
```
|
||||
|
||||
On macOS, the native player bridge uses MPVKit libmpv artifacts from the `MPVKit` submodule. For development, the submodule is configured to use [NuvioMedia/MPVKit](https://github.com/NuvioMedia/MPVKit) on the `Nuvio` branch. If you already have a checkout, sync and update it with:
|
||||
|
||||
```bash
|
||||
git submodule sync MPVKit
|
||||
git submodule update --init --recursive MPVKit
|
||||
git -C MPVKit remote set-url origin https://github.com/NuvioMedia/MPVKit.git
|
||||
git -C MPVKit fetch origin Nuvio
|
||||
git -C MPVKit switch Nuvio
|
||||
git -C MPVKit pull --ff-only
|
||||
```
|
||||
|
||||
If Gradle reports missing MPVKit artifacts, build the macOS runtime before running the app:
|
||||
|
||||
```bash
|
||||
cd MPVKit
|
||||
make build platform=macos
|
||||
```
|
||||
|
||||
You can also point Gradle at a separate MPVKit checkout:
|
||||
|
||||
```bash
|
||||
./gradlew :composeApp:run -Pnuvio.mpvkit.dir=/absolute/path/to/MPVKit
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `composeApp/` contains the Kotlin Multiplatform and Compose Multiplatform app code.
|
||||
- `composeApp/src/commonMain/` contains shared UI, features, repositories, and platform-agnostic logic.
|
||||
- `composeApp/src/desktopMain/` contains desktop-specific app code, storage, settings, player integration, and desktop resources.
|
||||
- `composeApp/src/desktopMain/native/macos/` and `composeApp/src/desktopMain/native/windows/` contain the native player bridges.
|
||||
- `composeApp/src/desktopMain/resources/player-ui/` contains the desktop native player control UI.
|
||||
- `composeApp/src/desktopMain/resources/icons/` contains desktop app icons for macOS, Windows, and Linux packaging.
|
||||
|
||||
## Desktop Player
|
||||
|
||||
Nuvio Desktop uses a native desktop player path with MPVKit/libmpv integration and desktop-owned controls. The desktop player is separate from the mobile Compose player surface so desktop behavior can match desktop input, keyboard, windowing, and playback expectations.
|
||||
Versioning is driven from `iosApp/Configuration/Version.xcconfig`, which is used as the shared source of truth for both iOS and Android builds.
|
||||
|
||||
## Legal & DMCA
|
||||
|
||||
|
|
@ -94,15 +68,34 @@ Nuvio functions solely as a client-side interface for browsing metadata and play
|
|||
|
||||
Nuvio is not affiliated with any third-party extensions, catalogs, sources, or content providers. It does not host, store, or distribute any media content.
|
||||
|
||||
For comprehensive legal information, including the full disclaimer, third-party extension policy, and DMCA/Copyright information, please visit the [Legal & Disclaimer Page](https://nuvioapp.space/legal).
|
||||
For comprehensive legal information, including our full disclaimer, third-party extension policy, and DMCA/Copyright information, please visit our [Legal & Disclaimer Page](https://nuvioapp.space/legal).
|
||||
|
||||
## Built With
|
||||
|
||||
- Kotlin Multiplatform
|
||||
- Compose Multiplatform for Desktop
|
||||
- Compose Multiplatform
|
||||
- Kotlin
|
||||
- MPVKit and libmpv
|
||||
- AndroidX Media3
|
||||
- AVFoundation and native iOS integrations
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#NuvioMedia/NuvioMobile&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=NuvioMedia/NuvioMobile&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=NuvioMedia/NuvioMobile&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=NuvioMedia/NuvioMobile&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
[license-shield]: https://img.shields.io/github/license/NuvioMedia/NuvioDesktop.svg?style=for-the-badge
|
||||
[license-url]: https://github.com/NuvioMedia/NuvioDesktop/blob/Dev/LICENSE
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/NuvioMedia/NuvioMobile.svg?style=for-the-badge
|
||||
[contributors-url]: https://github.com/NuvioMedia/NuvioMobile/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/NuvioMedia/NuvioMobile.svg?style=for-the-badge
|
||||
[forks-url]: https://github.com/NuvioMedia/NuvioMobile/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/NuvioMedia/NuvioMobile.svg?style=for-the-badge
|
||||
[stars-url]: https://github.com/NuvioMedia/NuvioMobile/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/NuvioMedia/NuvioMobile.svg?style=for-the-badge
|
||||
[issues-url]: https://github.com/NuvioMedia/NuvioMobile/issues
|
||||
[license-shield]: https://img.shields.io/github/license/NuvioMedia/NuvioMobile.svg?style=for-the-badge
|
||||
[license-url]: https://github.com/NuvioMedia/NuvioMobile/blob/main/LICENSE
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
package com.nuvio.app.core.sync
|
||||
|
||||
internal const val MOBILE_SYNC_PLATFORM = "mobile"
|
||||
internal const val HOME_CATALOG_SHARED_SYNC_PLATFORM = "home_catalog_shared"
|
||||
internal val HOME_CATALOG_LEGACY_SYNC_PLATFORMS = listOf(MOBILE_SYNC_PLATFORM, "tv")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ package com.nuvio.app.features.home
|
|||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.core.auth.AuthRepository
|
||||
import com.nuvio.app.core.auth.AuthState
|
||||
import com.nuvio.app.core.sync.MOBILE_SYNC_PLATFORM
|
||||
import com.nuvio.app.core.sync.HOME_CATALOG_LEGACY_SYNC_PLATFORMS
|
||||
import com.nuvio.app.core.sync.HOME_CATALOG_SHARED_SYNC_PLATFORM
|
||||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
|
|
@ -25,6 +26,7 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
@Serializable
|
||||
|
|
@ -53,6 +55,14 @@ private data class SupabaseHomeCatalogSettingsBlob(
|
|||
@SerialName("updated_at") val updatedAt: String? = null,
|
||||
)
|
||||
|
||||
private data class RemoteHomeCatalogSettings(
|
||||
val platform: String,
|
||||
val payload: SyncHomeCatalogPayload,
|
||||
val updatedAt: String?,
|
||||
val hasHideUnreleasedContent: Boolean,
|
||||
val hasHideCatalogUnderline: Boolean,
|
||||
)
|
||||
|
||||
object HomeCatalogSettingsSyncService {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val log = Logger.withTag("HomeCatalogSettingsSyncService")
|
||||
|
|
@ -62,6 +72,8 @@ object HomeCatalogSettingsSyncService {
|
|||
}
|
||||
|
||||
private const val PUSH_DEBOUNCE_MS = 1500L
|
||||
private const val HIDE_UNRELEASED_CONTENT_KEY = "hide_unreleased_content"
|
||||
private const val HIDE_CATALOG_UNDERLINE_KEY = "hide_catalog_underline"
|
||||
|
||||
@Volatile
|
||||
var isSyncingFromRemote: Boolean = false
|
||||
|
|
@ -76,31 +88,18 @@ object HomeCatalogSettingsSyncService {
|
|||
|
||||
suspend fun pullFromServer(profileId: Int) {
|
||||
runCatching {
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_platform", MOBILE_SYNC_PLATFORM)
|
||||
}
|
||||
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_home_catalog_settings", params)
|
||||
val blobs = result.decodeList<SupabaseHomeCatalogSettingsBlob>()
|
||||
val blob = blobs.firstOrNull()
|
||||
val localPayload = HomeCatalogSettingsRepository.exportToSyncPayload()
|
||||
val remote = fetchBestRemotePayload(profileId, localPayload)
|
||||
|
||||
if (blob == null) {
|
||||
if (remote == null) {
|
||||
log.i { "pullFromServer — no remote home catalog settings found" }
|
||||
val localPayload = HomeCatalogSettingsRepository.exportToSyncPayload()
|
||||
if (localPayload.items.isNotEmpty()) {
|
||||
pushToRemote(profileId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val remotePayload = runCatching {
|
||||
json.decodeFromJsonElement(SyncHomeCatalogPayload.serializer(), blob.settingsJson)
|
||||
}.getOrNull()
|
||||
|
||||
if (remotePayload == null) {
|
||||
log.w { "pullFromServer — failed to parse remote home catalog settings" }
|
||||
return
|
||||
}
|
||||
val remotePayload = remote.payload
|
||||
|
||||
if (remotePayload.items.isEmpty()) {
|
||||
log.i { "pullFromServer — remote has empty items, preserving local catalog order" }
|
||||
|
|
@ -118,6 +117,9 @@ object HomeCatalogSettingsSyncService {
|
|||
HomeCatalogSettingsRepository.applyFromRemote(remotePayload)
|
||||
isSyncingFromRemote = false
|
||||
log.i { "pullFromServer — applied ${remotePayload.items.size} items from remote" }
|
||||
if (remote.platform != HOME_CATALOG_SHARED_SYNC_PLATFORM) {
|
||||
pushToRemote(profileId)
|
||||
}
|
||||
}.onFailure { e ->
|
||||
isSyncingFromRemote = false
|
||||
log.e(e) { "pullFromServer — FAILED" }
|
||||
|
|
@ -142,11 +144,11 @@ object HomeCatalogSettingsSyncService {
|
|||
private suspend fun pushToRemote(profileId: Int) {
|
||||
runCatching {
|
||||
val payload = HomeCatalogSettingsRepository.exportToSyncPayload()
|
||||
val jsonElement = json.encodeToJsonElement(SyncHomeCatalogPayload.serializer(), payload)
|
||||
val jsonElement = mergedSharedPayloadJson(profileId, payload)
|
||||
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_platform", MOBILE_SYNC_PLATFORM)
|
||||
put("p_platform", HOME_CATALOG_SHARED_SYNC_PLATFORM)
|
||||
put("p_settings_json", jsonElement)
|
||||
}
|
||||
SupabaseProvider.client.postgrest.rpc("sync_push_home_catalog_settings", params)
|
||||
|
|
@ -172,4 +174,114 @@ object HomeCatalogSettingsSyncService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchBestRemotePayload(
|
||||
profileId: Int,
|
||||
localPayload: SyncHomeCatalogPayload,
|
||||
): RemoteHomeCatalogSettings? {
|
||||
val shared = fetchRemotePayload(
|
||||
profileId = profileId,
|
||||
platform = HOME_CATALOG_SHARED_SYNC_PLATFORM,
|
||||
localPayload = localPayload,
|
||||
)
|
||||
val legacyRows = HOME_CATALOG_LEGACY_SYNC_PLATFORMS
|
||||
.mapNotNull { platform ->
|
||||
fetchRemotePayload(
|
||||
profileId = profileId,
|
||||
platform = platform,
|
||||
localPayload = localPayload,
|
||||
)
|
||||
}
|
||||
val rows = listOfNotNull(shared) + legacyRows
|
||||
val selected = rows
|
||||
.filter { it.payload.items.isNotEmpty() }
|
||||
.maxByOrNull { it.updatedAt.orEmpty() }
|
||||
?: shared
|
||||
?: legacyRows.maxByOrNull { it.updatedAt.orEmpty() }
|
||||
|
||||
return selected?.withNewestStandaloneSettings(rows)
|
||||
}
|
||||
|
||||
private suspend fun fetchRemotePayload(
|
||||
profileId: Int,
|
||||
platform: String,
|
||||
localPayload: SyncHomeCatalogPayload,
|
||||
): RemoteHomeCatalogSettings? {
|
||||
val blob = fetchRemoteBlob(profileId, platform) ?: return null
|
||||
val payload = decodePayloadPreservingLocalDefaults(blob.settingsJson, localPayload)
|
||||
if (payload == null) {
|
||||
log.w { "pullFromServer — failed to parse remote home catalog settings for platform=$platform" }
|
||||
return null
|
||||
}
|
||||
return RemoteHomeCatalogSettings(
|
||||
platform = platform,
|
||||
payload = payload,
|
||||
updatedAt = blob.updatedAt,
|
||||
hasHideUnreleasedContent = blob.settingsJson.containsKey(HIDE_UNRELEASED_CONTENT_KEY),
|
||||
hasHideCatalogUnderline = blob.settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY),
|
||||
)
|
||||
}
|
||||
|
||||
private fun RemoteHomeCatalogSettings.withNewestStandaloneSettings(
|
||||
rows: List<RemoteHomeCatalogSettings>,
|
||||
): RemoteHomeCatalogSettings {
|
||||
val hideUnreleasedSource = rows
|
||||
.filter { it.hasHideUnreleasedContent }
|
||||
.maxByOrNull { it.updatedAt.orEmpty() }
|
||||
val hideUnderlineSource = rows
|
||||
.filter { it.hasHideCatalogUnderline }
|
||||
.maxByOrNull { it.updatedAt.orEmpty() }
|
||||
|
||||
return copy(
|
||||
payload = payload.copy(
|
||||
hideUnreleasedContent = hideUnreleasedSource?.payload?.hideUnreleasedContent
|
||||
?: payload.hideUnreleasedContent,
|
||||
hideCatalogUnderline = hideUnderlineSource?.payload?.hideCatalogUnderline
|
||||
?: payload.hideCatalogUnderline,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchRemoteBlob(
|
||||
profileId: Int,
|
||||
platform: String,
|
||||
): SupabaseHomeCatalogSettingsBlob? {
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_platform", platform)
|
||||
}
|
||||
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_home_catalog_settings", params)
|
||||
return result.decodeList<SupabaseHomeCatalogSettingsBlob>().firstOrNull()
|
||||
}
|
||||
|
||||
private fun decodePayloadPreservingLocalDefaults(
|
||||
settingsJson: JsonObject,
|
||||
localPayload: SyncHomeCatalogPayload,
|
||||
): SyncHomeCatalogPayload? = runCatching {
|
||||
val decoded = json.decodeFromJsonElement(SyncHomeCatalogPayload.serializer(), settingsJson)
|
||||
decoded.copy(
|
||||
hideUnreleasedContent = if (settingsJson.containsKey(HIDE_UNRELEASED_CONTENT_KEY)) {
|
||||
decoded.hideUnreleasedContent
|
||||
} else {
|
||||
localPayload.hideUnreleasedContent
|
||||
},
|
||||
hideCatalogUnderline = if (settingsJson.containsKey(HIDE_CATALOG_UNDERLINE_KEY)) {
|
||||
decoded.hideCatalogUnderline
|
||||
} else {
|
||||
localPayload.hideCatalogUnderline
|
||||
},
|
||||
)
|
||||
}.getOrNull()
|
||||
|
||||
private suspend fun mergedSharedPayloadJson(
|
||||
profileId: Int,
|
||||
payload: SyncHomeCatalogPayload,
|
||||
): JsonObject {
|
||||
val localJson = json.encodeToJsonElement(SyncHomeCatalogPayload.serializer(), payload).jsonObject
|
||||
val remoteJson = fetchRemoteBlob(profileId, HOME_CATALOG_SHARED_SYNC_PLATFORM)?.settingsJson
|
||||
return buildJsonObject {
|
||||
remoteJson?.forEach { (key, value) -> put(key, value) }
|
||||
localJson.forEach { (key, value) -> put(key, value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ import kotlin.math.roundToInt
|
|||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
private val EmptyPlayerControlsState = PlayerControlsState()
|
||||
private val IgnorePlayerControlsAction: (PlayerControlsAction) -> Boolean = { false }
|
||||
private val IgnorePlayerControlsEvent: (String, Double) -> Boolean = { _, _ -> false }
|
||||
private val IgnorePlayerControlsScrub: (Long) -> Boolean = { false }
|
||||
|
||||
@Composable
|
||||
internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
||||
val runtime = this
|
||||
|
|
@ -106,225 +111,229 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
|||
val openingOverlayWanted = playerSettingsUiState.showLoadingOverlay &&
|
||||
!initialLoadCompleted &&
|
||||
errorMessage == null
|
||||
val episodeText = if (seasonNumber != null && episodeNumber != null && !episodeTitle.isNullOrBlank()) {
|
||||
stringResource(
|
||||
Res.string.compose_player_episode_title_format,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
episodeTitle.orEmpty(),
|
||||
)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val allFilterLabel = stringResource(Res.string.collections_tab_all)
|
||||
val playingLabel = stringResource(Res.string.compose_player_playing)
|
||||
val sourceFilters = buildPlayerControlFilters(
|
||||
allLabel = allFilterLabel,
|
||||
selectedFilter = null,
|
||||
)
|
||||
val sourceItems = buildPlayerControlSourceItems()
|
||||
val episodeItems = buildPlayerControlEpisodeItems()
|
||||
val episodeSeasons = buildPlayerControlSeasonItems(episodeItems)
|
||||
val episodeStreamFilters = buildPlayerControlEpisodeStreamFilters(
|
||||
allLabel = allFilterLabel,
|
||||
selectedFilter = null,
|
||||
)
|
||||
val episodeStreamItems = buildPlayerControlEpisodeStreamItems()
|
||||
val playerControlAddonSubtitles = buildPlayerControlAddonSubtitleItems()
|
||||
val playerControlAutoSyncCues = buildPlayerControlSubtitleCueItems()
|
||||
val themeColors = MaterialTheme.nuvio.colors
|
||||
val selectedEpisodeLabel = episodeStreamsPanelState.selectedEpisode?.let { selected ->
|
||||
val selectedCode = selected.playerControlsEpisodeCode()
|
||||
buildString {
|
||||
append(selectedCode)
|
||||
if (selected.title.isNotBlank()) {
|
||||
if (isNotEmpty()) append(" • ")
|
||||
append(selected.title)
|
||||
}
|
||||
}
|
||||
}.orEmpty()
|
||||
val nativeSkipInterval = activeSkipInterval.takeIf { initialLoadCompleted && !pausedOverlayVisible }
|
||||
val nextEpisodeForControls = nextEpisodeInfo.takeIf { isSeries && showNextEpisodeCard }
|
||||
val nextEpisodeStatus = when {
|
||||
nextEpisodeForControls == null -> ""
|
||||
!nextEpisodeForControls.hasAired && !nextEpisodeForControls.unairedMessage.isNullOrBlank() ->
|
||||
nextEpisodeForControls.unairedMessage.orEmpty()
|
||||
nextEpisodeAutoPlaySearching -> stringResource(Res.string.player_next_episode_finding_source)
|
||||
!nextEpisodeAutoPlaySourceName.isNullOrBlank() && nextEpisodeAutoPlayCountdown != null ->
|
||||
stringResource(
|
||||
Res.string.player_next_episode_playing_via_countdown,
|
||||
nextEpisodeAutoPlaySourceName.orEmpty(),
|
||||
nextEpisodeAutoPlayCountdown ?: 0,
|
||||
)
|
||||
else -> ""
|
||||
}
|
||||
val playerControlsState = PlayerControlsState(
|
||||
title = title,
|
||||
episodeText = episodeText,
|
||||
streamTitle = activeStreamTitle,
|
||||
providerName = activeProviderName,
|
||||
pauseOverlayWatchingLabel = stringResource(Res.string.compose_player_youre_watching),
|
||||
pauseOverlayLogo = logo,
|
||||
pauseOverlayEpisodeInfo = if (seasonNumber != null && episodeNumber != null) {
|
||||
stringResource(Res.string.compose_player_episode_code_full, seasonNumber, episodeNumber)
|
||||
} else {
|
||||
activeProviderName
|
||||
},
|
||||
pauseOverlayEpisodeTitle = activeEpisodeTitle.orEmpty(),
|
||||
pauseOverlayDescription = (pauseDescription ?: activeStreamSubtitle).orEmpty(),
|
||||
resizeModeLabel = stringResource(resizeMode.labelRes),
|
||||
playbackSpeedLabel = formatPlaybackSpeedLabel(playbackSnapshot.playbackSpeed),
|
||||
subtitlesLabel = stringResource(Res.string.compose_player_subs),
|
||||
audioLabel = stringResource(Res.string.compose_player_audio),
|
||||
sourcesLabel = stringResource(Res.string.compose_player_sources),
|
||||
episodesLabel = stringResource(Res.string.compose_player_episodes),
|
||||
externalPlayerLabel = stringResource(Res.string.streams_open_external_player),
|
||||
playLabel = stringResource(Res.string.detail_btn_play),
|
||||
pauseLabel = stringResource(Res.string.compose_action_pause),
|
||||
closeLabel = stringResource(Res.string.compose_player_close),
|
||||
lockLabel = stringResource(Res.string.compose_player_lock_controls),
|
||||
unlockLabel = stringResource(Res.string.compose_player_unlock_controls),
|
||||
submitIntroLabel = stringResource(Res.string.submit_intro_action),
|
||||
videoSettingsLabel = stringResource(Res.string.player_action_video_settings),
|
||||
tapToUnlockLabel = stringResource(Res.string.compose_player_tap_to_unlock),
|
||||
playbackErrorTitle = stringResource(Res.string.compose_player_playback_error),
|
||||
playbackErrorMessage = errorMessage.orEmpty(),
|
||||
playbackErrorActionLabel = stringResource(Res.string.compose_player_go_back),
|
||||
sourcesPanelTitle = stringResource(Res.string.compose_player_panel_sources),
|
||||
episodesPanelTitle = stringResource(Res.string.compose_player_panel_episodes),
|
||||
streamsPanelTitle = stringResource(Res.string.compose_player_panel_streams),
|
||||
allFilterLabel = allFilterLabel,
|
||||
reloadLabel = stringResource(Res.string.compose_action_reload),
|
||||
backLabel = stringResource(Res.string.action_back),
|
||||
panelCloseLabel = stringResource(Res.string.action_close),
|
||||
cancelLabel = stringResource(Res.string.action_cancel),
|
||||
playingLabel = playingLabel,
|
||||
noStreamsLabel = stringResource(Res.string.compose_player_no_streams_found),
|
||||
noEpisodesLabel = stringResource(Res.string.compose_player_no_episodes_available),
|
||||
submitIntroPanelTitle = stringResource(Res.string.submit_intro_title),
|
||||
submitIntroSegmentTypeLabel = stringResource(Res.string.submit_intro_segment_type_label),
|
||||
submitIntroSegmentIntroLabel = stringResource(Res.string.submit_intro_segment_intro),
|
||||
submitIntroSegmentRecapLabel = stringResource(Res.string.submit_intro_segment_recap),
|
||||
submitIntroSegmentOutroLabel = stringResource(Res.string.submit_intro_segment_outro),
|
||||
submitIntroStartTimeLabel = stringResource(Res.string.submit_intro_start_time_label),
|
||||
submitIntroEndTimeLabel = stringResource(Res.string.submit_intro_end_time_label),
|
||||
submitIntroCaptureLabel = stringResource(Res.string.submit_intro_capture_button),
|
||||
submitIntroSubmitLabel = stringResource(Res.string.submit_intro_button_submit),
|
||||
p2pConsentTitle = stringResource(Res.string.p2p_consent_title),
|
||||
p2pConsentBody = stringResource(Res.string.p2p_consent_body),
|
||||
p2pConsentEnableLabel = stringResource(Res.string.p2p_consent_enable),
|
||||
p2pConsentCancelLabel = stringResource(Res.string.p2p_consent_cancel),
|
||||
subtitlesPanelTitle = stringResource(Res.string.compose_player_subtitles),
|
||||
subtitleBuiltInTabLabel = stringResource(Res.string.compose_player_built_in),
|
||||
subtitleAddonsTabLabel = stringResource(Res.string.addon_title),
|
||||
subtitleStyleTabLabel = stringResource(Res.string.compose_player_style),
|
||||
noneLabel = stringResource(Res.string.compose_player_none),
|
||||
fetchSubtitlesLabel = stringResource(Res.string.compose_player_fetch_subtitles),
|
||||
subtitleDelayLabel = stringResource(Res.string.compose_player_subtitle_delay),
|
||||
resetLabel = stringResource(Res.string.compose_player_reset),
|
||||
autoSyncLabel = stringResource(Res.string.compose_player_auto_sync),
|
||||
reloadSmallLabel = stringResource(Res.string.compose_player_reload),
|
||||
captureLineLabel = stringResource(Res.string.compose_player_capture_line),
|
||||
selectAddonSubtitleFirstLabel = stringResource(Res.string.compose_player_select_addon_subtitle_first),
|
||||
loadingSubtitleLinesLabel = stringResource(Res.string.compose_player_loading_lines),
|
||||
fontSizeLabel = stringResource(Res.string.compose_player_font_size),
|
||||
outlineLabel = stringResource(Res.string.compose_player_outline),
|
||||
boldLabel = stringResource(Res.string.compose_player_bold),
|
||||
bottomOffsetLabel = stringResource(Res.string.compose_player_bottom_offset),
|
||||
colorLabel = stringResource(Res.string.compose_player_color),
|
||||
textOpacityLabel = stringResource(Res.string.compose_player_text_opacity),
|
||||
outlineColorLabel = stringResource(Res.string.compose_player_outline_color),
|
||||
resetDefaultsLabel = stringResource(Res.string.compose_player_reset_defaults),
|
||||
onLabel = stringResource(Res.string.compose_action_on),
|
||||
offLabel = stringResource(Res.string.compose_action_off),
|
||||
themeAccentColor = themeColors.accent.toCssColorString(),
|
||||
themeAccentStrongColor = themeColors.accentStrong.toCssColorString(),
|
||||
themeOnAccentColor = themeColors.onAccent.toCssColorString(),
|
||||
themeFocusColor = themeColors.focusRing.toCssColorString(),
|
||||
themeSelectedSurfaceColor = themeColors.accent.copy(alpha = 0.24f).toCssColorString(),
|
||||
themeSelectedSurfaceHoverColor = themeColors.accent.copy(alpha = 0.34f).toCssColorString(),
|
||||
themeSelectedRingColor = themeColors.accent.copy(alpha = 0.35f).toCssColorString(),
|
||||
themeTimelineFillColor = themeColors.playerTimelineFill.toCssColorString(),
|
||||
themeTimelineTrackColor = themeColors.playerTimelineTrack.toCssColorString(),
|
||||
themeBufferingColor = themeColors.playerBuffering.toCssColorString(),
|
||||
themeBufferingTrackColor = themeColors.playerBuffering.copy(alpha = 0.28f).toCssColorString(),
|
||||
themeControlForegroundColor = themeColors.playerControlsForeground.toCssColorString(),
|
||||
isPlaying = playbackSnapshot.isPlaying,
|
||||
isLoading = playbackSnapshot.isLoading,
|
||||
isLocked = playerControlsLocked,
|
||||
lockedOverlayVisible = lockedOverlayVisible,
|
||||
controlsVisible = controlsVisible && !playerControlsLocked,
|
||||
parentalWarnings = parentalWarnings,
|
||||
showParentalGuide = showParentalGuide,
|
||||
showSubmitIntro = isSeries &&
|
||||
playerSettingsUiState.introSubmitEnabled &&
|
||||
playerSettingsUiState.introDbApiKey.isNotBlank() &&
|
||||
!activeSubmitIntroImdbId().isNullOrBlank(),
|
||||
showVideoSettings = isIos,
|
||||
showSources = activeVideoId != null,
|
||||
showEpisodes = isSeries,
|
||||
showExternalPlayer = args.onOpenInExternalPlayer != null,
|
||||
durationMs = playbackSnapshot.durationMs,
|
||||
positionMs = displayedPositionMs,
|
||||
sourceIsLoading = sourceStreamsState.isAnyLoading,
|
||||
sourceFilters = sourceFilters,
|
||||
sourceItems = sourceItems,
|
||||
episodeItems = episodeItems,
|
||||
episodeSeasons = episodeSeasons,
|
||||
episodeStreamsVisible = episodeStreamsPanelState.showStreams,
|
||||
episodeStreamsIsLoading = episodeStreamsRepoState.isAnyLoading,
|
||||
selectedEpisodeLabel = selectedEpisodeLabel,
|
||||
episodeStreamFilters = episodeStreamFilters,
|
||||
episodeStreamItems = episodeStreamItems,
|
||||
submitIntroSegmentType = submitIntroSegmentType,
|
||||
submitIntroStartTime = submitIntroStartTimeStr,
|
||||
submitIntroEndTime = submitIntroEndTimeStr,
|
||||
isSubmitIntroSubmitting = isSubmitIntroSubmitting,
|
||||
submitIntroStatusMessage = submitIntroStatusMessage.orEmpty(),
|
||||
showP2pConsent = playerControlsPendingP2pSwitch != null,
|
||||
subtitleActiveTab = activeSubtitleTab.name,
|
||||
addonSubtitleItems = playerControlAddonSubtitles,
|
||||
isLoadingAddonSubtitles = isLoadingAddonSubtitles,
|
||||
selectedAddonSubtitleId = selectedAddonSubtitleId.orEmpty(),
|
||||
useCustomSubtitles = useCustomSubtitles,
|
||||
subtitleStyle = subtitleStyle,
|
||||
subtitleDelayMs = subtitleDelayMs,
|
||||
hasSelectedAddonSubtitle = selectedAddonSubtitle != null,
|
||||
subtitleAutoSyncCapturedPositionMs = subtitleAutoSyncState.capturedPositionMs ?: -1L,
|
||||
subtitleAutoSyncCues = playerControlAutoSyncCues,
|
||||
subtitleAutoSyncIsLoading = subtitleAutoSyncState.isLoading,
|
||||
subtitleAutoSyncErrorMessage = subtitleAutoSyncState.errorMessage.orEmpty(),
|
||||
closeModalsToken = playerControlsCloseModalsToken,
|
||||
showOpeningOverlay = openingOverlayWanted,
|
||||
openingArtwork = background ?: poster,
|
||||
openingLogo = logo,
|
||||
openingTitle = title,
|
||||
openingMessage = p2pInitialLoadingMessage,
|
||||
openingProgress = p2pInitialLoadingProgress,
|
||||
skipPromptVisible = nativeSkipInterval != null && !playerControlsLocked,
|
||||
skipPromptLabel = skipPromptLabel(nativeSkipInterval?.type),
|
||||
skipPromptStartMs = ((nativeSkipInterval?.startTime ?: 0.0) * 1000).toLong().coerceAtLeast(0L),
|
||||
skipPromptEndMs = ((nativeSkipInterval?.endTime ?: 0.0) * 1000).toLong().coerceAtLeast(0L),
|
||||
skipPromptDismissed = skipIntervalDismissed,
|
||||
nextEpisodeVisible = nextEpisodeForControls != null && !playerControlsLocked,
|
||||
nextEpisodeHeaderLabel = stringResource(Res.string.player_next_episode),
|
||||
nextEpisodeTitle = nextEpisodeForControls?.let {
|
||||
val playerControlsState = if (isDesktop) {
|
||||
val episodeText = if (seasonNumber != null && episodeNumber != null && !episodeTitle.isNullOrBlank()) {
|
||||
stringResource(
|
||||
Res.string.compose_player_episode_title_format,
|
||||
it.season,
|
||||
it.episode,
|
||||
it.title,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
episodeTitle.orEmpty(),
|
||||
)
|
||||
}.orEmpty(),
|
||||
nextEpisodeThumbnail = nextEpisodeForControls?.thumbnail.orEmpty(),
|
||||
nextEpisodeStatus = nextEpisodeStatus,
|
||||
nextEpisodeActionLabel = if (nextEpisodeForControls?.hasAired == true) {
|
||||
stringResource(Res.string.detail_btn_play)
|
||||
} else {
|
||||
stringResource(Res.string.player_next_episode_unaired)
|
||||
},
|
||||
nextEpisodePlayable = nextEpisodeForControls?.hasAired == true,
|
||||
)
|
||||
""
|
||||
}
|
||||
val allFilterLabel = stringResource(Res.string.collections_tab_all)
|
||||
val playingLabel = stringResource(Res.string.compose_player_playing)
|
||||
val sourceFilters = buildPlayerControlFilters(
|
||||
allLabel = allFilterLabel,
|
||||
selectedFilter = null,
|
||||
)
|
||||
val sourceItems = buildPlayerControlSourceItems()
|
||||
val episodeItems = buildPlayerControlEpisodeItems()
|
||||
val episodeSeasons = buildPlayerControlSeasonItems(episodeItems)
|
||||
val episodeStreamFilters = buildPlayerControlEpisodeStreamFilters(
|
||||
allLabel = allFilterLabel,
|
||||
selectedFilter = null,
|
||||
)
|
||||
val episodeStreamItems = buildPlayerControlEpisodeStreamItems()
|
||||
val playerControlAddonSubtitles = buildPlayerControlAddonSubtitleItems()
|
||||
val playerControlAutoSyncCues = buildPlayerControlSubtitleCueItems()
|
||||
val themeColors = MaterialTheme.nuvio.colors
|
||||
val selectedEpisodeLabel = episodeStreamsPanelState.selectedEpisode?.let { selected ->
|
||||
val selectedCode = selected.playerControlsEpisodeCode()
|
||||
buildString {
|
||||
append(selectedCode)
|
||||
if (selected.title.isNotBlank()) {
|
||||
if (isNotEmpty()) append(" • ")
|
||||
append(selected.title)
|
||||
}
|
||||
}
|
||||
}.orEmpty()
|
||||
val nativeSkipInterval = activeSkipInterval.takeIf { initialLoadCompleted && !pausedOverlayVisible }
|
||||
val nextEpisodeForControls = nextEpisodeInfo.takeIf { isSeries && showNextEpisodeCard }
|
||||
val nextEpisodeStatus = when {
|
||||
nextEpisodeForControls == null -> ""
|
||||
!nextEpisodeForControls.hasAired && !nextEpisodeForControls.unairedMessage.isNullOrBlank() ->
|
||||
nextEpisodeForControls.unairedMessage.orEmpty()
|
||||
nextEpisodeAutoPlaySearching -> stringResource(Res.string.player_next_episode_finding_source)
|
||||
!nextEpisodeAutoPlaySourceName.isNullOrBlank() && nextEpisodeAutoPlayCountdown != null ->
|
||||
stringResource(
|
||||
Res.string.player_next_episode_playing_via_countdown,
|
||||
nextEpisodeAutoPlaySourceName.orEmpty(),
|
||||
nextEpisodeAutoPlayCountdown ?: 0,
|
||||
)
|
||||
else -> ""
|
||||
}
|
||||
PlayerControlsState(
|
||||
title = title,
|
||||
episodeText = episodeText,
|
||||
streamTitle = activeStreamTitle,
|
||||
providerName = activeProviderName,
|
||||
pauseOverlayWatchingLabel = stringResource(Res.string.compose_player_youre_watching),
|
||||
pauseOverlayLogo = logo,
|
||||
pauseOverlayEpisodeInfo = if (seasonNumber != null && episodeNumber != null) {
|
||||
stringResource(Res.string.compose_player_episode_code_full, seasonNumber, episodeNumber)
|
||||
} else {
|
||||
activeProviderName
|
||||
},
|
||||
pauseOverlayEpisodeTitle = activeEpisodeTitle.orEmpty(),
|
||||
pauseOverlayDescription = (pauseDescription ?: activeStreamSubtitle).orEmpty(),
|
||||
resizeModeLabel = stringResource(resizeMode.labelRes),
|
||||
playbackSpeedLabel = formatPlaybackSpeedLabel(playbackSnapshot.playbackSpeed),
|
||||
subtitlesLabel = stringResource(Res.string.compose_player_subs),
|
||||
audioLabel = stringResource(Res.string.compose_player_audio),
|
||||
sourcesLabel = stringResource(Res.string.compose_player_sources),
|
||||
episodesLabel = stringResource(Res.string.compose_player_episodes),
|
||||
externalPlayerLabel = stringResource(Res.string.streams_open_external_player),
|
||||
playLabel = stringResource(Res.string.detail_btn_play),
|
||||
pauseLabel = stringResource(Res.string.compose_action_pause),
|
||||
closeLabel = stringResource(Res.string.compose_player_close),
|
||||
lockLabel = stringResource(Res.string.compose_player_lock_controls),
|
||||
unlockLabel = stringResource(Res.string.compose_player_unlock_controls),
|
||||
submitIntroLabel = stringResource(Res.string.submit_intro_action),
|
||||
videoSettingsLabel = stringResource(Res.string.player_action_video_settings),
|
||||
tapToUnlockLabel = stringResource(Res.string.compose_player_tap_to_unlock),
|
||||
playbackErrorTitle = stringResource(Res.string.compose_player_playback_error),
|
||||
playbackErrorMessage = errorMessage.orEmpty(),
|
||||
playbackErrorActionLabel = stringResource(Res.string.compose_player_go_back),
|
||||
sourcesPanelTitle = stringResource(Res.string.compose_player_panel_sources),
|
||||
episodesPanelTitle = stringResource(Res.string.compose_player_panel_episodes),
|
||||
streamsPanelTitle = stringResource(Res.string.compose_player_panel_streams),
|
||||
allFilterLabel = allFilterLabel,
|
||||
reloadLabel = stringResource(Res.string.compose_action_reload),
|
||||
backLabel = stringResource(Res.string.action_back),
|
||||
panelCloseLabel = stringResource(Res.string.action_close),
|
||||
cancelLabel = stringResource(Res.string.action_cancel),
|
||||
playingLabel = playingLabel,
|
||||
noStreamsLabel = stringResource(Res.string.compose_player_no_streams_found),
|
||||
noEpisodesLabel = stringResource(Res.string.compose_player_no_episodes_available),
|
||||
submitIntroPanelTitle = stringResource(Res.string.submit_intro_title),
|
||||
submitIntroSegmentTypeLabel = stringResource(Res.string.submit_intro_segment_type_label),
|
||||
submitIntroSegmentIntroLabel = stringResource(Res.string.submit_intro_segment_intro),
|
||||
submitIntroSegmentRecapLabel = stringResource(Res.string.submit_intro_segment_recap),
|
||||
submitIntroSegmentOutroLabel = stringResource(Res.string.submit_intro_segment_outro),
|
||||
submitIntroStartTimeLabel = stringResource(Res.string.submit_intro_start_time_label),
|
||||
submitIntroEndTimeLabel = stringResource(Res.string.submit_intro_end_time_label),
|
||||
submitIntroCaptureLabel = stringResource(Res.string.submit_intro_capture_button),
|
||||
submitIntroSubmitLabel = stringResource(Res.string.submit_intro_button_submit),
|
||||
p2pConsentTitle = stringResource(Res.string.p2p_consent_title),
|
||||
p2pConsentBody = stringResource(Res.string.p2p_consent_body),
|
||||
p2pConsentEnableLabel = stringResource(Res.string.p2p_consent_enable),
|
||||
p2pConsentCancelLabel = stringResource(Res.string.p2p_consent_cancel),
|
||||
subtitlesPanelTitle = stringResource(Res.string.compose_player_subtitles),
|
||||
subtitleBuiltInTabLabel = stringResource(Res.string.compose_player_built_in),
|
||||
subtitleAddonsTabLabel = stringResource(Res.string.addon_title),
|
||||
subtitleStyleTabLabel = stringResource(Res.string.compose_player_style),
|
||||
noneLabel = stringResource(Res.string.compose_player_none),
|
||||
fetchSubtitlesLabel = stringResource(Res.string.compose_player_fetch_subtitles),
|
||||
subtitleDelayLabel = stringResource(Res.string.compose_player_subtitle_delay),
|
||||
resetLabel = stringResource(Res.string.compose_player_reset),
|
||||
autoSyncLabel = stringResource(Res.string.compose_player_auto_sync),
|
||||
reloadSmallLabel = stringResource(Res.string.compose_player_reload),
|
||||
captureLineLabel = stringResource(Res.string.compose_player_capture_line),
|
||||
selectAddonSubtitleFirstLabel = stringResource(Res.string.compose_player_select_addon_subtitle_first),
|
||||
loadingSubtitleLinesLabel = stringResource(Res.string.compose_player_loading_lines),
|
||||
fontSizeLabel = stringResource(Res.string.compose_player_font_size),
|
||||
outlineLabel = stringResource(Res.string.compose_player_outline),
|
||||
boldLabel = stringResource(Res.string.compose_player_bold),
|
||||
bottomOffsetLabel = stringResource(Res.string.compose_player_bottom_offset),
|
||||
colorLabel = stringResource(Res.string.compose_player_color),
|
||||
textOpacityLabel = stringResource(Res.string.compose_player_text_opacity),
|
||||
outlineColorLabel = stringResource(Res.string.compose_player_outline_color),
|
||||
resetDefaultsLabel = stringResource(Res.string.compose_player_reset_defaults),
|
||||
onLabel = stringResource(Res.string.compose_action_on),
|
||||
offLabel = stringResource(Res.string.compose_action_off),
|
||||
themeAccentColor = themeColors.accent.toCssColorString(),
|
||||
themeAccentStrongColor = themeColors.accentStrong.toCssColorString(),
|
||||
themeOnAccentColor = themeColors.onAccent.toCssColorString(),
|
||||
themeFocusColor = themeColors.focusRing.toCssColorString(),
|
||||
themeSelectedSurfaceColor = themeColors.accent.copy(alpha = 0.24f).toCssColorString(),
|
||||
themeSelectedSurfaceHoverColor = themeColors.accent.copy(alpha = 0.34f).toCssColorString(),
|
||||
themeSelectedRingColor = themeColors.accent.copy(alpha = 0.35f).toCssColorString(),
|
||||
themeTimelineFillColor = themeColors.playerTimelineFill.toCssColorString(),
|
||||
themeTimelineTrackColor = themeColors.playerTimelineTrack.toCssColorString(),
|
||||
themeBufferingColor = themeColors.playerBuffering.toCssColorString(),
|
||||
themeBufferingTrackColor = themeColors.playerBuffering.copy(alpha = 0.28f).toCssColorString(),
|
||||
themeControlForegroundColor = themeColors.playerControlsForeground.toCssColorString(),
|
||||
isPlaying = playbackSnapshot.isPlaying,
|
||||
isLoading = playbackSnapshot.isLoading,
|
||||
isLocked = playerControlsLocked,
|
||||
lockedOverlayVisible = lockedOverlayVisible,
|
||||
controlsVisible = controlsVisible && !playerControlsLocked,
|
||||
parentalWarnings = parentalWarnings,
|
||||
showParentalGuide = showParentalGuide,
|
||||
showSubmitIntro = isSeries &&
|
||||
playerSettingsUiState.introSubmitEnabled &&
|
||||
playerSettingsUiState.introDbApiKey.isNotBlank() &&
|
||||
!activeSubmitIntroImdbId().isNullOrBlank(),
|
||||
showVideoSettings = isIos,
|
||||
showSources = activeVideoId != null,
|
||||
showEpisodes = isSeries,
|
||||
showExternalPlayer = args.onOpenInExternalPlayer != null,
|
||||
durationMs = playbackSnapshot.durationMs,
|
||||
positionMs = displayedPositionMs,
|
||||
sourceIsLoading = sourceStreamsState.isAnyLoading,
|
||||
sourceFilters = sourceFilters,
|
||||
sourceItems = sourceItems,
|
||||
episodeItems = episodeItems,
|
||||
episodeSeasons = episodeSeasons,
|
||||
episodeStreamsVisible = episodeStreamsPanelState.showStreams,
|
||||
episodeStreamsIsLoading = episodeStreamsRepoState.isAnyLoading,
|
||||
selectedEpisodeLabel = selectedEpisodeLabel,
|
||||
episodeStreamFilters = episodeStreamFilters,
|
||||
episodeStreamItems = episodeStreamItems,
|
||||
submitIntroSegmentType = submitIntroSegmentType,
|
||||
submitIntroStartTime = submitIntroStartTimeStr,
|
||||
submitIntroEndTime = submitIntroEndTimeStr,
|
||||
isSubmitIntroSubmitting = isSubmitIntroSubmitting,
|
||||
submitIntroStatusMessage = submitIntroStatusMessage.orEmpty(),
|
||||
showP2pConsent = playerControlsPendingP2pSwitch != null,
|
||||
subtitleActiveTab = activeSubtitleTab.name,
|
||||
addonSubtitleItems = playerControlAddonSubtitles,
|
||||
isLoadingAddonSubtitles = isLoadingAddonSubtitles,
|
||||
selectedAddonSubtitleId = selectedAddonSubtitleId.orEmpty(),
|
||||
useCustomSubtitles = useCustomSubtitles,
|
||||
subtitleStyle = subtitleStyle,
|
||||
subtitleDelayMs = subtitleDelayMs,
|
||||
hasSelectedAddonSubtitle = selectedAddonSubtitle != null,
|
||||
subtitleAutoSyncCapturedPositionMs = subtitleAutoSyncState.capturedPositionMs ?: -1L,
|
||||
subtitleAutoSyncCues = playerControlAutoSyncCues,
|
||||
subtitleAutoSyncIsLoading = subtitleAutoSyncState.isLoading,
|
||||
subtitleAutoSyncErrorMessage = subtitleAutoSyncState.errorMessage.orEmpty(),
|
||||
closeModalsToken = playerControlsCloseModalsToken,
|
||||
showOpeningOverlay = openingOverlayWanted,
|
||||
openingArtwork = background ?: poster,
|
||||
openingLogo = logo,
|
||||
openingTitle = title,
|
||||
openingMessage = p2pInitialLoadingMessage,
|
||||
openingProgress = p2pInitialLoadingProgress,
|
||||
skipPromptVisible = nativeSkipInterval != null && !playerControlsLocked,
|
||||
skipPromptLabel = skipPromptLabel(nativeSkipInterval?.type),
|
||||
skipPromptStartMs = ((nativeSkipInterval?.startTime ?: 0.0) * 1000).toLong().coerceAtLeast(0L),
|
||||
skipPromptEndMs = ((nativeSkipInterval?.endTime ?: 0.0) * 1000).toLong().coerceAtLeast(0L),
|
||||
skipPromptDismissed = skipIntervalDismissed,
|
||||
nextEpisodeVisible = nextEpisodeForControls != null && !playerControlsLocked,
|
||||
nextEpisodeHeaderLabel = stringResource(Res.string.player_next_episode),
|
||||
nextEpisodeTitle = nextEpisodeForControls?.let {
|
||||
stringResource(
|
||||
Res.string.compose_player_episode_title_format,
|
||||
it.season,
|
||||
it.episode,
|
||||
it.title,
|
||||
)
|
||||
}.orEmpty(),
|
||||
nextEpisodeThumbnail = nextEpisodeForControls?.thumbnail.orEmpty(),
|
||||
nextEpisodeStatus = nextEpisodeStatus,
|
||||
nextEpisodeActionLabel = if (nextEpisodeForControls?.hasAired == true) {
|
||||
stringResource(Res.string.detail_btn_play)
|
||||
} else {
|
||||
stringResource(Res.string.player_next_episode_unaired)
|
||||
},
|
||||
nextEpisodePlayable = nextEpisodeForControls?.hasAired == true,
|
||||
)
|
||||
} else {
|
||||
EmptyPlayerControlsState
|
||||
}
|
||||
val gestureCallbacks = rememberSurfaceGestureCallbacks()
|
||||
|
||||
Box(
|
||||
|
|
@ -369,15 +378,31 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
|||
resizeMode = resizeMode,
|
||||
initialPositionMs = activeInitialPositionMs.takeIf { isDesktop } ?: 0L,
|
||||
playerControlsState = playerControlsState,
|
||||
onPlayerControlsAction = { action -> handlePlayerControlsAction(action) },
|
||||
onPlayerControlsEvent = { type, value -> handlePlayerControlsEvent(type, value) },
|
||||
onPlayerControlsScrubChange = { positionMs ->
|
||||
handlePlayerControlsScrubChange(positionMs)
|
||||
true
|
||||
onPlayerControlsAction = if (isDesktop) {
|
||||
{ action -> handlePlayerControlsAction(action) }
|
||||
} else {
|
||||
IgnorePlayerControlsAction
|
||||
},
|
||||
onPlayerControlsScrubFinished = { positionMs ->
|
||||
handlePlayerControlsScrubFinished(positionMs)
|
||||
true
|
||||
onPlayerControlsEvent = if (isDesktop) {
|
||||
{ type, value -> handlePlayerControlsEvent(type, value) }
|
||||
} else {
|
||||
IgnorePlayerControlsEvent
|
||||
},
|
||||
onPlayerControlsScrubChange = if (isDesktop) {
|
||||
{ positionMs ->
|
||||
handlePlayerControlsScrubChange(positionMs)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
IgnorePlayerControlsScrub
|
||||
},
|
||||
onPlayerControlsScrubFinished = if (isDesktop) {
|
||||
{ positionMs ->
|
||||
handlePlayerControlsScrubFinished(positionMs)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
IgnorePlayerControlsScrub
|
||||
},
|
||||
onControllerReady = { controller ->
|
||||
playerController = controller
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
CURRENT_PROJECT_VERSION=75
|
||||
MARKETING_VERSION=0.2.4
|
||||
CURRENT_PROJECT_VERSION=77
|
||||
MARKETING_VERSION=0.2.5
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue