// // AttachmentRowController.swift // ComposeUI // // Created by Shadowfacts on 3/12/23. // import SwiftUI import TuskerComponents import Vision class AttachmentRowController: ViewController { let parent: ComposeController let attachment: DraftAttachment @Published var descriptionMode: DescriptionMode = .allowEntry @Published var textRecognitionError: Error? init(parent: ComposeController, attachment: DraftAttachment) { self.parent = parent self.attachment = attachment } var view: some View { AttachmentView(attachment: attachment) } private func removeAttachment() { withAnimation { parent.draft.attachments.removeAll(where: { $0.id == attachment.id }) } } private func editDrawing() { guard case .drawing(let drawing) = attachment.data else { return } parent.config.presentDrawing?(drawing) { newDrawing in self.attachment.data = .drawing(newDrawing) } } private func recognizeText() { descriptionMode = .recognizingText DispatchQueue.global(qos: .userInitiated).async { self.attachment.data.getData(features: self.parent.mastodonController.instanceFeatures, skipAllConversion: true) { result in let data: Data switch result { case .success((let d, _)): data = d case .failure(let error): self.descriptionMode = .allowEntry self.textRecognitionError = error return } let handler = VNImageRequestHandler(data: data) let request = VNRecognizeTextRequest { request, error in DispatchQueue.main.async { if let results = request.results as? [VNRecognizedTextObservation] { var text = "" for observation in results { let result = observation.topCandidates(1).first! text.append(result.string) text.append("\n") } self.attachment.attachmentDescription = text } self.descriptionMode = .allowEntry } } request.recognitionLevel = .accurate request.usesLanguageCorrection = true DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch let error as NSError where error.code == 1 { // The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for. return } catch { DispatchQueue.main.async { self.descriptionMode = .allowEntry self.textRecognitionError = error } } } } } } struct AttachmentView: View { @ObservedObject private var attachment: DraftAttachment @EnvironmentObject private var controller: AttachmentRowController init(attachment: DraftAttachment) { self.attachment = attachment } var body: some View { HStack(alignment: .center, spacing: 4) { AttachmentThumbnailView(attachment: attachment, fullSize: false) .frame(width: 80, height: 80) .cornerRadius(8) .contextMenu { if case .drawing(_) = attachment.data { Button(action: controller.editDrawing) { Label("Edit Drawing", systemImage: "hand.draw") } } else if attachment.data.type == .image { Button(action: controller.recognizeText) { Label("Recognize Text", systemImage: "doc.text.viewfinder") } } Button(role: .destructive, action: controller.removeAttachment) { Label("Delete", systemImage: "trash") } } previewIfAvailable: { AttachmentThumbnailView(attachment: attachment, fullSize: true) } switch controller.descriptionMode { case .allowEntry: AttachmentDescriptionTextView( text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80 ) case .recognizingText: ProgressView() .progressViewStyle(.circular) } } .alertWithData("Text Recognition Failed", data: $controller.textRecognitionError) { _ in Button("OK") {} } message: { error in Text(error.localizedDescription) } } } } extension AttachmentRowController { enum DescriptionMode { case allowEntry, recognizingText } } private extension View { @available(iOS, obsoleted: 16.0) @ViewBuilder func contextMenu(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View { if #available(iOS 16.0, *) { self.contextMenu(menuItems: menuItems, preview: preview) } else { self.contextMenu(menuItems: menuItems) } } }