diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index ae50519c..13f7086a 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -1,4 +1,5 @@ import FluidMenuBarExtra +import NetworkExtension import SwiftUI @main @@ -26,7 +27,7 @@ struct DesktopApp: App { @MainActor class AppDelegate: NSObject, NSApplicationDelegate { - private var menuBarExtra: FluidMenuBarExtra? + private var menuBar: MenuBarController? let vpn: CoderVPNService let state: AppState @@ -36,11 +37,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(_: Notification) { - menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") { + menuBar = .init(menuBarExtra: FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") { VPNMenu().frame(width: 256) .environmentObject(self.vpn) .environmentObject(self.state) - } + }) + // Subscribe to system VPN updates + NotificationCenter.default.addObserver( + self, + selector: #selector(vpnDidUpdate(_:)), + name: .NEVPNStatusDidChange, + object: nil + ) } // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` @@ -59,6 +67,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } +extension AppDelegate { + @objc private func vpnDidUpdate(_ notification: Notification) { + guard let connection = notification.object as? NETunnelProviderSession else { + return + } + vpn.vpnDidUpdate(connection) + menuBar?.vpnDidUpdate(connection) + } +} + @MainActor func appActivate() { NSApp.activate() diff --git a/Coder Desktop/Coder Desktop/MenuBarIconController.swift b/Coder Desktop/Coder Desktop/MenuBarIconController.swift new file mode 100644 index 00000000..867e1837 --- /dev/null +++ b/Coder Desktop/Coder Desktop/MenuBarIconController.swift @@ -0,0 +1,57 @@ +import FluidMenuBarExtra +import NetworkExtension +import SwiftUI + +@MainActor +class MenuBarController { + let menuBarExtra: FluidMenuBarExtra + private let onImage = NSImage(named: "MenuBarIcon")! + private let offOpacity = CGFloat(0.3) + private let onOpacity = CGFloat(1.0) + + private var animationTask: Task? + + init(menuBarExtra: FluidMenuBarExtra) { + self.menuBarExtra = menuBarExtra + } + + func vpnDidUpdate(_ connection: NETunnelProviderSession) { + switch connection.status { + case .connected: + stopAnimation() + menuBarExtra.setOpacity(onOpacity) + case .connecting, .reasserting, .disconnecting: + startAnimation() + case .invalid, .disconnected: + stopAnimation() + menuBarExtra.setOpacity(offOpacity) + @unknown default: + stopAnimation() + menuBarExtra.setOpacity(offOpacity) + } + } + + func startAnimation() { + if animationTask != nil { return } + animationTask = Task { + defer { animationTask = nil } + let totalFrames = 60 + let cycleDurationMs: UInt64 = 2000 + let frameDurationMs = cycleDurationMs / UInt64(totalFrames - 1) + repeat { + for frame in 0 ..< totalFrames { + if Task.isCancelled { break } + let progress = Double(frame) / Double(totalFrames - 1) + let alpha = 0.3 + 0.7 * (0.5 - 0.5 * cos(2 * Double.pi * progress)) + menuBarExtra.setOpacity(CGFloat(alpha)) + try? await Task.sleep(for: .milliseconds(frameDurationMs)) + } + } while !Task.isCancelled + } + } + + func stopAnimation() { + animationTask?.cancel() + animationTask = nil + } +} diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift index 793b0eb0..1e29ae75 100644 --- a/Coder Desktop/Coder Desktop/VPNService.swift +++ b/Coder Desktop/Coder Desktop/VPNService.swift @@ -70,12 +70,6 @@ final class CoderVPNService: NSObject, VPNService { Task { await loadNetworkExtensionConfig() } - NotificationCenter.default.addObserver( - self, - selector: #selector(vpnDidUpdate(_:)), - name: .NEVPNStatusDidChange, - object: nil - ) } deinit { @@ -159,13 +153,7 @@ final class CoderVPNService: NSObject, VPNService { } extension CoderVPNService { - // The number of NETunnelProviderSession states makes the excessive branching - // necessary. - // swiftlint:disable:next cyclomatic_complexity - @objc private func vpnDidUpdate(_ notification: Notification) { - guard let connection = notification.object as? NETunnelProviderSession else { - return - } + public func vpnDidUpdate(_ connection: NETunnelProviderSession) { switch (tunnelState, connection.status) { // Any -> Disconnected: Update UI w/ error if present case (_, .disconnected): diff --git a/Coder Desktop/project.yml b/Coder Desktop/project.yml index 8b9b18fe..2872515b 100644 --- a/Coder Desktop/project.yml +++ b/Coder Desktop/project.yml @@ -89,8 +89,10 @@ packages: url: https://github.com/SimplyDanny/SwiftLintPlugins from: 0.57.1 FluidMenuBarExtra: - url: https://github.com/lfroms/fluid-menu-bar-extra - from: 1.1.0 + # Forked so we can dynamically update the menu bar icon. + # The upstream repo has a purposefully limited API + url: https://github.com/coder/fluid-menu-bar-extra + revision: 020be37 KeychainAccess: url: https://github.com/kishikawakatsumi/KeychainAccess branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy