LaunchSplashView.swift 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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")
  18. private let loadingTrackView = NSView()
  19. private let loadingFillView = NSView()
  20. private let statusLabel = NSTextField(labelWithString: "CONNECTING SERVICES")
  21. private let percentLabel = NSTextField(labelWithString: "0%")
  22. private let taglineLabel = NSTextField(labelWithString: "Plan smart, achieve more")
  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 = "100%"
  46. statusLabel.stringValue = "READY"
  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. iconView.translatesAutoresizingMaskIntoConstraints = false
  74. iconView.imageScaling = .scaleProportionallyUpOrDown
  75. iconView.wantsLayer = true
  76. iconView.layer?.cornerRadius = 20
  77. iconView.layer?.masksToBounds = true
  78. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  79. titleLabel.alignment = .center
  80. titleLabel.font = NSFont.systemFont(ofSize: 42, weight: .bold)
  81. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  82. subtitleLabel.alignment = .center
  83. subtitleLabel.font = NSFont.systemFont(ofSize: 17, weight: .medium)
  84. loadingTrackView.translatesAutoresizingMaskIntoConstraints = false
  85. loadingFillView.translatesAutoresizingMaskIntoConstraints = false
  86. statusLabel.translatesAutoresizingMaskIntoConstraints = false
  87. statusLabel.alignment = .left
  88. statusLabel.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .semibold)
  89. percentLabel.translatesAutoresizingMaskIntoConstraints = false
  90. percentLabel.alignment = .right
  91. percentLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .semibold)
  92. taglineLabel.translatesAutoresizingMaskIntoConstraints = false
  93. taglineLabel.alignment = .center
  94. taglineLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
  95. addSubview(iconView)
  96. addSubview(titleLabel)
  97. addSubview(subtitleLabel)
  98. addSubview(loadingTrackView)
  99. loadingTrackView.addSubview(loadingFillView)
  100. addSubview(statusLabel)
  101. addSubview(percentLabel)
  102. addSubview(taglineLabel)
  103. let fillWidthConstraint = loadingFillView.widthAnchor.constraint(equalToConstant: 0)
  104. loadingFillWidthConstraint = fillWidthConstraint
  105. NSLayoutConstraint.activate([
  106. iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
  107. iconView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -138),
  108. iconView.widthAnchor.constraint(equalToConstant: 104),
  109. iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor),
  110. titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 22),
  111. titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
  112. titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
  113. trailingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 24),
  114. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 14),
  115. subtitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
  116. subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
  117. trailingAnchor.constraint(greaterThanOrEqualTo: subtitleLabel.trailingAnchor, constant: 24),
  118. loadingTrackView.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 30),
  119. loadingTrackView.centerXAnchor.constraint(equalTo: centerXAnchor),
  120. loadingTrackView.widthAnchor.constraint(equalToConstant: 320),
  121. loadingTrackView.heightAnchor.constraint(equalToConstant: 5),
  122. loadingFillView.leadingAnchor.constraint(equalTo: loadingTrackView.leadingAnchor),
  123. loadingFillView.topAnchor.constraint(equalTo: loadingTrackView.topAnchor),
  124. loadingFillView.bottomAnchor.constraint(equalTo: loadingTrackView.bottomAnchor),
  125. fillWidthConstraint,
  126. statusLabel.topAnchor.constraint(equalTo: loadingTrackView.bottomAnchor, constant: 8),
  127. statusLabel.leadingAnchor.constraint(equalTo: loadingTrackView.leadingAnchor),
  128. percentLabel.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor),
  129. percentLabel.trailingAnchor.constraint(equalTo: loadingTrackView.trailingAnchor),
  130. taglineLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 26),
  131. taglineLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
  132. ])
  133. }
  134. private func startAnimations() {
  135. stopAnimations()
  136. textAnimationFrame = 0
  137. progressValue = 0
  138. updateLoadingFill(for: progressValue)
  139. percentLabel.stringValue = "0%"
  140. textAnimationTimer = Timer.scheduledTimer(withTimeInterval: 0.42, repeats: true) { [weak self] _ in
  141. self?.updateAnimatedText()
  142. }
  143. progressAnimationTimer = Timer.scheduledTimer(withTimeInterval: 0.045, repeats: true) { [weak self] _ in
  144. self?.advanceLoadingBar()
  145. }
  146. if let textAnimationTimer {
  147. RunLoop.main.add(textAnimationTimer, forMode: .common)
  148. }
  149. if let progressAnimationTimer {
  150. RunLoop.main.add(progressAnimationTimer, forMode: .common)
  151. }
  152. }
  153. private func stopAnimations() {
  154. textAnimationTimer?.invalidate()
  155. textAnimationTimer = nil
  156. progressAnimationTimer?.invalidate()
  157. progressAnimationTimer = nil
  158. }
  159. private func updateAnimatedText() {
  160. textAnimationFrame = (textAnimationFrame + 1) % 4
  161. subtitleLabel.stringValue = "Preparing your meetings workspace" + String(repeating: ".", count: textAnimationFrame)
  162. }
  163. private func advanceLoadingBar() {
  164. let next = min(progressValue + 0.8, 100)
  165. progressValue = next
  166. updateLoadingFill(for: next)
  167. percentLabel.stringValue = "\(Int(next.rounded()))%"
  168. }
  169. private func updateLoadingFill(for percent: Double) {
  170. let clamped = max(0, min(percent, 100))
  171. let totalWidth: CGFloat = 320
  172. loadingFillWidthConstraint?.constant = totalWidth * CGFloat(clamped / 100)
  173. }
  174. override func viewWillMove(toSuperview newSuperview: NSView?) {
  175. if newSuperview == nil {
  176. stopAnimations()
  177. }
  178. super.viewWillMove(toSuperview: newSuperview)
  179. }
  180. }