| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962 |
- //
- // ViewController.swift
- // smart_printer
- //
- import Cocoa
- class ViewController: NSViewController {
- private var sidebar: SidebarView!
- private var mainContentView: NSView!
- private var homeContentView: NSView!
- private var scanContentView: NSView!
- private var printContentView: NSView!
- private var scanAndHomeContentView: NSView!
- private var settingsContentView: NSView!
- private weak var settingsView: SettingsView?
- private var contentContainer: NSView!
- private var paywallOverlay: PaywallOverlayView?
- private var photoPreviewOverlay: PhotoPreviewOverlayView?
- private var filePreviewOverlay: FilePreviewOverlayView?
- private var printTextOverlay: PrintTextOverlayView?
- private var printContactsOverlay: PrintContactsOverlayView?
- private var drawPrintOverlay: DrawPrintOverlayView?
- private var ocrFileOverlay: OCRFileOverlayView?
- private var scanFileOverlay: ScanFileOverlayView?
- private var scannerOverlay: ScannerOverlayView?
- private var scannerDidComplete = false
- private var headerView: NSView!
- private var contentTopBelowHeader: NSLayoutConstraint!
- private var contentTopBelowWindow: NSLayoutConstraint!
- override func loadView() {
- let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
- container.autoresizingMask = [.width, .height]
- container.wantsLayer = true
- container.layer?.backgroundColor = AppTheme.background.cgColor
- view = container
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- setupLayout()
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(showSettingsFromMenu),
- name: .showSettings,
- object: nil
- )
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(appearanceDidChange),
- name: .appearanceDidChange,
- object: nil
- )
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(premiumStatusDidChange),
- name: .premiumStatusDidChange,
- object: nil
- )
- }
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
- @objc private func showSettingsFromMenu() {
- navigateToSettings()
- }
- @objc private func appearanceDidChange() {
- refreshAppearance()
- }
- @objc private func premiumStatusDidChange() {
- Task { @MainActor in
- paywallOverlay?.refreshStoreState()
- sidebar?.refreshPremiumState()
- settingsView?.refreshPremiumState()
- if StoreManager.shared.isPremium {
- navigateHomeAfterPremiumUnlock()
- }
- }
- }
- private func navigateHomeAfterPremiumUnlock() {
- guard paywallOverlay != nil else { return }
- paywallOverlay?.onDismiss = { [weak self] in
- guard let self else { return }
- self.setTrafficLightsHidden(false)
- self.paywallOverlay = nil
- self.sidebar.select(.home)
- self.showDestination(.home)
- }
- paywallOverlay?.dismiss()
- }
- private func refreshAppearance() {
- view.layer?.backgroundColor = AppTheme.background.cgColor
- mainContentView?.layer?.backgroundColor = AppTheme.background.cgColor
- view.window?.backgroundColor = AppTheme.background
- sidebar?.refreshAppearance()
- view.refreshAppearanceRecursively()
- paywallOverlay?.refreshAppearance()
- photoPreviewOverlay?.refreshAppearance()
- filePreviewOverlay?.refreshAppearance()
- printTextOverlay?.refreshAppearance()
- printContactsOverlay?.refreshAppearance()
- drawPrintOverlay?.refreshAppearance()
- ocrFileOverlay?.refreshAppearance()
- scanFileOverlay?.refreshAppearance()
- scannerOverlay?.refreshAppearance()
- }
- func presentScanner(showScanPreview: Bool = false, onComplete: @escaping (Result<NSImage, Error>) -> Void) {
- guard guardPremiumFeature(.scannerScan) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- scannerDidComplete = false
- if let existing = scannerOverlay, existing.superview != nil {
- configureScannerOverlay(existing, showScanPreview: showScanPreview, onComplete: onComplete)
- Task { await existing.activate() }
- return
- }
- dismissScannerOverlay()
- let overlay = ScannerOverlayView()
- configureScannerOverlay(overlay, showScanPreview: showScanPreview, onComplete: onComplete)
- scannerOverlay = overlay
- overlay.present(in: mainContentView)
- Task { await overlay.activate() }
- }
- private func configureScannerOverlay(
- _ overlay: ScannerOverlayView,
- showScanPreview: Bool,
- onComplete: @escaping (Result<NSImage, Error>) -> Void
- ) {
- overlay.onDismiss = { [weak self] in
- guard let self else { return }
- let cancelled = !self.scannerDidComplete && self.scannerOverlay?.hasResult != true
- self.scannerOverlay = nil
- if cancelled {
- onComplete(.failure(ScanError.cancelled))
- }
- }
- overlay.onScanComplete = { [weak self] image in
- Task { @MainActor [weak self] in
- guard let self else { return }
- let processed: NSImage
- if let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
- processed = await ScanDocumentProcessor.process(cgImage) ?? image
- } else {
- processed = image
- }
- let displayImage = processed.normalizedForDisplay()
- if showScanPreview {
- overlay.updateScanPreview(displayImage)
- } else {
- self.scannerDidComplete = true
- onComplete(.success(displayImage))
- overlay.dismiss()
- }
- }
- }
- }
- func presentPrintText() {
- guard guardPremiumFeature(.printText) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- let overlay = PrintTextOverlayView()
- overlay.onDismiss = { [weak self] in
- self?.printTextOverlay = nil
- }
- printTextOverlay = overlay
- overlay.present(in: mainContentView)
- }
- func presentPrintContacts() {
- guard guardPremiumFeature(.printContacts) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printTextOverlay?.dismiss(animated: false)
- drawPrintOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- contentContainer.isHidden = true
- if let overlay = printContactsOverlay {
- overlay.present(in: mainContentView)
- return
- }
- let overlay = PrintContactsOverlayView()
- overlay.onDismiss = { [weak self] in
- self?.printContactsOverlay = nil
- self?.restoreContentAfterOverlay()
- }
- printContactsOverlay = overlay
- overlay.present(in: mainContentView)
- }
- func presentOCRFile(imageURL: URL) {
- guard guardPremiumFeature(.ocrFile) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- let overlay = OCRFileOverlayView(imageURL: imageURL)
- overlay.onDismiss = { [weak self] in
- self?.ocrFileOverlay = nil
- }
- ocrFileOverlay = overlay
- overlay.present(in: mainContentView)
- }
- func presentScanFile(pages: [ScanPage]) {
- guard guardPremiumFeature(.scannerScan) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- if let overlay = scanFileOverlay {
- overlay.appendPages(pages)
- overlay.present(in: mainContentView)
- return
- }
- let overlay = ScanFileOverlayView(pages: pages)
- overlay.onDismiss = { [weak self] in
- self?.scanFileOverlay = nil
- Task { @MainActor in
- await ScannerService.shared.releaseScanner()
- }
- }
- scanFileOverlay = overlay
- overlay.present(in: mainContentView)
- }
- func presentDrawPrint() {
- guard guardPremiumFeature(.drawPrint) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- let overlay = DrawPrintOverlayView()
- overlay.onDismiss = { [weak self] in
- self?.drawPrintOverlay = nil
- }
- drawPrintOverlay = overlay
- overlay.present(in: mainContentView)
- }
- func presentPhotoPreview(urls: [URL]) {
- guard guardPremiumFeature(.photoPreview) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- contentContainer.isHidden = true
- let overlay = PhotoPreviewOverlayView(urls: urls)
- overlay.onDismiss = { [weak self] in
- self?.photoPreviewOverlay = nil
- self?.restoreContentAfterOverlay()
- }
- photoPreviewOverlay = overlay
- overlay.present(in: mainContentView)
- }
- func presentFilePreview(urls: [URL]) {
- guard guardPremiumFeature(.filePreview) else { return }
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- contentContainer.isHidden = true
- let overlay = FilePreviewOverlayView(urls: urls)
- overlay.onDismiss = { [weak self] in
- self?.filePreviewOverlay = nil
- self?.restoreContentAfterOverlay()
- }
- filePreviewOverlay = overlay
- overlay.present(in: mainContentView)
- }
- private func setupLayout() {
- sidebar = SidebarView()
- mainContentView = NSView()
- mainContentView.translatesAutoresizingMaskIntoConstraints = false
- mainContentView.wantsLayer = true
- mainContentView.layer?.backgroundColor = AppTheme.background.cgColor
- headerView = makeHeader()
- contentContainer = NSView()
- contentContainer.translatesAutoresizingMaskIntoConstraints = false
- homeContentView = makeHomeContentView()
- scanContentView = makeScanContentView()
- printContentView = makePrintContentView()
- scanAndHomeContentView = makeScanAndHomeContentView()
- settingsContentView = makeSettingsContentView()
- contentContainer.addSubview(homeContentView)
- contentContainer.addSubview(scanContentView)
- contentContainer.addSubview(printContentView)
- contentContainer.addSubview(scanAndHomeContentView)
- contentContainer.addSubview(settingsContentView)
- view.addSubview(sidebar)
- view.addSubview(mainContentView)
- mainContentView.addSubview(contentContainer)
- mainContentView.addSubview(headerView, positioned: .above, relativeTo: contentContainer)
- sidebar.onDestinationSelected = { [weak self] destination in
- self?.showDestination(destination)
- }
- sidebar.onPremiumUpgradeTapped = { [weak self] in
- self?.showDestination(.scanAndHome)
- }
- sidebar.onManageSubscriptionTapped = {
- StoreManager.shared.showManageSubscriptions()
- }
- sidebar.refreshPremiumState()
- NSLayoutConstraint.activate([
- sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- sidebar.topAnchor.constraint(equalTo: view.topAnchor),
- sidebar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- mainContentView.leadingAnchor.constraint(equalTo: sidebar.trailingAnchor),
- mainContentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- mainContentView.topAnchor.constraint(equalTo: view.topAnchor),
- mainContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- headerView.leadingAnchor.constraint(equalTo: mainContentView.leadingAnchor),
- headerView.trailingAnchor.constraint(equalTo: mainContentView.trailingAnchor),
- headerView.topAnchor.constraint(equalTo: mainContentView.topAnchor, constant: AppTheme.headerTopInset),
- headerView.heightAnchor.constraint(equalToConstant: AppTheme.headerHeight),
- contentContainer.leadingAnchor.constraint(equalTo: mainContentView.leadingAnchor),
- contentContainer.trailingAnchor.constraint(equalTo: mainContentView.trailingAnchor),
- contentContainer.bottomAnchor.constraint(equalTo: mainContentView.bottomAnchor),
- ])
- pinContentView(homeContentView)
- pinContentView(scanContentView)
- pinContentView(printContentView)
- pinContentView(scanAndHomeContentView)
- pinContentView(settingsContentView)
- contentTopBelowHeader = contentContainer.topAnchor.constraint(
- equalTo: headerView.bottomAnchor,
- constant: AppTheme.contentBelowHeaderGap
- )
- contentTopBelowWindow = contentContainer.topAnchor.constraint(equalTo: mainContentView.topAnchor, constant: 12)
- contentTopBelowHeader.isActive = true
- showDestination(.home)
- }
- private func pinContentView(_ contentView: NSView) {
- NSLayoutConstraint.activate([
- contentView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
- contentView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
- contentView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
- contentView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
- ])
- }
- private func dismissScannerOverlay() {
- guard let scanner = scannerOverlay else { return }
- scanner.onDismiss = nil
- scanner.teardown()
- scanner.removeFromSuperview()
- scannerOverlay = nil
- }
- private func dismissFeatureOverlays() {
- photoPreviewOverlay?.dismiss(animated: false)
- filePreviewOverlay?.dismiss(animated: false)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.dismiss(animated: false)
- ocrFileOverlay?.dismiss(animated: false)
- scanFileOverlay?.dismiss(animated: false)
- dismissScannerOverlay()
- }
- private func hasActiveFeatureOverlay() -> Bool {
- [
- photoPreviewOverlay,
- filePreviewOverlay,
- printTextOverlay,
- printContactsOverlay,
- drawPrintOverlay,
- ocrFileOverlay,
- scanFileOverlay,
- scannerOverlay,
- ].contains { $0?.superview != nil }
- }
- private func restoreContentAfterOverlay() {
- guard !hasActiveFeatureOverlay() else { return }
- contentContainer.isHidden = false
- }
- private func showDestination(_ destination: SidebarDestination) {
- if destination == .scan || destination == .print || destination == .scanAndHome {
- Task { @MainActor in
- await StoreManager.shared.ensureEntitlementsResolved()
- self.showDestinationAfterEntitlementCheck(destination)
- }
- return
- }
- showDestinationAfterEntitlementCheck(destination)
- }
- private func showDestinationAfterEntitlementCheck(_ destination: SidebarDestination) {
- if destination == .scanAndHome {
- if StoreManager.shared.isPremium {
- sidebar.select(.scan)
- showDestinationAfterEntitlementCheck(.scan)
- } else {
- presentPaywall(returnToHomeOnDismiss: true)
- }
- return
- }
- if destination == .scan, !StoreManager.shared.isPremium {
- sidebar.select(.home)
- showDestinationContent(.home)
- presentPaywall(returnToHomeOnDismiss: true)
- return
- }
- if destination == .print, !StoreManager.shared.isPremium {
- sidebar.select(.home)
- showDestinationContent(.home)
- presentPaywall(returnToHomeOnDismiss: true)
- return
- }
- showDestinationContent(destination)
- dismissPaywall()
- }
- private func showDestinationContent(_ destination: SidebarDestination) {
- dismissFeatureOverlays()
- contentContainer.isHidden = false
- homeContentView.isHidden = destination != .home && destination != .scanAndHome
- scanContentView.isHidden = destination != .scan
- printContentView.isHidden = destination != .print
- scanAndHomeContentView.isHidden = true
- settingsContentView.isHidden = destination != .settings
- let isSettings = destination == .settings
- headerView.isHidden = isSettings
- contentTopBelowHeader.isActive = !isSettings
- contentTopBelowWindow.isActive = isSettings
- }
- func presentPaywall(returnToHomeOnDismiss: Bool = true) {
- if let paywallOverlay {
- Task { @MainActor in
- await StoreManager.shared.ensureEntitlementsResolved()
- paywallOverlay.refreshStoreState()
- }
- return
- }
- setTrafficLightsHidden(true)
- let overlay = PaywallOverlayView()
- overlay.onDismiss = { [weak self] in
- guard let self else { return }
- self.setTrafficLightsHidden(false)
- self.paywallOverlay = nil
- if returnToHomeOnDismiss {
- self.sidebar.select(.home)
- self.showDestination(.home)
- }
- }
- overlay.present(in: view)
- paywallOverlay = overlay
- }
- private func guardPremiumFeature(_ feature: AppFeature) -> Bool {
- PremiumAccess.require(feature: feature, from: view.window)
- }
- private func dismissPaywall() {
- paywallOverlay?.removeFromSuperview()
- paywallOverlay = nil
- setTrafficLightsHidden(false)
- }
- private func setTrafficLightsHidden(_ isHidden: Bool) {
- guard let window = view.window else { return }
- let buttons: [NSWindow.ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton]
- for buttonType in buttons {
- window.standardWindowButton(buttonType)?.isHidden = isHidden
- }
- }
- private func makeHomeContentView() -> NSView {
- let container = NSView()
- container.translatesAutoresizingMaskIntoConstraints = false
- let panel = ContentPanelView()
- panel.translatesAutoresizingMaskIntoConstraints = false
- let scrollView = makeScrollView()
- panel.addSubview(scrollView)
- container.addSubview(panel)
- let inset = AppTheme.contentPanelInset
- NSLayoutConstraint.activate([
- panel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: inset),
- panel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -inset),
- panel.topAnchor.constraint(equalTo: container.topAnchor),
- panel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -inset),
- scrollView.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
- scrollView.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
- scrollView.topAnchor.constraint(equalTo: panel.topAnchor),
- scrollView.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
- ])
- return container
- }
- private func makeScanContentView() -> NSView {
- makeDestinationScrollView(
- title: "Scan",
- features: featureCards(
- FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
- FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile)
- )
- )
- }
- private func makePrintContentView() -> NSView {
- makeDestinationScrollView(
- title: "Print",
- features: featureCards(
- FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
- FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
- FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", iconKind: .drawPrint)
- )
- )
- }
- private func makeScanAndHomeContentView() -> NSView {
- let container = NSView()
- container.translatesAutoresizingMaskIntoConstraints = false
- container.isHidden = true
- return container
- }
- private func makeDestinationScrollView(title: String, features: [FeatureCardData]) -> NSView {
- let container = NSView()
- container.translatesAutoresizingMaskIntoConstraints = false
- container.isHidden = true
- let scrollView = NSScrollView()
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- scrollView.hasVerticalScroller = true
- scrollView.hasHorizontalScroller = false
- scrollView.autohidesScrollers = true
- scrollView.drawsBackground = false
- scrollView.borderType = .noBorder
- let documentView = FlippedDocumentView()
- documentView.translatesAutoresizingMaskIntoConstraints = false
- let sectionTitle = makeSectionTitle(title, icon: .grid)
- let grid = makeFeatureGrid(features: features, columns: AppTheme.featureGridColumns)
- documentView.addSubview(sectionTitle)
- documentView.addSubview(grid)
- scrollView.documentView = documentView
- container.addSubview(scrollView)
- let contentGuide = scrollView.contentView
- NSLayoutConstraint.activate([
- scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
- scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
- scrollView.topAnchor.constraint(equalTo: container.topAnchor),
- scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
- documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
- documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
- documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
- documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
- sectionTitle.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- sectionTitle.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- sectionTitle.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
- grid.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- grid.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- grid.topAnchor.constraint(equalTo: sectionTitle.bottomAnchor, constant: 16),
- grid.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
- ])
- return container
- }
- private func makeHeader() -> NSView {
- let header = NSView()
- header.translatesAutoresizingMaskIntoConstraints = false
- let titleLabel = NSTextField.themeLabel("Smart Printer", style: .primary, font: AppTheme.semiboldFont(size: 18))
- titleLabel.alignment = .center
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- header.addSubview(titleLabel)
- NSLayoutConstraint.activate([
- titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
- titleLabel.topAnchor.constraint(equalTo: header.topAnchor, constant: 2),
- ])
- return header
- }
- private func makeSettingsContentView() -> NSView {
- let container = NSView()
- container.translatesAutoresizingMaskIntoConstraints = false
- let settingsView = SettingsView()
- self.settingsView = settingsView
- container.addSubview(settingsView)
- NSLayoutConstraint.activate([
- settingsView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
- settingsView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
- settingsView.topAnchor.constraint(equalTo: container.topAnchor),
- settingsView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
- ])
- return container
- }
- private func navigateToSettings() {
- if paywallOverlay != nil {
- dismissPaywall()
- }
- sidebar.select(.settings)
- showDestination(.settings)
- }
- private func makeScrollView() -> NSScrollView {
- let scrollView = NSScrollView()
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- scrollView.hasVerticalScroller = true
- scrollView.hasHorizontalScroller = false
- scrollView.autohidesScrollers = true
- scrollView.drawsBackground = false
- scrollView.borderType = .noBorder
- let documentView = FlippedDocumentView()
- documentView.translatesAutoresizingMaskIntoConstraints = false
- let quickStartSection = makeQuickStartSection()
- let createPrintSection = makeCreatePrintSection()
- documentView.addSubview(quickStartSection)
- documentView.addSubview(createPrintSection)
- scrollView.documentView = documentView
- let contentGuide = scrollView.contentView
- NSLayoutConstraint.activate([
- documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
- documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
- documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
- documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
- quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- quickStartSection.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 20),
- createPrintSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- createPrintSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- createPrintSection.topAnchor.constraint(equalTo: quickStartSection.bottomAnchor, constant: 36),
- createPrintSection.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
- ])
- return scrollView
- }
- private enum SectionTitleIcon {
- case none
- case sparkle
- case grid
- }
- private func makeSectionTitle(_ text: String, icon: SectionTitleIcon = .none) -> NSView {
- let container = NSView()
- container.translatesAutoresizingMaskIntoConstraints = false
- var leadingAnchor = container.leadingAnchor
- var iconSpacing: CGFloat = 0
- if icon != .none {
- let titleIcon: NSView
- switch icon {
- case .sparkle:
- titleIcon = SparkleIconView()
- case .grid:
- let gridIcon = NSImageView()
- if let image = NSImage(systemSymbolName: "square.grid.2x2", accessibilityDescription: nil) {
- let config = NSImage.SymbolConfiguration(pointSize: 16, weight: .medium)
- gridIcon.image = image.withSymbolConfiguration(config)
- }
- gridIcon.contentTintColor = AppTheme.blue
- titleIcon = gridIcon
- case .none:
- titleIcon = NSView()
- }
- titleIcon.translatesAutoresizingMaskIntoConstraints = false
- container.addSubview(titleIcon)
- NSLayoutConstraint.activate([
- titleIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor),
- titleIcon.centerYAnchor.constraint(equalTo: container.centerYAnchor),
- titleIcon.widthAnchor.constraint(equalToConstant: 24),
- titleIcon.heightAnchor.constraint(equalToConstant: 24),
- ])
- leadingAnchor = titleIcon.trailingAnchor
- iconSpacing = 8
- }
- let label = NSTextField.themeLabel(text, style: .primary, font: .systemFont(ofSize: 20, weight: .bold))
- label.translatesAutoresizingMaskIntoConstraints = false
- container.addSubview(label)
- NSLayoutConstraint.activate([
- container.heightAnchor.constraint(equalToConstant: 28),
- label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: iconSpacing),
- label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
- label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
- ])
- return container
- }
- private func makeQuickStartSection() -> NSView {
- let section = NSView()
- section.translatesAutoresizingMaskIntoConstraints = false
- let title = makeSectionTitle("Quick Start", icon: .sparkle)
- section.addSubview(title)
- let cardsStack = NSStackView()
- cardsStack.orientation = .horizontal
- cardsStack.spacing = AppTheme.quickStartSpacing
- cardsStack.distribution = .fillEqually
- cardsStack.translatesAutoresizingMaskIntoConstraints = false
- let cards: [QuickStartCardData] = [
- QuickStartCardData(
- title: "From Photos",
- 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)],
- iconKind: .photos
- ),
- QuickStartCardData(
- title: "From Files",
- 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)],
- iconKind: .files
- ),
- QuickStartCardData(
- title: "Import File",
- 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)],
- iconKind: .importFile
- ),
- ]
- 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)
- NSLayoutConstraint.activate([
- title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
- title.topAnchor.constraint(equalTo: section.topAnchor),
- cardsStack.leadingAnchor.constraint(equalTo: section.leadingAnchor),
- cardsStack.trailingAnchor.constraint(equalTo: section.trailingAnchor),
- cardsStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
- cardsStack.bottomAnchor.constraint(equalTo: section.bottomAnchor),
- ])
- return section
- }
- private func makeCreatePrintSection() -> NSView {
- let section = NSView()
- section.translatesAutoresizingMaskIntoConstraints = false
- let title = makeSectionTitle("Create & Print", icon: .grid)
- section.addSubview(title)
- let features = featureCards(
- FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
- FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
- FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
- FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", iconKind: .drawPrint),
- FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile)
- )
- let grid = makeFeatureGrid(features: features, columns: AppTheme.featureGridColumns)
- section.addSubview(grid)
- NSLayoutConstraint.activate([
- title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
- title.topAnchor.constraint(equalTo: section.topAnchor),
- grid.leadingAnchor.constraint(equalTo: section.leadingAnchor),
- grid.trailingAnchor.constraint(equalTo: section.trailingAnchor),
- grid.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
- grid.bottomAnchor.constraint(equalTo: section.bottomAnchor),
- ])
- return section
- }
- private func featureCards(_ cards: FeatureCardData...) -> [FeatureCardData] {
- cards.map { card in
- var updated = card
- if card.iconKind == .printText {
- updated.onActivate = { [weak self] in
- PrintTextService.present(from: self?.view.window)
- }
- } else if card.iconKind == .printContacts {
- updated.onActivate = { [weak self] in
- PrintContactsService.present(from: self?.view.window)
- }
- } else if card.iconKind == .drawPrint {
- updated.onActivate = { [weak self] in
- DrawPrintService.present(from: self?.view.window)
- }
- } else if card.iconKind == .ocrFile {
- updated.onActivate = { [weak self] in
- OCRFileService.present(from: self?.view.window)
- }
- } else if card.iconKind == .scanFile {
- if card.title == "From Photos" {
- updated.onActivate = { [weak self] in
- DocumentPickerService.present(.photos, from: self?.view.window)
- }
- } else {
- updated.onActivate = { [weak self] in
- ScanFileService.present(from: self?.view.window)
- }
- }
- }
- return updated
- }
- }
- private func makeFeatureGrid(features: [FeatureCardData], columns: Int) -> NSView {
- let grid = NSStackView()
- grid.orientation = .vertical
- grid.spacing = AppTheme.featureGridSpacing
- grid.distribution = .fill
- grid.alignment = .leading
- grid.translatesAutoresizingMaskIntoConstraints = false
- var index = 0
- while index < features.count {
- let row = NSStackView()
- row.orientation = .horizontal
- row.spacing = AppTheme.featureGridSpacing
- row.distribution = .fillEqually
- row.translatesAutoresizingMaskIntoConstraints = false
- let rowCount = min(columns, features.count - index)
- for offset in 0..<rowCount {
- row.addArrangedSubview(FeatureCardView(data: features[index + offset]))
- }
- for _ in 0..<(columns - rowCount) {
- let spacer = NSView()
- spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
- spacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
- row.addArrangedSubview(spacer)
- }
- grid.addArrangedSubview(row)
- row.leadingAnchor.constraint(equalTo: grid.leadingAnchor).isActive = true
- row.trailingAnchor.constraint(equalTo: grid.trailingAnchor).isActive = true
- index += rowCount
- }
- return grid
- }
- }
- private final class FlippedDocumentView: NSView {
- override var isFlipped: Bool { true }
- }
- extension Notification.Name {
- static let showSettings = Notification.Name("showSettings")
- }
|