ソースを参照

Add Quick Start pickers for photos, files, and imports.

Wire the gallery, file manager, and import cards to dedicated pickers with type filtering, printing, and imported file storage.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 ヶ月 前
コミット
7c1fe4003b

+ 188 - 0
smart_printer/DocumentPickerService.swift

@@ -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)
+        }
+    }
+}

+ 60 - 0
smart_printer/ImportedFilesStore.swift

@@ -0,0 +1,60 @@
+import Cocoa
+
+enum ImportedFilesStore {
+    private static let bookmarksKey = "importedFileBookmarks"
+    private static let defaults = UserDefaults.standard
+
+    static var fileURLs: [URL] {
+        guard let bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] else { return [] }
+        return bookmarks.compactMap { resolveBookmark($0) }
+    }
+
+    static func add(urls: [URL]) -> Int {
+        guard AppSettings.saveRecentFiles else { return urls.count }
+
+        var bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] ?? []
+        var added = 0
+
+        for url in urls {
+            let accessed = url.startAccessingSecurityScopedResource()
+            defer {
+                if accessed { url.stopAccessingSecurityScopedResource() }
+            }
+
+            guard let bookmark = try? url.bookmarkData(
+                options: .withSecurityScope,
+                includingResourceValuesForKeys: nil,
+                relativeTo: nil
+            ) else { continue }
+
+            if !bookmarks.contains(bookmark) {
+                bookmarks.append(bookmark)
+                added += 1
+            }
+        }
+
+        defaults.set(bookmarks, forKey: bookmarksKey)
+        return added
+    }
+
+    private static func resolveBookmark(_ data: Data) -> URL? {
+        var isStale = false
+        guard let url = try? URL(
+            resolvingBookmarkData: data,
+            options: .withSecurityScope,
+            relativeTo: nil,
+            bookmarkDataIsStale: &isStale
+        ) else { return nil }
+
+        if isStale {
+            remove(url)
+        }
+        return url
+    }
+
+    private static func remove(_ url: URL) {
+        guard var bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] else { return }
+        bookmarks.removeAll { resolveBookmark($0)?.path == url.path }
+        defaults.set(bookmarks, forKey: bookmarksKey)
+    }
+}

+ 93 - 0
smart_printer/PrintService.swift

@@ -0,0 +1,93 @@
+import Cocoa
+import PDFKit
+import UniformTypeIdentifiers
+
+enum PrintService {
+    static func print(urls: [URL]) {
+        guard !urls.isEmpty else { return }
+
+        for url in urls {
+            let accessed = url.startAccessingSecurityScopedResource()
+            defer {
+                if accessed { url.stopAccessingSecurityScopedResource() }
+            }
+            printFile(at: url)
+        }
+    }
+
+    private static func printFile(at url: URL) {
+        let type = UTType(filenameExtension: url.pathExtension) ?? .data
+
+        if type.conforms(to: .pdf), let document = PDFDocument(url: url) {
+            printPDF(document)
+        } else if type.conforms(to: .image), let image = NSImage(contentsOf: url) {
+            printImage(image)
+        } else if type.conforms(to: .plainText) || type.conforms(to: .text),
+                  let text = try? String(contentsOf: url, encoding: .utf8) {
+            printText(text)
+        } else if type.conforms(to: .rtf),
+                  let data = try? Data(contentsOf: url),
+                  let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
+            printAttributedText(attributed)
+        } else if type.conforms(to: .html),
+                  let data = try? Data(contentsOf: url),
+                  let attributed = NSAttributedString(html: data, documentAttributes: nil) {
+            printAttributedText(attributed)
+        } else {
+            showUnsupportedAlert(for: url)
+        }
+    }
+
+    private static func printPDF(_ document: PDFDocument) {
+        let printInfo = configuredPrintInfo()
+        guard let operation = document.printOperation(
+            for: printInfo,
+            scalingMode: .pageScaleToFit,
+            autoRotate: true
+        ) else { return }
+        operation.run()
+    }
+
+    private static func printImage(_ image: NSImage) {
+        let size = image.size
+        guard size.width > 0, size.height > 0 else { return }
+
+        let imageView = NSImageView(frame: NSRect(origin: .zero, size: size))
+        imageView.image = image
+        imageView.imageScaling = .scaleProportionallyUpOrDown
+
+        let operation = NSPrintOperation(view: imageView, printInfo: configuredPrintInfo())
+        operation.run()
+    }
+
+    private static func printText(_ text: String) {
+        printAttributedText(NSAttributedString(string: text))
+    }
+
+    private static func printAttributedText(_ text: NSAttributedString) {
+        let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 612, height: 792))
+        textView.textStorage?.setAttributedString(text)
+        textView.isEditable = false
+
+        let operation = NSPrintOperation(view: textView, printInfo: configuredPrintInfo())
+        operation.run()
+    }
+
+    private static func configuredPrintInfo() -> NSPrintInfo {
+        let printInfo = NSPrintInfo.shared.copy() as! NSPrintInfo
+        let printerName = AppSettings.effectiveDefaultPrinter
+        if let printer = NSPrinter(name: printerName) {
+            printInfo.printer = printer
+        }
+        return printInfo
+    }
+
+    private static func showUnsupportedAlert(for url: URL) {
+        let alert = NSAlert()
+        alert.messageText = "Unsupported File"
+        alert.informativeText = "\"\(url.lastPathComponent)\" cannot be printed."
+        alert.alertStyle = .warning
+        alert.addButton(withTitle: "OK")
+        alert.runModal()
+    }
+}

