| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- //
- // LaunchSplashView.swift
- // AI Companion for Meet
- //
- import Cocoa
- final class LaunchSplashView: NSView {
- struct Theme {
- let background: NSColor
- let cardBackground: NSColor
- let cardBorder: NSColor
- let titleText: NSColor
- let subtitleText: NSColor
- let accent: NSColor
- }
- private let iconView = NSImageView()
- private let titleLabel = NSTextField(labelWithString: "")
- private let subtitleLabel = NSTextField(labelWithString: "Preparing your meetings workspace".localized)
- private let loadingTrackView = NSView()
- private let loadingFillView = NSView()
- private let statusLabel = NSTextField(labelWithString: "CONNECTING SERVICES".localized)
- private let percentLabel = NSTextField(labelWithString: "%d%%".localized(with: 0))
- private let taglineLabel = NSTextField(labelWithString: "Plan smart, achieve more".localized)
- private var loadingFillWidthConstraint: NSLayoutConstraint?
- private var textAnimationTimer: Timer?
- private var progressAnimationTimer: Timer?
- private var textAnimationFrame = 0
- private var progressValue: Double = 0
- override init(frame frameRect: NSRect) {
- super.init(frame: frameRect)
- setupView()
- }
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupView()
- }
- func configure(appName: String, appIcon: NSImage?, theme: Theme) {
- titleLabel.stringValue = appName
- iconView.image = appIcon
- apply(theme: theme)
- startAnimations()
- }
- func completeLoading(duration: TimeInterval = 0.22, completion: (() -> Void)? = nil) {
- stopAnimations()
- progressValue = 100
- percentLabel.stringValue = "%d%%".localized(with: 100)
- statusLabel.stringValue = "READY".localized
- updateLoadingFill(for: 100)
- layoutSubtreeIfNeeded()
- // Keep the fully filled bar visible briefly before dismiss.
- DispatchQueue.main.asyncAfter(deadline: .now() + max(0, duration)) {
- completion?()
- }
- }
- func apply(theme: Theme) {
- wantsLayer = true
- layer?.backgroundColor = theme.background.cgColor
- titleLabel.textColor = theme.titleText
- subtitleLabel.textColor = theme.subtitleText
- statusLabel.textColor = theme.subtitleText.withAlphaComponent(0.9)
- percentLabel.textColor = theme.subtitleText.withAlphaComponent(0.9)
- taglineLabel.textColor = theme.subtitleText.withAlphaComponent(0.85)
- loadingTrackView.wantsLayer = true
- loadingTrackView.layer?.cornerRadius = 2.5
- loadingTrackView.layer?.masksToBounds = true
- loadingTrackView.layer?.backgroundColor = theme.cardBorder.withAlphaComponent(0.3).cgColor
- loadingFillView.wantsLayer = true
- loadingFillView.layer?.cornerRadius = 2.5
- loadingFillView.layer?.masksToBounds = true
- loadingFillView.layer?.backgroundColor = theme.accent.cgColor
- }
- private func setupView() {
- translatesAutoresizingMaskIntoConstraints = false
- AppLanguageManager.applyLayoutDirection(to: self)
- iconView.translatesAutoresizingMaskIntoConstraints = false
- iconView.imageScaling = .scaleProportionallyUpOrDown
- iconView.wantsLayer = true
- iconView.layer?.cornerRadius = 20
- iconView.layer?.masksToBounds = true
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- titleLabel.alignment = .center
- titleLabel.font = NSFont.systemFont(ofSize: 42, weight: .bold)
- subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
- subtitleLabel.alignment = .center
- subtitleLabel.font = NSFont.systemFont(ofSize: 17, weight: .medium)
- loadingTrackView.translatesAutoresizingMaskIntoConstraints = false
- loadingFillView.translatesAutoresizingMaskIntoConstraints = false
- statusLabel.translatesAutoresizingMaskIntoConstraints = false
- statusLabel.alignment = AppLanguageManager.textAlignment
- statusLabel.baseWritingDirection = AppLanguageManager.baseWritingDirection
- statusLabel.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .semibold)
- percentLabel.translatesAutoresizingMaskIntoConstraints = false
- percentLabel.alignment = AppLanguageManager.trailingEdgeTextAlignment
- percentLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .semibold)
- taglineLabel.translatesAutoresizingMaskIntoConstraints = false
- taglineLabel.alignment = .center
- taglineLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
- addSubview(iconView)
- addSubview(titleLabel)
- addSubview(subtitleLabel)
- addSubview(loadingTrackView)
- loadingTrackView.addSubview(loadingFillView)
- addSubview(statusLabel)
- addSubview(percentLabel)
- addSubview(taglineLabel)
- let fillWidthConstraint = loadingFillView.widthAnchor.constraint(equalToConstant: 0)
- loadingFillWidthConstraint = fillWidthConstraint
- NSLayoutConstraint.activate([
- iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
- iconView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -138),
- iconView.widthAnchor.constraint(equalToConstant: 104),
- iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor),
- titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 22),
- titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
- titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
- trailingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 24),
- subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 14),
- subtitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
- subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 24),
- trailingAnchor.constraint(greaterThanOrEqualTo: subtitleLabel.trailingAnchor, constant: 24),
- loadingTrackView.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 30),
- loadingTrackView.centerXAnchor.constraint(equalTo: centerXAnchor),
- loadingTrackView.widthAnchor.constraint(equalToConstant: 320),
- loadingTrackView.heightAnchor.constraint(equalToConstant: 5),
- loadingFillView.leadingAnchor.constraint(equalTo: loadingTrackView.leadingAnchor),
- loadingFillView.topAnchor.constraint(equalTo: loadingTrackView.topAnchor),
- loadingFillView.bottomAnchor.constraint(equalTo: loadingTrackView.bottomAnchor),
- fillWidthConstraint,
- statusLabel.topAnchor.constraint(equalTo: loadingTrackView.bottomAnchor, constant: 8),
- statusLabel.leadingAnchor.constraint(equalTo: loadingTrackView.leadingAnchor),
- percentLabel.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor),
- percentLabel.trailingAnchor.constraint(equalTo: loadingTrackView.trailingAnchor),
- taglineLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 26),
- taglineLabel.centerXAnchor.constraint(equalTo: centerXAnchor)
- ])
- }
- private func startAnimations() {
- stopAnimations()
- textAnimationFrame = 0
- progressValue = 0
- updateLoadingFill(for: progressValue)
- percentLabel.stringValue = "%d%%".localized(with: 0)
- textAnimationTimer = Timer.scheduledTimer(withTimeInterval: 0.42, repeats: true) { [weak self] _ in
- self?.updateAnimatedText()
- }
- progressAnimationTimer = Timer.scheduledTimer(withTimeInterval: 0.045, repeats: true) { [weak self] _ in
- self?.advanceLoadingBar()
- }
- if let textAnimationTimer {
- RunLoop.main.add(textAnimationTimer, forMode: .common)
- }
- if let progressAnimationTimer {
- RunLoop.main.add(progressAnimationTimer, forMode: .common)
- }
- }
- private func stopAnimations() {
- textAnimationTimer?.invalidate()
- textAnimationTimer = nil
- progressAnimationTimer?.invalidate()
- progressAnimationTimer = nil
- }
- private func updateAnimatedText() {
- textAnimationFrame = (textAnimationFrame + 1) % 4
- subtitleLabel.stringValue = "Preparing your meetings workspace".localized + String(repeating: ".", count: textAnimationFrame)
- }
- private func advanceLoadingBar() {
- let next = min(progressValue + 0.8, 100)
- progressValue = next
- updateLoadingFill(for: next)
- percentLabel.stringValue = "%d%%".localized(with: Int(next.rounded()))
- }
- private func updateLoadingFill(for percent: Double) {
- let clamped = max(0, min(percent, 100))
- let totalWidth: CGFloat = 320
- loadingFillWidthConstraint?.constant = totalWidth * CGFloat(clamped / 100)
- }
- override func viewWillMove(toSuperview newSuperview: NSView?) {
- if newSuperview == nil {
- stopAnimations()
- }
- super.viewWillMove(toSuperview: newSuperview)
- }
- }
|