Skip to content

Commit 6eb7311

Browse files
committed
Add iOS 18 control widget
1 parent 493a0f4 commit 6eb7311

File tree

12 files changed

+306
-150
lines changed

12 files changed

+306
-150
lines changed

ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public struct ActiveDashboardView: View {
111111
}
112112
}
113113

114+
@available(iOS 16.0, *)
114115
@ViewBuilder
115116
private var content1: some View {
116117
TabView(selection: $selection) {

Library/Network/CommandClient.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ public class CommandClient: ObservableObject {
107107
case .connections:
108108
clientOptions.command = LibboxCommandConnections
109109
}
110-
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
110+
switch connectionType {
111+
case .log:
112+
clientOptions.statusInterval = Int64(500 * NSEC_PER_MSEC)
113+
default:
114+
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
115+
}
111116
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)!
112117
do {
113118
for i in 0 ..< 10 {
@@ -152,21 +157,23 @@ public class CommandClient: ObservableObject {
152157
}
153158
}
154159

155-
func clearLog() {
160+
func clearLogs() {
156161
DispatchQueue.main.async { [self] in
157162
commandClient.logList.removeAll()
158163
}
159164
}
160165

161-
func writeLog(_ message: String?) {
162-
guard let message else {
166+
func writeLogs(_ messageList: (any LibboxStringIteratorProtocol)?) {
167+
guard let messageList else {
163168
return
164169
}
165170
DispatchQueue.main.async { [self] in
166-
if commandClient.logList.count > commandClient.logMaxLines {
167-
commandClient.logList.removeFirst()
171+
if commandClient.logList.count >= commandClient.logMaxLines {
172+
commandClient.logList.removeSubrange(0 ..< Int(messageList.len()))
173+
}
174+
while messageList.hasNext() {
175+
commandClient.logList.append(messageList.next())
168176
}
169-
commandClient.logList.append(message)
170177
}
171178
}
172179

Library/Network/ExtensionProfile.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Libbox
33
import NetworkExtension
44

55
public class ExtensionProfile: ObservableObject {
6+
public static let controlKind = "io.nekohasekai.sfavt.widget.ServiceToggle"
7+
68
private let manager: NEVPNManager
79
private var connection: NEVPNConnection
810
private var observer: Any?

Library/Network/ExtensionProvider.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import Foundation
22
import Libbox
33
import NetworkExtension
4+
#if os(iOS)
5+
import WidgetKit
6+
#endif
47

58
open class ExtensionProvider: NEPacketTunnelProvider {
69
public var username: String? = nil
@@ -48,13 +51,16 @@ open class ExtensionProvider: NEPacketTunnelProvider {
4851
}
4952
writeMessage("(packet-tunnel): Here I stand")
5053
await startService()
54+
#if os(iOS)
55+
if #available(iOS 18.0, *) {
56+
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
57+
}
58+
#endif
5159
}
5260

5361
func writeMessage(_ message: String) {
5462
if let commandServer {
5563
commandServer.writeMessage(message)
56-
} else {
57-
NSLog(message)
5864
}
5965
}
6066

@@ -153,6 +159,11 @@ open class ExtensionProvider: NEPacketTunnelProvider {
153159
await SharedPreferences.startedByUser.set(reason == .userInitiated)
154160
}
155161
#endif
162+
#if os(iOS)
163+
if #available(iOS 18.0, *) {
164+
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
165+
}
166+
#endif
156167
}
157168

158169
override open func handleAppMessage(_ messageData: Data) async -> Data? {

Library/Network/NEVPNStatus+isConnected.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import NetworkExtension
44
public extension NEVPNStatus {
55
var isEnabled: Bool {
66
switch self {
7-
case .connected, .disconnected, .reasserting:
7+
case .connecting, .connected, .disconnected, .reasserting:
8+
return true
9+
default:
10+
return false
11+
}
12+
}
13+
14+
var isStarted: Bool {
15+
switch self {
16+
case .connecting, .connected, .reasserting:
817
return true
918
default:
1019
return false

WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@
44
"idiom" : "universal",
55
"platform" : "ios",
66
"size" : "1024x1024"
7+
},
8+
{
9+
"appearances" : [
10+
{
11+
"appearance" : "luminosity",
12+
"value" : "dark"
13+
}
14+
],
15+
"idiom" : "universal",
16+
"platform" : "ios",
17+
"size" : "1024x1024"
18+
},
19+
{
20+
"appearances" : [
21+
{
22+
"appearance" : "luminosity",
23+
"value" : "tinted"
24+
}
25+
],
26+
"idiom" : "universal",
27+
"platform" : "ios",
28+
"size" : "1024x1024"
729
}
830
],
931
"info" : {

WidgetExtension/WidgetExtensionBundle.swift renamed to WidgetExtension/ExtensionBundle.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import SwiftUI
22
import WidgetKit
33

44
@main
5-
struct WidgetExtensionBundle: WidgetBundle {
5+
struct ExtensionBundle: WidgetBundle {
66
var body: some Widget {
7-
WidgetExtension()
7+
ServiceToggleControl()
88
}
99
}

WidgetExtension/Intents.swift

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import AppIntents
2+
import Library
3+
import SwiftUI
4+
import WidgetKit
5+
6+
struct ServiceToggleControl: ControlWidget {
7+
var body: some ControlWidgetConfiguration {
8+
StaticControlConfiguration(
9+
kind: ExtensionProfile.controlKind,
10+
provider: Provider()
11+
) { value in
12+
ControlWidgetToggle(
13+
"sing-box",
14+
isOn: value,
15+
action: ToggleServiceIntent()
16+
) { isOn in
17+
Label(isOn ? "Running" : "Stopped", systemImage: "shippingbox.fill")
18+
}
19+
.tint(.init(red: CGFloat(Double(69) / 255), green: CGFloat(Double(90) / 255), blue: CGFloat(Double(100) / 255)))
20+
}
21+
.displayName("Toggle")
22+
.description("Start or stop sing-box service.")
23+
}
24+
}
25+
26+
extension ServiceToggleControl {
27+
struct Provider: ControlValueProvider {
28+
var previewValue: Bool {
29+
false
30+
}
31+
32+
func currentValue() async throws -> Bool {
33+
guard let extensionProfile = try await (ExtensionProfile.load()) else {
34+
return false
35+
}
36+
return extensionProfile.status.isStarted
37+
}
38+
}
39+
}
40+
41+
struct ToggleServiceIntent: SetValueIntent, LiveActivityIntent {
42+
static var title: LocalizedStringResource = "Toggle sing-box"
43+
44+
static var description =
45+
IntentDescription("Toggle sing-box service")
46+
47+
@Parameter(title: "Running")
48+
var value: Bool
49+
50+
func perform() async throws -> some IntentResult {
51+
guard let extensionProfile = try await (ExtensionProfile.load()) else {
52+
return .result()
53+
}
54+
if value {
55+
try await extensionProfile.start()
56+
} else {
57+
try await extensionProfile.stop()
58+
}
59+
return .result()
60+
}
61+
}

WidgetExtension/WidgetExtension.entitlements

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<dict>
55
<key>com.apple.security.application-groups</key>
66
<array>
7-
<string>group.org.sagernet.sfa</string>
7+
<string>group.io.nekohasekai.sfavt</string>
88
</array>
99
</dict>
1010
</plist>

0 commit comments

Comments
 (0)