// // ComposeAttachmentRow.swift // Tusker // // Created by Shadowfacts on 8/19/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import SwiftUI import Photos import AVFoundation import Vision struct ComposeAttachmentRow: View { @ObservedObject var draft: OldDraft @ObservedObject var attachment: CompositionAttachment @EnvironmentObject var mastodonController: MastodonController @EnvironmentObject var uiState: ComposeUIState @State private var mode: Mode = .allowEntry @State private var isShowingTextRecognitionFailedAlert = false @State private var textRecognitionErrorMessage: String? = nil var body: some View { HStack(alignment: .center, spacing: 4) { ComposeAttachmentImage(attachment: attachment, fullSize: false) .frame(width: 80, height: 80) .cornerRadius(8) .contextMenu { if case .drawing(_) = attachment.data { Button(action: self.editDrawing) { Label("Edit Drawing", systemImage: "hand.draw") } } else if attachment.data.type == .image { Button(action: self.recognizeText) { Label("Recognize Text", systemImage: "doc.text.viewfinder") } } Button(role: .destructive, action: self.removeAttachment) { Label("Delete", systemImage: "trash") } } previewIfAvailable: { ComposeAttachmentImage(attachment: attachment, fullSize: true) } switch mode { case .allowEntry: ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80) .backgroundColor(.clear) case .recognizingText: ProgressView() } // todo: find a way to make this button not activated when the list row is selected, see FB8595628 // Button(action: self.removeAttachment) { // Image(systemName: "xmark.circle.fill") // .foregroundColor(.blue) // } } .onReceive(attachment.$attachmentDescription) { (newDesc) in if newDesc.isEmpty { uiState.attachmentsMissingDescriptions.insert(attachment.id) } else { uiState.attachmentsMissingDescriptions.remove(attachment.id) } } .alert(isPresented: $isShowingTextRecognitionFailedAlert) { Alert( title: Text("Text Recognition Failed"), message: Text(self.textRecognitionErrorMessage ?? ""), dismissButton: .default(Text("OK")) ) } } private func removeAttachment() { withAnimation { draft.attachments.removeAll { $0.id == attachment.id } } } private func editDrawing() { uiState.composeDrawingMode = .edit(id: attachment.id) uiState.delegate?.presentComposeDrawing() } private func recognizeText() { mode = .recognizingText DispatchQueue.global(qos: .userInitiated).async { self.attachment.data.getData(features: mastodonController.instanceFeatures, skipAllConversion: true) { (result) in let data: Data do { try data = result.get().0 } catch { DispatchQueue.main.async { self.mode = .allowEntry self.isShowingTextRecognitionFailedAlert = true self.textRecognitionErrorMessage = error.localizedDescription } return } let handler = VNImageRequestHandler(data: data, options: [:]) 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.mode = .allowEntry } } request.recognitionLevel = .accurate request.usesLanguageCorrection = true DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { // The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for. guard (error as NSError).code != 1 else { return } DispatchQueue.main.async { self.mode = .allowEntry self.isShowingTextRecognitionFailedAlert = true self.textRecognitionErrorMessage = error.localizedDescription } } } } } } } extension ComposeAttachmentRow { enum Mode { 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) } } } //struct ComposeAttachmentRow_Previews: PreviewProvider { // static var previews: some View { // ComposeAttachmentRow() // } //}