2026-01-08 19:16:32 +00:00
|
|
|
//
|
|
|
|
|
// CandidatesView.swift
|
|
|
|
|
// iris
|
|
|
|
|
//
|
|
|
|
|
// Created by Codex.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
struct CandidatesView: View {
|
|
|
|
|
@StateObject private var model = CandidatesViewModel()
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
List {
|
|
|
|
|
Section("Source") {
|
|
|
|
|
LabeledContent("Location") {
|
|
|
|
|
Text("London (demo)")
|
|
|
|
|
}
|
|
|
|
|
if let updated = model.lastUpdatedAt {
|
|
|
|
|
LabeledContent("Last update") { Text(timeOnly(from: updated)) }
|
|
|
|
|
} else {
|
|
|
|
|
LabeledContent("Last update") { Text("Never") }
|
|
|
|
|
}
|
|
|
|
|
if let error = model.lastError {
|
|
|
|
|
Text(error)
|
|
|
|
|
.font(.footnote)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !model.diagnostics.isEmpty {
|
|
|
|
|
Section("Diagnostics") {
|
|
|
|
|
ForEach(model.diagnostics.keys.sorted(), id: \.self) { key in
|
|
|
|
|
LabeledContent(key) {
|
|
|
|
|
Text(model.diagnostics[key] ?? "")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.textSelection(.enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Section("Candidates (\(model.candidates.count))") {
|
|
|
|
|
if model.candidates.isEmpty {
|
|
|
|
|
Text(model.isLoading ? "Loading…" : "No candidates")
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
} else {
|
|
|
|
|
ForEach(model.candidates, id: \.id) { candidate in
|
|
|
|
|
CandidateRow(candidate: candidate)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.navigationTitle("Candidates")
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
|
|
|
Button(model.isLoading ? "Refreshing…" : "Refresh") {
|
|
|
|
|
model.refresh()
|
|
|
|
|
}
|
|
|
|
|
.disabled(model.isLoading)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.onAppear { model.refresh() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func timeOnly(from date: Date) -> String {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .none
|
|
|
|
|
formatter.timeStyle = .medium
|
|
|
|
|
return formatter.string(from: date)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct CandidateRow: View {
|
2026-01-10 00:25:36 +00:00
|
|
|
let candidate: FeedItem
|
2026-01-08 19:16:32 +00:00
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
|
|
|
HStack(alignment: .firstTextBaseline) {
|
|
|
|
|
Text(candidate.title)
|
|
|
|
|
.font(.headline)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
Spacer()
|
|
|
|
|
Text(candidate.type.rawValue)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
2026-01-10 00:25:36 +00:00
|
|
|
if !candidate.subtitle.isEmpty {
|
|
|
|
|
Text(candidate.subtitle)
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
}
|
2026-01-08 19:16:32 +00:00
|
|
|
|
|
|
|
|
HStack(spacing: 12) {
|
2026-01-10 00:25:36 +00:00
|
|
|
Text(String(format: "prio %.2f", candidate.priority))
|
2026-01-08 19:16:32 +00:00
|
|
|
Text("ttl \(candidate.ttlSec)s")
|
|
|
|
|
}
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
.padding(.vertical, 4)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct CandidatesView_Previews: PreviewProvider {
|
|
|
|
|
static var previews: some View {
|
|
|
|
|
CandidatesView()
|
|
|
|
|
}
|
|
|
|
|
}
|