Преглед изворни кода

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 пре 2 часа
родитељ
комит
7c1fe4003b

+ 188 - 0
smart_printer/DocumentPickerService.swift

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

+ 60 - 0
smart_printer/ImportedFilesStore.swift

@@ -0,0 +1,60 @@
1
+import Cocoa
2
+
3
+enum ImportedFilesStore {
4
+    private static let bookmarksKey = "importedFileBookmarks"
5
+    private static let defaults = UserDefaults.standard
6
+
7
+    static var fileURLs: [URL] {
8
+        guard let bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] else { return [] }
9
+        return bookmarks.compactMap { resolveBookmark($0) }
10
+    }
11
+
12
+    static func add(urls: [URL]) -> Int {
13
+        guard AppSettings.saveRecentFiles else { return urls.count }
14
+
15
+        var bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] ?? []
16
+        var added = 0
17
+
18
+        for url in urls {
19
+            let accessed = url.startAccessingSecurityScopedResource()
20
+            defer {
21
+                if accessed { url.stopAccessingSecurityScopedResource() }
22
+            }
23
+
24
+            guard let bookmark = try? url.bookmarkData(
25
+                options: .withSecurityScope,
26
+                includingResourceValuesForKeys: nil,
27
+                relativeTo: nil
28
+            ) else { continue }
29
+
30
+            if !bookmarks.contains(bookmark) {
31
+                bookmarks.append(bookmark)
32
+                added += 1
33
+            }
34
+        }
35
+
36
+        defaults.set(bookmarks, forKey: bookmarksKey)
37
+        return added
38
+    }
39
+
40
+    private static func resolveBookmark(_ data: Data) -> URL? {
41
+        var isStale = false
42
+        guard let url = try? URL(
43
+            resolvingBookmarkData: data,
44
+            options: .withSecurityScope,
45
+            relativeTo: nil,
46
+            bookmarkDataIsStale: &isStale
47
+        ) else { return nil }
48
+
49
+        if isStale {
50
+            remove(url)
51
+        }
52
+        return url
53
+    }
54
+
55
+    private static func remove(_ url: URL) {
56
+        guard var bookmarks = defaults.array(forKey: bookmarksKey) as? [Data] else { return }
57
+        bookmarks.removeAll { resolveBookmark($0)?.path == url.path }
58
+        defaults.set(bookmarks, forKey: bookmarksKey)
59
+    }
60
+}

+ 93 - 0
smart_printer/PrintService.swift

@@ -0,0 +1,93 @@
1
+import Cocoa
2
+import PDFKit
3
+import UniformTypeIdentifiers
4
+
5
+enum PrintService {
6
+    static func print(urls: [URL]) {
7
+        guard !urls.isEmpty else { return }
8
+
9
+        for url in urls {
10
+            let accessed = url.startAccessingSecurityScopedResource()
11
+            defer {
12
+                if accessed { url.stopAccessingSecurityScopedResource() }
13
+            }
14
+            printFile(at: url)
15
+        }
16
+    }
17
+
18
+    private static func printFile(at url: URL) {
19
+        let type = UTType(filenameExtension: url.pathExtension) ?? .data
20
+
21
+        if type.conforms(to: .pdf), let document = PDFDocument(url: url) {
22
+            printPDF(document)
23
+        } else if type.conforms(to: .image), let image = NSImage(contentsOf: url) {
24
+            printImage(image)
25
+        } else if type.conforms(to: .plainText) || type.conforms(to: .text),
26
+                  let text = try? String(contentsOf: url, encoding: .utf8) {
27
+            printText(text)
28
+        } else if type.conforms(to: .rtf),
29
+                  let data = try? Data(contentsOf: url),
30
+                  let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
31
+            printAttributedText(attributed)
32
+        } else if type.conforms(to: .html),
33
+                  let data = try? Data(contentsOf: url),
34
+                  let attributed = NSAttributedString(html: data, documentAttributes: nil) {
35
+            printAttributedText(attributed)
36
+        } else {
37
+            showUnsupportedAlert(for: url)
38
+        }
39
+    }
40
+
41
+    private static func printPDF(_ document: PDFDocument) {
42
+        let printInfo = configuredPrintInfo()
43
+        guard let operation = document.printOperation(
44
+            for: printInfo,
45
+            scalingMode: .pageScaleToFit,
46
+            autoRotate: true
47
+        ) else { return }
48
+        operation.run()
49
+    }
50
+
51
+    private static func printImage(_ image: NSImage) {
52
+        let size = image.size
53
+        guard size.width > 0, size.height > 0 else { return }
54
+
55
+        let imageView = NSImageView(frame: NSRect(origin: .zero, size: size))
56
+        imageView.image = image
57
+        imageView.imageScaling = .scaleProportionallyUpOrDown
58
+
59
+        let operation = NSPrintOperation(view: imageView, printInfo: configuredPrintInfo())
60
+        operation.run()
61
+    }
62
+
63
+    private static func printText(_ text: String) {
64
+        printAttributedText(NSAttributedString(string: text))
65
+    }
66
+
67
+    private static func printAttributedText(_ text: NSAttributedString) {
68
+        let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 612, height: 792))
69
+        textView.textStorage?.setAttributedString(text)
70
+        textView.isEditable = false
71
+
72
+        let operation = NSPrintOperation(view: textView, printInfo: configuredPrintInfo())
73
+        operation.run()
74
+    }
75
+
76
+    private static func configuredPrintInfo() -> NSPrintInfo {
77
+        let printInfo = NSPrintInfo.shared.copy() as! NSPrintInfo
78
+        let printerName = AppSettings.effectiveDefaultPrinter
79
+        if let printer = NSPrinter(name: printerName) {
80
+            printInfo.printer = printer
81
+        }
82
+        return printInfo
83
+    }
84
+
85
+    private static func showUnsupportedAlert(for url: URL) {
86
+        let alert = NSAlert()
87
+        alert.messageText = "Unsupported File"
88
+        alert.informativeText = "\"\(url.lastPathComponent)\" cannot be printed."
89
+        alert.alertStyle = .warning
90
+        alert.addButton(withTitle: "OK")
91
+        alert.runModal()
92
+    }
93
+}

