Browse Source

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 month ago
parent
commit
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)