浏览代码

Add file import preview with print and remove photo edit button.

Imported files now open a preview screen like photos so users can review and print them directly, and the photo preview no longer exposes an external edit action.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 月之前
父节点
当前提交
d4d3b6a977

+ 2 - 13
smart_printer/DocumentPickerService.swift

@@ -134,22 +134,11 @@ enum DocumentPickerService {
                 PrintService.print(urls: documents, from: window)
             }
         case .importFile:
-            let added = ImportedFilesStore.add(urls: matched)
-            showImportSuccessAlert(count: added > 0 ? added : matched.count)
+            _ = ImportedFilesStore.add(urls: matched)
+            FilePreviewService.present(urls: matched, from: window)
         }
     }
 
-    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 {

+ 420 - 0
smart_printer/FilePreviewView.swift

@@ -0,0 +1,420 @@
+import Cocoa
+import PDFKit
+import UniformTypeIdentifiers
+
+// MARK: - File Preview Service
+
+enum FilePreviewService {
+    static func present(urls: [URL], from window: NSWindow?) {
+        guard !urls.isEmpty else { return }
+        let hostWindow = window ?? NSApp.keyWindow
+        guard let viewController = hostWindow?.contentViewController as? ViewController else { return }
+        viewController.presentFilePreview(urls: urls)
+    }
+}
+
+// MARK: - Overlay
+
+final class FilePreviewOverlayView: NSView, AppearanceRefreshable {
+    var onDismiss: (() -> Void)?
+
+    private let urls: [URL]
+    private var currentIndex = 0
+    private var securityAccess: [URL: Bool] = [:]
+
+    private let backButton = FileToolbarButton(symbolName: "chevron.left", accessibilityLabel: "Back")
+    private let titleLabel = NSTextField(labelWithString: "Print a file")
+    private let counterLabel = NSTextField(labelWithString: "")
+    private let previewContainer = NSView()
+    private var previewSubview: NSView?
+    private let printButton = FilePrintButton()
+
+    init(urls: [URL]) {
+        self.urls = urls
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+        setup()
+        refreshAppearance()
+        showFile(at: 0)
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(appearanceDidChange),
+            name: .appearanceDidChange,
+            object: nil
+        )
+    }
+
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
+    @objc private func appearanceDidChange() {
+        refreshAppearance()
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.background.cgColor
+        titleLabel.textColor = AppTheme.textPrimary
+        counterLabel.textColor = AppTheme.textSecondary
+        previewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
+        previewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
+        backButton.refreshAppearance()
+        printButton.refreshAppearance()
+    }
+
+    func present(in parent: NSView) {
+        guard superview == nil else { return }
+        parent.addSubview(self)
+        NSLayoutConstraint.activate([
+            leadingAnchor.constraint(equalTo: parent.leadingAnchor),
+            trailingAnchor.constraint(equalTo: parent.trailingAnchor),
+            topAnchor.constraint(equalTo: parent.topAnchor),
+            bottomAnchor.constraint(equalTo: parent.bottomAnchor),
+        ])
+        alphaValue = 0
+        NSAnimationContext.runAnimationGroup { context in
+            context.duration = 0.2
+            animator().alphaValue = 1
+        }
+    }
+
+    func dismiss(animated: Bool = true) {
+        stopSecurityAccess()
+        let remove = { [weak self] in
+            self?.removeFromSuperview()
+            self?.onDismiss?()
+        }
+        guard animated else {
+            remove()
+            return
+        }
+        NSAnimationContext.runAnimationGroup({ context in
+            context.duration = 0.15
+            animator().alphaValue = 0
+        }, completionHandler: remove)
+    }
+
+    private func setup() {
+        wantsLayer = true
+
+        titleLabel.font = AppTheme.semiboldFont(size: 18)
+        titleLabel.alignment = .center
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        counterLabel.font = AppTheme.regularFont(size: 13)
+        counterLabel.alignment = .center
+        counterLabel.translatesAutoresizingMaskIntoConstraints = false
+        counterLabel.isHidden = urls.count <= 1
+
+        previewContainer.wantsLayer = true
+        previewContainer.layer?.cornerRadius = AppTheme.contentPanelCornerRadius
+        previewContainer.layer?.borderWidth = 1.5
+        previewContainer.applyCardShadow()
+        previewContainer.translatesAutoresizingMaskIntoConstraints = false
+        previewContainer.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        previewContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
+        previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
+
+        backButton.translatesAutoresizingMaskIntoConstraints = false
+        printButton.translatesAutoresizingMaskIntoConstraints = false
+
+        backButton.onClick = { [weak self] in self?.dismiss() }
+        printButton.onClick = { [weak self] in self?.printCurrentFile() }
+
+        addSubview(backButton)
+        addSubview(titleLabel)
+        addSubview(counterLabel)
+        addSubview(previewContainer)
+        addSubview(printButton)
+
+        NSLayoutConstraint.activate([
+            backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
+            backButton.topAnchor.constraint(equalTo: topAnchor, constant: 20),
+            backButton.widthAnchor.constraint(equalToConstant: 40),
+            backButton.heightAnchor.constraint(equalToConstant: 40),
+
+            titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
+            titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
+
+            counterLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
+            counterLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
+
+            previewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AppTheme.contentPanelInset),
+            previewContainer.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AppTheme.contentPanelInset),
+            previewContainer.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 28),
+            previewContainer.bottomAnchor.constraint(equalTo: printButton.topAnchor, constant: -28),
+
+            printButton.centerXAnchor.constraint(equalTo: centerXAnchor),
+            printButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -28),
+            printButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 220),
+        ])
+    }
+
+    private func showFile(at index: Int) {
+        guard urls.indices.contains(index) else { return }
+        currentIndex = index
+        let url = urls[index]
+
+        if securityAccess[url] != true {
+            securityAccess[url] = url.startAccessingSecurityScopedResource()
+        }
+
+        previewSubview?.removeFromSuperview()
+        let preview = makePreview(for: url)
+        preview.translatesAutoresizingMaskIntoConstraints = false
+        previewContainer.addSubview(preview)
+        NSLayoutConstraint.activate([
+            preview.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 12),
+            preview.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -12),
+            preview.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 12),
+            preview.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -12),
+        ])
+        previewSubview = preview
+
+        if urls.count > 1 {
+            counterLabel.stringValue = "\(index + 1) of \(urls.count) — \(url.lastPathComponent)"
+            counterLabel.isHidden = false
+        } else {
+            counterLabel.stringValue = url.lastPathComponent
+            counterLabel.isHidden = false
+        }
+    }
+
+    private func makePreview(for url: URL) -> NSView {
+        let ext = url.pathExtension.lowercased()
+
+        if ext == "pdf", let document = PDFDocument(url: url) {
+            let pdfView = PDFView()
+            pdfView.document = document
+            pdfView.autoScales = true
+            pdfView.displayMode = .singlePageContinuous
+            pdfView.backgroundColor = .clear
+            return pdfView
+        }
+
+        if ext == "rtf",
+           let data = try? Data(contentsOf: url),
+           let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
+            return makeTextPreview(attributed)
+        }
+
+        if ["txt", "md", "csv", "json", "xml", "html", "htm"].contains(ext),
+           let text = try? String(contentsOf: url, encoding: .utf8) {
+            return makeTextPreview(NSAttributedString(string: text))
+        }
+
+        return makePlaceholderPreview(for: url)
+    }
+
+    private func makeTextPreview(_ text: NSAttributedString) -> NSView {
+        let scrollView = NSScrollView()
+        scrollView.hasVerticalScroller = true
+        scrollView.hasHorizontalScroller = false
+        scrollView.autohidesScrollers = true
+        scrollView.borderType = .noBorder
+        scrollView.drawsBackground = false
+
+        let textView = NSTextView()
+        textView.isEditable = false
+        textView.isSelectable = true
+        textView.drawsBackground = false
+        textView.textStorage?.setAttributedString(text)
+        if textView.font == nil {
+            textView.font = AppTheme.regularFont(size: 13)
+        }
+        textView.textColor = AppTheme.textPrimary
+
+        scrollView.documentView = textView
+        return scrollView
+    }
+
+    private func makePlaceholderPreview(for url: URL) -> NSView {
+        let container = NSView()
+
+        let iconView = NSImageView()
+        iconView.image = NSWorkspace.shared.icon(forFile: url.path)
+        iconView.imageScaling = .scaleProportionallyUpOrDown
+        iconView.translatesAutoresizingMaskIntoConstraints = false
+
+        let nameLabel = NSTextField(labelWithString: url.lastPathComponent)
+        nameLabel.font = AppTheme.semiboldFont(size: 16)
+        nameLabel.textColor = AppTheme.textPrimary
+        nameLabel.alignment = .center
+        nameLabel.lineBreakMode = .byTruncatingMiddle
+        nameLabel.maximumNumberOfLines = 2
+        nameLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        container.addSubview(iconView)
+        container.addSubview(nameLabel)
+
+        NSLayoutConstraint.activate([
+            iconView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+            iconView.centerYAnchor.constraint(equalTo: container.centerYAnchor, constant: -20),
+            iconView.widthAnchor.constraint(equalToConstant: 72),
+            iconView.heightAnchor.constraint(equalToConstant: 72),
+
+            nameLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 16),
+            nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
+            nameLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
+        ])
+
+        return container
+    }
+
+    private func currentURL() -> URL? {
+        guard urls.indices.contains(currentIndex) else { return nil }
+        return urls[currentIndex]
+    }
+
+    private func printCurrentFile() {
+        guard let url = currentURL() else { return }
+        PrintService.print(urls: [url], from: window)
+
+        if currentIndex < urls.count - 1 {
+            showFile(at: currentIndex + 1)
+        } else {
+            dismiss()
+        }
+    }
+
+    private func stopSecurityAccess() {
+        for (url, accessed) in securityAccess where accessed {
+            url.stopAccessingSecurityScopedResource()
+        }
+        securityAccess.removeAll()
+    }
+}
+
+// MARK: - Toolbar Button
+
+private final class FileToolbarButton: NSControl, AppearanceRefreshable {
+    var onClick: (() -> Void)?
+
+    private let iconView = NSImageView()
+
+    init(symbolName: String, accessibilityLabel: String) {
+        super.init(frame: .zero)
+        toolTip = accessibilityLabel
+
+        wantsLayer = true
+        layer?.cornerRadius = 12
+
+        if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityLabel) {
+            let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
+            iconView.image = image.withSymbolConfiguration(config)
+        }
+        iconView.translatesAutoresizingMaskIntoConstraints = false
+
+        addSubview(iconView)
+        NSLayoutConstraint.activate([
+            iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
+            iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
+        ])
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
+        layer?.borderColor = AppTheme.paywallBorder.cgColor
+        layer?.borderWidth = 1.5
+        iconView.contentTintColor = AppTheme.textPrimary
+    }
+
+    override func mouseUp(with event: NSEvent) {
+        guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
+        onClick?()
+    }
+
+    override func resetCursorRects() {
+        addCursorRect(bounds, cursor: .pointingHand)
+    }
+}
+
+// MARK: - Print Button
+
+private final class FilePrintButton: NSControl, AppearanceRefreshable {
+    var onClick: (() -> Void)?
+
+    private let titleLabel = NSTextField(labelWithString: "Print File")
+    private let iconView = NSImageView()
+    private var hoverTracker: HoverTracker?
+    private var isHovered = false
+
+    init() {
+        super.init(frame: .zero)
+        wantsLayer = true
+        layer?.cornerRadius = 22
+
+        titleLabel.font = AppTheme.semiboldFont(size: 16)
+        titleLabel.textColor = .white
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        if let image = NSImage(systemSymbolName: "printer.fill", accessibilityDescription: "Print") {
+            let config = NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
+            iconView.image = image.withSymbolConfiguration(config)
+        }
+        iconView.contentTintColor = .white
+        iconView.translatesAutoresizingMaskIntoConstraints = false
+
+        addSubview(titleLabel)
+        addSubview(iconView)
+
+        NSLayoutConstraint.activate([
+            heightAnchor.constraint(equalToConstant: 52),
+
+            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28),
+            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
+
+            iconView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 10),
+            iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
+            iconView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28),
+            iconView.widthAnchor.constraint(equalToConstant: 20),
+            iconView.heightAnchor.constraint(equalToConstant: 20),
+        ])
+
+        hoverTracker = HoverTracker(view: self) { [weak self] hovering in
+            self?.setHovered(hovering)
+        }
+        refreshAppearance()
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+
+    func refreshAppearance() {
+        let color = isHovered
+            ? AppTheme.blue.blended(withFraction: 0.15, of: .black) ?? AppTheme.blue
+            : AppTheme.blue
+        layer?.backgroundColor = color.cgColor
+        titleLabel.textColor = .white
+        iconView.contentTintColor = .white
+    }
+
+    private func setHovered(_ hovering: Bool) {
+        isHovered = hovering
+        animateHover {
+            let color = hovering
+                ? AppTheme.blue.blended(withFraction: 0.15, of: .black) ?? AppTheme.blue
+                : AppTheme.blue
+            layer?.backgroundColor = color.cgColor
+            layer?.transform = hovering
+                ? CATransform3DMakeScale(1.03, 1.03, 1)
+                : CATransform3DIdentity
+        }
+    }
+
+    override func mouseUp(with event: NSEvent) {
+        guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
+        onClick?()
+    }
+
+    override func resetCursorRects() {
+        addCursorRect(bounds, cursor: .pointingHand)
+    }
+}