+ 27 - 1
smart_printer/UIComponents.swift

@@ -220,8 +220,15 @@ final class PillButton: NSButton {
220
     private let horizontalPadding: CGFloat = 16
220
     private let horizontalPadding: CGFloat = 16
221
     private let baseColor: NSColor
221
     private let baseColor: NSColor
222
     private var hoverTracker: HoverTracker?
222
     private var hoverTracker: HoverTracker?
223
+    private let clickTarget: ButtonClickTarget
224
+
225
+    var onClick: (() -> Void)? {
226
+        get { clickTarget.handler }
227
+        set { clickTarget.handler = newValue }
228
+    }
223
 
229
 
224
     init(title: String, color: NSColor) {
230
     init(title: String, color: NSColor) {
231
+        clickTarget = ButtonClickTarget()
225
         baseColor = color
232
         baseColor = color
226
         super.init(frame: .zero)
233
         super.init(frame: .zero)
227
         self.title = title
234
         self.title = title
@@ -231,6 +238,8 @@ final class PillButton: NSButton {
231
         layer?.cornerRadius = 11
238
         layer?.cornerRadius = 11
232
         font = AppTheme.semiboldFont(size: 13)
239
         font = AppTheme.semiboldFont(size: 13)
233
         contentTintColor = .white
240
         contentTintColor = .white
241
+        target = clickTarget
242
+        action = #selector(ButtonClickTarget.activate)
234
 
243
 
235
         if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) {
244
         if let arrow = NSImage(systemSymbolName: "arrow.right", accessibilityDescription: nil) {
236
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
245
             let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
@@ -352,8 +361,10 @@ final class QuickStartCardView: NSView, AppearanceRefreshable {
352
     private var iconHeightConstraint: NSLayoutConstraint!
361
     private var iconHeightConstraint: NSLayoutConstraint!
353
     private var hoverTracker: HoverTracker?
362
     private var hoverTracker: HoverTracker?
354
     private var isHovered = false
363
     private var isHovered = false
364
+    private let onActivate: () -> Void
355
 
365
 
356
-    init(data: QuickStartCardData) {
366
+    init(data: QuickStartCardData, onActivate: @escaping () -> Void) {
367
+        self.onActivate = onActivate
357
         iconView = QuickStartIconView(kind: data.iconKind)
368
         iconView = QuickStartIconView(kind: data.iconKind)
358
         gradientView = GradientCardView(
369
         gradientView = GradientCardView(
359
             colors: data.gradientColors,
370
             colors: data.gradientColors,
@@ -378,6 +389,7 @@ final class QuickStartCardView: NSView, AppearanceRefreshable {
378
 
389
 
379
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
390
         let button = PillButton(title: data.buttonTitle, color: data.accentColor)
380
         button.translatesAutoresizingMaskIntoConstraints = false
391
         button.translatesAutoresizingMaskIntoConstraints = false
392
+        button.onClick = onActivate
381
 
393
 
382
         addSubview(gradient)
394
         addSubview(gradient)
383
         gradient.addSubview(titleLabel)
395
         gradient.addSubview(titleLabel)
@@ -444,11 +456,25 @@ final class QuickStartCardView: NSView, AppearanceRefreshable {
444
         }
456
         }
445
     }
457
     }
446
 
458
 
459
+    override func mouseUp(with event: NSEvent) {
460
+        let location = convert(event.locationInWindow, from: nil)
461
+        guard bounds.contains(location) else { return }
462
+        onActivate()
463
+    }
464
+
447
     override func resetCursorRects() {
465
     override func resetCursorRects() {
448
         addCursorRect(bounds, cursor: .pointingHand)
466
         addCursorRect(bounds, cursor: .pointingHand)
449
     }
467
     }
450
 }
468
 }
451
 
469
 
470
+private final class ButtonClickTarget: NSObject {
471
+    var handler: (() -> Void)?
472
+
473
+    @objc func activate() {
474
+        handler?()
475
+    }
476
+}
477
+
452
 // MARK: - Feature Card
478
 // MARK: - Feature Card
453
 
479
 
454
 struct FeatureCardData {
480
 struct FeatureCardData {

+ 10 - 5
smart_printer/ViewController.swift

@@ -460,7 +460,7 @@ class ViewController: NSViewController {
460
         let cards: [QuickStartCardData] = [
460
         let cards: [QuickStartCardData] = [
461
             QuickStartCardData(
461
             QuickStartCardData(
462
                 title: "From Photos",
462
                 title: "From Photos",
463
-                subtitle: "Take a photo from gallery",
463
+                subtitle: "Select photos from gallery",
464
                 buttonTitle: "Open Gallery",
464
                 buttonTitle: "Open Gallery",
465
                 accentColor: AppTheme.blue,
465
                 accentColor: AppTheme.blue,
466
                 gradientColors: [AppTheme.quickStartBlueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
466
                 gradientColors: [AppTheme.quickStartBlueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
@@ -468,7 +468,7 @@ class ViewController: NSViewController {
468
             ),
468
             ),
469
             QuickStartCardData(
469
             QuickStartCardData(
470
                 title: "From Files",
470
                 title: "From Files",
471
-                subtitle: "Take a photo from file manager",
471
+                subtitle: "Select Photos from file manager",
472
                 buttonTitle: "Browse Files",
472
                 buttonTitle: "Browse Files",
473
                 accentColor: AppTheme.green,
473
                 accentColor: AppTheme.green,
474
                 gradientColors: [AppTheme.greenLight, NSColor(red: 0.78, green: 0.94, blue: 0.84, alpha: 1)],
474
                 gradientColors: [AppTheme.greenLight, NSColor(red: 0.78, green: 0.94, blue: 0.84, alpha: 1)],
@@ -476,7 +476,7 @@ class ViewController: NSViewController {
476
             ),
476
             ),
477
             QuickStartCardData(
477
             QuickStartCardData(
478
                 title: "Import File",
478
                 title: "Import File",
479
-                subtitle: "Import a file from storage",
479
+                subtitle: "Add files from storage",
480
                 buttonTitle: "Import Now",
480
                 buttonTitle: "Import Now",
481
                 accentColor: AppTheme.orange,
481
                 accentColor: AppTheme.orange,
482
                 gradientColors: [AppTheme.orangeLight, NSColor(red: 1.0, green: 0.88, blue: 0.76, alpha: 1)],
482
                 gradientColors: [AppTheme.orangeLight, NSColor(red: 1.0, green: 0.88, blue: 0.76, alpha: 1)],
@@ -484,8 +484,13 @@ class ViewController: NSViewController {
484
             ),
484
             ),
485
         ]
485
         ]
486
 
486
 
487
-        for cardData in cards {
488
-            cardsStack.addArrangedSubview(QuickStartCardView(data: cardData))
487
+        let pickerKinds: [DocumentPickerKind] = [.photos, .files, .importFile]
488
+        for (index, cardData) in cards.enumerated() {
489
+            let kind = pickerKinds[index]
490
+            let card = QuickStartCardView(data: cardData) { [weak self] in
491
+                DocumentPickerService.present(kind, from: self?.view.window)
492
+            }
493
+            cardsStack.addArrangedSubview(card)
489
         }
494
         }
490
 
495
 
491
         section.addSubview(cardsStack)
496
         section.addSubview(cardsStack)