Skip to content
This repository was archived by the owner on Jan 20, 2021. It is now read-only.

Commit 9b3c74e

Browse files
Julian KahnertJulian Kahnert
authored andcommitted
add drag'n'drop
1 parent 069c24e commit 9b3c74e

File tree

10 files changed

+256
-111
lines changed

10 files changed

+256
-111
lines changed

AppClip/MainContentViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Combine
1010
import SwiftUI
1111

1212
final class MainContentViewModel: ObservableObject {
13-
static let imageConverter = ImageConverter { PathConstants.tempPdfURL }
13+
static let imageConverter = ImageConverter { PathConstants.appClipTempPdfURL }
1414

1515
@Published var showAppStoreOverlay = false
1616

AppClip/PDFSharingViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ final class PDFSharingViewModel: ObservableObject, Equatable {
2525
NotificationCenter.default.publisher(for: .foundProcessedDocument)
2626
.compactMap { _ -> URL? in
2727
let fileManager = FileManager.default
28-
return try? fileManager.contentsOfDirectory(at: PathConstants.tempPdfURL, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
28+
return try? fileManager.contentsOfDirectory(at: PathConstants.appClipTempPdfURL, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
2929
.max { url1, url2 in
3030
guard let date1 = (try? fileManager.attributesOfItem(atPath: url1.path))?[.creationDate] as? Date,
3131
let date2 = (try? fileManager.attributesOfItem(atPath: url2.path))?[.creationDate] as? Date else { return false }
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// NSItemProvider.swift
3+
//
4+
//
5+
// Created by Julian Kahnert on 28.12.20.
6+
//
7+
8+
import Foundation
9+
import PDFKit
10+
import UniformTypeIdentifiers
11+
#if os(macOS)
12+
import AppKit.NSImage
13+
private typealias Image = NSImage
14+
#else
15+
import UIKit.UIImage
16+
private typealias Image = UIImage
17+
#endif
18+
19+
public extension NSItemProvider {
20+
enum NSItemProviderError: Error {
21+
case timeout
22+
}
23+
24+
func saveData(at url: URL, with validUTIs: [UTType]) throws -> Bool {
25+
var error: Error?
26+
var data: Data?
27+
28+
for uti in validUTIs where hasItemConformingToTypeIdentifier(uti.identifier) {
29+
do {
30+
data = try syncLoadItem(forTypeIdentifier: uti)
31+
} catch let inputError {
32+
error = inputError
33+
}
34+
35+
guard let data = data else { continue }
36+
37+
if Image(data: data) != nil {
38+
let fileUrl = url.appendingPathComponent(UUID().uuidString).appendingPathExtension("jpeg")
39+
try data.write(to: fileUrl)
40+
return true
41+
} else if PDFDocument(data: data) != nil {
42+
let fileUrl = url.appendingPathComponent(UUID().uuidString).appendingPathExtension("pdf")
43+
try data.write(to: fileUrl)
44+
return true
45+
}
46+
}
47+
48+
if let err = error {
49+
throw err
50+
}
51+
52+
return false
53+
}
54+
55+
func syncLoadItem(forTypeIdentifier uti: UTType) throws -> Data? {
56+
var data: Data?
57+
var error: Error?
58+
let semaphore = DispatchSemaphore(value: 0)
59+
self.loadItem(forTypeIdentifier: uti.identifier, options: nil) { rawData, rawError in
60+
defer {
61+
semaphore.signal()
62+
}
63+
if let rawError = rawError {
64+
error = rawError
65+
}
66+
67+
if let pathData = rawData as? Data,
68+
let path = String(data: pathData, encoding: .utf8),
69+
let url = URL(string: path),
70+
let inputData = Self.getDataIfValid(from: url) {
71+
data = inputData
72+
73+
} else if let url = rawData as? URL,
74+
let inputData = Self.getDataIfValid(from: url) {
75+
data = inputData
76+
77+
} else if let inputData = Self.validate(rawData as? Data) {
78+
data = inputData
79+
80+
} else if let image = rawData as? Image {
81+
#if os(macOS)
82+
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil)!
83+
let bitmapRep = NSBitmapImageRep(cgImage: cgImage)
84+
data = bitmapRep.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 1)])
85+
#else
86+
data = image.jpegData(compressionQuality: 1)
87+
#endif
88+
}
89+
}
90+
let timeoutResult = semaphore.wait(timeout: .now() + .seconds(10))
91+
guard timeoutResult == .success else {
92+
throw NSItemProviderError.timeout
93+
}
94+
95+
if let error = error {
96+
throw error
97+
}
98+
99+
return data
100+
}
101+
102+
private static func getDataIfValid(from url: URL) -> Data? {
103+
guard let data = try? Data(contentsOf: url) else { return nil }
104+
return validate(data)
105+
}
106+
107+
private static func validate(_ data: Data?) -> Data? {
108+
guard let inputData = data else { return data }
109+
if PDFDocument(data: inputData) == nil && Image(data: inputData) == nil {
110+
return nil
111+
}
112+
return inputData
113+
}
114+
}

ArchiveCore/Sources/ArchiveViews/ArchiveTab/ArchiveView.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,30 @@ struct ArchiveView: View {
2525
#endif
2626

2727
var body: some View {
28-
2928
if viewModel.showLoadingView {
3029
LoadingView()
3130
} else {
32-
VStack {
33-
searchView
34-
if !viewModel.availableFilters.isEmpty {
35-
filterQueryItemView
36-
}
37-
documentsView
38-
.resignKeyboardOnDragGesture()
31+
#if os(macOS)
32+
NavigationView {
33+
mainArchiveView
34+
}
35+
#else
36+
mainArchiveView
37+
#endif
38+
}
39+
}
40+
41+
var mainArchiveView: some View {
42+
VStack {
43+
searchView
44+
if !viewModel.availableFilters.isEmpty {
45+
filterQueryItemView
3946
}
40-
.navigationBarTitle(Text("Archive"))
41-
.navigationBarItems(trailing: EditButton())
47+
documentsView
48+
.resignKeyboardOnDragGesture()
4249
}
50+
.navigationBarTitle(Text("Archive"))
51+
.navigationBarItems(trailing: EditButton())
4352
}
4453

4554
var searchView: some View {

ArchiveCore/Sources/ArchiveViews/MainNavigationView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public struct MainNavigationView: View {
6565
}
6666
}
6767

68-
Section(header: Text("Archive")) {
68+
Section(header: Text("Archive").foregroundColor(.paDarkRed)) {
6969
ForEach(viewModel.archiveCategories) { category in
7070
Button {
7171
viewModel.selectedArchive(category)
@@ -77,7 +77,7 @@ public struct MainNavigationView: View {
7777
.accentColor(.systemGray)
7878
}
7979

80-
Section(header: Text("Tags")) {
80+
Section(header: Text("Tags").foregroundColor(.paDarkRed)) {
8181
ForEach(viewModel.tagCategories) { category in
8282
Button {
8383
viewModel.selectedTag(category)

ArchiveCore/Sources/ArchiveViews/ScanTab/ScanTabView.swift

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,30 @@ public struct ScanTabView: View {
3030
Spacer()
3131
staticInfo
3232
Spacer()
33-
VStack(alignment: .leading) {
33+
VStack(alignment: .center) {
3434
ProgressView(viewModel.progressLabel, value: viewModel.progressValue)
3535
.opacity(viewModel.progressValue > 0.0 ? 1 : 0)
36+
#if os(macOS)
37+
importFieldView
38+
#else
3639
scanButton
40+
#endif
3741
}
3842
.fixedSize(horizontal: false, vertical: true)
3943
}
4044
.frame(maxHeight: maxFrameHeight)
4145
.padding(EdgeInsets(top: 32.0, leading: 16.0, bottom: 32.0, trailing: 16.0))
46+
.onDrop(of: [.image, .pdf, .fileURL],
47+
delegate: viewModel)
4248
}
4349

50+
@ViewBuilder
4451
private var staticInfo: some View {
52+
#if os(macOS)
53+
let content: LocalizedStringKey = "Import your documents, tag them and find them sorted in your iCloud Drive."
54+
#else
55+
let content: LocalizedStringKey = "Scan your documents, tag them and find them sorted in your iCloud Drive."
56+
#endif
4557
VStack(alignment: .leading) {
4658
Image("Logo")
4759
.resizable()
@@ -54,7 +66,7 @@ public struct ScanTabView: View {
5466
.foregroundColor(.paDarkRed)
5567
.font(.largeTitle)
5668
.fontWeight(.heavy)
57-
Text("Scan your documents, tag them and find them sorted in your iCloud Drive.")
69+
Text(content)
5870
.font(.title3)
5971
.fixedSize(horizontal: false, vertical: true)
6072
.lineLimit(nil)
@@ -69,17 +81,23 @@ public struct ScanTabView: View {
6981
}
7082
}
7183

72-
var scanButton: some View {
73-
HStack {
74-
Spacer()
75-
Button(action: {
76-
self.viewModel.startScanning()
77-
}, label: {
78-
Text("Scan")
79-
}).buttonStyle(FilledButtonStyle())
80-
.keyboardShortcut("s")
81-
Spacer()
82-
}
84+
private var importFieldView: some View {
85+
Label("Drag'n'Drop to import PDF or Image", systemImage: "doc.text.viewfinder")
86+
.foregroundColor(.paWhite)
87+
.padding(.horizontal, 50)
88+
.padding(.vertical, 20)
89+
.background(.paDarkGray)
90+
.cornerRadius(8)
91+
}
92+
93+
@available(macOS, unavailable)
94+
private var scanButton: some View {
95+
Button(action: {
96+
self.viewModel.startScanning()
97+
}, label: {
98+
Text("Scan")
99+
}).buttonStyle(FilledButtonStyle())
100+
.keyboardShortcut("s")
83101
.padding()
84102
}
85103
}

ArchiveCore/Sources/ArchiveViews/ScanTab/ScanTabViewModel.swift

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@
99
import AVKit
1010
import Combine
1111
import Foundation
12+
import PDFKit
1213
import SwiftUI
1314

14-
public final class ScanTabViewModel: ObservableObject, Log {
15+
#if os(macOS)
16+
import AppKit.NSImage
17+
private typealias UniversalImage = NSImage
18+
#else
19+
import UIKit.UIImage
20+
private typealias UniversalImage = UIImage
21+
#endif
22+
23+
public final class ScanTabViewModel: ObservableObject, DropDelegate, Log {
1524
@Published public var showDocumentScan: Bool = false
1625
@Published public private(set) var progressValue: CGFloat = 0.0
1726
@Published public private(set) var progressLabel: String = " "
@@ -90,6 +99,62 @@ public final class ScanTabViewModel: ObservableObject, Log {
9099
}
91100
}
92101

102+
public func performDrop(info: DropInfo) -> Bool {
103+
let types: [UTType] = [.fileURL, .image, .pdf]
104+
let items = info.itemProviders(for: types)
105+
106+
DispatchQueue.global(qos: .userInitiated).async {
107+
for item in items {
108+
let fileUrlType = UTType.fileURL.identifier
109+
var readDirectorySuccess = false
110+
if item.hasItemConformingToTypeIdentifier(fileUrlType) {
111+
let semaphore = DispatchSemaphore(value: 0)
112+
_ = item.loadObject(ofClass: URL.self) { rawUrl, rawError in
113+
guard let url = rawUrl,
114+
FileManager.default.directoryExists(atPath: url.path) else {
115+
semaphore.signal()
116+
return
117+
}
118+
119+
do {
120+
if let error = rawError {
121+
throw error
122+
}
123+
let urls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
124+
for url in urls {
125+
try self.imageConverter.handle(url)
126+
}
127+
readDirectorySuccess = true
128+
} catch {
129+
self.log.errorAndAssert("Failed to handle file url input.", metadata: ["error": "\(error)"])
130+
}
131+
semaphore.signal()
132+
}
133+
_ = semaphore.wait(timeout: .now() + .seconds(30))
134+
}
135+
if readDirectorySuccess {
136+
return
137+
}
138+
139+
for uti in types where item.hasItemConformingToTypeIdentifier(uti.identifier) {
140+
do {
141+
guard let data = try item.syncLoadItem(forTypeIdentifier: uti) else { continue }
142+
143+
let url = PathConstants.tempPdfURL.appendingPathComponent("\(UUID().uuidString).pdf")
144+
try data.write(to: url)
145+
try self.imageConverter.handle(url)
146+
return
147+
} catch {
148+
self.log.errorAndAssert("Failed to handle image/pdf with type \(uti.identifier). Try next ...", metadata: ["error": "\(error)"])
149+
// TODO: show error
150+
}
151+
}
152+
}
153+
}
154+
155+
return true
156+
}
157+
93158
public func process(_ images: [CIImage]) {
94159
assert(!Thread.isMainThread, "This might take some time and should not be executed on the main thread.")
95160

@@ -101,15 +166,14 @@ public final class ScanTabViewModel: ObservableObject, Log {
101166

102167
// save images in reversed order to fix the API output order
103168
do {
104-
defer {
105-
// notify ImageConverter even if the image saving has failed
106-
triggerImageProcessing()
107-
}
108169
try StorageHelper.save(images)
109170
} catch {
110171
assertionFailure("Could not save temp images with error:\n\(error)")
111172
NotificationCenter.default.postAlert(error)
112173
}
174+
175+
// notify ImageConverter even if the image saving has failed
176+
triggerImageProcessing()
113177
}
114178

115179
// MARK: - Helper Functions

0 commit comments

Comments
 (0)