mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 16:01:44 +00:00
246 lines
9.8 KiB
Swift
246 lines
9.8 KiB
Swift
import SwiftUI
|
|
|
|
struct NuvioSourcesPanel: View {
|
|
@ObservedObject var state: NuvioPlayerState
|
|
var onDismiss: () -> Void
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.black.opacity(0.52)
|
|
.onTapGesture { onDismiss() }
|
|
|
|
VStack(spacing: 0) {
|
|
HStack {
|
|
Text("Sources")
|
|
.font(.system(size: 18, weight: .bold))
|
|
.foregroundColor(.white)
|
|
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) {
|
|
onDismiss()
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.top, 16)
|
|
.padding(.bottom, 12)
|
|
|
|
let addonGroups = addonFilterGroups
|
|
if addonGroups.count > 1 || state.sourcesLoading {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
if addonGroups.count > 1 {
|
|
addonFilterChip(
|
|
label: "All",
|
|
isSelected: state.sourceSelectedFilter == nil,
|
|
isLoading: state.sourcesLoading,
|
|
hasError: false
|
|
) {
|
|
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)
|
|
}
|
|
.padding(.bottom, 12)
|
|
}
|
|
|
|
let streams = state.filteredSourceStreams
|
|
if state.sourcesLoading && streams.isEmpty {
|
|
sourceLoadingState
|
|
} else if streams.isEmpty {
|
|
sourceEmptyState
|
|
} else {
|
|
ScrollView {
|
|
VStack(spacing: 6) {
|
|
if state.sourcesLoading {
|
|
sourceProgressRow
|
|
}
|
|
ForEach(streams) { stream in
|
|
sourceStreamRow(stream: stream) {
|
|
state.sourceStreamSelectedUrl = stream.url
|
|
onDismiss()
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.bottom, 16)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: 520)
|
|
.frame(maxHeight: 600)
|
|
.background(Color(red: 0.12, green: 0.12, blue: 0.12))
|
|
.clipShape(RoundedRectangle(cornerRadius: 24))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 24)
|
|
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
Button(action: action) {
|
|
HStack(spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
HStack(spacing: 8) {
|
|
Text(stream.label)
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundColor(.white)
|
|
.lineLimit(1)
|
|
if stream.isCurrent {
|
|
Text("Playing")
|
|
.font(.system(size: 10, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 3)
|
|
.background(Color.white.opacity(0.15))
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
if let sub = stream.subtitle, !sub.isEmpty, sub != stream.label {
|
|
Text(sub)
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.white.opacity(0.6))
|
|
.lineLimit(2)
|
|
}
|
|
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 {
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 14, weight: .bold))
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
.background(stream.isCurrent ? Color.white.opacity(0.12) : Color.white.opacity(0.05))
|
|
.overlay(
|
|
stream.isCurrent ?
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(Color.white.opacity(0.2), lineWidth: 1) : nil
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|