// // 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: Draft @ObservedObject var attachment: CompositionAttachment let heightChanged: (CGFloat) -> Void @EnvironmentObject var uiState: ComposeUIState @State private var mode: Mode = .allowEntry @State private var image: UIImage? = nil @State private var imageContentMode: ContentMode = .fill @State private var imageBackgroundColor: Color = .black @State private var isShowingTextRecognitionFailedAlert = false @State private var textRecognitionErrorMessage: String? = nil @Environment(\.colorScheme) private var colorScheme: ColorScheme var body: some View { HStack(alignment: .center, spacing: 4) { imageView .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") } } } switch mode { case .allowEntry: ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80) .heightDidChange(self.heightChanged) .backgroundColor(.clear) .fontSize(17) 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) // } } .onAppear(perform: self.loadImage) .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")) ) } } @ViewBuilder private var imageView: some View { if let image = image { Image(uiImage: image) .resizable() .aspectRatio(contentMode: imageContentMode) .background(imageBackgroundColor) } else { Image(systemName: placeholderImageName) } } private var placeholderImageName: String { switch colorScheme { case .dark: return "photo.fill" case .light: return "photo" @unknown default: return "photo" } } private func loadImage() { switch attachment.data { case let .image(image): self.image = image case let .asset(asset): let size = CGSize(width: 80, height: 80) PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in DispatchQueue.main.async { self.image = image } } case let .video(url): let asset = AVURLAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) { self.image = UIImage(cgImage: cgImage) } case let .drawing(drawing): image = drawing.imageInLightMode(from: drawing.bounds) imageContentMode = .fit imageBackgroundColor = .white } } private func removeAttachment() { 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 { (data, mimeType) in 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 } } //struct ComposeAttachmentRow_Previews: PreviewProvider { // static var previews: some View { // ComposeAttachmentRow() // } //}