+ 27 - 1
smart_printer/UIComponents.swift

@@ -220,8 +220,15 @@ final class PillButton: NSButton {
     private let horizontalPadding: CGFloat = 16
     private let baseColor: NSColor
     private var hoverTracker: HoverTracker?
+    private let clickTarget: ButtonClickTarget
+
+    var onClick: (() -> Void)? {
+        get { clickTarget.handler }
+        set { clickTarget.handler = newValue }
+    }
 
     init(title: String, color: NSColor) {
+        clickTarget = ButtonClickTarget()
         baseColor = color
         super.init(frame: .zero)
         self.title = title
@@ -231,6 +238,8 @@ final class PillButton: NSButton {
         layer?.cornerRadius = 11
         font = AppTheme.semiboldFont(size: 13)
         contentTintColor = .white
+        target = clickTarget
+        action = #selector(ButtonClickTarget.activate)
 
         if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) {
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
@@ -352,8 +361,10 @@ final class QuickStartCardView: NSView, AppearanceRefreshable {
     private var iconHeightConstraint: NSLayoutConstraint!
     private var hoverTracker: HoverTracker?
     private var isHovered = false
+    private let onActivate: () -> Void
 
-    init(data: QuickStartCardData) {
+    init(data: QuickStartCardData, onActivate: @escaping () -> Void) {
+        self.onActivate = onActivate
         iconView = QuickStartIconView(kind: data.iconKind)
         gradientView = GradientCardView(
             colors: data.gradientColors,
@@ -378,6 +389,7 @@ final class QuickStartCardView: NSView, AppearanceRefreshable {
 
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
         button.translatesAutoresizingMaskIntoConstraints = false
+        button.onClick = onActivate
 
         addSubview(gradient)
         gradient.addSubview(titleLabel)
@@ -444,11 +456,25 @@ final class QuickStartCardView: NSView, AppearanceRefreshable {
         }
     }
 
+    override func mouseUp(with event: NSEvent) {
+        let location = convert(event.locationInWindow, from: nil)
+        guard bounds.contains(location) else { return }
+        onActivate()
+    }
+
     override func resetCursorRects() {
         addCursorRect(bounds, cursor: .pointingHand)
     }
 }
 
+private final class ButtonClickTarget: NSObject {
+    var handler: (() -> Void)?
+
+    @objc func activate() {
+        handler?()
+    }
+}
+
 // MARK: - Feature Card
 
 struct FeatureCardData {

+ 10 - 5
smart_printer/ViewController.swift

@@ -460,7 +460,7 @@ class ViewController: NSViewController {
         let cards: [QuickStartCardData] = [
             QuickStartCardData(
                 title: "From Photos",
-                subtitle: "Take a photo from gallery",
+                subtitle: "Select photos from gallery",
                 buttonTitle: "Open Gallery",
                 accentColor: AppTheme.blue,
                 gradientColors: [AppTheme.quickStartBlueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
@@ -468,7 +468,7 @@ class ViewController: NSViewController {
             ),
             QuickStartCardData(
                 title: "From Files",
-                subtitle: "Take a photo from file manager",
+                subtitle: "Select Photos from file manager",
                 buttonTitle: "Browse Files",
                 accentColor: AppTheme.green,
                 gradientColors: [AppTheme.greenLight, NSColor(red: 0.78, green: 0.94, blue: 0.84, alpha: 1)],
@@ -476,7 +476,7 @@ class ViewController: NSViewController {
             ),
             QuickStartCardData(
                 title: "Import File",
-                subtitle: "Import a file from storage",
+                subtitle: "Add files from storage",
                 buttonTitle: "Import Now",
                 accentColor: AppTheme.orange,
                 gradientColors: [AppTheme.orangeLight, NSColor(red: 1.0, green: 0.88, blue: 0.76, alpha: 1)],
@@ -484,8 +484,13 @@ class ViewController: NSViewController {
             ),
         ]
 
-        for cardData in cards {
-            cardsStack.addArrangedSubview(QuickStartCardView(data: cardData))
+        let pickerKinds: [DocumentPickerKind] = [.photos, .files, .importFile]
+        for (index, cardData) in cards.enumerated() {
+            let kind = pickerKinds[index]
+            let card = QuickStartCardView(data: cardData) { [weak self] in
+                DocumentPickerService.present(kind, from: self?.view.window)
+            }
+            cardsStack.addArrangedSubview(card)
         }
 
         section.addSubview(cardsStack)