Procházet zdrojové kódy

Fix contact printing layout and stop repeated contacts permission prompts.

Correct PDF text coordinate transforms so printed contacts read upright from the top, and cache contacts access so the system dialog is only shown when authorization is still undetermined.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 před 1 měsícem
rodič
revize
ae654c06aa

+ 23 - 0
smart_printer/AppDelegate.swift

@@ -4,6 +4,7 @@
 //
 
 import Cocoa
+import Contacts
 
 @main
 class AppDelegate: NSObject, NSApplicationDelegate {
@@ -19,9 +20,31 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         resolveMainWindowController()
         configureMainWindow()
         configurePreferencesMenu()
+        observeContactChanges()
+        prefetchContactsIfAuthorized()
         NSApp.activate(ignoringOtherApps: true)
     }
 
+    private func observeContactChanges() {
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(contactsDidChange),
+            name: .CNContactStoreDidChange,
+            object: nil
+        )
+    }
+
+    @objc private func contactsDidChange() {
+        ContactsService.invalidateCache()
+    }
+
+    private func prefetchContactsIfAuthorized() {
+        guard ContactsService.isAuthorized else { return }
+        Task { @MainActor in
+            _ = try? await ContactsService.loadContacts()
+        }
+    }
+
     @objc func showSettings(_ sender: Any?) {
         NSApp.activate(ignoringOtherApps: true)
         mainWindow?.makeKeyAndOrderFront(nil)

+ 52 - 13
smart_printer/PrintContactsView.swift

@@ -21,18 +21,61 @@ struct PrintableContact: Equatable {
 
 // MARK: - Contacts Service
 
+enum ContactsError: Error {
+    case accessDenied
+}
+
 enum ContactsService {
-    static func requestAccess() async -> Bool {
-        let store = CNContactStore()
-        do {
-            return try await store.requestAccess(for: .contacts)
-        } catch {
+    private static let store = CNContactStore()
+    private static var cachedContacts: [PrintableContact]?
+
+    static var isAuthorized: Bool {
+        switch CNContactStore.authorizationStatus(for: .contacts) {
+        case .authorized, .limited:
+            return true
+        default:
+            return false
+        }
+    }
+
+    @MainActor
+    static func ensureAccess() async -> Bool {
+        switch CNContactStore.authorizationStatus(for: .contacts) {
+        case .authorized, .limited:
+            return true
+        case .notDetermined:
+            do {
+                return try await store.requestAccess(for: .contacts)
+            } catch {
+                return false
+            }
+        case .denied, .restricted:
+            return false
+        @unknown default:
             return false
         }
     }
 
+    @MainActor
+    static func loadContacts(forceRefresh: Bool = false) async throws -> [PrintableContact] {
+        if !forceRefresh, let cachedContacts, isAuthorized {
+            return cachedContacts
+        }
+
+        guard await ensureAccess() else {
+            throw ContactsError.accessDenied
+        }
+
+        let contacts = try fetchContacts()
+        cachedContacts = contacts
+        return contacts
+    }
+
+    static func invalidateCache() {
+        cachedContacts = nil
+    }
+
     static func fetchContacts() throws -> [PrintableContact] {
-        let store = CNContactStore()
         let keys: [CNKeyDescriptor] = [
             CNContactIdentifierKey as CNKeyDescriptor,
             CNContactGivenNameKey as CNKeyDescriptor,
@@ -296,16 +339,12 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
 
     private func loadContacts() {
         Task { @MainActor in
-            let granted = await ContactsService.requestAccess()
-            guard granted else {
-                showAccessDeniedAlert()
-                return
-            }
-
             do {
-                allContacts = try ContactsService.fetchContacts()
+                allContacts = try await ContactsService.loadContacts()
                 selectedIDs = Set(allContacts.map(\.identifier))
                 reloadList()
+            } catch ContactsError.accessDenied {
+                showAccessDeniedAlert()
             } catch {
                 showLoadFailedAlert()
             }

+ 50 - 13
smart_printer/PrintService.swift

@@ -116,7 +116,7 @@ enum PrintService {
         guard let operation = document.printOperation(
             for: printInfo,
             scalingMode: .pageScaleToFit,
-            autoRotate: true
+            autoRotate: false
         ) else {
             showPrintFailedAlert()
             return
@@ -178,7 +178,7 @@ enum PrintService {
         printInfo.horizontalPagination = .fit
         printInfo.verticalPagination = .fit
         printInfo.isHorizontallyCentered = true
-        printInfo.isVerticallyCentered = true
+        printInfo.isVerticallyCentered = false
         printInfo.topMargin = 36
         printInfo.bottomMargin = 36
         printInfo.leftMargin = 36
@@ -192,9 +192,14 @@ enum PrintService {
         let pageSize = printInfo.paperSize
         guard pageSize.width > 0, pageSize.height > 0 else { return nil }
 
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.alignment = .left
+        paragraphStyle.lineBreakMode = .byWordWrapping
+
         let attributes: [NSAttributedString.Key: Any] = [
             .font: NSFont.systemFont(ofSize: 12),
             .foregroundColor: NSColor.black,
+            .paragraphStyle: paragraphStyle,
         ]
         let attributed = NSAttributedString(string: text, attributes: attributes)
 
@@ -214,8 +219,7 @@ enum PrintService {
             return nil
         }
 
-        var glyphIndex = 0
-        while glyphIndex < layoutManager.numberOfGlyphs {
+        while true {
             let textContainer = NSTextContainer(size: containerSize)
             textContainer.lineFragmentPadding = 0
             layoutManager.addTextContainer(textContainer)
@@ -223,24 +227,57 @@ enum PrintService {
             let glyphRange = layoutManager.glyphRange(for: textContainer)
             guard glyphRange.length > 0 else { break }
 
-            context.beginPDFPage(nil)
+            let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
+            let pageText = attributed.attributedSubstring(from: charRange)
 
-            NSGraphicsContext.saveGraphicsState()
-            NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false)
-            layoutManager.drawGlyphs(
-                forGlyphRange: glyphRange,
-                at: NSPoint(x: printInfo.leftMargin, y: printInfo.bottomMargin)
+            context.beginPDFPage(nil)
+            drawTextPage(
+                pageText,
+                in: context,
+                pageSize: pageSize,
+                printInfo: printInfo,
+                textWidth: textWidth,
+                textHeight: textHeight
             )
-            NSGraphicsContext.restoreGraphicsState()
-
             context.endPDFPage()
-            glyphIndex = NSMaxRange(glyphRange)
+
+            if NSMaxRange(glyphRange) >= layoutManager.numberOfGlyphs { break }
         }
 
         context.closePDF()
         return PDFDocument(data: pdfData as Data)
     }
 
+    private static func drawTextPage(
+        _ text: NSAttributedString,
+        in context: CGContext,
+        pageSize: NSSize,
+        printInfo: NSPrintInfo,
+        textWidth: CGFloat,
+        textHeight: CGFloat
+    ) {
+        context.saveGState()
+        context.translateBy(x: 0, y: pageSize.height)
+        context.scaleBy(x: 1, y: -1)
+
+        let drawRect = CGRect(
+            x: printInfo.leftMargin,
+            y: printInfo.topMargin,
+            width: textWidth,
+            height: textHeight
+        )
+
+        NSGraphicsContext.saveGraphicsState()
+        NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
+        text.draw(
+            with: drawRect,
+            options: [.usesLineFragmentOrigin, .usesFontLeading]
+        )
+        NSGraphicsContext.restoreGraphicsState()
+
+        context.restoreGState()
+    }
+
     private static func pdfDocument(from image: NSImage) -> PDFDocument? {
         let printInfo = configuredPrintInfo()
         let pageSize = printInfo.paperSize

+ 5 - 3
smart_printer/ViewController.swift

@@ -108,10 +108,12 @@ class ViewController: NSViewController {
         printTextOverlay?.dismiss(animated: false)
         drawPrintOverlay?.dismiss(animated: false)
 
-        let overlay = PrintContactsOverlayView()
-        overlay.onDismiss = { [weak self] in
-            self?.printContactsOverlay = nil
+        if let overlay = printContactsOverlay {
+            overlay.present(in: mainContentView)
+            return
         }
+
+        let overlay = PrintContactsOverlayView()
         printContactsOverlay = overlay
         overlay.present(in: mainContentView)
     }