|
|
@@ -271,11 +271,12 @@ private final class SplashViewController: NSViewController {
|
|
|
let isDark: Bool
|
|
|
let backgroundTop: NSColor
|
|
|
let backgroundBottom: NSColor
|
|
|
- let cardFill: NSColor
|
|
|
let titleColor: NSColor
|
|
|
let subtitleColor: NSColor
|
|
|
+ let statusColor: NSColor
|
|
|
+ let trackColor: NSColor
|
|
|
+ let fillColor: NSColor
|
|
|
let iconBadgeFill: NSColor
|
|
|
- let iconBadgeStroke: NSColor
|
|
|
}
|
|
|
|
|
|
private final class GradientBackgroundView: NSView {
|
|
|
@@ -303,14 +304,18 @@ private final class SplashViewController: NSViewController {
|
|
|
private var theme: Theme = SplashViewController.makeTheme(isDark: true)
|
|
|
private var dotsTimer: Timer?
|
|
|
private var dotsStep: Int = 0
|
|
|
+ private var progressTimer: Timer?
|
|
|
+ private var progressValue: Double = 0
|
|
|
|
|
|
private weak var rootBackground: GradientBackgroundView?
|
|
|
- private weak var cardEffectView: NSVisualEffectView?
|
|
|
private weak var iconBadgeView: NSView?
|
|
|
private weak var iconView: NSImageView?
|
|
|
private weak var titleLabel: NSTextField?
|
|
|
private weak var subtitleLabel: NSTextField?
|
|
|
private weak var contentStack: NSStackView?
|
|
|
+ private weak var statusLabel: NSTextField?
|
|
|
+ private weak var percentLabel: NSTextField?
|
|
|
+ private weak var progressIndicator: NSProgressIndicator?
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
super.viewDidLoad()
|
|
|
@@ -321,54 +326,32 @@ private final class SplashViewController: NSViewController {
|
|
|
super.viewDidAppear()
|
|
|
startAnimations()
|
|
|
startDots()
|
|
|
+ startProgress()
|
|
|
}
|
|
|
|
|
|
override func viewWillDisappear() {
|
|
|
super.viewWillDisappear()
|
|
|
dotsTimer?.invalidate()
|
|
|
dotsTimer = nil
|
|
|
+ progressTimer?.invalidate()
|
|
|
+ progressTimer = nil
|
|
|
}
|
|
|
|
|
|
override func loadView() {
|
|
|
let background = GradientBackgroundView()
|
|
|
|
|
|
- let card = NSVisualEffectView()
|
|
|
- card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- card.blendingMode = .withinWindow
|
|
|
- card.state = .active
|
|
|
- card.wantsLayer = true
|
|
|
- card.layer?.cornerRadius = 22
|
|
|
- card.layer?.masksToBounds = true
|
|
|
-
|
|
|
- // Subtle border similar to the app's card styling.
|
|
|
- let border = CALayer()
|
|
|
- border.borderWidth = 1
|
|
|
- border.cornerRadius = 22
|
|
|
- border.masksToBounds = true
|
|
|
- card.layer?.addSublayer(border)
|
|
|
- border.frame = card.bounds
|
|
|
- border.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
|
|
|
-
|
|
|
let stack = NSStackView()
|
|
|
stack.orientation = .vertical
|
|
|
stack.alignment = .centerX
|
|
|
- stack.spacing = 12
|
|
|
+ stack.spacing = 10
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
let iconBadge = NSView()
|
|
|
iconBadge.translatesAutoresizingMaskIntoConstraints = false
|
|
|
iconBadge.wantsLayer = true
|
|
|
- iconBadge.layer?.cornerRadius = 22
|
|
|
+ iconBadge.layer?.cornerRadius = 26
|
|
|
iconBadge.layer?.masksToBounds = true
|
|
|
|
|
|
- let iconBadgeBorder = CALayer()
|
|
|
- iconBadgeBorder.borderWidth = 1
|
|
|
- iconBadgeBorder.cornerRadius = 22
|
|
|
- iconBadgeBorder.masksToBounds = true
|
|
|
- iconBadge.layer?.addSublayer(iconBadgeBorder)
|
|
|
- iconBadgeBorder.frame = iconBadge.bounds
|
|
|
- iconBadgeBorder.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
|
|
|
-
|
|
|
let iconView = NSImageView()
|
|
|
iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
iconView.imageScaling = .scaleProportionallyUpOrDown
|
|
|
@@ -379,40 +362,67 @@ private final class SplashViewController: NSViewController {
|
|
|
let title = NSTextField(labelWithString: appName)
|
|
|
title.font = .systemFont(ofSize: 30, weight: .semibold)
|
|
|
|
|
|
- let subtitle = NSTextField(labelWithString: "Starting up…")
|
|
|
+ let subtitle = NSTextField(labelWithString: "Preparing your meetings workspace.")
|
|
|
subtitle.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
subtitle.alignment = .center
|
|
|
+ subtitle.maximumNumberOfLines = 2
|
|
|
+ subtitle.lineBreakMode = .byWordWrapping
|
|
|
+
|
|
|
+ let progressRow = NSStackView()
|
|
|
+ progressRow.orientation = .horizontal
|
|
|
+ progressRow.alignment = .centerY
|
|
|
+ progressRow.spacing = 10
|
|
|
+ progressRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let progress = NSProgressIndicator()
|
|
|
+ progress.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ progress.isIndeterminate = false
|
|
|
+ progress.minValue = 0
|
|
|
+ progress.maxValue = 100
|
|
|
+ progress.doubleValue = 10
|
|
|
+ progress.controlSize = .small
|
|
|
+ progress.style = .bar
|
|
|
+
|
|
|
+ let percent = NSTextField(labelWithString: "10%")
|
|
|
+ percent.font = .systemFont(ofSize: 12, weight: .medium)
|
|
|
+ percent.alignment = .right
|
|
|
+
|
|
|
+ progressRow.addArrangedSubview(progress)
|
|
|
+ progressRow.addArrangedSubview(percent)
|
|
|
+
|
|
|
+ let status = NSTextField(labelWithString: "CONNECTING SERVICES")
|
|
|
+ status.font = .systemFont(ofSize: 10.5, weight: .semibold)
|
|
|
+ status.alignment = .center
|
|
|
+ status.usesSingleLineMode = true
|
|
|
+ status.setAccessibilityLabel("Splash status")
|
|
|
|
|
|
iconBadge.addSubview(iconView)
|
|
|
NSLayoutConstraint.activate([
|
|
|
iconView.centerXAnchor.constraint(equalTo: iconBadge.centerXAnchor),
|
|
|
iconView.centerYAnchor.constraint(equalTo: iconBadge.centerYAnchor),
|
|
|
- iconView.widthAnchor.constraint(equalToConstant: 70),
|
|
|
+ iconView.widthAnchor.constraint(equalToConstant: 78),
|
|
|
iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor)
|
|
|
])
|
|
|
|
|
|
stack.addArrangedSubview(iconBadge)
|
|
|
stack.addArrangedSubview(title)
|
|
|
stack.addArrangedSubview(subtitle)
|
|
|
-
|
|
|
- card.addSubview(stack)
|
|
|
- background.addSubview(card)
|
|
|
+ stack.addArrangedSubview(progressRow)
|
|
|
+ stack.addArrangedSubview(status)
|
|
|
+ background.addSubview(stack)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- iconBadge.widthAnchor.constraint(equalToConstant: 120),
|
|
|
- iconBadge.heightAnchor.constraint(equalToConstant: 120),
|
|
|
-
|
|
|
- card.centerXAnchor.constraint(equalTo: background.centerXAnchor),
|
|
|
- card.centerYAnchor.constraint(equalTo: background.centerYAnchor),
|
|
|
- card.widthAnchor.constraint(greaterThanOrEqualToConstant: 360),
|
|
|
- card.leadingAnchor.constraint(greaterThanOrEqualTo: background.leadingAnchor, constant: 28),
|
|
|
- card.trailingAnchor.constraint(lessThanOrEqualTo: background.trailingAnchor, constant: -28),
|
|
|
- card.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
|
|
|
-
|
|
|
- stack.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
- stack.centerYAnchor.constraint(equalTo: card.centerYAnchor),
|
|
|
- stack.leadingAnchor.constraint(greaterThanOrEqualTo: card.leadingAnchor, constant: 30),
|
|
|
- stack.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -30)
|
|
|
+ iconBadge.widthAnchor.constraint(equalToConstant: 104),
|
|
|
+ iconBadge.heightAnchor.constraint(equalToConstant: 104),
|
|
|
+
|
|
|
+ stack.centerXAnchor.constraint(equalTo: background.centerXAnchor),
|
|
|
+ stack.centerYAnchor.constraint(equalTo: background.centerYAnchor),
|
|
|
+ stack.leadingAnchor.constraint(greaterThanOrEqualTo: background.leadingAnchor, constant: 28),
|
|
|
+ stack.trailingAnchor.constraint(lessThanOrEqualTo: background.trailingAnchor, constant: -28),
|
|
|
+
|
|
|
+ progress.widthAnchor.constraint(equalToConstant: 260),
|
|
|
+ progress.heightAnchor.constraint(equalToConstant: 6),
|
|
|
+ percent.widthAnchor.constraint(equalToConstant: 44)
|
|
|
])
|
|
|
|
|
|
// Initial state for animation.
|
|
|
@@ -420,12 +430,14 @@ private final class SplashViewController: NSViewController {
|
|
|
stack.setFrameOrigin(NSPoint(x: stack.frame.origin.x, y: stack.frame.origin.y - 8))
|
|
|
|
|
|
self.rootBackground = background
|
|
|
- self.cardEffectView = card
|
|
|
self.iconBadgeView = iconBadge
|
|
|
self.iconView = iconView
|
|
|
self.titleLabel = title
|
|
|
self.subtitleLabel = subtitle
|
|
|
self.contentStack = stack
|
|
|
+ self.statusLabel = status
|
|
|
+ self.percentLabel = percent
|
|
|
+ self.progressIndicator = progress
|
|
|
|
|
|
view = background
|
|
|
}
|
|
|
@@ -470,48 +482,74 @@ private final class SplashViewController: NSViewController {
|
|
|
guard let self, let subtitleLabel else { return }
|
|
|
self.dotsStep = (self.dotsStep + 1) % 4
|
|
|
let dots = String(repeating: ".", count: self.dotsStep)
|
|
|
- subtitleLabel.stringValue = "Starting up" + dots
|
|
|
+ // Keep the main subtitle stable; add subtle activity via ellipsis.
|
|
|
+ subtitleLabel.stringValue = "Preparing your meetings workspace" + dots
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func startProgress() {
|
|
|
+ progressTimer?.invalidate()
|
|
|
+ progressValue = 10
|
|
|
+ setProgressUI(value: progressValue)
|
|
|
+
|
|
|
+ // Animate progress smoothly for the 3s splash duration.
|
|
|
+ let tick: TimeInterval = 0.06
|
|
|
+ let target: Double = 100
|
|
|
+ let duration: Double = 3.0
|
|
|
+ let step = (target - progressValue) / (duration / tick)
|
|
|
+
|
|
|
+ progressTimer = Timer.scheduledTimer(withTimeInterval: tick, repeats: true) { [weak self] timer in
|
|
|
+ guard let self else { timer.invalidate(); return }
|
|
|
+ self.progressValue = min(target, self.progressValue + step)
|
|
|
+ self.setProgressUI(value: self.progressValue)
|
|
|
+ if self.progressValue >= target - 0.01 {
|
|
|
+ timer.invalidate()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func setProgressUI(value: Double) {
|
|
|
+ progressIndicator?.doubleValue = value
|
|
|
+ percentLabel?.stringValue = "\(Int(round(value)))%"
|
|
|
+ }
|
|
|
+
|
|
|
private func applyTheme() {
|
|
|
let isDark = view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
|
|
theme = Self.makeTheme(isDark: isDark)
|
|
|
|
|
|
rootBackground?.gradientLayer.colors = [theme.backgroundTop.cgColor, theme.backgroundBottom.cgColor]
|
|
|
|
|
|
- // Card styling.
|
|
|
- if let card = cardEffectView {
|
|
|
- card.material = theme.isDark ? .hudWindow : .sidebar
|
|
|
- card.layer?.backgroundColor = theme.cardFill.cgColor
|
|
|
- if let border = card.layer?.sublayers?.first(where: { $0.borderWidth > 0 }) {
|
|
|
- border.borderColor = (theme.isDark ? NSColor.white.withAlphaComponent(0.08) : NSColor.black.withAlphaComponent(0.10)).cgColor
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// Icon badge styling.
|
|
|
if let badge = iconBadgeView {
|
|
|
badge.layer?.backgroundColor = theme.iconBadgeFill.cgColor
|
|
|
- if let border = badge.layer?.sublayers?.first(where: { $0.borderWidth > 0 }) {
|
|
|
- border.borderColor = theme.iconBadgeStroke.cgColor
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
titleLabel?.textColor = theme.titleColor
|
|
|
subtitleLabel?.textColor = theme.subtitleColor
|
|
|
+ statusLabel?.textColor = theme.statusColor
|
|
|
+ percentLabel?.textColor = theme.subtitleColor
|
|
|
+
|
|
|
+ if let progress = progressIndicator {
|
|
|
+ progress.appearance = NSAppearance(named: theme.isDark ? .darkAqua : .aqua)
|
|
|
+ // Prefer tinting when available; fall back gracefully if not.
|
|
|
+ if #available(macOS 10.14, *) {
|
|
|
+ progress.contentFilters = []
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private static func makeTheme(isDark: Bool) -> Theme {
|
|
|
if isDark {
|
|
|
return Theme(
|
|
|
isDark: true,
|
|
|
- backgroundTop: NSColor(calibratedRed: 10 / 255, green: 11 / 255, blue: 12 / 255, alpha: 1),
|
|
|
- backgroundBottom: NSColor(calibratedRed: 18 / 255, green: 19 / 255, blue: 22 / 255, alpha: 1),
|
|
|
- cardFill: NSColor(calibratedRed: 16 / 255, green: 17 / 255, blue: 19 / 255, alpha: 0.72),
|
|
|
+ backgroundTop: NSColor(calibratedRed: 9 / 255, green: 9 / 255, blue: 10 / 255, alpha: 1),
|
|
|
+ backgroundBottom: NSColor(calibratedRed: 16 / 255, green: 16 / 255, blue: 18 / 255, alpha: 1),
|
|
|
titleColor: NSColor(calibratedWhite: 0.98, alpha: 1),
|
|
|
subtitleColor: NSColor(calibratedWhite: 0.78, alpha: 1),
|
|
|
- iconBadgeFill: NSColor.white.withAlphaComponent(0.06),
|
|
|
- iconBadgeStroke: NSColor.white.withAlphaComponent(0.10)
|
|
|
+ statusColor: NSColor(calibratedWhite: 0.66, alpha: 1),
|
|
|
+ trackColor: NSColor.white.withAlphaComponent(0.10),
|
|
|
+ fillColor: NSColor(calibratedRed: 96 / 255, green: 215 / 255, blue: 255 / 255, alpha: 1),
|
|
|
+ iconBadgeFill: NSColor.white.withAlphaComponent(0.08)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -519,11 +557,12 @@ private final class SplashViewController: NSViewController {
|
|
|
isDark: false,
|
|
|
backgroundTop: NSColor(calibratedWhite: 0.98, alpha: 1),
|
|
|
backgroundBottom: NSColor(calibratedWhite: 0.92, alpha: 1),
|
|
|
- cardFill: NSColor.white.withAlphaComponent(0.70),
|
|
|
titleColor: NSColor(calibratedWhite: 0.10, alpha: 1),
|
|
|
subtitleColor: NSColor(calibratedWhite: 0.34, alpha: 1),
|
|
|
- iconBadgeFill: NSColor.black.withAlphaComponent(0.04),
|
|
|
- iconBadgeStroke: NSColor.black.withAlphaComponent(0.08)
|
|
|
+ statusColor: NSColor(calibratedWhite: 0.42, alpha: 1),
|
|
|
+ trackColor: NSColor.black.withAlphaComponent(0.10),
|
|
|
+ fillColor: NSColor(calibratedRed: 0.18, green: 0.52, blue: 0.96, alpha: 1),
|
|
|
+ iconBadgeFill: NSColor.black.withAlphaComponent(0.04)
|
|
|
)
|
|
|
}
|
|
|
}
|