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.
This commit is contained in:
171
StackDex/Views/ResultEditorView.swift
Normal file
171
StackDex/Views/ResultEditorView.swift
Normal file
@@ -0,0 +1,171 @@
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user