diff --git a/ApplicationLibrary/Service/UIProfileUpdateTask.swift b/ApplicationLibrary/Service/UIProfileUpdateTask.swift index f35da8a..f6fc608 100644 --- a/ApplicationLibrary/Service/UIProfileUpdateTask.swift +++ b/ApplicationLibrary/Service/UIProfileUpdateTask.swift @@ -2,7 +2,7 @@ import BackgroundTasks import Foundation import Library -#if os(iOS) +#if os(iOS) || os(tvOS) public class UIProfileUpdateTask: BGAppRefreshTask { public static let taskSchedulerPermittedIdentifier = "\(FilePath.packageName).update_profiles" diff --git a/ApplicationLibrary/Views/Abstract/Formtem.swift b/ApplicationLibrary/Views/Abstract/Formtem.swift index 12495ca..e20f053 100644 --- a/ApplicationLibrary/Views/Abstract/Formtem.swift +++ b/ApplicationLibrary/Views/Abstract/Formtem.swift @@ -17,19 +17,21 @@ public func FormTextItem(_ name: String, _ value: String) -> some View { Text(value) .multilineTextAlignment(.trailing) .font(Font.system(.caption, design: .monospaced)) + #if os(iOS) || os(macOS) .textSelection(.enabled) + #endif } } public func FormItem(_ title: String, @ViewBuilder content: () -> some View) -> some View { - #if os(iOS) + #if os(iOS) || os(tvOS) HStack { Text(title) Spacer() Spacer() content() } - #else + #elseif os(macOS) content() #endif } diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index 531c934..ef4ee3c 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -33,11 +33,13 @@ public struct ActiveDashboardView: View { Text("Empty profiles") } else { VStack { - #if os(iOS) + #if os(iOS) || os(tvOS) if ApplicationLibrary.inPreview || profile.status.isConnected { ExtensionStatusView() .listStyle(.automatic) + #if os(iOS) .navigationBarTitleDisplayMode(.inline) + #endif } FormView { StartStopButton() @@ -50,7 +52,7 @@ public struct ActiveDashboardView: View { .pickerStyle(.inline) } } - #else + #elseif os(macOS) if ApplicationLibrary.inPreview || profile.status.isConnected { ExtensionStatusView() } @@ -77,36 +79,48 @@ public struct ActiveDashboardView: View { } } .alertBinding($alert) - #if os(iOS) - .onChange(of: scenePhase, perform: { newValue in - if newValue == .active { - Task.detached { - await doReload() - } - } - }) - .onChange(of: selection.wrappedValue, perform: { newValue in - if newValue == .dashboard { - Task.detached { - await doReload() - } - } - }) - #elseif os(macOS) - .onAppear { - if observer == nil { - observer = NotificationCenter.default.addObserver(forName: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { _ in - Task.detached { - await doReload() + .onChange(of: profile.status, perform: { newValue in + if newValue == .disconnecting || newValue == .connected { + Task.detached { + if let serviceError = try? String(contentsOf: ExtensionProvider.errorFile) { + DispatchQueue.main.async { + alert = Alert(errorMessage: serviceError) } - }) + try? FileManager.default.removeItem(at: ExtensionProvider.errorFile) + } } } - .onDisappear { - if let observer { - NotificationCenter.default.removeObserver(observer) + }) + #if os(iOS) || os(tvOS) + .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 } @@ -147,7 +161,7 @@ public struct ActiveDashboardView: View { NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil) if profile.status.isConnected { do { - try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + try LibboxNewStandaloneCommandClient()!.serviceReload() } catch { alert = Alert(error) } diff --git a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift index fb5738b..73ba37b 100644 --- a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift +++ b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift @@ -116,7 +116,7 @@ public struct ExtensionStatusView: View { private func closeConnections() { do { - try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.closeConnections() + try LibboxNewStandaloneCommandClient()!.closeConnections() } catch { alert = Alert(error) } @@ -162,9 +162,13 @@ public struct ExtensionStatusView: View { .font(.system(size: 16)) } .frame(minWidth: 125) - .padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13)) - .background(backgroundColor) - .cornerRadius(10) + #if os(tvOS) + .padding(EdgeInsets(top: 20, leading: 26, bottom: 20, trailing: 26)) + #else + .padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13)) + #endif + .background(backgroundColor) + .cornerRadius(10) } private var backgroundColor: Color { @@ -172,6 +176,8 @@ public struct ExtensionStatusView: View { return Color(uiColor: .secondarySystemGroupedBackground) #elseif os(macOS) return Color(nsColor: .textBackgroundColor) + #elseif os(tvOS) + return Color(uiColor: .black) #endif } } diff --git a/ApplicationLibrary/Views/Dashboard/StartStopButton.swift b/ApplicationLibrary/Views/Dashboard/StartStopButton.swift index b6d792b..036beb8 100644 --- a/ApplicationLibrary/Views/Dashboard/StartStopButton.swift +++ b/ApplicationLibrary/Views/Dashboard/StartStopButton.swift @@ -10,7 +10,7 @@ public struct StartStopButton: View { public var body: some View { viewBuilder { if ApplicationLibrary.inPreview { - #if os(iOS) + #if os(iOS) || os(tvOS) Toggle(isOn: .constant(true)) { Text("Enabled") } @@ -23,7 +23,7 @@ public struct StartStopButton: View { } else if let profile = extensionProfile.wrappedValue { Button0(profile) } else { - #if os(iOS) + #if os(iOS) || os(tvOS) Toggle(isOn: .constant(false)) { Text("Enabled") } @@ -49,7 +49,7 @@ public struct StartStopButton: View { var body: some View { viewBuilder { - #if os(iOS) + #if os(iOS) || os(tvOS) Toggle(isOn: Binding(get: { profile.status.isConnected }, set: { newValue, _ in diff --git a/ApplicationLibrary/Views/Groups/GroupItemView.swift b/ApplicationLibrary/Views/Groups/GroupItemView.swift index cf4a374..cb379d0 100644 --- a/ApplicationLibrary/Views/Groups/GroupItemView.swift +++ b/ApplicationLibrary/Views/Groups/GroupItemView.swift @@ -14,8 +14,7 @@ public struct GroupItemView: View { self.item = item } - @State private var errorPresented = false - @State private var errorMessage = "" + @State private var alert: Alert? public var body: some View { HStack { @@ -60,24 +59,17 @@ public struct GroupItemView: View { } } } - .alert(isPresented: $errorPresented) { - Alert( - title: Text("Error"), - message: Text(errorMessage), - dismissButton: .default(Text("Ok")) - ) - } + .alertBinding($alert) } private func selectOutbound() { do { - try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.selectOutbound(group.tag, outboundTag: item.tag) + try LibboxNewStandaloneCommandClient()!.selectOutbound(group.tag, outboundTag: item.tag) var newGroup = group newGroup.selected = item.tag _group.wrappedValue = newGroup } catch { - errorMessage = error.localizedDescription - errorPresented = true + alert = Alert(error) return } } @@ -87,6 +79,8 @@ public struct GroupItemView: View { return Color(uiColor: .secondarySystemGroupedBackground) #elseif os(macOS) return Color(nsColor: .textBackgroundColor) + #elseif os(tvOS) + return Color.black #endif } } diff --git a/ApplicationLibrary/Views/Groups/GroupView.swift b/ApplicationLibrary/Views/Groups/GroupView.swift index 2da860e..c43951b 100644 --- a/ApplicationLibrary/Views/Groups/GroupView.swift +++ b/ApplicationLibrary/Views/Groups/GroupView.swift @@ -113,7 +113,7 @@ public struct GroupView: View { } private func doURLTest() { - try? LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.urlTest(group.tag) + try? LibboxNewStandaloneCommandClient()!.urlTest(group.tag) } } diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index 227c05f..56755df 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -40,6 +40,9 @@ public struct LogView: View { ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in Text(it.element) .font(logFont) + #if os(tvOS) + .focusable() + #endif Spacer(minLength: 5) } @@ -52,6 +55,10 @@ public struct LogView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding() } + #if os(tvOS) + .focusEffectDisabled() + .focusSection() + #endif .onAppear { reader.scrollTo(logClient.logList.count - 1) } diff --git a/ApplicationLibrary/Views/NavigationPage.swift b/ApplicationLibrary/Views/NavigationPage.swift index 89af19b..24eff7f 100644 --- a/ApplicationLibrary/Views/NavigationPage.swift +++ b/ApplicationLibrary/Views/NavigationPage.swift @@ -75,6 +75,10 @@ public extension NavigationPage { } switch self { case .groups: + #if os(tvOS) + // TODO: fix groups ui + return false + #endif return profile?.status.isConnectedStrict == true default: return true diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift index a5899a8..06e6a84 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift @@ -1,133 +1,136 @@ -import Foundation -import Library -import SwiftUI +#if os(iOS) || os(macOS) + import Foundation + import Library + import SwiftUI -public struct EditProfileContentView: View { - #if os(macOS) - public static let windowID = "edit-profile-content" - #endif - - public struct Context: Codable, Hashable { - public let profileID: Int64 - public let readOnly: Bool - } - - private let profileID: Int64? - private let readOnly: Bool - - public init(_ context: Context?) { - profileID = context?.profileID - readOnly = context?.readOnly == true - } - - @Environment(\.dismiss) private var dismiss - - @State private var isLoading = true - @State private var profile: Profile! - @State private var profileContent: String = "" - @State private var isChanged = false - @State private var 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) + public struct EditProfileContentView: View { #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) + public static let windowID = "edit-profile-content" #endif - } - private var navigationTitle: String { - if readOnly { - return "View Content" - } else { - return "Edit Content" + 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) + .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() { - 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 - } -} +#endif diff --git a/ApplicationLibrary/Views/Profile/EditProfileView.swift b/ApplicationLibrary/Views/Profile/EditProfileView.swift index 9d1879d..caf7f01 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileView.swift @@ -46,20 +46,24 @@ public struct EditProfileView: View { FormTextItem("Last Updated", profile.lastUpdatedString) } } - #if os(iOS) + #if os(iOS) || os(tvOS) Section("Action") { if profile.type != .remote { - NavigationLink { - EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) - } label: { - Text("Edit Content").foregroundColor(.accentColor) - } + #if os(iOS) + NavigationLink { + EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) + } label: { + Text("Edit Content").foregroundColor(.accentColor) + } + #endif } else { - NavigationLink { - EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) - } label: { - Text("View Content").foregroundColor(.accentColor) - } + #if os(iOS) + NavigationLink { + EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) + } label: { + Text("View Content").foregroundColor(.accentColor) + } + #endif Button("Update") { isLoading = true Task.detached { @@ -67,17 +71,19 @@ public struct EditProfileView: View { } } .disabled(isLoading) - } - if #available(iOS 16.0, *) { - ShareLink(item: profile.shareLink) { - Text("Share") - } - } else { - Button("Share") { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { - windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil) + #if os(iOS) + if #available(iOS 16.0, *) { + ShareLink(item: profile.shareLink) { + Text("Share") + } + } else { + Button("Share") { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil) + } + } } - } + #endif } } #endif diff --git a/ApplicationLibrary/Views/Profile/ImportProfileView.swift b/ApplicationLibrary/Views/Profile/ImportProfileView.swift new file mode 100644 index 0000000..0ddcf4d --- /dev/null +++ b/ApplicationLibrary/Views/Profile/ImportProfileView.swift @@ -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 diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift index fe7a4d0..9bcaffe 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -41,8 +41,10 @@ public struct NewProfileView: View { .multilineTextAlignment(.trailing) } Picker(selection: $profileType) { - Text("Local").tag(ProfileType.local) - Text("iCloud").tag(ProfileType.icloud) + #if !os(tvOS) + Text("Local").tag(ProfileType.local) + Text("iCloud").tag(ProfileType.icloud) + #endif Text("Remote").tag(ProfileType.remote) } label: { Text("Type") @@ -54,6 +56,9 @@ public struct NewProfileView: View { } label: { Text("File") } + #if os(tvOS) + .disabled(true) + #endif viewBuilder { if fileImport { HStack { @@ -98,21 +103,23 @@ public struct NewProfileView: View { } .navigationTitle("New Profile") .alertBinding($alert) - .fileImporter( - isPresented: $pickerPresented, - allowedContentTypes: [.json], - allowsMultipleSelection: false - ) { result in - do { - let urls = try result.get() - if !urls.isEmpty { - fileURL = urls[0] + #if os(iOS) || os(macOS) + .fileImporter( + isPresented: $pickerPresented, + allowedContentTypes: [.json], + allowsMultipleSelection: false + ) { result in + do { + let urls = try result.get() + if !urls.isEmpty { + fileURL = urls[0] + } + } catch { + alert = Alert(error) + return } - } catch { - alert = Alert(error) - return } - } + #endif } private func createProfile() async { diff --git a/ApplicationLibrary/Views/Profile/ProfileView.swift b/ApplicationLibrary/Views/Profile/ProfileView.swift index ffdedee..3c40009 100644 --- a/ApplicationLibrary/Views/Profile/ProfileView.swift +++ b/ApplicationLibrary/Views/Profile/ProfileView.swift @@ -1,6 +1,7 @@ import Foundation import Libbox import Library +import Network import SwiftUI public struct ProfileView: View { @@ -16,12 +17,16 @@ public struct ProfileView: View { @State private var alert: Alert? @State private var profileList: [Profile] = [] - #if os(iOS) + #if os(iOS) || os(tvOS) @State private var editMode = EditMode.inactive #elseif os(macOS) @Environment(\.openWindow) private var openWindow #endif + #if os(tvOS) + @Environment(\.devicePickerSupports) private var devicePickerSupports + #endif + @State private var observer: Any? public init() {} @@ -35,7 +40,7 @@ public struct ProfileView: View { } } } else { - #if os(iOS) + #if os(iOS) || os(tvOS) ZStack { if let importRemoteProfileRequest { NavigationLink( @@ -51,16 +56,31 @@ public struct ProfileView: View { ) } FormView { - NavigationLink { - NewProfileView { - Task.detached { - doReload() + #if os(iOS) || os(tvOS) + NavigationLink { + NewProfileView { + 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: { - Text("New Profile").foregroundColor(.accentColor) - } - .disabled(editMode.isEditing) + #endif if profileList.isEmpty { Text("Empty Profiles") } 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) .onDelete(perform: deleteProfile) @@ -194,7 +231,7 @@ public struct ProfileView: View { 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."), primaryButton: .default(Text("Import")) { - #if os(iOS) + #if os(iOS) || os(tvOS) importRemoteProfilePresented = true #elseif os(macOS) openWindow(id: NewProfileView.windowID, value: importRemoteProfileRequest!) diff --git a/ApplicationLibrary/Views/Setting/ServiceLogView.swift b/ApplicationLibrary/Views/Setting/ServiceLogView.swift index d1c906a..9543686 100644 --- a/ApplicationLibrary/Views/Setting/ServiceLogView.swift +++ b/ApplicationLibrary/Views/Setting/ServiceLogView.swift @@ -34,12 +34,15 @@ public struct ServiceLogView: View { } } } + #if !os(tvOS) .toolbar { Button("Export") { fileExporterPresented = true } .disabled(content.isEmpty) } + #endif + #if !os(tvOS) .fileExporter( isPresented: $fileExporterPresented, document: LogDocument(content), @@ -47,10 +50,14 @@ public struct ServiceLogView: View { defaultFilename: "service-log.txt", onCompletion: { _ in } ) + #endif .navigationTitle("Service Log") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif + #if os(tvOS) + .focusable() + #endif } private func loadContent() { @@ -65,25 +72,27 @@ public struct ServiceLogView: View { isLoading = false } - private struct LogDocument: FileDocument { - static var readableContentTypes = [UTType.text] + #if !os(tvOS) + private struct LogDocument: FileDocument { + static var readableContentTypes = [UTType.text] - let content: String + let content: String - init(_ content: String) { - self.content = content - } + init(_ content: String) { + self.content = content + } - init(configuration: ReadConfiguration) throws { - if let data = configuration.file.regularFileContents { - content = String(decoding: data, as: UTF8.self) - } else { - content = "" + init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + content = String(decoding: data, as: UTF8.self) + } else { + content = "" + } + } + + func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: Data(content.utf8)) } } - - func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { - FileWrapper(regularFileWithContents: Data(content.utf8)) - } - } + #endif } diff --git a/ApplicationLibrary/Views/Setting/SettingView.swift b/ApplicationLibrary/Views/Setting/SettingView.swift index 3a2f00d..b480473 100644 --- a/ApplicationLibrary/Views/Setting/SettingView.swift +++ b/ApplicationLibrary/Views/Setting/SettingView.swift @@ -95,7 +95,7 @@ public struct SettingView: View { Section("Core") { FormTextItem("Version", version) FormTextItem("Data Size", dataSize) - #if os(iOS) + #if os(iOS) || os(tvOS) NavigationLink(destination: ServiceLogView()) { Text("View Service Log") } diff --git a/Extension/Info.plist b/Extension/Info.plist index 3059459..a188818 100644 --- a/Extension/Info.plist +++ b/Extension/Info.plist @@ -2,6 +2,10 @@ + UIRequiredDeviceCapabilities + + arm64 + NSExtension NSExtensionPointIdentifier diff --git a/IntentsExtension/Intents.swift b/IntentsExtension/Intents.swift index f8997cc..5e05ef4 100644 --- a/IntentsExtension/Intents.swift +++ b/IntentsExtension/Intents.swift @@ -35,7 +35,7 @@ struct StartServiceIntent: AppIntent { if !profileChanged { return .result() } - try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + try LibboxNewStandaloneCommandClient()!.serviceReload() } else if extensionProfile.status.isConnected { extensionProfile.stop() try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) @@ -62,7 +62,7 @@ struct RestartServiceIntent: AppIntent { return .result() } if extensionProfile.status == .connected { - try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + try LibboxNewStandaloneCommandClient()!.serviceReload() } else if extensionProfile.status.isConnected { extensionProfile.stop() try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) diff --git a/Library/Database/Profile.swift b/Library/Database/Profile.swift index e1c587b..b0cdf77 100644 --- a/Library/Database/Profile.swift +++ b/Library/Database/Profile.swift @@ -1,5 +1,6 @@ import Foundation import GRDB +import Network public class Profile: Record, Identifiable, ObservableObject { public var id: Int64? @@ -15,16 +16,15 @@ public class Profile: Record, Identifiable, ObservableObject { @Published public var autoUpdate: Bool public var lastUpdated: Date? - public init(id: Int64? = nil, name: String, order: UInt32 = 0, type: ProfileType, path: String, remoteURL: String? = nil, 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.name = name self.order = order self.type = type self.path = path self.remoteURL = remoteURL + self.autoUpdate = autoUpdate self.lastUpdated = lastUpdated - - autoUpdate = false super.init() } diff --git a/Library/Discovery/NWSocket.swift b/Library/Discovery/NWSocket.swift new file mode 100644 index 0000000..292c674 --- /dev/null +++ b/Library/Discovery/NWSocket.swift @@ -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! + 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() + } +} diff --git a/Library/Discovery/ProfileServer.swift b/Library/Discovery/ProfileServer.swift new file mode 100644 index 0000000..795ea5b --- /dev/null +++ b/Library/Discovery/ProfileServer.swift @@ -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()) + } + } +} diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index e86d8a5..6cd273a 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -3,6 +3,8 @@ import Libbox import NetworkExtension open class ExtensionProvider: NEPacketTunnelProvider { + public static let errorFile = FilePath.workingDirectory.appendingPathComponent("network_extension_error") + public var username: String? = nil private var commandServer: LibboxCommandServer! private var boxService: LibboxBoxService! @@ -10,6 +12,8 @@ open class ExtensionProvider: NEPacketTunnelProvider { override open func startTunnel(options _: [String: NSObject]?) async throws { NSLog("Here I am") + try? FileManager.default.removeItem(at: ExtensionProvider.errorFile) + do { try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true) } catch { @@ -19,13 +23,17 @@ open class ExtensionProvider: NEPacketTunnelProvider { if let username { 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 { writeFatalError("(packet-tunnel) error: setup service: \(error.localizedDescription)") return } } 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? @@ -36,7 +44,7 @@ open class ExtensionProvider: NEPacketTunnelProvider { LibboxSetMemoryLimit(!SharedPreferences.disableMemoryLimit) - commandServer = LibboxNewCommandServer(FilePath.sharedDirectory.relativePath, serverInterface(self), Int32(SharedPreferences.maxLogLines)) + commandServer = LibboxNewCommandServer(serverInterface(self), Int32(SharedPreferences.maxLogLines)) do { try commandServer.start() } catch { @@ -58,19 +66,13 @@ open class ExtensionProvider: NEPacketTunnelProvider { private func writeError(_ message: String) { writeMessage(message) - #if os(iOS) - ServiceNotification.postServiceNotification(title: "Service Error", message: message) - #else - if Variant.useSystemExtension { - NSLog(message) - } else { - displayMessage(message) { _ in - } - } - #endif + try? message.write(to: ExtensionProvider.errorFile, atomically: true, encoding: .utf8) } public func writeFatalError(_ message: String) { + #if DEBUG + NSLog(message) + #endif writeError(message) cancelTunnelWithError(NSError(domain: message, code: 0)) } @@ -91,7 +93,7 @@ open class ExtensionProvider: NEPacketTunnelProvider { do { configContent = try profile.read() } catch { - writeFatalError("(packet-tunnel) error: read config file: \(error.localizedDescription)") + writeFatalError("(packet-tunnel) error: read config file \(profile.path): \(error.localizedDescription)") return } var error: NSError? diff --git a/Library/Shared/FilePath.swift b/Library/Shared/FilePath.swift index 412fd28..8ec982c 100644 --- a/Library/Shared/FilePath.swift +++ b/Library/Shared/FilePath.swift @@ -7,25 +7,42 @@ public enum FilePath { public extension FilePath { 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 { - FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: FilePath.groupName)! - } - - static var cacheDirectory: URL { - sharedDirectory + #if os(iOS) + static let sharedDirectory = defaultSharedDirectory! + #elseif os(tvOS) + static let sharedDirectory = defaultSharedDirectory .appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true) - } + #elseif os(macOS) + static var sharedDirectory: URL! = defaultSharedDirectory + #endif - static var workingDirectory: URL { - cacheDirectory.appendingPathComponent("Working", isDirectory: true) - } + #if os(iOS) + 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 { - FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true) - } + #if os(macOS) + 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 { diff --git a/Library/Shared/ServiceNotification.swift b/Library/Shared/ServiceNotification.swift deleted file mode 100644 index 2c0107f..0000000 --- a/Library/Shared/ServiceNotification.swift +++ /dev/null @@ -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] - } - } - } -} diff --git a/Library/Shared/Variant.swift b/Library/Shared/Variant.swift index e670a49..a894d12 100644 --- a/Library/Shared/Variant.swift +++ b/Library/Shared/Variant.swift @@ -11,5 +11,7 @@ public enum Variant { public static let applicationName = "SFI" #elseif os(macOS) public static let applicationName = "SFM" + #elseif os(tvOS) + public static let applicationName = "SFT" #endif } diff --git a/MacLibrary/ApplicationDelegate.swift b/MacLibrary/ApplicationDelegate.swift index 02e0f73..c539ca6 100644 --- a/MacLibrary/ApplicationDelegate.swift +++ b/MacLibrary/ApplicationDelegate.swift @@ -7,7 +7,7 @@ import Library open class ApplicationDelegate: NSObject, NSApplicationDelegate { public func applicationDidFinishLaunching(_: Notification) { 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 launchedAsLogInItem = event?.eventID == kAEOpenApplication && diff --git a/MacLibrary/MainView.swift b/MacLibrary/MainView.swift index cbb9b2b..a7f861f 100644 --- a/MacLibrary/MainView.swift +++ b/MacLibrary/MainView.swift @@ -43,14 +43,6 @@ public struct MainView: View { } #endif .alertBinding($alert) - .onAppear { - ServiceNotification.setServiceNotificationListener { notification in - alert = Alert(title: Text(notification.title), message: Text(notification.body)) - } - } - .onDisappear { - ServiceNotification.removeServiceNotificationListener() - } .toolbar { ToolbarItem(placement: .navigation) { StartStopButton() diff --git a/MacLibrary/MenuView.swift b/MacLibrary/MenuView.swift index 0717e21..7d062bd 100644 --- a/MacLibrary/MenuView.swift +++ b/MacLibrary/MenuView.swift @@ -191,7 +191,7 @@ public struct MenuView: View { NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil) if profile.status.isConnected { do { - try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload() + try LibboxNewStandaloneCommandClient()?.serviceReload() } catch { alert = Alert(error) } diff --git a/README.md b/README.md index 9e87c4b..eaae019 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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 diff --git a/SFI/ApplicationDelegate.swift b/SFI/ApplicationDelegate.swift index d79a1b6..f15af5c 100644 --- a/SFI/ApplicationDelegate.swift +++ b/SFI/ApplicationDelegate.swift @@ -1,12 +1,16 @@ import ApplicationLibrary import Foundation +import Libbox import Library +import Network import UIKit class ApplicationDelegate: NSObject, UIApplicationDelegate { + private var profileServer: ProfileServer? + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { NSLog("Here I stand") - ServiceNotification.register() + LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, false) Task.detached { do { try await UIProfileUpdateTask.setup() @@ -18,9 +22,25 @@ class ApplicationDelegate: NSObject, UIApplicationDelegate { Task.detached { await self.requestNetworkPermission() } + if #available(iOS 16.0, *) { + Task.detached { + await self.setupProfileServer() + } + } 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() { if UIDevice.current.userInterfaceIdiom != .phone { return diff --git a/SFI/Info.plist b/SFI/Info.plist index 8836d9a..ca8e00b 100644 --- a/SFI/Info.plist +++ b/SFI/Info.plist @@ -2,6 +2,16 @@ + NSApplicationServices + + Advertises + + + NSApplicationServiceIdentifier + sing-box:profile + + + BGTaskSchedulerPermittedIdentifiers io.nekohasekai.sfa.update_profiles diff --git a/SFI/MainView.swift b/SFI/MainView.swift index 1f2c242..e848f66 100644 --- a/SFI/MainView.swift +++ b/SFI/MainView.swift @@ -10,11 +10,6 @@ struct MainView: View { @State private var extensionProfile: ExtensionProfile? @State private var profileLoading = true @State private var logClient: LogClient! - - @State private var serviceNotificationTitle = "" - @State private var serviceNotificationContent = "" - @State private var serviceNotificationPresented = false - @State private var importRemoteProfile: LibboxImportRemoteProfile? var body: some View { @@ -30,23 +25,6 @@ struct MainView: View { ContentView() } } - .alert(isPresented: $serviceNotificationPresented, content: { - Alert( - title: Text(serviceNotificationTitle), - message: Text(serviceNotificationContent), - dismissButton: .default(Text("Ok")) - ) - }) - .onAppear { - ServiceNotification.setServiceNotificationListener { notification in - serviceNotificationTitle = notification.title - serviceNotificationContent = notification.body - serviceNotificationPresented = true - } - } - .onDisappear { - ServiceNotification.removeServiceNotificationListener() - } .onChange(of: scenePhase, perform: { newValue in if newValue == .active { Task.detached { diff --git a/SFT/Application.swift b/SFT/Application.swift new file mode 100644 index 0000000..4d6348e --- /dev/null +++ b/SFT/Application.swift @@ -0,0 +1,12 @@ +import SwiftUI + +@main +struct Application: App { + @UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate + + var body: some Scene { + WindowGroup { + MainView() + } + } +} diff --git a/SFT/ApplicationDelegate.swift b/SFT/ApplicationDelegate.swift new file mode 100644 index 0000000..00c51e3 --- /dev/null +++ b/SFT/ApplicationDelegate.swift @@ -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 + } +} diff --git a/SFT/Assets.xcassets/AccentColor.colorset/Contents.json b/SFT/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/SFT/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..997b20e --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tv - App Icon - AppStore - Back -1280 x 768 pt.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/tv - App Icon - AppStore - Back -1280 x 768 pt.png b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/tv - App Icon - AppStore - Back -1280 x 768 pt.png new file mode 100644 index 0000000..b5a0eab Binary files /dev/null and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/tv - App Icon - AppStore - Back -1280 x 768 pt.png differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..2e00335 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..a381da2 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tv - App Icon - AppStore - Front -1280 x 768 pt.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/tv - App Icon - AppStore - Front -1280 x 768 pt.png b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/tv - App Icon - AppStore - Front -1280 x 768 pt.png new file mode 100644 index 0000000..04e7758 Binary files /dev/null and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/tv - App Icon - AppStore - Front -1280 x 768 pt.png differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..9be4df7 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -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 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/tv- App Icon Small - Back - 400 x 240 pt.png b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/tv- App Icon Small - Back - 400 x 240 pt.png new file mode 100644 index 0000000..758b93c Binary files /dev/null and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/tv- App Icon Small - Back - 400 x 240 pt.png differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..795cce1 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..e7d7bcd --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -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 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/tv- App Icon Small - Front - 400 x 240 pt.png b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/tv- App Icon Small - Front - 400 x 240 pt.png new file mode 100644 index 0000000..b3c7871 Binary files /dev/null and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/tv- App Icon Small - Front - 400 x 240 pt.png differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 0000000..f47ba43 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -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 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 0000000..795cce1 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 0000000..795cce1 --- /dev/null +++ b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/Assets.xcassets/Contents.json b/SFT/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SFT/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SFT/ContentView.swift b/SFT/ContentView.swift new file mode 100644 index 0000000..98f5449 --- /dev/null +++ b/SFT/ContentView.swift @@ -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 } + } + } + } + } +} diff --git a/SFT/Info.plist b/SFT/Info.plist new file mode 100644 index 0000000..1020a15 --- /dev/null +++ b/SFT/Info.plist @@ -0,0 +1,52 @@ + + + + + UIRequiredDeviceCapabilities + + arm64 + + NSApplicationServices + + Browses + + + NSApplicationServiceIdentifier + sing-box:profile + NSApplicationServiceUsageDescription + Import sing-box profile from other devices + NSApplicationServicePlatformSupport + + iOS + iPadOS + + + + + BGTaskSchedulerPermittedIdentifiers + + io.nekohasekai.sfa.update_profiles + + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLIconFile + AppIcon.icns + CFBundleURLName + sing-box + CFBundleURLSchemes + + sing-box + + + + ITSAppUsesNonExemptEncryption + + UIBackgroundModes + + fetch + + + diff --git a/SFT/MainView.swift b/SFT/MainView.swift new file mode 100644 index 0000000..6095b6d --- /dev/null +++ b/SFT/MainView.swift @@ -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() + } + } +} diff --git a/SFT/SFT.entitlements b/SFT/SFT.entitlements new file mode 100644 index 0000000..2468f0c --- /dev/null +++ b/SFT/SFT.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.application-groups + + group.io.nekohasekai.sfa + + + diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index c97cb56..93b26a1 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -48,6 +48,12 @@ 3A4EAD362A4FEB9C005435B3 /* ProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */; }; 3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; }; 3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */; }; + 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 */; }; 3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */; }; 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 */; }; 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 */; }; + 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, ); }; }; 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 */; }; + 3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC8CF9A2A736C750002AF3C /* ImportProfileView.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 */; }; 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 */; }; @@ -154,6 +165,27 @@ remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3; 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 */ = { isa = PBXContainerItemProxy; containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; @@ -270,6 +302,28 @@ name = "Embed Foundation Extensions"; 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 */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -373,6 +427,10 @@ 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApplicationLibrary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3A4EAD202A4FEB3C005435B3 /* ApplicationLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationLibrary.swift; sourceTree = ""; }; 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+isConnected.swift"; sourceTree = ""; }; + 3A4FB1642A73568E007012B9 /* SFT.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SFT.entitlements; sourceTree = ""; }; + 3A4FB1652A73574B007012B9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3A4FB1672A7358C9007012B9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; + 3A4FB1692A735AC9007012B9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIProfileUpdateTask.swift; sourceTree = ""; }; 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdateTask.swift; sourceTree = ""; }; 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Date.swift"; sourceTree = ""; }; @@ -397,11 +455,17 @@ 3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3AB1220A2A70FD500087CD55 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; + 3AC03B9A2A72BF3300B7946F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 3AC03B9C2A72BF3500B7946F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = ""; }; 3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCensorship.swift; sourceTree = ""; }; + 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportProfileView.swift; sourceTree = ""; }; 3AD0953C2A70EB310052764E /* Profile+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Share.swift"; sourceTree = ""; }; + 3ADBB4242A7389640041D44F /* ProfileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileServer.swift; sourceTree = ""; }; + 3ADBB4292A73A7060041D44F /* NWSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSocket.swift; sourceTree = ""; }; 3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveDashboardView.swift; sourceTree = ""; }; 3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; 3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; @@ -481,6 +545,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3AC03B932A72BF3300B7946F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A4FB1572A73467F007012B9 /* Library.framework in Frameworks */, + 3A4FB15C2A73468C007012B9 /* ApplicationLibrary.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3AEC20F02A459AB400A63465 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -619,6 +692,29 @@ path = Setting; sourceTree = ""; }; + 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 = ""; + }; + 3ADBB4262A739DD00041D44F /* Discovery */ = { + isa = PBXGroup; + children = ( + 3ADBB4242A7389640041D44F /* ProfileServer.swift */, + 3ADBB4292A73A7060041D44F /* NWSocket.swift */, + ); + path = Discovery; + sourceTree = ""; + }; 3ADF8DF52A4AFA8E00900CC8 /* Profile */ = { isa = PBXGroup; children = ( @@ -627,6 +723,7 @@ 3ADF8DFC2A4B096000900CC8 /* EditProfileWindowView.swift */, 3ADF8E002A4B0F6300900CC8 /* EditProfileView.swift */, 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */, + 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */, ); path = Profile; sourceTree = ""; @@ -638,6 +735,7 @@ 3AEC20F42A459AB400A63465 /* SFI */, 3AEC210A2A459B1900A63465 /* SFM */, 3AEECC052A6DF9CA006A0E0C /* SFM.System */, + 3AC03B972A72BF3300B7946F /* SFT */, 3AEC211E2A459B4700A63465 /* Library */, 3A4EAD112A4FEAE6005435B3 /* ApplicationLibrary */, 3AEECC302A6DFDAD006A0E0C /* MacLibrary */, @@ -661,6 +759,7 @@ 3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfa.system.systemextension */, 3AEECC042A6DF9CA006A0E0C /* SFM.app */, 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */, + 3AC03B962A72BF3300B7946F /* sing-box.app */, ); name = Products; sourceTree = ""; @@ -703,6 +802,7 @@ 3AEC211E2A459B4700A63465 /* Library */ = { isa = PBXGroup; children = ( + 3ADBB4262A739DD00041D44F /* Discovery */, 3AEC21462A45A9CE00A63465 /* Shared */, 3AEC21432A45A92B00A63465 /* Network */, 3AEC213A2A459FD200A63465 /* Database */, @@ -746,7 +846,6 @@ 3AEC21462A45A9CE00A63465 /* Shared */ = { isa = PBXGroup; children = ( - 3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */, 3AEC21472A45A9DE00A63465 /* Bundle+Version.swift */, 3AEC214B2A45AA8E00A63465 /* FilePath.swift */, 3A2223552A6E1BDE00C50B23 /* Variant.swift */, @@ -914,6 +1013,28 @@ productReference = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; 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 */ = { isa = PBXNativeTarget; buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */; @@ -1076,6 +1197,9 @@ 3A77016C2A4E6B34008F031F = { CreatedOnToolsVersion = 15.0; }; + 3AC03B952A72BF3300B7946F = { + CreatedOnToolsVersion = 14.3.1; + }; 3AEC20F22A459AB400A63465 = { CreatedOnToolsVersion = 15.0; }; @@ -1121,6 +1245,7 @@ 3AEC20F22A459AB400A63465 /* SFI */, 3AEC21082A459B1900A63465 /* SFM */, 3AEECC032A6DF9CA006A0E0C /* SFM.System */, + 3AC03B952A72BF3300B7946F /* SFT */, 3AEC211C2A459B4700A63465 /* Library */, 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */, 3AEECC2E2A6DFDAD006A0E0C /* MacLibrary */, @@ -1139,6 +1264,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3AC03B942A72BF3300B7946F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AC03B9D2A72BF3500B7946F /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3AEC20F12A459AB400A63465 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1210,6 +1343,7 @@ 3A4EAD2D2A4FEB77005435B3 /* ProfileView.swift in Sources */, 3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */, 3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */, + 3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */, 3A4EAD292A4FEB6D005435B3 /* Formtem.swift in Sources */, 3A4EAD302A4FEB77005435B3 /* NewProfileView.swift in Sources */, 3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */, @@ -1247,6 +1381,17 @@ ); 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 */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1284,11 +1429,12 @@ 3AEC21452A45A93800A63465 /* HTTPClient.swift in Sources */, 3AEC213C2A459FDF00A63465 /* Databse.swift in Sources */, 3A4EAD3C2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift in Sources */, + 3ADBB4252A7389640041D44F /* ProfileServer.swift in Sources */, 3AEC21422A45A8FF00A63465 /* Profile+Update.swift in Sources */, 3A22235A2A6E212A00C50B23 /* SystemExtension.swift in Sources */, 3AE4D0B42A6E2BA3009FEA9E /* Extension+Iterator.swift in Sources */, 3AEC214C2A45AA8E00A63465 /* FilePath.swift in Sources */, - 3AC194502A502DFE00BD8CB9 /* ServiceNotification.swift in Sources */, + 3ADBB42A2A73A7060041D44F /* NWSocket.swift in Sources */, 3AE4D0B22A6E2B6A009FEA9E /* ExtensionPlatformInterface.swift in Sources */, 3AEC21402A45A28F00A63465 /* ProfileManager.swift in Sources */, 3A9144D92A46AE370036E9AD /* ShadredPreferences+Database.swift in Sources */, @@ -1355,6 +1501,21 @@ target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */; 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 */ = { isa = PBXTargetDependency; target = 3AEC211C2A459B4700A63465 /* Library */; @@ -1446,20 +1607,19 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; OTHER_CODE_SIGN_FLAGS = ""; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; }; name = Debug; }; @@ -1486,19 +1646,18 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; OTHER_CODE_SIGN_FLAGS = ""; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.extension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; }; name = Release; }; @@ -1534,11 +1693,14 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; 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_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 17.0; }; name = Debug; }; @@ -1574,10 +1736,13 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; 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_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 17.0; }; name = Release; }; @@ -1604,7 +1769,6 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; OTHER_CODE_SIGN_FLAGS = ""; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1644,7 +1808,6 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; OTHER_CODE_SIGN_FLAGS = ""; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.intents; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1662,6 +1825,75 @@ }; 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 */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1820,7 +2052,6 @@ ); MARKETING_VERSION = 1.3.5; OTHER_CODE_SIGN_FLAGS = "--deep"; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1861,7 +2092,6 @@ ); MARKETING_VERSION = 1.3.5; OTHER_CODE_SIGN_FLAGS = "--deep"; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1955,6 +2185,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; @@ -1963,6 +2194,7 @@ DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=appletvos*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1984,20 +2216,20 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; OTHER_CODE_SIGN_FLAGS = "--deep"; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 17.0; }; name = Debug; }; @@ -2007,6 +2239,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; @@ -2015,6 +2248,7 @@ DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=appletvos*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Z56Z6NYZN2; "DEVELOPMENT_TEAM[sdk=macosx*]" = Z56Z6NYZN2; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2036,19 +2270,19 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; OTHER_CODE_SIGN_FLAGS = "--deep"; - OTHER_LDFLAGS = "-ld64"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.library; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 17.0; }; name = Release; }; @@ -2309,6 +2543,15 @@ defaultConfigurationIsVisible = 0; 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" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist b/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist index 03cfbf9..e96a579 100644 --- a/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist @@ -69,7 +69,7 @@ MacLibrary.xcscheme_^#shared#^_ orderHint - 2 + 6 MessageExtension.xcscheme_^#shared#^_ @@ -81,14 +81,14 @@ isShown orderHint - 11 + 12 MyPlayground (Playground) 2.xcscheme isShown orderHint - 12 + 13 MyPlayground (Playground) 3.xcscheme @@ -116,7 +116,7 @@ isShown orderHint - 10 + 11 SFA.xcscheme_^#shared#^_ @@ -131,17 +131,22 @@ SFM.System.xcscheme_^#shared#^_ orderHint - 4 + 3 SFM.xcscheme orderHint 1 + SFT.xcscheme_^#shared#^_ + + orderHint + 4 + SystemExtension.xcscheme_^#shared#^_ orderHint - 3 + 2 Test.xcscheme_^#shared#^_ @@ -153,14 +158,14 @@ isShown orderHint - 8 + 15 Tour (Playground) 2.xcscheme isShown orderHint - 9 + 16 Tour (Playground) 3.xcscheme @@ -188,21 +193,21 @@ isShown orderHint - 7 + 14 TransactionObserver (Playground) 1.xcscheme isShown orderHint - 15 + 9 TransactionObserver (Playground) 2.xcscheme isShown orderHint - 16 + 10 TransactionObserver (Playground) 3.xcscheme @@ -230,7 +235,7 @@ isShown orderHint - 14 + 8 mactest.xcscheme_^#shared#^_