From 72dfbac9a9dde836489fc3b32835d8bc3ca6f28e Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Tue, 12 May 2026 19:22:54 +0530 Subject: [PATCH] feat(player): Improve source panel source handling --- MPVKit/Sources/DesktopMPVBridge/Bridge.swift | 150 ++++++++++++++++ .../DesktopMPVBridge/NuvioControlsView.swift | 3 + .../DesktopMPVBridge/NuvioEpisodesPanel.swift | 15 +- .../DesktopMPVBridge/NuvioPlayerState.swift | 5 + .../NuvioSharedComponents.swift | 28 +++ .../DesktopMPVBridge/NuvioSourcesPanel.swift | 166 ++++++++++++++---- .../nuvio/app/features/player/PlayerScreen.kt | 58 +++++- .../player/PlayerStreamsRepository.kt | 9 +- .../player/MacOSMpvPlayerBackend.desktop.kt | 22 ++- .../player/MacOSNativePlayerBridge.desktop.kt | 12 +- 10 files changed, 409 insertions(+), 59 deletions(-) diff --git a/MPVKit/Sources/DesktopMPVBridge/Bridge.swift b/MPVKit/Sources/DesktopMPVBridge/Bridge.swift index 4aafc209..a191ddda 100644 --- a/MPVKit/Sources/DesktopMPVBridge/Bridge.swift +++ b/MPVKit/Sources/DesktopMPVBridge/Bridge.swift @@ -13,6 +13,14 @@ private func player(_ ptr: UnsafeMutableRawPointer) -> NuvioPlayerWindow { return Unmanaged.fromOpaque(ptr).takeUnretainedValue() } +private func onMainSync(_ work: () -> Void) { + if Thread.isMainThread { + work() + } else { + DispatchQueue.main.sync(execute: work) + } +} + @_cdecl("nuvio_player_create") public func nuvio_player_create() -> UnsafeMutableRawPointer { let p = NuvioPlayerWindow() @@ -539,6 +547,75 @@ public func nuvio_player_pop_episode_back(_ ptr: UnsafeMutableRawPointer) -> Boo return v } +@_cdecl("nuvio_player_begin_source_data_update") +public func nuvio_player_begin_source_data_update(_ ptr: UnsafeMutableRawPointer) { + let p = player(ptr) + p.state.pendingSourceStreams = [] + p.state.pendingSourceAddonGroups = [] +} + +@_cdecl("nuvio_player_stage_source_stream") +public func nuvio_player_stage_source_stream( + _ ptr: UnsafeMutableRawPointer, + _ id: UnsafePointer, + _ label: UnsafePointer, + _ subtitle: UnsafePointer?, + _ addonName: UnsafePointer, + _ addonId: UnsafePointer, + _ url: UnsafePointer, + _ videoSize: Int64, + _ isCurrent: Bool +) { + let info = NuvioStreamInfo( + id: String(cString: id), + label: String(cString: label), + subtitle: subtitle.map { String(cString: $0) }, + addonName: String(cString: addonName), + addonId: String(cString: addonId), + url: String(cString: url), + videoSize: videoSize, + isCurrent: isCurrent + ) + player(ptr).state.pendingSourceStreams.append(info) +} + +@_cdecl("nuvio_player_stage_source_addon_group") +public func nuvio_player_stage_source_addon_group( + _ ptr: UnsafeMutableRawPointer, + _ id: UnsafePointer, + _ addonName: UnsafePointer, + _ addonId: UnsafePointer, + _ isLoading: Bool, + _ hasError: Bool +) { + let info = NuvioAddonGroupInfo( + id: String(cString: id), + addonName: String(cString: addonName), + addonId: String(cString: addonId), + isLoading: isLoading, + hasError: hasError + ) + player(ptr).state.pendingSourceAddonGroups.append(info) +} + +@_cdecl("nuvio_player_commit_source_data_update") +public func nuvio_player_commit_source_data_update( + _ ptr: UnsafeMutableRawPointer, + _ loading: Bool, + _ selectedFilter: UnsafePointer? +) { + let p = player(ptr) + let streams = p.state.pendingSourceStreams + let groups = p.state.pendingSourceAddonGroups + let filter = selectedFilter.map { String(cString: $0) } + onMainSync { + p.state.sourceSelectedFilter = filter + p.state.sourceAddonGroups = groups + p.state.sourceStreams = streams + p.state.sourcesLoading = loading + } +} + @_cdecl("nuvio_player_set_sources_loading") public func nuvio_player_set_sources_loading(_ ptr: UnsafeMutableRawPointer, _ loading: Bool) { let p = player(ptr) @@ -560,6 +637,7 @@ public func nuvio_player_add_source_stream( _ addonName: UnsafePointer, _ addonId: UnsafePointer, _ url: UnsafePointer, + _ videoSize: Int64, _ isCurrent: Bool ) { let info = NuvioStreamInfo( @@ -569,6 +647,7 @@ public func nuvio_player_add_source_stream( addonName: String(cString: addonName), addonId: String(cString: addonId), url: String(cString: url), + videoSize: videoSize, isCurrent: isCurrent ) let p = player(ptr) @@ -636,6 +715,75 @@ public func nuvio_player_add_episode( DispatchQueue.main.async { p.state.episodes.append(info) } } +@_cdecl("nuvio_player_begin_episode_streams_data_update") +public func nuvio_player_begin_episode_streams_data_update(_ ptr: UnsafeMutableRawPointer) { + let p = player(ptr) + p.state.pendingEpisodeStreams = [] + p.state.pendingEpisodeAddonGroups = [] +} + +@_cdecl("nuvio_player_stage_episode_stream") +public func nuvio_player_stage_episode_stream( + _ ptr: UnsafeMutableRawPointer, + _ id: UnsafePointer, + _ label: UnsafePointer, + _ subtitle: UnsafePointer?, + _ addonName: UnsafePointer, + _ addonId: UnsafePointer, + _ url: UnsafePointer, + _ videoSize: Int64, + _ isCurrent: Bool +) { + let info = NuvioStreamInfo( + id: String(cString: id), + label: String(cString: label), + subtitle: subtitle.map { String(cString: $0) }, + addonName: String(cString: addonName), + addonId: String(cString: addonId), + url: String(cString: url), + videoSize: videoSize, + isCurrent: isCurrent + ) + player(ptr).state.pendingEpisodeStreams.append(info) +} + +@_cdecl("nuvio_player_stage_episode_addon_group") +public func nuvio_player_stage_episode_addon_group( + _ ptr: UnsafeMutableRawPointer, + _ id: UnsafePointer, + _ addonName: UnsafePointer, + _ addonId: UnsafePointer, + _ isLoading: Bool, + _ hasError: Bool +) { + let info = NuvioAddonGroupInfo( + id: String(cString: id), + addonName: String(cString: addonName), + addonId: String(cString: addonId), + isLoading: isLoading, + hasError: hasError + ) + player(ptr).state.pendingEpisodeAddonGroups.append(info) +} + +@_cdecl("nuvio_player_commit_episode_streams_data_update") +public func nuvio_player_commit_episode_streams_data_update( + _ ptr: UnsafeMutableRawPointer, + _ loading: Bool, + _ selectedFilter: UnsafePointer? +) { + let p = player(ptr) + let streams = p.state.pendingEpisodeStreams + let groups = p.state.pendingEpisodeAddonGroups + let filter = selectedFilter.map { String(cString: $0) } + onMainSync { + p.state.episodeSelectedFilter = filter + p.state.episodeAddonGroups = groups + p.state.episodeStreams = streams + p.state.episodeStreamsLoading = loading + } +} + @_cdecl("nuvio_player_set_episode_streams_loading") public func nuvio_player_set_episode_streams_loading(_ ptr: UnsafeMutableRawPointer, _ loading: Bool) { let p = player(ptr) @@ -657,6 +805,7 @@ public func nuvio_player_add_episode_stream( _ addonName: UnsafePointer, _ addonId: UnsafePointer, _ url: UnsafePointer, + _ videoSize: Int64, _ isCurrent: Bool ) { let info = NuvioStreamInfo( @@ -666,6 +815,7 @@ public func nuvio_player_add_episode_stream( addonName: String(cString: addonName), addonId: String(cString: addonId), url: String(cString: url), + videoSize: videoSize, isCurrent: isCurrent ) let p = player(ptr) diff --git a/MPVKit/Sources/DesktopMPVBridge/NuvioControlsView.swift b/MPVKit/Sources/DesktopMPVBridge/NuvioControlsView.swift index f9f0654b..14e690c0 100644 --- a/MPVKit/Sources/DesktopMPVBridge/NuvioControlsView.swift +++ b/MPVKit/Sources/DesktopMPVBridge/NuvioControlsView.swift @@ -377,6 +377,9 @@ struct NuvioControlsView: View { }) if state.hasVideoId { actionPillButton(icon: "arrow.left.arrow.right", label: "Sources", action: { + if state.sourceStreams.isEmpty && state.sourceAddonGroups.isEmpty { + state.sourcesLoading = true + } state.sourcesOpenRequested = true state.showSourcesPanel = true state.showEpisodesPanel = false diff --git a/MPVKit/Sources/DesktopMPVBridge/NuvioEpisodesPanel.swift b/MPVKit/Sources/DesktopMPVBridge/NuvioEpisodesPanel.swift index 71a52388..18df1cd6 100644 --- a/MPVKit/Sources/DesktopMPVBridge/NuvioEpisodesPanel.swift +++ b/MPVKit/Sources/DesktopMPVBridge/NuvioEpisodesPanel.swift @@ -301,11 +301,16 @@ struct NuvioEpisodesPanel: View { .foregroundColor(.white.opacity(0.6)) .lineLimit(2) } - Text(stream.addonName) - .font(.system(size: 11)) - .italic() - .foregroundColor(.white.opacity(0.6)) - .lineLimit(1) + HStack(spacing: 8) { + if stream.videoSize > 0 { + streamSizeBadge(bytes: stream.videoSize) + } + Text(stream.addonName) + .font(.system(size: 11)) + .italic() + .foregroundColor(.white.opacity(0.6)) + .lineLimit(1) + } } Spacer() } diff --git a/MPVKit/Sources/DesktopMPVBridge/NuvioPlayerState.swift b/MPVKit/Sources/DesktopMPVBridge/NuvioPlayerState.swift index be3efc8e..da40e321 100644 --- a/MPVKit/Sources/DesktopMPVBridge/NuvioPlayerState.swift +++ b/MPVKit/Sources/DesktopMPVBridge/NuvioPlayerState.swift @@ -37,6 +37,7 @@ struct NuvioStreamInfo: Identifiable { let addonName: String let addonId: String let url: String + let videoSize: Int64 let isCurrent: Bool } @@ -146,12 +147,16 @@ final class NuvioPlayerState: ObservableObject { var sourceFilterSelectedValue: String? = nil var sourceFilterChanged: Bool = false var sourceReloadRequested: Bool = false + var pendingSourceStreams: [NuvioStreamInfo] = [] + var pendingSourceAddonGroups: [NuvioAddonGroupInfo] = [] var episodeSelectedId: String? = nil var episodeStreamSelectedUrl: String? = nil var episodeFilterSelectedValue: String? = nil var episodeFilterChanged: Bool = false var episodeReloadRequested: Bool = false var episodeBackRequested: Bool = false + var pendingEpisodeStreams: [NuvioStreamInfo] = [] + var pendingEpisodeAddonGroups: [NuvioAddonGroupInfo] = [] var submitIntroRequested: Bool = false var submitIntroSegmentType: String = "intro" var submitIntroStartSec: Double = 0 diff --git a/MPVKit/Sources/DesktopMPVBridge/NuvioSharedComponents.swift b/MPVKit/Sources/DesktopMPVBridge/NuvioSharedComponents.swift index 8142e1dd..9d57ddb0 100644 --- a/MPVKit/Sources/DesktopMPVBridge/NuvioSharedComponents.swift +++ b/MPVKit/Sources/DesktopMPVBridge/NuvioSharedComponents.swift @@ -56,6 +56,34 @@ func addonFilterChip(label: String, isSelected: Bool, isLoading: Bool, hasError: .buttonStyle(.plain) } +func streamSizeBadge(bytes: Int64) -> some View { + Text(formattedStreamVideoSize(bytes)) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Color.black.opacity(0.45)) + .clipShape(Capsule()) +} + +func formattedStreamVideoSize(_ bytes: Int64) -> String { + let value = Double(bytes) + let gib = value / 1_073_741_824.0 + if gib >= 1.0 { + return "\(roundedStreamSize(gib)) GB" + } + let mib = value / 1_048_576.0 + return "\(Int(mib.rounded())) MB" +} + +func roundedStreamSize(_ value: Double) -> String { + let rounded = (value * 10).rounded() / 10 + if rounded == Double(Int(rounded)) { + return "\(Int(rounded))" + } + return String(format: "%.1f", rounded) +} + struct GestureFeedbackPill: View { let feedback: GestureFeedbackState diff --git a/MPVKit/Sources/DesktopMPVBridge/NuvioSourcesPanel.swift b/MPVKit/Sources/DesktopMPVBridge/NuvioSourcesPanel.swift index 7c108ded..68d7b6e0 100644 --- a/MPVKit/Sources/DesktopMPVBridge/NuvioSourcesPanel.swift +++ b/MPVKit/Sources/DesktopMPVBridge/NuvioSourcesPanel.swift @@ -17,6 +17,18 @@ struct NuvioSourcesPanel: View { Spacer() HStack(spacing: 8) { panelChipButton(label: "Reload", icon: "arrow.clockwise") { + state.sourcesLoading = true + if !state.sourceAddonGroups.isEmpty { + state.sourceAddonGroups = state.sourceAddonGroups.map { + NuvioAddonGroupInfo( + id: $0.id, + addonName: $0.addonName, + addonId: $0.addonId, + isLoading: true, + hasError: false + ) + } + } state.sourceReloadRequested = true } panelChipButton(label: "Close", icon: nil) { @@ -28,31 +40,35 @@ struct NuvioSourcesPanel: View { .padding(.top, 16) .padding(.bottom, 12) - let addonGroups = state.sourceAddonGroups - if addonGroups.count > 1 { + let addonGroups = addonFilterGroups + if addonGroups.count > 1 || state.sourcesLoading { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { - addonFilterChip( - label: "All", - isSelected: state.sourceSelectedFilter == nil, - isLoading: false, - hasError: false - ) { - state.sourceSelectedFilter = nil - state.sourceFilterSelectedValue = nil - state.sourceFilterChanged = true - } - ForEach(addonGroups) { group in + if addonGroups.count > 1 { addonFilterChip( - label: group.addonName, - isSelected: state.sourceSelectedFilter == group.addonId, - isLoading: group.isLoading, - hasError: group.hasError + label: "All", + isSelected: state.sourceSelectedFilter == nil, + isLoading: state.sourcesLoading, + hasError: false ) { - state.sourceSelectedFilter = group.addonId - state.sourceFilterSelectedValue = group.addonId + state.sourceSelectedFilter = nil + state.sourceFilterSelectedValue = nil state.sourceFilterChanged = true } + ForEach(addonGroups) { group in + addonFilterChip( + label: group.addonName, + isSelected: state.sourceSelectedFilter == group.addonId, + isLoading: group.isLoading, + hasError: group.hasError + ) { + state.sourceSelectedFilter = group.addonId + state.sourceFilterSelectedValue = group.addonId + state.sourceFilterChanged = true + } + } + } else { + sourceLoadingChip(label: addonGroups.first?.addonName ?? "Fetching sources") } } .padding(.horizontal, 20) @@ -62,22 +78,15 @@ struct NuvioSourcesPanel: View { let streams = state.filteredSourceStreams if state.sourcesLoading && streams.isEmpty { - Spacer() - ProgressView() - .progressViewStyle(.circular) - .scaleEffect(0.8) - .frame(height: 80) - Spacer() + sourceLoadingState } else if streams.isEmpty { - Spacer() - Text("No streams found") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.6)) - .frame(height: 80) - Spacer() + sourceEmptyState } else { ScrollView { VStack(spacing: 6) { + if state.sourcesLoading { + sourceProgressRow + } ForEach(streams) { stream in sourceStreamRow(stream: stream) { state.sourceStreamSelectedUrl = stream.url @@ -101,6 +110,84 @@ struct NuvioSourcesPanel: View { } } + var addonFilterGroups: [NuvioAddonGroupInfo] { + var seen = Set() + return state.sourceAddonGroups.filter { group in + let key = group.addonName.isEmpty ? group.addonId : group.addonName + if seen.contains(key) { return false } + seen.insert(key) + return true + } + } + + var sourceLoadingState: some View { + VStack(spacing: 12) { + Spacer() + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(0.9) + .frame(width: 32, height: 32) + Text("Fetching sources") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white.opacity(0.74)) + Spacer() + } + .frame(maxWidth: .infinity) + .frame(minHeight: 180) + } + + var sourceEmptyState: some View { + VStack(spacing: 10) { + Spacer() + Image(systemName: "magnifyingglass") + .font(.system(size: 24, weight: .semibold)) + .foregroundColor(.white.opacity(0.4)) + Text("No streams found") + .font(.system(size: 14)) + .foregroundColor(.white.opacity(0.6)) + Spacer() + } + .frame(maxWidth: .infinity) + .frame(minHeight: 180) + } + + var sourceProgressRow: some View { + HStack(spacing: 10) { + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(0.55) + .frame(width: 16, height: 16) + Text("Fetching more sources") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.white.opacity(0.68)) + Spacer() + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(Color.white.opacity(0.06)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + + func sourceLoadingChip(label: String) -> some View { + HStack(spacing: 6) { + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(0.5) + .frame(width: 12, height: 12) + Text(label) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.white.opacity(0.78)) + } + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.white.opacity(0.08)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.white.opacity(0.12), lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: 20)) + } + func sourceStreamRow(stream: NuvioStreamInfo, action: @escaping () -> Void) -> some View { Button(action: action) { HStack(spacing: 12) { @@ -126,11 +213,16 @@ struct NuvioSourcesPanel: View { .foregroundColor(.white.opacity(0.6)) .lineLimit(2) } - Text(stream.addonName) - .font(.system(size: 11)) - .italic() - .foregroundColor(.white.opacity(0.6)) - .lineLimit(1) + HStack(spacing: 8) { + if stream.videoSize > 0 { + streamSizeBadge(bytes: stream.videoSize) + } + Text(stream.addonName) + .font(.system(size: 11)) + .italic() + .foregroundColor(.white.opacity(0.6)) + .lineLimit(1) + } } Spacer() if stream.isCurrent { @@ -141,7 +233,7 @@ struct NuvioSourcesPanel: View { } .padding(.horizontal, 16) .padding(.vertical, 12) - .background(stream.isCurrent ? Color.white.opacity(0.12) : Color.clear) + .background(stream.isCurrent ? Color.white.opacity(0.12) : Color.white.opacity(0.05)) .overlay( stream.isCurrent ? RoundedRectangle(cornerRadius: 12) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt index f0dd559e..627b9d98 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreen.kt @@ -273,6 +273,35 @@ fun PlayerScreen( val canSubmitIntro = isSeries && playerSettingsUiState.introSubmitEnabled && playerSettingsUiState.introDbApiKey.isNotBlank() + val activeContentType = contentType ?: parentMetaType + val activeSourceRequestToken = remember( + activeContentType, + activeVideoId, + activeSeasonNumber, + activeEpisodeNumber, + ) { + activeVideoId?.let { + playerStreamsRequestToken( + type = activeContentType, + videoId = it, + season = activeSeasonNumber, + episode = activeEpisodeNumber, + ) + } + } + val activeEpisodeStreamsRequestToken = remember( + activeContentType, + episodeStreamsPanelState.selectedEpisode, + ) { + episodeStreamsPanelState.selectedEpisode?.let { + playerStreamsRequestToken( + type = activeContentType, + videoId = it.id, + season = it.season, + episode = it.episode, + ) + } + } // Skip intro/outro/recap state var skipIntervals by remember { mutableStateOf>(emptyList()) } @@ -1172,7 +1201,16 @@ fun PlayerScreen( playerController?.pushAddonSubtitles(addonSubtitles, isLoadingAddonSubtitles) } - LaunchedEffect(playerController, sourceStreamsState) { + LaunchedEffect(playerController, sourceStreamsState, activeSourceRequestToken) { + val requestToken = sourceStreamsState.requestToken ?: return@LaunchedEffect + if (requestToken != activeSourceRequestToken) return@LaunchedEffect + if ( + sourceStreamsState.groups.isEmpty() && + !sourceStreamsState.isAnyLoading && + sourceStreamsState.emptyStateReason == null + ) { + return@LaunchedEffect + } playerController?.pushSourceData( streams = sourceStreamsState.allStreams, groups = sourceStreamsState.groups, @@ -1182,7 +1220,16 @@ fun PlayerScreen( ) } - LaunchedEffect(playerController, episodeStreamsRepoState) { + LaunchedEffect(playerController, episodeStreamsRepoState, activeEpisodeStreamsRequestToken) { + val requestToken = episodeStreamsRepoState.requestToken ?: return@LaunchedEffect + if (requestToken != activeEpisodeStreamsRequestToken) return@LaunchedEffect + if ( + episodeStreamsRepoState.groups.isEmpty() && + !episodeStreamsRepoState.isAnyLoading && + episodeStreamsRepoState.emptyStateReason == null + ) { + return@LaunchedEffect + } playerController?.pushEpisodeStreamsData( streams = episodeStreamsRepoState.allStreams, groups = episodeStreamsRepoState.groups, @@ -2162,6 +2209,13 @@ private fun findPreferredTrackIndex( return -1 } +private fun playerStreamsRequestToken( + type: String, + videoId: String, + season: Int?, + episode: Int?, +): String = "$type::$videoId::$season::$episode" + private fun findPreferredSubtitleTrackIndex( tracks: List, targets: List, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt index 78f55bdb..f72e1481 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt @@ -132,7 +132,10 @@ object PlayerStreamsRepository { setRequestKey(requestKey) jobHolder()?.cancel() - stateFlow.value = StreamsUiState() + stateFlow.value = StreamsUiState( + requestToken = requestKey, + isAnyLoading = true, + ) val embeddedStreams = MetaDetailsRepository.findEmbeddedStreams(videoId) if (embeddedStreams.isNotEmpty()) { @@ -144,6 +147,7 @@ object PlayerStreamsRepository { isLoading = false, ) stateFlow.value = StreamsUiState( + requestToken = requestKey, groups = listOf(group), activeAddonIds = setOf("embedded"), isAnyLoading = false, @@ -161,6 +165,7 @@ object PlayerStreamsRepository { if (installedAddons.isEmpty() && pluginScrapers.isEmpty()) { stateFlow.value = StreamsUiState( + requestToken = requestKey, isAnyLoading = false, emptyStateReason = com.nuvio.app.features.streams.StreamsEmptyStateReason.NoAddonsInstalled, ) @@ -187,6 +192,7 @@ object PlayerStreamsRepository { if (streamAddons.isEmpty() && pluginScrapers.isEmpty()) { stateFlow.value = StreamsUiState( + requestToken = requestKey, isAnyLoading = false, emptyStateReason = com.nuvio.app.features.streams.StreamsEmptyStateReason.NoCompatibleAddons, ) @@ -209,6 +215,7 @@ object PlayerStreamsRepository { ) } stateFlow.value = StreamsUiState( + requestToken = requestKey, groups = initialGroups, activeAddonIds = initialGroups.map { it.addonId }.toSet(), isAnyLoading = true, diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSMpvPlayerBackend.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSMpvPlayerBackend.desktop.kt index 94d349fb..4aba7dcd 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSMpvPlayerBackend.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSMpvPlayerBackend.desktop.kt @@ -320,11 +320,9 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend { selectedFilter: String?, currentStreamUrl: String?, ) { - bridge.nuvio_player_set_sources_loading(playerPtr, loading) - bridge.nuvio_player_set_source_selected_filter(playerPtr, selectedFilter) - bridge.nuvio_player_clear_source_addon_groups(playerPtr) + bridge.nuvio_player_begin_source_data_update(playerPtr) groups.forEach { group -> - bridge.nuvio_player_add_source_addon_group( + bridge.nuvio_player_stage_source_addon_group( playerPtr, group.addonId, group.addonName, @@ -333,9 +331,8 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend { group.error != null, ) } - bridge.nuvio_player_clear_source_streams(playerPtr) streams.forEach { stream -> - bridge.nuvio_player_add_source_stream( + bridge.nuvio_player_stage_source_stream( playerPtr, stream.addonId + "_" + (stream.url ?: stream.infoHash ?: ""), stream.streamLabel, @@ -343,9 +340,11 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend { stream.addonName, stream.addonId, stream.directPlaybackUrl ?: "", + stream.behaviorHints.videoSize ?: 0L, stream.directPlaybackUrl == currentStreamUrl, ) } + bridge.nuvio_player_commit_source_data_update(playerPtr, loading, selectedFilter) } override fun pushEpisodes(episodes: List) { @@ -370,11 +369,9 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend { selectedFilter: String?, currentStreamUrl: String?, ) { - bridge.nuvio_player_set_episode_streams_loading(playerPtr, loading) - bridge.nuvio_player_set_episode_selected_filter(playerPtr, selectedFilter) - bridge.nuvio_player_clear_episode_addon_groups(playerPtr) + bridge.nuvio_player_begin_episode_streams_data_update(playerPtr) groups.forEach { group -> - bridge.nuvio_player_add_episode_addon_group( + bridge.nuvio_player_stage_episode_addon_group( playerPtr, group.addonId, group.addonName, @@ -383,9 +380,8 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend { group.error != null, ) } - bridge.nuvio_player_clear_episode_streams(playerPtr) streams.forEach { stream -> - bridge.nuvio_player_add_episode_stream( + bridge.nuvio_player_stage_episode_stream( playerPtr, stream.addonId + "_" + (stream.url ?: stream.infoHash ?: ""), stream.streamLabel, @@ -393,9 +389,11 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend { stream.addonName, stream.addonId, stream.directPlaybackUrl ?: "", + stream.behaviorHints.videoSize ?: 0L, stream.directPlaybackUrl == currentStreamUrl, ) } + bridge.nuvio_player_commit_episode_streams_data_update(playerPtr, loading, selectedFilter) } override fun showEpisodeStreamsView(season: Int?, episode: Int?, title: String?) { diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSNativePlayerBridge.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSNativePlayerBridge.desktop.kt index 95320a29..1705f427 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSNativePlayerBridge.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/player/MacOSNativePlayerBridge.desktop.kt @@ -148,18 +148,26 @@ internal interface MacOSMPVBridgeLib : Library { fun nuvio_player_pop_episode_reload(player: Pointer): Boolean fun nuvio_player_pop_episode_back(player: Pointer): Boolean + fun nuvio_player_begin_source_data_update(player: Pointer) + fun nuvio_player_stage_source_stream(player: Pointer, id: String, label: String, subtitle: String?, addonName: String, addonId: String, url: String, videoSize: Long, isCurrent: Boolean) + fun nuvio_player_stage_source_addon_group(player: Pointer, id: String, addonName: String, addonId: String, isLoading: Boolean, hasError: Boolean) + fun nuvio_player_commit_source_data_update(player: Pointer, loading: Boolean, selectedFilter: String?) fun nuvio_player_set_sources_loading(player: Pointer, loading: Boolean) fun nuvio_player_clear_source_streams(player: Pointer) - fun nuvio_player_add_source_stream(player: Pointer, id: String, label: String, subtitle: String?, addonName: String, addonId: String, url: String, isCurrent: Boolean) + fun nuvio_player_add_source_stream(player: Pointer, id: String, label: String, subtitle: String?, addonName: String, addonId: String, url: String, videoSize: Long, isCurrent: Boolean) fun nuvio_player_clear_source_addon_groups(player: Pointer) fun nuvio_player_add_source_addon_group(player: Pointer, id: String, addonName: String, addonId: String, isLoading: Boolean, hasError: Boolean) fun nuvio_player_set_source_selected_filter(player: Pointer, addonId: String?) fun nuvio_player_clear_episodes(player: Pointer) fun nuvio_player_add_episode(player: Pointer, id: String, title: String, overview: String?, thumbnail: String?, season: Int, episode: Int) + fun nuvio_player_begin_episode_streams_data_update(player: Pointer) + fun nuvio_player_stage_episode_stream(player: Pointer, id: String, label: String, subtitle: String?, addonName: String, addonId: String, url: String, videoSize: Long, isCurrent: Boolean) + fun nuvio_player_stage_episode_addon_group(player: Pointer, id: String, addonName: String, addonId: String, isLoading: Boolean, hasError: Boolean) + fun nuvio_player_commit_episode_streams_data_update(player: Pointer, loading: Boolean, selectedFilter: String?) fun nuvio_player_set_episode_streams_loading(player: Pointer, loading: Boolean) fun nuvio_player_clear_episode_streams(player: Pointer) - fun nuvio_player_add_episode_stream(player: Pointer, id: String, label: String, subtitle: String?, addonName: String, addonId: String, url: String, isCurrent: Boolean) + fun nuvio_player_add_episode_stream(player: Pointer, id: String, label: String, subtitle: String?, addonName: String, addonId: String, url: String, videoSize: Long, isCurrent: Boolean) fun nuvio_player_clear_episode_addon_groups(player: Pointer) fun nuvio_player_add_episode_addon_group(player: Pointer, id: String, addonName: String, addonId: String, isLoading: Boolean, hasError: Boolean) fun nuvio_player_set_episode_selected_filter(player: Pointer, addonId: String?)