| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021 |
- //
- // 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 scannerCancelled = false
- private var scannerOutcomeDelivered = false
- private var paywallReturnToHomeOnDismiss = true
- private var headerView: NSView!
- private var headerFreeUsesBanner: FreeUsesBannerView!
- 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
- )
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(entitlementsDidResolve),
- name: .entitlementsDidResolve,
- object: nil
- )
- }
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
- @objc private func showSettingsFromMenu() {
- navigateToSettings()
- }
- @objc private func appearanceDidChange() {
- refreshAppearance()
- }
- @objc private func premiumStatusDidChange() {
- refreshPremiumUI()
- }
- @objc private func entitlementsDidResolve() {
- refreshPremiumUI()
- }
- private func refreshPremiumUI() {
- Task { @MainActor in
- paywallOverlay?.refreshStoreState()
- sidebar?.refreshPremiumState()
- settingsView?.refreshPremiumState()
- if StoreManager.shared.isPremium {
- navigateHomeAfterPremiumUnlock()
- }
- }
- }
- private func navigateHomeAfterPremiumUnlock() {
- guard paywallOverlay != nil else { return }
- paywallReturnToHomeOnDismiss = true
- paywallOverlay?.onDismiss = { [weak self] in
- self?.handlePaywallDismiss()
- }
- 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()
- headerFreeUsesBanner?.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
- scannerCancelled = false
- scannerOutcomeDelivered = 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 }
- self.scannerCancelled = true
- let cancelled = !self.scannerOutcomeDelivered
- && !self.scannerDidComplete
- && self.scannerOverlay?.hasResult != true
- self.scannerOverlay = nil
- if cancelled {
- self.scannerOutcomeDelivered = true
- onComplete(.failure(ScanError.cancelled))
- }
- }
- overlay.onScanComplete = { [weak self] image in
- Task { @MainActor [weak self] in
- guard let self, !self.scannerCancelled 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
- }
- guard !self.scannerCancelled else { return }
- let displayImage = processed.normalizedForDisplay()
- if showScanPreview {
- overlay.updateScanPreview(displayImage)
- } else {
- guard !self.scannerOutcomeDelivered else { return }
- self.scannerDidComplete = true
- self.scannerOutcomeDelivered = 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 {
- contentContainer.isHidden = true
- 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)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.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)
- printTextOverlay?.dismiss(animated: false)
- printContactsOverlay?.dismiss(animated: false)
- drawPrintOverlay?.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 || FreeTierManager.remainingUses(isPremium: false) > 0 {
- sidebar.select(.scan)
- showDestinationAfterEntitlementCheck(.scan)
- } else {
- presentPaywall(returnToHomeOnDismiss: true)
- }
- return
- }
- if destination == .scan, !PremiumAccess.canAccess(.scannerScan) {
- sidebar.select(.home)
- showDestinationContent(.home)
- presentPaywall(returnToHomeOnDismiss: true)
- return
- }
- if destination == .print, !PremiumAccess.canAccess(.printText) {
- 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) {
- paywallReturnToHomeOnDismiss = returnToHomeOnDismiss
- if let paywallOverlay {
- paywallOverlay.onDismiss = { [weak self] in
- self?.handlePaywallDismiss()
- }
- Task { @MainActor in
- await StoreManager.shared.ensureEntitlementsResolved()
- paywallOverlay.refreshStoreState()
- }
- return
- }
- setTrafficLightsHidden(true)
- let overlay = PaywallOverlayView()
- overlay.onDismiss = { [weak self] in
- self?.handlePaywallDismiss()
- }
- overlay.present(in: view)
- paywallOverlay = overlay
- }
- private func handlePaywallDismiss() {
- setTrafficLightsHidden(false)
- paywallOverlay = nil
- if paywallReturnToHomeOnDismiss {
- sidebar.select(.home)
- showDestination(.home)
- }
- }
- 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 freeUsesBanner = FreeUsesBannerView()
- let grid = makeFeatureGrid(features: features, columns: AppTheme.featureGridColumns)
- documentView.addSubview(sectionTitle)
- documentView.addSubview(freeUsesBanner)
- 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),
- freeUsesBanner.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- freeUsesBanner.trailingAnchor.constraint(lessThanOrEqualTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- freeUsesBanner.topAnchor.constraint(equalTo: sectionTitle.bottomAnchor, constant: 12),
- grid.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- grid.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- grid.topAnchor.constraint(equalTo: freeUsesBanner.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(AppStoreConfig.displayName, style: .primary, font: AppTheme.semiboldFont(size: 18))
- titleLabel.alignment = .center
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- headerFreeUsesBanner = FreeUsesBannerView()
- headerFreeUsesBanner.translatesAutoresizingMaskIntoConstraints = false
- header.addSubview(titleLabel)
- header.addSubview(headerFreeUsesBanner)
- NSLayoutConstraint.activate([
- titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
- titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
- headerFreeUsesBanner.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -AppTheme.contentPadding),
- headerFreeUsesBanner.centerYAnchor.constraint(equalTo: header.centerYAnchor),
- ])
- 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 freeUsesBanner = FreeUsesBannerView()
- let quickStartSection = makeQuickStartSection()
- let createPrintSection = makeCreatePrintSection()
- documentView.addSubview(freeUsesBanner)
- 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),
- freeUsesBanner.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- freeUsesBanner.trailingAnchor.constraint(lessThanOrEqualTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- freeUsesBanner.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 12),
- quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
- quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
- quickStartSection.topAnchor.constraint(equalTo: freeUsesBanner.bottomAnchor, constant: 16),
- 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")
- }
|