-
Notifications
You must be signed in to change notification settings - Fork 3
Shortcuts and control center #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
caaf87e
d08a652
617eefd
c2683ef
0f00b4e
9c1866f
a915ced
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,5 @@ | ||
| EasyTier.xcodeproj/project.xcworkspace/xcuserdata/ | ||
| xcuserdata | ||
| build.log | ||
| compile_commands.json | ||
|
|
||
| CLAUDE.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "colors" : [ | ||
| { | ||
| "idiom" : "universal" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "images" : [ | ||
| { | ||
| "idiom" : "universal", | ||
| "platform" : "ios", | ||
| "size" : "1024x1024" | ||
| }, | ||
| { | ||
| "appearances" : [ | ||
| { | ||
| "appearance" : "luminosity", | ||
| "value" : "dark" | ||
| } | ||
| ], | ||
| "idiom" : "universal", | ||
| "platform" : "ios", | ||
| "size" : "1024x1024" | ||
| }, | ||
| { | ||
| "appearances" : [ | ||
| { | ||
| "appearance" : "luminosity", | ||
| "value" : "tinted" | ||
| } | ||
| ], | ||
| "idiom" : "universal", | ||
| "platform" : "ios", | ||
| "size" : "1024x1024" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "colors" : [ | ||
| { | ||
| "idiom" : "universal" | ||
| } | ||
| ], | ||
| "info" : { | ||
| "author" : "xcode", | ||
| "version" : 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import WidgetKit | ||
| import SwiftUI | ||
|
|
||
| @main | ||
| struct ControlWidgetsBundle: WidgetBundle { | ||
| var body: some Widget { | ||
| ControlWidgetsControl() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||
| import AppIntents | ||||||||||||||||||||||||
| import SwiftUI | ||||||||||||||||||||||||
| import WidgetKit | ||||||||||||||||||||||||
| import NetworkExtension | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| struct ControlWidgetsControl: ControlWidget { | ||||||||||||||||||||||||
| static let kind: String = "site.yinmo.easytier.controlwidgets" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| var body: some ControlWidgetConfiguration { | ||||||||||||||||||||||||
| StaticControlConfiguration( | ||||||||||||||||||||||||
| kind: Self.kind, | ||||||||||||||||||||||||
| provider: VPNControlProvider() | ||||||||||||||||||||||||
| ) { isConnected in | ||||||||||||||||||||||||
| ControlWidgetToggle( | ||||||||||||||||||||||||
| "EasyTier", | ||||||||||||||||||||||||
| isOn: isConnected, | ||||||||||||||||||||||||
| action: ToggleVPNIntent() | ||||||||||||||||||||||||
| ) { isOn in | ||||||||||||||||||||||||
| Label(isOn ? "vpn_connected" : "vpn_disconnected", systemImage: "network") | ||||||||||||||||||||||||
| .controlWidgetActionHint(isOn ? "vpn_disconnect" : "vpn_connect") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| .displayName("EasyTier") | ||||||||||||||||||||||||
| .description("toggle_vpn_connection") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| extension ControlWidgetsControl { | ||||||||||||||||||||||||
| struct VPNControlProvider: ControlValueProvider { | ||||||||||||||||||||||||
| var previewValue: Bool { | ||||||||||||||||||||||||
| false | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| func currentValue() async throws -> Bool { | ||||||||||||||||||||||||
| let managers = try await NETunnelProviderManager.loadAllFromPreferences() | ||||||||||||||||||||||||
| guard let manager = managers.first else { | ||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return [.connecting, .connected, .reasserting].contains(manager.connection.status) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| struct ToggleVPNIntent: SetValueIntent { | ||||||||||||||||||||||||
| static let title: LocalizedStringResource = "toggle_vpn" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @Parameter(title: "vpn_connected") | ||||||||||||||||||||||||
| var value: Bool | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| func perform() async throws -> some IntentResult { | ||||||||||||||||||||||||
| let managers = try await NETunnelProviderManager.loadAllFromPreferences() | ||||||||||||||||||||||||
| guard let manager = managers.first else { | ||||||||||||||||||||||||
| return .result() | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if value { | ||||||||||||||||||||||||
| // Connect - need to load config from App Group | ||||||||||||||||||||||||
| let defaults = UserDefaults(suiteName: "group.site.yinmo.easytier") | ||||||||||||||||||||||||
| guard let configData = defaults?.data(forKey: "LastVPNConfig"), | ||||||||||||||||||||||||
| let config = try? JSONDecoder().decode([String: String].self, from: configData) else { | ||||||||||||||||||||||||
| // Try to start with empty options as fallback | ||||||||||||||||||||||||
| try manager.connection.startVPNTunnel() | ||||||||||||||||||||||||
| return .result() | ||||||||||||||||||||||||
|
Comment on lines
+61
to
+63
|
||||||||||||||||||||||||
| // Try to start with empty options as fallback | |
| try manager.connection.startVPNTunnel() | |
| return .result() | |
| // Config is required to start the VPN tunnel; fail explicitly instead of using empty options. | |
| throw NSError( | |
| domain: "site.yinmo.easytier.ToggleVPNIntent", | |
| code: 1, | |
| userInfo: [ | |
| NSLocalizedDescriptionKey: "VPN configuration is not available. Please open EasyTier to configure the VPN before connecting from the widget." | |
| ] | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>com.apple.security.application-groups</key> | ||
| <array> | ||
| <string>group.site.yinmo.easytier</string> | ||
| </array> | ||
| </dict> | ||
| </plist> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>NSExtension</key> | ||
| <dict> | ||
| <key>NSExtensionPointIdentifier</key> | ||
| <string>com.apple.widgetkit-extension</string> | ||
| </dict> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the shortcuts, ToggleVPNIntent creates a new NETunnelProviderManager instance on each invocation rather than reusing a shared instance. While this fetches the current state, simultaneous operations from the widget and the main app could potentially conflict when both try to manage the VPN connection state.