// // 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? private var descriptionObservation: NSKeyValueObservation? init(parent: ComposeController, attachment: DraftAttachment) { self.parent = parent self.attachment = attachment descriptionObservation = attachment.observe(\.attachmentDescription, changeHandler: { [unowned self] _, _ in // the faultingState is non-zero for objects that are being cascade deleted when the draft is deleted if attachment.faultingState == 0 { self.updateAttachmentDescriptionState() } }) } private func updateAttachmentDescriptionState() { if attachment.attachmentDescription.isEmpty { parent.attachmentsMissingDescriptions.insert(attachment.id) } else { parent.attachmentsMissingDescriptions.remove(attachment.id) } } var view: some View { AttachmentView(attachment: attachment) } private func removeAttachment() { withAnimation { parent.draft.attachments.remove(attachment) } } private func editDrawing() { guard case .drawing(let drawing) = attachment.data else { return } parent.config.presentDrawing?(drawing) { newDrawing in self.attachment.drawing = newDrawing } } private func recognizeText() { descriptionMode = .recognizingText DispatchQueue.global(qos: .userInitiated).async { self.attachment.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 attachment.drawingData != nil { Button(action: controller.editDrawing) { Label("Edit Drawing", systemImage: "hand.draw") } } else if attachment.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) } .onAppear(perform: controller.updateAttachmentDescriptionState) } } } 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) } } }