Skip to content

Commit 3442516

Browse files
1 parent f33afbd commit 3442516

File tree

4 files changed

+83
-18
lines changed

4 files changed

+83
-18
lines changed

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import FluidMenuBarExtra
2+
import NetworkExtension
23
import SwiftUI
34

45
@main
@@ -26,7 +27,7 @@ struct DesktopApp: App {
2627

2728
@MainActor
2829
class AppDelegate: NSObject, NSApplicationDelegate {
29-
private var menuBarExtra: FluidMenuBarExtra?
30+
private var menuBar: MenuBarController?
3031
let vpn: CoderVPNService
3132
let state: AppState
3233

@@ -36,11 +37,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3637
}
3738

3839
func applicationDidFinishLaunching(_: Notification) {
39-
menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
40+
menuBar = .init(menuBarExtra: FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
4041
VPNMenu<CoderVPNService>().frame(width: 256)
4142
.environmentObject(self.vpn)
4243
.environmentObject(self.state)
43-
}
44+
})
45+
// Subscribe to system VPN updates
46+
NotificationCenter.default.addObserver(
47+
self,
48+
selector: #selector(vpnDidUpdate(_:)),
49+
name: .NEVPNStatusDidChange,
50+
object: nil
51+
)
4452
}
4553

4654
// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
@@ -59,6 +67,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5967
}
6068
}
6169

70+
extension AppDelegate {
71+
@objc private func vpnDidUpdate(_ notification: Notification) {
72+
guard let connection = notification.object as? NETunnelProviderSession else {
73+
return
74+
}
75+
vpn.vpnDidUpdate(connection)
76+
menuBar?.vpnDidUpdate(connection)
77+
}
78+
}
79+
6280
@MainActor
6381
func appActivate() {
6482
NSApp.activate()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import FluidMenuBarExtra
2+
import NetworkExtension
3+
import SwiftUI
4+
5+
@MainActor
6+
class MenuBarController {
7+
let menuBarExtra: FluidMenuBarExtra
8+
private let onImage = NSImage(named: "MenuBarIcon")!
9+
private let offOpacity = CGFloat(0.3)
10+
private let onOpacity = CGFloat(1.0)
11+
12+
private var animationTask: Task<Void, Never>?
13+
14+
init(menuBarExtra: FluidMenuBarExtra) {
15+
self.menuBarExtra = menuBarExtra
16+
}
17+
18+
func vpnDidUpdate(_ connection: NETunnelProviderSession) {
19+
switch connection.status {
20+
case .connected:
21+
stopAnimation()
22+
menuBarExtra.setOpacity(onOpacity)
23+
case .connecting, .reasserting, .disconnecting:
24+
startAnimation()
25+
case .invalid, .disconnected:
26+
stopAnimation()
27+
menuBarExtra.setOpacity(offOpacity)
28+
@unknown default:
29+
stopAnimation()
30+
menuBarExtra.setOpacity(offOpacity)
31+
}
32+
}
33+
34+
func startAnimation() {
35+
if animationTask != nil { return }
36+
animationTask = Task {
37+
defer { animationTask = nil }
38+
let totalFrames = 60
39+
let cycleDurationMs: UInt64 = 2000
40+
let frameDurationMs = cycleDurationMs / UInt64(totalFrames - 1)
41+
repeat {
42+
for frame in 0 ..< totalFrames {
43+
if Task.isCancelled { break }
44+
let progress = Double(frame) / Double(totalFrames - 1)
45+
let alpha = 0.3 + 0.7 * (0.5 - 0.5 * cos(2 * Double.pi * progress))
46+
menuBarExtra.setOpacity(CGFloat(alpha))
47+
try? await Task.sleep(for: .milliseconds(frameDurationMs))
48+
}
49+
} while !Task.isCancelled
50+
}
51+
}
52+
53+
func stopAnimation() {
54+
animationTask?.cancel()
55+
animationTask = nil
56+
}
57+
}

Coder Desktop/Coder Desktop/VPNService.swift

+1-13
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@ final class CoderVPNService: NSObject, VPNService {
7070
Task {
7171
await loadNetworkExtensionConfig()
7272
}
73-
NotificationCenter.default.addObserver(
74-
self,
75-
selector: #selector(vpnDidUpdate(_:)),
76-
name: .NEVPNStatusDidChange,
77-
object: nil
78-
)
7973
}
8074

8175
deinit {
@@ -159,13 +153,7 @@ final class CoderVPNService: NSObject, VPNService {
159153
}
160154

161155
extension CoderVPNService {
162-
// The number of NETunnelProviderSession states makes the excessive branching
163-
// necessary.
164-
// swiftlint:disable:next cyclomatic_complexity
165-
@objc private func vpnDidUpdate(_ notification: Notification) {
166-
guard let connection = notification.object as? NETunnelProviderSession else {
167-
return
168-
}
156+
public func vpnDidUpdate(_ connection: NETunnelProviderSession) {
169157
switch (tunnelState, connection.status) {
170158
// Any -> Disconnected: Update UI w/ error if present
171159
case (_, .disconnected):

Coder Desktop/project.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ packages:
8989
url: https://github.com/SimplyDanny/SwiftLintPlugins
9090
from: 0.57.1
9191
FluidMenuBarExtra:
92-
url: https://github.com/lfroms/fluid-menu-bar-extra
93-
from: 1.1.0
92+
# Forked so we can dynamically update the menu bar icon.
93+
# The upstream repo has a purposefully limited API
94+
url: https://github.com/coder/fluid-menu-bar-extra
95+
revision: 020be37
9496
KeychainAccess:
9597
url: https://github.com/kishikawakatsumi/KeychainAccess
9698
branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf

0 commit comments

Comments
 (0)