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>
158 lines
No EOL
5.2 KiB
Swift
158 lines
No EOL
5.2 KiB
Swift
import SwiftUI
|
|
import TVAnarchyCore
|
|
|
|
/// Shared offline download UI: summary, overall percent, and queued episode list.
|
|
struct OfflineDownloadPanel: View {
|
|
@Bindable var offline: OfflineCacheController
|
|
var compact = false
|
|
var maxQueueRows = 8
|
|
|
|
private var active: Bool { offline.isDownloading }
|
|
|
|
var body: some View {
|
|
if active {
|
|
VStack(alignment: .leading, spacing: compact ? 6 : 10) {
|
|
summaryRow
|
|
progressRow
|
|
if !compact, !offline.downloadQueue.isEmpty { queueList }
|
|
else if !compact, offline.downloadQueue.isEmpty, let status = offline.status {
|
|
Text(status).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.modifier(OfflineDownloadPanelChrome(compact: compact))
|
|
}
|
|
}
|
|
|
|
private var summaryRow: some View {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "arrow.down.circle.fill")
|
|
.foregroundStyle(.tint)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
if compact {
|
|
Text("Offline cache")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text(offline.downloadSummaryLine)
|
|
.font(compact ? .callout : .headline)
|
|
.lineLimit(compact ? 2 : 2)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private var progressRow: some View {
|
|
if let overall = offline.queueProgress {
|
|
ProgressView(value: overall)
|
|
Text("Overall \(Int(overall * 100))%")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
} else if let file = offline.downloadingProgress {
|
|
ProgressView(value: file)
|
|
Text("\(Int(file * 100))%")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
ProgressView()
|
|
}
|
|
}
|
|
|
|
private var queueList: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Queue (\(offline.downloadQueue.count))")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
let rows = offline.downloadQueue.prefix(maxQueueRows)
|
|
ForEach(Array(rows)) { item in
|
|
queueRow(item)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxHeight: 220)
|
|
let extra = offline.downloadQueue.count - min(offline.downloadQueue.count, maxQueueRows)
|
|
if extra > 0 {
|
|
Text("+\(extra) more…")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func queueRow(_ item: OfflineQueueItem) -> some View {
|
|
HStack(spacing: 8) {
|
|
queueIcon(item.state)
|
|
.frame(width: 14)
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(item.name)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
Text(item.show)
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer(minLength: 0)
|
|
queueTrailing(item)
|
|
}
|
|
.opacity(item.state == .pending ? 0.55 : 1)
|
|
}
|
|
|
|
@ViewBuilder private func queueIcon(_ state: OfflineQueueState) -> some View {
|
|
switch state {
|
|
case .pending:
|
|
Image(systemName: "circle")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
case .downloading:
|
|
ProgressView().controlSize(.mini)
|
|
case .done:
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.green)
|
|
case .failed:
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private func queueTrailing(_ item: OfflineQueueItem) -> some View {
|
|
switch item.state {
|
|
case .downloading:
|
|
if let p = item.progress {
|
|
Text("\(Int(p * 100))%")
|
|
.font(.caption2.monospacedDigit())
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text("…")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
case .done:
|
|
Text("done")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
case .failed:
|
|
Text("failed")
|
|
.font(.caption2)
|
|
.foregroundStyle(.orange)
|
|
case .pending:
|
|
Text("queued")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct OfflineDownloadPanelChrome: ViewModifier {
|
|
let compact: Bool
|
|
func body(content: Content) -> some View {
|
|
if compact { content }
|
|
else {
|
|
content
|
|
.padding(10)
|
|
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
|
|
}
|
|
}
|
|
} |