+ 0 - 15
smart_printer/PhotoPreviewView.swift

@@ -21,7 +21,6 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
     private var securityAccess: [URL: Bool] = [:]
 
     private let backButton = PhotoToolbarButton(symbolName: "chevron.left", accessibilityLabel: "Back")
-    private let editButton = PhotoToolbarButton(symbolName: "pencil", accessibilityLabel: "Edit")
     private let titleLabel = NSTextField(labelWithString: "Print a photo")
     private let counterLabel = NSTextField(labelWithString: "")
     private let imageContainer = NSView()
@@ -61,7 +60,6 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         imageContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
         imageContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
         backButton.refreshAppearance()
-        editButton.refreshAppearance()
         printButton.refreshAppearance()
     }
 
@@ -128,16 +126,13 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
 
         backButton.translatesAutoresizingMaskIntoConstraints = false
-        editButton.translatesAutoresizingMaskIntoConstraints = false
         printButton.translatesAutoresizingMaskIntoConstraints = false
 
         backButton.onClick = { [weak self] in self?.dismiss() }
-        editButton.onClick = { [weak self] in self?.editCurrentPhoto() }
         printButton.onClick = { [weak self] in self?.printCurrentPhoto() }
 
         addSubview(backButton)
         addSubview(titleLabel)
