import SwiftUI import AppKit import UniformTypeIdentifiers final class ResultWindow { static let shared = ResultWindow() private var window: NSWindow? @MainActor func show(image: CGImage) { window?.close() let model = CaptureModel(image: image) let hosting = NSHostingController(rootView: ResultView(model: model) { [weak self] in self?.window?.close() }) let w = NSWindow(contentViewController: hosting) w.title = "Kapture — Screenshot" w.level = .floating w.styleMask = [.titled, .closable, .resizable] w.setContentSize(NSSize(width: 694, height: 520)) w.isReleasedWhenClosed = false w.center() w.makeKeyAndOrderFront(nil) window = w } @MainActor func showOpenPanel() { let panel = NSOpenPanel() panel.allowedContentTypes = [.image] panel.allowsMultipleSelection = false guard panel.runModal() == .OK, let url = panel.url else { return } guard let image = NSImage(contentsOf: url), let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { self.show(image: Self.failureImage("Could not load the selected image.")) return } show(image: cgImage) } static func failureImage(_ message: String) -> CGImage { let size = NSSize(width: 800, height: 200) guard let rep = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { return NSImage(size: size).cgImage(forProposedRect: nil, context: nil, hints: nil)! } NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep) NSColor.white.setFill() NSRect(origin: .zero, size: size).fill() let attrs: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: 30), .foregroundColor: NSColor.systemRed, ] message.draw(at: NSPoint(x: 40, y: 80), withAttributes: attrs) NSGraphicsContext.restoreGraphicsState() return rep.cgImage! } } @MainActor final class CaptureModel: ObservableObject { let image: CGImage @Published var mode: CaptureMode = .screenshot @Published var extractedText: String? @Published var isOCRRunning = false @Published var statusMessage: String? enum CaptureMode { case screenshot case text } init(image: CGImage) { self.image = image } var displayText: String { if isOCRRunning { return "Recognizing text…" } return extractedText ?? "Text appears here — click “Get Text”." } func runOCR() { guard !isOCRRunning else { return } isOCRRunning = true statusMessage = nil OCRService.shared.recognize(image) { [weak self] text in Task { @MainActor in guard let self else { return } self.isOCRRunning = false self.mode = .text if text.isEmpty { self.extractedText = "No text found in the screenshot." } else { self.extractedText = text let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(text, forType: .string) self.statusMessage = "Copied to clipboard." } } } } func copyText() { guard let text = extractedText else { return } let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(text, forType: .string) statusMessage = "Copied to clipboard." } func savePNG() { let panel = NSSavePanel() panel.allowedContentTypes = [.png] panel.nameFieldStringValue = "Kapture-\(Self.timestamp()).png" guard panel.runModal() == .OK, let url = panel.url else { return } guard let dest = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil) else { statusMessage = "Could not create PNG file." return } CGImageDestinationAddImage(dest, image, nil) if CGImageDestinationFinalize(dest) { statusMessage = "Saved PNG." } else { statusMessage = "Could not save PNG." } } private static func timestamp() -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd-HHmmss" return formatter.string(from: Date()) } } struct ResultView: View { @ObservedObject var model: CaptureModel let onClose: () -> Void var body: some View { VStack(spacing: 10) { HStack { Picker("", selection: $model.mode) { Text("Screenshot").tag(CaptureModel.CaptureMode.screenshot) Text("Text").tag(CaptureModel.CaptureMode.text) if model.isOCRRunning { Text("…").tag(CaptureModel.CaptureMode.text) } } .pickerStyle(.segmented) .fixedSize() Spacer() Button("Save PNG…") { model.savePNG() } Button("Get Text") { model.runOCR() } .disabled(model.isOCRRunning || model.extractedText != nil || model.mode == .text) Button("Close") { onClose() } } .padding(.horizontal, 12) .padding(.top, 10) if model.mode == .screenshot { Image(nsImage: NSImage(cgImage: model.image, size: .zero)) .resizable() .aspectRatio(contentMode: .fit) .border(Color.secondary.opacity(0.4)) .padding(.horizontal, 12) } else { ZStack(alignment: .bottomTrailing) { ScrollView { Text(model.displayText) .font(.system(.body)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding(8) } .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color.secondary.opacity(0.4)) ) HStack(spacing: 8) { if let msg = model.statusMessage { Text(msg) .font(.caption) .foregroundColor(.secondary) } Button("Copy Text") { model.copyText() } } .padding(8) } } if model.statusMessage != nil && model.mode == .screenshot { Text(model.statusMessage!) .font(.caption) .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) } } .padding(.bottom, 10) .frame(minWidth: 480, minHeight: 380) } }