Add support for tvOS

This commit is contained in:
世界
2023-07-29 09:44:31 +08:00
parent 5a8f3a770f
commit bb0385f53c
63 changed files with 1542 additions and 389 deletions
@@ -2,7 +2,7 @@ import BackgroundTasks
import Foundation import Foundation
import Library import Library
#if os(iOS) #if os(iOS) || os(tvOS)
public class UIProfileUpdateTask: BGAppRefreshTask { public class UIProfileUpdateTask: BGAppRefreshTask {
public static let taskSchedulerPermittedIdentifier = "\(FilePath.packageName).update_profiles" public static let taskSchedulerPermittedIdentifier = "\(FilePath.packageName).update_profiles"
@@ -17,19 +17,21 @@ public func FormTextItem(_ name: String, _ value: String) -> some View {
Text(value) Text(value)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.font(Font.system(.caption, design: .monospaced)) .font(Font.system(.caption, design: .monospaced))
#if os(iOS) || os(macOS)
.textSelection(.enabled) .textSelection(.enabled)
#endif
} }
} }
public func FormItem(_ title: String, @ViewBuilder content: () -> some View) -> some View { public func FormItem(_ title: String, @ViewBuilder content: () -> some View) -> some View {
#if os(iOS) #if os(iOS) || os(tvOS)
HStack { HStack {
Text(title) Text(title)
Spacer() Spacer()
Spacer() Spacer()
content() content()
} }
#else #elseif os(macOS)
content() content()
#endif #endif
} }
@@ -33,11 +33,13 @@ public struct ActiveDashboardView: View {
Text("Empty profiles") Text("Empty profiles")
} else { } else {
VStack { VStack {
#if os(iOS) #if os(iOS) || os(tvOS)
if ApplicationLibrary.inPreview || profile.status.isConnected { if ApplicationLibrary.inPreview || profile.status.isConnected {
ExtensionStatusView() ExtensionStatusView()
.listStyle(.automatic) .listStyle(.automatic)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#endif
} }
FormView { FormView {
StartStopButton() StartStopButton()
@@ -50,7 +52,7 @@ public struct ActiveDashboardView: View {
.pickerStyle(.inline) .pickerStyle(.inline)
} }
} }
#else #elseif os(macOS)
if ApplicationLibrary.inPreview || profile.status.isConnected { if ApplicationLibrary.inPreview || profile.status.isConnected {
ExtensionStatusView() ExtensionStatusView()
} }
@@ -77,36 +79,48 @@ public struct ActiveDashboardView: View {
} }
} }
.alertBinding($alert) .alertBinding($alert)
#if os(iOS) .onChange(of: profile.status, perform: { newValue in
.onChange(of: scenePhase, perform: { newValue in if newValue == .disconnecting || newValue == .connected {
if newValue == .active { Task.detached {
Task.detached { if let serviceError = try? String(contentsOf: ExtensionProvider.errorFile) {
await doReload() DispatchQueue.main.async {
} alert = Alert(errorMessage: serviceError)
}
})
.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()
} }
}) try? FileManager.default.removeItem(at: ExtensionProvider.errorFile)
}
} }
} }
.onDisappear { })
if let observer { #if os(iOS) || os(tvOS)
NotificationCenter.default.removeObserver(observer) .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 #endif
} }
@@ -147,7 +161,7 @@ public struct ActiveDashboardView: View {
NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil) NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil)
if profile.status.isConnected { if profile.status.isConnected {
do { do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() try LibboxNewStandaloneCommandClient()!.serviceReload()
} catch { } catch {
alert = Alert(error) alert = Alert(error)
} }
@@ -116,7 +116,7 @@ public struct ExtensionStatusView: View {
private func closeConnections() { private func closeConnections() {
do { do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.closeConnections() try LibboxNewStandaloneCommandClient()!.closeConnections()
} catch { } catch {
alert = Alert(error) alert = Alert(error)
} }
@@ -162,9 +162,13 @@ public struct ExtensionStatusView: View {
.font(.system(size: 16)) .font(.system(size: 16))
} }
.frame(minWidth: 125) .frame(minWidth: 125)
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13)) #if os(tvOS)
.background(backgroundColor) .padding(EdgeInsets(top: 20, leading: 26, bottom: 20, trailing: 26))
.cornerRadius(10) #else
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
#endif
.background(backgroundColor)
.cornerRadius(10)
} }
private var backgroundColor: Color { private var backgroundColor: Color {
@@ -172,6 +176,8 @@ public struct ExtensionStatusView: View {
return Color(uiColor: .secondarySystemGroupedBackground) return Color(uiColor: .secondarySystemGroupedBackground)
#elseif os(macOS) #elseif os(macOS)
return Color(nsColor: .textBackgroundColor) return Color(nsColor: .textBackgroundColor)
#elseif os(tvOS)
return Color(uiColor: .black)
#endif #endif
} }
} }
@@ -10,7 +10,7 @@ public struct StartStopButton: View {
public var body: some View { public var body: some View {
viewBuilder { viewBuilder {
if ApplicationLibrary.inPreview { if ApplicationLibrary.inPreview {
#if os(iOS) #if os(iOS) || os(tvOS)
Toggle(isOn: .constant(true)) { Toggle(isOn: .constant(true)) {
Text("Enabled") Text("Enabled")
} }
@@ -23,7 +23,7 @@ public struct StartStopButton: View {
} else if let profile = extensionProfile.wrappedValue { } else if let profile = extensionProfile.wrappedValue {
Button0(profile) Button0(profile)
} else { } else {
#if os(iOS) #if os(iOS) || os(tvOS)
Toggle(isOn: .constant(false)) { Toggle(isOn: .constant(false)) {
Text("Enabled") Text("Enabled")
} }
@@ -49,7 +49,7 @@ public struct StartStopButton: View {
var body: some View { var body: some View {
viewBuilder { viewBuilder {
#if os(iOS) #if os(iOS) || os(tvOS)
Toggle(isOn: Binding(get: { Toggle(isOn: Binding(get: {
profile.status.isConnected profile.status.isConnected
}, set: { newValue, _ in }, set: { newValue, _ in
@@ -14,8 +14,7 @@ public struct GroupItemView: View {
self.item = item self.item = item
} }
@State private var errorPresented = false @State private var alert: Alert?
@State private var errorMessage = ""
public var body: some View { public var body: some View {
HStack { HStack {
@@ -60,24 +59,17 @@ public struct GroupItemView: View {
} }
} }
} }
.alert(isPresented: $errorPresented) { .alertBinding($alert)
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
} }
private func selectOutbound() { private func selectOutbound() {
do { do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.selectOutbound(group.tag, outboundTag: item.tag) try LibboxNewStandaloneCommandClient()!.selectOutbound(group.tag, outboundTag: item.tag)
var newGroup = group var newGroup = group
newGroup.selected = item.tag newGroup.selected = item.tag
_group.wrappedValue = newGroup _group.wrappedValue = newGroup
} catch { } catch {
errorMessage = error.localizedDescription alert = Alert(error)
errorPresented = true
return return
} }
} }
@@ -87,6 +79,8 @@ public struct GroupItemView: View {
return Color(uiColor: .secondarySystemGroupedBackground) return Color(uiColor: .secondarySystemGroupedBackground)
#elseif os(macOS) #elseif os(macOS)
return Color(nsColor: .textBackgroundColor) return Color(nsColor: .textBackgroundColor)
#elseif os(tvOS)
return Color.black
#endif #endif
} }
} }
@@ -113,7 +113,7 @@ public struct GroupView: View {
} }
private func doURLTest() { private func doURLTest() {
try? LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.urlTest(group.tag) try? LibboxNewStandaloneCommandClient()!.urlTest(group.tag)
} }
} }
@@ -40,6 +40,9 @@ public struct LogView: View {
ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in
Text(it.element) Text(it.element)
.font(logFont) .font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 5) Spacer(minLength: 5)
} }
@@ -52,6 +55,10 @@ public struct LogView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding() .padding()
} }
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
.onAppear { .onAppear {
reader.scrollTo(logClient.logList.count - 1) reader.scrollTo(logClient.logList.count - 1)
} }
@@ -75,6 +75,10 @@ public extension NavigationPage {
} }
switch self { switch self {
case .groups: case .groups:
#if os(tvOS)
// TODO: fix groups ui
return false
#endif
return profile?.status.isConnectedStrict == true return profile?.status.isConnectedStrict == true
default: default:
return true return true
@@ -1,133 +1,136 @@
import Foundation #if os(iOS) || os(macOS)
import Library import Foundation
import SwiftUI import Library
import SwiftUI
public struct EditProfileContentView: View { 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 alert: Alert?
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))
.autocorrectionDisabled()
#if os(iOS)
.textInputAutocapitalization(.none)
.background(Color(UIColor.secondarySystemGroupedBackground))
#elseif os(macOS)
.padding()
#endif
.onChange(of: profileContent) { _ in
isChanged = true
}
}
}
.alertBinding($alert)
.navigationTitle(navigationTitle)
#if os(macOS) #if os(macOS)
.toolbar { public static let windowID = "edit-profile-content"
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 #endif
}
private var navigationTitle: String { public struct Context: Codable, Hashable {
if readOnly { public let profileID: Int64
return "View Content" public let readOnly: Bool
} else { }
return "Edit Content"
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 alert: Alert?
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))
.autocorrectionDisabled()
#if os(iOS)
.textInputAutocapitalization(.none)
.background(Color(UIColor.secondarySystemGroupedBackground))
#elseif os(macOS)
.padding()
#endif
.onChange(of: profileContent) { _ in
isChanged = true
}
}
}
.alertBinding($alert)
.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 {
alert = Alert(error, dismiss.callAsFunction)
}
}
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 {
alert = Alert(error)
return
}
isChanged = false
} }
} }
private func loadContent() { #endif
do {
try loadContent0()
} catch {
alert = Alert(error, dismiss.callAsFunction)
}
}
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 {
alert = Alert(error)
return
}
isChanged = false
}
}
@@ -46,20 +46,24 @@ public struct EditProfileView: View {
FormTextItem("Last Updated", profile.lastUpdatedString) FormTextItem("Last Updated", profile.lastUpdatedString)
} }
} }
#if os(iOS) #if os(iOS) || os(tvOS)
Section("Action") { Section("Action") {
if profile.type != .remote { if profile.type != .remote {
NavigationLink { #if os(iOS)
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) NavigationLink {
} label: { EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
Text("Edit Content").foregroundColor(.accentColor) } label: {
} Text("Edit Content").foregroundColor(.accentColor)
}
#endif
} else { } else {
NavigationLink { #if os(iOS)
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) NavigationLink {
} label: { EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
Text("View Content").foregroundColor(.accentColor) } label: {
} Text("View Content").foregroundColor(.accentColor)
}
#endif
Button("Update") { Button("Update") {
isLoading = true isLoading = true
Task.detached { Task.detached {
@@ -67,17 +71,19 @@ public struct EditProfileView: View {
} }
} }
.disabled(isLoading) .disabled(isLoading)
} #if os(iOS)
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
ShareLink(item: profile.shareLink) { ShareLink(item: profile.shareLink) {
Text("Share") Text("Share")
} }
} else { } else {
Button("Share") { Button("Share") {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil) windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil)
}
}
} }
} #endif
} }
} }
#endif #endif
@@ -0,0 +1,164 @@
#if os(tvOS)
import DeviceDiscoveryUI
import Libbox
import Library
import SwiftUI
public struct ImportProfileView: View {
@Environment(\.dismiss) private var dismiss
@State private var isLoading = false
@State private var selected = false
@State private var alert: Alert?
@State private var connection: NWSocket?
@State private var profiles: [LibboxProfilePreview]?
private let callback: () -> Void
public init(callback: @escaping () -> Void) {
self.callback = callback
}
public var body: some View {
VStack {
if !selected {
DevicePicker(
.applicationService(name: "sing-box:profile"))
{ endpoint in
selected = true
Task.detached {
await handleEndpoint(endpoint)
}
} label: {
Text("Select Device")
} fallback: {
EmptyView()
} parameters: {
.applicationService
}
} else if let profiles {
Form {
Text("\(profiles.count) Profiles")
ForEach(profiles, id: \.profileID) { profile in
Button(profile.name) {
isLoading = true
Task.detached {
selectProfile(profileID: profile.profileID)
isLoading = false
}
}.disabled(isLoading)
}
}
} else {
Text("Connecting...")
}
}
.focusSection()
.alertBinding($alert)
.navigationTitle("Import Profile")
}
private func reset() {
selected = false
profiles = nil
}
private func handleEndpoint(_ endpoint: NWEndpoint) async {
let connection = NWConnection(to: endpoint, using: NWParameters.applicationService)
self.connection = NWSocket(connection)
connection.start(queue: .global())
do {
try loopMessages()
} catch {
alert = Alert(error)
reset()
}
}
private func loopMessages() throws {
guard let connection else {
return
}
while true {
let message = try connection.read()
var error: NSError?
switch Int64(message[0]) {
case LibboxMessageTypeError:
let message = LibboxDecodeErrorMessage(message, &error)
if let error {
throw error
}
if let message {
throw NSError(domain: "remote error: \(message.message)", code: 0)
}
case LibboxMessageTypeProfileList:
let decoder = LibboxProfileDecoder()
try decoder.decode(message)
let iterator = decoder.iterator()!
var profiles = [LibboxProfilePreview]()
while iterator.hasNext() {
let profile = iterator.next()!
if profile.type == LibboxProfileTypeiCloud {
// not supported on tvOS
continue
}
profiles.append(profile)
}
self.profiles = profiles
case LibboxMessageTypeProfileContent:
let content = LibboxDecodeProfileContent(message, &error)
if let error {
throw error
}
try importProfile(content!)
default:
throw NSError(domain: "unknown message type \(message[0])", code: 0)
}
}
}
private func selectProfile(profileID: Int64) {
guard let connection else {
return
}
let request = LibboxProfileContentRequest()
request.profileID = profileID
do {
try connection.write(request.encode())
} catch {
alert = Alert(error)
reset()
}
}
private func importProfile(_ content: LibboxProfileContent) throws {
var type: ProfileType = .local
switch content.type {
case LibboxProfileTypeLocal:
type = .local
case LibboxProfileTypeiCloud:
type = .icloud
case LibboxProfileTypeRemote:
type = .remote
default:
break
}
let nextProfileID = try ProfileManager.nextID()
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
try content.config.write(to: profileConfig, atomically: true, encoding: .utf8)
var lastUpdated: Date?
if content.lastUpdated > 0 {
lastUpdated = Date(timeIntervalSince1970: Double(content.lastUpdated))
}
try ProfileManager.create(Profile(name: content.name, type: type, path: profileConfig.relativePath, remoteURL: content.remotePath, autoUpdate: content.autoUpdate, lastUpdated: lastUpdated))
DispatchQueue.main.async {
dismiss()
callback()
}
}
}
#endif
@@ -41,8 +41,10 @@ public struct NewProfileView: View {
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
} }
Picker(selection: $profileType) { Picker(selection: $profileType) {
Text("Local").tag(ProfileType.local) #if !os(tvOS)
Text("iCloud").tag(ProfileType.icloud) Text("Local").tag(ProfileType.local)
Text("iCloud").tag(ProfileType.icloud)
#endif
Text("Remote").tag(ProfileType.remote) Text("Remote").tag(ProfileType.remote)
} label: { } label: {
Text("Type") Text("Type")
@@ -54,6 +56,9 @@ public struct NewProfileView: View {
} label: { } label: {
Text("File") Text("File")
} }
#if os(tvOS)
.disabled(true)
#endif
viewBuilder { viewBuilder {
if fileImport { if fileImport {
HStack { HStack {
@@ -98,21 +103,23 @@ public struct NewProfileView: View {
} }
.navigationTitle("New Profile") .navigationTitle("New Profile")
.alertBinding($alert) .alertBinding($alert)
.fileImporter( #if os(iOS) || os(macOS)
isPresented: $pickerPresented, .fileImporter(
allowedContentTypes: [.json], isPresented: $pickerPresented,
allowsMultipleSelection: false allowedContentTypes: [.json],
) { result in allowsMultipleSelection: false
do { ) { result in
let urls = try result.get() do {
if !urls.isEmpty { let urls = try result.get()
fileURL = urls[0] if !urls.isEmpty {
fileURL = urls[0]
}
} catch {
alert = Alert(error)
return
} }
} catch {
alert = Alert(error)
return
} }
} #endif
} }
private func createProfile() async { private func createProfile() async {
@@ -1,6 +1,7 @@
import Foundation import Foundation
import Libbox import Libbox
import Library import Library
import Network
import SwiftUI import SwiftUI
public struct ProfileView: View { public struct ProfileView: View {
@@ -16,12 +17,16 @@ public struct ProfileView: View {
@State private var alert: Alert? @State private var alert: Alert?
@State private var profileList: [Profile] = [] @State private var profileList: [Profile] = []
#if os(iOS) #if os(iOS) || os(tvOS)
@State private var editMode = EditMode.inactive @State private var editMode = EditMode.inactive
#elseif os(macOS) #elseif os(macOS)
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
#endif #endif
#if os(tvOS)
@Environment(\.devicePickerSupports) private var devicePickerSupports
#endif
@State private var observer: Any? @State private var observer: Any?
public init() {} public init() {}
@@ -35,7 +40,7 @@ public struct ProfileView: View {
} }
} }
} else { } else {
#if os(iOS) #if os(iOS) || os(tvOS)
ZStack { ZStack {
if let importRemoteProfileRequest { if let importRemoteProfileRequest {
NavigationLink( NavigationLink(
@@ -51,16 +56,31 @@ public struct ProfileView: View {
) )
} }
FormView { FormView {
NavigationLink { #if os(iOS) || os(tvOS)
NewProfileView { NavigationLink {
Task.detached { NewProfileView {
doReload() Task.detached {
doReload()
}
}
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
.disabled(editMode.isEditing)
#endif
#if os(tvOS)
if devicePickerSupports(.applicationService(name: "sing-box"), parameters: { .applicationService }) {
NavigationLink {
ImportProfileView {
Task.detached {
doReload()
}
}
} label: {
Text("Import Profile").foregroundColor(.accentColor)
} }
} }
} label: { #endif
Text("New Profile").foregroundColor(.accentColor)
}
.disabled(editMode.isEditing)
if profileList.isEmpty { if profileList.isEmpty {
Text("Empty Profiles") Text("Empty Profiles")
} else { } else {
@@ -77,6 +97,23 @@ public struct ProfileView: View {
} }
} }
} }
.contextMenu {
if profile.type == .remote {
Button {
isUpdating = true
Task.detached {
updateProfile(profile)
}
} label: {
Label("Update", systemImage: "arrow.clockwise")
}
}
Button(role: .destructive) {
deleteProfile(profile)
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
} }
.onMove(perform: moveProfile) .onMove(perform: moveProfile)
.onDelete(perform: deleteProfile) .onDelete(perform: deleteProfile)
@@ -194,7 +231,7 @@ public struct ProfileView: View {
title: Text("Import Remote Profile"), title: Text("Import Remote Profile"),
message: Text("Are you sure to import remote configuration \(newValue.name)? You will connect to \(newValue.host) to download the configuration."), message: Text("Are you sure to import remote configuration \(newValue.name)? You will connect to \(newValue.host) to download the configuration."),
primaryButton: .default(Text("Import")) { primaryButton: .default(Text("Import")) {
#if os(iOS) #if os(iOS) || os(tvOS)
importRemoteProfilePresented = true importRemoteProfilePresented = true
#elseif os(macOS) #elseif os(macOS)
openWindow(id: NewProfileView.windowID, value: importRemoteProfileRequest!) openWindow(id: NewProfileView.windowID, value: importRemoteProfileRequest!)
@@ -34,12 +34,15 @@ public struct ServiceLogView: View {
} }
} }
} }
#if !os(tvOS)
.toolbar { .toolbar {
Button("Export") { Button("Export") {
fileExporterPresented = true fileExporterPresented = true
} }
.disabled(content.isEmpty) .disabled(content.isEmpty)
} }
#endif
#if !os(tvOS)
.fileExporter( .fileExporter(
isPresented: $fileExporterPresented, isPresented: $fileExporterPresented,
document: LogDocument(content), document: LogDocument(content),
@@ -47,10 +50,14 @@ public struct ServiceLogView: View {
defaultFilename: "service-log.txt", defaultFilename: "service-log.txt",
onCompletion: { _ in } onCompletion: { _ in }
) )
#endif
.navigationTitle("Service Log") .navigationTitle("Service Log")
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#endif #endif
#if os(tvOS)
.focusable()
#endif
} }
private func loadContent() { private func loadContent() {
@@ -65,25 +72,27 @@ public struct ServiceLogView: View {
isLoading = false isLoading = false
} }
private struct LogDocument: FileDocument { #if !os(tvOS)
static var readableContentTypes = [UTType.text] private struct LogDocument: FileDocument {
static var readableContentTypes = [UTType.text]
let content: String let content: String
init(_ content: String) { init(_ content: String) {
self.content = content self.content = content
} }
init(configuration: ReadConfiguration) throws { init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents { if let data = configuration.file.regularFileContents {
content = String(decoding: data, as: UTF8.self) content = String(decoding: data, as: UTF8.self)
} else { } else {
content = "" content = ""
}
}
func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: Data(content.utf8))
} }
} }
#endif
func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: Data(content.utf8))
}
}
} }
@@ -95,7 +95,7 @@ public struct SettingView: View {
Section("Core") { Section("Core") {
FormTextItem("Version", version) FormTextItem("Version", version)
FormTextItem("Data Size", dataSize) FormTextItem("Data Size", dataSize)
#if os(iOS) #if os(iOS) || os(tvOS)
NavigationLink(destination: ServiceLogView()) { NavigationLink(destination: ServiceLogView()) {
Text("View Service Log") Text("View Service Log")
} }
+4
View File
@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>
+2 -2
View File
@@ -35,7 +35,7 @@ struct StartServiceIntent: AppIntent {
if !profileChanged { if !profileChanged {
return .result() return .result()
} }
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() try LibboxNewStandaloneCommandClient()!.serviceReload()
} else if extensionProfile.status.isConnected { } else if extensionProfile.status.isConnected {
extensionProfile.stop() extensionProfile.stop()
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
@@ -62,7 +62,7 @@ struct RestartServiceIntent: AppIntent {
return .result() return .result()
} }
if extensionProfile.status == .connected { if extensionProfile.status == .connected {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() try LibboxNewStandaloneCommandClient()!.serviceReload()
} else if extensionProfile.status.isConnected { } else if extensionProfile.status.isConnected {
extensionProfile.stop() extensionProfile.stop()
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
+3 -3
View File
@@ -1,5 +1,6 @@
import Foundation import Foundation
import GRDB import GRDB
import Network
public class Profile: Record, Identifiable, ObservableObject { public class Profile: Record, Identifiable, ObservableObject {
public var id: Int64? public var id: Int64?
@@ -15,16 +16,15 @@ public class Profile: Record, Identifiable, ObservableObject {
@Published public var autoUpdate: Bool @Published public var autoUpdate: Bool
public var lastUpdated: Date? public var lastUpdated: Date?
public init(id: Int64? = nil, name: String, order: UInt32 = 0, type: ProfileType, path: String, remoteURL: String? = nil, lastUpdated: Date? = nil) { public init(id: Int64? = nil, name: String, order: UInt32 = 0, type: ProfileType, path: String, remoteURL: String? = nil, autoUpdate: Bool = false, lastUpdated: Date? = nil) {
self.id = id self.id = id
self.name = name self.name = name
self.order = order self.order = order
self.type = type self.type = type
self.path = path self.path = path
self.remoteURL = remoteURL self.remoteURL = remoteURL
self.autoUpdate = autoUpdate
self.lastUpdated = lastUpdated self.lastUpdated = lastUpdated
autoUpdate = false
super.init() super.init()
} }
+63
View File
@@ -0,0 +1,63 @@
import Foundation
import Libbox
import Network
public class NWSocket {
private let connection: NWConnection
public init(_ connection: NWConnection) {
self.connection = connection
}
public func read() throws -> Data {
let semaphore = DispatchSemaphore(value: 0)
var result: Result<Data, Error>!
connection.receive(minimumIncompleteLength: 2, maximumLength: 2) { content, _, _, error in
if let error {
result = .failure(error)
} else {
result = .success(content!)
}
semaphore.signal()
}
semaphore.wait()
let lengthChunk = try result.get()
let length = Int(LibboxDecodeLengthChunk(lengthChunk))
connection.receive(minimumIncompleteLength: length, maximumLength: length) { content, _, _, error in
if let error {
result = .failure(error)
} else {
result = .success(content!)
}
semaphore.signal()
}
semaphore.wait()
return try result.get()
}
public func write(_ data: Data?) throws {
guard let data else {
return
}
let semaphore = DispatchSemaphore(value: 0)
var result: Error?
connection.send(content: LibboxEncodeChunkedMessage(data), isComplete: false, completion: .contentProcessed { error in
result = error
semaphore.wait()
})
if let result {
throw result
}
}
public func send(_ data: Data?) {
guard let data else {
return
}
connection.send(content: LibboxEncodeChunkedMessage(data), completion: .idempotent)
}
public func cancel() {
connection.cancel()
}
}
+128
View File
@@ -0,0 +1,128 @@
import Foundation
import Libbox
import Network
public class ProfileServer {
private var listener: NWListener
@available(iOS 16.0, macOS 13.0, *)
public init() throws {
listener = try NWListener(using: .applicationService)
listener.service = NWListener.Service(applicationService: "sing-box:profile")
listener.newConnectionHandler = { connection in
connection.stateUpdateHandler = { state in
if state == .ready {
Task.detached {
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100)
ProfileConnection(connection).process()
}
}
}
connection.start(queue: .global())
}
}
public func start() {
listener.start(queue: .global())
}
public func cancel() {
listener.cancel()
}
class ProfileConnection {
private let connection: NWSocket
init(_ connection: NWConnection) {
self.connection = NWSocket(connection)
}
func process() {
do {
try writeProfilePreviewList()
} catch {
NSLog("profile server: write profile list: \(error.localizedDescription)")
writeError(error.localizedDescription)
return
}
do {
while true {
let message = try connection.read()
try processMessage(message)
}
} catch {
NSLog("profile server: process connection: \(error.localizedDescription)")
writeError(error.localizedDescription)
}
}
private func processMessage(_ data: Data) throws {
if data.count == 0 {
return
}
let messageType = Int64(data[0])
switch messageType {
case LibboxMessageTypeProfileContentRequest:
var error: NSError?
let request = LibboxDecodeProfileContentRequest(data, &error)
if let error {
throw error
}
let profile = try ProfileManager.get(request!.profileID)
guard let profile else {
throw NSError(domain: "profile not found", code: 0)
}
let content = LibboxProfileContent()
content.name = profile.name
switch profile.type {
case .local:
content.type = LibboxProfileTypeLocal
case .icloud:
content.type = LibboxProfileTypeiCloud
case .remote:
content.type = LibboxProfileTypeRemote
}
content.config = try profile.read()
if profile.type != .local {
content.remotePath = profile.remoteURL!
}
if profile.type == .remote {
content.autoUpdate = profile.autoUpdate
if let lastUpdated = profile.lastUpdated {
content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970)
}
}
try connection.write(content.encode())
default:
throw NSError(domain: "unexpected message type \(messageType)", code: 0)
}
}
private func writeProfilePreviewList() throws {
let profiles = try ProfileManager.list()
let encoder = LibboxProfileEncoder()
for profile in profiles {
let preview = LibboxProfilePreview()
preview.profileID = profile.mustID
preview.name = profile.name
switch profile.type {
case .local:
preview.type = LibboxProfileTypeLocal
case .icloud:
preview.type = LibboxProfileTypeiCloud
case .remote:
preview.type = LibboxProfileTypeRemote
}
encoder.append(preview)
}
try connection.write(encoder.encode())
}
private func writeError(_ message: String) {
let errorMessage = LibboxErrorMessage()
errorMessage.message = message
try? connection.write(errorMessage.encode())
}
}
}
+16 -14
View File
@@ -3,6 +3,8 @@ import Libbox
import NetworkExtension import NetworkExtension
open class ExtensionProvider: NEPacketTunnelProvider { open class ExtensionProvider: NEPacketTunnelProvider {
public static let errorFile = FilePath.workingDirectory.appendingPathComponent("network_extension_error")
public var username: String? = nil public var username: String? = nil
private var commandServer: LibboxCommandServer! private var commandServer: LibboxCommandServer!
private var boxService: LibboxBoxService! private var boxService: LibboxBoxService!
@@ -10,6 +12,8 @@ open class ExtensionProvider: NEPacketTunnelProvider {
override open func startTunnel(options _: [String: NSObject]?) async throws { override open func startTunnel(options _: [String: NSObject]?) async throws {
NSLog("Here I am") NSLog("Here I am")
try? FileManager.default.removeItem(at: ExtensionProvider.errorFile)
do { do {
try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true)
} catch { } catch {
@@ -19,13 +23,17 @@ open class ExtensionProvider: NEPacketTunnelProvider {
if let username { if let username {
var error: NSError? var error: NSError?
LibboxSetupWithUsername(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, username, &error) LibboxSetupWithUsername(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, username, &error)
if let error { if let error {
writeFatalError("(packet-tunnel) error: setup service: \(error.localizedDescription)") writeFatalError("(packet-tunnel) error: setup service: \(error.localizedDescription)")
return return
} }
} else { } else {
LibboxSetup(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath) var isTVOS = false
#if os(tvOS)
isTVOS = true
#endif
LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, isTVOS)
} }
var error: NSError? var error: NSError?
@@ -36,7 +44,7 @@ open class ExtensionProvider: NEPacketTunnelProvider {
LibboxSetMemoryLimit(!SharedPreferences.disableMemoryLimit) LibboxSetMemoryLimit(!SharedPreferences.disableMemoryLimit)
commandServer = LibboxNewCommandServer(FilePath.sharedDirectory.relativePath, serverInterface(self), Int32(SharedPreferences.maxLogLines)) commandServer = LibboxNewCommandServer(serverInterface(self), Int32(SharedPreferences.maxLogLines))
do { do {
try commandServer.start() try commandServer.start()
} catch { } catch {
@@ -58,19 +66,13 @@ open class ExtensionProvider: NEPacketTunnelProvider {
private func writeError(_ message: String) { private func writeError(_ message: String) {
writeMessage(message) writeMessage(message)
#if os(iOS) try? message.write(to: ExtensionProvider.errorFile, atomically: true, encoding: .utf8)
ServiceNotification.postServiceNotification(title: "Service Error", message: message)
#else
if Variant.useSystemExtension {
NSLog(message)
} else {
displayMessage(message) { _ in
}
}
#endif
} }
public func writeFatalError(_ message: String) { public func writeFatalError(_ message: String) {
#if DEBUG
NSLog(message)
#endif
writeError(message) writeError(message)
cancelTunnelWithError(NSError(domain: message, code: 0)) cancelTunnelWithError(NSError(domain: message, code: 0))
} }
@@ -91,7 +93,7 @@ open class ExtensionProvider: NEPacketTunnelProvider {
do { do {
configContent = try profile.read() configContent = try profile.read()
} catch { } catch {
writeFatalError("(packet-tunnel) error: read config file: \(error.localizedDescription)") writeFatalError("(packet-tunnel) error: read config file \(profile.path): \(error.localizedDescription)")
return return
} }
var error: NSError? var error: NSError?
+31 -14
View File
@@ -7,25 +7,42 @@ public enum FilePath {
public extension FilePath { public extension FilePath {
static let groupName = "group.\(packageName)" static let groupName = "group.\(packageName)"
static var sharedDirectory = defaultSharedDirectory private static let defaultSharedDirectory: URL! = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: FilePath.groupName)
private static var defaultSharedDirectory: URL { #if os(iOS)
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: FilePath.groupName)! static let sharedDirectory = defaultSharedDirectory!
} #elseif os(tvOS)
static let sharedDirectory = defaultSharedDirectory
static var cacheDirectory: URL {
sharedDirectory
.appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true)
} #elseif os(macOS)
static var sharedDirectory: URL! = defaultSharedDirectory
#endif
static var workingDirectory: URL { #if os(iOS)
cacheDirectory.appendingPathComponent("Working", isDirectory: true) static let cacheDirectory = sharedDirectory
} .appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true)
#elseif os(tvOS)
static let cacheDirectory = sharedDirectory
#elseif os(macOS)
static var cacheDirectory: URL {
sharedDirectory
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true)
}
#endif
static var iCloudDirectory: URL { #if os(macOS)
FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true) static var workingDirectory: URL {
} cacheDirectory.appendingPathComponent("Working", isDirectory: true)
}
#else
static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true)
#endif
static var iCloudDirectory: URL! = FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true)
} }
public extension URL { public extension URL {
-45
View File
@@ -1,45 +0,0 @@
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 {
if let listener = ServiceNotification.listener {
listener(notification.request.content)
return []
} else {
return [.alert]
}
}
}
}
+2
View File
@@ -11,5 +11,7 @@ public enum Variant {
public static let applicationName = "SFI" public static let applicationName = "SFI"
#elseif os(macOS) #elseif os(macOS)
public static let applicationName = "SFM" public static let applicationName = "SFM"
#elseif os(tvOS)
public static let applicationName = "SFT"
#endif #endif
} }
+1 -1
View File
@@ -7,7 +7,7 @@ import Library
open class ApplicationDelegate: NSObject, NSApplicationDelegate { open class ApplicationDelegate: NSObject, NSApplicationDelegate {
public func applicationDidFinishLaunching(_: Notification) { public func applicationDidFinishLaunching(_: Notification) {
NSLog("Here I stand") NSLog("Here I stand")
// ServiceNotification.register() // Not work LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, false)
let event = NSAppleEventManager.shared().currentAppleEvent let event = NSAppleEventManager.shared().currentAppleEvent
let launchedAsLogInItem = let launchedAsLogInItem =
event?.eventID == kAEOpenApplication && event?.eventID == kAEOpenApplication &&
-8
View File
@@ -43,14 +43,6 @@ public struct MainView: View {
} }
#endif #endif
.alertBinding($alert) .alertBinding($alert)
.onAppear {
ServiceNotification.setServiceNotificationListener { notification in
alert = Alert(title: Text(notification.title), message: Text(notification.body))
}
}
.onDisappear {
ServiceNotification.removeServiceNotificationListener()
}
.toolbar { .toolbar {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
StartStopButton() StartStopButton()
+1 -1
View File
@@ -191,7 +191,7 @@ public struct MenuView: View {
NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil) NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil)
if profile.status.isConnected { if profile.status.isConnected {
do { do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() try LibboxNewStandaloneCommandClient()?.serviceReload()
} catch { } catch {
alert = Alert(error) alert = Alert(error)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
# sing-box-for-apple # sing-box-for-apple
Experimental iOS/macOS client for sing-box, the universal proxy platform. Experimental iOS/macOS/tvOS client for sing-box, the universal proxy platform.
## Documentation ## Documentation
+21 -1
View File
@@ -1,12 +1,16 @@
import ApplicationLibrary import ApplicationLibrary
import Foundation import Foundation
import Libbox
import Library import Library
import Network
import UIKit import UIKit
class ApplicationDelegate: NSObject, UIApplicationDelegate { class ApplicationDelegate: NSObject, UIApplicationDelegate {
private var profileServer: ProfileServer?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
NSLog("Here I stand") NSLog("Here I stand")
ServiceNotification.register() LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, false)
Task.detached { Task.detached {
do { do {
try await UIProfileUpdateTask.setup() try await UIProfileUpdateTask.setup()
@@ -18,9 +22,25 @@ class ApplicationDelegate: NSObject, UIApplicationDelegate {
Task.detached { Task.detached {
await self.requestNetworkPermission() await self.requestNetworkPermission()
} }
if #available(iOS 16.0, *) {
Task.detached {
await self.setupProfileServer()
}
}
return true return true
} }
@available(iOS 16.0, *)
private func setupProfileServer() {
do {
let profileServer = try ProfileServer()
profileServer.start()
self.profileServer = profileServer
} catch {
NSLog("setup profile server error: \(error.localizedDescription)")
}
}
private func requestNetworkPermission() { private func requestNetworkPermission() {
if UIDevice.current.userInterfaceIdiom != .phone { if UIDevice.current.userInterfaceIdiom != .phone {
return return
+10
View File
@@ -2,6 +2,16 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSApplicationServices</key>
<dict>
<key>Advertises</key>
<array>
<dict>
<key>NSApplicationServiceIdentifier</key>
<string>sing-box:profile</string>
</dict>
</array>
</dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>io.nekohasekai.sfa.update_profiles</string> <string>io.nekohasekai.sfa.update_profiles</string>
-22
View File
@@ -10,11 +10,6 @@ struct MainView: View {
@State private var extensionProfile: ExtensionProfile? @State private var extensionProfile: ExtensionProfile?
@State private var profileLoading = true @State private var profileLoading = true
@State private var logClient: LogClient! @State private var logClient: LogClient!
@State private var serviceNotificationTitle = ""
@State private var serviceNotificationContent = ""
@State private var serviceNotificationPresented = false
@State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var importRemoteProfile: LibboxImportRemoteProfile?
var body: some View { var body: some View {
@@ -30,23 +25,6 @@ struct MainView: View {
ContentView() 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 .onChange(of: scenePhase, perform: { newValue in
if newValue == .active { if newValue == .active {
Task.detached { Task.detached {
+12
View File
@@ -0,0 +1,12 @@
import SwiftUI
@main
struct Application: App {
@UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
var body: some Scene {
WindowGroup {
MainView()
}
}
}
+21
View File
@@ -0,0 +1,21 @@
import ApplicationLibrary
import Foundation
import Libbox
import Library
import UIKit
class ApplicationDelegate: NSObject, UIApplicationDelegate {
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
NSLog("Here I stand")
LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, true)
Task.detached {
do {
try await UIProfileUpdateTask.setup()
NSLog("setup background task success")
} catch {
NSLog("setup background task error: \(error.localizedDescription)")
}
}
return true
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "tv - App Icon - AppStore - Back -1280 x 768 pt.png",
"idiom" : "tv"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,17 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"layers" : [
{
"filename" : "Front.imagestacklayer"
},
{
"filename" : "Middle.imagestacklayer"
},
{
"filename" : "Back.imagestacklayer"
}
]
}
@@ -0,0 +1,11 @@
{
"images" : [
{
"idiom" : "tv"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "tv - App Icon - AppStore - Front -1280 x 768 pt.png",
"idiom" : "tv"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,17 @@
{
"images" : [
{
"filename" : "tv- App Icon Small - Back - 400 x 240 pt.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,17 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"layers" : [
{
"filename" : "Front.imagestacklayer"
},
{
"filename" : "Middle.imagestacklayer"
},
{
"filename" : "Back.imagestacklayer"
}
]
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
},
{
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,17 @@
{
"images" : [
{
"filename" : "tv- App Icon Small - Front - 400 x 240 pt.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,32 @@
{
"assets" : [
{
"filename" : "App Icon - App Store.imagestack",
"idiom" : "tv",
"role" : "primary-app-icon",
"size" : "1280x768"
},
{
"filename" : "App Icon.imagestack",
"idiom" : "tv",
"role" : "primary-app-icon",
"size" : "400x240"
},
{
"filename" : "Top Shelf Image Wide.imageset",
"idiom" : "tv",
"role" : "top-shelf-image-wide",
"size" : "2320x720"
},
{
"filename" : "Top Shelf Image.imageset",
"idiom" : "tv",
"role" : "top-shelf-image",
"size" : "1920x720"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
},
{
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
},
{
"idiom" : "tv",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+63
View File
@@ -0,0 +1,63 @@
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
.focusSection()
}
.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
.focusSection()
}
.navigationViewStyle(.stack)
.tag(page)
.tabItem { page.label }
}
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>NSApplicationServices</key>
<dict>
<key>Browses</key>
<array>
<dict>
<key>NSApplicationServiceIdentifier</key>
<string>sing-box:profile</string>
<key>NSApplicationServiceUsageDescription</key>
<string>Import sing-box profile from other devices</string>
<key>NSApplicationServicePlatformSupport</key>
<array>
<string>iOS</string>
<string>iPadOS</string>
</array>
</dict>
</array>
</dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>io.nekohasekai.sfa.update_profiles</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLIconFile</key>
<string>AppIcon.icns</string>
<key>CFBundleURLName</key>
<string>sing-box</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sing-box</string>
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
</dict>
</plist>
+83
View File
@@ -0,0 +1,83 @@
import ApplicationLibrary
import Libbox
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 importRemoteProfile: LibboxImportRemoteProfile?
var body: some View {
viewBuilder {
if profileLoading {
ProgressView().onAppear {
Task.detached {
logClient = LogClient(SharedPreferences.maxLogLines)
await loadProfile()
}
}
} else {
ContentView()
}
}
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
Task.detached {
await loadProfile()
}
}
})
.environment(\.selection, $selection)
.environment(\.extensionProfile, $extensionProfile)
.environment(\.logClient, $logClient)
.environment(\.importRemoteProfile, $importRemoteProfile)
.onOpenURL(perform: openURL)
}
private func openURL(url: URL) {
if url.host == "import-remote-profile" {
var error: NSError?
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if error != nil {
return
}
if selection != .profiles {
selection = .profiles
}
}
}
private func loadProfile() async {
defer {
profileLoading = false
}
if ApplicationLibrary.inPreview {
return
}
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()
}
}
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.nekohasekai.sfa</string>
</array>
</dict>
</plist>
+267 -24
View File
@@ -48,6 +48,12 @@
3A4EAD362A4FEB9C005435B3 /* ProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */; }; 3A4EAD362A4FEB9C005435B3 /* ProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */; };
3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; }; 3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; };
3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */; }; 3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */; };
3A4FB1572A73467F007012B9 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
3A4FB1582A73467F007012B9 /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3A4FB15C2A73468C007012B9 /* ApplicationLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; };
3A4FB1602A7346A1007012B9 /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3A4FB1682A7358C9007012B9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4FB1672A7358C9007012B9 /* ApplicationDelegate.swift */; };
3A4FB16A2A735AC9007012B9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4FB1692A735AC9007012B9 /* MainView.swift */; };
3A57DF372A4D5D2600690BC5 /* Profile+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */; }; 3A57DF372A4D5D2600690BC5 /* Profile+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */; };
3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF412A4D927A00690BC5 /* Profile+Hashable.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 */; }; 3A5F26C82A503D4A00C27EDF /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
@@ -71,11 +77,16 @@
3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 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, ); }; }; 3A9759212A4EB69C00E4404B /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3AB1220B2A70FD500087CD55 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1220A2A70FD500087CD55 /* Alert.swift */; }; 3AB1220B2A70FD500087CD55 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1220A2A70FD500087CD55 /* Alert.swift */; };
3AC03B992A72BF3300B7946F /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC03B982A72BF3300B7946F /* Application.swift */; };
3AC03B9B2A72BF3300B7946F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC03B9A2A72BF3300B7946F /* ContentView.swift */; };
3AC03B9D2A72BF3500B7946F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AC03B9C2A72BF3500B7946F /* Assets.xcassets */; };
3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (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 */; }; 3AC1944F2A50247300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */; };
3AC194502A502DFE00BD8CB9 /* ServiceNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */; };
3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */; }; 3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */; };
3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */; };
3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD0953C2A70EB310052764E /* Profile+Share.swift */; }; 3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD0953C2A70EB310052764E /* Profile+Share.swift */; };
3ADBB4252A7389640041D44F /* ProfileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADBB4242A7389640041D44F /* ProfileServer.swift */; };
3ADBB42A2A73A7060041D44F /* NWSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADBB4292A73A7060041D44F /* NWSocket.swift */; };
3AE4D0B22A6E2B6A009FEA9E /* ExtensionPlatformInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A62A4AA0FF002B34AC /* ExtensionPlatformInterface.swift */; }; 3AE4D0B22A6E2B6A009FEA9E /* ExtensionPlatformInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A62A4AA0FF002B34AC /* ExtensionPlatformInterface.swift */; };
3AE4D0B32A6E2B94009FEA9E /* Extension+RunBlocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A82A4AA155002B34AC /* Extension+RunBlocking.swift */; }; 3AE4D0B32A6E2B94009FEA9E /* Extension+RunBlocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342A82A4AA155002B34AC /* Extension+RunBlocking.swift */; };
3AE4D0B42A6E2BA3009FEA9E /* Extension+Iterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342AA2A4AA173002B34AC /* Extension+Iterator.swift */; }; 3AE4D0B42A6E2BA3009FEA9E /* Extension+Iterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF342AA2A4AA173002B34AC /* Extension+Iterator.swift */; };
@@ -154,6 +165,27 @@
remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3; remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3;
remoteInfo = ApplicationLibrary; remoteInfo = ApplicationLibrary;
}; };
3A4FB1592A73467F007012B9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3AEC211C2A459B4700A63465;
remoteInfo = Library;
};
3A4FB15E2A73468C007012B9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3;
remoteInfo = ApplicationLibrary;
};
3A4FB1612A7346A1007012B9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3A096F852A4ED3DE00D4A2ED;
remoteInfo = Extension;
};
3A76504A2A4F07F6003945C5 /* PBXContainerItemProxy */ = { 3A76504A2A4F07F6003945C5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
@@ -270,6 +302,28 @@
name = "Embed Foundation Extensions"; name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3A4FB15B2A73467F007012B9 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
3A4FB1582A73467F007012B9 /* Library.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
3A4FB1632A7346A1007012B9 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
3A4FB1602A7346A1007012B9 /* Extension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
3A5F26CA2A503D4B00C27EDF /* Embed Frameworks */ = { 3A5F26CA2A503D4B00C27EDF /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -373,6 +427,10 @@
3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApplicationLibrary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 3A4EAD202A4FEB3C005435B3 /* ApplicationLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationLibrary.swift; sourceTree = "<group>"; };
3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+isConnected.swift"; sourceTree = "<group>"; }; 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+isConnected.swift"; sourceTree = "<group>"; };
3A4FB1642A73568E007012B9 /* SFT.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SFT.entitlements; sourceTree = "<group>"; };
3A4FB1652A73574B007012B9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3A4FB1672A7358C9007012B9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; };
3A4FB1692A735AC9007012B9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIProfileUpdateTask.swift; sourceTree = "<group>"; }; 3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIProfileUpdateTask.swift; sourceTree = "<group>"; };
3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateTask.swift; sourceTree = "<group>"; }; 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateTask.swift; sourceTree = "<group>"; };
3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Date.swift"; sourceTree = "<group>"; }; 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Date.swift"; sourceTree = "<group>"; };
@@ -397,11 +455,17 @@
3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3AB1220A2A70FD500087CD55 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; }; 3AB1220A2A70FD500087CD55 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
3ABA46D22A6A32A100D8366B /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = Library/Frameworks/Messages.framework; sourceTree = DEVELOPER_DIR; }; 3ABA46D22A6A32A100D8366B /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = Library/Frameworks/Messages.framework; sourceTree = DEVELOPER_DIR; };
3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceNotification.swift; sourceTree = "<group>"; }; 3AC03B962A72BF3300B7946F /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
3AC03B982A72BF3300B7946F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
3AC03B9A2A72BF3300B7946F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
3AC03B9C2A72BF3500B7946F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; }; 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; };
3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; }; 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; };
3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCensorship.swift; sourceTree = "<group>"; }; 3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCensorship.swift; sourceTree = "<group>"; };
3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportProfileView.swift; sourceTree = "<group>"; };
3AD0953C2A70EB310052764E /* Profile+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Share.swift"; sourceTree = "<group>"; }; 3AD0953C2A70EB310052764E /* Profile+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Share.swift"; sourceTree = "<group>"; };
3ADBB4242A7389640041D44F /* ProfileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileServer.swift; sourceTree = "<group>"; };
3ADBB4292A73A7060041D44F /* NWSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSocket.swift; sourceTree = "<group>"; };
3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveDashboardView.swift; sourceTree = "<group>"; }; 3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveDashboardView.swift; sourceTree = "<group>"; };
3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; }; 3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; }; 3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
@@ -481,6 +545,15 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3AC03B932A72BF3300B7946F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3A4FB1572A73467F007012B9 /* Library.framework in Frameworks */,
3A4FB15C2A73468C007012B9 /* ApplicationLibrary.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AEC20F02A459AB400A63465 /* Frameworks */ = { 3AEC20F02A459AB400A63465 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -619,6 +692,29 @@
path = Setting; path = Setting;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3AC03B972A72BF3300B7946F /* SFT */ = {
isa = PBXGroup;
children = (
3A4FB1642A73568E007012B9 /* SFT.entitlements */,
3AC03B982A72BF3300B7946F /* Application.swift */,
3AC03B9A2A72BF3300B7946F /* ContentView.swift */,
3AC03B9C2A72BF3500B7946F /* Assets.xcassets */,
3A4FB1652A73574B007012B9 /* Info.plist */,
3A4FB1672A7358C9007012B9 /* ApplicationDelegate.swift */,
3A4FB1692A735AC9007012B9 /* MainView.swift */,
);
path = SFT;
sourceTree = "<group>";
};
3ADBB4262A739DD00041D44F /* Discovery */ = {
isa = PBXGroup;
children = (
3ADBB4242A7389640041D44F /* ProfileServer.swift */,
3ADBB4292A73A7060041D44F /* NWSocket.swift */,
);
path = Discovery;
sourceTree = "<group>";
};
3ADF8DF52A4AFA8E00900CC8 /* Profile */ = { 3ADF8DF52A4AFA8E00900CC8 /* Profile */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -627,6 +723,7 @@
3ADF8DFC2A4B096000900CC8 /* EditProfileWindowView.swift */, 3ADF8DFC2A4B096000900CC8 /* EditProfileWindowView.swift */,
3ADF8E002A4B0F6300900CC8 /* EditProfileView.swift */, 3ADF8E002A4B0F6300900CC8 /* EditProfileView.swift */,
3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */, 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */,
3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */,
); );
path = Profile; path = Profile;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -638,6 +735,7 @@
3AEC20F42A459AB400A63465 /* SFI */, 3AEC20F42A459AB400A63465 /* SFI */,
3AEC210A2A459B1900A63465 /* SFM */, 3AEC210A2A459B1900A63465 /* SFM */,
3AEECC052A6DF9CA006A0E0C /* SFM.System */, 3AEECC052A6DF9CA006A0E0C /* SFM.System */,
3AC03B972A72BF3300B7946F /* SFT */,
3AEC211E2A459B4700A63465 /* Library */, 3AEC211E2A459B4700A63465 /* Library */,
3A4EAD112A4FEAE6005435B3 /* ApplicationLibrary */, 3A4EAD112A4FEAE6005435B3 /* ApplicationLibrary */,
3AEECC302A6DFDAD006A0E0C /* MacLibrary */, 3AEECC302A6DFDAD006A0E0C /* MacLibrary */,
@@ -661,6 +759,7 @@
3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfa.system.systemextension */, 3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfa.system.systemextension */,
3AEECC042A6DF9CA006A0E0C /* SFM.app */, 3AEECC042A6DF9CA006A0E0C /* SFM.app */,
3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */, 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */,
3AC03B962A72BF3300B7946F /* sing-box.app */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -703,6 +802,7 @@
3AEC211E2A459B4700A63465 /* Library */ = { 3AEC211E2A459B4700A63465 /* Library */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3ADBB4262A739DD00041D44F /* Discovery */,
3AEC21462A45A9CE00A63465 /* Shared */, 3AEC21462A45A9CE00A63465 /* Shared */,
3AEC21432A45A92B00A63465 /* Network */, 3AEC21432A45A92B00A63465 /* Network */,
3AEC213A2A459FD200A63465 /* Database */, 3AEC213A2A459FD200A63465 /* Database */,
@@ -746,7 +846,6 @@
3AEC21462A45A9CE00A63465 /* Shared */ = { 3AEC21462A45A9CE00A63465 /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */,
3AEC21472A45A9DE00A63465 /* Bundle+Version.swift */, 3AEC21472A45A9DE00A63465 /* Bundle+Version.swift */,
3AEC214B2A45AA8E00A63465 /* FilePath.swift */, 3AEC214B2A45AA8E00A63465 /* FilePath.swift */,
3A2223552A6E1BDE00C50B23 /* Variant.swift */, 3A2223552A6E1BDE00C50B23 /* Variant.swift */,
@@ -914,6 +1013,28 @@
productReference = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; productReference = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */;
productType = "com.apple.product-type.extensionkit-extension"; productType = "com.apple.product-type.extensionkit-extension";
}; };
3AC03B952A72BF3300B7946F /* SFT */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3AC03BA32A72BF3500B7946F /* Build configuration list for PBXNativeTarget "SFT" */;
buildPhases = (
3AC03B922A72BF3300B7946F /* Sources */,
3AC03B932A72BF3300B7946F /* Frameworks */,
3AC03B942A72BF3300B7946F /* Resources */,
3A4FB15B2A73467F007012B9 /* Embed Frameworks */,
3A4FB1632A7346A1007012B9 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
3A4FB15A2A73467F007012B9 /* PBXTargetDependency */,
3A4FB15F2A73468C007012B9 /* PBXTargetDependency */,
3A4FB1622A7346A1007012B9 /* PBXTargetDependency */,
);
name = SFT;
productName = SFT;
productReference = 3AC03B962A72BF3300B7946F /* sing-box.app */;
productType = "com.apple.product-type.application";
};
3AEC20F22A459AB400A63465 /* SFI */ = { 3AEC20F22A459AB400A63465 /* SFI */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */; buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */;
@@ -1076,6 +1197,9 @@
3A77016C2A4E6B34008F031F = { 3A77016C2A4E6B34008F031F = {
CreatedOnToolsVersion = 15.0; CreatedOnToolsVersion = 15.0;
}; };
3AC03B952A72BF3300B7946F = {
CreatedOnToolsVersion = 14.3.1;
};
3AEC20F22A459AB400A63465 = { 3AEC20F22A459AB400A63465 = {
CreatedOnToolsVersion = 15.0; CreatedOnToolsVersion = 15.0;
}; };
@@ -1121,6 +1245,7 @@
3AEC20F22A459AB400A63465 /* SFI */, 3AEC20F22A459AB400A63465 /* SFI */,
3AEC21082A459B1900A63465 /* SFM */, 3AEC21082A459B1900A63465 /* SFM */,
3AEECC032A6DF9CA006A0E0C /* SFM.System */, 3AEECC032A6DF9CA006A0E0C /* SFM.System */,
3AC03B952A72BF3300B7946F /* SFT */,
3AEC211C2A459B4700A63465 /* Library */, 3AEC211C2A459B4700A63465 /* Library */,
3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */, 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */,
3AEECC2E2A6DFDAD006A0E0C /* MacLibrary */, 3AEECC2E2A6DFDAD006A0E0C /* MacLibrary */,
@@ -1139,6 +1264,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3AC03B942A72BF3300B7946F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AC03B9D2A72BF3500B7946F /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AEC20F12A459AB400A63465 /* Resources */ = { 3AEC20F12A459AB400A63465 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1210,6 +1343,7 @@
3A4EAD2D2A4FEB77005435B3 /* ProfileView.swift in Sources */, 3A4EAD2D2A4FEB77005435B3 /* ProfileView.swift in Sources */,
3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */, 3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */,
3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */, 3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */,
3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */,
3A4EAD292A4FEB6D005435B3 /* Formtem.swift in Sources */, 3A4EAD292A4FEB6D005435B3 /* Formtem.swift in Sources */,
3A4EAD302A4FEB77005435B3 /* NewProfileView.swift in Sources */, 3A4EAD302A4FEB77005435B3 /* NewProfileView.swift in Sources */,
3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */, 3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */,
@@ -1247,6 +1381,17 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3AC03B922A72BF3300B7946F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3A4FB1682A7358C9007012B9 /* ApplicationDelegate.swift in Sources */,
3A4FB16A2A735AC9007012B9 /* MainView.swift in Sources */,
3AC03B9B2A72BF3300B7946F /* ContentView.swift in Sources */,
3AC03B992A72BF3300B7946F /* Application.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AEC20EF2A459AB400A63465 /* Sources */ = { 3AEC20EF2A459AB400A63465 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1284,11 +1429,12 @@
3AEC21452A45A93800A63465 /* HTTPClient.swift in Sources */, 3AEC21452A45A93800A63465 /* HTTPClient.swift in Sources */,
3AEC213C2A459FDF00A63465 /* Databse.swift in Sources */, 3AEC213C2A459FDF00A63465 /* Databse.swift in Sources */,
3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */, 3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */,
3ADBB4252A7389640041D44F /* ProfileServer.swift in Sources */,
3AEC21422A45A8FF00A63465 /* Profile+Update.swift in Sources */, 3AEC21422A45A8FF00A63465 /* Profile+Update.swift in Sources */,
3A22235A2A6E212A00C50B23 /* SystemExtension.swift in Sources */, 3A22235A2A6E212A00C50B23 /* SystemExtension.swift in Sources */,
3AE4D0B42A6E2BA3009FEA9E /* Extension+Iterator.swift in Sources */, 3AE4D0B42A6E2BA3009FEA9E /* Extension+Iterator.swift in Sources */,
3AEC214C2A45AA8E00A63465 /* FilePath.swift in Sources */, 3AEC214C2A45AA8E00A63465 /* FilePath.swift in Sources */,
3AC194502A502DFE00BD8CB9 /* ServiceNotification.swift in Sources */, 3ADBB42A2A73A7060041D44F /* NWSocket.swift in Sources */,
3AE4D0B22A6E2B6A009FEA9E /* ExtensionPlatformInterface.swift in Sources */, 3AE4D0B22A6E2B6A009FEA9E /* ExtensionPlatformInterface.swift in Sources */,
3AEC21402A45A28F00A63465 /* ProfileManager.swift in Sources */, 3AEC21402A45A28F00A63465 /* ProfileManager.swift in Sources */,
3A9144D92A46AE370036E9AD /* ShadredPreferences+Database.swift in Sources */, 3A9144D92A46AE370036E9AD /* ShadredPreferences+Database.swift in Sources */,
@@ -1355,6 +1501,21 @@
target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */; target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */;
targetProxy = 3A4EAD392A4FEC20005435B3 /* PBXContainerItemProxy */; targetProxy = 3A4EAD392A4FEC20005435B3 /* PBXContainerItemProxy */;
}; };
3A4FB15A2A73467F007012B9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3AEC211C2A459B4700A63465 /* Library */;
targetProxy = 3A4FB1592A73467F007012B9 /* PBXContainerItemProxy */;
};
3A4FB15F2A73468C007012B9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */;
targetProxy = 3A4FB15E2A73468C007012B9 /* PBXContainerItemProxy */;
};
3A4FB1622A7346A1007012B9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3A096F852A4ED3DE00D4A2ED /* Extension */;
targetProxy = 3A4FB1612A7346A1007012B9 /* PBXContainerItemProxy */;
};
3A76504B2A4F07F6003945C5 /* PBXTargetDependency */ = { 3A76504B2A4F07F6003945C5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 3AEC211C2A459B4700A63465 /* Library */; target = 3AEC211C2A459B4700A63465 /* Library */;
@@ -1446,20 +1607,19 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
OTHER_CODE_SIGN_FLAGS = ""; OTHER_CODE_SIGN_FLAGS = "";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
}; };
name = Debug; name = Debug;
}; };
@@ -1486,19 +1646,18 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
OTHER_CODE_SIGN_FLAGS = ""; OTHER_CODE_SIGN_FLAGS = "";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
}; };
name = Release; name = Release;
}; };
@@ -1534,11 +1693,14 @@
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
}; };
name = Debug; name = Debug;
}; };
@@ -1574,10 +1736,13 @@
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
}; };
name = Release; name = Release;
}; };
@@ -1604,7 +1769,6 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
OTHER_CODE_SIGN_FLAGS = ""; OTHER_CODE_SIGN_FLAGS = "";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1644,7 +1808,6 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
OTHER_CODE_SIGN_FLAGS = ""; OTHER_CODE_SIGN_FLAGS = "";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1662,6 +1825,75 @@
}; };
name = Release; name = Release;
}; };
3AC03BA12A72BF3500B7946F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SFT/SFT.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z56Z6NYZN2;
ENABLE_PREVIEWS = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SFT/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "sing-box";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIUserInterfaceStyle = Automatic;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = "-ld_classic";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
SDKROOT = appletvos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 17.0;
};
name = Debug;
};
3AC03BA22A72BF3500B7946F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SFT/SFT.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z56Z6NYZN2;
ENABLE_PREVIEWS = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SFT/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "sing-box";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIUserInterfaceStyle = Automatic;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.5;
OTHER_LDFLAGS = "-ld_classic";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
SDKROOT = appletvos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 17.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
3AEC20CB2A45991900A63465 /* Debug */ = { 3AEC20CB2A45991900A63465 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -1820,7 +2052,6 @@
); );
MARKETING_VERSION = 1.3.5; MARKETING_VERSION = 1.3.5;
OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box"; PRODUCT_NAME = "sing-box";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1861,7 +2092,6 @@
); );
MARKETING_VERSION = 1.3.5; MARKETING_VERSION = 1.3.5;
OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box"; PRODUCT_NAME = "sing-box";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1955,6 +2185,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
@@ -1963,6 +2194,7 @@
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = Z56Z6NYZN2;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2;
"DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1984,20 +2216,20 @@
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
}; };
name = Debug; name = Debug;
}; };
@@ -2007,6 +2239,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
@@ -2015,6 +2248,7 @@
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = Z56Z6NYZN2;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2;
"DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2036,19 +2270,19 @@
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = "-ld64";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7"; TARGETED_DEVICE_FAMILY = "1,2,3";
TVOS_DEPLOYMENT_TARGET = 17.0;
}; };
name = Release; name = Release;
}; };
@@ -2309,6 +2543,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
3AC03BA32A72BF3500B7946F /* Build configuration list for PBXNativeTarget "SFT" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AC03BA12A72BF3500B7946F /* Debug */,
3AC03BA22A72BF3500B7946F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = { 3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@@ -69,7 +69,7 @@
<key>MacLibrary.xcscheme_^#shared#^_</key> <key>MacLibrary.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>6</integer>
</dict> </dict>
<key>MessageExtension.xcscheme_^#shared#^_</key> <key>MessageExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
@@ -81,14 +81,14 @@
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>11</integer> <integer>12</integer>
</dict> </dict>
<key>MyPlayground (Playground) 2.xcscheme</key> <key>MyPlayground (Playground) 2.xcscheme</key>
<dict> <dict>
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>12</integer> <integer>13</integer>
</dict> </dict>
<key>MyPlayground (Playground) 3.xcscheme</key> <key>MyPlayground (Playground) 3.xcscheme</key>
<dict> <dict>
@@ -116,7 +116,7 @@
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>10</integer> <integer>11</integer>
</dict> </dict>
<key>SFA.xcscheme_^#shared#^_</key> <key>SFA.xcscheme_^#shared#^_</key>
<dict> <dict>
@@ -131,17 +131,22 @@
<key>SFM.System.xcscheme_^#shared#^_</key> <key>SFM.System.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>3</integer>
</dict> </dict>
<key>SFM.xcscheme</key> <key>SFM.xcscheme</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>1</integer> <integer>1</integer>
</dict> </dict>
<key>SFT.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
</dict>
<key>SystemExtension.xcscheme_^#shared#^_</key> <key>SystemExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>2</integer>
</dict> </dict>
<key>Test.xcscheme_^#shared#^_</key> <key>Test.xcscheme_^#shared#^_</key>
<dict> <dict>
@@ -153,14 +158,14 @@
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>8</integer> <integer>15</integer>
</dict> </dict>
<key>Tour (Playground) 2.xcscheme</key> <key>Tour (Playground) 2.xcscheme</key>
<dict> <dict>
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>9</integer> <integer>16</integer>
</dict> </dict>
<key>Tour (Playground) 3.xcscheme</key> <key>Tour (Playground) 3.xcscheme</key>
<dict> <dict>
@@ -188,21 +193,21 @@
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>7</integer> <integer>14</integer>
</dict> </dict>
<key>TransactionObserver (Playground) 1.xcscheme</key> <key>TransactionObserver (Playground) 1.xcscheme</key>
<dict> <dict>
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>15</integer> <integer>9</integer>
</dict> </dict>
<key>TransactionObserver (Playground) 2.xcscheme</key> <key>TransactionObserver (Playground) 2.xcscheme</key>
<dict> <dict>
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>16</integer> <integer>10</integer>
</dict> </dict>
<key>TransactionObserver (Playground) 3.xcscheme</key> <key>TransactionObserver (Playground) 3.xcscheme</key>
<dict> <dict>
@@ -230,7 +235,7 @@
<key>isShown</key> <key>isShown</key>
<false/> <false/>
<key>orderHint</key> <key>orderHint</key>
<integer>14</integer> <integer>8</integer>
</dict> </dict>
<key>mactest.xcscheme_^#shared#^_</key> <key>mactest.xcscheme_^#shared#^_</key>
<dict> <dict>