mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 07:51:46 +00:00
feat(player): Improve source panel source handling
This commit is contained in:
parent
849c265a3f
commit
72dfbac9a9
10 changed files with 409 additions and 59 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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?) {
|
||||||
|
|
|
||||||
|
|
@ -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?)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue