Skip to content

Commit ddfbb62

Browse files
committed
feat: sync the widget and app.
ref sing-box.
1 parent 88027cc commit ddfbb62

File tree

4 files changed

+80
-88
lines changed

4 files changed

+80
-88
lines changed

ControlWidgets/AppIntent.swift

Lines changed: 0 additions & 35 deletions
This file was deleted.

ControlWidgets/ControlWidgetsControl.swift

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,78 @@
88
import AppIntents
99
import SwiftUI
1010
import WidgetKit
11+
import NetworkExtension
1112

1213
struct ControlWidgetsControl: ControlWidget {
1314
static let kind: String = "site.yinmo.easytier.ControlWidgets"
1415

1516
var body: some ControlWidgetConfiguration {
16-
AppIntentControlConfiguration(
17+
StaticControlConfiguration(
1718
kind: Self.kind,
1819
provider: VPNControlProvider()
1920
) { isConnected in
2021
ControlWidgetToggle(
22+
"EasyTier",
2123
isOn: isConnected,
2224
action: ToggleVPNIntent()
23-
) {
24-
Label("EasyTier", systemImage: "network")
25+
) { isOn in
26+
Label(isOn ? "Connected" : "Disconnected", systemImage: "network")
27+
.controlWidgetActionHint(isOn ? "Disconnect" : "Connect")
2528
}
2629
}
2730
.displayName("EasyTier")
2831
.description("Toggle VPN connection")
2932
}
3033
}
3134

32-
struct VPNControlProvider: AppIntentControlValueProvider {
33-
func previewValue(configuration: VPNControlConfiguration) -> Bool {
34-
false
35-
}
36-
37-
func currentValue(configuration: VPNControlConfiguration) async throws -> Bool {
38-
let defaults = UserDefaults(suiteName: "group.site.yinmo.easytier")
39-
return defaults?.bool(forKey: "VPNIsConnected") ?? false
35+
extension ControlWidgetsControl {
36+
struct VPNControlProvider: ControlValueProvider {
37+
var previewValue: Bool {
38+
false
39+
}
40+
41+
func currentValue() async throws -> Bool {
42+
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
43+
guard let manager = managers.first else {
44+
return false
45+
}
46+
return manager.connection.status == .connected
47+
}
4048
}
4149
}
4250

43-
struct VPNControlConfiguration: ControlConfigurationIntent {
44-
static let title: LocalizedStringResource = "VPN Control"
51+
struct ToggleVPNIntent: SetValueIntent {
52+
static let title: LocalizedStringResource = "Toggle VPN"
53+
54+
@Parameter(title: "Connected")
55+
var value: Bool
56+
57+
func perform() async throws -> some IntentResult {
58+
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
59+
guard let manager = managers.first else {
60+
return .result()
61+
}
62+
63+
if value {
64+
// Connect - need to load config from App Group
65+
let defaults = UserDefaults(suiteName: "group.site.yinmo.easytier")
66+
guard let configData = defaults?.data(forKey: "LastVPNConfig"),
67+
let config = try? JSONDecoder().decode([String: String].self, from: configData) else {
68+
// Try to start with empty options as fallback
69+
try manager.connection.startVPNTunnel()
70+
return .result()
71+
}
72+
73+
// Convert to NSDictionary for VPN options
74+
var options: [String: NSObject] = [:]
75+
for (key, val) in config {
76+
options[key] = val as NSString
77+
}
78+
try manager.connection.startVPNTunnel(options: options)
79+
} else {
80+
manager.connection.stopVPNTunnel()
81+
}
82+
83+
return .result()
84+
}
4585
}

EasyTier/Utils/NEManager.swift

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,18 @@ class NEManager: NEManagerProtocol {
5555
}
5656

5757
// Sync VPN connection status to App Group for Control Widget
58-
let defaults = UserDefaults(suiteName: "group.site.yinmo.easytier")
59-
let isConnected = self.status == .connected
60-
defaults?.set(isConnected, forKey: "VPNIsConnected")
61-
defaults?.synchronize()
62-
63-
// Reload Control Center Widget to reflect new state
64-
if #available(iOS 18.0, *) {
65-
ControlCenter.shared.reloadControls(ofKind: "site.yinmo.easytier.ControlWidgets")
66-
}
58+
self.syncWidgetState()
6759
}
6860
}
6961
}
7062

