Add CI/CD configuration and API documentation
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,46 @@
|
||||
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,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.sagernet.sfa</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,89 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct WidgetExtensionBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
WidgetExtension()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user