|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+import Cocoa
|
|
|
2
|
+import UniformTypeIdentifiers
|
|
|
3
|
+
|
|
|
4
|
+enum DocumentPickerKind {
|
|
|
5
|
+ case photos
|
|
|
6
|
+ case files
|
|
|
7
|
+ case importFile
|
|
|
8
|
+}
|
|
|
9
|
+
|
|
|
10
|
+enum DocumentPickerService {
|
|
|
11
|
+ private static var activeDelegate: DocumentPickerPanelDelegate?
|
|
|
12
|
+
|
|
|
13
|
+ private static let photoExtensions: Set<String> = [
|
|
|
14
|
+ "jpg", "jpeg", "png", "heic", "heif", "gif", "tiff", "tif", "bmp", "webp", "raw", "svg",
|
|
|
15
|
+ ]
|
|
|
16
|
+
|
|
|
17
|
+ private static let documentExtensions: Set<String> = [
|
|
|
18
|
+ "pdf", "txt", "rtf", "csv", "xml", "html", "htm", "json", "md",
|
|
|
19
|
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pages", "numbers", "key",
|
|
|
20
|
+ ]
|
|
|
21
|
+
|
|
|
22
|
+ static func present(_ kind: DocumentPickerKind, from window: NSWindow?) {
|
|
|
23
|
+ let panel = NSOpenPanel()
|
|
|
24
|
+ panel.canChooseDirectories = false
|
|
|
25
|
+ panel.canChooseFiles = true
|
|
|
26
|
+ panel.allowsOtherFileTypes = false
|
|
|
27
|
+
|
|
|
28
|
+ switch kind {
|
|
|
29
|
+ case .photos:
|
|
|
30
|
+ panel.title = "Open Gallery"
|
|
|
31
|
+ panel.message = "Select photos from your gallery."
|
|
|
32
|
+ panel.prompt = "Select Photos"
|
|
|
33
|
+ panel.allowsMultipleSelection = true
|
|
|
34
|
+ panel.allowedContentTypes = photoContentTypes
|
|
|
35
|
+ panel.directoryURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first
|
|
|
36
|
+ case .files:
|
|
|
37
|
+ panel.title = "Open File Manager"
|
|
|
38
|
+ panel.message = "Select photos or files from your file manager."
|
|
|
39
|
+ panel.prompt = "Select"
|
|
|
40
|
+ panel.allowsMultipleSelection = true
|
|
|
41
|
+ panel.allowedContentTypes = fileManagerContentTypes
|
|
|
42
|
+ panel.directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
|
43
|
+ case .importFile:
|
|
|
44
|
+ panel.title = "Import Files"
|
|
|
45
|
+ panel.message = "Add files from storage."
|
|
|
46
|
+ panel.prompt = "Add Files"
|
|
|
47
|
+ panel.allowsMultipleSelection = true
|
|
|
48
|
+ panel.allowedContentTypes = documentContentTypes
|
|
|
49
|
+ panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
|
|
50
|
+ }
|
|
|
51
|
+
|
|
|
52
|
+ let delegate = DocumentPickerPanelDelegate(kind: kind)
|
|
|
53
|
+ activeDelegate = delegate
|
|
|
54
|
+ panel.delegate = delegate
|
|
|
55
|
+
|
|
|
56
|
+ let completion: (NSApplication.ModalResponse) -> Void = { response in
|
|
|
57
|
+ defer { activeDelegate = nil }
|
|
|
58
|
+ guard response == .OK else { return }
|
|
|
59
|
+ handleSelection(panel.urls, kind: kind)
|
|
|
60
|
+ }
|
|
|
61
|
+
|
|
|
62
|
+ if let window = window ?? NSApp.keyWindow {
|
|
|
63
|
+ panel.beginSheetModal(for: window, completionHandler: completion)
|
|
|
64
|
+ } else if panel.runModal() == .OK {
|
|
|
65
|
+ completion(.OK)
|
|
|
66
|
+ } else {
|
|
|
67
|
+ activeDelegate = nil
|
|
|
68
|
+ }
|
|
|
69
|
+ }
|
|
|
70
|
+
|
|
|
71
|
+ private static var photoContentTypes: [UTType] {
|
|
|
72
|
+ photoExtensions.compactMap { UTType(filenameExtension: $0) }
|
|
|
73
|
+ }
|
|
|
74
|
+
|
|
|
75
|
+ private static var documentContentTypes: [UTType] {
|
|
|
76
|
+ documentExtensions.compactMap { UTType(filenameExtension: $0) }
|
|
|
77
|
+ }
|
|
|
78
|
+
|
|
|
79
|
+ private static var fileManagerContentTypes: [UTType] {
|
|
|
80
|
+ photoContentTypes + documentContentTypes
|
|
|
81
|
+ }
|
|
|
82
|
+
|
|
|
83
|
+ fileprivate static func isPhoto(_ url: URL) -> Bool {
|
|
|
84
|
+ photoExtensions.contains(url.pathExtension.lowercased())
|
|
|
85
|
+ }
|
|
|
86
|
+
|
|
|
87
|
+ fileprivate static func isDocument(_ url: URL) -> Bool {
|
|
|
88
|
+ let ext = url.pathExtension.lowercased()
|
|
|
89
|
+ guard documentExtensions.contains(ext) else { return false }
|
|
|
90
|
+ if let type = contentType(for: url), type.conforms(to: .image) { return false }
|
|
|
91
|
+ return true
|
|
|
92
|
+ }
|
|
|
93
|
+
|
|
|
94
|
+ fileprivate static func isSelectableInFileManager(_ url: URL) -> Bool {
|
|
|
95
|
+ isPhoto(url) || isDocument(url)
|
|
|
96
|
+ }
|
|
|
97
|
+
|
|
|
98
|
+ fileprivate static func contentType(for url: URL) -> UTType? {
|
|
|
99
|
+ if let values = try? url.resourceValues(forKeys: [.contentTypeKey]),
|
|
|
100
|
+ let type = values.contentType {
|
|
|
101
|
+ return type
|
|
|
102
|
+ }
|
|
|
103
|
+ let ext = url.pathExtension.lowercased()
|
|
|
104
|
+ guard !ext.isEmpty else { return nil }
|
|
|
105
|
+ return UTType(filenameExtension: ext)
|
|
|
106
|
+ }
|
|
|
107
|
+
|
|
|
108
|
+ private static func handleSelection(_ urls: [URL], kind: DocumentPickerKind) {
|
|
|
109
|
+ let matched: [URL]
|
|
|
110
|
+ switch kind {
|
|
|
111
|
+ case .photos:
|
|
|
112
|
+ matched = urls.filter { isPhoto($0) }
|
|
|
113
|
+ case .files:
|
|
|
114
|
+ matched = urls.filter { isSelectableInFileManager($0) }
|
|
|
115
|
+ case .importFile:
|
|
|
116
|
+ matched = urls.filter { isDocument($0) }
|
|
|
117
|
+ }
|
|
|
118
|
+
|
|
|
119
|
+ guard !matched.isEmpty else {
|
|
|
120
|
+ showNoMatchingFilesAlert(for: kind)
|
|
|
121
|
+ return
|
|
|
122
|
+ }
|
|
|
123
|
+
|
|
|
124
|
+ switch kind {
|
|
|
125
|
+ case .photos, .files:
|
|
|
126
|
+ PrintService.print(urls: matched)
|
|
|
127
|
+ case .importFile:
|
|
|
128
|
+ let added = ImportedFilesStore.add(urls: matched)
|
|
|
129
|
+ showImportSuccessAlert(count: added > 0 ? added : matched.count)
|
|
|
130
|
+ }
|
|
|
131
|
+ }
|
|
|
132
|
+
|
|
|
133
|
+ private static func showImportSuccessAlert(count: Int) {
|
|
|
134
|
+ let alert = NSAlert()
|
|
|
135
|
+ alert.messageText = "Files Added"
|
|
|
136
|
+ alert.informativeText = count == 1
|
|
|
137
|
+ ? "1 file was added successfully."
|
|
|
138
|
+ : "\(count) files were added successfully."
|
|
|
139
|
+ alert.alertStyle = .informational
|
|
|
140
|
+ alert.addButton(withTitle: "OK")
|
|
|
141
|
+ alert.runModal()
|
|
|
142
|
+ }
|
|
|
143
|
+
|
|
|
144
|
+ private static func showNoMatchingFilesAlert(for kind: DocumentPickerKind) {
|
|
|
145
|
+ let alert = NSAlert()
|
|
|
146
|
+ switch kind {
|
|
|
147
|
+ case .photos:
|
|
|
148
|
+ alert.messageText = "No Photos Selected"
|
|
|
149
|
+ alert.informativeText = "Please select picture files such as JPEG, PNG, or HEIC."
|
|
|
150
|
+ case .files:
|
|
|
151
|
+ alert.messageText = "No Photos or Files Selected"
|
|
|
152
|
+ alert.informativeText = "Please select pictures or document files such as JPEG, PNG, PDF, or DOC."
|
|
|
153
|
+ case .importFile:
|
|
|
154
|
+ alert.messageText = "No Files to Add"
|
|
|
155
|
+ alert.informativeText = "Please select files such as PDF, TXT, or DOC to import."
|
|
|
156
|
+ }
|
|
|
157
|
+ alert.alertStyle = .warning
|
|
|
158
|
+ alert.addButton(withTitle: "OK")
|
|
|
159
|
+ alert.runModal()
|
|
|
160
|
+ }
|
|
|
161
|
+}
|
|
|
162
|
+
|
|
|
163
|
+private final class DocumentPickerPanelDelegate: NSObject, NSOpenSavePanelDelegate {
|
|
|
164
|
+ private let kind: DocumentPickerKind
|
|
|
165
|
+
|
|
|
166
|
+ init(kind: DocumentPickerKind) {
|
|
|
167
|
+ self.kind = kind
|
|
|
168
|
+ }
|
|
|
169
|
+
|
|
|
170
|
+ func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
|
|
|
171
|
+ guard let panel = sender as? NSOpenPanel else { return false }
|
|
|
172
|
+
|
|
|
173
|
+ var isDirectory: ObjCBool = false
|
|
|
174
|
+ if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory),
|
|
|
175
|
+ isDirectory.boolValue {
|
|
|
176
|
+ return panel.canChooseDirectories
|
|
|
177
|
+ }
|
|
|
178
|
+
|
|
|
179
|
+ switch kind {
|
|
|
180
|
+ case .photos:
|
|
|
181
|
+ return DocumentPickerService.isPhoto(url)
|
|
|
182
|
+ case .files:
|
|
|
183
|
+ return DocumentPickerService.isSelectableInFileManager(url)
|
|
|
184
|
+ case .importFile:
|
|
|
185
|
+ return DocumentPickerService.isDocument(url)
|
|
|
186
|
+ }
|
|
|
187
|
+ }
|
|
|
188
|
+}
|