Skip to content

Commit 9f0c424

Browse files
democlaude
andcommitted
feat: v0.2.1 — redesigned status bar icon, onboarding improvements, about window
- Status bar icon: white rounded-rect badge with cutout shield and colored status dot - Gateway and server cert pin now required in onboarding (defaults empty) - Settings save closes window and auto-triggers reconnect - Save button disabled when config unchanged - Added About VPN MenuBar window with version info - Updated appcast.xml with v0.2.1 signed release Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9b5caef commit 9f0c424

13 files changed

Lines changed: 206 additions & 46 deletions

VPNMenuBar-0.2.1.zip

1.66 MB
Binary file not shown.

VPNMenuBar.app/Contents/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
<key>CFBundlePackageType</key>
2424
<string>APPL</string>
2525
<key>CFBundleShortVersionString</key>
26-
<string>0.2.0</string>
26+
<string>0.2.1</string>
2727
<key>CFBundleSupportedPlatforms</key>
2828
<array>
2929
<string>MacOSX</string>
3030
</array>
3131
<key>CFBundleVersion</key>
32-
<string>2</string>
32+
<string>3</string>
3333
<key>DTCompiler</key>
3434
<string>com.apple.compilers.llvm.clang.1_0</string>
3535
<key>DTPlatformBuild</key>
65.9 KB
Binary file not shown.

