OCR desde imagen del portapapeles + botón copiar diagnóstico iCloud (build 61)
- Quick Update: checkClipboard ahora detecta también una IMAGEN copiada (pb.hasImages) y hace OCR automático — te ofrece pegar el importe detectado sin pulsar 'Escanear'. Gated por changeCount. El 'Pegar €X' del teclado también usa el importe OCR'd de la imagen. - Settings iCloud: botón 'Copiar diagnóstico de iCloud' SIEMPRE visible (el desplegable de detalle solo salía con un error activo, difícil de encontrar). Copia versión/build, contadores locales, últimas fechas import/export, criticidad, hint y el error completo con códigos internos. Strings ×7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
@@ -24,6 +24,7 @@ struct QuickUpdateView: View {
|
||||
@FocusState private var focusedSource: NSManagedObjectID?
|
||||
@State private var clipboardAmount: Decimal?
|
||||
@State private var lastSuggestedRaw: String?
|
||||
@State private var lastPasteboardChangeCount = -1
|
||||
|
||||
// Per-field paste/scan (feedback #3): keyboard toolbar acts on the focused
|
||||
// field — paste a clipboard amount or OCR a number from a screenshot.
|
||||
@@ -173,12 +174,14 @@ struct QuickUpdateView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clipboard amount for the keyboard toolbar (independent of the top banner's
|
||||
/// one-shot suggestion state).
|
||||
/// Clipboard amount for the keyboard toolbar — a live text amount, or the
|
||||
/// amount OCR'd from a copied image (held in clipboardAmount).
|
||||
private var clipboardFieldAmount: Decimal? {
|
||||
guard let raw = UIPasteboard.general.string,
|
||||
let parsed = CurrencyFormatter.parseUserInput(raw), parsed > 0 else { return nil }
|
||||
return parsed
|
||||
if let raw = UIPasteboard.general.string,
|
||||
let parsed = CurrencyFormatter.parseUserInput(raw), parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
return clipboardAmount
|
||||
}
|
||||
|
||||
private func decimalInputString(_ value: Decimal) -> String {
|
||||
@@ -286,17 +289,30 @@ struct QuickUpdateView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// Reads the pasteboard and offers the parsed amount for the next pending source.
|
||||
/// Skips values already suggested (or already typed) so returning to the app
|
||||
/// with the same clipboard doesn't nag.
|
||||
/// Reads the pasteboard and offers a parsed amount for the next pending source.
|
||||
/// Handles BOTH a copied text value and a copied image (screenshot) — the
|
||||
/// image is OCR'd on-device and its most prominent number offered. Gated by
|
||||
/// the pasteboard change count so returning with the same clipboard doesn't nag.
|
||||
private func checkClipboard() {
|
||||
guard let raw = UIPasteboard.general.string, raw != lastSuggestedRaw,
|
||||
let parsed = CurrencyFormatter.parseUserInput(raw), parsed > 0,
|
||||
nextEmptySource != nil else {
|
||||
let pb = UIPasteboard.general
|
||||
guard pb.changeCount != lastPasteboardChangeCount, nextEmptySource != nil else { return }
|
||||
lastPasteboardChangeCount = pb.changeCount
|
||||
|
||||
// 1. Text amount (cheap, synchronous).
|
||||
if let raw = pb.string, let parsed = CurrencyFormatter.parseUserInput(raw), parsed > 0 {
|
||||
clipboardAmount = parsed
|
||||
return
|
||||
}
|
||||
lastSuggestedRaw = raw
|
||||
clipboardAmount = parsed
|
||||
// 2. Copied image → OCR the amount automatically.
|
||||
if pb.hasImages, let image = pb.image {
|
||||
isScanning = true
|
||||
ImageAmountScanner.scan(image) { candidates in
|
||||
DispatchQueue.main.async {
|
||||
isScanning = false
|
||||
if let best = candidates.first { clipboardAmount = best.value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyClipboard(_ amount: Decimal, to source: InvestmentSource) {
|
||||
|
||||
@@ -464,6 +464,18 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Always-available diagnostics: copies status + last error (with
|
||||
// the inner CloudKit codes) so it can be shared for support.
|
||||
Button {
|
||||
UIPasteboard.general.string = iCloudDiagnostics()
|
||||
forceUploadResult = String(localized: "icloud_diagnostics_copied")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 4) { forceUploadResult = nil }
|
||||
} label: {
|
||||
Label(String(localized: "icloud_copy_diagnostics"), systemImage: "doc.on.doc")
|
||||
.font(.caption)
|
||||
.foregroundColor(.appPrimary)
|
||||
}
|
||||
|
||||
// Force upload — use when data exists locally but hasn't reached iCloud
|
||||
if cloudStack.localSourceCount > 0 {
|
||||
if let result = forceUploadResult {
|
||||
@@ -874,6 +886,20 @@ struct SettingsView: View {
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: bytes)
|
||||
}
|
||||
|
||||
/// Copyable iCloud diagnostics snapshot for support/debugging.
|
||||
private func iCloudDiagnostics() -> String {
|
||||
let df = ISO8601DateFormatter()
|
||||
var lines: [String] = ["Portfolio Journal — iCloud diagnostics"]
|
||||
lines.append("app: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"))")
|
||||
lines.append("local: \(cloudStack.localSourceCount) sources · \(cloudStack.localSnapshotCount) snapshots")
|
||||
lines.append("lastImport: \(cloudStack.lastImportDate.map { df.string(from: $0) } ?? "never")")
|
||||
lines.append("lastExport: \(cloudStack.lastExportDate.map { df.string(from: $0) } ?? "never")")
|
||||
lines.append("critical: \(cloudStack.syncErrorIsCritical)")
|
||||
lines.append("hint: \(cloudStack.lastSyncErrorHint ?? "—")")
|
||||
lines.append("error: \(cloudStack.lastSyncError ?? "none this session")")
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Activity View
|
||||
|
||||
Reference in New Issue
Block a user