Add iOS 18 control widget
This commit is contained in:
@@ -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" : {
|
||||
|
||||
@@ -2,8 +2,8 @@ import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct WidgetExtensionBundle: WidgetBundle {
|
||||
struct ExtensionBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
WidgetExtension()
|
||||
ServiceToggleControl()
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import AppIntents
|
||||
import Library
|
||||
import WidgetKit
|
||||
|
||||
struct ConfigurationIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource = "Configuration"
|
||||
static var description = IntentDescription("Configuration sing-bix widget.")
|
||||
}
|
||||
|
||||
struct StartServiceIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Start sing-box"
|
||||
|
||||
static var description =
|
||||
IntentDescription("Start sing-box servie")
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard let extensionProfile = try await (ExtensionProfile.load()) else {
|
||||
throw NSError(domain: "NetworkExtension not installed", code: 0)
|
||||
}
|
||||
if extensionProfile.status == .connected {
|
||||
return .result()
|
||||
} else {
|
||||
try await extensionProfile.start()
|
||||
}
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct StopServiceIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Stop sing-box"
|
||||
|
||||
static var description =
|
||||
IntentDescription("Stop sing-box service")
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Stop sing-box service")
|
||||
}
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard let extensionProfile = try await (ExtensionProfile.load()) else {
|
||||
return .result()
|
||||
}
|
||||
extensionProfile.stop()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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, LiveActivityIntent {
|
||||
static var title: LocalizedStringResource = "Toggle sing-box"
|
||||
|
||||
static var description =
|
||||
IntentDescription("Toggle sing-box service")
|
||||
|
||||
@Parameter(title: "Running")
|
||||
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()
|
||||
}
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.sagernet.sfa</string>
|
||||
<string>group.io.nekohasekai.sfavt</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import AppIntents
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct Provider: AppIntentTimelineProvider {
|
||||
func placeholder(in _: Context) -> ExtensionStatus {
|
||||
ExtensionStatus(date: .now, isConnected: false, profileList: [])
|
||||
}
|
||||
|
||||
func snapshot(for _: ConfigurationIntent, in _: Context) async -> ExtensionStatus {
|
||||
var status = ExtensionStatus(date: .now, isConnected: false, profileList: [])
|
||||
|
||||
do {
|
||||
status.isConnected = try await ExtensionProfile.load()?.status.isStrictConnected ?? false
|
||||
|
||||
let profileList = try ProfileManager.list()
|
||||
let selectedProfileID = SharedPreferences.selectedProfileID
|
||||
for profile in profileList {
|
||||
status.profileList.append(ProfileEntry(profile: profile, isSelected: profile.id == selectedProfileID))
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func timeline(for intent: ConfigurationIntent, in context: Context) async -> Timeline<ExtensionStatus> {
|
||||
await Timeline(entries: [snapshot(for: intent, in: context)], policy: .never)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtensionStatus: TimelineEntry {
|
||||
var date: Date
|
||||
var isConnected: Bool
|
||||
var profileList: [ProfileEntry]
|
||||
}
|
||||
|
||||
struct ProfileEntry {
|
||||
let profile: Profile
|
||||
let isSelected: Bool
|
||||
}
|
||||
|
||||
struct WidgetView: View {
|
||||
@Environment(\.widgetFamily) private var family
|
||||
|
||||
var status: ExtensionStatus
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
LabeledContent {
|
||||
Text(LibboxVersion())
|
||||
.font(.caption)
|
||||
} label: {
|
||||
Text("sing-box")
|
||||
.font(.headline)
|
||||
}
|
||||
VStack {
|
||||
viewBuilder {
|
||||
if !status.isConnected {
|
||||
Button(intent: StartServiceIntent()) {
|
||||
Image(systemName: "play.fill")
|
||||
}
|
||||
} else {
|
||||
Button(intent: StopServiceIntent()) {
|
||||
Image(systemName: "stop.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
.controlSize(.large)
|
||||
.invalidatableContent()
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetExtension: Widget {
|
||||
@State private var extensionProfile: ExtensionProfile?
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: "sing-box", intent: ConfigurationIntent.self, provider: Provider()) { status in
|
||||
WidgetView(status: status)
|
||||
}
|
||||
.supportedFamilies([.systemSmall])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user