VPNMenuBar/App/VPNMenuBarApp.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct VPNMenuBarApp: App {
1515
controller: coordinator.controller,
1616
onOpenSettings: coordinator.openSettings,
1717
onCheckDependencies: coordinator.openDependencyAlert,
18+
onAbout: coordinator.openAbout,
1819
updaterController: coordinator.updaterController
1920
)
2021
} label: {
@@ -58,6 +59,7 @@ final class AppCoordinator: ObservableObject {
5859
private var onboardingWindow: NSWindow?
5960
private var dependencyAlertWindow: NSWindow?
6061
private var settingsWindow: NSWindow?
62+
private var aboutWindow: NSWindow?
6163
private var cancellables = Set<AnyCancellable>()
6264

6365
init() {
@@ -181,6 +183,32 @@ final class AppCoordinator: ObservableObject {
181183
NSApp.activate(ignoringOtherApps: true)
182184
}
183185

186+
func openAbout() {
187+
if let win = aboutWindow {
188+
win.makeKeyAndOrderFront(nil)
189+
NSApp.activate(ignoringOtherApps: true)
190+
return
191+
}
192+
let hosting = NSHostingController(rootView: AboutView())
193+
let win = NSWindow(contentViewController: hosting)
194+
win.title = "About VPN MenuBar"
195+
win.styleMask = [.titled, .closable]
196+
win.center()
197+
win.isReleasedWhenClosed = false
198+
aboutWindow = win
199+
200+
NotificationCenter.default.addObserver(
201+
forName: NSWindow.willCloseNotification,
202+
object: win,
203+
queue: .main
204+
) { [weak self] _ in
205+
self?.aboutWindow = nil
206+
}
207+
208+
win.makeKeyAndOrderFront(nil)
209+
NSApp.activate(ignoringOtherApps: true)
210+
}
211+
184212
func openDependencyAlert() {
185213
if let win = dependencyAlertWindow {
186214
win.makeKeyAndOrderFront(nil)

VPNMenuBar/Config/VPNConfig.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ struct VPNConfig: Codable, Equatable {
66
var passwordPrefix: String
77
var totpSecret: String
88

9-
// Advanceddefaults migrated from vpn.py / arch-aware
10-
var gateway: String = "vpn.example.com"
11-
var serverCertPin: String = "pin-sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
9+
// RequiredVPN server settings (user must fill in their own values)
10+
var gateway: String = ""
11+
var serverCertPin: String = ""
1212
var openconnectPath: String = ArchDetector.defaultPaths.openconnect
1313
var vpncScriptPath: String = ArchDetector.defaultPaths.vpncScript
1414
var skipDNSModification: Bool = true
@@ -18,5 +18,6 @@ struct VPNConfig: Codable, Equatable {
1818

1919
var isConfigured: Bool {
2020
!username.isEmpty && !passwordPrefix.isEmpty && !totpSecret.isEmpty
21+
&& !gateway.isEmpty && !serverCertPin.isEmpty
2122
}
2223
}

VPNMenuBar/Info.plist

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
<key>CFBundlePackageType</key>
1818
<string>APPL</string>
1919
<key>CFBundleShortVersionString</key>
20-
<string>0.1.0</string>
20+
<string>0.2.1</string>
2121
<key>CFBundleVersion</key>
22-
<string>1</string>
22+
<string>3</string>
2323
<key>LSMinimumSystemVersion</key>
2424
<string>13.0</string>
2525
<key>LSUIElement</key>
@@ -28,5 +28,11 @@
2828
<string>Personal use only</string>
2929
<key>NSUserNotificationAlertStyle</key>
3030
<string>alert</string>
31+
<key>SUEnableAutomaticChecks</key>
32+
<true/>
33+
<key>SUFeedURL</key>
34+
<string>https://raw.githubusercontent.com/CoderZCC/VPNMenuBar/main/appcast.xml</string>
35+
<key>SUPublicEDKey</key>
36+
<string>cyiD6I9dAeSGGdVG2VAADG9ecdvdLHDHBIFfnjR9zJY=</string>
3137
</dict>
3238
</plist>

VPNMenuBar/UI/AboutView.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import SwiftUI
2+
3+
struct AboutView: View {
4+
private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
5+
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "VPN MenuBar"
6+
private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
7+
private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
8+
private let copyright = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") as? String ?? ""
9+
10+
var body: some View {
11+
VStack(spacing: 12) {
12+
Image(nsImage: NSApp.applicationIconImage)
13+
.resizable()
14+
.frame(width: 64, height: 64)
15+
16+
Text(appName)
17+
.font(.title2).bold()
18+
19+
Text("Version \(version) (\(build))")
20+
.font(.callout)
21+
.foregroundColor(.secondary)
22+
23+
Text(copyright)
24+
.font(.caption)
25+
.foregroundColor(.secondary)
26+
27+
Divider()
28+
29+
Link("GitHub Repository",
30+
destination: URL(string: "https://github.com/CoderZCC/VPNMenuBar")!)
31+
.font(.callout)
32+
}
33+
.padding(24)
34+
.frame(width: 280)
35+
}
36+
}

VPNMenuBar/UI/MenuContentView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ struct MenuContentView: View {
55
@ObservedObject var controller: VPNController
66
var onOpenSettings: () -> Void
77
var onCheckDependencies: () -> Void
8+
var onAbout: () -> Void
89
let updaterController: SPUStandardUpdaterController
910

1011
var body: some View {
@@ -45,6 +46,8 @@ struct MenuContentView: View {
4546
updaterController.checkForUpdates(nil)
4647
}
4748

49+
Button("About VPN MenuBar") { onAbout() }
50+
4851
Divider()
4952

5053
Button("Quit") {

VPNMenuBar/UI/OnboardingView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,16 @@ struct OnboardingView: View {
104104
private var credentialsStep: some View {
105105
VStack(alignment: .leading, spacing: 12) {
106106
Text("Credentials").font(.title2).bold()
107+
TextField("Gateway (e.g. vpn.company.com)", text: $config.gateway)
108+
TextField("Server cert pin (pin-sha256:...)", text: $config.serverCertPin)
107109
TextField("Username", text: $config.username)
108110
RevealableSecureField(title: "Password prefix", text: $config.passwordPrefix)
109111
RevealableSecureField(title: "TOTP secret (Base32)", text: $config.totpSecret)
110112
HStack {
111113
ImportSecretFromImageButton(secret: $config.totpSecret, username: $config.username)
112114
Spacer()
113115
}
114-
Text("These three fields are required. Defaults for gateway, cert pin, and paths are already filled in and can be edited later in Settings → Advanced.")
116+
Text("All fields above are required. Paths can be edited later in Settings → Advanced.")
115117
.font(.caption).foregroundColor(.secondary)
116118
}
117119
}

VPNMenuBar/UI/SettingsView.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ struct SettingsView: View {
55
let configStore: ConfigStore
66

77
@State private var config: VPNConfig = VPNConfig(username: "", passwordPrefix: "", totpSecret: "")
8+
@State private var originalConfig: VPNConfig = VPNConfig(username: "", passwordPrefix: "", totpSecret: "")
89
@State private var launchAtLogin: Bool = LoginItemManager.isEnabledPreference
910
@State private var showSavedAlert: Bool = false
1011
@State private var savedAlertTitle: String = ""
1112
@State private var savedAlertMessage: String = ""
1213

14+
private var hasChanges: Bool { config != originalConfig }
15+
1316
var body: some View {
1417
Form {
1518
Section("Required") {
19+
TextField("Gateway", text: $config.gateway)
20+
TextField("Server cert pin", text: $config.serverCertPin)
1621
TextField("Username", text: $config.username)
1722
RevealableSecureField(title: "Password prefix", text: $config.passwordPrefix)
1823
RevealableSecureField(title: "TOTP secret (Base32)", text: $config.totpSecret)
@@ -23,8 +28,6 @@ struct SettingsView: View {
2328
}
2429

2530
Section("Advanced") {
26-
TextField("Gateway", text: $config.gateway)
27-
TextField("Server cert pin", text: $config.serverCertPin)
2831
TextField("openconnect path", text: $config.openconnectPath)
2932
TextField("vpnc-script path", text: $config.vpncScriptPath)
3033
Toggle("Skip DNS modification (use bundled vpnc-script--no-dns)",
@@ -43,6 +46,7 @@ struct SettingsView: View {
4346
Spacer()
4447
Button("Save") { save() }
4548
.keyboardShortcut(.defaultAction)
49+
.disabled(!hasChanges)
4650
}
4751
}
4852
}
@@ -59,22 +63,25 @@ struct SettingsView: View {
5963
private func load() {
6064
if let existing = (try? configStore.load()) ?? nil {
6165
config = existing
66+
originalConfig = existing
6267
}
6368
}
6469

6570
private func save() {
6671
do {
6772
try configStore.save(config)
68-
savedAlertTitle = "Settings Saved"
69-
if controller.state.isConnected {
70-
savedAlertMessage = "Configuration saved. The new values will take effect on the next connection."
71-
} else {
72-
savedAlertMessage = "Your configuration has been saved."
73+
// Close settings window and trigger a reconnect
74+
NSApp.keyWindow?.close()
75+
Task {
76+
if controller.state.isConnected {
77+
await controller.disconnect()
78+
}
79+
await controller.connect()
7380
}
7481
} catch {
7582
savedAlertTitle = "Save Failed"
7683
savedAlertMessage = "The config file may be read-only — check permissions under ~/Library/Application Support."
84+
showSavedAlert = true
7785
}
78-
showSavedAlert = true
7986
}
8087
}

0 commit comments

Comments
 (0)