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.
172 lines
5.9 KiB
Swift
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
|
|
}
|
|
)
|
|
}
|
|
}
|