Surface the existing pin (keep-from-cull) and per-file delete actions as visible inline buttons on each offline cache row instead of context-menu-only: a star toggles protection from auto-cull (and restore-if-missing), a trash culls that file early. Aligns wording/icons to the star metaphor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
No EOL
3.6 KiB
Swift
95 lines
No EOL
3.6 KiB
Swift
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
|
|
}
|
|
} |