63+
// Notify Control Widget to refresh its state
64+
private func syncWidgetState() {
65+
if #available(iOS 18.0, *) {
66+
ControlCenter.shared.reloadControls(ofKind: "site.yinmo.easytier.ControlWidgets")
67+
}
68+
}
69+
7170
private func reset() {
7271
manager = nil
7372
connection = nil
@@ -173,13 +172,29 @@ class NEManager: NEManagerProtocol {
173172
if let routes = config.routes {
174173
options["routes"] = NSArray(array: routes.map { $0 as NSString })
175174
}
175+
176+
// Save config to App Group for Widget use
177+
let defaults = UserDefaults(suiteName: "group.site.yinmo.easytier")
178+
var configDict: [String: String] = [:]
179+
for (key, value) in options {
180+
if let strValue = value as? String {
181+
configDict[key] = strValue
182+
}
183+
}
184+
if let configData = try? JSONEncoder().encode(configDict) {
185+
defaults?.set(configData, forKey: "LastVPNConfig")
186+
defaults?.synchronize()
187+
}
188+
176189
do {
177190
try manager.connection.startVPNTunnel(options: options)
178191
} catch {
179192
Self.logger.error("connect() start vpn tunnel failed: \(String(describing: error))")
180193
throw error
181194
}
182195
Self.logger.info("connect() started")
196+
// Immediately sync widget state after initiating connection
197+
syncWidgetState()
183198
}
184199

185200
func disconnect() async {
@@ -188,6 +203,8 @@ class NEManager: NEManagerProtocol {
188203
return
189204
}
190205
manager.connection.stopVPNTunnel()
206+
// Immediately sync widget state after initiating disconnection
207+
syncWidgetState()
191208
}
192209

193210
func updateName(name: String, server: String) async {

EasyTier/Views/DashboardView.swift

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ struct DashboardView<Manager: NEManagerProtocol>: View {
3131
@State var errorMessage: TextItem?
3232

3333
@State private var darwinObserver: DNObserver? = nil
34-
@State private var widgetToggleObserver: DNObserver? = nil
3534

3635
var selectedProfile: ProfileSummary? {
3736
guard let selectedProfileId else { return nil }
@@ -239,34 +238,6 @@ struct DashboardView<Manager: NEManagerProtocol>: View {
239238
}
240239
}
241240
}
242-
243-
// Register Darwin notification observer for Control Widget toggle
244-
widgetToggleObserver = DNObserver(name: "site.yinmo.easytier.toggleVPN") {
245-
let defaults = UserDefaults(suiteName: "group.site.yinmo.easytier")
246-
let desiredState = defaults?.bool(forKey: "VPNDesiredState") ?? false
247-
248-
DispatchQueue.main.async {
249-
if desiredState {
250-
// Connect using selected profile or first available
251-
guard let profile = self.selectedProfile ?? self.networks.first else {
252-
DashboardLogger.warning("No profile available for widget toggle")
253-
return
254-
}
255-
Task {
256-
do {
257-
try await self.manager.connect(profile: profile)
258-
} catch {
259-
DashboardLogger.error("Widget toggle connect failed: \(error.localizedDescription)")
260-
}
261-
}
262-
} else {
263-
// Disconnect
264-
Task {
265-
await self.manager.disconnect()
266-
}
267-
}
268-
}
269-
}
270241
}
271242
.onChange(of: selectedProfile) {
272243
lastSelected = selectedProfile?.id.uuidString
@@ -276,9 +247,8 @@ struct DashboardView<Manager: NEManagerProtocol>: View {
276247
}
277248
}
278249
.onDisappear {
279-
// Release observers to remove registration
250+
// Release observer to remove registration
280251
darwinObserver = nil
281-
widgetToggleObserver = nil
282252
}
283253
.fileImporter(
284254
isPresented: $showImportPicker,

0 commit comments

Comments
 (0)