|
@@ -13,6 +13,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
private let loginStateKey = "zoom_app.isLoggedIn"
|
|
private let loginStateKey = "zoom_app.isLoggedIn"
|
|
|
private var statusItem: NSStatusItem?
|
|
private var statusItem: NSStatusItem?
|
|
|
private var statusMenu: NSMenu?
|
|
private var statusMenu: NSMenu?
|
|
|
|
|
+ private var mainWindowController: NSWindowController?
|
|
|
|
|
+ private weak var mainWindow: NSWindow?
|
|
|
|
|
+ private var mainOriginalContentViewController: NSViewController?
|
|
|
|
|
|
|
|
private enum MenuItemTag: Int {
|
|
private enum MenuItemTag: Int {
|
|
|
case openHome = 10
|
|
case openHome = 10
|
|
@@ -35,15 +38,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
|
|
|
|
|
|
|
|
|
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
|
|
|
+ // Ensure we are a regular app so windows can appear reliably.
|
|
|
|
|
+ // (Some configurations behave like an accessory/menubar app otherwise.)
|
|
|
|
|
+ NSApp.setActivationPolicy(.regular)
|
|
|
|
|
+
|
|
|
let darkEnabled = systemPrefersDarkMode()
|
|
let darkEnabled = systemPrefersDarkMode()
|
|
|
UserDefaults.standard.set(darkEnabled, forKey: darkModeDefaultsKey)
|
|
UserDefaults.standard.set(darkEnabled, forKey: darkModeDefaultsKey)
|
|
|
NSApp.appearance = NSAppearance(named: darkEnabled ? .darkAqua : .aqua)
|
|
NSApp.appearance = NSAppearance(named: darkEnabled ? .darkAqua : .aqua)
|
|
|
configureStatusItem()
|
|
configureStatusItem()
|
|
|
|
|
|
|
|
- // Force a consistent launch size (avoid state restoration overriding it).
|
|
|
|
|
- DispatchQueue.main.async { [weak self] in
|
|
|
|
|
- self?.applyDefaultWindowSizeIfNeeded()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ showSplashThenMainWindow()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func applicationWillTerminate(_ aNotification: Notification) {
|
|
func applicationWillTerminate(_ aNotification: Notification) {
|
|
@@ -75,6 +79,70 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
return style?.lowercased() == "dark"
|
|
return style?.lowercased() == "dark"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func showSplashThenMainWindow() {
|
|
|
|
|
+ let (window, originalVC) = openMainWindowIfNeededAndReturnWindow()
|
|
|
|
|
+ guard let window else { return }
|
|
|
|
|
+
|
|
|
|
|
+ let splashVC = SplashViewController()
|
|
|
|
|
+ mainOriginalContentViewController = originalVC
|
|
|
|
|
+ mainWindow = window
|
|
|
|
|
+
|
|
|
|
|
+ // Show main window immediately with splash content.
|
|
|
|
|
+ window.contentViewController = splashVC
|
|
|
|
|
+ window.isRestorable = false
|
|
|
|
|
+ window.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
|
|
|
|
|
+ window.center()
|
|
|
|
|
+ NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
+ window.makeKeyAndOrderFront(nil)
|
|
|
|
|
+
|
|
|
|
|
+ Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
|
|
|
|
|
+ guard let self else { return }
|
|
|
|
|
+ self.restoreMainContentAfterSplash()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @discardableResult
|
|
|
|
|
+ private func openMainWindowIfNeededAndReturnWindow() -> (NSWindow?, NSViewController?) {
|
|
|
|
|
+ if let existing = windowHostingViewController() {
|
|
|
|
|
+ existing.isRestorable = false
|
|
|
|
|
+ existing.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
|
|
|
|
|
+ existing.center()
|
|
|
|
|
+ existing.makeKeyAndOrderFront(nil)
|
|
|
|
|
+ return (existing, existing.contentViewController)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let storyboard = NSStoryboard(name: "Main", bundle: nil)
|
|
|
|
|
+ if let controller = storyboard.instantiateInitialController() as? NSWindowController {
|
|
|
|
|
+ mainWindowController = controller
|
|
|
|
|
+ controller.showWindow(nil)
|
|
|
|
|
+ if let window = controller.window {
|
|
|
|
|
+ window.isRestorable = false
|
|
|
|
|
+ window.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
|
|
|
|
|
+ window.center()
|
|
|
|
|
+ window.makeKeyAndOrderFront(nil)
|
|
|
|
|
+ return (window, window.contentViewController)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return (nil, nil)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func restoreMainContentAfterSplash() {
|
|
|
|
|
+ guard let window = mainWindow else { return }
|
|
|
|
|
+
|
|
|
|
|
+ // If we captured the original VC (from storyboard), restore it.
|
|
|
|
|
+ if let original = mainOriginalContentViewController {
|
|
|
|
|
+ window.contentViewController = original
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.isRestorable = false
|
|
|
|
|
+ window.setContentSize(NSSize(width: DefaultWindowSize.width, height: DefaultWindowSize.height))
|
|
|
|
|
+ window.center()
|
|
|
|
|
+ window.makeKeyAndOrderFront(nil)
|
|
|
|
|
+
|
|
|
|
|
+ mainOriginalContentViewController = nil
|
|
|
|
|
+ mainWindow = nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func configureStatusItem() {
|
|
private func configureStatusItem() {
|
|
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
|
if let button = item.button {
|
|
if let button = item.button {
|
|
@@ -198,3 +266,265 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+private final class SplashViewController: NSViewController {
|
|
|
|
|
+ private struct Theme {
|
|
|
|
|
+ let isDark: Bool
|
|
|
|
|
+ let backgroundTop: NSColor
|
|
|
|
|
+ let backgroundBottom: NSColor
|
|
|
|
|
+ let cardFill: NSColor
|
|
|
|
|
+ let titleColor: NSColor
|
|
|
|
|
+ let subtitleColor: NSColor
|
|
|
|
|
+ let iconBadgeFill: NSColor
|
|
|
|
|
+ let iconBadgeStroke: NSColor
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private final class GradientBackgroundView: NSView {
|
|
|
|
|
+ let gradientLayer = CAGradientLayer()
|
|
|
|
|
+
|
|
|
|
|
+ override init(frame frameRect: NSRect) {
|
|
|
|
|
+ super.init(frame: frameRect)
|
|
|
|
|
+ wantsLayer = true
|
|
|
|
|
+ layer = CALayer()
|
|
|
|
|
+ gradientLayer.startPoint = CGPoint(x: 0.1, y: 1.0)
|
|
|
|
|
+ gradientLayer.endPoint = CGPoint(x: 0.9, y: 0.0)
|
|
|
|
|
+ gradientLayer.locations = [0.0, 1.0]
|
|
|
|
|
+ layer?.addSublayer(gradientLayer)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @available(*, unavailable)
|
|
|
|
|
+ required init?(coder: NSCoder) { nil }
|
|
|
|
|
+
|
|
|
|
|
+ override func layout() {
|
|
|
|
|
+ super.layout()
|
|
|
|
|
+ gradientLayer.frame = bounds
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var theme: Theme = SplashViewController.makeTheme(isDark: true)
|
|
|
|
|
+ private var dotsTimer: Timer?
|
|
|
|
|
+ private var dotsStep: Int = 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?
|
|
|
|
|
+
|
|
|
|
|
+ override func viewDidLoad() {
|
|
|
|
|
+ super.viewDidLoad()
|
|
|
|
|
+ applyTheme()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func viewDidAppear() {
|
|
|
|
|
+ super.viewDidAppear()
|
|
|
|
|
+ startAnimations()
|
|
|
|
|
+ startDots()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func viewWillDisappear() {
|
|
|
|
|
+ super.viewWillDisappear()
|
|
|
|
|
+ dotsTimer?.invalidate()
|
|
|
|
|
+ dotsTimer = 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.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let iconBadge = NSView()
|
|
|
|
|
+ iconBadge.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ iconBadge.wantsLayer = true
|
|
|
|
|
+ iconBadge.layer?.cornerRadius = 22
|
|
|
|
|
+ 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
|
|
|
|
|
+ iconView.image = NSApp.applicationIconImage
|
|
|
|
|
+ iconView.wantsLayer = true
|
|
|
|
|
+
|
|
|
|
|
+ let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Zoom App"
|
|
|
|
|
+ let title = NSTextField(labelWithString: appName)
|
|
|
|
|
+ title.font = .systemFont(ofSize: 30, weight: .semibold)
|
|
|
|
|
+
|
|
|
|
|
+ let subtitle = NSTextField(labelWithString: "Starting up…")
|
|
|
|
|
+ subtitle.font = .systemFont(ofSize: 14, weight: .regular)
|
|
|
|
|
+ subtitle.alignment = .center
|
|
|
|
|
+
|
|
|
|
|
+ iconBadge.addSubview(iconView)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ iconView.centerXAnchor.constraint(equalTo: iconBadge.centerXAnchor),
|
|
|
|
|
+ iconView.centerYAnchor.constraint(equalTo: iconBadge.centerYAnchor),
|
|
|
|
|
+ iconView.widthAnchor.constraint(equalToConstant: 70),
|
|
|
|
|
+ iconView.heightAnchor.constraint(equalTo: iconView.widthAnchor)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ stack.addArrangedSubview(iconBadge)
|
|
|
|
|
+ stack.addArrangedSubview(title)
|
|
|
|
|
+ stack.addArrangedSubview(subtitle)
|
|
|
|
|
+
|
|
|
|
|
+ card.addSubview(stack)
|
|
|
|
|
+ background.addSubview(card)
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ // Initial state for animation.
|
|
|
|
|
+ stack.alphaValue = 0
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ view = background
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func startAnimations() {
|
|
|
|
|
+ guard let stack = contentStack, let iconView else { return }
|
|
|
|
|
+
|
|
|
|
|
+ // Entrance animation: fade + slight rise + gentle icon scale.
|
|
|
|
|
+ iconView.layer?.transform = CATransform3DMakeScale(0.92, 0.92, 1)
|
|
|
|
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
|
|
+ context.duration = 0.55
|
|
|
|
|
+ context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
|
|
|
+ stack.animator().alphaValue = 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let scale = CABasicAnimation(keyPath: "transform")
|
|
|
|
|
+ scale.fromValue = CATransform3DMakeScale(0.92, 0.92, 1)
|
|
|
|
|
+ scale.toValue = CATransform3DIdentity
|
|
|
|
|
+ scale.duration = 0.55
|
|
|
|
|
+ scale.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
|
|
|
+ iconView.layer?.add(scale, forKey: "splash.scaleIn")
|
|
|
|
|
+ iconView.layer?.transform = CATransform3DIdentity
|
|
|
|
|
+
|
|
|
|
|
+ // Subtle breathing pulse after entrance.
|
|
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) { [weak self] in
|
|
|
|
|
+ guard let self, let iconLayer = self.iconView?.layer else { return }
|
|
|
|
|
+ let pulse = CABasicAnimation(keyPath: "transform")
|
|
|
|
|
+ pulse.fromValue = CATransform3DIdentity
|
|
|
|
|
+ pulse.toValue = CATransform3DMakeScale(1.03, 1.03, 1)
|
|
|
|
|
+ pulse.duration = 0.9
|
|
|
|
|
+ pulse.autoreverses = true
|
|
|
|
|
+ pulse.repeatCount = .infinity
|
|
|
|
|
+ pulse.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
|
|
+ iconLayer.add(pulse, forKey: "splash.pulse")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func startDots() {
|
|
|
|
|
+ dotsTimer?.invalidate()
|
|
|
|
|
+ dotsStep = 0
|
|
|
|
|
+ dotsTimer = Timer.scheduledTimer(withTimeInterval: 0.45, repeats: true) { [weak self] _ in
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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),
|
|
|
|
|
+ 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)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Theme(
|
|
|
|
|
+ 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)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|