LaunchSplashView.swift 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. //
  2. // LaunchSplashView.swift
  3. // AI Companion for Meet
  4. //
  5. import Cocoa
  6. final class LaunchSplashView: NSView {
  7. struct Theme {
  8. let background: NSColor
  9. let cardBackground: NSColor
  10. let cardBorder: NSColor
  11. let titleText: NSColor
  12. let subtitleText: NSColor
  13. let accent: NSColor
  14. }
  15. private let iconView = NSImageView()
  16. private let titleLabel = NSTextField(labelWithString: "")
  17. private let subtitleLabel = NSTextField(labelWithString: "Preparing your meetings workspace".localized)
  18. private let loadingTrackView = NSView()
  19. private let loadingFillView = NSView()
  20. private let statusLabel = NSTextField(labelWithString: "CONNECTING SERVICES".localized)
  21. private let percentLabel = NSTextField(labelWithString: "%d%%".localized(with: 0))
  22. private let taglineLabel = NSTextField(labelWithString: "Plan smart, achieve more".localized)
  23. private var loadingFillWidthConstraint: NSLayoutConstraint?
  24. private var textAnimationTimer: Timer?
  25. private var progressAnimationTimer: Timer?
  26. private var textAnimationFrame = 0
  27. private var progressValue: Double = 0
  28. override init(frame frameRect: NSRect) {
  29. super.init(frame: frameRect)
  30. setupView()
  31. }
  32. required init?(coder: NSCoder) {
  33. super.init(coder: coder)
  34. setupView()
  35. }
  36. func configure(appName: String, appIcon: NSImage?, theme: Theme) {
  37. titleLabel.stringValue = appName
  38. iconView.image = appIcon
  39. apply(theme: theme)
  40. startAnimations()
  41. }
  42. func completeLoading(duration: TimeInterval = 0.22, completion: (() -> Void)? = nil) {
  43. stopAnimations()
  44. progressValue = 100
  45. percentLabel.stringValue = "%d%%".localized(with: 100)
  46. statusLabel.stringValue = "READY".localized
  47. updateLoadingFill(for: 100)
  48. layoutSubtreeIfNeeded()
  49. // Keep the fully filled bar visible briefly before dismiss.
  50. DispatchQueue.main.asyncAfter(deadline: .now() + max(0, duration)) {
  51. completion?()
  52. }
  53. }
  54. func apply(theme: Theme) {
  55. wantsLayer = true
  56. layer?.backgroundColor = theme.background.cgColor
  57. titleLabel.textColor = theme.titleText
  58. subtitleLabel.textColor = theme.subtitleText
  59. statusLabel.textColor = theme.subtitleText.withAlphaComponent(0.9)
  60. percentLabel.textColor = theme.subtitleText.withAlphaComponent(0.9)
  61. taglineLabel.textColor = theme.subtitleText.withAlphaComponent(0.85)
  62. loadingTrackView.wantsLayer = true
  63. loadingTrackView.layer?.cornerRadius = 2.5
  64. loadingTrackView.layer?.masksToBounds = true
  65. loadingTrackView.layer?.backgroundColor = theme.cardBorder.withAlphaComponent(0.3).cgColor
  66. loadingFillView.wantsLayer = true
  67. loadingFillView.layer?.cornerRadius = 2.5
  68. loadingFillView.layer?.masksToBounds = true
  69. loadingFillView.layer?.backgroundColor = theme.accent.cgColor
  70. }
  71. private func setupView() {
  72. translatesAutoresizingMaskIntoConstraints = false
  73. AppLanguageManager.applyLayoutDirection(to: self)
  74. iconView.translatesAutoresizingMaskIntoConstraints = false
  75. iconView.imageScaling = .scaleProportionallyUpOrDown
  76. iconView.wantsLayer = true
  77. iconView.layer?.cornerRadius = 20
  78. iconView.layer?.masksToBounds = true
  79. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  80. titleLabel.alignment = .center
  81. titleLabel.font = NSFont.systemFont(ofSize: 42, weight: .bold)
  82. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  83. subtitleLabel.alignment = .center
  84. subtitleLabel.font = NSFont.systemFont(ofSize: 17, weight: .medium)
  85. loadingTrackView.translatesAutoresizingMaskIntoConstraints = false
  86. loadingFillView.translatesAutoresizingMaskIntoConstraints = false
  87. statusLabel.translatesAutoresizingMaskIntoConstraints = false
  88. statusLabel.alignment = AppLanguageManager.textAlignment
  89. statusLabel.baseWritingDirection = AppLanguageManager.baseWritingDirection
  90. statusLabel.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .semibold)
  91. percentLabel.translatesAutoresizingMaskIntoConstraints = false
  92. percentLabel.alignment = AppLanguageManager.trailingEdgeTextAlignment
  93. percentLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .semibold)
  94. taglineLabel.translatesAutoresizingMaskIntoConstraints = false
  95. taglineLabel.alignment = .center
  96. taglineLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
  97. addSubview(iconView)
  98. addSubview(titleLabel)
  99. addSubview(subtitleLabel)
  100. addSubview(loadingTrackView)
  101. loadingTrackView.addSubview(loadingFillView)
  102. addSubview(statusLabel)
  103. addSubview(percentLabel)
  104. addSubview(taglineLabel)
  105. let fillWidthConstraint = loadingFillView.widthAnchor.constraint(equalToConstant: 0)
  106. loadingFillWidthConstraint = fillWidthConstraint
  107. NSLayoutConstraint.activate([
  108. iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
  109. iconView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -138),
  110. iconView.widthAnchor.constraint(equalToConstant: 104),
  111. iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor),
  112. titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 22),
  113. titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
  114. titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
  115. trailingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 24),
  116. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 14),
  117. subtitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
  118. subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
  119. trailingAnchor.constraint(greaterThanOrEqualTo: subtitleLabel.trailingAnchor, constant: 24),
  120. loadingTrackView.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 30),
  121. loadingTrackView.centerXAnchor.constraint(equalTo: centerXAnchor),
  122. loadingTrackView.widthAnchor.constraint(equalToConstant: 320),
  123. loadingTrackView.heightAnchor.constraint(equalToConstant: 5),
  124. loadingFillView.leadingAnchor.constraint(equalTo: loadingTrackView.leadingAnchor),
  125. loadingFillView.topAnchor.constraint(equalTo: loadingTrackView.topAnchor),
  126. loadingFillView.bottomAnchor.constraint(equalTo: loadingTrackView.bottomAnchor),
  127. fillWidthConstraint,
  128. statusLabel.topAnchor.constraint(equalTo: loadingTrackView.bottomAnchor, constant: 8),
  129. statusLabel.leadingAnchor.constraint(equalTo: loadingTrackView.leadingAnchor),
  130. percentLabel.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor),
  131. percentLabel.trailingAnchor.constraint(equalTo: loadingTrackView.trailingAnchor),
  132. taglineLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 26),
  133. taglineLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
  134. ])
  135. }
  136. private func startAnimations() {
  137. stopAnimations()
  138. textAnimationFrame = 0
  139. progressValue = 0
  140. updateLoadingFill(for: progressValue)
  141. percentLabel.stringValue = "%d%%".localized(with: 0)
  142. textAnimationTimer = Timer.scheduledTimer(withTimeInterval: 0.42, repeats: true) { [weak self] _ in
  143. self?.updateAnimatedText()
  144. }
  145. progressAnimationTimer = Timer.scheduledTimer(withTimeInterval: 0.045, repeats: true) { [weak self] _ in
  146. self?.advanceLoadingBar()
  147. }
  148. if let textAnimationTimer {
  149. RunLoop.main.add(textAnimationTimer, forMode: .common)
  150. }
  151. if let progressAnimationTimer {
  152. RunLoop.main.add(progressAnimationTimer, forMode: .common)
  153. }
  154. }
  155. private func stopAnimations() {
  156. textAnimationTimer?.invalidate()
  157. textAnimationTimer = nil
  158. progressAnimationTimer?.invalidate()
  159. progressAnimationTimer = nil
  160. }
  161. private func updateAnimatedText() {
  162. textAnimationFrame = (textAnimationFrame + 1) % 4
  163. subtitleLabel.stringValue = "Preparing your meetings workspace".localized + String(repeating: ".", count: textAnimationFrame)
  164. }
  165. private func advanceLoadingBar() {
  166. let next = min(progressValue + 0.8, 100)
  167. progressValue = next
  168. updateLoadingFill(for: next)
  169. percentLabel.stringValue = "%d%%".localized(with: Int(next.rounded()))
  170. }
  171. private func updateLoadingFill(for percent: Double) {
  172. let clamped = max(0, min(percent, 100))
  173. let totalWidth: CGFloat = 320
  174. loadingFillWidthConstraint?.constant = totalWidth * CGFloat(clamped / 100)
  175. }
  176. override func viewWillMove(toSuperview newSuperview: NSView?) {
  177. if newSuperview == nil {
  178. stopAnimations()
  179. }
  180. super.viewWillMove(toSuperview: newSuperview)
  181. }
  182. }