Add iOS 18 control widget

This commit is contained in:
世界
2024-10-06 21:41:10 +08:00
parent 9dadf5f648
commit 7e67e68670
12 changed files with 306 additions and 150 deletions
@@ -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()
}
}
-46
View File
@@ -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()
}
}
+1 -1
View File
@@ -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>
-89
View File
@@ -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])
}
}