-        addSubview(editButton)
         addSubview(counterLabel)
         addSubview(imageContainer)
         imageContainer.addSubview(imageView)
@@ -149,11 +144,6 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
             backButton.widthAnchor.constraint(equalToConstant: 40),
             backButton.heightAnchor.constraint(equalToConstant: 40),
 
-            editButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24),
-            editButton.topAnchor.constraint(equalTo: backButton.topAnchor),
-            editButton.widthAnchor.constraint(equalToConstant: 40),
-            editButton.heightAnchor.constraint(equalToConstant: 40),
-
             titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
             titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
 
@@ -200,11 +190,6 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         return urls[currentIndex]
     }
 
-    private func editCurrentPhoto() {
-        guard let url = currentURL() else { return }
-        NSWorkspace.shared.open(url)
-    }
-
     private func printCurrentPhoto() {
         guard let url = currentURL() else { return }
         PrintService.print(urls: [url], from: window)

+ 15 - 0
smart_printer/ViewController.swift

@@ -16,6 +16,7 @@ class ViewController: NSViewController {
     private var contentContainer: NSView!
     private var paywallOverlay: PaywallOverlayView?
     private var photoPreviewOverlay: PhotoPreviewOverlayView?
+    private var filePreviewOverlay: FilePreviewOverlayView?
 
     private var headerView: NSView!
     private var contentTopBelowHeader: NSLayoutConstraint!
@@ -78,10 +79,12 @@ class ViewController: NSViewController {
         view.refreshAppearanceRecursively()
         paywallOverlay?.refreshAppearance()
         photoPreviewOverlay?.refreshAppearance()
+        filePreviewOverlay?.refreshAppearance()
     }
 
     func presentPhotoPreview(urls: [URL]) {
         photoPreviewOverlay?.dismiss(animated: false)
+        filePreviewOverlay?.dismiss(animated: false)
 
         let overlay = PhotoPreviewOverlayView(urls: urls)
         overlay.onDismiss = { [weak self] in
@@ -91,6 +94,18 @@ class ViewController: NSViewController {
         overlay.present(in: mainContentView)
     }
 
+    func presentFilePreview(urls: [URL]) {
+        photoPreviewOverlay?.dismiss(animated: false)
+        filePreviewOverlay?.dismiss(animated: false)
+
+        let overlay = FilePreviewOverlayView(urls: urls)
+        overlay.onDismiss = { [weak self] in
+            self?.filePreviewOverlay = nil
+        }
+        filePreviewOverlay = overlay
+        overlay.present(in: mainContentView)
+    }
+
     private func setupLayout() {
         sidebar = SidebarView()