Files
stackdex_neu/StackDex/Views/ResultEditorView.swift
Matthias a60a76b797 Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
2026-04-19 21:11:32 +02:00

172 lines
5.9 KiB
Swift

import SwiftData
import SwiftUI
import UIKit
struct ResultEditorView: View {
enum Mode {
case recognized
case manual
var title: String {
switch self {
case .recognized:
return "Review Result"
case .manual:
return "Manual Entry"
}
}
}
@Environment(\.modelContext) private var modelContext
@ObservedObject var flowModel: ScanFlowModel
let mode: Mode
var body: some View {
Group {
if let session = flowModel.currentSession {
Form {
if let image = session.thumbnailJPEGData.flatMap(UIImage.init(data:)) {
Section {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: 18))
}
}
Section("Recognition") {
LabeledContent("Source", value: draftBinding.wrappedValue.source.title)
LabeledContent("Confidence", value: draftBinding.wrappedValue.confidence.title)
Text(draftBinding.wrappedValue.confidence.helperText)
.font(.footnote)
.foregroundStyle(.secondary)
Text(draftBinding.wrappedValue.source.detail)
.font(.footnote)
.foregroundStyle(.secondary)
}
Section("Card details") {
TextField("Card name", text: draftBinding.cardName)
.textInputAutocapitalization(.words)
TextField("Card number", text: draftBinding.cardNumber)
.textInputAutocapitalization(.never)
TextField("Set", text: draftBinding.setIdentifier)
.textInputAutocapitalization(.words)
Picker("Rarity", selection: draftBinding.rarity) {
ForEach(CardRarity.allCases) { rarity in
Text(rarity.rawValue).tag(rarity.rawValue)
}
}
}
if !draftBinding.wrappedValue.notes.isEmpty {
Section("Hints") {
ForEach(Array(draftBinding.wrappedValue.notes.enumerated()), id: \.offset) { _, note in
Text(note)
.font(.subheadline)
}
}
}
if !draftBinding.wrappedValue.rawText.isEmpty {
Section("OCR fragments") {
Text(draftBinding.wrappedValue.rawText)
.font(.footnote.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
Section {
Button("Confirm and Log") {
confirmDraft()
}
.buttonStyle(.borderedProminent)
Button(mode == .recognized ? "Manual Entry" : "Re-scan") {
if mode == .recognized {
flowModel.openManualEntryFromResult()
} else {
flowModel.returnToScan()
}
}
if mode == .recognized {
Button("Re-scan") {
flowModel.returnToScan()
}
.foregroundStyle(.red)
}
}
}
} else {
ContentUnavailableView("No scan loaded", systemImage: "rectangle.and.text.magnifyingglass", description: Text("Capture or import a card image first."))
}
}
.navigationTitle(mode.title)
.navigationBarTitleDisplayMode(.inline)
}
private var draftBinding: Binding<CardRecognitionDraft> {
Binding(
get: {
flowModel.currentSession?.draft ?? .manualPrefill()
},
set: { flowModel.updateDraft($0) }
)
}
private func confirmDraft() {
let draft = draftBinding.wrappedValue
modelContext.insert(ConfirmedScanRecord(draft: draft))
flowModel.returnToScan(message: "Saved to temporary local log.")
}
}
private extension Binding where Value == CardRecognitionDraft {
var cardName: Binding<String> {
Binding<String>(
get: { wrappedValue.cardName },
set: {
var draft = wrappedValue
draft.cardName = $0
wrappedValue = draft
}
)
}
var cardNumber: Binding<String> {
Binding<String>(
get: { wrappedValue.cardNumber },
set: {
var draft = wrappedValue
draft.cardNumber = $0
wrappedValue = draft
}
)
}
var setIdentifier: Binding<String> {
Binding<String>(
get: { wrappedValue.setIdentifier },
set: {
var draft = wrappedValue
draft.setIdentifier = $0
wrappedValue = draft
}
)
}
var rarity: Binding<String> {
Binding<String>(
get: { wrappedValue.rarity },
set: {
var draft = wrappedValue
draft.rarity = $0
wrappedValue = draft
}
)
}
}