ViewController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. //
  2. // ViewController.swift
  3. // smart_printer
  4. //
  5. import Cocoa
  6. class ViewController: NSViewController {
  7. private var sidebar: SidebarView!
  8. private var homeContentView: NSView!
  9. private var scanContentView: NSView!
  10. private var scanAndHomeContentView: NSView!
  11. private var contentContainer: NSView!
  12. override func loadView() {
  13. let container = NSView(frame: NSRect(x: 0, y: 0, width: AppTheme.windowWidth, height: AppTheme.windowHeight))
  14. container.autoresizingMask = [.width, .height]
  15. container.wantsLayer = true
  16. container.layer?.backgroundColor = AppTheme.background.cgColor
  17. view = container
  18. }
  19. override func viewDidLoad() {
  20. super.viewDidLoad()
  21. setupLayout()
  22. }
  23. private func setupLayout() {
  24. sidebar = SidebarView()
  25. let mainContent = NSView()
  26. mainContent.translatesAutoresizingMaskIntoConstraints = false
  27. mainContent.wantsLayer = true
  28. mainContent.layer?.backgroundColor = AppTheme.background.cgColor
  29. let header = makeHeader()
  30. contentContainer = NSView()
  31. contentContainer.translatesAutoresizingMaskIntoConstraints = false
  32. homeContentView = makeHomeContentView()
  33. scanContentView = makeScanContentView()
  34. scanAndHomeContentView = makeScanAndHomeContentView()
  35. contentContainer.addSubview(homeContentView)
  36. contentContainer.addSubview(scanContentView)
  37. contentContainer.addSubview(scanAndHomeContentView)
  38. let wavePattern = WavePatternView()
  39. wavePattern.translatesAutoresizingMaskIntoConstraints = false
  40. view.addSubview(sidebar)
  41. view.addSubview(mainContent)
  42. mainContent.addSubview(header)
  43. mainContent.addSubview(contentContainer)
  44. mainContent.addSubview(wavePattern)
  45. sidebar.onDestinationSelected = { [weak self] destination in
  46. self?.showDestination(destination)
  47. }
  48. NSLayoutConstraint.activate([
  49. sidebar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  50. sidebar.topAnchor.constraint(equalTo: view.topAnchor),
  51. sidebar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  52. mainContent.leadingAnchor.constraint(equalTo: sidebar.trailingAnchor),
  53. mainContent.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  54. mainContent.topAnchor.constraint(equalTo: view.topAnchor),
  55. mainContent.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  56. header.leadingAnchor.constraint(equalTo: mainContent.leadingAnchor),
  57. header.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
  58. header.topAnchor.constraint(equalTo: mainContent.topAnchor, constant: 16),
  59. header.heightAnchor.constraint(equalToConstant: 44),
  60. contentContainer.leadingAnchor.constraint(equalTo: mainContent.leadingAnchor),
  61. contentContainer.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
  62. contentContainer.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
  63. contentContainer.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
  64. wavePattern.trailingAnchor.constraint(equalTo: mainContent.trailingAnchor),
  65. wavePattern.bottomAnchor.constraint(equalTo: mainContent.bottomAnchor),
  66. wavePattern.widthAnchor.constraint(equalToConstant: 200),
  67. wavePattern.heightAnchor.constraint(equalToConstant: 140),
  68. ])
  69. pinContentView(homeContentView)
  70. pinContentView(scanContentView)
  71. pinContentView(scanAndHomeContentView)
  72. showDestination(.home)
  73. }
  74. private func pinContentView(_ contentView: NSView) {
  75. NSLayoutConstraint.activate([
  76. contentView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
  77. contentView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
  78. contentView.topAnchor.constraint(equalTo: contentContainer.topAnchor),
  79. contentView.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
  80. ])
  81. }
  82. private func showDestination(_ destination: SidebarDestination) {
  83. homeContentView.isHidden = destination != .home
  84. scanContentView.isHidden = destination != .scan
  85. scanAndHomeContentView.isHidden = destination != .scanAndHome
  86. }
  87. private func makeHomeContentView() -> NSView {
  88. let container = NSView()
  89. container.translatesAutoresizingMaskIntoConstraints = false
  90. let scrollView = makeScrollView()
  91. container.addSubview(scrollView)
  92. NSLayoutConstraint.activate([
  93. scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  94. scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  95. scrollView.topAnchor.constraint(equalTo: container.topAnchor),
  96. scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  97. ])
  98. return container
  99. }
  100. private func makeScanContentView() -> NSView {
  101. makeDestinationScrollView(
  102. title: "Scan",
  103. features: [
  104. FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
  105. FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
  106. ]
  107. )
  108. }
  109. private func makeScanAndHomeContentView() -> NSView {
  110. makeDestinationScrollView(
  111. title: "Premium",
  112. features: [
  113. FeatureCardData(title: "From Photos", subtitle: "Take a photo from gallery", iconKind: .scanFile),
  114. FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
  115. FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
  116. FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
  117. ]
  118. )
  119. }
  120. private func makeDestinationScrollView(title: String, features: [FeatureCardData]) -> NSView {
  121. let container = NSView()
  122. container.translatesAutoresizingMaskIntoConstraints = false
  123. container.isHidden = true
  124. let scrollView = NSScrollView()
  125. scrollView.translatesAutoresizingMaskIntoConstraints = false
  126. scrollView.hasVerticalScroller = true
  127. scrollView.hasHorizontalScroller = false
  128. scrollView.autohidesScrollers = true
  129. scrollView.drawsBackground = false
  130. scrollView.borderType = .noBorder
  131. let documentView = FlippedDocumentView()
  132. documentView.translatesAutoresizingMaskIntoConstraints = false
  133. let sectionTitle = makeSectionTitle(title, icon: .grid)
  134. let grid = makeFeatureRow(features: features)
  135. documentView.addSubview(sectionTitle)
  136. documentView.addSubview(grid)
  137. scrollView.documentView = documentView
  138. container.addSubview(scrollView)
  139. let contentGuide = scrollView.contentView
  140. NSLayoutConstraint.activate([
  141. scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  142. scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  143. scrollView.topAnchor.constraint(equalTo: container.topAnchor),
  144. scrollView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  145. documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
  146. documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
  147. documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
  148. documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
  149. sectionTitle.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  150. sectionTitle.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  151. sectionTitle.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
  152. grid.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  153. grid.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  154. grid.topAnchor.constraint(equalTo: sectionTitle.bottomAnchor, constant: 16),
  155. grid.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
  156. ])
  157. return container
  158. }
  159. private func makeHeader() -> NSView {
  160. let header = NSView()
  161. header.translatesAutoresizingMaskIntoConstraints = false
  162. let titleLabel = NSTextField(labelWithString: "Smart Printer")
  163. titleLabel.font = AppTheme.semiboldFont(size: 18)
  164. titleLabel.textColor = AppTheme.textPrimary
  165. titleLabel.alignment = .center
  166. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  167. let settingsButton = NSButton()
  168. settingsButton.isBordered = false
  169. settingsButton.translatesAutoresizingMaskIntoConstraints = false
  170. if let image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") {
  171. let config = NSImage.SymbolConfiguration(pointSize: 18, weight: .regular)
  172. settingsButton.image = image.withSymbolConfiguration(config)
  173. }
  174. settingsButton.contentTintColor = AppTheme.textSecondary
  175. header.addSubview(titleLabel)
  176. header.addSubview(settingsButton)
  177. NSLayoutConstraint.activate([
  178. titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
  179. titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  180. settingsButton.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -28),
  181. settingsButton.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  182. settingsButton.widthAnchor.constraint(equalToConstant: 32),
  183. settingsButton.heightAnchor.constraint(equalToConstant: 32),
  184. ])
  185. return header
  186. }
  187. private func makeScrollView() -> NSScrollView {
  188. let scrollView = NSScrollView()
  189. scrollView.translatesAutoresizingMaskIntoConstraints = false
  190. scrollView.hasVerticalScroller = true
  191. scrollView.hasHorizontalScroller = false
  192. scrollView.autohidesScrollers = true
  193. scrollView.drawsBackground = false
  194. scrollView.borderType = .noBorder
  195. let documentView = FlippedDocumentView()
  196. documentView.translatesAutoresizingMaskIntoConstraints = false
  197. let quickStartSection = makeQuickStartSection()
  198. let createPrintSection = makeCreatePrintSection()
  199. documentView.addSubview(quickStartSection)
  200. documentView.addSubview(createPrintSection)
  201. scrollView.documentView = documentView
  202. let contentGuide = scrollView.contentView
  203. NSLayoutConstraint.activate([
  204. documentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
  205. documentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
  206. documentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),
  207. documentView.widthAnchor.constraint(equalTo: contentGuide.widthAnchor),
  208. quickStartSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  209. quickStartSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  210. quickStartSection.topAnchor.constraint(equalTo: documentView.topAnchor, constant: 8),
  211. createPrintSection.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: AppTheme.contentPadding),
  212. createPrintSection.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -AppTheme.contentPadding),
  213. createPrintSection.topAnchor.constraint(equalTo: quickStartSection.bottomAnchor, constant: 36),
  214. createPrintSection.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -40),
  215. ])
  216. return scrollView
  217. }
  218. private enum SectionTitleIcon {
  219. case none
  220. case sparkle
  221. case grid
  222. }
  223. private func makeSectionTitle(_ text: String, icon: SectionTitleIcon = .none) -> NSView {
  224. let container = NSView()
  225. container.translatesAutoresizingMaskIntoConstraints = false
  226. var leadingAnchor = container.leadingAnchor
  227. var iconSpacing: CGFloat = 0
  228. if icon != .none {
  229. let titleIcon: NSView
  230. switch icon {
  231. case .sparkle:
  232. titleIcon = SparkleIconView()
  233. case .grid:
  234. let gridIcon = NSImageView()
  235. if let image = NSImage(systemSymbolName: "square.grid.2x2", accessibilityDescription: nil) {
  236. let config = NSImage.SymbolConfiguration(pointSize: 16, weight: .medium)
  237. gridIcon.image = image.withSymbolConfiguration(config)
  238. }
  239. gridIcon.contentTintColor = AppTheme.textPrimary
  240. titleIcon = gridIcon
  241. case .none:
  242. titleIcon = NSView()
  243. }
  244. titleIcon.translatesAutoresizingMaskIntoConstraints = false
  245. container.addSubview(titleIcon)
  246. NSLayoutConstraint.activate([
  247. titleIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  248. titleIcon.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  249. titleIcon.widthAnchor.constraint(equalToConstant: 24),
  250. titleIcon.heightAnchor.constraint(equalToConstant: 24),
  251. ])
  252. leadingAnchor = titleIcon.trailingAnchor
  253. iconSpacing = 8
  254. }
  255. let label = NSTextField(labelWithString: text)
  256. label.font = .systemFont(ofSize: 20, weight: .bold)
  257. label.textColor = AppTheme.textPrimary
  258. label.translatesAutoresizingMaskIntoConstraints = false
  259. container.addSubview(label)
  260. NSLayoutConstraint.activate([
  261. container.heightAnchor.constraint(equalToConstant: 28),
  262. label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: iconSpacing),
  263. label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  264. label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  265. ])
  266. return container
  267. }
  268. private func makeQuickStartSection() -> NSView {
  269. let section = NSView()
  270. section.translatesAutoresizingMaskIntoConstraints = false
  271. let title = makeSectionTitle("Quick Start", icon: .sparkle)
  272. section.addSubview(title)
  273. let cardsStack = NSStackView()
  274. cardsStack.orientation = .horizontal
  275. cardsStack.spacing = AppTheme.quickStartSpacing
  276. cardsStack.distribution = .fillEqually
  277. cardsStack.translatesAutoresizingMaskIntoConstraints = false
  278. let cards: [QuickStartCardData] = [
  279. QuickStartCardData(
  280. title: "From Photos",
  281. subtitle: "Take a photo from gallery",
  282. buttonTitle: "Open Gallery",
  283. accentColor: AppTheme.blue,
  284. gradientColors: [AppTheme.blueLight, NSColor(red: 0.82, green: 0.90, blue: 1.0, alpha: 1)],
  285. iconKind: .photos
  286. ),
  287. QuickStartCardData(
  288. title: "From Files",
  289. subtitle: "Take a photo from file manager",
  290. buttonTitle: "Browse Files",
  291. accentColor: AppTheme.green,
  292. gradientColors: [AppTheme.greenLight, NSColor(red: 0.78, green: 0.94, blue: 0.84, alpha: 1)],
  293. iconKind: .files
  294. ),
  295. QuickStartCardData(
  296. title: "Import File",
  297. subtitle: "Import a file from storage",
  298. buttonTitle: "Import Now",
  299. accentColor: AppTheme.orange,
  300. gradientColors: [AppTheme.orangeLight, NSColor(red: 1.0, green: 0.88, blue: 0.76, alpha: 1)],
  301. iconKind: .importFile
  302. ),
  303. ]
  304. for cardData in cards {
  305. cardsStack.addArrangedSubview(QuickStartCardView(data: cardData))
  306. }
  307. section.addSubview(cardsStack)
  308. NSLayoutConstraint.activate([
  309. title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  310. title.topAnchor.constraint(equalTo: section.topAnchor),
  311. cardsStack.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  312. cardsStack.trailingAnchor.constraint(equalTo: section.trailingAnchor),
  313. cardsStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
  314. cardsStack.bottomAnchor.constraint(equalTo: section.bottomAnchor),
  315. ])
  316. return section
  317. }
  318. private func makeCreatePrintSection() -> NSView {
  319. let section = NSView()
  320. section.translatesAutoresizingMaskIntoConstraints = false
  321. let title = makeSectionTitle("Create & Print", icon: .grid)
  322. section.addSubview(title)
  323. let features: [FeatureCardData] = [
  324. FeatureCardData(title: "Scan File", subtitle: "Scan any document", iconKind: .scanFile),
  325. FeatureCardData(title: "Print Text", subtitle: "Type text and print", iconKind: .printText),
  326. FeatureCardData(title: "Print Contacts", subtitle: "Print your contacts", iconKind: .printContacts),
  327. FeatureCardData(title: "Print Website", subtitle: "Print any website", iconKind: .printWebsite),
  328. FeatureCardData(title: "Draw & Print", subtitle: "Add drawings, text and more", iconKind: .drawPrint),
  329. FeatureCardData(title: "OCR File", subtitle: "Scan and print text from images", iconKind: .ocrFile),
  330. ]
  331. let topRow = makeFeatureRow(features: Array(features.prefix(4)))
  332. let bottomRow = makeFeatureRow(features: Array(features.suffix(2)))
  333. let grid = NSStackView(views: [topRow, bottomRow])
  334. grid.orientation = .vertical
  335. grid.spacing = AppTheme.featureGridSpacing
  336. grid.distribution = .fillEqually
  337. grid.translatesAutoresizingMaskIntoConstraints = false
  338. section.addSubview(grid)
  339. NSLayoutConstraint.activate([
  340. title.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  341. title.topAnchor.constraint(equalTo: section.topAnchor),
  342. grid.leadingAnchor.constraint(equalTo: section.leadingAnchor),
  343. grid.trailingAnchor.constraint(equalTo: section.trailingAnchor),
  344. grid.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
  345. grid.bottomAnchor.constraint(equalTo: section.bottomAnchor),
  346. ])
  347. return section
  348. }
  349. private func makeFeatureRow(features: [FeatureCardData]) -> NSStackView {
  350. let row = NSStackView()
  351. row.orientation = .horizontal
  352. row.spacing = AppTheme.featureGridSpacing
  353. row.distribution = .fillEqually
  354. row.translatesAutoresizingMaskIntoConstraints = false
  355. for data in features {
  356. row.addArrangedSubview(FeatureCardView(data: data))
  357. }
  358. return row
  359. }
  360. }
  361. private final class FlippedDocumentView: NSView {
  362. override var isFlipped: Bool { true }
  363. }