initial commit
This commit is contained in:
120
IrisCompanion/iris/Views/CandidatesView.swift
Normal file
120
IrisCompanion/iris/Views/CandidatesView.swift
Normal file
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// 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 {
|
||||
let candidate: Candidate
|
||||
|
||||
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)
|
||||
}
|
||||
Text(candidate.subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Text(String(format: "conf %.2f", candidate.confidence))
|
||||
Text("ttl \(candidate.ttlSec)s")
|
||||
Text(expiresText(now: Int(Date().timeIntervalSince1970)))
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
private func expiresText(now: Int) -> String {
|
||||
let expiresAt = candidate.createdAt + candidate.ttlSec
|
||||
let remaining = expiresAt - now
|
||||
if remaining <= 0 { return "expired" }
|
||||
if remaining < 60 { return "in \(remaining)s" }
|
||||
return "in \(remaining / 60)m"
|
||||
}
|
||||
}
|
||||
|
||||
struct CandidatesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CandidatesView()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user