SidebarView.swift 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import Cocoa
  2. enum SidebarDestination: Int, CaseIterable {
  3. case home
  4. case scan
  5. case print
  6. case scanAndHome
  7. case settings
  8. }
  9. final class SidebarView: NSView, AppearanceRefreshable {
  10. private var appNameLabel: NSTextField!
  11. private var logoIcon: NSImageView!
  12. private var premiumCard: SidebarPremiumCardView!
  13. private var freeUsesBanner: FreeUsesBannerView!
  14. var onDestinationSelected: ((SidebarDestination) -> Void)?
  15. var onPremiumUpgradeTapped: (() -> Void)?
  16. var onManageSubscriptionTapped: (() -> Void)?
  17. private var navItems: [SidebarNavItem] = []
  18. init() {
  19. super.init(frame: .zero)
  20. wantsLayer = true
  21. layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
  22. translatesAutoresizingMaskIntoConstraints = false
  23. setup()
  24. }
  25. @available(*, unavailable)
  26. required init?(coder: NSCoder) { nil }
  27. func select(_ destination: SidebarDestination) {
  28. for (index, item) in navItems.enumerated() {
  29. item.isSelected = navDestination(at: index) == destination
  30. }
  31. }
  32. func refreshPremiumState() {
  33. premiumCard?.refreshFromStore()
  34. freeUsesBanner?.refreshFromStore()
  35. }
  36. func refreshAppearance() {
  37. layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
  38. appNameLabel?.refreshThemeLabelColor()
  39. logoIcon?.image = nil
  40. logoIcon?.image = NSImage(named: "AppLogo")
  41. navItems.forEach { $0.refreshAppearance() }
  42. freeUsesBanner?.refreshAppearance()
  43. premiumCard?.refreshAppearance()
  44. }
  45. private func navDestination(at index: Int) -> SidebarDestination {
  46. switch index {
  47. case 0: return .home
  48. case 1: return .scan
  49. case 2: return .print
  50. case 3: return .settings
  51. default: return .home
  52. }
  53. }
  54. private func setup() {
  55. let logoContainer = NSView()
  56. logoContainer.translatesAutoresizingMaskIntoConstraints = false
  57. let logoIcon = NSImageView()
  58. self.logoIcon = logoIcon
  59. logoIcon.translatesAutoresizingMaskIntoConstraints = false
  60. logoIcon.image = NSImage(named: "AppLogo")
  61. logoIcon.imageScaling = .scaleProportionallyUpOrDown
  62. appNameLabel = NSTextField.themeLabel(AppStoreConfig.displayName, style: .primary, font: AppTheme.semiboldFont(size: 15))
  63. appNameLabel.alignment = .center
  64. appNameLabel.translatesAutoresizingMaskIntoConstraints = false
  65. let navStack = NSStackView()
  66. navStack.orientation = .vertical
  67. navStack.spacing = 4
  68. navStack.alignment = .leading
  69. navStack.translatesAutoresizingMaskIntoConstraints = false
  70. let bottomNavStack = NSStackView()
  71. bottomNavStack.orientation = .vertical
  72. bottomNavStack.spacing = 12
  73. bottomNavStack.alignment = .leading
  74. bottomNavStack.translatesAutoresizingMaskIntoConstraints = false
  75. let homeItem = SidebarNavItem(title: "Home", symbolName: "house.fill", isSelected: true)
  76. let scanItem = SidebarNavItem(title: "Scan", symbolName: "viewfinder")
  77. let printItem = SidebarNavItem(title: "Print", symbolName: "printer")
  78. let settingsItem = SidebarNavItem(title: "Settings", symbolName: "gearshape")
  79. premiumCard = SidebarPremiumCardView()
  80. premiumCard.onUpgradeTapped = { [weak self] in
  81. self?.onPremiumUpgradeTapped?()
  82. }
  83. premiumCard.onManageSubscriptionTapped = { [weak self] in
  84. self?.onManageSubscriptionTapped?()
  85. }
  86. freeUsesBanner = FreeUsesBannerView()
  87. navItems = [homeItem, scanItem, printItem, settingsItem]
  88. for (index, item) in navItems.enumerated() {
  89. item.onClick = { [weak self] in
  90. guard let self else { return }
  91. let destination = self.navDestination(at: index)
  92. self.select(destination)
  93. self.onDestinationSelected?(destination)
  94. }
  95. }
  96. for item in navItems {
  97. navStack.addArrangedSubview(item)
  98. }
  99. bottomNavStack.addArrangedSubview(freeUsesBanner)
  100. bottomNavStack.addArrangedSubview(premiumCard)
  101. addSubview(logoContainer)
  102. logoContainer.addSubview(logoIcon)
  103. addSubview(appNameLabel)
  104. addSubview(navStack)
  105. addSubview(bottomNavStack)
  106. let divider = NSBox()
  107. divider.boxType = .separator
  108. divider.translatesAutoresizingMaskIntoConstraints = false
  109. addSubview(divider)
  110. NSLayoutConstraint.activate([
  111. widthAnchor.constraint(equalToConstant: AppTheme.sidebarWidth),
  112. logoContainer.topAnchor.constraint(equalTo: topAnchor, constant: 52),
  113. logoContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
  114. logoContainer.widthAnchor.constraint(equalToConstant: 56),
  115. logoContainer.heightAnchor.constraint(equalToConstant: 56),
  116. logoIcon.centerXAnchor.constraint(equalTo: logoContainer.centerXAnchor),
  117. logoIcon.centerYAnchor.constraint(equalTo: logoContainer.centerYAnchor),
  118. logoIcon.widthAnchor.constraint(equalToConstant: 48),
  119. logoIcon.heightAnchor.constraint(equalToConstant: 48),
  120. appNameLabel.topAnchor.constraint(equalTo: logoContainer.bottomAnchor, constant: 8),
  121. appNameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
  122. navStack.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: 32),
  123. navStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
  124. navStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
  125. bottomNavStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
  126. bottomNavStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
  127. bottomNavStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
  128. homeItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
  129. scanItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
  130. printItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
  131. settingsItem.widthAnchor.constraint(equalTo: navStack.widthAnchor),
  132. freeUsesBanner.widthAnchor.constraint(equalTo: bottomNavStack.widthAnchor),
  133. premiumCard.widthAnchor.constraint(equalTo: bottomNavStack.widthAnchor),
  134. divider.trailingAnchor.constraint(equalTo: trailingAnchor),
  135. divider.topAnchor.constraint(equalTo: topAnchor),
  136. divider.bottomAnchor.constraint(equalTo: bottomAnchor),
  137. divider.widthAnchor.constraint(equalToConstant: 1),
  138. ])
  139. }
  140. }