From 3f80316403aafe05c338222a767dd14231552d3a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 20 Feb 2025 17:56:54 +1100 Subject: [PATCH 1/2] feat: animate menu bar icon with vpn state --- .../Coder Desktop/Coder_DesktopApp.swift | 24 +++++++- .../Coder Desktop/MenuBarIconController.swift | 57 +++++++++++++++++++ Coder Desktop/Coder Desktop/VPNService.swift | 14 +---- Coder Desktop/project.yml | 6 +- 4 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 Coder Desktop/Coder Desktop/MenuBarIconController.swift 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..a1295917 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/ethanndickson/fluid-menu-bar-extra + revision: 020be37 KeychainAccess: url: https://github.com/kishikawakatsumi/KeychainAccess branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf From 1b2c5cd0efda2afacfa4f4a47be04d479281a37e Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 20 Feb 2025 21:52:17 +1100 Subject: [PATCH 2/2] switch repo --- Coder Desktop/project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Coder Desktop/project.yml b/Coder Desktop/project.yml index a1295917..2872515b 100644 --- a/Coder Desktop/project.yml +++ b/Coder Desktop/project.yml @@ -91,7 +91,7 @@ packages: FluidMenuBarExtra: # Forked so we can dynamically update the menu bar icon. # The upstream repo has a purposefully limited API - url: https://github.com/ethanndickson/fluid-menu-bar-extra + url: https://github.com/coder/fluid-menu-bar-extra revision: 020be37 KeychainAccess: url: https://github.com/kishikawakatsumi/KeychainAccess 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