forked from shadowfacts/Tusker
207 lines
7.6 KiB
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()
|
|
// }
|
|
//}
|