tv-anarchy/Sources/TVAnarchy/MetadataView.swift

105 lines
3.8 KiB
Swift
Raw Permalink Normal View History

import SwiftUI
import TVAnarchyCore
/// Metadata pipeline: shows each library title with its regex-parsed format
/// badge (instant, offline) and an Enrich action that resolves TMDB/IMDb,
/// writes the `.meta` cache, and pushes poster/overview into the Library grid.
struct MetadataView: View {
@Bindable var metadata: MetadataController
var body: some View {
VStack(alignment: .leading, spacing: 0) {
header
Divider()
List(metadata.rows) { row in
MetaRowView(row: row) { Task { await metadata.enrich(row.id) } }
}
}
.onAppear { metadata.reload() }
}
private var header: some View {
HStack(spacing: 12) {
Text("Metadata").font(.title2).bold()
Spacer()
if let msg = metadata.lastMessage {
Text(msg).font(.caption).foregroundStyle(.secondary).lineLimit(1)
.onTapGesture { copyToClipboard(msg) }.help("Click to copy")
}
if metadata.busy { ProgressView().controlSize(.small) }
Button { Task { await metadata.enrichAll() } } label: {
Label("Enrich all", systemImage: "wand.and.stars")
}
.disabled(metadata.busy || metadata.rows.isEmpty)
}
.padding(16)
}
}
private struct MetaRowView: View {
let row: MetadataController.Row
let onEnrich: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 12) {
PosterThumb(urlString: row.meta?.posterURL).frame(width: 46, height: 69)
VStack(alignment: .leading, spacing: 4) {
Text(row.meta?.resolvedTitle ?? row.show.name).font(.headline).lineLimit(1)
badges
if let overview = row.meta?.overview, !overview.isEmpty {
Text(overview).font(.caption).foregroundStyle(.secondary).lineLimit(2)
}
}
Spacer()
if row.enriching {
ProgressView().controlSize(.small)
} else {
Button(row.meta == nil ? "Enrich" : "Re-enrich", action: onEnrich)
.buttonStyle(.bordered)
}
}
.padding(.vertical, 4)
}
private var badges: some View {
HStack(spacing: 6) {
if let s = row.sample {
if let q = s.quality { tag(q.uppercased(), .blue) }
if let c = s.codec { tag(c.uppercased(), .purple) }
}
if !row.show.countSummary.isEmpty {
Text(row.show.countSummary).font(.caption2).foregroundStyle(.secondary)
}
if let r = row.meta?.imdbRating { tag(String(format: "IMDb %.1f", r), .yellow) }
if let genres = row.meta?.genres, let g = genres.first { tag(g, .gray) }
}
}
private func tag(_ text: String, _ color: Color) -> some View {
Text(text)
.font(.caption2.monospacedDigit())
.padding(.horizontal, 6).padding(.vertical, 2)
.background(color.opacity(0.2), in: Capsule())
.foregroundStyle(color)
}
}
/// Small poster that fetches a remote TMDB URL (or shows a placeholder).
private struct PosterThumb: View {
let urlString: String?
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 6).fill(.quaternary)
if let s = urlString, let url = URL(string: s) {
AsyncImage(url: url) { img in
img.resizable().aspectRatio(contentMode: .fill)
} placeholder: { ProgressView().controlSize(.small) }
.clipShape(RoundedRectangle(cornerRadius: 6))
} else {
Image(systemName: "film").foregroundStyle(.secondary)
}
}
}
}