commit f441b89efb7f3443f9395dfc98b132f8866bcb95 Author: 世界 Date: Thu Jun 29 18:37:30 2023 +0800 Init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b443cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea/ +/Libbox.xcframework/ +xcuserdata/ +.DS_Store/ \ No newline at end of file diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..43f030c --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +5.7 \ No newline at end of file diff --git a/ApplicationLibrary/ApplicationLibrary.swift b/ApplicationLibrary/ApplicationLibrary.swift new file mode 100644 index 0000000..3a001dd --- /dev/null +++ b/ApplicationLibrary/ApplicationLibrary.swift @@ -0,0 +1,5 @@ +import Foundation + +public class ApplicationLibrary { + public static let bundle = Bundle(for: ApplicationLibrary.self) +} diff --git a/ApplicationLibrary/Assets.xcassets/Contents.json b/ApplicationLibrary/Assets.xcassets/Contents.json new file mode 100644 index 0000000..74d6a72 --- /dev/null +++ b/ApplicationLibrary/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/ApplicationLibrary/Assets.xcassets/save.symbolset/Contents.json b/ApplicationLibrary/Assets.xcassets/save.symbolset/Contents.json new file mode 100644 index 0000000..51af668 --- /dev/null +++ b/ApplicationLibrary/Assets.xcassets/save.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "save-save_symbol.svg", + "idiom": "universal" + } + ] +} diff --git a/ApplicationLibrary/Assets.xcassets/save.symbolset/save-save_symbol.svg b/ApplicationLibrary/Assets.xcassets/save.symbolset/save-save_symbol.svg new file mode 100644 index 0000000..2b67790 --- /dev/null +++ b/ApplicationLibrary/Assets.xcassets/save.symbolset/save-save_symbol.svg @@ -0,0 +1,127 @@ + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + Template v.1.0 + + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ApplicationLibrary/Service/ProfileUpdateTask.swift b/ApplicationLibrary/Service/ProfileUpdateTask.swift new file mode 100644 index 0000000..f6a2dd2 --- /dev/null +++ b/ApplicationLibrary/Service/ProfileUpdateTask.swift @@ -0,0 +1,50 @@ +import Foundation +import Library + +public enum ProfileUpdateTask { + private static var timer: Timer? + + public static func setup() throws { + var earliestBeginDate: Date? + if let updatedAt = try oldestUpdated() { + if updatedAt > Date(timeIntervalSinceNow: -taskInterval) { + earliestBeginDate = updatedAt.addingTimeInterval(taskInterval) + } + } + timer = Timer(fire: earliestBeginDate ?? Date.now, interval: taskInterval, repeats: true, block: { _ in + do { + _ = try updateProfiles() + NSLog("profile update task succeed") + } catch { + NSLog("profile update task failed: \(error.localizedDescription)") + } + }) + } + + static let taskInterval: TimeInterval = 15 * 60 + + static func oldestUpdated() throws -> Date? { + let profiles = try ProfileManager.listAutoUpdateEnabled() + return profiles.map { profile in + profile.lastUpdated! + } + .min() + } + + static func updateProfiles() throws -> Bool { + let profiles = try ProfileManager.listAutoUpdateEnabled() + var success = true + for profile in profiles { + if profile.lastUpdated! > Date(timeIntervalSinceNow: -taskInterval) { + continue + } + do { + try profile.updateRemoteProfile() + } catch { + NSLog("Update profile \(profile.name) failed: \(error.localizedDescription)") + success = false + } + } + return success + } +} diff --git a/ApplicationLibrary/Service/UIProfileUpdateTask.swift b/ApplicationLibrary/Service/UIProfileUpdateTask.swift new file mode 100644 index 0000000..f35da8a --- /dev/null +++ b/ApplicationLibrary/Service/UIProfileUpdateTask.swift @@ -0,0 +1,47 @@ +import BackgroundTasks +import Foundation +import Library + +#if os(iOS) + public class UIProfileUpdateTask: BGAppRefreshTask { + public static let taskSchedulerPermittedIdentifier = "\(FilePath.packageName).update_profiles" + + public static func setup() async throws { + let success = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskSchedulerPermittedIdentifier, using: nil) { task in + NSLog("profile update task started") + do { + let success = try ProfileUpdateTask.updateProfiles() + try? scheduleUpdate(Date(timeIntervalSinceNow: ProfileUpdateTask.taskInterval)) + task.setTaskCompleted(success: success) + NSLog("profile update task succeed") + } catch { + try? scheduleUpdate(nil) + task.setTaskCompleted(success: false) + NSLog("profile update task failed: \(error.localizedDescription)") + } + task.expirationHandler = { + try? scheduleUpdate(nil) + NSLog("profile update task expired") + } + } + if !success { + throw NSError(domain: "register failed", code: 0) + } + if await BGTaskScheduler.shared.pendingTaskRequests().isEmpty { + var earliestBeginDate: Date? = nil + if let updatedAt = try ProfileUpdateTask.oldestUpdated() { + if updatedAt > Date(timeIntervalSinceNow: -ProfileUpdateTask.taskInterval) { + earliestBeginDate = updatedAt.addingTimeInterval(ProfileUpdateTask.taskInterval) + } + } + try scheduleUpdate(earliestBeginDate) + } + } + + private static func scheduleUpdate(_ earliestBeginDate: Date?) throws { + let request = BGAppRefreshTaskRequest(identifier: taskSchedulerPermittedIdentifier) + request.earliestBeginDate = earliestBeginDate + try BGTaskScheduler.shared.submit(request) + } + } +#endif diff --git a/ApplicationLibrary/Views/Abstract/Binding+Unwrap.swift b/ApplicationLibrary/Views/Abstract/Binding+Unwrap.swift new file mode 100644 index 0000000..f3d782d --- /dev/null +++ b/ApplicationLibrary/Views/Abstract/Binding+Unwrap.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public extension Binding { + func unwrapped(_ defaultValue: T) -> Binding where Value == T? { + Binding(get: { + wrappedValue ?? defaultValue + }, set: { newValue in + wrappedValue = newValue + }) + } +} diff --git a/ApplicationLibrary/Views/Abstract/Formtem.swift b/ApplicationLibrary/Views/Abstract/Formtem.swift new file mode 100644 index 0000000..12495ca --- /dev/null +++ b/ApplicationLibrary/Views/Abstract/Formtem.swift @@ -0,0 +1,35 @@ +import Foundation +import SwiftUI + +public func FormView(@ViewBuilder content: () -> some View) -> some View { + Form { + content() + } + #if os(macOS) + .formStyle(.grouped) + #endif +} + +public func FormTextItem(_ name: String, _ value: String) -> some View { + HStack { + Text(name) + Spacer() + Text(value) + .multilineTextAlignment(.trailing) + .font(Font.system(.caption, design: .monospaced)) + .textSelection(.enabled) + } +} + +public func FormItem(_ title: String, @ViewBuilder content: () -> some View) -> some View { + #if os(iOS) + HStack { + Text(title) + Spacer() + Spacer() + content() + } + #else + content() + #endif +} diff --git a/ApplicationLibrary/Views/Abstract/ViewBuilder.swift b/ApplicationLibrary/Views/Abstract/ViewBuilder.swift new file mode 100644 index 0000000..e13c1e4 --- /dev/null +++ b/ApplicationLibrary/Views/Abstract/ViewBuilder.swift @@ -0,0 +1,6 @@ +import Foundation +import SwiftUI + +public func viewBuilder(@ViewBuilder _ builder: () -> some View) -> some View { + builder() +} diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift new file mode 100644 index 0000000..7436d02 --- /dev/null +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -0,0 +1,144 @@ +import Foundation +import Libbox +import Library +import SwiftUI + +public struct ActiveDashboardView: View { + public static let NotificationUpdateSelectedProfile = Notification.Name("update-selected-profile") + + @Environment(\.scenePhase) var scenePhase + @Environment(\.selection) private var selection + + @EnvironmentObject private var profile: ExtensionProfile + + @State private var isLoading = true + @State private var profileList: [Profile] = [] + @State private var selectedProfileID: Int64! + @State private var reasserting = false + @State private var observer: Any? + + @State private var errorPresented = false + @State private var errorMessage = "" + + public init() {} + + public var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + await doReload() + } + } + } else { + if profileList.isEmpty { + Text("Empty profiles") + } else { + #if os(iOS) + StartStopButton() + #endif + if profile.status.isConnected { + Section("Status") { + ExtensionStatusView() + } + } + Section("Profile") { + #if os(iOS) + Picker(selection: $selectedProfileID) { + ForEach(profileList, id: \.id) { profile in + Text(profile.name).tag(profile.id) + } + } label: {} + .pickerStyle(.inline) + + #elseif os(macOS) + ForEach(profileList, id: \.id) { profile in + Picker(profile.name, selection: $selectedProfileID) { + Text("").tag(profile.id) + } + } + .pickerStyle(.radioGroup) + #endif + } + .onChange(of: selectedProfileID) { _ in + reasserting = true + Task.detached { + await switchProfile(selectedProfileID!) + } + } + .disabled(!profile.status.isSwitchable || reasserting) + } + } + } + #if os(iOS) + .onChange(of: scenePhase, perform: { newValue in + if newValue == .active { + Task.detached { + await doReload() + } + } + }) + .onChange(of: selection.wrappedValue, perform: { newValue in + if newValue == .dashboard { + Task.detached { + await doReload() + } + } + }) + #elseif os(macOS) + .onAppear { + if observer == nil { + observer = NotificationCenter.default.addObserver(forName: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { _ in + Task.detached { + await doReload() + } + }) + } + } + .onDisappear { + if let observer { + NotificationCenter.default.removeObserver(observer) + } + } + #endif + } + + private func doReload() { + defer { + isLoading = false + } + do { + profileList = try ProfileManager.list() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + if profileList.isEmpty { + return + } + + selectedProfileID = SharedPreferences.selectedProfileID + if profileList.filter({ profile in + profile.id == selectedProfileID + }) + .isEmpty { + selectedProfileID = profileList[0].id! + SharedPreferences.selectedProfileID = selectedProfileID + } + } + + private func switchProfile(_ newProfileID: Int64) { + SharedPreferences.selectedProfileID = newProfileID + NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil) + if profile.status.isConnected { + do { + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + reasserting = false + } +} diff --git a/ApplicationLibrary/Views/Dashboard/DashboardView.swift b/ApplicationLibrary/Views/Dashboard/DashboardView.swift new file mode 100644 index 0000000..d6d4698 --- /dev/null +++ b/ApplicationLibrary/Views/Dashboard/DashboardView.swift @@ -0,0 +1,17 @@ +import SwiftUI + +public struct DashboardView: View { + @Environment(\.extensionProfile) private var extensionProfile + + public init() {} + + public var body: some View { + FormView { + if let profile = extensionProfile.wrappedValue { + ActiveDashboardView().environmentObject(profile) + } else { + InstallProfileButton() + } + }.navigationTitle("Dashboard") + } +} diff --git a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift new file mode 100644 index 0000000..ff022f5 --- /dev/null +++ b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift @@ -0,0 +1,116 @@ +import Libbox +import Library +import SwiftUI + +public struct ExtensionStatusView: View { + @State private var commandClient: LibboxCommandClient? + @State private var message: LibboxStatusMessage? + @State private var connectTask: Task? + @State private var errorPresented = false + @State private var errorMessage = "" + + private let infoFont = Font.system(.caption, design: .monospaced) + + public init() {} + + public var body: some View { + viewBuilder { + if let message { + FormTextItem("Memory", LibboxFormatBytes(message.memory)) + FormTextItem("Goroutines", "\(message.goroutines)") + FormTextItem("Connections", "\(message.connections)").contextMenu { + Button("Close", role: .destructive) { + Task.detached { + closeConnections() + } + } + } + } else { + FormTextItem("Memory", "Loading...") + FormTextItem("Goroutines", "Loading...") + FormTextItem("Connections", "Loading...") + } + } + + .onAppear(perform: doReload) + .onDisappear { + connectTask?.cancel() + if let commandClient { + try? commandClient.disconnect() + } + commandClient = nil + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + private func doReload() { + connectTask?.cancel() + connectTask = Task.detached { + await connect() + } + } + + private func connect() async { + let clientOptions = LibboxCommandClientOptions() + clientOptions.command = LibboxCommandStatus + clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) + let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, statusHandler(self), clientOptions)! + + do { + for i in 0 ..< 10 { + try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) + try Task.checkCancellation() + let isConnected: Bool + do { + try client.connect() + isConnected = true + } catch { + isConnected = false + } + try Task.checkCancellation() + if isConnected { + commandClient = client + return + } + } + } catch { + NSLog("failed to connect status: \(error.localizedDescription)") + try? client.disconnect() + } + } + + private func closeConnections() { + do { + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.closeConnections() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + + private class statusHandler: NSObject, LibboxCommandClientHandlerProtocol { + private let statusView: ExtensionStatusView + + init(_ statusView: ExtensionStatusView) { + self.statusView = statusView + } + + func connected() {} + + func disconnected(_: String?) {} + + func writeLog(_: String?) {} + + func writeStatus(_ message: LibboxStatusMessage?) { + statusView.message = message + } + + func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {} + } +} diff --git a/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift b/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift new file mode 100644 index 0000000..15bb915 --- /dev/null +++ b/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift @@ -0,0 +1,35 @@ +import Library +import SwiftUI + +public struct InstallProfileButton: View { + @Environment(\.extensionProfile) private var extensionProfile + + @State private var errorPresented = false + @State private var errorMessage = "" + + public init() {} + + public var body: some View { + Button("Install NetworkExtension") { + Task { + await installProfile() + } + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + private func installProfile() async { + do { + try await ExtensionProfile.install() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } +} diff --git a/ApplicationLibrary/Views/Dashboard/StartStopButton.swift b/ApplicationLibrary/Views/Dashboard/StartStopButton.swift new file mode 100644 index 0000000..bf510ba --- /dev/null +++ b/ApplicationLibrary/Views/Dashboard/StartStopButton.swift @@ -0,0 +1,90 @@ +import Library +import NetworkExtension +import SwiftUI + +public struct StartStopButton: View { + @Environment(\.extensionProfile) private var extensionProfile + + public init() {} + + public var body: some View { + viewBuilder { + if let profile = extensionProfile.wrappedValue { + Button0(profile) + } else { + #if os(iOS) + Toggle(isOn: .constant(false)) { + Text("Enabled") + } + #elseif os(macOS) + Button(action: {}, label: { + Label("Start", systemImage: "play.fill") + }) + .disabled(true) + #endif + } + } + } + + private struct Button0: View { + @Environment(\.logClient) private var logClient + @ObservedObject private var profile: ExtensionProfile + @State private var errorPresented = false + @State private var errorMessage = "" + + init(_ profile: ExtensionProfile) { + self.profile = profile + } + + var body: some View { + viewBuilder { + #if os(iOS) + Toggle(isOn: Binding(get: { + profile.status.isConnected + }, set: { newValue, _ in + Task.detached { + await switchProfile(newValue) + } + })) { + Text("Enabled") + } + #elseif os(macOS) + Button(action: { + Task.detached { + await switchProfile(!profile.status.isConnected) + } + }, label: { + if !profile.status.isConnected { + Label("Start", systemImage: "play.fill") + } else { + Label("Stop", systemImage: "stop.fill") + } + }) + #endif + } + .disabled(!profile.status.isEnabled) + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + private func switchProfile(_ isEnabled: Bool) async { + do { + if isEnabled { + try await profile.start() + logClient.wrappedValue?.reconnect() + } else { + profile.stop() + } + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + } + } +} diff --git a/ApplicationLibrary/Views/EnvironmentValues.swift b/ApplicationLibrary/Views/EnvironmentValues.swift new file mode 100644 index 0000000..19e8374 --- /dev/null +++ b/ApplicationLibrary/Views/EnvironmentValues.swift @@ -0,0 +1,57 @@ +import Foundation +import Library +import SwiftUI + +public extension EnvironmentValues { + private struct showMenuBarExtraKey: EnvironmentKey { + static let defaultValue: Binding = .constant(true) + } + + var showMenuBarExtra: Binding { + get { + self[showMenuBarExtraKey.self] + } + set { + self[showMenuBarExtraKey.self] = newValue + } + } + + private struct selectionKey: EnvironmentKey { + static let defaultValue: Binding = .constant(.dashboard) + } + + var selection: Binding { + get { + self[selectionKey.self] + } + set { + self[selectionKey.self] = newValue + } + } + + private struct extensionProfileKey: EnvironmentKey { + static let defaultValue: Binding = .constant(nil) + } + + var extensionProfile: Binding { + get { + self[extensionProfileKey.self] + } + set { + self[extensionProfileKey.self] = newValue + } + } + + private struct logClientKey: EnvironmentKey { + static let defaultValue: Binding = .constant(nil) + } + + var logClient: Binding { + get { + self[logClientKey.self] + } + set { + self[logClientKey.self] = newValue + } + } +} diff --git a/ApplicationLibrary/Views/Groups/GroupItemView.swift b/ApplicationLibrary/Views/Groups/GroupItemView.swift new file mode 100644 index 0000000..cf4a374 --- /dev/null +++ b/ApplicationLibrary/Views/Groups/GroupItemView.swift @@ -0,0 +1,92 @@ +import Libbox +import Library +import SwiftUI + +public struct GroupItemView: View { + private let _group: Binding + private var group: OutboundGroup { + _group.wrappedValue + } + + private let item: OutboundGroupItem + public init(_ group: Binding, _ item: OutboundGroupItem) { + _group = group + self.item = item + } + + @State private var errorPresented = false + @State private var errorMessage = "" + + public var body: some View { + HStack { + if group.selected == item.tag { + Rectangle() + .fill(Color.accentColor) + .frame(width: 6) + } else { + Rectangle() + .fill(.clear) + .frame(width: 6) + } + VStack { + HStack { + Text(item.tag) + .truncationMode(.tail) + .lineLimit(1) + .font(.system(size: 14)) + Spacer(minLength: 6) + } + Spacer(minLength: 6) + HStack(alignment: .center) { + Text(item.type) + .foregroundColor(.secondary) + .font(.system(size: 12)) + Spacer(minLength: 6) + if item.urlTestDelay > 0 { + Text(item.delayString) + .foregroundColor(item.delayColor) + .font(.system(size: 11)) + } + } + } + .frame(height: 36) + .padding([.top, .bottom, .trailing], 12) + } + .background(backgroundColor) + .onTapGesture { + if group.selectable, group.selected != item.tag { + Task.detached { + selectOutbound() + } + } + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + private func selectOutbound() { + do { + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.selectOutbound(group.tag, outboundTag: item.tag) + var newGroup = group + newGroup.selected = item.tag + _group.wrappedValue = newGroup + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + } + + private var backgroundColor: Color { + #if os(iOS) + return Color(uiColor: .secondarySystemGroupedBackground) + #elseif os(macOS) + return Color(nsColor: .textBackgroundColor) + #endif + } +} diff --git a/ApplicationLibrary/Views/Groups/GroupListView.swift b/ApplicationLibrary/Views/Groups/GroupListView.swift new file mode 100644 index 0000000..48fa28d --- /dev/null +++ b/ApplicationLibrary/Views/Groups/GroupListView.swift @@ -0,0 +1,119 @@ +import Libbox +import Library +import SwiftUI + +public struct GroupListView: View { + @State private var isLoading = true + @State private var connectTask: Task? + @State private var commandClient: LibboxCommandClient? + @State private var groups: [OutboundGroup] = [] + @State private var groupExpand: [String: Bool] = [:] + + public init() {} + public var body: some View { + VStack { + if isLoading { + Text("Loading...") + } else if !groups.isEmpty { + ScrollView { + VStack { + ForEach(groups, id: \.hashValue) { it in + GroupView(it, Binding(get: { + groupExpand[it.tag] ?? it.selectable + }, set: { newValue in + groupExpand[it.tag] = newValue + })) + Spacer() + } + }.padding() + } + } else { + Text("Empty groups") + } + } + .onAppear(perform: doReload) + .onDisappear { + connectTask?.cancel() + if let commandClient { + try? commandClient.disconnect() + } + commandClient = nil + } + .navigationTitle("Groups") + } + + private func doReload() { + connectTask?.cancel() + connectTask = Task.detached { + await connect() + } + } + + private func connect() async { + let clientOptions = LibboxCommandClientOptions() + clientOptions.command = LibboxCommandGroup + clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) + let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, groupsHandler(self), clientOptions)! + + do { + for i in 0 ..< 10 { + try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) + try Task.checkCancellation() + let isConnected: Bool + do { + try client.connect() + isConnected = true + } catch { + isConnected = false + } + try Task.checkCancellation() + if isConnected { + commandClient = client + return + } + } + } catch { + NSLog("failed to connect status: \(error.localizedDescription)") + try? client.disconnect() + } + } + + private func setGroups(_ groupIterator: LibboxOutboundGroupIteratorProtocol) { + var goGroups = [LibboxOutboundGroup]() + while groupIterator.hasNext() { + goGroups.append(groupIterator.next()!) + } + var groups = [OutboundGroup]() + for goGroup in goGroups { + var items = [OutboundGroupItem]() + let itemIterator = goGroup.getItems()! + while itemIterator.hasNext() { + let goItem = itemIterator.next()! + items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay))) + } + groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, items: items)) + } + self.groups = groups + isLoading = false + } + + private class groupsHandler: NSObject, LibboxCommandClientHandlerProtocol { + private let groupListView: GroupListView + + init(_ statusView: GroupListView) { + groupListView = statusView + } + + func connected() {} + + func disconnected(_: String?) {} + + func writeLog(_: String?) {} + + func writeStatus(_: LibboxStatusMessage?) {} + + func writeGroups(_ groupIterator: LibboxOutboundGroupIteratorProtocol?) { + groupListView.setGroups(groupIterator!) + } + } +} diff --git a/ApplicationLibrary/Views/Groups/GroupView.swift b/ApplicationLibrary/Views/Groups/GroupView.swift new file mode 100644 index 0000000..5423390 --- /dev/null +++ b/ApplicationLibrary/Views/Groups/GroupView.swift @@ -0,0 +1,141 @@ +import Libbox +import Library +import SwiftUI + +public struct GroupView: View { + private var expland: Binding + @State private var group: OutboundGroup + @State private var geometryWidth: CGFloat = 300 + + @State private var errorPresented = false + @State private var errorMessage = "" + + public init(_ group: OutboundGroup, _ expland: Binding) { + self.group = group + self.expland = expland + } + + private var title: some View { + HStack { + Text(group.tag) + .font(.system(size: 17)) + Text(group.displayType) + .font(.system(size: 13)) + .foregroundColor(.secondary) + Text("\(group.items.count)") + .font(.system(size: 11)) + .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) + .background(Color.gray.opacity(0.5)) + .cornerRadius(4) + Button { + expland.wrappedValue = !expland.wrappedValue + } label: { + if expland.wrappedValue { + Image(systemName: "arrow.down.to.line") + } else { + Image(systemName: "arrow.up.to.line") + } + } + #if os(macOS) + .buttonStyle(.plain) + #endif + Button { + Task.detached { + doURLTest() + } + } label: { + Image(systemName: "bolt.fill") + } + #if os(macOS) + .buttonStyle(.plain) + #endif + Spacer(minLength: 6) + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + public var body: some View { + Section { + if expland.wrappedValue { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), + count: explandColumnCount())) + { + ForEach(group.items, id: \.tag) { it in + GroupItemView($group, it) + } + } + } else { + VStack { + ForEach(Array(itemGroups.enumerated()), id: \.offset) { items in + HStack { + ForEach(items.element, id: \.tag) { it in + Rectangle() + .fill(it.delayColor) + .frame(width: 10, height: 10) + } + }.frame(maxWidth: .infinity, alignment: .topLeading) + } + } + } + } header: { + title + .frame(maxWidth: .infinity, alignment: .topLeading) + } + .background { + GeometryReader { geometry in + Rectangle() + .fill(.clear) + .frame(height: 1) + .onChange(of: geometry.size.width) { newValue in + geometryWidth = newValue + } + .onAppear { + geometryWidth = geometry.size.width + } + }.padding() + } + } + + private var itemGroups: [[OutboundGroupItem]] { + let count = Int(Int(geometryWidth) / 20) + if count == 0 { + return [group.items] + } else { + return group.items.chunked( + into: count + ) + } + } + + private func explandColumnCount() -> Int { + let count = Int(Int(geometryWidth) / 180) + #if os(iOS) + return count < 2 ? 2 : count + #else + return count < 1 ? 1 : count + #endif + } + + private func doURLTest() { + do { + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.urlTest(group.tag) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } +} + +private extension Array { + func chunked(into size: Int) -> [[Element]] { + stride(from: 0, to: count, by: size).map { + Array(self[$0 ..< Swift.min($0 + size, count)]) + } + } +} diff --git a/ApplicationLibrary/Views/Groups/OutboundGroup.swift b/ApplicationLibrary/Views/Groups/OutboundGroup.swift new file mode 100644 index 0000000..09eb008 --- /dev/null +++ b/ApplicationLibrary/Views/Groups/OutboundGroup.swift @@ -0,0 +1,32 @@ +import Foundation +import SwiftUI + +public struct OutboundGroup: Codable { + let tag: String + let type: String + var selected: String + let selectable: Bool + let items: [OutboundGroupItem] + + var hashValue: Int { + var value = tag.hashValue + (value, _) = value.addingReportingOverflow(selected.hashValue) + for item in items { + (value, _) = value.addingReportingOverflow(item.urlTestTime.hashValue) + } + return value + } +} + +public extension OutboundGroup { + var displayType: String { + switch type { + case "selector": + return "Selector" + case "urltest": + return "URLTest" + default: + return "Unknown" + } + } +} diff --git a/ApplicationLibrary/Views/Groups/OutboundGroupItem.swift b/ApplicationLibrary/Views/Groups/OutboundGroupItem.swift new file mode 100644 index 0000000..a03c909 --- /dev/null +++ b/ApplicationLibrary/Views/Groups/OutboundGroupItem.swift @@ -0,0 +1,29 @@ +import Foundation +import SwiftUI + +public struct OutboundGroupItem: Codable { + public let tag: String + public let type: String + + public let urlTestTime: Date + public let urlTestDelay: UInt16 +} + +public extension OutboundGroupItem { + var delayString: String { + "\(urlTestDelay)ms" + } + + var delayColor: Color { + switch urlTestDelay { + case 0: + return .gray + case ..<800: + return .green + case 800 ..< 1500: + return .yellow + default: + return .orange + } + } +} diff --git a/ApplicationLibrary/Views/Log/LogClient.swift b/ApplicationLibrary/Views/Log/LogClient.swift new file mode 100644 index 0000000..b130b9c --- /dev/null +++ b/ApplicationLibrary/Views/Log/LogClient.swift @@ -0,0 +1,108 @@ +import Foundation +import Libbox +import Library +import SwiftUI + +public class LogClient: ObservableObject { + private var maxLines: Int + @Published public var isConnected: Bool + @Published public var logList: [String] + + private var commandClient: LibboxCommandClient! + private var connectTask: Task? + + public init(_ maxLines: Int) { + self.maxLines = maxLines + isConnected = false + logList = [] + } + + deinit { + if let connectTask { + connectTask.cancel() + } + if let commandClient { + try? commandClient.disconnect() + } + } + + public func reconnect() { + if isConnected { + return + } + if let connectTask { + connectTask.cancel() + } + connectTask = Task.detached { + await self.connect() + } + } + + private func connect() async { + let clientOptions = LibboxCommandClientOptions() + clientOptions.command = LibboxCommandLog + clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) + let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, logHandler(self), clientOptions)! + + do { + for i in 0 ..< 10 { + try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) + try Task.checkCancellation() + let isConnected: Bool + do { + try client.connect() + isConnected = true + } catch { + isConnected = false + } + try Task.checkCancellation() + if isConnected { + commandClient = client + return + } + } + } catch { + try? client.disconnect() + } + } + + private class logHandler: NSObject, LibboxCommandClientHandlerProtocol { + private let logClient: LogClient + + init(_ logClient: LogClient) { + self.logClient = logClient + } + + @MainActor + func connected() { + logClient.logList.removeAll() + logClient.isConnected = true + } + + @MainActor + func disconnected(_ message: String?) { + if let message { + logClient.logList.append("(log client closed) \(message)") + } else { + logClient.logList.append("(log client closed)") + } + try? logClient.commandClient?.disconnect() + logClient.commandClient = nil + logClient.isConnected = false + } + + @MainActor + func writeLog(_ message: String?) { + guard let message else { + return + } + if logClient.logList.count > logClient.maxLines { + logClient.logList.removeFirst() + } + logClient.logList.append(message) + } + + func writeStatus(_: LibboxStatusMessage?) {} + func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {} + } +} diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift new file mode 100644 index 0000000..bcedf0a --- /dev/null +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -0,0 +1,72 @@ +import SwiftUI + +public struct LogView: View { + @Environment(\.logClient) private var logClient + + public init() {} + + public var body: some View { + viewBuilder { + if let logClient = logClient.wrappedValue { + LogView0().environmentObject(logClient) + } else { + Text("Service not started") + } + } + .navigationTitle("Logs") + } + + private struct LogView0: View { + @Environment(\.selection) private var selection + @Environment(\.extensionProfile) private var extensionProfile + @EnvironmentObject private var logClient: LogClient + + private let logFont = Font.system(.caption2, design: .monospaced) + + var body: some View { + viewBuilder { + if logClient.logList.isEmpty { + VStack { + if logClient.isConnected { + Text("Empty logs") + } else { + Text("Service not started").onAppear(perform: connectLog) + } + }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } else { + ScrollViewReader { reader in + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in + Text(it.element) + .font(logFont) + Spacer(minLength: 5) + } + + .onChange(of: logClient.logList.count) { newCount in + withAnimation { + reader.scrollTo(newCount - 1) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding() + } + .onAppear { + reader.scrollTo(logClient.logList.count - 1) + } + } + } + } + } + + private func connectLog() { + guard let profile = extensionProfile.wrappedValue else { + return + } + if profile.status.isConnected, !logClient.isConnected { + logClient.reconnect() + } + } + } +} diff --git a/ApplicationLibrary/Views/NavigationPage.swift b/ApplicationLibrary/Views/NavigationPage.swift new file mode 100644 index 0000000..9d07eee --- /dev/null +++ b/ApplicationLibrary/Views/NavigationPage.swift @@ -0,0 +1,86 @@ +import Foundation +import Library +import SwiftUI + +public enum NavigationPage: Int, CaseIterable, Identifiable { + public var id: Self { + self + } + + case dashboard + case groups + case logs + case profiles + case settings +} + +public extension NavigationPage { + var label: some View { + Label(title, systemImage: iconImage) + } + + var title: String { + switch self { + case .dashboard: + return NSLocalizedString("Dashboard", comment: "") + case .groups: + return NSLocalizedString("Groups", comment: "") + case .logs: + return NSLocalizedString("Logs", comment: "") + case .profiles: + return NSLocalizedString("Profiles", comment: "") + case .settings: + return NSLocalizedString("Settings", comment: "") + } + } + + private var iconImage: String { + switch self { + case .dashboard: + return "text.and.command.macwindow" + case .groups: + return "rectangle.3.group.fill" + case .logs: + return "doc.text.fill" + case .profiles: + return "list.bullet.rectangle.fill" + case .settings: + return "gear.circle.fill" + } + } + + var contentView: some View { + viewBuilder { + switch self { + case .dashboard: + DashboardView() + case .groups: + GroupListView() + case .logs: + LogView() + case .profiles: + ProfileView() + case .settings: + SettingView() + } + } + #if os(iOS) + .background(Color(uiColor: .systemGroupedBackground)) + #endif + } + + func visible(_ profile: ExtensionProfile?) -> Bool { + switch self { + case .groups: + return profile?.status.isConnectedStrict == true + case .profiles, .settings: + #if os(iOS) + return profile?.status.isConnected != true + #else + fallthrough + #endif + default: + return true + } + } +} diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift new file mode 100644 index 0000000..cf138f5 --- /dev/null +++ b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift @@ -0,0 +1,149 @@ +import Foundation +import Library +import SwiftUI + +public struct EditProfileContentView: View { + #if os(macOS) + public static let windowID = "edit-profile-content" + #endif + + public struct Context: Codable, Hashable { + public let profileID: Int64 + public let readOnly: Bool + } + + private let profileID: Int64? + private let readOnly: Bool + + public init(_ context: Context?) { + profileID = context?.profileID + readOnly = context?.readOnly == true + } + + @Environment(\.dismiss) private var dismiss + + @State private var isLoading = true + @State private var profile: Profile! + @State private var profileContent: String = "" + @State private var isChanged = false + + @State private var errorPresented = false + @State private var errorMessage = "" + @State private var fatalError = false + + public var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + loadContent() + } + } + } else { + viewBuilder { + if readOnly { + TextEditor(text: .constant(profileContent)) + } else { + TextEditor(text: $profileContent) + } + } + .font(Font.system(.caption2, design: .monospaced)) + .disableAutocorrection(true) + #if os(iOS) + .textInputAutocapitalization(.none) + .background(Color(UIColor.secondarySystemGroupedBackground)) + #elseif os(macOS) + .padding() + #endif + .onChange(of: profileContent) { _ in + isChanged = true + } + } + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok"), action: { + if fatalError { + dismiss() + } + }) + ) + } + .navigationTitle(navigationTitle) + #if os(macOS) + .toolbar { + ToolbarItemGroup(placement: .navigation) { + if !readOnly { + Button(action: { + Task.detached { + saveContent() + } + }, label: { + Image("save", label: Text("Save")) + }) + .disabled(!isChanged) + } + } + } + #elseif os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if !readOnly { + Button("Save") { + Task.detached { + saveContent() + } + }.disabled(!isChanged) + } + } + } + .navigationBarTitleDisplayMode(.inline) + #endif + } + + private var navigationTitle: String { + if readOnly { + return "View Content" + } else { + return "Edit Content" + } + } + + private func loadContent() { + do { + try loadContent0() + } catch { + errorMessage = error.localizedDescription + fatalError = true + errorPresented = true + } + } + + private func loadContent0() throws { + guard let profileID else { + throw NSError(domain: "Context destroyed", code: 0) + } + guard let profile = try ProfileManager.get(profileID) else { + throw NSError(domain: "Profile missing", code: 0) + } + profileContent = try profile.read() + self.profile = profile + isLoading = false + } + + private func saveContent() { + guard let profile else { + return + } + do { + try profile.write(profileContent) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + isChanged = false + } +} diff --git a/ApplicationLibrary/Views/Profile/EditProfileView.swift b/ApplicationLibrary/Views/Profile/EditProfileView.swift new file mode 100644 index 0000000..5f5c25f --- /dev/null +++ b/ApplicationLibrary/Views/Profile/EditProfileView.swift @@ -0,0 +1,172 @@ +import Library +import SwiftUI + +public struct EditProfileView: View { + #if os(macOS) + @Environment(\.openWindow) private var openWindow + #endif + + @EnvironmentObject private var profile: Profile + + @State private var isLoading = false + @State private var isChanged = false + @State private var errorPresented = false + @State private var errorMessage = "" + + public init() {} + + public var body: some View { + FormView { + FormItem("Name") { + TextField("Name", text: $profile.name, prompt: Text("Required")) + .multilineTextAlignment(.trailing) + } + + Picker(selection: $profile.type) { + Text("Local").tag(ProfileType.local) + Text("iCloud").tag(ProfileType.icloud) + Text("Remote").tag(ProfileType.remote) + } label: { + Text("Type") + } + .disabled(true) + if profile.type == .icloud { + FormItem("Path") { + TextField("Path", text: $profile.path, prompt: Text("Required")) + .multilineTextAlignment(.trailing) + } + } else if profile.type == .remote { + FormItem("URL") { + TextField("URL", text: $profile.remoteURL.unwrapped(""), prompt: Text("Required")) + .multilineTextAlignment(.trailing) + } + Toggle("Auto Update", isOn: $profile.autoUpdate) + } + if profile.type == .remote { + Section("Status") { + FormTextItem("Last Updated", profile.lastUpdatedString) + } + } + #if os(iOS) + Section("Action") { + if profile.type != .remote { + NavigationLink { + EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) + } label: { + Text("Edit Content").foregroundColor(.accentColor) + } + } else { + NavigationLink { + EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) + } label: { + Text("View Content").foregroundColor(.accentColor) + } + Button("Update") { + isLoading = true + Task.detached { + await updateProfile() + } + } + .disabled(isLoading) + } + } + #endif + } + .onChange(of: profile.name, perform: { _ in + isChanged = true + }) + .onChange(of: profile.remoteURL, perform: { _ in + isChanged = true + }) + .onChange(of: profile.autoUpdate, perform: { _ in + isChanged = true + }) + .disabled(isLoading) + #if os(macOS) + .toolbar { + ToolbarItemGroup(placement: .navigation) { + Button(action: { + isLoading = true + Task.detached { + await saveProfile() + } + }, label: { + Image("save", bundle: ApplicationLibrary.bundle, label: Text("Save")) + }) + .disabled(isLoading || !isChanged) + if profile.type != .remote { + Button(action: { + openWindow(id: EditProfileContentView.windowID, value: EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) + }, label: { + Label("Edit Content", systemImage: "pencil") + }) + .disabled(isLoading) + } else { + Button(action: { + isLoading = true + Task.detached { + await updateProfile() + } + }, label: { + Label("Update", systemImage: "arrow.clockwise") + }) + .disabled(isLoading) + Button(action: { + openWindow(id: EditProfileContentView.windowID, value: EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) + }, label: { + Label("View Content", systemImage: "doc.text.fill") + }) + .disabled(isLoading) + } + } + } + #elseif os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + isLoading = true + Task.detached { + await saveProfile() + } + }.disabled(!isChanged) + } + } + #endif + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + .navigationTitle("Edit Profile") + } + + private func updateProfile() async { + defer { + isLoading = false + } + do { + try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) + try profile.updateRemoteProfile() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + + private func saveProfile() async { + do { + _ = try ProfileManager.update(profile) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + isChanged = false + isLoading = false + await MainActor.run { + NotificationCenter.default.post(name: ProfileView.notificationName, object: nil) + } + } +} diff --git a/ApplicationLibrary/Views/Profile/EditProfileWindowView.swift b/ApplicationLibrary/Views/Profile/EditProfileWindowView.swift new file mode 100644 index 0000000..d82609d --- /dev/null +++ b/ApplicationLibrary/Views/Profile/EditProfileWindowView.swift @@ -0,0 +1,69 @@ + +import Library +import SwiftUI + +#if os(macOS) + public struct EditProfileWindowView: View { + public static let windowID = "edit-profile" + + private var profileID: Int64? + + public init(_ profileID: Int64?) { + self.profileID = profileID + } + + @Environment(\.dismiss) private var dismiss + + @State private var isLoading = true + @State private var profile: Profile! + @State private var errorPresented = false + @State private var errorMessage = "" + + public var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + await doReload() + } + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok"), action: { + dismiss() + }) + ) + } + } else { + EditProfileView().environmentObject(profile!) + } + } + .onExitCommand { + dismiss() + } + } + + private func doReload() async { + guard let profileID else { + errorMessage = "Context destroyed" + errorPresented = true + return + } + do { + profile = try ProfileManager.get(profileID) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + if profile == nil { + errorMessage = "Profile deleted" + errorPresented = true + return + } + isLoading = false + } + } +#endif diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift new file mode 100644 index 0000000..2d0da38 --- /dev/null +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -0,0 +1,223 @@ +import Foundation +import Libbox +import Library +import SwiftUI + +public struct NewProfileView: View { + #if os(macOS) + public static let windowID = "new-profile" + #endif + + @Environment(\.dismiss) private var dismiss + + @State private var isSaving = false + @State private var profileName = "" + @State private var profileType = ProfileType.local + @State private var fileImport = false + @State private var fileURL: URL! + @State private var remotePath = "" + @State private var pickerPresented = false + @State private var errorPresented = false + @State private var errorMessage = "" + + private let callback: (() -> Void)? + public init(_ callback: (() -> Void)? = nil) { + self.callback = callback + } + + public var body: some View { + FormView { + FormItem("Name") { + TextField("Name", text: $profileName, prompt: Text("Required")) + .multilineTextAlignment(.trailing) + } + Picker(selection: $profileType) { + Text("Local").tag(ProfileType.local) + Text("iCloud").tag(ProfileType.icloud) + Text("Remote").tag(ProfileType.remote) + } label: { + Text("Type") + } + if profileType == .local { + Picker(selection: $fileImport) { + Text("Create New").tag(false) + Text("Import").tag(true) + } label: { + Text("File") + } + viewBuilder { + if fileImport { + HStack { + Text("File Path") + Spacer() + Spacer() + if let fileURL { + Button(fileURL.fileName) { + pickerPresented = true + } + } else { + Button("Choose") { + pickerPresented = true + } + } + } + } + } + } else if profileType == .icloud { + FormItem("Path") { + TextField("Path", text: $remotePath, prompt: Text("Required")) + .multilineTextAlignment(.trailing) + } + } else if profileType == .remote { + FormItem("URL") { + TextField("URL", text: $remotePath, prompt: Text("Required")) + .multilineTextAlignment(.trailing) + } + } + Section { + if !isSaving { + Button("Create") { + isSaving = true + Task.detached { + await createProfile() + } + } + } else { + ProgressView() + } + } + } + .navigationTitle("New Profile") + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + .fileImporter( + isPresented: $pickerPresented, + allowedContentTypes: [.json], + allowsMultipleSelection: false + ) { result in + do { + let urls = try result.get() + if !urls.isEmpty { + fileURL = urls[0] + } + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + } + } + + private func createProfile() async { + defer { + isSaving = false + } + if profileName.isEmpty { + errorMessage = "Missing profile name" + errorPresented = true + return + } + if remotePath.isEmpty { + if profileType == .icloud { + errorMessage = "Missing path" + errorPresented = true + return + } else if profileType == .remote { + errorMessage = "Missing URL" + errorPresented = true + return + } + } + do { + try createProfile0() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + await MainActor.run { + dismiss() + if let callback { + callback() + } + #if os(macOS) + NotificationCenter.default.post(name: ProfileView.notificationName, object: nil) + resetFields() + #endif + } + } + + private func resetFields() { + profileName = "" + profileType = .local + fileImport = false + fileURL = nil + remotePath = "" + } + + private func createProfile0() throws { + let nextProfileID = try ProfileManager.nextID() + + var savePath = "" + var remoteURL: String? = nil + + if profileType == .local { + let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true) + try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true) + let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json") + if fileImport { + guard let fileURL else { + errorMessage = "Missing file" + errorPresented = true + return + } + if !fileURL.startAccessingSecurityScopedResource() { + errorMessage = "Missing access to selected file" + errorPresented = true + return + } + defer { + fileURL.stopAccessingSecurityScopedResource() + } + try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8) + } else { + try "{}".write(to: profileConfig, atomically: true, encoding: .utf8) + } + savePath = profileConfig.relativePath + } else if profileType == .icloud { + if !FileManager.default.fileExists(atPath: FilePath.iCloudDirectory.path) { + try FileManager.default.createDirectory(at: FilePath.iCloudDirectory, withIntermediateDirectories: true) + } + let saveURL = FilePath.iCloudDirectory.appendingPathComponent(remotePath, isDirectory: false) + _ = saveURL.startAccessingSecurityScopedResource() + defer { + saveURL.stopAccessingSecurityScopedResource() + } + do { + _ = try String(contentsOf: saveURL) + } catch { + try "{}".write(to: saveURL, atomically: true, encoding: .utf8) + } + savePath = remotePath + } else if profileType == .remote { + let remoteContent = try HTTPClient().getString(remotePath) + var error: NSError? + LibboxCheckConfig(remoteContent, &error) + if let error { + throw error + } + let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true) + try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true) + let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json") + try remoteContent.write(to: profileConfig, atomically: true, encoding: .utf8) + savePath = profileConfig.relativePath + remoteURL = remotePath + } + try ProfileManager.create(Profile(name: profileName, type: profileType, path: savePath, remoteURL: remoteURL)) + } +} diff --git a/ApplicationLibrary/Views/Profile/ProfileView.swift b/ApplicationLibrary/Views/Profile/ProfileView.swift new file mode 100644 index 0000000..3a7afd3 --- /dev/null +++ b/ApplicationLibrary/Views/Profile/ProfileView.swift @@ -0,0 +1,232 @@ +import Foundation +import Library +import SwiftUI + +public struct ProfileView: View { + public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") + + @State private var isLoading = true + @State private var isUpdating = false + + @State private var errorPresented = false + @State private var errorMessage = "" + + @State private var profileList: [Profile] = [] + + #if os(iOS) + @State private var editMode = EditMode.inactive + #elseif os(macOS) + @Environment(\.openWindow) private var openWindow + #endif + + @State private var observer: Any? + + public init() {} + + public var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + doReload() + } + } + } else { + #if os(iOS) + FormView { + NavigationLink { + NewProfileView { + Task.detached { + doReload() + } + } + } label: { + Text("New Profile").foregroundColor(.accentColor) + } + .disabled(editMode.isEditing) + if profileList.isEmpty { + Text("Empty Profiles") + } else { + List { + ForEach(profileList, id: \.mustID) { profile in + viewBuilder { + if editMode.isEditing == true { + Text(profile.name) + } else { + NavigationLink { + EditProfileView().environmentObject(profile) + } label: { + Text(profile.name) + } + } + } + } + .onMove(perform: moveProfile) + .onDelete(perform: deleteProfile) + } + } + } + #elseif os(macOS) + if profileList.isEmpty { + Text("Empty Profiles") + } else { + FormView { + List { + ForEach(profileList, id: \.mustID) { profile in + + HStack { + VStack(alignment: .leading) { + Text(profile.name) + if profile.type == .remote { + Spacer(minLength: 4) + Text("Last Updated: \(profile.lastUpdatedString)").font(.caption) + } + } + HStack { + if profile.type == .remote { + Button(action: { + isUpdating = true + Task.detached { + updateProfile(profile) + } + }, label: { + Image(systemName: "arrow.clockwise") + }) + } + Button(action: { + openWindow(id: EditProfileWindowView.windowID, value: profile.mustID) + }, label: { + Image(systemName: "pencil") + }) + Button(action: { + deleteProfile(profile) + }, label: { + Image(systemName: "trash.fill") + }) + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + } + .onMove(perform: moveProfile) + .onDelete(perform: deleteProfile) + } + } + } + #endif + } + } + .disabled(isUpdating) + .navigationTitle("Profiles") + #if os(macOS) + .onAppear { + if observer == nil { + observer = NotificationCenter.default.addObserver(forName: ProfileView.notificationName, object: nil, queue: .main) { _ in + Task.detached { + doReload() + } + } + } + } + .onDisappear { + if let observer { + NotificationCenter.default.removeObserver(observer) + } + observer = nil + } + .toolbar { + ToolbarItem { + Button(action: { + openWindow(id: NewProfileView.windowID) + }, label: { + Label("New Profile", systemImage: "plus.square.fill") + }) + } + } + #elseif os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + EditButton().disabled(profileList.isEmpty) + } + } + .environment(\.editMode, $editMode) + #endif + } + + private func deleteSelectedProfiles(_ profileID: [Int64]) { + do { + if try ProfileManager.delete(by: profileID) > 0 { + isLoading = true + } + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + + private func doReload() { + defer { + isLoading = false + } + do { + profileList = try ProfileManager.list() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + } + + private func updateProfile(_ profile: Profile) { + do { + _ = try profile.updateRemoteProfile() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + isUpdating = false + } + + private func deleteProfile(_ profile: Profile) { + Task.detached { + do { + _ = try ProfileManager.delete(profile) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + isLoading = true + } + } + + private func moveProfile(from source: IndexSet, to destination: Int) { + profileList.move(fromOffsets: source, toOffset: destination) + for (index, profile) in profileList.enumerated() { + profile.order = UInt32(index) + } + do { + try ProfileManager.update(profileList) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + } + + private func deleteProfile(where profileIndex: IndexSet) { + let profileToDelete = profileIndex.map { index in + profileList[index] + } + profileList.remove(atOffsets: profileIndex) + Task.detached { + do { + _ = try ProfileManager.delete(profileToDelete) + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + } +} diff --git a/ApplicationLibrary/Views/Setting/ServiceLogView.swift b/ApplicationLibrary/Views/Setting/ServiceLogView.swift new file mode 100644 index 0000000..d1c906a --- /dev/null +++ b/ApplicationLibrary/Views/Setting/ServiceLogView.swift @@ -0,0 +1,89 @@ +import Foundation +import Library +import SwiftUI +import UniformTypeIdentifiers + +public struct ServiceLogView: View { + #if os(macOS) + public static let windowID = "service-log" + #endif + + @State private var isLoading = true + @State private var content = "" + @State private var fileExporterPresented = false + private let logFont = Font.system(.caption, design: .monospaced) + + public init() {} + + public var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + loadContent() + } + } + } else { + if content.isEmpty { + Text("Empty content") + } else { + ScrollView { + Text(content).font(logFont) + } + .padding() + } + } + } + .toolbar { + Button("Export") { + fileExporterPresented = true + } + .disabled(content.isEmpty) + } + .fileExporter( + isPresented: $fileExporterPresented, + document: LogDocument(content), + contentType: .text, + defaultFilename: "service-log.txt", + onCompletion: { _ in } + ) + .navigationTitle("Service Log") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + } + + private func loadContent() { + do { + content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log")) + } catch {} + if content.isEmpty { + do { + content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old")) + } catch {} + } + isLoading = false + } + + private struct LogDocument: FileDocument { + static var readableContentTypes = [UTType.text] + + let content: String + + init(_ content: String) { + self.content = content + } + + init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + content = String(decoding: data, as: UTF8.self) + } else { + content = "" + } + } + + func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: Data(content.utf8)) + } + } +} diff --git a/ApplicationLibrary/Views/Setting/SettingView.swift b/ApplicationLibrary/Views/Setting/SettingView.swift new file mode 100644 index 0000000..f5dc268 --- /dev/null +++ b/ApplicationLibrary/Views/Setting/SettingView.swift @@ -0,0 +1,147 @@ +import Foundation +import Libbox +import Library +import SwiftUI +#if os(macOS) + import ServiceManagement +#endif + +public struct SettingView: View { + #if os(macOS) + @Environment(\.openWindow) private var openWindow + #endif + + @State private var isLoading = true + + #if os(macOS) + @State private var startAtLogin = false + @Environment(\.showMenuBarExtra) private var showMenuBarExtra + #endif + + @State private var disableMemoryLimit = false + @State private var version = "" + @State private var dataSize = "" + + @State private var errorPresented = false + @State private var errorMessage = "" + + public init() {} + + public var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + await loadSettings() + } + } + } else { + FormView { + #if os(macOS) + Section("MacOS") { + Toggle("Start At Login", isOn: $startAtLogin) + .onChange(of: startAtLogin) { newValue in + Task.detached { + updateLoginItems(newValue) + } + } + Toggle("Show in Menu Bar", isOn: showMenuBarExtra) + .onChange(of: showMenuBarExtra.wrappedValue) { newValue in + Task.detached { + SharedPreferences.showMenuBarExtra = newValue + } + } + } + #endif + Section("Packet Tunnel") { + Toggle("Disable Memory Limit", isOn: $disableMemoryLimit) + .onChange(of: disableMemoryLimit) { newValue in + Task.detached { + SharedPreferences.disableMemoryLimit = newValue + } + } + } + Section("Core") { + FormTextItem("Version", version) + FormTextItem("Data Size", dataSize) + #if os(iOS) + NavigationLink(destination: ServiceLogView()) { + Text("View Service Log") + } + #elseif os(macOS) + Button("View Service Log") { + openWindow(id: ServiceLogView.windowID) + } + #endif + Button("Clear Working Directory") { + Task.detached { + clearWorkingDirectory() + } + } + .foregroundColor(.red) + } + } + } + } + .navigationTitle("Settings") + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + #if os(macOS) + private func updateLoginItems(_ startAtLogin: Bool) { + do { + if startAtLogin { + if SMAppService.mainApp.status == .enabled { + try? SMAppService.mainApp.unregister() + } + + try SMAppService.mainApp.register() + } else { + try SMAppService.mainApp.unregister() + } + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + #endif + + private func loadSettings() async { + #if os(macOS) + startAtLogin = SMAppService.mainApp.status == .enabled + #endif + disableMemoryLimit = SharedPreferences.disableMemoryLimit + version = LibboxVersion() + dataSize = "Loading..." + isLoading = false + dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown" + } + + private func clearWorkingDirectory() { + try? FileManager.default.removeItem(at: FilePath.workingDirectory) + isLoading = true + } +} + +private extension URL { + func formattedSize() throws -> String? { + guard let urls = FileManager.default.enumerator(at: self, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { + return nil + } + let size = try urls.lazy.reduce(0) { + try ($1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0 + } + let formatter = ByteCountFormatter() + formatter.countStyle = .file + guard let byteCount = formatter.string(for: size) else { + return nil + } + return byteCount + } +} diff --git a/Extension/Extension+Iterator.swift b/Extension/Extension+Iterator.swift new file mode 100644 index 0000000..b7d57bb --- /dev/null +++ b/Extension/Extension+Iterator.swift @@ -0,0 +1,12 @@ +import Foundation +import Libbox + +extension LibboxStringIteratorProtocol { + func toArray() -> [String] { + var array: [String] = [] + while hasNext() { + array.append(next()) + } + return array + } +} diff --git a/Extension/Extension+RunBlocking.swift b/Extension/Extension+RunBlocking.swift new file mode 100644 index 0000000..bc5a8c4 --- /dev/null +++ b/Extension/Extension+RunBlocking.swift @@ -0,0 +1,23 @@ +import Foundation +import Libbox +import NetworkExtension + +func runBlocking(_ body: @escaping () async throws -> T) throws -> T { + let semaphore = DispatchSemaphore(value: 0) + let box = resultBox() + Task { + do { + let value = try await body() + box.result = .success(value) + } catch { + box.result = .failure(error) + } + semaphore.signal() + } + semaphore.wait() + return try box.result.get() +} + +private class resultBox { + var result: Result! +} diff --git a/Extension/Extension.entitlements b/Extension/Extension.entitlements new file mode 100644 index 0000000..f5d4a23 --- /dev/null +++ b/Extension/Extension.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.io.nekohasekai.sfa + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/Extension/ExtensionPlatformInterface.swift b/Extension/ExtensionPlatformInterface.swift new file mode 100644 index 0000000..71aa666 --- /dev/null +++ b/Extension/ExtensionPlatformInterface.swift @@ -0,0 +1,156 @@ +import Foundation +import Libbox +import NetworkExtension + +class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol { + private let tunnel: NEPacketTunnelProvider + private let commandServer: LibboxCommandServer + + init(_ tunnel: NEPacketTunnelProvider, _ logServer: LibboxCommandServer) { + self.tunnel = tunnel + commandServer = logServer + } + + func openTun(_ options: LibboxTunOptionsProtocol?, ret0_: UnsafeMutablePointer?) throws { + guard let options else { + throw NSError(domain: "nil options", code: 0) + } + guard let ret0_ else { + throw NSError(domain: "nil return pointer", code: 0) + } + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + if options.getAutoRoute() { + settings.mtu = NSNumber(value: options.getMTU()) + + var error: NSError? + let dnsServer = options.getDNSServerAddress(&error) + if let error { + throw error + } + settings.dnsSettings = NEDNSSettings(servers: [dnsServer]) + + var ipv4Address: [String] = [] + var ipv4Mask: [String] = [] + let ipv4AddressIterator = options.getInet4Address()! + while ipv4AddressIterator.hasNext() { + let ipv4Prefix = ipv4AddressIterator.next()! + ipv4Address.append(ipv4Prefix.address) + ipv4Mask.append(ipv4Prefix.mask()) + } + let ipv4Settings = NEIPv4Settings(addresses: ipv4Address, subnetMasks: ipv4Mask) + var ipv4Routes: [NEIPv4Route] = [] + let inet4RouteAddressIterator = options.getInet4RouteAddress()! + if inet4RouteAddressIterator.hasNext() { + while inet4RouteAddressIterator.hasNext() { + let ipv4RoutePrefix = inet4RouteAddressIterator.next()! + ipv4Routes.append(NEIPv4Route(destinationAddress: ipv4RoutePrefix.address, subnetMask: ipv4RoutePrefix.mask())) + } + } else { + ipv4Routes.append(NEIPv4Route.default()) + } + for (index, address) in ipv4Address.enumerated() { + ipv4Routes.append(NEIPv4Route(destinationAddress: address, subnetMask: ipv4Mask[index])) + } + ipv4Settings.includedRoutes = ipv4Routes + settings.ipv4Settings = ipv4Settings + + var ipv6Address: [String] = [] + var ipv6Prefixes: [NSNumber] = [] + let ipv6AddressIterator = options.getInet6Address()! + while ipv6AddressIterator.hasNext() { + let ipv6Prefix = ipv6AddressIterator.next()! + ipv6Address.append(ipv6Prefix.address) + ipv6Prefixes.append(NSNumber(value: ipv6Prefix.prefix)) + } + let ipv6Settings = NEIPv6Settings(addresses: ipv6Address, networkPrefixLengths: ipv6Prefixes) + var ipv6Routes: [NEIPv6Route] = [] + let inet6RouteAddressIterator = options.getInet6RouteAddress()! + if inet6RouteAddressIterator.hasNext() { + while inet6RouteAddressIterator.hasNext() { + let ipv6RoutePrefix = inet4RouteAddressIterator.next()! + ipv6Routes.append(NEIPv6Route(destinationAddress: ipv6RoutePrefix.description, networkPrefixLength: NSNumber(value: ipv6RoutePrefix.prefix))) + } + } else { + ipv6Routes.append(NEIPv6Route.default()) + } + ipv6Settings.includedRoutes = ipv6Routes + settings.ipv6Settings = ipv6Settings + } + + if options.isHTTPProxyEnabled() { + let proxySettings = NEProxySettings() + let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort())) + proxySettings.httpEnabled = true + proxySettings.httpServer = proxyServer + proxySettings.httpsEnabled = true + proxySettings.httpsServer = proxyServer + settings.proxySettings = proxySettings + } + + try runBlocking { [self] in + try await tunnel.setTunnelNetworkSettings(settings) + } + + if let tunFd = tunnel.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32 { + ret0_.pointee = tunFd + return + } + + let tunFdFromLoop = LibboxGetTunnelFileDescriptor() + if tunFdFromLoop != -1 { + ret0_.pointee = tunFdFromLoop + } else { + throw NSError(domain: "missing file descriptor", code: 0) + } + } + + func usePlatformAutoDetectControl() -> Bool { + true + } + + func autoDetectControl(_: Int32) throws {} + + func findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer?) throws { + throw NSError(domain: "not implemented", code: 0) + } + + func packageName(byUid _: Int32, error _: NSErrorPointer) -> String { + "" + } + + func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer?) throws { + throw NSError(domain: "not implemented", code: 0) + } + + func useProcFS() -> Bool { + false + } + + func writeLog(_ message: String?) { + guard let message else { + return + } + commandServer.writeMessage(message) + } + + func usePlatformDefaultInterfaceMonitor() -> Bool { + false + } + + func startDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {} + + func closeDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {} + + func useGetter() -> Bool { + false + } + + func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol { + throw NSError(domain: "not implemented", code: 0) + } + + func underNetworkExtension() -> Bool { + true + } +} diff --git a/Extension/Info.plist b/Extension/Info.plist new file mode 100644 index 0000000..3059459 --- /dev/null +++ b/Extension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + diff --git a/Extension/PacketTunnelProvider.swift b/Extension/PacketTunnelProvider.swift new file mode 100644 index 0000000..533b5a8 --- /dev/null +++ b/Extension/PacketTunnelProvider.swift @@ -0,0 +1,174 @@ +import Foundation +import Libbox +import Library +import NetworkExtension + +class PacketTunnelProvider: NEPacketTunnelProvider { + private var commandServer: LibboxCommandServer! + private var boxService: LibboxBoxService! + + override func startTunnel(options _: [String: NSObject]?) async throws { + NSLog("Here I am") + do { + try FileManager.default.createDirectory(at: FilePath.cacheDirectory, withIntermediateDirectories: true) + } catch { + writeFatalError("(packet-tunnel) error: create cache directory: \(error.localizedDescription)") + return + } + var error: NSError? + LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &error) + if let error { + writeError("(packet-tunnel) redirect stderr error: \(error.localizedDescription)") + } + + LibboxSetMemoryLimit(!SharedPreferences.disableMemoryLimit) + + commandServer = LibboxNewCommandServer(FilePath.sharedDirectory.relativePath, serverInterface(self), Int32(SharedPreferences.maxLogLines)) + do { + try commandServer.start() + } catch { + writeFatalError("(packet-tunnel): log server start error: \(error.localizedDescription)") + return + } + writeMessage("(packet-tunnel) log server started") + + do { + try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true) + } catch { + writeFatalError("(packet-tunnel) error: create working directory: \(error.localizedDescription)") + return + } + + LibboxSetup(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, -1, -1) + + startService() + } + + private func writeMessage(_ message: String) { + if let commandServer { + commandServer.writeMessage(message) + } else { + NSLog(message) + } + } + + private func writeError(_ message: String) { + writeMessage(message) + #if os(iOS) + ServiceNotification.postServiceNotification(title: "Service Error", message: message) + #else + displayMessage(message) { _ in + } + #endif + } + + private func writeFatalError(_ message: String) { + writeError(message) + cancelTunnelWithError(NSError(domain: message, code: 0)) + } + + private func startService() { + let profile: Profile? + do { + profile = try ProfileManager.get(Int64(SharedPreferences.selectedProfileID)) + } catch { + writeFatalError("(packet-tunnel) error: missing default profile: \(error.localizedDescription)") + return + } + guard let profile else { + writeFatalError("(packet-tunnel) error: missing default profile") + return + } + let configContent: String + do { + configContent = try profile.read() + } catch { + writeFatalError("(packet-tunnel) error: read config file: \(error.localizedDescription)") + return + } + var error: NSError? + let service = LibboxNewService(configContent, ExtensionPlatformInterface(self, commandServer), &error) + if let error { + writeError("(packet-tunnel) error: create service: \(error.localizedDescription)") + return + } + guard let service else { + return + } + do { + try service.start() + } catch { + writeError("(packet-tunnel) error: start service: \(error.localizedDescription)") + return + } + boxService = service + commandServer.setService(service) + #if os(macOS) + Task.detached { + SharedPreferences.startedByUser = true + } + #endif + } + + private func stopService() { + if let service = boxService { + do { + try service.close() + } catch { + writeError("(packet-tunnel) error: stop service: \(error.localizedDescription)") + } + boxService = nil + commandServer.setService(nil) + } + } + + private func reloadService() { + writeMessage("(packet-tunnel) reloading service") + reasserting = true + defer { + reasserting = false + } + stopService() + startService() + } + + override func stopTunnel(with reason: NEProviderStopReason) async { + writeMessage("(packet-tunnel) stopping, reason: \(reason)") + stopService() + if let server = commandServer { + try? server.close() + commandServer = nil + } + #if os(macOS) + if reason == .userInitiated { + SharedPreferences.startedByUser = reason == .userInitiated + } + #endif + } + + override func handleAppMessage(_ messageData: Data) async -> Data? { + messageData + } + + override func sleep() async {} + + override func wake() {} + + private class serverInterface: NSObject, LibboxCommandServerHandlerProtocol { + unowned let tunnel: PacketTunnelProvider + + init(_ tunnel: PacketTunnelProvider) { + self.tunnel = tunnel + super.init() + } + + func serviceReload() throws { + tunnel.reloadService() + } + + func serviceStop() throws { + tunnel.stopService() + tunnel.writeMessage("(packet-tunnel) debug: service stopped") + } + } +} diff --git a/IntentsExtension/Info.plist b/IntentsExtension/Info.plist new file mode 100644 index 0000000..8d15acb --- /dev/null +++ b/IntentsExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + EXAppExtensionAttributes + + EXExtensionPointIdentifier + com.apple.appintents-extension + + + diff --git a/IntentsExtension/Intents.swift b/IntentsExtension/Intents.swift new file mode 100644 index 0000000..f8997cc --- /dev/null +++ b/IntentsExtension/Intents.swift @@ -0,0 +1,197 @@ +import AppIntents +import Foundation +import Libbox +import Library + +struct StartServiceIntent: AppIntent { + public static var title: LocalizedStringResource = "Start sing-box" + + static var description = + IntentDescription("Start or reload sing-box servie with specified profile") + + static var parameterSummary: some ParameterSummary { + Summary("Start sing-box service with profile \(\.$profile).") + } + + @Parameter(title: "Profile", optionsProvider: ProfileProvider()) + var profile: String + + func perform() async throws -> some IntentResult { + guard let extensionProfile = try await (ExtensionProfile.load()) else { + throw NSError(domain: "NetworkExtension not installed", code: 0) + } + let profileList = try ProfileManager.list() + let specifiedProfile = profileList.first { $0.name == profile } + var profileChanged = false + if let specifiedProfile { + if SharedPreferences.selectedProfileID != specifiedProfile.id! { + SharedPreferences.selectedProfileID = specifiedProfile.id! + profileChanged = true + } + } else if profile != "default" { + throw NSError(domain: "Specified profile not found: \(profile)", code: 0) + } + if extensionProfile.status == .connected { + if !profileChanged { + return .result() + } + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + } else if extensionProfile.status.isConnected { + extensionProfile.stop() + try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) + try await extensionProfile.start() + } else { + try await extensionProfile.start() + } + return .result() + } +} + +struct RestartServiceIntent: AppIntent { + static var title: LocalizedStringResource = "Restart sing-box" + + static var description = + IntentDescription("Restart sing-box service") + + static var parameterSummary: some ParameterSummary { + Summary("Restart sing-box service") + } + + func perform() async throws -> some IntentResult { + guard let extensionProfile = try await (ExtensionProfile.load()) else { + return .result() + } + if extensionProfile.status == .connected { + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + } else if extensionProfile.status.isConnected { + extensionProfile.stop() + try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) + try await extensionProfile.start() + } 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() + } +} + +struct ToggleServiceIntent: AppIntent { + 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") + } + + func perform() async throws -> some IntentResult { + guard let extensionProfile = try await (ExtensionProfile.load()) else { + return .result(value: false) + } + if extensionProfile.status.isConnected { + extensionProfile.stop() + return .result(value: false) + + } else { + try await extensionProfile.start() + return .result(value: true) + } + } +} + +struct GetServiceStatus: AppIntent { + static var title: LocalizedStringResource = "Get is sing-box service started" + + static var description = + IntentDescription("Get is sing-box service started") + + static var parameterSummary: some ParameterSummary { + Summary("Get is sing-box service started") + } + + func perform() async throws -> some IntentResult { + guard let extensionProfile = try await (ExtensionProfile.load()) else { + return .result(value: false) + } + return .result(value: extensionProfile.status.isConnected) + } +} + +struct GetCurrentProfile: AppIntent { + static var title: LocalizedStringResource = "Get current sing-box profile" + + static var description = + IntentDescription("Get current sing-box profile") + + static var parameterSummary: some ParameterSummary { + Summary("Get current sing-box profile") + } + + func perform() async throws -> some IntentResult { + guard let profile = try ProfileManager.get(SharedPreferences.selectedProfileID) else { + throw NSError(domain: "No profile selected", code: 0) + } + return .result(value: profile.name) + } +} + +struct UpdateProfileIntent: AppIntent { + static var title: LocalizedStringResource = "Update sing-box profile" + + static var description = + IntentDescription("Update specified sing-box profile") + + static var parameterSummary: some ParameterSummary { + Summary("Update sing-box profile \(\.$profile).") + } + + @Parameter(title: "Profile", optionsProvider: RemoteProfileProvider()) + var profile: String + + init() {} + func perform() async throws -> some IntentResult { + guard let profile = try ProfileManager.get(by: profile) else { + throw NSError(domain: "Specified profile not found: \(profile)", code: 0) + } + if profile.type != .remote { + throw NSError(domain: "Specified profile is not a remote profile", code: 0) + } + try profile.updateRemoteProfile() + return .result() + } +} + +class ProfileProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + var profileNames = try ProfileManager.list().map(\.name) + if !profileNames.contains("default") { + profileNames.insert("default", at: 0) + } + return profileNames + } +} + +class RemoteProfileProvider: DynamicOptionsProvider { + func results() async throws -> [String] { + try ProfileManager.listRemote().map(\.name) + } +} diff --git a/IntentsExtension/IntentsExtension.entitlements b/IntentsExtension/IntentsExtension.entitlements new file mode 100644 index 0000000..0c77e93 --- /dev/null +++ b/IntentsExtension/IntentsExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.io.nekohasekai.sfa + + com.apple.security.network.client + + + diff --git a/IntentsExtension/IntentsExtension.swift b/IntentsExtension/IntentsExtension.swift new file mode 100644 index 0000000..40f0c0e --- /dev/null +++ b/IntentsExtension/IntentsExtension.swift @@ -0,0 +1,4 @@ +import AppIntents + +@main +struct IntentsExtension: AppIntentsExtension {} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3e3e29e --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/Library/Database/Databse.swift b/Library/Database/Databse.swift new file mode 100644 index 0000000..acbf88e --- /dev/null +++ b/Library/Database/Databse.swift @@ -0,0 +1,36 @@ +import Foundation +import GRDB + +class Database { + private static var writer: (any DatabaseWriter)? + + static func sharedWriter() throws -> any DatabaseWriter { + if let writer { + return writer + } + let database = try DatabasePool(path: FilePath.sharedDirectory.appendingPathComponent("settings.db").relativePath) + var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks() + migrator.eraseDatabaseOnSchemaChange = true + + migrator.registerMigration("initialize") { db in + try db.create(table: "profiles") { t in + t.autoIncrementedPrimaryKey("id") + t.column("name", .text).notNull() + t.column("order", .integer).notNull() + t.column("type", .integer).notNull().defaults(to: ProfileType.local.rawValue) + t.column("path", .text).notNull() + t.column("remoteURL", .text) + t.column("autoUpdate", .boolean).notNull().defaults(to: false) + t.column("lastUpdated", .datetime) + } + try db.create(table: "preferences") { t in + t.primaryKey("name", .text, onConflict: .replace).notNull() + t.column("data", .blob) + } + } + + try migrator.migrate(database) + writer = database + return database + } +} diff --git a/Library/Database/Profile+Date.swift b/Library/Database/Profile+Date.swift new file mode 100644 index 0000000..23f3088 --- /dev/null +++ b/Library/Database/Profile+Date.swift @@ -0,0 +1,16 @@ +// +// Profile+Date.swift +// Library +// +// Created by 世界 on 2023/6/29. +// + +import Foundation + +public extension Profile { + var lastUpdatedString: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return dateFormatter.string(from: lastUpdated!) + } +} diff --git a/Library/Database/Profile+Hashable.swift b/Library/Database/Profile+Hashable.swift new file mode 100644 index 0000000..a250097 --- /dev/null +++ b/Library/Database/Profile+Hashable.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Profile: Hashable { + public static func == (lhs: Profile, rhs: Profile) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Library/Database/Profile+RW.swift b/Library/Database/Profile+RW.swift new file mode 100644 index 0000000..7899f50 --- /dev/null +++ b/Library/Database/Profile+RW.swift @@ -0,0 +1,31 @@ +import Foundation + +public extension Profile { + func read() throws -> String { + switch type { + case .local, .remote: + return try String(contentsOfFile: path) + case .icloud: + let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path) + _ = saveURL.startAccessingSecurityScopedResource() + defer { + saveURL.stopAccessingSecurityScopedResource() + } + return try String(contentsOf: saveURL) + } + } + + func write(_ content: String) throws { + switch type { + case .local, .remote: + try content.write(toFile: path, atomically: true, encoding: .utf8) + case .icloud: + let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path) + _ = saveURL.startAccessingSecurityScopedResource() + defer { + saveURL.stopAccessingSecurityScopedResource() + } + try content.write(to: saveURL, atomically: true, encoding: .utf8) + } + } +} diff --git a/Library/Database/Profile+Update.swift b/Library/Database/Profile+Update.swift new file mode 100644 index 0000000..c27f4e2 --- /dev/null +++ b/Library/Database/Profile+Update.swift @@ -0,0 +1,20 @@ +import Foundation +import GRDB +import Libbox + +public extension Profile { + func updateRemoteProfile() throws { + if type != .remote { + return + } + let remoteContent = try HTTPClient().getString(remoteURL) + var error: NSError? + LibboxCheckConfig(remoteContent, &error) + if let error { + throw error + } + try write(remoteContent) + lastUpdated = Date() + try ProfileManager.update(self) + } +} diff --git a/Library/Database/Profile.swift b/Library/Database/Profile.swift new file mode 100644 index 0000000..971148a --- /dev/null +++ b/Library/Database/Profile.swift @@ -0,0 +1,73 @@ +import Foundation +import GRDB + +public class Profile: Record, Identifiable, ObservableObject { + public var id: Int64? + public var mustID: Int64 { + id! + } + + @Published public var name: String + public var order: UInt32 + public var type: ProfileType + public var path: String + @Published public var remoteURL: String? + @Published public var autoUpdate: Bool + public var lastUpdated: Date? + + public init(id: Int64? = nil, name: String, order: UInt32 = 0, type: ProfileType, path: String, remoteURL: String? = nil) { + self.id = id + self.name = name + self.order = order + self.type = type + self.path = path + self.remoteURL = remoteURL + + autoUpdate = false + lastUpdated = nil + if type == .remote { + lastUpdated = Date() + } + super.init() + } + + override public class var databaseTableName: String { + "profiles" + } + + enum Columns: String, ColumnExpression { + case id, name, order, type, path, remoteURL, autoUpdate, lastUpdated, userAgent + } + + required init(row: Row) throws { + id = row[Columns.id] + name = row[Columns.name] ?? "" + order = row[Columns.order] ?? 0 + type = ProfileType(rawValue: row[Columns.type] ?? ProfileType.local.rawValue)! + path = row[Columns.path] ?? "" + remoteURL = row[Columns.remoteURL] ?? "" + autoUpdate = row[Columns.autoUpdate] ?? false + lastUpdated = row[Columns.lastUpdated] ?? Date() + try super.init(row: row) + } + + override public func encode(to container: inout PersistenceContainer) throws { + container[Columns.id] = id + container[Columns.name] = name + container[Columns.order] = order + container[Columns.type] = type.rawValue + container[Columns.path] = path + container[Columns.remoteURL] = remoteURL + container[Columns.autoUpdate] = autoUpdate + container[Columns.lastUpdated] = lastUpdated + } + + override public func didInsert(_ inserted: InsertionSuccess) { + super.didInsert(inserted) + id = inserted.rowID + } +} + +public enum ProfileType: Int { + case local = 0, icloud, remote +} diff --git a/Library/Database/ProfileManager.swift b/Library/Database/ProfileManager.swift new file mode 100644 index 0000000..61416ea --- /dev/null +++ b/Library/Database/ProfileManager.swift @@ -0,0 +1,98 @@ +import Foundation +import GRDB + +public enum ProfileManager { + public static func create(_ profile: Profile) throws { + profile.order = try nextOrder() + try Database.sharedWriter().write { db in + try profile.insert(db, onConflict: .fail) + } + } + + public static func get(_ profileID: Int64) throws -> Profile? { + try Database.sharedWriter().read { db in + try Profile.fetchOne(db, id: profileID) + } + } + + public static func get(by profileName: String) throws -> Profile? { + try Database.sharedWriter().read { db in + try Profile.filter(Column("name") == profileName).fetchOne(db) + } + } + + public static func delete(_ profile: Profile) throws { + _ = try Database.sharedWriter().write { db in + try profile.delete(db) + } + } + + public static func delete(by id: Int64) throws { + _ = try Database.sharedWriter().write { db in + try Profile.deleteOne(db, id: id) + } + } + + public static func delete(_ profileList: [Profile]) throws -> Int { + try Database.sharedWriter().write { db in + try Profile.deleteAll(db, keys: profileList.map { + ["id": $0.id!] + }) + } + } + + public static func delete(by id: [Int64]) throws -> Int { + try Database.sharedWriter().write { db in + try Profile.deleteAll(db, ids: id) + } + } + + public static func update(_ profile: Profile) throws { + _ = try Database.sharedWriter().write { db in + try profile.updateChanges(db) + } + } + + public static func update(_ profileList: [Profile]) throws { + // TODO: batch update + try Database.sharedWriter().write { db in + for profile in profileList { + try profile.updateChanges(db) + } + } + } + + public static func list() throws -> [Profile] { + try Database.sharedWriter().read { db in + try Profile.all().order(Column("order").asc).fetchAll(db) + } + } + + public static func listRemote() throws -> [Profile] { + try Database.sharedWriter().read { db in + try Profile.filter(Column("type") == ProfileType.remote.rawValue).order(Column("order").asc).fetchAll(db) + } + } + + public static func listAutoUpdateEnabled() throws -> [Profile] { + try Database.sharedWriter().read { db in + try Profile.filter(Column("autoUpdate") == true).order(Column("order").asc).fetchAll(db) + } + } + + public static func nextID() throws -> Int64 { + try Database.sharedWriter().read { db in + if let lastProfile = try Profile.select(Column("id")).order(Column("id").desc).fetchOne(db) { + return lastProfile.id! + 1 + } else { + return 1 + } + } + } + + private static func nextOrder() throws -> UInt32 { + try Database.sharedWriter().read { db in + try UInt32(Profile.fetchCount(db)) + } + } +} diff --git a/Library/Database/ShadredPreferences+Database.swift b/Library/Database/ShadredPreferences+Database.swift new file mode 100644 index 0000000..b9bc893 --- /dev/null +++ b/Library/Database/ShadredPreferences+Database.swift @@ -0,0 +1,116 @@ +import BinaryCodable +import Foundation +import GRDB + +extension SharedPreferences { + @propertyWrapper public class Preference { + private let name: String + private let defaultValue: T + + init(_ name: String, defaultValue: T) { + self.name = name + self.defaultValue = defaultValue + } + + public var wrappedValue: T { + get { + do { + return try SharedPreferences.read(name) ?? defaultValue + } catch { + NSLog("read preferences error: \(error)") + return defaultValue + } + } + set { + do { + try SharedPreferences.write(name, newValue) + } catch { + NSLog("write preferences error: \(error)") + } + } + } + } + + @propertyWrapper public class NullablePreference { + private let name: String + + init(_ name: String) { + self.name = name + } + + public var wrappedValue: T? { + get { + do { + return try SharedPreferences.read(name) + } catch { + NSLog("read preferences error: \(error)") + return nil + } + } + set { + do { + try SharedPreferences.write(name, newValue) + } catch { + NSLog("write preferences error: \(error)") + } + } + } + } + + private static func read(_ name: String) throws -> T? { + guard let item = try (Database.sharedWriter().read { db in + try Item.fetchOne(db, id: name) + }) + else { + return nil + } + return try BinaryDecoder().decode(from: item.data) + } + + private static func write(_ name: String, _ value: (some Codable)?) throws { + if value == nil { + _ = try Database.sharedWriter().write { db in + try Item.deleteOne(db, id: name) + } + } else { + let data = try BinaryEncoder().encode(value) + try Database.sharedWriter().write { db in + try Item(name: name, data: data).insert(db) + } + } + } +} + +private class Item: Record, Identifiable { + public var id: String { + name + } + + public var name: String + public var data: Data + + init(name: String, data: Data) { + self.name = name + self.data = data + super.init() + } + + override public class var databaseTableName: String { + "preferences" + } + + enum Columns: String, ColumnExpression { + case name, data + } + + required init(row: Row) throws { + name = row[Columns.name] + data = row[Columns.data] + try super.init(row: row) + } + + override public func encode(to container: inout PersistenceContainer) throws { + container[Columns.name] = name + container[Columns.data] = data + } +} diff --git a/Library/Database/SharedPreferences.swift b/Library/Database/SharedPreferences.swift new file mode 100644 index 0000000..0da39d7 --- /dev/null +++ b/Library/Database/SharedPreferences.swift @@ -0,0 +1,19 @@ +import Foundation + +public enum SharedPreferences { + @Preference("selected_profile_id", defaultValue: -1) public static var selectedProfileID + + #if os(macOS) + private static let disableMemoryLimitByDefault = true + #else + private static let disableMemoryLimitByDefault = false + #endif + @Preference("disable_memory_limit", defaultValue: disableMemoryLimitByDefault) public static var disableMemoryLimit + + @Preference("max_log_lines", defaultValue: 300) public static var maxLogLines + + #if os(macOS) + @Preference("show_menu_bar_extra", defaultValue: true) public static var showMenuBarExtra + @Preference("started_by_user", defaultValue: false) public static var startedByUser + #endif +} diff --git a/Library/Library.swift b/Library/Library.swift new file mode 100644 index 0000000..b0c1d88 --- /dev/null +++ b/Library/Library.swift @@ -0,0 +1,3 @@ +import Foundation + +public class Library {} diff --git a/Library/Network/ExtensionProfile.swift b/Library/Network/ExtensionProfile.swift new file mode 100644 index 0000000..5ebeab5 --- /dev/null +++ b/Library/Network/ExtensionProfile.swift @@ -0,0 +1,70 @@ +import Foundation +import NetworkExtension + +public class ExtensionProfile: ObservableObject { + private let manager: NEVPNManager + private var connection: NEVPNConnection + private var observer: Any? + + @Published public var status: NEVPNStatus + + public init(_ manager: NEVPNManager) { + self.manager = manager + connection = manager.connection + status = manager.connection.status + } + + deinit { + unregister() + } + + public func register() { + observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name.NEVPNStatusDidChange, + object: manager.connection, + queue: .main + ) { [weak self] notification in + guard let self else { + return + } + self.connection = notification.object as! NEVPNConnection + self.status = self.connection.status + } + } + + private func unregister() { + if let observer { + NotificationCenter.default.removeObserver(observer) + } + } + + public func start() async throws { + manager.isEnabled = true + try await manager.saveToPreferences() + try manager.connection.startVPNTunnel() + } + + public func stop() { + manager.connection.stopVPNTunnel() + } + + public static func load() async throws -> ExtensionProfile? { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + if managers.isEmpty { + return nil + } + let profile = ExtensionProfile(managers[0]) + return profile + } + + public static func install() async throws { + let manager = NETunnelProviderManager() + manager.localizedDescription = "utun interface" + let tunnelProtocol = NETunnelProviderProtocol() + tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).extension" + tunnelProtocol.serverAddress = "sing-box" + manager.protocolConfiguration = tunnelProtocol + manager.isEnabled = true + try await manager.saveToPreferences() + } +} diff --git a/Library/Network/HTTPClient.swift b/Library/Network/HTTPClient.swift new file mode 100644 index 0000000..528a04a --- /dev/null +++ b/Library/Network/HTTPClient.swift @@ -0,0 +1,40 @@ +import Foundation +import Libbox + +public class HTTPClient { + private static var userAgent: String { + var userAgent = FilePath.httpClientName + userAgent += "/" + userAgent += Bundle.main.version + userAgent += " (Build " + userAgent += Bundle.main.versionNumber + userAgent += "; sing-box " + userAgent += LibboxVersion() + userAgent += ")" + return userAgent + } + + private let client: any LibboxHTTPClientProtocol + + public init() { + client = LibboxNewHTTPClient()! + client.modernTLS() + } + + public func getString(_ url: String?) throws -> String { + let request = client.newRequest()! + request.setUserAgent(HTTPClient.userAgent) + try request.setURL(url) + let response = try request.execute() + var error: NSError? + let contentString = response.getContentString(&error) + if let error { + throw error + } + return contentString + } + + deinit { + client.close() + } +} diff --git a/Library/Network/NEVPNStatus+isConnected.swift b/Library/Network/NEVPNStatus+isConnected.swift new file mode 100644 index 0000000..d5b626b --- /dev/null +++ b/Library/Network/NEVPNStatus+isConnected.swift @@ -0,0 +1,40 @@ +import Foundation +import NetworkExtension + +public extension NEVPNStatus { + var isEnabled: Bool { + switch self { + case .connected, .disconnected, .reasserting: + return true + default: + return false + } + } + + var isSwitchable: Bool { + switch self { + case .connected, .disconnected: + return true + default: + return false + } + } + + var isConnected: Bool { + switch self { + case .connecting, .connected, .disconnecting, .reasserting: + return true + default: + return false + } + } + + var isConnectedStrict: Bool { + switch self { + case .connected, .reasserting: + return true + default: + return false + } + } +} diff --git a/Library/Shared/Bundle+Version.swift b/Library/Shared/Bundle+Version.swift new file mode 100644 index 0000000..d527e0f --- /dev/null +++ b/Library/Shared/Bundle+Version.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Bundle { + var version: String { + infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" + } + + var versionNumber: String { + infoDictionary?["CFBundleVersion"] as? String ?? "unknown" + } +} diff --git a/Library/Shared/FilePath.swift b/Library/Shared/FilePath.swift new file mode 100644 index 0000000..55cbaa0 --- /dev/null +++ b/Library/Shared/FilePath.swift @@ -0,0 +1,34 @@ +import Foundation + +public enum FilePath { + public static let packageName = "io.nekohasekai.sfa" + #if os(iOS) + public static let httpClientName = "SFI" + #elseif os(macOS) + public static let httpClientName = "SFM" + #endif +} + +public extension FilePath { + static let groupName = "group.\(packageName)" + + static let sharedDirectory: URL! = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupName) + + static let cacheDirectory = sharedDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Caches", isDirectory: true) + + static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true) + + static let iCloudDirectory = FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true) +} + +public extension URL { + var fileName: String { + var path = relativePath + if let index = path.lastIndex(of: "/") { + path = String(path[path.index(index, offsetBy: 1)...]) + } + return path + } +} diff --git a/Library/Shared/ServiceNotification.swift b/Library/Shared/ServiceNotification.swift new file mode 100644 index 0000000..5c97e1c --- /dev/null +++ b/Library/Shared/ServiceNotification.swift @@ -0,0 +1,46 @@ +import Foundation +import UserNotifications + +public enum ServiceNotification { + private static let delegate = Delegate() + + public static func register() { + UNUserNotificationCenter.current().delegate = delegate + UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { + _, _ in + } + } + + private static var listener: ((UNNotificationContent) -> Void)? + + public static func setServiceNotificationListener(listener: @escaping (UNNotificationContent) -> Void) { + ServiceNotification.listener = listener + } + + public static func removeServiceNotificationListener() { + ServiceNotification.listener = nil + } + + public static func postServiceNotification(content: UNNotificationContent) { + UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: "service-notification", content: content, trigger: nil)) + } + + public static func postServiceNotification(title: String, message: String) { + let content = UNMutableNotificationContent() + content.title = title + content.body = message + postServiceNotification(content: content) + } + + private class Delegate: NSObject, UNUserNotificationCenterDelegate { + func userNotificationCenter(_: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + NSLog("userNotificationCenter") + if let listener = ServiceNotification.listener { + listener(notification.request.content) + return [] + } else { + return [.alert] + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e87c4b --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# sing-box-for-apple + +Experimental iOS/macOS client for sing-box, the universal proxy platform. + +## Documentation + +[SFI](https://sing-box.sagernet.org/installation/clients/sfi/) | [SFM](https://sing-box.sagernet.org/installation/clients/sfm/) + +## License + +``` +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +``` \ No newline at end of file diff --git a/SFI/Application.swift b/SFI/Application.swift new file mode 100644 index 0000000..13f42e5 --- /dev/null +++ b/SFI/Application.swift @@ -0,0 +1,16 @@ +import ApplicationLibrary +import Foundation +import Library +import SwiftUI +import UIKit + +@main +struct Application: App { + @UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate + + var body: some Scene { + WindowGroup { + MainView() + } + } +} diff --git a/SFI/ApplicationDelegate.swift b/SFI/ApplicationDelegate.swift new file mode 100644 index 0000000..d1cd727 --- /dev/null +++ b/SFI/ApplicationDelegate.swift @@ -0,0 +1,20 @@ +import ApplicationLibrary +import Foundation +import Library +import UIKit + +class ApplicationDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + NSLog("Here I stand") + ServiceNotification.register() + Task.detached { + do { + try await UIProfileUpdateTask.setup() + NSLog("setup background task success") + } catch { + NSLog("setup background task error: \(error.localizedDescription)") + } + } + return true + } +} diff --git a/SFI/Assets.xcassets/AccentColor.colorset/Contents.json b/SFI/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..0afb3cf --- /dev/null +++ b/SFI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors": [ + { + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/100.png b/SFI/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..87b49a2 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/1024.png b/SFI/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..c85573a Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/114.png b/SFI/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..a89cecf Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/120.png b/SFI/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..08eb7d9 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/144.png b/SFI/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..48a3e75 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/152.png b/SFI/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..00a9960 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/167.png b/SFI/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..713f83f Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/180.png b/SFI/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..42819bb Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/20.png b/SFI/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..eb62ba3 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/29.png b/SFI/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..44d1f93 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/40.png b/SFI/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..9f2320e Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/50.png b/SFI/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..f6871ce Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/57.png b/SFI/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..7942d46 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/58.png b/SFI/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..8e5cb59 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/60.png b/SFI/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..5951cae Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/72.png b/SFI/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..6b405eb Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/76.png b/SFI/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..9166aa4 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/80.png b/SFI/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..f4c0c93 Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/87.png b/SFI/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..3c1f73d Binary files /dev/null and b/SFI/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/SFI/Assets.xcassets/AppIcon.appiconset/Contents.json b/SFI/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..799689a --- /dev/null +++ b/SFI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,324 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + }, + { + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, + { + "filename" : "1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFI/Assets.xcassets/Contents.json b/SFI/Assets.xcassets/Contents.json new file mode 100644 index 0000000..74d6a72 --- /dev/null +++ b/SFI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/SFI/ContentView.swift b/SFI/ContentView.swift new file mode 100644 index 0000000..1649141 --- /dev/null +++ b/SFI/ContentView.swift @@ -0,0 +1,61 @@ +import ApplicationLibrary +import Library +import SwiftUI + +struct ContentView: View { + @Environment(\.selection) private var selection + @Environment(\.extensionProfile) private var extensionProfile + + var body: some View { + viewBuilder { + if let profile = extensionProfile.wrappedValue { + ContentView0().environmentObject(profile) + } else { + ContentView1() + } + } + } + + struct ContentView0: View { + @Environment(\.selection) private var selection + @EnvironmentObject private var extensionProfile: ExtensionProfile + + var body: some View { + TabView(selection: selection) { + ForEach(NavigationPage.allCases.filter { it in + it.visible(extensionProfile) + }, id: \.self) { page in + NavigationView { + page.contentView + } + .navigationViewStyle(.stack) + .tag(page) + .tabItem { page.label } + } + }.onChange(of: extensionProfile.status) { _ in + if !selection.wrappedValue.visible(extensionProfile) { + selection.wrappedValue = NavigationPage.dashboard + } + } + } + } + + struct ContentView1: View { + @Environment(\.selection) private var selection + + var body: some View { + TabView(selection: selection) { + ForEach(NavigationPage.allCases.filter { it in + it.visible(nil) + }, id: \.self) { page in + NavigationView { + page.contentView + } + .navigationViewStyle(.stack) + .tag(page) + .tabItem { page.label } + } + } + } + } +} diff --git a/SFI/Info.plist b/SFI/Info.plist new file mode 100644 index 0000000..374a712 --- /dev/null +++ b/SFI/Info.plist @@ -0,0 +1,28 @@ + + + + + BGTaskSchedulerPermittedIdentifiers + + io.nekohasekai.sfa.update_profiles + + ITSAppUsesNonExemptEncryption + + NSUbiquitousContainers + + iCloud.io.nekohasekai.sfa + + NSUbiquitousContainerIsDocumentScopePublic + + NSUbiquitousContainerName + sing-box + NSUbiquitousContainerSupportedFolderLevels + Any + + + UIBackgroundModes + + fetch + + + diff --git a/SFI/MainView.swift b/SFI/MainView.swift new file mode 100644 index 0000000..12a63dc --- /dev/null +++ b/SFI/MainView.swift @@ -0,0 +1,84 @@ +import ApplicationLibrary +import Library +import SwiftUI + +struct MainView: View { + @Environment(\.scenePhase) var scenePhase + + @State private var selection = NavigationPage.dashboard + @State private var extensionProfile: ExtensionProfile? + @State private var profileLoading = true + @State private var logClient: LogClient! + + @State private var serviceNotificationTitle = "" + @State private var serviceNotificationContent = "" + @State private var serviceNotificationPresented = false + + var body: some View { + viewBuilder { + if profileLoading { + ProgressView().onAppear { + Task.detached { + logClient = LogClient(SharedPreferences.maxLogLines) + await loadProfile() + } + } + } else { + ContentView() + } + } + .alert(isPresented: $serviceNotificationPresented, content: { + Alert( + title: Text(serviceNotificationTitle), + message: Text(serviceNotificationContent), + dismissButton: .default(Text("Ok")) + ) + }) + .onAppear { + ServiceNotification.setServiceNotificationListener { notification in + serviceNotificationTitle = notification.title + serviceNotificationContent = notification.body + serviceNotificationPresented = true + } + } + .onDisappear { + ServiceNotification.removeServiceNotificationListener() + } + .onChange(of: scenePhase, perform: { newValue in + if newValue == .active { + Task.detached { + await loadProfile() + } + } + }) + .environment(\.selection, $selection) + .environment(\.extensionProfile, $extensionProfile) + .environment(\.logClient, $logClient) + } + + private func loadProfile() async { + defer { + profileLoading = false + } + if let newProfile = try? await ExtensionProfile.load() { + if extensionProfile == nil || extensionProfile?.status == .invalid { + newProfile.register() + extensionProfile = newProfile + } + } else { + extensionProfile = nil + } + } + + private func connectLog() { + guard let profile = extensionProfile else { + return + } + guard let logClient else { + return + } + if profile.status.isConnected, !logClient.isConnected { + logClient.reconnect() + } + } +} diff --git a/SFI/SFI.entitlements b/SFI/SFI.entitlements new file mode 100644 index 0000000..ed88ee1 --- /dev/null +++ b/SFI/SFI.entitlements @@ -0,0 +1,26 @@ + + + + + com.apple.developer.icloud-container-identifiers + + iCloud.io.nekohasekai.sfa + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.developer.ubiquity-container-identifiers + + iCloud.io.nekohasekai.sfa + + com.apple.security.application-groups + + group.io.nekohasekai.sfa + + + diff --git a/SFM/Application.swift b/SFM/Application.swift new file mode 100644 index 0000000..c43fb7f --- /dev/null +++ b/SFM/Application.swift @@ -0,0 +1,80 @@ +import ApplicationLibrary +import Library +import SwiftUI + +@main +struct Application: App { + @NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate + + @State private var showMenuBarExtra = false + @State private var isMenuPresented = false + + var body: some Scene { + Window("sing-box", id: "main", content: { + MainView() + .onAppear { + Task.detached { + await initialize() + } + } + .environment(\.showMenuBarExtra, $showMenuBarExtra) + }) + .commands { + if showMenuBarExtra { + CommandGroup(replacing: .appTermination) { + Button("Quit sing-box") { + hide(closeApp: true) + } + .keyboardShortcut("q", modifiers: [.command]) + } + CommandGroup(replacing: .saveItem) { + Button("Close") { + hide(closeApp: false) + } + .keyboardShortcut("w", modifiers: [.command]) + } + } + SidebarCommands() + } + + Window("New Profile", id: NewProfileView.windowID) { + NewProfileView() + } + + WindowGroup("Edit Profile", id: EditProfileWindowView.windowID, for: Int64.self) { profileID in + EditProfileWindowView(profileID.wrappedValue) + }.commandsRemoved() + + WindowGroup("Edit Content", id: EditProfileContentView.windowID, for: EditProfileContentView.Context.self) { context in + EditProfileContentView(context.wrappedValue) + }.commandsRemoved() + + Window("Service Log", id: ServiceLogView.windowID) { + ServiceLogView() + } + MenuBarExtra(isInserted: $showMenuBarExtra) { + MenuView(isMenuPresented: $isMenuPresented) + } label: { + Image(systemName: "network.badge.shield.half.filled") + } + .menuBarExtraStyle(.window) + .menuBarExtraAccess(isPresented: $isMenuPresented) + } + + private func initialize() async { + let initialShowMenuBarExtra = SharedPreferences.showMenuBarExtra + await MainActor.run { + showMenuBarExtra = initialShowMenuBarExtra + } + } + + private func hide(closeApp: Bool) { + if closeApp || NSApp.keyWindow?.identifier?.rawValue == "main" { + let transformState = ProcessApplicationTransformState(kProcessTransformToUIElementApplication) + var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess)) + TransformProcessType(&psn, transformState) + NSApp.setActivationPolicy(.accessory) + } + NSApp.keyWindow?.close() + } +} diff --git a/SFM/ApplicationDelegate.swift b/SFM/ApplicationDelegate.swift new file mode 100644 index 0000000..716362e --- /dev/null +++ b/SFM/ApplicationDelegate.swift @@ -0,0 +1,51 @@ +import AppKit +import ApplicationLibrary +import Foundation +import Libbox +import Library + +class ApplicationDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_: Notification) { + NSLog("Here I stand") + // ServiceNotification.register() // Not work + let event = NSAppleEventManager.shared().currentAppleEvent + let launchedAsLogInItem = + event?.eventID == kAEOpenApplication && + event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem + if !launchedAsLogInItem || !SharedPreferences.showMenuBarExtra { + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + } else { + NSApp.windows.first?.close() + } + Task.detached { + do { + try await self.postStart(launchedAsLogInItem) + } catch { + NSLog("application setup error: \(error.localizedDescription)") + } + } + } + + private func postStart(_ launchedAsLogInItem: Bool) async throws { + try ProfileUpdateTask.setup() + if launchedAsLogInItem { + if SharedPreferences.startedByUser { + if let profile = try await ExtensionProfile.load() { + try await profile.start() + } + } + } + } + + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + !SharedPreferences.showMenuBarExtra + } + + func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + if !flag, NSApp.activationPolicy() == .accessory { + NSApp.setActivationPolicy(.regular) + } + return true + } +} diff --git a/SFM/Assets.xcassets/AccentColor.colorset/Contents.json b/SFM/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..0afb3cf --- /dev/null +++ b/SFM/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors": [ + { + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/Contents.json b/SFM/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..f4ac7bc --- /dev/null +++ b/SFM/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "apple-16 1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "apple-16 2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "apple-32 1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "apple-32 2x 1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "apple-128 1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "apple-128 2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "apple-256 1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "apple-256 2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "apple-512 1x.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "apple-512 2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-128 1x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-128 1x.png new file mode 100644 index 0000000..9b10cd7 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-128 1x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-128 2x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-128 2x.png new file mode 100644 index 0000000..21693a6 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-128 2x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-16 1x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-16 1x.png new file mode 100644 index 0000000..28df597 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-16 1x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-16 2x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-16 2x.png new file mode 100644 index 0000000..6852edd Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-16 2x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-256 1x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-256 1x.png new file mode 100644 index 0000000..db0a7f0 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-256 1x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-256 2x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-256 2x.png new file mode 100644 index 0000000..4449735 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-256 2x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-32 1x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-32 1x.png new file mode 100644 index 0000000..171cb5b Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-32 1x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-32 2x 1.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-32 2x 1.png new file mode 100644 index 0000000..1654da9 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-32 2x 1.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-512 1x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-512 1x.png new file mode 100644 index 0000000..99e829a Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-512 1x.png differ diff --git a/SFM/Assets.xcassets/AppIcon.appiconset/apple-512 2x.png b/SFM/Assets.xcassets/AppIcon.appiconset/apple-512 2x.png new file mode 100644 index 0000000..88a80d6 Binary files /dev/null and b/SFM/Assets.xcassets/AppIcon.appiconset/apple-512 2x.png differ diff --git a/SFM/Assets.xcassets/Contents.json b/SFM/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFM/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFM/Info.plist b/SFM/Info.plist new file mode 100644 index 0000000..710b062 --- /dev/null +++ b/SFM/Info.plist @@ -0,0 +1,20 @@ + + + + + ITSAppUsesNonExemptEncryption + + NSUbiquitousContainers + + iCloud.io.nekohasekai.sfa + + NSUbiquitousContainerIsDocumentScopePublic + + NSUbiquitousContainerName + sing-box + NSUbiquitousContainerSupportedFolderLevels + Any + + + + diff --git a/SFM/MainView.swift b/SFM/MainView.swift new file mode 100644 index 0000000..34eb03f --- /dev/null +++ b/SFM/MainView.swift @@ -0,0 +1,101 @@ +import ApplicationLibrary +import Library +import SwiftUI + +struct MainView: View { + @Environment(\.controlActiveState) private var controlActiveState + + @State private var selection = NavigationPage.dashboard + @State private var extensionProfile: ExtensionProfile? + @State private var profileLoading = true + @State private var logClient: LogClient! + + @State private var serviceNotificationTitle = "" + @State private var serviceNotificationContent = "" + @State private var serviceNotificationPresented = false + + var body: some View { + NavigationSplitView { + VStack { + SidebarView() + } + .frame(minWidth: 150) + } detail: { + if profileLoading { + ProgressView().onAppear { + Task { + logClient = LogClient(SharedPreferences.maxLogLines) + await loadProfile() + } + } + } else { + selection.contentView + } + } + .alert(isPresented: $serviceNotificationPresented, content: { + Alert( + title: Text(serviceNotificationTitle), + message: Text(serviceNotificationContent), + dismissButton: .default(Text("Ok")) + ) + }) + .onAppear { + ServiceNotification.setServiceNotificationListener { notification in + serviceNotificationTitle = notification.title + serviceNotificationContent = notification.body + serviceNotificationPresented = true + } + } + .onDisappear { + ServiceNotification.removeServiceNotificationListener() + } + .toolbar { + ToolbarItem(placement: .navigation) { + StartStopButton() + } + } + .onChange(of: controlActiveState, perform: { newValue in + if newValue != .inactive { + Task { + await loadProfile() + connectLog() + } + } + }) + .onChange(of: selection, perform: { value in + if value == .logs { + connectLog() + } + }) + .formStyle(.grouped) + .environment(\.selection, $selection) + .environment(\.extensionProfile, $extensionProfile) + .environment(\.logClient, $logClient) + } + + private func loadProfile() async { + defer { + profileLoading = false + } + if let newProfile = try? await ExtensionProfile.load() { + if extensionProfile == nil { + newProfile.register() + extensionProfile = newProfile + } + } else { + extensionProfile = nil + } + } + + private func connectLog() { + guard let profile = extensionProfile else { + return + } + guard let logClient else { + return + } + if profile.status.isConnected, !logClient.isConnected { + logClient.reconnect() + } + } +} diff --git a/SFM/MenuView.swift b/SFM/MenuView.swift new file mode 100644 index 0000000..4a42dfb --- /dev/null +++ b/SFM/MenuView.swift @@ -0,0 +1,210 @@ +import ApplicationLibrary +import Foundation +import Libbox +import Library +import MacControlCenterUI +import MenuBarExtraAccess +import SwiftUI + +struct MenuView: View { + @Environment(\.openWindow) private var openWindow + + private static let sliderWidth: CGFloat = 270 + + @Binding var isMenuPresented: Bool + + @State private var isLoading = true + @State private var profile: ExtensionProfile? + + var body: some View { + MacControlCenterMenu(isPresented: $isMenuPresented) { + MenuHeader("sing-box") { + if isLoading { + Text("Loading...").foregroundColor(.secondary).onAppear { + Task.detached { + await loadProfile() + } + } + } else if let profile { + Text(LibboxVersion()).foregroundColor(.secondary) + StatusSwitch(profile) + } else { + Text("NetworkExtension not installed") + } + } + .frame(minWidth: MenuView.sliderWidth) + if let profile { + ProfilePicker(profile) + } + Divider() + MenuCommand { + NSApp.setActivationPolicy(.regular) + openWindow(id: "main") + } label: { + Text("Open") + } + MenuCommand { + NSApp.terminate(nil) + } label: { + Text("Exit") + } + } + } + + private func loadProfile() async { + profile = try? await ExtensionProfile.load() + if let profile { + profile.register() + } + isLoading = false + } + + private struct StatusSwitch: View { + @ObservedObject private var profile: ExtensionProfile + @State private var errorPresented = false + @State private var errorMessage = "" + + init(_ profile: ExtensionProfile) { + self.profile = profile + } + + var body: some View { + Toggle(isOn: Binding(get: { + profile.status.isConnected + }, set: { _ in + Task.detached { + await switchProfile(!profile.status.isConnected) + } + })) {} + .toggleStyle(.switch) + .disabled(!profile.status.isEnabled) + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + private func switchProfile(_ isEnabled: Bool) async { + do { + if isEnabled { + try await profile.start() + } else { + profile.stop() + } + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + } + } + + private struct ProfilePicker: View { + @ObservedObject private var profile: ExtensionProfile + + init(_ profile: ExtensionProfile) { + self.profile = profile + } + + @State private var isLoading = true + @State private var profileList: [Profile] = [] + @State private var selectedProfileID: Int64! + @State private var reasserting = false + + @State private var errorPresented = false + @State private var errorMessage = "" + @State private var observer: Any? + + var body: some View { + viewBuilder { + if isLoading { + ProgressView().onAppear { + Task.detached { + await doReload() + } + } + } else { + if profileList.isEmpty { + Text("Empty profiles") + } else { + MenuSection("Profile") + Picker("", selection: $selectedProfileID) { + ForEach(profileList, id: \.id) { profile in + Text(profile.name) + } + } + .pickerStyle(.inline) + .onChange(of: selectedProfileID) { _ in + reasserting = true + Task.detached { + await switchProfile(selectedProfileID!) + } + } + .disabled(!profile.status.isSwitchable || reasserting) + } + } + } + .onAppear { + if observer == nil { + observer = NotificationCenter.default.addObserver(forName: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { _ in + doReload() + }) + } + } + .onDisappear { + if let observer { + NotificationCenter.default.removeObserver(observer) + } + } + .alert(isPresented: $errorPresented) { + Alert( + title: Text("Error"), + message: Text(errorMessage), + dismissButton: .default(Text("Ok")) + ) + } + } + + private func doReload() { + defer { + isLoading = false + } + do { + profileList = try ProfileManager.list() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + return + } + if profileList.isEmpty { + return + } + + selectedProfileID = SharedPreferences.selectedProfileID + if profileList.filter({ profile in + profile.id == selectedProfileID + }) + .isEmpty { + selectedProfileID = profileList[0].id! + SharedPreferences.selectedProfileID = selectedProfileID + } + } + + private func switchProfile(_ newProfileID: Int64) { + SharedPreferences.selectedProfileID = newProfileID + NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil) + if profile.status.isConnected { + do { + try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + } catch { + errorMessage = error.localizedDescription + errorPresented = true + } + } + reasserting = false + } + } +} diff --git a/SFM/SFM.entitlements b/SFM/SFM.entitlements new file mode 100644 index 0000000..d7564b5 --- /dev/null +++ b/SFM/SFM.entitlements @@ -0,0 +1,34 @@ + + + + + com.apple.developer.icloud-container-identifiers + + iCloud.io.nekohasekai.sfa + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.developer.ubiquity-container-identifiers + + iCloud.io.nekohasekai.sfa + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.io.nekohasekai.sfa + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/SFM/SidebarView.swift b/SFM/SidebarView.swift new file mode 100644 index 0000000..fb9078a --- /dev/null +++ b/SFM/SidebarView.swift @@ -0,0 +1,47 @@ +import ApplicationLibrary +import Library +import SwiftUI + +struct SidebarView: View { + @Environment(\.selection) private var selection + @Environment(\.extensionProfile) private var extensionProfile + + var body: some View { + viewBuilder { + if let profile = extensionProfile.wrappedValue { + SidebarView0().environmentObject(profile) + } else { + SidebarView1() + } + } + } + + struct SidebarView0: View { + @Environment(\.selection) private var selection + @EnvironmentObject private var extensionProfile: ExtensionProfile + + var body: some View { + List(NavigationPage.allCases.filter { it in + it.visible(extensionProfile) + }, selection: selection) { it in + it.label + }.onChange(of: extensionProfile.status) { _ in + if !selection.wrappedValue.visible(extensionProfile) { + selection.wrappedValue = NavigationPage.dashboard + } + } + } + } + + struct SidebarView1: View { + @Environment(\.selection) private var selection + + var body: some View { + List(NavigationPage.allCases.filter { it in + it.visible(nil) + }, selection: selection) { it in + it.label + } + } + } +} diff --git a/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json b/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json b/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/Contents.json b/WidgetExtension/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json b/WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Info.plist b/WidgetExtension/Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/WidgetExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/WidgetExtension/Intents.swift b/WidgetExtension/Intents.swift new file mode 100644 index 0000000..6be7028 --- /dev/null +++ b/WidgetExtension/Intents.swift @@ -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() + } +} diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtension/WidgetExtension.entitlements new file mode 100644 index 0000000..6524ff1 --- /dev/null +++ b/WidgetExtension/WidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.sagernet.sfa + + + diff --git a/WidgetExtension/WidgetExtension.swift b/WidgetExtension/WidgetExtension.swift new file mode 100644 index 0000000..5ba8777 --- /dev/null +++ b/WidgetExtension/WidgetExtension.swift @@ -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 { + 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]) + } +} diff --git a/WidgetExtension/WidgetExtensionBundle.swift b/WidgetExtension/WidgetExtensionBundle.swift new file mode 100644 index 0000000..fab679b --- /dev/null +++ b/WidgetExtension/WidgetExtensionBundle.swift @@ -0,0 +1,9 @@ +import SwiftUI +import WidgetKit + +@main +struct WidgetExtensionBundle: WidgetBundle { + var body: some Widget { + WidgetExtension() + } +} diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1046b3f --- /dev/null +++ b/sing-box.xcodeproj/project.pbxproj @@ -0,0 +1,1783 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 3A017F922A4AB2E4009149FA /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 3A017F912A4AB2E4009149FA /* GRDB */; }; + 3A096F8A2A4ED3DE00D4A2ED /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A096F892A4ED3DE00D4A2ED /* PacketTunnelProvider.swift */; }; + 3A096F8F2A4ED3DE00D4A2ED /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3A1CF2F02A50E5EE000A8289 /* GroupListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1CF2EF2A50E5EE000A8289 /* GroupListView.swift */; }; + 3A1CF2F22A50E613000A8289 /* OutboundGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1CF2F12A50E613000A8289 /* OutboundGroup.swift */; }; + 3A1CF2F42A50E937000A8289 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1CF2F32A50E937000A8289 /* SidebarView.swift */; }; + 3A1CF2F62A50EE9C000A8289 /* GroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1CF2F52A50EE9C000A8289 /* GroupView.swift */; }; + 3A1CF2F82A50F0A5000A8289 /* GroupItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1CF2F72A50F0A5000A8289 /* GroupItemView.swift */; }; + 3A1CF2FA2A50F0BD000A8289 /* OutboundGroupItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1CF2F92A50F0BD000A8289 /* OutboundGroupItem.swift */; }; + 3A251C122A52D09700651082 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */; }; + 3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; + 3A3AA7FF2A4EFDB3002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; + 3A3DEBEB2A4FFE2D00373BF4 /* AppIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A3DEBE62A4FFA6000373BF4 /* AppIntents.framework */; }; + 3A44BB822A4DC28700E4C9F8 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A44BB812A4DC28700E4C9F8 /* MainView.swift */; }; + 3A4EAD162A4FEAE6005435B3 /* ApplicationLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; }; + 3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; + 3A4EAD212A4FEB3C005435B3 /* ApplicationLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4EAD202A4FEB3C005435B3 /* ApplicationLibrary.swift */; }; + 3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC21742A45B0B800A63465 /* NavigationPage.swift */; }; + 3A4EAD232A4FEB5A005435B3 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC21782A45BA5300A63465 /* EnvironmentValues.swift */; }; + 3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A22A4A9B9B002B34AC /* InstallProfileButton.swift */; }; + 3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342CC2A4AA88C002B34AC /* StartStopButton.swift */; }; + 3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342D02A4AACC4002B34AC /* ExtensionStatusView.swift */; }; + 3A4EAD272A4FEB65005435B3 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */; }; + 3A4EAD282A4FEB65005435B3 /* ActiveDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */; }; + 3A4EAD292A4FEB6D005435B3 /* Formtem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342D32A4AADB2002B34AC /* Formtem.swift */; }; + 3A4EAD2A2A4FEB6D005435B3 /* Binding+Unwrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8E022A4B118700900CC8 /* Binding+Unwrap.swift */; }; + 3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7E90302A46745A00D53052 /* ViewBuilder.swift */; }; + 3A4EAD2C2A4FEB77005435B3 /* EditProfileWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8DFC2A4B096000900CC8 /* EditProfileWindowView.swift */; }; + 3A4EAD2D2A4FEB77005435B3 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */; }; + 3A4EAD2E2A4FEB77005435B3 /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8E002A4B0F6300900CC8 /* EditProfileView.swift */; }; + 3A4EAD2F2A4FEB77005435B3 /* EditProfileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */; }; + 3A4EAD302A4FEB77005435B3 /* NewProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8DF82A4AFCB400900CC8 /* NewProfileView.swift */; }; + 3A4EAD312A4FEB7B005435B3 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB5E712A4BF6F6009757F1 /* SettingView.swift */; }; + 3A4EAD322A4FEB7B005435B3 /* ServiceLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */; }; + 3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA1ABBB2A4C4107000FD4BA /* LogClient.swift */; }; + 3A4EAD342A4FEB7F005435B3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA1ABB92A4C4054000FD4BA /* LogView.swift */; }; + 3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */; }; + 3A4EAD362A4FEB9C005435B3 /* ProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */; }; + 3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; }; + 3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */; }; + 3A57DF372A4D5D2600690BC5 /* Profile+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */; }; + 3A57DF3C2A4D705000690BC5 /* MacControlCenterUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3A57DF3B2A4D705000690BC5 /* MacControlCenterUI */; }; + 3A57DF402A4D70B600690BC5 /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF3F2A4D70B600690BC5 /* MenuView.swift */; }; + 3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */; }; + 3A5F26C82A503D4A00C27EDF /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; + 3A5F26C92A503D4A00C27EDF /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3A648D2D2A4EEAA600D95A12 /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A648D2C2A4EEAA600D95A12 /* Library.swift */; }; + 3A648D542A4EF4C700D95A12 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; }; + 3A76504C2A4F08BA003945C5 /* Libbox.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC20DB2A4599D000A63465 /* Libbox.xcframework */; }; + 3A7701702A4E6B34008F031F /* IntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A77016F2A4E6B34008F031F /* IntentsExtension.swift */; }; + 3A7701722A4E6B34008F031F /* Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7701712A4E6B34008F031F /* Intents.swift */; }; + 3A7E90352A46756300D53052 /* SharedPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7E90342A46756300D53052 /* SharedPreferences.swift */; }; + 3A7E90382A46778E00D53052 /* BinaryCodable in Frameworks */ = {isa = PBXBuildFile; productRef = 3A7E90372A46778E00D53052 /* BinaryCodable */; }; + 3A8512122A5E8EF70076D233 /* Extension+Iterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342AA2A4AA173002B34AC /* Extension+Iterator.swift */; }; + 3A8512132A5E8EF70076D233 /* ExtensionPlatformInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A62A4AA0FF002B34AC /* ExtensionPlatformInterface.swift */; }; + 3A8512142A5E8EF70076D233 /* Extension+RunBlocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A82A4AA155002B34AC /* Extension+RunBlocking.swift */; }; + 3A8655142A4FA26600B7181F /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3A9108E42A511AE70088B196 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9108E32A511AE70088B196 /* ContentView.swift */; }; + 3A9144D92A46AE370036E9AD /* ShadredPreferences+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9144D82A46AE370036E9AD /* ShadredPreferences+Database.swift */; }; + 3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; + 3A9759212A4EB69C00E4404B /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3AC1944F2A50247300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */; }; + 3AC194502A502DFE00BD8CB9 /* ServiceNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */; }; + 3AC194522A50303300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */; }; + 3ADF8DFB2A4AFDF500900CC8 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC210D2A459B1900A63465 /* MainView.swift */; }; + 3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3AEC20F62A459AB400A63465 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC20F52A459AB400A63465 /* Application.swift */; }; + 3AEC20FA2A459AB500A63465 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AEC20F92A459AB500A63465 /* Assets.xcassets */; }; + 3AEC210C2A459B1900A63465 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC210B2A459B1900A63465 /* Application.swift */; }; + 3AEC21102A459B1A00A63465 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AEC210F2A459B1A00A63465 /* Assets.xcassets */; }; + 3AEC212F2A459D5600A63465 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC212E2A459D5600A63465 /* Profile.swift */; }; + 3AEC213C2A459FDF00A63465 /* Databse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC213B2A459FDF00A63465 /* Databse.swift */; }; + 3AEC21402A45A28F00A63465 /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC213F2A45A28F00A63465 /* ProfileManager.swift */; }; + 3AEC21422A45A8FF00A63465 /* Profile+Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC21412A45A8FF00A63465 /* Profile+Update.swift */; }; + 3AEC21452A45A93800A63465 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC21442A45A93800A63465 /* HTTPClient.swift */; }; + 3AEC21482A45A9DE00A63465 /* Bundle+Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC21472A45A9DE00A63465 /* Bundle+Version.swift */; }; + 3AEC214A2A45AA5600A63465 /* Profile+RW.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC21492A45AA5600A63465 /* Profile+RW.swift */; }; + 3AEC214C2A45AA8E00A63465 /* FilePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC214B2A45AA8E00A63465 /* FilePath.swift */; }; + 3AF342A02A4A9916002B34AC /* ExtensionProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF3429F2A4A9916002B34AC /* ExtensionProfile.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3A096F7D2A4ED1AD00D4A2ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AEC211C2A459B4700A63465; + remoteInfo = Library; + }; + 3A096F8D2A4ED3DE00D4A2ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A096F852A4ED3DE00D4A2ED; + remoteInfo = Extension; + }; + 3A44BB7E2A4DC1D800E4C9F8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A096F852A4ED3DE00D4A2ED; + remoteInfo = Extension; + }; + 3A4EAD142A4FEAE6005435B3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3; + remoteInfo = ApplicationLibrary; + }; + 3A4EAD1D2A4FEB02005435B3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AEC211C2A459B4700A63465; + remoteInfo = Library; + }; + 3A4EAD392A4FEC20005435B3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3; + remoteInfo = ApplicationLibrary; + }; + 3A76504A2A4F07F6003945C5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AEC211C2A459B4700A63465; + remoteInfo = Library; + }; + 3A77017D2A4E6B5E008F031F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AEC211C2A459B4700A63465; + remoteInfo = Library; + }; + 3A8655152A4FA26600B7181F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A77016C2A4E6B34008F031F; + remoteInfo = IntentsExtension; + }; + 3AC1944A2A50014000BD8CB9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A77016C2A4E6B34008F031F; + remoteInfo = IntentsExtension; + }; + 3AEAEE9A2A4F16430059612D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A096F852A4ED3DE00D4A2ED; + remoteInfo = Extension; + }; + 3AEC21372A459E0A00A63465 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AEC211C2A459B4700A63465; + remoteInfo = Library; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 3A096F762A4ED1A600D4A2ED /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 3A096F8F2A4ED3DE00D4A2ED /* Extension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3A44BB782A4DC17000E4C9F8 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3A5F26CA2A503D4B00C27EDF /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3A5F26C92A503D4A00C27EDF /* Library.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3A8655132A4FA25C00B7181F /* Embed ExtensionKit Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + 3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */, + ); + name = "Embed ExtensionKit Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3A8655172A4FA26600B7181F /* Embed ExtensionKit Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + 3A8655142A4FA26600B7181F /* IntentsExtension.appex in Embed ExtensionKit Extensions */, + ); + name = "Embed ExtensionKit Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3A9759222A4EB69C00E4404B /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3A9759212A4EB69C00E4404B /* Library.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 3ADB2DE02A4ABEF700FB9254 /* Embed System Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(SYSTEM_EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + ); + name = "Embed System Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3A096F862A4ED3DE00D4A2ED /* Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Extension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A096F892A4ED3DE00D4A2ED /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; + 3A096F8B2A4ED3DE00D4A2ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3A096F8C2A4ED3DE00D4A2ED /* Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Extension.entitlements; sourceTree = ""; }; + 3A1CF2EF2A50E5EE000A8289 /* GroupListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListView.swift; sourceTree = ""; }; + 3A1CF2F12A50E613000A8289 /* OutboundGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboundGroup.swift; sourceTree = ""; }; + 3A1CF2F32A50E937000A8289 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + 3A1CF2F52A50EE9C000A8289 /* GroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = ""; }; + 3A1CF2F72A50F0A5000A8289 /* GroupItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItemView.swift; sourceTree = ""; }; + 3A1CF2F92A50F0BD000A8289 /* OutboundGroupItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboundGroupItem.swift; sourceTree = ""; }; + 3A3DEBE12A4FFA1A00373BF4 /* ExtensionFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExtensionFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/ExtensionFoundation.framework; sourceTree = DEVELOPER_DIR; }; + 3A3DEBE62A4FFA6000373BF4 /* AppIntents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppIntents.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/AppIntents.framework; sourceTree = DEVELOPER_DIR; }; + 3A44BB662A4DBF7900E4C9F8 /* SFI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SFI.entitlements; sourceTree = ""; }; + 3A44BB802A4DC25E00E4C9F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 3A44BB812A4DC28700E4C9F8 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApplicationLibrary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A4EAD202A4FEB3C005435B3 /* ApplicationLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationLibrary.swift; sourceTree = ""; }; + 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+isConnected.swift"; sourceTree = ""; }; + 3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIProfileUpdateTask.swift; sourceTree = ""; }; + 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateTask.swift; sourceTree = ""; }; + 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Date.swift"; sourceTree = ""; }; + 3A57DF3F2A4D70B600690BC5 /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; + 3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Hashable.swift"; sourceTree = ""; }; + 3A648D2C2A4EEAA600D95A12 /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = ""; }; + 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = IntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A77016F2A4E6B34008F031F /* IntentsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsExtension.swift; sourceTree = ""; }; + 3A7701712A4E6B34008F031F /* Intents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Intents.swift; sourceTree = ""; }; + 3A7701732A4E6B34008F031F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3A7701802A4E71F5008F031F /* IntentsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IntentsExtension.entitlements; sourceTree = ""; }; + 3A7E90302A46745A00D53052 /* ViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewBuilder.swift; sourceTree = ""; }; + 3A7E90342A46756300D53052 /* SharedPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedPreferences.swift; sourceTree = ""; }; + 3A9108E32A511AE70088B196 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 3A9144D82A46AE370036E9AD /* ShadredPreferences+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShadredPreferences+Database.swift"; sourceTree = ""; }; + 3AA1ABB92A4C4054000FD4BA /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; + 3AA1ABBB2A4C4107000FD4BA /* LogClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogClient.swift; sourceTree = ""; }; + 3AAB5E712A4BF6F6009757F1 /* SettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = ""; }; + 3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLogView.swift; sourceTree = ""; }; + 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileContentView.swift; sourceTree = ""; }; + 3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceNotification.swift; sourceTree = ""; }; + 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; + 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; + 3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveDashboardView.swift; sourceTree = ""; }; + 3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; + 3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 3ADF8DF82A4AFCB400900CC8 /* NewProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileView.swift; sourceTree = ""; }; + 3ADF8DFC2A4B096000900CC8 /* EditProfileWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileWindowView.swift; sourceTree = ""; }; + 3ADF8E002A4B0F6300900CC8 /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = ""; }; + 3ADF8E022A4B118700900CC8 /* Binding+Unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Unwrap.swift"; sourceTree = ""; }; + 3AEAEE9C2A4F1A9D0059612D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3AEC20DB2A4599D000A63465 /* Libbox.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Libbox.xcframework; sourceTree = ""; }; + 3AEC20F32A459AB400A63465 /* SFI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SFI.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AEC20F52A459AB400A63465 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + 3AEC20F92A459AB500A63465 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3AEC21092A459B1900A63465 /* sing-box β.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box β.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AEC210B2A459B1900A63465 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + 3AEC210D2A459B1900A63465 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 3AEC210F2A459B1A00A63465 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3AEC21142A459B1A00A63465 /* SFM.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SFM.entitlements; sourceTree = ""; }; + 3AEC211D2A459B4700A63465 /* Library.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Library.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AEC212E2A459D5600A63465 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; + 3AEC213B2A459FDF00A63465 /* Databse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Databse.swift; sourceTree = ""; }; + 3AEC213F2A45A28F00A63465 /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; + 3AEC21412A45A8FF00A63465 /* Profile+Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Update.swift"; sourceTree = ""; }; + 3AEC21442A45A93800A63465 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; + 3AEC21472A45A9DE00A63465 /* Bundle+Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Version.swift"; sourceTree = ""; }; + 3AEC21492A45AA5600A63465 /* Profile+RW.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+RW.swift"; sourceTree = ""; }; + 3AEC214B2A45AA8E00A63465 /* FilePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePath.swift; sourceTree = ""; }; + 3AEC21742A45B0B800A63465 /* NavigationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationPage.swift; sourceTree = ""; }; + 3AEC21782A45BA5300A63465 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; + 3AF3429F2A4A9916002B34AC /* ExtensionProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionProfile.swift; sourceTree = ""; }; + 3AF342A22A4A9B9B002B34AC /* InstallProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallProfileButton.swift; sourceTree = ""; }; + 3AF342A62A4AA0FF002B34AC /* ExtensionPlatformInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPlatformInterface.swift; sourceTree = ""; }; + 3AF342A82A4AA155002B34AC /* Extension+RunBlocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+RunBlocking.swift"; sourceTree = ""; }; + 3AF342AA2A4AA173002B34AC /* Extension+Iterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension+Iterator.swift"; sourceTree = ""; }; + 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; + 3AF342CC2A4AA88C002B34AC /* StartStopButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStopButton.swift; sourceTree = ""; }; + 3AF342D02A4AACC4002B34AC /* ExtensionStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionStatusView.swift; sourceTree = ""; }; + 3AF342D32A4AADB2002B34AC /* Formtem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formtem.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3A096F832A4ED3DE00D4A2ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A3AA7FF2A4EFDB3002F78AB /* Library.framework in Frameworks */, + 3A648D542A4EF4C700D95A12 /* NetworkExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A4EAD0D2A4FEAE6005435B3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A77016A2A4E6B34008F031F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A3DEBEB2A4FFE2D00373BF4 /* AppIntents.framework in Frameworks */, + 3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC20F02A459AB400A63465 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */, + 3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC21062A459B1900A63465 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A5F26C82A503D4A00C27EDF /* Library.framework in Frameworks */, + 3A4EAD162A4FEAE6005435B3 /* ApplicationLibrary.framework in Frameworks */, + 3A57DF3C2A4D705000690BC5 /* MacControlCenterUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC211A2A459B4700A63465 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A017F922A4AB2E4009149FA /* GRDB in Frameworks */, + 3A76504C2A4F08BA003945C5 /* Libbox.xcframework in Frameworks */, + 3A7E90382A46778E00D53052 /* BinaryCodable in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3A096F882A4ED3DE00D4A2ED /* Extension */ = { + isa = PBXGroup; + children = ( + 3AF342A62A4AA0FF002B34AC /* ExtensionPlatformInterface.swift */, + 3AF342A82A4AA155002B34AC /* Extension+RunBlocking.swift */, + 3AF342AA2A4AA173002B34AC /* Extension+Iterator.swift */, + 3A096F892A4ED3DE00D4A2ED /* PacketTunnelProvider.swift */, + 3A096F8B2A4ED3DE00D4A2ED /* Info.plist */, + 3A096F8C2A4ED3DE00D4A2ED /* Extension.entitlements */, + ); + path = Extension; + sourceTree = ""; + }; + 3A1CF2EE2A50E5D5000A8289 /* Groups */ = { + isa = PBXGroup; + children = ( + 3A1CF2F92A50F0BD000A8289 /* OutboundGroupItem.swift */, + 3A1CF2EF2A50E5EE000A8289 /* GroupListView.swift */, + 3A1CF2F12A50E613000A8289 /* OutboundGroup.swift */, + 3A1CF2F52A50EE9C000A8289 /* GroupView.swift */, + 3A1CF2F72A50F0A5000A8289 /* GroupItemView.swift */, + ); + path = Groups; + sourceTree = ""; + }; + 3A4EAD112A4FEAE6005435B3 /* ApplicationLibrary */ = { + isa = PBXGroup; + children = ( + 3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */, + 3A55F9562A4D1366003C4EF4 /* Service */, + 3AEC21732A45B0AC00A63465 /* Views */, + 3A4EAD202A4FEB3C005435B3 /* ApplicationLibrary.swift */, + ); + path = ApplicationLibrary; + sourceTree = ""; + }; + 3A55F9562A4D1366003C4EF4 /* Service */ = { + isa = PBXGroup; + children = ( + 3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */, + 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */, + ); + path = Service; + sourceTree = ""; + }; + 3A77016E2A4E6B34008F031F /* IntentsExtension */ = { + isa = PBXGroup; + children = ( + 3A7701802A4E71F5008F031F /* IntentsExtension.entitlements */, + 3A77016F2A4E6B34008F031F /* IntentsExtension.swift */, + 3A7701712A4E6B34008F031F /* Intents.swift */, + 3A7701732A4E6B34008F031F /* Info.plist */, + ); + path = IntentsExtension; + sourceTree = ""; + }; + 3AA1ABB62A4C401A000FD4BA /* Log */ = { + isa = PBXGroup; + children = ( + 3AA1ABB92A4C4054000FD4BA /* LogView.swift */, + 3AA1ABBB2A4C4107000FD4BA /* LogClient.swift */, + ); + path = Log; + sourceTree = ""; + }; + 3AAB5E702A4BF6EA009757F1 /* Setting */ = { + isa = PBXGroup; + children = ( + 3AAB5E712A4BF6F6009757F1 /* SettingView.swift */, + 3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */, + ); + path = Setting; + sourceTree = ""; + }; + 3ADF8DF52A4AFA8E00900CC8 /* Profile */ = { + isa = PBXGroup; + children = ( + 3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */, + 3ADF8DF82A4AFCB400900CC8 /* NewProfileView.swift */, + 3ADF8DFC2A4B096000900CC8 /* EditProfileWindowView.swift */, + 3ADF8E002A4B0F6300900CC8 /* EditProfileView.swift */, + 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */, + ); + path = Profile; + sourceTree = ""; + }; + 3AEC20BC2A45991900A63465 = { + isa = PBXGroup; + children = ( + 3AEC20DB2A4599D000A63465 /* Libbox.xcframework */, + 3AEC20F42A459AB400A63465 /* SFI */, + 3AEC210A2A459B1900A63465 /* SFM */, + 3AEC211E2A459B4700A63465 /* Library */, + 3A77016E2A4E6B34008F031F /* IntentsExtension */, + 3A096F882A4ED3DE00D4A2ED /* Extension */, + 3A4EAD112A4FEAE6005435B3 /* ApplicationLibrary */, + 3AEC20C72A45991900A63465 /* Products */, + 3AEC21012A459AE300A63465 /* Frameworks */, + ); + sourceTree = ""; + }; + 3AEC20C72A45991900A63465 /* Products */ = { + isa = PBXGroup; + children = ( + 3AEC20F32A459AB400A63465 /* SFI.app */, + 3AEC21092A459B1900A63465 /* sing-box β.app */, + 3AEC211D2A459B4700A63465 /* Library.framework */, + 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */, + 3A096F862A4ED3DE00D4A2ED /* Extension.appex */, + 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */, + ); + name = Products; + sourceTree = ""; + }; + 3AEC20F42A459AB400A63465 /* SFI */ = { + isa = PBXGroup; + children = ( + 3A44BB802A4DC25E00E4C9F8 /* Info.plist */, + 3A44BB662A4DBF7900E4C9F8 /* SFI.entitlements */, + 3AEC20F52A459AB400A63465 /* Application.swift */, + 3AEC20F92A459AB500A63465 /* Assets.xcassets */, + 3A44BB812A4DC28700E4C9F8 /* MainView.swift */, + 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */, + 3A9108E32A511AE70088B196 /* ContentView.swift */, + ); + path = SFI; + sourceTree = ""; + }; + 3AEC21012A459AE300A63465 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3A3DEBE62A4FFA6000373BF4 /* AppIntents.framework */, + 3A3DEBE12A4FFA1A00373BF4 /* ExtensionFoundation.framework */, + 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3AEC210A2A459B1900A63465 /* SFM */ = { + isa = PBXGroup; + children = ( + 3AEC210D2A459B1900A63465 /* MainView.swift */, + 3AEC210B2A459B1900A63465 /* Application.swift */, + 3AEC210F2A459B1A00A63465 /* Assets.xcassets */, + 3AEC21142A459B1A00A63465 /* SFM.entitlements */, + 3A57DF3F2A4D70B600690BC5 /* MenuView.swift */, + 3AEAEE9C2A4F1A9D0059612D /* Info.plist */, + 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */, + 3A1CF2F32A50E937000A8289 /* SidebarView.swift */, + ); + path = SFM; + sourceTree = ""; + }; + 3AEC211E2A459B4700A63465 /* Library */ = { + isa = PBXGroup; + children = ( + 3AEC21462A45A9CE00A63465 /* Shared */, + 3AEC21432A45A92B00A63465 /* Network */, + 3AEC213A2A459FD200A63465 /* Database */, + 3A648D2C2A4EEAA600D95A12 /* Library.swift */, + ); + path = Library; + sourceTree = ""; + }; + 3AEC213A2A459FD200A63465 /* Database */ = { + isa = PBXGroup; + children = ( + 3AEC212E2A459D5600A63465 /* Profile.swift */, + 3AEC213B2A459FDF00A63465 /* Databse.swift */, + 3AEC213F2A45A28F00A63465 /* ProfileManager.swift */, + 3AEC21412A45A8FF00A63465 /* Profile+Update.swift */, + 3AEC21492A45AA5600A63465 /* Profile+RW.swift */, + 3A7E90342A46756300D53052 /* SharedPreferences.swift */, + 3A9144D82A46AE370036E9AD /* ShadredPreferences+Database.swift */, + 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */, + 3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */, + ); + path = Database; + sourceTree = ""; + }; + 3AEC21432A45A92B00A63465 /* Network */ = { + isa = PBXGroup; + children = ( + 3AEC21442A45A93800A63465 /* HTTPClient.swift */, + 3AF3429F2A4A9916002B34AC /* ExtensionProfile.swift */, + 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */, + ); + path = Network; + sourceTree = ""; + }; + 3AEC21462A45A9CE00A63465 /* Shared */ = { + isa = PBXGroup; + children = ( + 3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */, + 3AEC21472A45A9DE00A63465 /* Bundle+Version.swift */, + 3AEC214B2A45AA8E00A63465 /* FilePath.swift */, + ); + path = Shared; + sourceTree = ""; + }; + 3AEC21732A45B0AC00A63465 /* Views */ = { + isa = PBXGroup; + children = ( + 3AF342D22A4AADA5002B34AC /* Abstract */, + 3AF342A12A4A9B8D002B34AC /* Dashboard */, + 3A1CF2EE2A50E5D5000A8289 /* Groups */, + 3AA1ABB62A4C401A000FD4BA /* Log */, + 3ADF8DF52A4AFA8E00900CC8 /* Profile */, + 3AAB5E702A4BF6EA009757F1 /* Setting */, + 3AEC21742A45B0B800A63465 /* NavigationPage.swift */, + 3AEC21782A45BA5300A63465 /* EnvironmentValues.swift */, + ); + path = Views; + sourceTree = ""; + }; + 3AF342A12A4A9B8D002B34AC /* Dashboard */ = { + isa = PBXGroup; + children = ( + 3AF342A22A4A9B9B002B34AC /* InstallProfileButton.swift */, + 3AF342CC2A4AA88C002B34AC /* StartStopButton.swift */, + 3AF342D02A4AACC4002B34AC /* ExtensionStatusView.swift */, + 3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */, + 3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */, + ); + path = Dashboard; + sourceTree = ""; + }; + 3AF342D22A4AADA5002B34AC /* Abstract */ = { + isa = PBXGroup; + children = ( + 3A7E90302A46745A00D53052 /* ViewBuilder.swift */, + 3AF342D32A4AADB2002B34AC /* Formtem.swift */, + 3ADF8E022A4B118700900CC8 /* Binding+Unwrap.swift */, + ); + path = Abstract; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3A4EAD0B2A4FEAE6005435B3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC21182A459B4700A63465 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3A096F852A4ED3DE00D4A2ED /* Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A096F902A4ED3DE00D4A2ED /* Build configuration list for PBXNativeTarget "Extension" */; + buildPhases = ( + 3A096F822A4ED3DE00D4A2ED /* Sources */, + 3A096F832A4ED3DE00D4A2ED /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3A76504B2A4F07F6003945C5 /* PBXTargetDependency */, + ); + name = Extension; + productName = Extension; + productReference = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A4EAD182A4FEAE6005435B3 /* Build configuration list for PBXNativeTarget "ApplicationLibrary" */; + buildPhases = ( + 3A4EAD0B2A4FEAE6005435B3 /* Headers */, + 3A4EAD0C2A4FEAE6005435B3 /* Sources */, + 3A4EAD0D2A4FEAE6005435B3 /* Frameworks */, + 3A4EAD0E2A4FEAE6005435B3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3A4EAD1E2A4FEB02005435B3 /* PBXTargetDependency */, + ); + name = ApplicationLibrary; + productName = ApplicationLibrary; + productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; + productType = "com.apple.product-type.framework"; + }; + 3A77016C2A4E6B34008F031F /* IntentsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */; + buildPhases = ( + 3A7701692A4E6B34008F031F /* Sources */, + 3A77016A2A4E6B34008F031F /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3A77017E2A4E6B5E008F031F /* PBXTargetDependency */, + ); + name = IntentsExtension; + productName = IntentsExtension; + productReference = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; + productType = "com.apple.product-type.extensionkit-extension"; + }; + 3AEC20F22A459AB400A63465 /* SFI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */; + buildPhases = ( + 3AEC20EF2A459AB400A63465 /* Sources */, + 3AEC20F02A459AB400A63465 /* Frameworks */, + 3AEC20F12A459AB400A63465 /* Resources */, + 3A44BB782A4DC17000E4C9F8 /* Embed Foundation Extensions */, + 3A9759222A4EB69C00E4404B /* Embed Frameworks */, + 3A8655172A4FA26600B7181F /* Embed ExtensionKit Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 3AEC21382A459E0A00A63465 /* PBXTargetDependency */, + 3A44BB7F2A4DC1D800E4C9F8 /* PBXTargetDependency */, + 3AEAEE9B2A4F16430059612D /* PBXTargetDependency */, + 3A8655162A4FA26600B7181F /* PBXTargetDependency */, + 3A4EAD3A2A4FEC20005435B3 /* PBXTargetDependency */, + ); + name = SFI; + packageProductDependencies = ( + ); + productName = SFI; + productReference = 3AEC20F32A459AB400A63465 /* SFI.app */; + productType = "com.apple.product-type.application"; + }; + 3AEC21082A459B1900A63465 /* SFM */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AEC21152A459B1A00A63465 /* Build configuration list for PBXNativeTarget "SFM" */; + buildPhases = ( + 3AEC21052A459B1900A63465 /* Sources */, + 3AEC21062A459B1900A63465 /* Frameworks */, + 3AEC21072A459B1900A63465 /* Resources */, + 3ADB2DE02A4ABEF700FB9254 /* Embed System Extensions */, + 3A096F762A4ED1A600D4A2ED /* Embed Foundation Extensions */, + 3A8655132A4FA25C00B7181F /* Embed ExtensionKit Extensions */, + 3A5F26CA2A503D4B00C27EDF /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3A096F7E2A4ED1AD00D4A2ED /* PBXTargetDependency */, + 3A096F8E2A4ED3DE00D4A2ED /* PBXTargetDependency */, + 3A4EAD152A4FEAE6005435B3 /* PBXTargetDependency */, + 3AC1944B2A50014000BD8CB9 /* PBXTargetDependency */, + ); + name = SFM; + packageProductDependencies = ( + 3A57DF3B2A4D705000690BC5 /* MacControlCenterUI */, + ); + productName = SFM; + productReference = 3AEC21092A459B1900A63465 /* sing-box β.app */; + productType = "com.apple.product-type.application"; + }; + 3AEC211C2A459B4700A63465 /* Library */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AEC21252A459B4700A63465 /* Build configuration list for PBXNativeTarget "Library" */; + buildPhases = ( + 3AEC21182A459B4700A63465 /* Headers */, + 3AEC21192A459B4700A63465 /* Sources */, + 3AEC211A2A459B4700A63465 /* Frameworks */, + 3AEC211B2A459B4700A63465 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Library; + packageProductDependencies = ( + 3A7E90372A46778E00D53052 /* BinaryCodable */, + 3A017F912A4AB2E4009149FA /* GRDB */, + ); + productName = Library; + productReference = 3AEC211D2A459B4700A63465 /* Library.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3AEC20BD2A45991900A63465 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 3A096F852A4ED3DE00D4A2ED = { + CreatedOnToolsVersion = 15.0; + }; + 3A4EAD0F2A4FEAE6005435B3 = { + CreatedOnToolsVersion = 15.0; + LastSwiftMigration = 1500; + }; + 3A77016C2A4E6B34008F031F = { + CreatedOnToolsVersion = 15.0; + }; + 3AEC20F22A459AB400A63465 = { + CreatedOnToolsVersion = 15.0; + }; + 3AEC21082A459B1900A63465 = { + CreatedOnToolsVersion = 15.0; + }; + 3AEC211C2A459B4700A63465 = { + CreatedOnToolsVersion = 15.0; + LastSwiftMigration = 1500; + }; + }; + }; + buildConfigurationList = 3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "zh-Hans", + ); + mainGroup = 3AEC20BC2A45991900A63465; + packageReferences = ( + 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */, + 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */, + 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */, + ); + productRefGroup = 3AEC20C72A45991900A63465 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3AEC20F22A459AB400A63465 /* SFI */, + 3AEC21082A459B1900A63465 /* SFM */, + 3AEC211C2A459B4700A63465 /* Library */, + 3A77016C2A4E6B34008F031F /* IntentsExtension */, + 3A096F852A4ED3DE00D4A2ED /* Extension */, + 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3A4EAD0E2A4FEAE6005435B3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC20F12A459AB400A63465 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AEC20FA2A459AB500A63465 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC21072A459B1900A63465 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A251C122A52D09700651082 /* Assets.xcassets in Resources */, + 3AEC21102A459B1A00A63465 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC211B2A459B4700A63465 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3A096F822A4ED3DE00D4A2ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A8512142A5E8EF70076D233 /* Extension+RunBlocking.swift in Sources */, + 3A096F8A2A4ED3DE00D4A2ED /* PacketTunnelProvider.swift in Sources */, + 3A8512132A5E8EF70076D233 /* ExtensionPlatformInterface.swift in Sources */, + 3A8512122A5E8EF70076D233 /* Extension+Iterator.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A4EAD0C2A4FEAE6005435B3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A4EAD2E2A4FEB77005435B3 /* EditProfileView.swift in Sources */, + 3A4EAD2A2A4FEB6D005435B3 /* Binding+Unwrap.swift in Sources */, + 3A4EAD2D2A4FEB77005435B3 /* ProfileView.swift in Sources */, + 3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */, + 3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */, + 3A4EAD292A4FEB6D005435B3 /* Formtem.swift in Sources */, + 3A4EAD302A4FEB77005435B3 /* NewProfileView.swift in Sources */, + 3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */, + 3A4EAD232A4FEB5A005435B3 /* EnvironmentValues.swift in Sources */, + 3A4EAD282A4FEB65005435B3 /* ActiveDashboardView.swift in Sources */, + 3A1CF2F02A50E5EE000A8289 /* GroupListView.swift in Sources */, + 3A4EAD322A4FEB7B005435B3 /* ServiceLogView.swift in Sources */, + 3A4EAD312A4FEB7B005435B3 /* SettingView.swift in Sources */, + 3A1CF2F82A50F0A5000A8289 /* GroupItemView.swift in Sources */, + 3A4EAD2F2A4FEB77005435B3 /* EditProfileContentView.swift in Sources */, + 3A4EAD272A4FEB65005435B3 /* DashboardView.swift in Sources */, + 3A4EAD342A4FEB7F005435B3 /* LogView.swift in Sources */, + 3A4EAD362A4FEB9C005435B3 /* ProfileUpdateTask.swift in Sources */, + 3A1CF2F62A50EE9C000A8289 /* GroupView.swift in Sources */, + 3A4EAD212A4FEB3C005435B3 /* ApplicationLibrary.swift in Sources */, + 3A1CF2F22A50E613000A8289 /* OutboundGroup.swift in Sources */, + 3A4EAD2C2A4FEB77005435B3 /* EditProfileWindowView.swift in Sources */, + 3A1CF2FA2A50F0BD000A8289 /* OutboundGroupItem.swift in Sources */, + 3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */, + 3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */, + 3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */, + 3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3A7701692A4E6B34008F031F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A7701702A4E6B34008F031F /* IntentsExtension.swift in Sources */, + 3A7701722A4E6B34008F031F /* Intents.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC20EF2A459AB400A63465 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AC1944F2A50247300BD8CB9 /* ApplicationDelegate.swift in Sources */, + 3AEC20F62A459AB400A63465 /* Application.swift in Sources */, + 3A9108E42A511AE70088B196 /* ContentView.swift in Sources */, + 3A44BB822A4DC28700E4C9F8 /* MainView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC21052A459B1900A63465 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AEC210C2A459B1900A63465 /* Application.swift in Sources */, + 3A57DF402A4D70B600690BC5 /* MenuView.swift in Sources */, + 3A1CF2F42A50E937000A8289 /* SidebarView.swift in Sources */, + 3ADF8DFB2A4AFDF500900CC8 /* MainView.swift in Sources */, + 3AC194522A50303300BD8CB9 /* ApplicationDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3AEC21192A459B4700A63465 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AEC214A2A45AA5600A63465 /* Profile+RW.swift in Sources */, + 3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */, + 3A7E90352A46756300D53052 /* SharedPreferences.swift in Sources */, + 3AEC21482A45A9DE00A63465 /* Bundle+Version.swift in Sources */, + 3A57DF372A4D5D2600690BC5 /* Profile+Date.swift in Sources */, + 3AEC212F2A459D5600A63465 /* Profile.swift in Sources */, + 3A648D2D2A4EEAA600D95A12 /* Library.swift in Sources */, + 3AEC21452A45A93800A63465 /* HTTPClient.swift in Sources */, + 3AEC213C2A459FDF00A63465 /* Databse.swift in Sources */, + 3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */, + 3AEC21422A45A8FF00A63465 /* Profile+Update.swift in Sources */, + 3AEC214C2A45AA8E00A63465 /* FilePath.swift in Sources */, + 3AC194502A502DFE00BD8CB9 /* ServiceNotification.swift in Sources */, + 3AEC21402A45A28F00A63465 /* ProfileManager.swift in Sources */, + 3A9144D92A46AE370036E9AD /* ShadredPreferences+Database.swift in Sources */, + 3AF342A02A4A9916002B34AC /* ExtensionProfile.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3A096F7E2A4ED1AD00D4A2ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AEC211C2A459B4700A63465 /* Library */; + targetProxy = 3A096F7D2A4ED1AD00D4A2ED /* PBXContainerItemProxy */; + }; + 3A096F8E2A4ED3DE00D4A2ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A096F852A4ED3DE00D4A2ED /* Extension */; + targetProxy = 3A096F8D2A4ED3DE00D4A2ED /* PBXContainerItemProxy */; + }; + 3A44BB7F2A4DC1D800E4C9F8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A096F852A4ED3DE00D4A2ED /* Extension */; + targetProxy = 3A44BB7E2A4DC1D800E4C9F8 /* PBXContainerItemProxy */; + }; + 3A4EAD152A4FEAE6005435B3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */; + targetProxy = 3A4EAD142A4FEAE6005435B3 /* PBXContainerItemProxy */; + }; + 3A4EAD1E2A4FEB02005435B3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AEC211C2A459B4700A63465 /* Library */; + targetProxy = 3A4EAD1D2A4FEB02005435B3 /* PBXContainerItemProxy */; + }; + 3A4EAD3A2A4FEC20005435B3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */; + targetProxy = 3A4EAD392A4FEC20005435B3 /* PBXContainerItemProxy */; + }; + 3A76504B2A4F07F6003945C5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AEC211C2A459B4700A63465 /* Library */; + targetProxy = 3A76504A2A4F07F6003945C5 /* PBXContainerItemProxy */; + }; + 3A77017E2A4E6B5E008F031F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AEC211C2A459B4700A63465 /* Library */; + targetProxy = 3A77017D2A4E6B5E008F031F /* PBXContainerItemProxy */; + }; + 3A8655162A4FA26600B7181F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A77016C2A4E6B34008F031F /* IntentsExtension */; + targetProxy = 3A8655152A4FA26600B7181F /* PBXContainerItemProxy */; + }; + 3AC1944B2A50014000BD8CB9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A77016C2A4E6B34008F031F /* IntentsExtension */; + targetProxy = 3AC1944A2A50014000BD8CB9 /* PBXContainerItemProxy */; + }; + 3AEAEE9B2A4F16430059612D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A096F852A4ED3DE00D4A2ED /* Extension */; + targetProxy = 3AEAEE9A2A4F16430059612D /* PBXContainerItemProxy */; + }; + 3AEC21382A459E0A00A63465 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AEC211C2A459B4700A63465 /* Library */; + targetProxy = 3AEC21372A459E0A00A63465 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3A096F912A4ED3DE00D4A2ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Extension/Extension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Extension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Extension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3A096F922A4ED3DE00D4A2ED /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Extension/Extension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Extension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Extension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3A4EAD192A4FEAE6005435B3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.application; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3A4EAD1A2A4FEAE6005435B3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.application; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3A7701782A4E6B34008F031F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = IntentsExtension/IntentsExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IntentsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = IntentsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LINK_WITH_STANDARD_LIBRARIES = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + REEXPORTED_LIBRARY_PATHS = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3A7701792A4E6B34008F031F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = IntentsExtension/IntentsExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IntentsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = IntentsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LINK_WITH_STANDARD_LIBRARIES = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + REEXPORTED_LIBRARY_PATHS = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3AEC20CB2A45991900A63465 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3AEC20CC2A45991900A63465 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3AEC20FF2A459AB500A63465 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = SFI/SFI.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SFI/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "sing-box β"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = "--deep"; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; + PRODUCT_MODULE_NAME = SFI; + PRODUCT_NAME = SFI; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3AEC21002A459AB500A63465 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = SFI/SFI.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SFI/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "sing-box β"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = "--deep"; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3AEC21162A459B1A00A63465 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SFM/SFM.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SFM/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "sing-box"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; + PRODUCT_NAME = "sing-box β"; + PROVISIONING_PROFILE_SPECIFIER = ""; + REEXPORTED_LIBRARY_PATHS = ""; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 3AEC21172A459B1A00A63465 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SFM/SFM.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SFM/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "sing-box"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + OTHER_CODE_SIGN_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; + PRODUCT_NAME = "sing-box β"; + PROVISIONING_PROFILE_SPECIFIER = ""; + REEXPORTED_LIBRARY_PATHS = ""; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 3AEC21262A459B4700A63465 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2; + "DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_ON_DEMAND_RESOURCES = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_CODE_SIGN_FLAGS = "--deep"; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3AEC21272A459B4700A63465 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2; + "DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_ON_DEMAND_RESOURCES = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_CODE_SIGN_FLAGS = "--deep"; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3A096F902A4ED3DE00D4A2ED /* Build configuration list for PBXNativeTarget "Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A096F912A4ED3DE00D4A2ED /* Debug */, + 3A096F922A4ED3DE00D4A2ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A4EAD182A4FEAE6005435B3 /* Build configuration list for PBXNativeTarget "ApplicationLibrary" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A4EAD192A4FEAE6005435B3 /* Debug */, + 3A4EAD1A2A4FEAE6005435B3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A7701782A4E6B34008F031F /* Debug */, + 3A7701792A4E6B34008F031F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AEC20CB2A45991900A63465 /* Debug */, + 3AEC20CC2A45991900A63465 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AEC20FF2A459AB500A63465 /* Debug */, + 3AEC21002A459AB500A63465 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AEC21152A459B1A00A63465 /* Build configuration list for PBXNativeTarget "SFM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AEC21162A459B1A00A63465 /* Debug */, + 3AEC21172A459B1A00A63465 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3AEC21252A459B4700A63465 /* Build configuration list for PBXNativeTarget "Library" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AEC21262A459B4700A63465 /* Debug */, + 3AEC21272A459B4700A63465 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/groue/GRDB.swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.15.1; + }; + }; + 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/MacControlCenterUI"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.1; + }; + }; + 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/christophhagen/BinaryCodable"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3A017F912A4AB2E4009149FA /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + package = 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */; + productName = GRDB; + }; + 3A57DF3B2A4D705000690BC5 /* MacControlCenterUI */ = { + isa = XCSwiftPackageProductDependency; + package = 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */; + productName = MacControlCenterUI; + }; + 3A7E90372A46778E00D53052 /* BinaryCodable */ = { + isa = XCSwiftPackageProductDependency; + package = 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */; + productName = BinaryCodable; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 3AEC20BD2A45991900A63465 /* Project object */; +} diff --git a/sing-box.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/sing-box.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/sing-box.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..36c6a71 --- /dev/null +++ b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "binarycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/christophhagen/BinaryCodable", + "state" : { + "revision" : "295ca6399b2b01d1aa4fa84d666416f3bf99ffde", + "version" : "2.0.0" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift", + "state" : { + "revision" : "284a4ee1acf9bf6c8b3d045494d0a7e3046ebf92", + "version" : "6.15.1" + } + }, + { + "identity" : "maccontrolcenterui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/orchetect/MacControlCenterUI", + "state" : { + "revision" : "e7f7e0834a146b59a9d86b5751b711eb3a57be69", + "version" : "2.0.1" + } + }, + { + "identity" : "menubarextraaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/orchetect/MenuBarExtraAccess", + "state" : { + "revision" : "8757eb7c2cd708320df92e6ad6572efe90e58f16", + "version" : "1.0.4" + } + } + ], + "version" : 2 +} diff --git a/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist b/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..a6e861a --- /dev/null +++ b/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,255 @@ + + + + + SchemeUserState + + ApplicationLibrary.xcscheme_^#shared#^_ + + orderHint + 2 + + Associations (Playground) 1.xcscheme + + isShown + + orderHint + 8 + + Associations (Playground) 2.xcscheme + + isShown + + orderHint + 9 + + Associations (Playground) 3.xcscheme + + isShown + + orderHint + 27 + + Associations (Playground) 4.xcscheme + + isShown + + orderHint + 28 + + Associations (Playground) 5.xcscheme + + isShown + + orderHint + 29 + + Associations (Playground).xcscheme + + isShown + + orderHint + 7 + + ExtensionMac.xcscheme_^#shared#^_ + + orderHint + 22 + + Launcher.xcscheme_^#shared#^_ + + orderHint + 18 + + MacExtension.xcscheme_^#shared#^_ + + orderHint + 5 + + MyPlayground (Playground) 1.xcscheme + + isShown + + orderHint + 4 + + MyPlayground (Playground) 2.xcscheme + + isShown + + orderHint + 6 + + MyPlayground (Playground) 3.xcscheme + + isShown + + orderHint + 23 + + MyPlayground (Playground) 4.xcscheme + + isShown + + orderHint + 24 + + MyPlayground (Playground) 5.xcscheme + + isShown + + orderHint + 26 + + MyPlayground (Playground).xcscheme + + isShown + + orderHint + 3 + + SFA.xcscheme_^#shared#^_ + + orderHint + 21 + + SFI.xcscheme + + orderHint + 0 + + SFM.xcscheme_^#shared#^_ + + orderHint + 1 + + SystemExtension.xcscheme_^#shared#^_ + + orderHint + 16 + + Test.xcscheme_^#shared#^_ + + orderHint + 10 + + Tour (Playground) 1.xcscheme + + isShown + + orderHint + 15 + + Tour (Playground) 2.xcscheme + + isShown + + orderHint + 17 + + Tour (Playground) 3.xcscheme + + isShown + + orderHint + 33 + + Tour (Playground) 4.xcscheme + + isShown + + orderHint + 34 + + Tour (Playground) 5.xcscheme + + isShown + + orderHint + 35 + + Tour (Playground).xcscheme + + isShown + + orderHint + 14 + + TransactionObserver (Playground) 1.xcscheme + + isShown + + orderHint + 12 + + TransactionObserver (Playground) 2.xcscheme + + isShown + + orderHint + 13 + + TransactionObserver (Playground) 3.xcscheme + + isShown + + orderHint + 30 + + TransactionObserver (Playground) 4.xcscheme + + isShown + + orderHint + 31 + + TransactionObserver (Playground) 5.xcscheme + + isShown + + orderHint + 32 + + TransactionObserver (Playground).xcscheme + + isShown + + orderHint + 11 + + mactest.xcscheme_^#shared#^_ + + orderHint + 25 + + sing-box.xcscheme_^#shared#^_ + + orderHint + 20 + + test.xcscheme_^#shared#^_ + + orderHint + 19 + + + SuppressBuildableAutocreation + + 3A096F852A4ED3DE00D4A2ED + + primary + + + 3A77016C2A4E6B34008F031F + + primary + + + 3AEC211C2A459B4700A63465 + + primary + + + + +