ViewController.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. //
  2. // ViewController.swift
  3. // smart_printer
  4. //
  5. import Cocoa
  6. class ViewController: NSViewController {
  7. private var sidebar: SidebarView!
  8. private var mainContentView: NSView!
  9. private var homeContentView: NSView!
  10. private var scanContentView: NSView!
  11. private var scanAndHomeContentView: NSView!
  12. private var settingsContentView: NSView!
  13. private var contentContainer: NSView!
  14. private var paywallOverlay: PaywallOverlayView?
  15. private var photoPreviewOverlay: PhotoPreviewOverlayView?
  16. private var filePreviewOverlay: FilePreviewOverlayView?
  17. private var printTextOverlay: PrintTextOverlayView?
  18. private var printContactsOverlay: PrintContactsOverlayView?
  19. private var drawPrintOverlay: DrawPrintOverlayView?
  20. private var headerView: NSView!
  21. private var contentTopBelowHeader: NSLayoutConstraint!
  22. private var contentTopBelowWindow: NSLayoutConstraint!
  23. override func loadView() {
  24. let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
  25. container.autoresizingMask = [.width, .height]
  26. container.wantsLayer = true
  27. container.layer?.backgroundColor = AppTheme.background.cgColor
  28. view = container
  29. }
  30. override func viewDidLoad() {
  31. super.viewDidLoad()
  32. setupLayout()
  33. NotificationCenter.default.addObserver(
  34. self,
  35. selector: #selector(showSettingsFromMenu),
  36. name: .showSettings,
  37. object: nil
  38. )
  39. NotificationCenter.default.addObserver(
  40. self,
  41. selector: #selector(appearanceDidChange),
  42. name: .appearanceDidChange,
  43. object: nil
  44. )
  45. NotificationCenter.default.addObserver(
  46. self,
  47. selector: #selector(premiumStatusDidChange),
  48. name: .premiumStatusDidChange,
  49. object: nil
  50. )
  51. }
  52. deinit {
  53. NotificationCenter.default.removeObserver(self)
  54. }
  55. @objc private func showSettingsFromMenu() {
  56. navigateToSettings()
  57. }
  58. @objc private func appearanceDidChange() {
  59. refreshAppearance()
  60. }
  61. @objc private func premiumStatusDidChange() {
  62. Task { @MainActor in
  63. paywallOverlay?.refreshStoreState()
  64. }
  65. }
  66. private func refreshAppearance() {
  67. view.layer?.backgroundColor = AppTheme.background.cgColor
  68. mainContentView?.layer?.backgroundColor = AppTheme.background.cgColor
  69. view.window?.backgroundColor = AppTheme.background
  70. sidebar?.refreshAppearance()
  71. view.refreshAppearanceRecursively()
  72. paywallOverlay?.refreshAppearance()
  73. photoPreviewOverlay?.refreshAppearance()
  74. filePreviewOverlay?.refreshAppearance()
  75. printTextOverlay?.refreshAppearance()
  76. printContactsOverlay?.refreshAppearance()
  77. drawPrintOverlay?.refreshAppearance()
  78. }
  79. func presentPrintText() {
  80. photoPreviewOverlay?.dismiss(animated: false)
  81. filePreviewOverlay?.dismiss(animated: false)
  82. printContactsOverlay?.dismiss(animated: false)
  83. drawPrintOverlay?.dismiss(animated: false)
  84. let overlay = PrintTextOverlayView()
  85. overlay.onDismiss = { [weak self] in
  86. self?.printTextOverlay = nil
  87. }
  88. printTextOverlay = overlay
  89. overlay.present(in: mainContentView)
  90. }
  91. func presentPrintContacts() {
  92. photoPreviewOverlay?.dismiss(animated: false)
  93. filePreviewOverlay?.dismiss(animated: false)
  94. printTextOverlay?.dismiss(animated: false)
  95. drawPrintOverlay?.dismiss(animated: false)
  96. if let overlay = printContactsOverlay {
  97. overlay.present(in: mainContentView)
  98. return
  99. }
  100. let overlay = PrintContactsOverlayView()
  101. printContactsOverlay = overlay
  102. overlay.present(in: mainContentView)
  103. }
  104. func presentDrawPrint() {
  105. photoPreviewOverlay?.dismiss(animated: false)
  106. filePreviewOverlay?.dismiss(animated: false)
  107. printTextOverlay?.dismiss(animated: false)
  108. printContactsOverlay?.dismiss(animated: false)
  109. let overlay = DrawPrintOverlayView()
  110. overlay.onDismiss = { [weak self] in
  111. self?.drawPrintOverlay = nil
  112. }
  113. drawPrintOverlay = overlay
  114. overlay.present(in: mainContentView)
  115. }
  116. func presentPhotoPreview(urls: [URL]) {
  117. photoPreviewOverlay?.dismiss(animated: false)
  118. filePreviewOverlay?.dismiss(animated: false)
  119. let overlay = PhotoPreviewOverlayView(urls: urls)
  120. overlay.onDismiss = { [weak self] in
  121. self?.photoPreviewOverlay = nil
  122. }
  123. photoPreviewOverlay = overlay
  124. overlay.present(in: mainContentView)
  125. }
  126. func presentFilePreview(urls: [URL]) {
  127. photoPreviewOverlay?.dismiss(animated: false)
  128. filePreviewOverlay?.dismiss(animated: false)
  129. let overlay = FilePreviewOverlayView(urls: urls)
  130. overlay.onDismiss = { [weak self] in
  131. self?.filePreviewOverlay = nil
  132. }
  133. filePreviewOverlay = overlay
  134. overlay.present(in: mainContentView)
  135. }
  136. private func setupLayout() {
  137. sidebar = SidebarView()
  138. mainContentView = NSView()
  139. mainContentView.translatesAutoresizingMaskIntoConstraints = false
  140. mainContentView.wantsLayer = true
  141. mainContentView.layer?.backgroundColor = AppTheme.background.cgColor
  142. headerView = makeHeader()
  143. contentContainer = NSView()
  144. contentContainer.translatesAutoresizingMaskIntoConstraints = false
  145. homeContentView = makeHomeContentView()
  146. scanContentView = makeScanContentView()
  147. scanAndHomeContentView = makeScanAndHomeContentView()
  148. settingsContentView = makeSettingsContentView()
  149. contentContainer.addSubview(homeContentView)
  150. contentContainer.addSubview(scanContentView)
  151. contentContainer.addSubview(scanAndHomeContentView)
  152. contentContainer.addSubview(settingsContentView)
  153. view.addSubview(sidebar)
  154. view.addSubview(mainContentView)
  155. mainContentView.addSubview(contentContainer)
  156. mainContentView.addSubview(headerView, positioned: .above, relativeTo: contentContainer)
  157. sidebar.onDestinationSelected = { [weak self] destination in
  158. self?.showDestination(destination)
  159. }
  160. NSLayoutConstraint.activate([
  161. sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  162. sidebar.topAnchor.constraint(equalTo: view.topAnchor),
  163. sidebar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  164. mainContentView.leadingAnchor.constraint(equalTo: sidebar.trailingAnchor),
  165. mainContentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  166. mainContentView.topAnchor.constraint(equalTo: view.topAnchor),
  167. mainContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  168. headerView.leadingAnchor.constraint(equalTo: mainContentView.leadingAnchor),
  169. headerView.trailingAnchor.constraint(equalTo: mainContentView.trailingAnchor),
  170. headerView.topAnchor.constraint(equalTo: mainContentView.topAnchor, constant: AppTheme.headerTopInset),
  171. headerView.heightAnchor.constraint(equalToConstant: AppTheme.headerHeight),
  172. contentContainer.leadingAnchor.constraint(equalTo: mainContentView.leadingAnchor),
  173. contentContainer.trailingAnchor.constraint(equalTo: mainContentView.trailingAnchor),
  174. contentContainer.bottomAnchor.constraint(equalTo: mainContentView.bottomAnchor),
  175. ])
  176. pinContentView(homeContentView)
  177. pinContentView(scanContentView)
  178. pinContentView(scanAndHomeContentView)
  179. pinContentView(settingsContentView)
  180. contentTopBelowHeader = contentContainer.topAnchor.constraint(
  181. equalTo: headerView.bottomAnchor,
  182. constant: AppTheme.contentBelowHeaderGap
  183. )
  184. contentTopBelowWindow = contentContainer.topAnchor.constraint(equalTo: mainContentView.topAnchor, constant: 12)
  185. contentTopBelowHeader.isActive = true
  186. showDestination(.home)
  187. }
  188. private func pinContentView(_ contentView: NSView) {
  189. NSLayoutConstraint.activate([
  190. contentView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
  191. contentView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
  192. contentView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
  193. contentView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
  194. ])
  195. }
  196. private func dismissFeatureOverlays() {
  197. photoPreviewOverlay?.dismiss(animated: false)
  198. filePreviewOverlay?.dismiss(animated: false)
  199. printTextOverlay?.dismiss(animated: false)
  200. printContactsOverlay?.dismiss(animated: false)
  201. drawPrintOverlay?.dismiss(animated: false)
  202. }
  203. private func showDestination(_ destination: SidebarDestination) {
  204. dismissFeatureOverlays()
  205. homeContentView.isHidden = destination != .home
  206. scanContentView.isHidden = destination != .scan
  207. scanAndHomeContentView.isHidden = destination != .scanAndHome
  208. settingsContentView.isHidden = destination != .settings
  209. let isSettings = destination == .settings
  210. headerView.isHidden = isSettings
  211. contentTopBelowHeader.isActive = !isSettings
  212. contentTopBelowWindow.isActive = isSettings
  213. if destination == .scanAndHome {
  214. presentPaywall()
  215. } else {
  216. dismissPaywall()
  217. }
  218. }
  219. private func presentPaywall() {
  220. if let paywallOverlay {
  221. Task { @MainActor in
  222. await StoreManager.shared.refreshPremiumStatus()
  223. paywallOverlay.refreshStoreState()
  224. }
  225. return
  226. }
  227. setTrafficLightsHidden(true)
  228. let overlay = PaywallOverlayView()
  229. overlay.onDismiss = { [weak self] in
  230. self?.setTrafficLightsHidden(false)
  231. self?.paywallOverlay = nil
  232. self?.sidebar.select(.home)
  233. self?.showDestination(.home)
  234. }
  235. overlay.present(in: view)
  236. paywallOverlay = overlay
  237. }
  238. private func dismissPaywall() {
  239. paywallOverlay?.removeFromSuperview()
  240. paywallOverlay = nil
  241. setTrafficLightsHidden(false)
  242. }
  243. private func setTrafficLightsHidden(_ isHidden: Bool) {
  244. guard let window = view.window else { return }
  245. let buttons: [NSWindow.ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton]
  246. for buttonType in buttons {
  247. window.standardWindowButton(buttonType)?.isHidden = isHidden
  248. }
  249. }
  250. private func makeHomeContentView() -> NSView {
  251. let container = NSView()
  252. container.translatesAutoresizingMaskIntoConstraints = false
  253. let panel = ContentPanelView()
  254. panel.translatesAutoresizingMaskIntoConstraints = false
  255. let scrollView = makeScrollView()
  256. panel.addSubview(scrollView)
  257. container.addSubview(panel)
  258. let inset = AppTheme.contentPanelInset
  259. NSLayoutConstraint.activate([
  260. panel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: inset),
  261. panel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -inset),
  262. panel.topAnchor.constraint(equalTo: container.topAnchor),
  263. panel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -inset),
  264. scrollView.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
  265. scrollView.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
  266. scrollView.topAnchor.constraint(equalTo: panel.topAnchor),
  267. scrollView.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
  268. ])
  269. return container
  270. }
  271. private func makeScanContentView() -> NSView {
  272. makeDestinationScrollView(
  273. title: "Scan",
  274. features: [
  275. FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
  276. FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
  277. ]
  278. )
  279. }
  280. private func makeScanAndHomeContentView() -> NSView {
  281. makeDestinationScrollView(
  282. title: "Premium",
  283. features: featureCards(
  284. FeatureCardData(title: "From Photos", subtitle: "Take a photo from gallery", iconKind: .scanFile),
  285. FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
  286. FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
  287. FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts)
  288. )
  289. )
  290. }
  291. private func makeDestinationScrollView(title: String, features: [FeatureCardData]) -> NSView {
  292. let container = NSView()
  293. container.translatesAutoresizingMaskIntoConstraints = false
  294. container.isHidden = true
  295. let scrollView = NSScrollView()
  296. scrollView.translatesAutoresizingMaskIntoConstraints = false
  297. scrollView.hasVerticalScroller = true
  298. scrollView.hasHorizontalScroller = false
  299. scrollView.autohidesScrollers = true
  300. scrollView.drawsBackground = false
  301. scrollView.borderType = .noBorder
  302. let documentView = FlippedDocumentView()
  303. documentView.translatesAutoresizingMaskIntoConstraints = false
  304. let sectionTitle = makeSectionTitle(title, icon: .grid)
  305. let grid = makeFeatureGrid(features: features, columns: AppTheme.featureGridColumns)
  306. documentView.addSubview(sectionTitle)
  307. documentView.addSubview(grid)
  308. scrollView.documentView = documentView
  309. container.addSubview(scrollView)
  310. let contentGuide = scrollView.contentView
  311. NSLayoutConstraint.activate([
  312. scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  313. scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  314. scrollView.topAnchor.constraint(equalTo: container.topAnchor),
  315. scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  316. documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
  317. documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
  318. documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
  319. documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
  320. sectionTitle.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  321. sectionTitle.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  322. sectionTitle.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
  323. grid.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  324. grid.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  325. grid.topAnchor.constraint(equalTo: sectionTitle.bottomAnchor, constant: 16),
  326. grid.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
  327. ])
  328. return container
  329. }
  330. private func makeHeader() -> NSView {
  331. let header = NSView()
  332. header.translatesAutoresizingMaskIntoConstraints = false
  333. let titleLabel = NSTextField.themeLabel("Smart Printer", style: .primary, font: AppTheme.semiboldFont(size: 18))
  334. titleLabel.alignment = .center
  335. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  336. header.addSubview(titleLabel)
  337. NSLayoutConstraint.activate([
  338. titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
  339. titleLabel.topAnchor.constraint(equalTo: header.topAnchor, constant: 2),
  340. ])
  341. return header
  342. }
  343. private func makeSettingsContentView() -> NSView {
  344. let container = NSView()
  345. container.translatesAutoresizingMaskIntoConstraints = false
  346. let settingsView = SettingsView()
  347. container.addSubview(settingsView)
  348. NSLayoutConstraint.activate([
  349. settingsView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  350. settingsView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  351. settingsView.topAnchor.constraint(equalTo: container.topAnchor),
  352. settingsView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  353. ])
  354. return container
  355. }
  356. private func navigateToSettings() {
  357. if paywallOverlay != nil {
  358. dismissPaywall()
  359. }
  360. sidebar.select(.settings)
  361. showDestination(.settings)
  362. }
  363. private func makeScrollView() -> NSScrollView {
  364. let scrollView = NSScrollView()
  365. scrollView.translatesAutoresizingMaskIntoConstraints = false
  366. scrollView.hasVerticalScroller = true
  367. scrollView.hasHorizontalScroller = false
  368. scrollView.autohidesScrollers = true
  369. scrollView.drawsBackground = false
  370. scrollView.borderType = .noBorder
  371. let documentView = FlippedDocumentView()
  372. documentView.translatesAutoresizingMaskIntoConstraints = false
  373. let quickStartSection = makeQuickStartSection()
  374. let createPrintSection = makeCreatePrintSection()
  375. documentView.addSubview(quickStartSection)
  376. documentView.addSubview(createPrintSection)
  377. scrollView.documentView = documentView
  378. let contentGuide = scrollView.contentView
  379. NSLayoutConstraint.activate([
  380. documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
  381. documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
  382. documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
  383. documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
  384. quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  385. quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  386. quickStartSection.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 20),
  387. createPrintSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  388. createPrintSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  389. createPrintSection.topAnchor.constraint(equalTo: quickStartSection.bottomAnchor, constant: 36),
  390. createPrintSection.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
  391. ])
  392. return scrollView
  393. }
  394. private enum SectionTitleIcon {
  395. case none
  396. case sparkle
  397. case grid
  398. }
  399. private func makeSectionTitle(_ text: String, icon: SectionTitleIcon = .none) -> NSView {
  400. let container = NSView()
  401. container.translatesAutoresizingMaskIntoConstraints = false
  402. var leadingAnchor = container.leadingAnchor
  403. var iconSpacing: CGFloat = 0
  404. if icon != .none {
  405. let titleIcon: NSView
  406. switch icon {
  407. case .sparkle:
  408. titleIcon = SparkleIconView()
  409. case .grid:
  410. let gridIcon = NSImageView()
  411. if let image = NSImage(systemSymbolName: "square.grid.2x2", accessibilityDescription: nil) {
  412. let config = NSImage.SymbolConfiguration(pointSize: 16, weight: .medium)
  413. gridIcon.image = image.withSymbolConfiguration(config)
  414. }
  415. gridIcon.contentTintColor = AppTheme.blue
  416. titleIcon = gridIcon
  417. case .none:
  418. titleIcon = NSView()
  419. }
  420. titleIcon.translatesAutoresizingMaskIntoConstraints = false
  421. container.addSubview(titleIcon)
  422. NSLayoutConstraint.activate([
  423. titleIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  424. titleIcon.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  425. titleIcon.widthAnchor.constraint(equalToConstant: 24),
  426. titleIcon.heightAnchor.constraint(equalToConstant: 24),
  427. ])
  428. leadingAnchor = titleIcon.trailingAnchor
  429. iconSpacing = 8
  430. }
  431. let label = NSTextField.themeLabel(text, style: .primary, font: .systemFont(ofSize: 20, weight: .bold))
  432. label.translatesAutoresizingMaskIntoConstraints = false
  433. container.addSubview(label)
  434. NSLayoutConstraint.activate([
  435. container.heightAnchor.constraint(equalToConstant: 28),
  436. label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: iconSpacing),
  437. label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  438. label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  439. ])
  440. return container
  441. }
  442. private func makeQuickStartSection() -> NSView {
  443. let section = NSView()
  444. section.translatesAutoresizingMaskIntoConstraints = false
  445. let title = makeSectionTitle("Quick Start", icon: .sparkle)
  446. section.addSubview(title)
  447. let cardsStack = NSStackView()
  448. cardsStack.orientation = .horizontal
  449. cardsStack.spacing = AppTheme.quickStartSpacing
  450. cardsStack.distribution = .fillEqually
  451. cardsStack.translatesAutoresizingMaskIntoConstraints = false
  452. let cards: [QuickStartCardData] = [
  453. QuickStartCardData(
  454. title: "From Photos",
  455. subtitle: "Select photos from gallery",
  456. buttonTitle: "Open Gallery",
  457. accentColor: AppTheme.blue,
  458. gradientColors: [AppTheme.quickStartBlueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
  459. iconKind: .photos
  460. ),
  461. QuickStartCardData(
  462. title: "From Files",
  463. subtitle: "Select Photos from file manager",
  464. buttonTitle: "Browse Files",
  465. accentColor: AppTheme.green,
  466. gradientColors: [AppTheme.greenLight, NSColor(red: 0.78, green: 0.94, blue: 0.84, alpha: 1)],
  467. iconKind: .files
  468. ),
  469. QuickStartCardData(
  470. title: "Import File",
  471. subtitle: "Add files from storage",
  472. buttonTitle: "Import Now",
  473. accentColor: AppTheme.orange,
  474. gradientColors: [AppTheme.orangeLight, NSColor(red: 1.0, green: 0.88, blue: 0.76, alpha: 1)],
  475. iconKind: .importFile
  476. ),
  477. ]
  478. let pickerKinds: [DocumentPickerKind] = [.photos, .files, .importFile]
  479. for (index, cardData) in cards.enumerated() {
  480. let kind = pickerKinds[index]
  481. let card = QuickStartCardView(data: cardData) { [weak self] in
  482. DocumentPickerService.present(kind, from: self?.view.window)
  483. }
  484. cardsStack.addArrangedSubview(card)
  485. }
  486. section.addSubview(cardsStack)
  487. NSLayoutConstraint.activate([
  488. title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  489. title.topAnchor.constraint(equalTo: section.topAnchor),
  490. cardsStack.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  491. cardsStack.trailingAnchor.constraint(equalTo: section.trailingAnchor),
  492. cardsStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
  493. cardsStack.bottomAnchor.constraint(equalTo: section.bottomAnchor),
  494. ])
  495. return section
  496. }
  497. private func makeCreatePrintSection() -> NSView {
  498. let section = NSView()
  499. section.translatesAutoresizingMaskIntoConstraints = false
  500. let title = makeSectionTitle("Create & Print", icon: .grid)
  501. section.addSubview(title)
  502. let features = featureCards(
  503. FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
  504. FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
  505. FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
  506. FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", iconKind: .drawPrint),
  507. FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
  508. FeatureCardData(title: "Print Website", subtitle: "Print any website", iconKind: .printWebsite)
  509. )
  510. let grid = makeFeatureGrid(features: features, columns: AppTheme.featureGridColumns)
  511. section.addSubview(grid)
  512. NSLayoutConstraint.activate([
  513. title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  514. title.topAnchor.constraint(equalTo: section.topAnchor),
  515. grid.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  516. grid.trailingAnchor.constraint(equalTo: section.trailingAnchor),
  517. grid.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
  518. grid.bottomAnchor.constraint(equalTo: section.bottomAnchor),
  519. ])
  520. return section
  521. }
  522. private func featureCards(_ cards: FeatureCardData...) -> [FeatureCardData] {
  523. cards.map { card in
  524. var updated = card
  525. if card.iconKind == .printText {
  526. updated.onActivate = { [weak self] in
  527. PrintTextService.present(from: self?.view.window)
  528. }
  529. } else if card.iconKind == .printContacts {
  530. updated.onActivate = { [weak self] in
  531. PrintContactsService.present(from: self?.view.window)
  532. }
  533. } else if card.iconKind == .drawPrint {
  534. updated.onActivate = { [weak self] in
  535. DrawPrintService.present(from: self?.view.window)
  536. }
  537. }
  538. return updated
  539. }
  540. }
  541. private func makeFeatureGrid(features: [FeatureCardData], columns: Int) -> NSView {
  542. let grid = NSStackView()
  543. grid.orientation = .vertical
  544. grid.spacing = AppTheme.featureGridSpacing
  545. grid.distribution = .fill
  546. grid.alignment = .leading
  547. grid.translatesAutoresizingMaskIntoConstraints = false
  548. var index = 0
  549. while index < features.count {
  550. let row = NSStackView()
  551. row.orientation = .horizontal
  552. row.spacing = AppTheme.featureGridSpacing
  553. row.distribution = .fillEqually
  554. row.translatesAutoresizingMaskIntoConstraints = false
  555. let rowCount = min(columns, features.count - index)
  556. for offset in 0..<rowCount {
  557. row.addArrangedSubview(FeatureCardView(data: features[index + offset]))
  558. }
  559. for _ in 0..<(columns - rowCount) {
  560. let spacer = NSView()
  561. spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
  562. spacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  563. row.addArrangedSubview(spacer)
  564. }
  565. grid.addArrangedSubview(row)
  566. row.leadingAnchor.constraint(equalTo: grid.leadingAnchor).isActive = true
  567. row.trailingAnchor.constraint(equalTo: grid.trailingAnchor).isActive = true
  568. index += rowCount
  569. }
  570. return grid
  571. }
  572. }
  573. private final class FlippedDocumentView: NSView {
  574. override var isFlipped: Bool { true }
  575. }
  576. extension Notification.Name {
  577. static let showSettings = Notification.Name("showSettings")
  578. }