From f765bf1e3b85e9392e861f0ad3047027c05c1e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 5 Feb 2026 15:16:18 +0800 Subject: [PATCH] Decouple WidgetExtension from Library --- WidgetExtension/ServiceToggleControl.swift | 19 ++---- WidgetExtension/WidgetTunnelControl.swift | 70 ++++++++++++++++++++++ sing-box.xcodeproj/project.pbxproj | 15 ----- 3 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 WidgetExtension/WidgetTunnelControl.swift diff --git a/WidgetExtension/ServiceToggleControl.swift b/WidgetExtension/ServiceToggleControl.swift index 6cdd3c1..b69bdef 100644 --- a/WidgetExtension/ServiceToggleControl.swift +++ b/WidgetExtension/ServiceToggleControl.swift @@ -1,12 +1,11 @@ import AppIntents -import Library import SwiftUI import WidgetKit struct ServiceToggleControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( - kind: ExtensionProfile.controlKind, + kind: WidgetAppConfiguration.widgetControlKind, provider: Provider() ) { value in ControlWidgetToggle( @@ -15,7 +14,7 @@ struct ServiceToggleControl: ControlWidget { action: ToggleServiceControlIntent() ) { isOn in Label(isOn ? "Running" : "Stopped", systemImage: "shippingbox.fill") -// .controlWidgetActionHint(isOn ? "Stop" : "Start") + .controlWidgetActionHint(isOn ? "Stop" : "Start") } .tint(.init(red: CGFloat(Double(69) / 255), green: CGFloat(Double(90) / 255), blue: CGFloat(Double(100) / 255))) } @@ -31,10 +30,7 @@ extension ServiceToggleControl { } func currentValue() async throws -> Bool { - guard let extensionProfile = try await (ExtensionProfile.load()) else { - return false - } - return await extensionProfile.status.isStarted + try await WidgetTunnelControl.currentIsStarted() } } } @@ -46,14 +42,7 @@ struct ToggleServiceControlIntent: SetValueIntent { var value: Bool func perform() async throws -> some IntentResult { - guard let extensionProfile = try await (ExtensionProfile.load()) else { - return .result() - } - if value { - try await extensionProfile.start() - } else { - try await extensionProfile.stop() - } + try await WidgetTunnelControl.setStarted(value) return .result() } } diff --git a/WidgetExtension/WidgetTunnelControl.swift b/WidgetExtension/WidgetTunnelControl.swift new file mode 100644 index 0000000..3e3964a --- /dev/null +++ b/WidgetExtension/WidgetTunnelControl.swift @@ -0,0 +1,70 @@ +import Foundation +import NetworkExtension + +enum WidgetAppConfiguration { + static let packageName: String = { + guard let value = Bundle.main.object(forInfoDictionaryKey: "BasePackageIdentifier") as? String else { + fatalError("Missing BasePackageIdentifier in Info.plist") + } + return value + }() + + static let appGroupID: String = { + guard let value = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String else { + fatalError("Missing AppGroupIdentifier in Info.plist") + } + return value + }() + + static var widgetControlKind: String { "\(packageName).widget.ServiceToggle" } +} + +extension NEVPNStatus { + var isStarted: Bool { + switch self { + case .connecting, .connected, .reasserting: + return true + default: + return false + } + } +} + +enum WidgetTunnelControl { + static func currentIsStarted() async throws -> Bool { + guard let manager = try await loadManager() else { + return false + } + return manager.connection.status.isStarted + } + + static func setStarted(_ started: Bool) async throws { + guard let manager = try await loadManager() else { + NSLog("[WidgetTunnelControl] No tunnel configuration found") + throw NSError(domain: "WidgetTunnelControl", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Tunnel configuration not found", + ]) + } + + if started { + if manager.isEnabled == false { + manager.isEnabled = true + try await manager.saveToPreferences() + } + do { + try manager.connection.startVPNTunnel() + } catch { + NSLog("[WidgetTunnelControl] startVPNTunnel failed: \(error.localizedDescription)") + throw error + } + } else { + manager.connection.stopVPNTunnel() + } + } + + private static func loadManager() async throws -> NETunnelProviderManager? { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + return managers.first + } +} + diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index 3271573..f7e3b93 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -39,7 +39,6 @@ 3AE395F42C21A5CA00647718 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AE395F32C21A5CA00647718 /* WidgetKit.framework */; }; 3AE395F62C21A5CA00647718 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AE395F52C21A5CA00647718 /* SwiftUI.framework */; }; 3AE396032C21A5CC00647718 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 3AE396072C21A60C00647718 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3AE4D0BD2A6E2DDC009FEA9E /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3AE4D0BE2A6E2DDC009FEA9E /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3AE595822F08C3D400C13426 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; @@ -192,13 +191,6 @@ remoteGlobalIDString = 3AE395F12C21A5CA00647718; remoteInfo = WidgetExtensionExtension; }; - 3AE396092C21A60C00647718 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 3AEC211C2A459B4700A63465; - remoteInfo = Library; - }; 3AE4D0BA2A6E2C55009FEA9E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; @@ -707,7 +699,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3AE396072C21A60C00647718 /* Library.framework in Frameworks */, 3AE395F62C21A5CA00647718 /* SwiftUI.framework in Frameworks */, 3AE395F42C21A5CA00647718 /* WidgetKit.framework in Frameworks */, ); @@ -1098,7 +1089,6 @@ buildRules = ( ); dependencies = ( - 3AE3960A2C21A60C00647718 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */, @@ -1722,11 +1712,6 @@ target = 3AE395F12C21A5CA00647718 /* WidgetExtension */; targetProxy = 3AE396012C21A5CC00647718 /* PBXContainerItemProxy */; }; - 3AE3960A2C21A60C00647718 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 3AEC211C2A459B4700A63465 /* Library */; - targetProxy = 3AE396092C21A60C00647718 /* PBXContainerItemProxy */; - }; 3AE4D0BB2A6E2C55009FEA9E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3AEC211C2A459B4700A63465 /* Library */;