Tusker/Tusker/Screens/Compose/ComposeAttachmentRow.swift

207 lines
7.6 KiB
Swift

//
// 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) {
if #available(iOS 14.0, *) {
Label("Edit Drawing", systemImage: "hand.draw")
} else {
HStack {
Text("Edit Drawing")
Image(systemName: "hand.draw")
}
}
}
} else if attachment.data.type == .image {
Button(action: self.recognizeText) {
if #available(iOS 14.0, *) {
Label("Recognize Text", systemImage: "doc.text.viewfinder")
} else {
HStack {
Text("Recognize Text")
Image(systemName: "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:
if #available(iOS 14.0, *) {
ProgressView()
} else {
ActivityIndicatorView()
}
}
// 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()
// }
//}