Skip to content

Commit

Permalink
Add iOS 18 control widget
Browse files Browse the repository at this point in the history
  • Loading branch information
nekohasekai committed Jun 19, 2024
1 parent 75a79c9 commit 6735fe6
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public struct ActiveDashboardView: View {
}
}

@available(iOS 16.0, *)
@ViewBuilder
private var content1: some View {
TabView(selection: $selection) {
Expand Down
19 changes: 13 additions & 6 deletions Library/Network/CommandClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ public class CommandClient: ObservableObject {
case .connections:
clientOptions.command = LibboxCommandConnections
}
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
switch connectionType {
case .log:
clientOptions.statusInterval = Int64(500 * NSEC_PER_MSEC)
default:
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
}
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)!
do {
for i in 0 ..< 10 {
Expand Down Expand Up @@ -158,15 +163,17 @@ public class CommandClient: ObservableObject {
}
}

func writeLog(_ message: String?) {
guard let message else {
func writeLog(_ messageList: (any LibboxStringIteratorProtocol)?) {
guard let messageList else {
return
}
DispatchQueue.main.async { [self] in
if commandClient.logList.count > commandClient.logMaxLines {
commandClient.logList.removeFirst()
if commandClient.logList.count >= commandClient.logMaxLines {
commandClient.logList.removeSubrange(0 ..< Int(messageList.len()))
}
while messageList.hasNext() {
commandClient.logList.append(messageList.next())
}
commandClient.logList.append(message)
}
}

Expand Down
3 changes: 3 additions & 0 deletions Library/Network/ExtensionProfile.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Foundation
import Libbox
import NetworkExtension
import WidgetKit

public class ExtensionProfile: ObservableObject {
public static let controlKind = "io.nekohasekai.sfa.widget.ServiceToggle"

private let manager: NEVPNManager
private var connection: NEVPNConnection
private var observer: Any?
Expand Down
13 changes: 13 additions & 0 deletions Library/Network/ExtensionProvider.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation
import Libbox
import NetworkExtension
#if os(iOS)
import WidgetKit
#endif

open class ExtensionProvider: NEPacketTunnelProvider {
public var username: String? = nil
Expand Down Expand Up @@ -48,6 +51,11 @@ open class ExtensionProvider: NEPacketTunnelProvider {
}
writeMessage("(packet-tunnel): Here I stand")
await startService()
#if os(iOS)
if #available(iOS 18.0, *) {
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
}
#endif
}

func writeMessage(_ message: String) {
Expand Down Expand Up @@ -153,6 +161,11 @@ open class ExtensionProvider: NEPacketTunnelProvider {
await SharedPreferences.startedByUser.set(reason == .userInitiated)
}
#endif
#if os(iOS)
if #available(iOS 18.0, *) {
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
}
#endif
}

override open func handleAppMessage(_ messageData: Data) async -> Data? {
Expand Down
11 changes: 10 additions & 1 deletion Library/Network/NEVPNStatus+isConnected.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import NetworkExtension
public extension NEVPNStatus {
var isEnabled: Bool {
switch self {
case .connected, .disconnected, .reasserting:
case .connecting, .connected, .disconnected, .reasserting:
return true
default:
return false
}
}

var isStarted: Bool {
switch self {
case .connecting, .connected, .reasserting:
return true
default:
return false
Expand Down
22 changes: 22 additions & 0 deletions WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@
"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" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import SwiftUI
import WidgetKit

@main
struct WidgetExtensionBundle: WidgetBundle {
struct ExtensionBundle: WidgetBundle {
var body: some Widget {
WidgetExtension()
ServiceToggleControl()
}
}
46 changes: 0 additions & 46 deletions WidgetExtension/Intents.swift

This file was deleted.

66 changes: 66 additions & 0 deletions WidgetExtension/ServiceToggleControl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import AppIntents
import Library
import SwiftUI
import WidgetKit

struct ServiceToggleControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: ExtensionProfile.controlKind,
provider: Provider()
) { value in
ControlWidgetToggle(
"sing-box",
isOn: value,
action: ToggleServiceIntent()
) { isOn in
Label(isOn ? "Running" : "Stopped", systemImage: "shippingbox.fill")
}
.tint(.init(red: CGFloat(Double(69) / 255), green: CGFloat(Double(90) / 255), blue: CGFloat(Double(100) / 255)))
}
.displayName("Toggle")
.description("Start or stop sing-box service.")
}
}

extension ServiceToggleControl {
struct Provider: ControlValueProvider {
var previewValue: Bool {
false
}

func currentValue() async throws -> Bool {
guard let extensionProfile = try await (ExtensionProfile.load()) else {
return false
}
return extensionProfile.status.isStarted
}
}
}

struct ToggleServiceIntent: SetValueIntent {
static var title: LocalizedStringResource = "Toggle sing-box"

static var description =
IntentDescription("Toggle sing-box service")

static var parameterSummary: some ParameterSummary {
Summary("Toggle sing-box service")
}

@Parameter(title: "Service status")
var value: Bool

func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
guard let extensionProfile = try await (ExtensionProfile.load()) else {
return .result(value: false)
}
if value {
try await extensionProfile.start()
return .result(value: true)
} else {
try await extensionProfile.stop()
return .result(value: false)
}
}
}
89 changes: 0 additions & 89 deletions WidgetExtension/WidgetExtension.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.sagernet.sfa</string>
<string>group.io.nekohasekai.sfa</string>
</array>
</dict>
</plist>
Loading

0 comments on commit 6735fe6

Please sign in to comment.