tv-anarchy/Sources/TVAnarchy/PlayerQueueRail.swift

95 lines
3.6 KiB
Swift
Raw Permalink Normal View History

import SwiftUI
import TVAnarchyCore
/// Horizontal upcoming-episode rail only items after the current one.
struct PlayerQueueRail: View {
@Bindable var controller: PlayerController
@Bindable var library: LibraryController
@Bindable var playlist: PlaylistController
private var paths: [String] {
if !controller.playbackQueuePaths.isEmpty { return controller.playbackQueuePaths }
return playlist.queue.map(\.path)
}
private var upcoming: [(offset: Int, path: String)] {
guard paths.count > 1 else { return [] }
let cur = currentIndex ?? -1
let start = cur + 1
guard start < paths.count else { return [] }
return paths.enumerated().compactMap { idx, path in
idx >= start ? (idx, path) : nil
}
}
var body: some View {
if !upcoming.isEmpty {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
Text("Up next")
.font(.caption)
.foregroundStyle(.secondary)
Text("· \(upcoming.count) episode\(upcoming.count == 1 ? "" : "s")")
.font(.caption2)
.foregroundStyle(.tertiary)
}
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: upcoming.count > 4) {
HStack(spacing: 8) {
ForEach(upcoming, id: \.path) { item in
queueChip(path: item.path, index: item.offset)
.id(item.path)
}
}
}
.onAppear {
if let first = upcoming.first?.path {
proxy.scrollTo(first, anchor: .leading)
}
}
.onChange(of: currentIndex) {
if let first = upcoming.first?.path {
withAnimation(.easeOut(duration: 0.2)) {
proxy.scrollTo(first, anchor: .leading)
}
}
}
}
}
}
}
private func queueChip(path: String, index: Int) -> some View {
let parts = PlayerController.queueChipParts(for: path, library: library)
return VStack(alignment: .leading, spacing: 3) {
if !parts.code.isEmpty {
Text(parts.code)
.font(.caption2.bold().monospacedDigit())
.foregroundStyle(.secondary)
}
Text(parts.title.isEmpty ? PlayerController.label(for: path, library: library) : parts.title)
.font(.caption)
.lineLimit(2)
.frame(minWidth: 130, maxWidth: 170, alignment: .leading)
}
.padding(.horizontal, 10).padding(.vertical, 8)
.background(Color.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 8))
.overlay {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.white.opacity(0.1), lineWidth: 1)
}
.help(PlayerController.label(for: path, library: library))
}
private var currentIndex: Int? {
if !controller.playbackQueuePaths.isEmpty,
paths == controller.playbackQueuePaths {
return controller.currentQueueIndex
}
if paths == playlist.queue.map(\.path),
let pos = controller.activeSnapshot.status.playlistPos {
return pos
}
return nil
}
}