|
|
@@ -0,0 +1,188 @@
|
|
|
+import Cocoa
|
|
|
+import UniformTypeIdentifiers
|
|
|
+
|
|
|
+enum DocumentPickerKind {
|
|
|
+ case photos
|
|
|
+ case files
|
|
|
+ case importFile
|
|
|
+}
|
|
|
+
|
|
|
+enum DocumentPickerService {
|
|
|
+ private static var activeDelegate: DocumentPickerPanelDelegate?
|
|
|
+
|
|
|
+ private static let photoExtensions: Set<String> = [
|
|
|
+ "jpg", "jpeg", "png", "heic", "heif", "gif", "tiff", "tif", "bmp", "webp", "raw", "svg",
|
|
|
+ ]
|
|
|
+
|
|
|
+ private static let documentExtensions: Set<String> = [
|
|
|
+ "pdf", "txt", "rtf", "csv", "xml", "html", "htm", "json", "md",
|
|
|
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pages", "numbers", "key",
|
|
|
+ ]
|
|
|
+
|
|
|
+ static func present(_ kind: DocumentPickerKind, from window: NSWindow?) {
|
|
|
+ let panel = NSOpenPanel()
|
|
|
+ panel.canChooseDirectories = false
|
|
|
+ panel.canChooseFiles = true
|
|
|
+ panel.allowsOtherFileTypes = false
|
|
|
+
|
|
|
+ switch kind {
|
|
|
+ case .photos:
|
|
|
+ panel.title = "Open Gallery"
|
|
|
+ panel.message = "Select photos from your gallery."
|
|
|
+ panel.prompt = "Select Photos"
|
|
|
+ panel.allowsMultipleSelection = true
|
|
|
+ panel.allowedContentTypes = photoContentTypes
|
|
|
+ panel.directoryURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first
|
|
|
+ case .files:
|
|
|
+ panel.title = "Open File Manager"
|
|
|
+ panel.message = "Select photos or files from your file manager."
|
|
|
+ panel.prompt = "Select"
|
|
|
+ panel.allowsMultipleSelection = true
|
|
|
+ panel.allowedContentTypes = fileManagerContentTypes
|
|
|
+ panel.directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
|
+ case .importFile:
|
|
|
+ panel.title = "Import Files"
|
|
|
+ panel.message = "Add files from storage."
|
|
|
+ panel.prompt = "Add Files"
|
|
|
+ panel.allowsMultipleSelection = true
|
|
|
+ panel.allowedContentTypes = documentContentTypes
|
|
|
+ panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
|
|
+ }
|
|
|
+
|
|
|
+ let delegate = DocumentPickerPanelDelegate(kind: kind)
|
|
|
+ activeDelegate = delegate
|
|
|
+ panel.delegate = delegate
|
|
|
+
|
|
|
+ let completion: (NSApplication.ModalResponse) -> Void = { response in
|
|
|
+ defer { activeDelegate = nil }
|
|
|
+ guard response == .OK else { return }
|
|
|
+ handleSelection(panel.urls, kind: kind)
|
|
|
+ }
|
|
|
+
|
|
|
+ if let window = window ?? NSApp.keyWindow {
|
|
|
+ panel.beginSheetModal(for: window, completionHandler: completion)
|
|
|
+ } else if panel.runModal() == .OK {
|
|
|
+ completion(.OK)
|
|
|
+ } else {
|
|
|
+ activeDelegate = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static var photoContentTypes: [UTType] {
|
|
|
+ photoExtensions.compactMap { UTType(filenameExtension: $0) }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static var documentContentTypes: [UTType] {
|
|
|
+ documentExtensions.compactMap { UTType(filenameExtension: $0) }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static var fileManagerContentTypes: [UTType] {
|
|
|
+ photoContentTypes + documentContentTypes
|
|
|
+ }
|
|
|
+
|
|
|
+ fileprivate static func isPhoto(_ url: URL) -> Bool {
|
|
|
+ photoExtensions.contains(url.pathExtension.lowercased())
|
|
|
+ }
|
|
|
+
|
|
|
+ fileprivate static func isDocument(_ url: URL) -> Bool {
|
|
|
+ let ext = url.pathExtension.lowercased()
|
|
|
+ guard documentExtensions.contains(ext) else { return false }
|
|
|
+ if let type = contentType(for: url), type.conforms(to: .image) { return false }
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ fileprivate static func isSelectableInFileManager(_ url: URL) -> Bool {
|
|
|
+ isPhoto(url) || isDocument(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ fileprivate static func contentType(for url: URL) -> UTType? {
|
|
|
+ if let values = try? url.resourceValues(forKeys: [.contentTypeKey]),
|
|
|
+ let type = values.contentType {
|
|
|
+ return type
|
|
|
+ }
|
|
|
+ let ext = url.pathExtension.lowercased()
|
|
|
+ guard !ext.isEmpty else { return nil }
|
|
|
+ return UTType(filenameExtension: ext)
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func handleSelection(_ urls: [URL], kind: DocumentPickerKind) {
|
|
|
+ let matched: [URL]
|
|
|
+ switch kind {
|
|
|
+ case .photos:
|
|
|
+ matched = urls.filter { isPhoto($0) }
|
|
|
+ case .files:
|
|
|
+ matched = urls.filter { isSelectableInFileManager($0) }
|
|
|
+ case .importFile:
|
|
|
+ matched = urls.filter { isDocument($0) }
|
|
|
+ }
|
|
|
+
|
|
|
+ guard !matched.isEmpty else {
|
|
|
+ showNoMatchingFilesAlert(for: kind)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ switch kind {
|
|
|
+ case .photos, .files:
|
|
|
+ PrintService.print(urls: matched)
|
|
|
+ case .importFile:
|
|
|
+ let added = ImportedFilesStore.add(urls: matched)
|
|
|
+ showImportSuccessAlert(count: added > 0 ? added : matched.count)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func showImportSuccessAlert(count: Int) {
|
|
|
+ let alert = NSAlert()
|
|
|
+ alert.messageText = "Files Added"
|
|
|
+ alert.informativeText = count == 1
|
|
|
+ ? "1 file was added successfully."
|
|
|
+ : "\(count) files were added successfully."
|
|
|
+ alert.alertStyle = .informational
|
|
|
+ alert.addButton(withTitle: "OK")
|
|
|
+ alert.runModal()
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func showNoMatchingFilesAlert(for kind: DocumentPickerKind) {
|
|
|
+ let alert = NSAlert()
|
|
|
+ switch kind {
|
|
|
+ case .photos:
|
|
|
+ alert.messageText = "No Photos Selected"
|
|
|
+ alert.informativeText = "Please select picture files such as JPEG, PNG, or HEIC."
|
|
|
+ case .files:
|
|
|
+ alert.messageText = "No Photos or Files Selected"
|
|
|
+ alert.informativeText = "Please select pictures or document files such as JPEG, PNG, PDF, or DOC."
|
|
|
+ case .importFile:
|
|
|
+ alert.messageText = "No Files to Add"
|
|
|
+ alert.informativeText = "Please select files such as PDF, TXT, or DOC to import."
|
|
|
+ }
|
|
|
+ alert.alertStyle = .warning
|
|
|
+ alert.addButton(withTitle: "OK")
|
|
|
+ alert.runModal()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private final class DocumentPickerPanelDelegate: NSObject, NSOpenSavePanelDelegate {
|
|
|
+ private let kind: DocumentPickerKind
|
|
|
+
|
|
|
+ init(kind: DocumentPickerKind) {
|
|
|
+ self.kind = kind
|
|
|
+ }
|
|
|
+
|
|
|
+ func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
|
|
|
+ guard let panel = sender as? NSOpenPanel else { return false }
|
|
|
+
|
|
|
+ var isDirectory: ObjCBool = false
|
|
|
+ if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory),
|
|
|
+ isDirectory.boolValue {
|
|
|
+ return panel.canChooseDirectories
|
|
|
+ }
|
|
|
+
|
|
|
+ switch kind {
|
|
|
+ case .photos:
|
|
|
+ return DocumentPickerService.isPhoto(url)
|
|
|
+ case .files:
|
|
|
+ return DocumentPickerService.isSelectableInFileManager(url)
|
|
|
+ case .importFile:
|
|
|
+ return DocumentPickerService.isDocument(url)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|