feat(player): Improve source panel source handling

This commit is contained in:
tapframe 2026-05-12 19:22:54 +05:30
parent 849c265a3f
commit 72dfbac9a9
10 changed files with 409 additions and 59 deletions

View file

@ -13,6 +13,14 @@ private func player(_ ptr: UnsafeMutableRawPointer) -> NuvioPlayerWindow {
return Unmanaged<NuvioPlayerWindow>.fromOpaque(ptr).takeUnretainedValue() return Unmanaged<NuvioPlayerWindow>.fromOpaque(ptr).takeUnretainedValue()
} }
private func onMainSync(_ work: () -> Void) {
if Thread.isMainThread {
work()
} else {
DispatchQueue.main.sync(execute: work)
}
}
@_cdecl("nuvio_player_create") @_cdecl("nuvio_player_create")
public func nuvio_player_create() -> UnsafeMutableRawPointer { public func nuvio_player_create() -> UnsafeMutableRawPointer {
let p = NuvioPlayerWindow() let p = NuvioPlayerWindow()
@ -539,6 +547,75 @@ public func nuvio_player_pop_episode_back(_ ptr: UnsafeMutableRawPointer) -> Boo
return v 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<CChar>,
_ label: UnsafePointer<CChar>,
_ subtitle: UnsafePointer<CChar>?,
_ addonName: UnsafePointer<CChar>,
_ addonId: UnsafePointer<CChar>,
_ url: UnsafePointer<CChar>,
_ 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<CChar>,
_ addonName: UnsafePointer<CChar>,
_ addonId: UnsafePointer<CChar>,
_ 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<CChar>?
) {
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") @_cdecl("nuvio_player_set_sources_loading")
public func nuvio_player_set_sources_loading(_ ptr: UnsafeMutableRawPointer, _ loading: Bool) { public func nuvio_player_set_sources_loading(_ ptr: UnsafeMutableRawPointer, _ loading: Bool) {
let p = player(ptr) let p = player(ptr)
@ -560,6 +637,7 @@ public func nuvio_player_add_source_stream(
_ addonName: UnsafePointer<CChar>, _ addonName: UnsafePointer<CChar>,
_ addonId: UnsafePointer<CChar>, _ addonId: UnsafePointer<CChar>,
_ url: UnsafePointer<CChar>, _ url: UnsafePointer<CChar>,
_ videoSize: Int64,
_ isCurrent: Bool _ isCurrent: Bool
) { ) {
let info = NuvioStreamInfo( let info = NuvioStreamInfo(
@ -569,6 +647,7 @@ public func nuvio_player_add_source_stream(
addonName: String(cString: addonName), addonName: String(cString: addonName),
addonId: String(cString: addonId), addonId: String(cString: addonId),
url: String(cString: url), url: String(cString: url),
videoSize: videoSize,
isCurrent: isCurrent isCurrent: isCurrent
) )
let p = player(ptr) let p = player(ptr)
@ -636,6 +715,75 @@ public func nuvio_player_add_episode(
DispatchQueue.main.async { p.state.episodes.append(info) } 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<CChar>,
_ label: UnsafePointer<CChar>,
_ subtitle: UnsafePointer<CChar>?,
_ addonName: UnsafePointer<CChar>,
_ addonId: UnsafePointer<CChar>,
_ url: UnsafePointer<CChar>,
_ 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<CChar>,
_ addonName: UnsafePointer<CChar>,
_ addonId: UnsafePointer<CChar>,
_ 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<CChar>?
) {
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") @_cdecl("nuvio_player_set_episode_streams_loading")
public func nuvio_player_set_episode_streams_loading(_ ptr: UnsafeMutableRawPointer, _ loading: Bool) { public func nuvio_player_set_episode_streams_loading(_ ptr: UnsafeMutableRawPointer, _ loading: Bool) {
let p = player(ptr) let p = player(ptr)
@ -657,6 +805,7 @@ public func nuvio_player_add_episode_stream(
_ addonName: UnsafePointer<CChar>, _ addonName: UnsafePointer<CChar>,
_ addonId: UnsafePointer<CChar>, _ addonId: UnsafePointer<CChar>,
_ url: UnsafePointer<CChar>, _ url: UnsafePointer<CChar>,
_ videoSize: Int64,
_ isCurrent: Bool _ isCurrent: Bool
) { ) {
let info = NuvioStreamInfo( let info = NuvioStreamInfo(
@ -666,6 +815,7 @@ public func nuvio_player_add_episode_stream(
addonName: String(cString: addonName), addonName: String(cString: addonName),
addonId: String(cString: addonId), addonId: String(cString: addonId),
url: String(cString: url), url: String(cString: url),
videoSize: videoSize,
isCurrent: isCurrent isCurrent: isCurrent
) )
let p = player(ptr) let p = player(ptr)

View file

@ -377,6 +377,9 @@ struct NuvioControlsView: View {
}) })
if state.hasVideoId { if state.hasVideoId {
actionPillButton(icon: "arrow.left.arrow.right", label: "Sources", action: { actionPillButton(icon: "arrow.left.arrow.right", label: "Sources", action: {
if state.sourceStreams.isEmpty && state.sourceAddonGroups.isEmpty {
state.sourcesLoading = true
}
state.sourcesOpenRequested = true state.sourcesOpenRequested = true
state.showSourcesPanel = true state.showSourcesPanel = true
state.showEpisodesPanel = false state.showEpisodesPanel = false

View file

@ -301,11 +301,16 @@ struct NuvioEpisodesPanel: View {
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
.lineLimit(2) .lineLimit(2)
} }
Text(stream.addonName) HStack(spacing: 8) {
.font(.system(size: 11)) if stream.videoSize > 0 {
.italic() streamSizeBadge(bytes: stream.videoSize)
.foregroundColor(.white.opacity(0.6)) }
.lineLimit(1) Text(stream.addonName)
.font(.system(size: 11))
.italic()
.foregroundColor(.white.opacity(0.6))
.lineLimit(1)
}
} }
Spacer() Spacer()
} }

View file

@ -37,6 +37,7 @@ struct NuvioStreamInfo: Identifiable {
let addonName: String let addonName: String
let addonId: String let addonId: String
let url: String let url: String
let videoSize: Int64
let isCurrent: Bool let isCurrent: Bool
} }
@ -146,12 +147,16 @@ final class NuvioPlayerState: ObservableObject {
var sourceFilterSelectedValue: String? = nil var sourceFilterSelectedValue: String? = nil
var sourceFilterChanged: Bool = false var sourceFilterChanged: Bool = false
var sourceReloadRequested: Bool = false var sourceReloadRequested: Bool = false
var pendingSourceStreams: [NuvioStreamInfo] = []
var pendingSourceAddonGroups: [NuvioAddonGroupInfo] = []
var episodeSelectedId: String? = nil var episodeSelectedId: String? = nil
var episodeStreamSelectedUrl: String? = nil var episodeStreamSelectedUrl: String? = nil
var episodeFilterSelectedValue: String? = nil var episodeFilterSelectedValue: String? = nil
var episodeFilterChanged: Bool = false var episodeFilterChanged: Bool = false
var episodeReloadRequested: Bool = false var episodeReloadRequested: Bool = false
var episodeBackRequested: Bool = false var episodeBackRequested: Bool = false
var pendingEpisodeStreams: [NuvioStreamInfo] = []
var pendingEpisodeAddonGroups: [NuvioAddonGroupInfo] = []
var submitIntroRequested: Bool = false var submitIntroRequested: Bool = false
var submitIntroSegmentType: String = "intro" var submitIntroSegmentType: String = "intro"
var submitIntroStartSec: Double = 0 var submitIntroStartSec: Double = 0

View file

@ -56,6 +56,34 @@ func addonFilterChip(label: String, isSelected: Bool, isLoading: Bool, hasError:
.buttonStyle(.plain) .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 { struct GestureFeedbackPill: View {
let feedback: GestureFeedbackState let feedback: GestureFeedbackState

View file

@ -17,6 +17,18 @@ struct NuvioSourcesPanel: View {
Spacer() Spacer()
HStack(spacing: 8) { HStack(spacing: 8) {
panelChipButton(label: "Reload", icon: "arrow.clockwise") { 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 state.sourceReloadRequested = true
} }
panelChipButton(label: "Close", icon: nil) { panelChipButton(label: "Close", icon: nil) {
@ -28,31 +40,35 @@ struct NuvioSourcesPanel: View {
.padding(.top, 16) .padding(.top, 16)
.padding(.bottom, 12) .padding(.bottom, 12)
let addonGroups = state.sourceAddonGroups let addonGroups = addonFilterGroups
if addonGroups.count > 1 { if addonGroups.count > 1 || state.sourcesLoading {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) { HStack(spacing: 8) {
addonFilterChip( if addonGroups.count > 1 {
label: "All",
isSelected: state.sourceSelectedFilter == nil,
isLoading: false,
hasError: false
) {
state.sourceSelectedFilter = nil
state.sourceFilterSelectedValue = nil
state.sourceFilterChanged = true
}
ForEach(addonGroups) { group in
addonFilterChip( addonFilterChip(
label: group.addonName, label: "All",
isSelected: state.sourceSelectedFilter == group.addonId, isSelected: state.sourceSelectedFilter == nil,
isLoading: group.isLoading, isLoading: state.sourcesLoading,
hasError: group.hasError hasError: false
) { ) {
state.sourceSelectedFilter = group.addonId state.sourceSelectedFilter = nil
state.sourceFilterSelectedValue = group.addonId state.sourceFilterSelectedValue = nil
state.sourceFilterChanged = true 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) .padding(.horizontal, 20)
@ -62,22 +78,15 @@ struct NuvioSourcesPanel: View {
let streams = state.filteredSourceStreams let streams = state.filteredSourceStreams
if state.sourcesLoading && streams.isEmpty { if state.sourcesLoading && streams.isEmpty {
Spacer() sourceLoadingState
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(0.8)
.frame(height: 80)
Spacer()
} else if streams.isEmpty { } else if streams.isEmpty {
Spacer() sourceEmptyState
Text("No streams found")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.6))
.frame(height: 80)
Spacer()
} else { } else {
ScrollView { ScrollView {
VStack(spacing: 6) { VStack(spacing: 6) {
if state.sourcesLoading {
sourceProgressRow
}
ForEach(streams) { stream in ForEach(streams) { stream in
sourceStreamRow(stream: stream) { sourceStreamRow(stream: stream) {
state.sourceStreamSelectedUrl = stream.url state.sourceStreamSelectedUrl = stream.url
@ -101,6 +110,84 @@ struct NuvioSourcesPanel: View {
} }
} }
var addonFilterGroups: [NuvioAddonGroupInfo] {
var seen = Set<String>()
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 { func sourceStreamRow(stream: NuvioStreamInfo, action: @escaping () -> Void) -> some View {
Button(action: action) { Button(action: action) {
HStack(spacing: 12) { HStack(spacing: 12) {
@ -126,11 +213,16 @@ struct NuvioSourcesPanel: View {
.foregroundColor(.white.opacity(0.6)) .foregroundColor(.white.opacity(0.6))
.lineLimit(2) .lineLimit(2)
} }
Text(stream.addonName) HStack(spacing: 8) {
.font(.system(size: 11)) if stream.videoSize > 0 {
.italic() streamSizeBadge(bytes: stream.videoSize)
.foregroundColor(.white.opacity(0.6)) }
.lineLimit(1) Text(stream.addonName)
.font(.system(size: 11))
.italic()
.foregroundColor(.white.opacity(0.6))
.lineLimit(1)
}
} }
Spacer() Spacer()
if stream.isCurrent { if stream.isCurrent {
@ -141,7 +233,7 @@ struct NuvioSourcesPanel: View {
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 12) .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( .overlay(
stream.isCurrent ? stream.isCurrent ?
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)

View file

@ -273,6 +273,35 @@ fun PlayerScreen(
val canSubmitIntro = isSeries && val canSubmitIntro = isSeries &&
playerSettingsUiState.introSubmitEnabled && playerSettingsUiState.introSubmitEnabled &&
playerSettingsUiState.introDbApiKey.isNotBlank() 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 // Skip intro/outro/recap state
var skipIntervals by remember { mutableStateOf<List<SkipInterval>>(emptyList()) } var skipIntervals by remember { mutableStateOf<List<SkipInterval>>(emptyList()) }
@ -1172,7 +1201,16 @@ fun PlayerScreen(
playerController?.pushAddonSubtitles(addonSubtitles, isLoadingAddonSubtitles) 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( playerController?.pushSourceData(
streams = sourceStreamsState.allStreams, streams = sourceStreamsState.allStreams,
groups = sourceStreamsState.groups, 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( playerController?.pushEpisodeStreamsData(
streams = episodeStreamsRepoState.allStreams, streams = episodeStreamsRepoState.allStreams,
groups = episodeStreamsRepoState.groups, groups = episodeStreamsRepoState.groups,
@ -2162,6 +2209,13 @@ private fun <T> findPreferredTrackIndex(
return -1 return -1
} }
private fun playerStreamsRequestToken(
type: String,
videoId: String,
season: Int?,
episode: Int?,
): String = "$type::$videoId::$season::$episode"
private fun findPreferredSubtitleTrackIndex( private fun findPreferredSubtitleTrackIndex(
tracks: List<SubtitleTrack>, tracks: List<SubtitleTrack>,
targets: List<String>, targets: List<String>,

View file

@ -132,7 +132,10 @@ object PlayerStreamsRepository {
setRequestKey(requestKey) setRequestKey(requestKey)
jobHolder()?.cancel() jobHolder()?.cancel()
stateFlow.value = StreamsUiState() stateFlow.value = StreamsUiState(
requestToken = requestKey,
isAnyLoading = true,
)
val embeddedStreams = MetaDetailsRepository.findEmbeddedStreams(videoId) val embeddedStreams = MetaDetailsRepository.findEmbeddedStreams(videoId)
if (embeddedStreams.isNotEmpty()) { if (embeddedStreams.isNotEmpty()) {
@ -144,6 +147,7 @@ object PlayerStreamsRepository {
isLoading = false, isLoading = false,
) )
stateFlow.value = StreamsUiState( stateFlow.value = StreamsUiState(
requestToken = requestKey,
groups = listOf(group), groups = listOf(group),
activeAddonIds = setOf("embedded"), activeAddonIds = setOf("embedded"),
isAnyLoading = false, isAnyLoading = false,
@ -161,6 +165,7 @@ object PlayerStreamsRepository {
if (installedAddons.isEmpty() && pluginScrapers.isEmpty()) { if (installedAddons.isEmpty() && pluginScrapers.isEmpty()) {
stateFlow.value = StreamsUiState( stateFlow.value = StreamsUiState(
requestToken = requestKey,
isAnyLoading = false, isAnyLoading = false,
emptyStateReason = com.nuvio.app.features.streams.StreamsEmptyStateReason.NoAddonsInstalled, emptyStateReason = com.nuvio.app.features.streams.StreamsEmptyStateReason.NoAddonsInstalled,
) )
@ -187,6 +192,7 @@ object PlayerStreamsRepository {
if (streamAddons.isEmpty() && pluginScrapers.isEmpty()) { if (streamAddons.isEmpty() && pluginScrapers.isEmpty()) {
stateFlow.value = StreamsUiState( stateFlow.value = StreamsUiState(
requestToken = requestKey,
isAnyLoading = false, isAnyLoading = false,
emptyStateReason = com.nuvio.app.features.streams.StreamsEmptyStateReason.NoCompatibleAddons, emptyStateReason = com.nuvio.app.features.streams.StreamsEmptyStateReason.NoCompatibleAddons,
) )
@ -209,6 +215,7 @@ object PlayerStreamsRepository {
) )
} }
stateFlow.value = StreamsUiState( stateFlow.value = StreamsUiState(
requestToken = requestKey,
groups = initialGroups, groups = initialGroups,
activeAddonIds = initialGroups.map { it.addonId }.toSet(), activeAddonIds = initialGroups.map { it.addonId }.toSet(),
isAnyLoading = true, isAnyLoading = true,

View file

@ -320,11 +320,9 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
selectedFilter: String?, selectedFilter: String?,
currentStreamUrl: String?, currentStreamUrl: String?,
) { ) {
bridge.nuvio_player_set_sources_loading(playerPtr, loading) bridge.nuvio_player_begin_source_data_update(playerPtr)
bridge.nuvio_player_set_source_selected_filter(playerPtr, selectedFilter)
bridge.nuvio_player_clear_source_addon_groups(playerPtr)
groups.forEach { group -> groups.forEach { group ->
bridge.nuvio_player_add_source_addon_group( bridge.nuvio_player_stage_source_addon_group(
playerPtr, playerPtr,
group.addonId, group.addonId,
group.addonName, group.addonName,
@ -333,9 +331,8 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
group.error != null, group.error != null,
) )
} }
bridge.nuvio_player_clear_source_streams(playerPtr)
streams.forEach { stream -> streams.forEach { stream ->
bridge.nuvio_player_add_source_stream( bridge.nuvio_player_stage_source_stream(
playerPtr, playerPtr,
stream.addonId + "_" + (stream.url ?: stream.infoHash ?: ""), stream.addonId + "_" + (stream.url ?: stream.infoHash ?: ""),
stream.streamLabel, stream.streamLabel,
@ -343,9 +340,11 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
stream.addonName, stream.addonName,
stream.addonId, stream.addonId,
stream.directPlaybackUrl ?: "", stream.directPlaybackUrl ?: "",
stream.behaviorHints.videoSize ?: 0L,
stream.directPlaybackUrl == currentStreamUrl, stream.directPlaybackUrl == currentStreamUrl,
) )
} }
bridge.nuvio_player_commit_source_data_update(playerPtr, loading, selectedFilter)
} }
override fun pushEpisodes(episodes: List<MetaVideo>) { override fun pushEpisodes(episodes: List<MetaVideo>) {
@ -370,11 +369,9 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
selectedFilter: String?, selectedFilter: String?,
currentStreamUrl: String?, currentStreamUrl: String?,
) { ) {
bridge.nuvio_player_set_episode_streams_loading(playerPtr, loading) bridge.nuvio_player_begin_episode_streams_data_update(playerPtr)
bridge.nuvio_player_set_episode_selected_filter(playerPtr, selectedFilter)
bridge.nuvio_player_clear_episode_addon_groups(playerPtr)
groups.forEach { group -> groups.forEach { group ->
bridge.nuvio_player_add_episode_addon_group( bridge.nuvio_player_stage_episode_addon_group(
playerPtr, playerPtr,
group.addonId, group.addonId,
group.addonName, group.addonName,
@ -383,9 +380,8 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
group.error != null, group.error != null,
) )
} }
bridge.nuvio_player_clear_episode_streams(playerPtr)
streams.forEach { stream -> streams.forEach { stream ->
bridge.nuvio_player_add_episode_stream( bridge.nuvio_player_stage_episode_stream(
playerPtr, playerPtr,
stream.addonId + "_" + (stream.url ?: stream.infoHash ?: ""), stream.addonId + "_" + (stream.url ?: stream.infoHash ?: ""),
stream.streamLabel, stream.streamLabel,
@ -393,9 +389,11 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
stream.addonName, stream.addonName,
stream.addonId, stream.addonId,
stream.directPlaybackUrl ?: "", stream.directPlaybackUrl ?: "",
stream.behaviorHints.videoSize ?: 0L,
stream.directPlaybackUrl == currentStreamUrl, stream.directPlaybackUrl == currentStreamUrl,
) )
} }
bridge.nuvio_player_commit_episode_streams_data_update(playerPtr, loading, selectedFilter)
} }
override fun showEpisodeStreamsView(season: Int?, episode: Int?, title: String?) { override fun showEpisodeStreamsView(season: Int?, episode: Int?, title: String?) {

View file

@ -148,18 +148,26 @@ internal interface MacOSMPVBridgeLib : Library {
fun nuvio_player_pop_episode_reload(player: Pointer): Boolean fun nuvio_player_pop_episode_reload(player: Pointer): Boolean
fun nuvio_player_pop_episode_back(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_set_sources_loading(player: Pointer, loading: Boolean)
fun nuvio_player_clear_source_streams(player: Pointer) 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_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_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_set_source_selected_filter(player: Pointer, addonId: String?)
fun nuvio_player_clear_episodes(player: Pointer) 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_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_set_episode_streams_loading(player: Pointer, loading: Boolean)
fun nuvio_player_clear_episode_streams(player: Pointer) 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_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_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?) fun nuvio_player_set_episode_selected_filter(player: Pointer, addonId: String?)