From bb0385f53c6c9173b873b906e7cdb71626a0856c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 29 Jul 2023 09:44:17 +0800 Subject: [PATCH] Add support for tvOS --- .../Service/UIProfileUpdateTask.swift | 2 +- .../Views/Abstract/Formtem.swift | 6 +- .../Views/Dashboard/ActiveDashboardView.swift | 70 +++-- .../Views/Dashboard/ExtensionStatusView.swift | 14 +- .../Views/Dashboard/StartStopButton.swift | 6 +- .../Views/Groups/GroupItemView.swift | 18 +- .../Views/Groups/GroupView.swift | 2 +- ApplicationLibrary/Views/Log/LogView.swift | 7 + ApplicationLibrary/Views/NavigationPage.swift | 4 + .../Profile/EditProfileContentView.swift | 255 +++++++-------- .../Views/Profile/EditProfileView.swift | 48 +-- .../Views/Profile/ImportProfileView.swift | 164 ++++++++++ .../Views/Profile/NewProfileView.swift | 37 ++- .../Views/Profile/ProfileView.swift | 59 +++- .../Views/Setting/ServiceLogView.swift | 41 ++- .../Views/Setting/SettingView.swift | 2 +- Extension/Info.plist | 4 + IntentsExtension/Intents.swift | 4 +- Library/Database/Profile.swift | 6 +- Library/Discovery/NWSocket.swift | 63 ++++ Library/Discovery/ProfileServer.swift | 128 ++++++++ Library/Network/ExtensionProvider.swift | 30 +- Library/Shared/FilePath.swift | 45 ++- Library/Shared/ServiceNotification.swift | 45 --- Library/Shared/Variant.swift | 2 + MacLibrary/ApplicationDelegate.swift | 2 +- MacLibrary/MainView.swift | 8 - MacLibrary/MenuView.swift | 2 +- README.md | 2 +- SFI/ApplicationDelegate.swift | 22 +- SFI/Info.plist | 10 + SFI/MainView.swift | 22 -- SFT/Application.swift | 12 + SFT/ApplicationDelegate.swift | 21 ++ .../AccentColor.colorset/Contents.json | 11 + .../Content.imageset/Contents.json | 12 + ... Icon - AppStore - Back -1280 x 768 pt.png | Bin 0 -> 12763 bytes .../Back.imagestacklayer/Contents.json | 6 + .../Contents.json | 17 + .../Content.imageset/Contents.json | 11 + .../Front.imagestacklayer/Contents.json | 6 + .../Content.imageset/Contents.json | 12 + ...Icon - AppStore - Front -1280 x 768 pt.png | Bin 0 -> 40351 bytes .../Middle.imagestacklayer/Contents.json | 6 + .../Content.imageset/Contents.json | 17 + ...- App Icon Small - Back - 400 x 240 pt.png | Bin 0 -> 7871 bytes .../Back.imagestacklayer/Contents.json | 6 + .../App Icon.imagestack/Contents.json | 17 + .../Content.imageset/Contents.json | 16 + .../Front.imagestacklayer/Contents.json | 6 + .../Content.imageset/Contents.json | 17 + ... App Icon Small - Front - 400 x 240 pt.png | Bin 0 -> 15382 bytes .../Middle.imagestacklayer/Contents.json | 6 + .../Contents.json | 32 ++ .../Contents.json | 16 + .../Top Shelf Image.imageset/Contents.json | 16 + SFT/Assets.xcassets/Contents.json | 6 + SFT/ContentView.swift | 63 ++++ SFT/Info.plist | 52 ++++ SFT/MainView.swift | 83 +++++ SFT/SFT.entitlements | 14 + sing-box.xcodeproj/project.pbxproj | 291 ++++++++++++++++-- .../xcschemes/xcschememanagement.plist | 29 +- 63 files changed, 1542 insertions(+), 389 deletions(-) create mode 100644 ApplicationLibrary/Views/Profile/ImportProfileView.swift create mode 100644 Library/Discovery/NWSocket.swift create mode 100644 Library/Discovery/ProfileServer.swift delete mode 100644 Library/Shared/ServiceNotification.swift create mode 100644 SFT/Application.swift create mode 100644 SFT/ApplicationDelegate.swift create mode 100644 SFT/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json create mode 100644 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 create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json create mode 100644 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 create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json create mode 100644 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 create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json create mode 100644 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 create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json create mode 100644 SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json create mode 100644 SFT/Assets.xcassets/Contents.json create mode 100644 SFT/ContentView.swift create mode 100644 SFT/Info.plist create mode 100644 SFT/MainView.swift create mode 100644 SFT/SFT.entitlements 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 0000000000000000000000000000000000000000..b5a0eab0e192f0ed529fac00f914a63203657b68 GIT binary patch literal 12763 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..04e77582cefce242149294c99fd1ce1f3b1c0077 GIT binary patch literal 40351 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 CWIAd9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..758b93c5df84e3dbad8746c4f335f22fa11e2b49 GIT binary patch literal 7871 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 + + + + 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#^_