diff --git a/ApplicationLibrary/Service/UIProfileUpdateTask.swift b/ApplicationLibrary/Service/UIProfileUpdateTask.swift index ea2269d..4fde2a7 100644 --- a/ApplicationLibrary/Service/UIProfileUpdateTask.swift +++ b/ApplicationLibrary/Service/UIProfileUpdateTask.swift @@ -9,21 +9,21 @@ import Library public class UIProfileUpdateTask: BGAppRefreshTask { private static let taskSchedulerPermittedIdentifier = "\(FilePath.packageName).update_profiles" - private actor Register { - private var registered = false - func configure() async throws { - if !registered { - let success = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskSchedulerPermittedIdentifier, using: nil) { task in - NSLog("profile update task started") - Task { - await UIProfileUpdateTask.getAndUpdateProfiles(task) - } + private static var registered = false + public static func configure() throws { + if !registered { + let success = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskSchedulerPermittedIdentifier, using: nil) { task in + NSLog("profile update task started") + Task { + await UIProfileUpdateTask.getAndUpdateProfiles(task) } - if !success { - throw NSError(domain: "register failed", code: 0) - } - registered = true } + if !success { + throw NSError(domain: "register task failed", code: 0) + } + registered = true + } + Task { BGTaskScheduler.shared.cancelAllTaskRequests() let profiles = try await ProfileManager.listAutoUpdateEnabled() if profiles.isEmpty { @@ -31,13 +31,8 @@ import Library } try scheduleUpdate(ProfileUpdateTask.calculateEarliestBeginDate(profiles)) } - } - - private static let register = Register() - public static func configure() async throws { - try await register.configure() - if await UIApplication.shared.backgroundRefreshStatus != .available { - Task { + Task { + if await UIApplication.shared.backgroundRefreshStatus != .available { await updateOnce() } } diff --git a/ApplicationLibrary/Views/Connections/Connection.swift b/ApplicationLibrary/Views/Connections/Connection.swift new file mode 100644 index 0000000..2047597 --- /dev/null +++ b/ApplicationLibrary/Views/Connections/Connection.swift @@ -0,0 +1,74 @@ +import Foundation + +public struct Connection: Codable { + public let id: String + public let inbound: String + public let inboundType: String + public let ipVersion: Int32 + public let network: String + public let source: String + public let destination: String + public let domain: String + public let displayDestination: String + public let protocolName: String + public let user: String + public let fromOutbound: String + public let createdAt: Date + public let closedAt: Date? + public var upload: Int64 + public var download: Int64 + public var uploadTotal: Int64 + public var downloadTotal: Int64 + public let rule: String + public let outbound: String + public let outboundType: String + public let chain: [String] + + var hashValue: Int { + var value = id.hashValue + (value, _) = value.addingReportingOverflow(upload.hashValue) + (value, _) = value.addingReportingOverflow(download.hashValue) + (value, _) = value.addingReportingOverflow(uploadTotal.hashValue) + (value, _) = value.addingReportingOverflow(downloadTotal.hashValue) + return value + } + + func performSearch(_ content: String) -> Bool { + for item in content.components(separatedBy: " ") { + let itemSep = item.components(separatedBy: ":") + if itemSep.count == 2 { + if !performSearchType(type: itemSep[0], value: itemSep[1]) { + return false + } + continue + } + if !performSearchPlain(item) { + return false + } + } + return true + } + + private func performSearchPlain(_ content: String) -> Bool { + destination.contains(content) || + domain.contains(content) + } + + private func performSearchType(type: String, value: String) -> Bool { + switch type { + // TODO: impl more + case "network": + return network == value + case "inbound": + return inbound.contains(value) + case "inbound.type": + return inboundType == value + case "source": + return source.contains(value) + case "destination": + return destination.contains(value) + default: + return false + } + } +} diff --git a/ApplicationLibrary/Views/Connections/ConnectionDetailsView.swift b/ApplicationLibrary/Views/Connections/ConnectionDetailsView.swift new file mode 100644 index 0000000..6a68aae --- /dev/null +++ b/ApplicationLibrary/Views/Connections/ConnectionDetailsView.swift @@ -0,0 +1,56 @@ +import Foundation +import Libbox +import SwiftUI + +public struct ConnectionDetailsView: View { + private let connection: Connection + public init(_ connection: Connection) { + self.connection = connection + } + + public var body: some View { + FormView { + if connection.closedAt != nil { + FormTextItem("State", "Closed") + FormTextItem("Created At", connection.createdAt.myFormat) + } else { + FormTextItem("State", "Active") + FormTextItem("Created At", connection.createdAt.myFormat) + } + if let closedAt = connection.closedAt { + FormTextItem("Closed At", closedAt.myFormat) + } + FormTextItem("Upload", LibboxFormatBytes(connection.uploadTotal)) + FormTextItem("Download", LibboxFormatBytes(connection.downloadTotal)) + Section("Metadata") { + FormTextItem("Inbound", connection.inbound) + FormTextItem("Inbound Type", connection.inboundType) + FormTextItem("IP Version", "\(connection.ipVersion)") + FormTextItem("Network", connection.network.uppercased()) + FormTextItem("Source", connection.source) + FormTextItem("Destination", connection.destination) + if !connection.domain.isEmpty { + FormTextItem("Domain", connection.domain) + } + if !connection.protocolName.isEmpty { + FormTextItem("Protocol", connection.protocolName) + } + if !connection.user.isEmpty { + FormTextItem("User", connection.user) + } + if !connection.fromOutbound.isEmpty { + FormTextItem("From Outbound", connection.fromOutbound) + } + if !connection.rule.isEmpty { + FormTextItem("Match Rule", connection.rule) + } + FormTextItem("Outbound", connection.outbound) + FormTextItem("Outbound Type", connection.outboundType) + if connection.chain.count > 1 { + FormTextItem("Chain", connection.chain.reversed().joined(separator: "/")) + } + } + } + .navigationTitle("Connection") + } +} diff --git a/ApplicationLibrary/Views/Connections/ConnectionListPage.swift b/ApplicationLibrary/Views/Connections/ConnectionListPage.swift new file mode 100644 index 0000000..01c26c4 --- /dev/null +++ b/ApplicationLibrary/Views/Connections/ConnectionListPage.swift @@ -0,0 +1,31 @@ +import Foundation +import SwiftUI + +public enum ConnectionListPage: Int, CaseIterable, Identifiable { + public var id: Self { + self + } + + case active + case closed +} + +public extension ConnectionListPage { + var title: String { + switch self { + case .active: + return NSLocalizedString("Active", comment: "") + case .closed: + return NSLocalizedString("Closed", comment: "") + } + } + + var label: some View { + switch self { + case .active: + return Label(title, systemImage: "play.fill") + case .closed: + return Label(title, systemImage: "stop.fill") + } + } +} diff --git a/ApplicationLibrary/Views/Connections/ConnectionListView.swift b/ApplicationLibrary/Views/Connections/ConnectionListView.swift new file mode 100644 index 0000000..661b12b --- /dev/null +++ b/ApplicationLibrary/Views/Connections/ConnectionListView.swift @@ -0,0 +1,158 @@ +import Libbox +import Library +import SwiftUI + +@MainActor +public struct ConnectionListView: View { + @Environment(\.scenePhase) private var scenePhase + @State private var isLoading = true + @StateObject private var commandClient = CommandClient(.connections) + @State private var connections: [Connection] = [] + @State private var selection: ConnectionListPage = .active + @State private var searchText = "" + @State private var alert: Alert? + + public init() {} + public var body: some View { + VStack { + if isLoading { + Text("Loading...") + } else { + if connections.isEmpty { + Text("Empty connections") + } else { + ScrollView { + LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading) { + ForEach(connections.filter { it in + searchText == "" || it.performSearch(searchText) + }, id: \.hashValue) { it in + ConnectionView(it) + } + } + } + .padding() + } + } + } + .toolbar { + ToolbarItem { + Menu { + Picker("State", selection: $commandClient.connectionStateFilter) { + ForEach(ConnectionStateFilter.allCases) { state in + Text(state.name) + } + } + + Picker("Sort By", selection: $commandClient.connectionSort) { + ForEach(ConnectionSort.allCases, id: \.self) { sortBy in + Text(sortBy.name) + } + } + + Button("Close All Connections", role: .destructive) { + do { + try LibboxNewStandaloneCommandClient()!.closeConnections() + } catch { + alert = Alert(error) + } + } + } label: { + Label("Filter", systemImage: "line.3.horizontal.circle") + } + } + } + .searchable(text: $searchText) + .alertBinding($alert) + .onAppear { + connect() + } + .onDisappear { + commandClient.disconnect() + } + .onChangeCompat(of: scenePhase) { newValue in + if newValue == .active { + commandClient.connect() + } else { + commandClient.disconnect() + } + } + .onChangeCompat(of: commandClient.connectionStateFilter) { it in + commandClient.filterConnectionsNow() + Task { + await SharedPreferences.connectionStateFilter.set(it.rawValue) + } + } + .onChangeCompat(of: commandClient.connectionSort) { it in + commandClient.filterConnectionsNow() + Task { + await SharedPreferences.connectionSort.set(it.rawValue) + } + } + .onReceive(commandClient.$connections, perform: { connections in + if let connections { + self.connections = convertConnections(connections) + isLoading = false + } + }) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + #if os(iOS) + .background(Color(uiColor: .systemGroupedBackground)) + #endif + } + + private var backgroundColor: Color { + #if os(iOS) + return Color(uiColor: .secondarySystemGroupedBackground) + #elseif os(macOS) + return Color(nsColor: .textBackgroundColor) + #elseif os(tvOS) + return Color(uiColor: .black) + #endif + } + + private func connect() { + if ApplicationLibrary.inPreview { + isLoading = false + } else { + commandClient.connect() + } + } + + private func convertConnections(_ goConnections: [LibboxConnection]) -> [Connection] { + var connections = [Connection]() + for goConnection in goConnections { + if goConnection.outboundType == "dns" { + continue + } + var closedAt: Date? + if goConnection.closedAt > 0 { + closedAt = Date(timeIntervalSince1970: Double(goConnection.closedAt) / 1000) + } + connections.append(Connection( + id: goConnection.id_, + inbound: goConnection.inbound, + inboundType: goConnection.inboundType, + ipVersion: goConnection.ipVersion, + network: goConnection.network, + source: goConnection.source, + destination: goConnection.destination, + domain: goConnection.domain, + displayDestination: goConnection.displayDestination(), + protocolName: goConnection.protocol, + user: goConnection.user, + fromOutbound: goConnection.fromOutbound, + createdAt: Date(timeIntervalSince1970: Double(goConnection.createdAt) / 1000), + closedAt: closedAt, + upload: goConnection.uplink, + download: goConnection.downlink, + uploadTotal: goConnection.uplinkTotal, + downloadTotal: goConnection.downlinkTotal, + rule: goConnection.rule, + outbound: goConnection.outbound, + outboundType: goConnection.outboundType, + chain: goConnection.chain()!.toArray() + )) + } + return connections + } +} diff --git a/ApplicationLibrary/Views/Connections/ConnectionView.swift b/ApplicationLibrary/Views/Connections/ConnectionView.swift new file mode 100644 index 0000000..137c289 --- /dev/null +++ b/ApplicationLibrary/Views/Connections/ConnectionView.swift @@ -0,0 +1,121 @@ +import Libbox +import SwiftUI + +@MainActor +public struct ConnectionView: View { + private let connection: Connection + public init(_ connection: Connection) { + self.connection = connection + } + + private func format(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } + + public func formatInterval(_ createdAt: Date, _ closedAt: Date) -> String { + LibboxFormatDuration(Int64((closedAt.timeIntervalSince1970 - createdAt.timeIntervalSince1970) * 1000)) + } + + @State private var alert: Alert? + + public var body: some View { + FormNavigationLink { + ConnectionDetailsView(connection) + } label: { + VStack { + HStack { + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text("\(connection.network.uppercased()) \(connection.displayDestination)") + Spacer() + if connection.closedAt == nil { + Text("Active").foregroundStyle(.green) + } else { + Text("Closed").foregroundStyle(.red) + } + } + .font(.caption2.monospaced().bold()) + .padding([.bottom], 4) + HStack { + if let closedAt = connection.closedAt { + VStack(alignment: .leading) { + Text("↑ \(LibboxFormatBytes(connection.uploadTotal))") + Text("↓ \(LibboxFormatBytes(connection.downloadTotal))") + } + .font(.caption2) + VStack(alignment: .leading) { + Text(format(connection.createdAt)) + Text(formatInterval(connection.createdAt, closedAt)) + } + Spacer() + VStack(alignment: .trailing) { + Text(connection.inboundType + "/" + connection.inbound) + Text(connection.chain.reversed().joined(separator: "/")) + } + } else { + VStack(alignment: .leading) { + Text("↑ \(LibboxFormatBytes(connection.upload))/s") + Text("↓ \(LibboxFormatBytes(connection.download))/s") + } + .font(.caption2) + VStack(alignment: .leading) { + Text(LibboxFormatBytes(connection.uploadTotal)) + Text(LibboxFormatBytes(connection.downloadTotal)) + } + Spacer() + VStack(alignment: .trailing) { + Text(connection.inboundType + "/" + connection.inbound) + Text(connection.chain.reversed().joined(separator: "/")) + } + } + } + .font(.caption2.monospaced()) + } + } + .foregroundColor(.textColor) + #if !os(tvOS) + .padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13)) + .background(backgroundColor) + .cornerRadius(10) + #endif + } + .background(.clear) + } + #if !os(tvOS) + .buttonStyle(.borderless) + #endif + .alertBinding($alert) + .contextMenu { + if connection.closedAt == nil { + Button("Close", role: .destructive) { + Task { + await closeConnection() + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + private var backgroundColor: Color { + #if os(iOS) + return Color(uiColor: .secondarySystemGroupedBackground) + #elseif os(macOS) + return Color(nsColor: .textBackgroundColor) + #elseif os(tvOS) + return Color.black + #endif + } + + private nonisolated func closeConnection() async { + do { + try LibboxNewStandaloneCommandClient()!.closeConnection(connection.id) + } catch { + await MainActor.run { + alert = Alert(error) + } + } + } +} diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index daff14d..84c218f 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -50,23 +50,24 @@ public struct ActiveDashboardView: View { VStack { #if os(iOS) || os(tvOS) if ApplicationLibrary.inPreview || profile.status.isConnectedStrict { - Picker("Page", selection: $selection) { - ForEach(DashboardPage.allCases) { page in - page.label - } + viewBuilder { + #if os(iOS) + if #available(iOS 16.0, *) { + content1 + } else { + content0 + } + #else + content0 + #endif } - .pickerStyle(.segmented) #if os(iOS) - .padding([.leading, .trailing]) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.inline) #endif - TabView(selection: $selection) { - ForEach(DashboardPage.allCases) { page in - page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled) - .tag(page) - } + .onAppear { + UIScrollView.appearance().isScrollEnabled = false } - .tabViewStyle(.page(indexDisplayMode: .always)) + .tabViewStyle(.page(indexDisplayMode: .never)) } else { OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled) } @@ -90,6 +91,46 @@ public struct ActiveDashboardView: View { .alertBinding($alert) } + @ViewBuilder + private var content0: some View { + Picker("Page", selection: $selection) { + ForEach(DashboardPage.allCases) { page in + page.label + } + } + .pickerStyle(.segmented) + #if os(iOS) + .padding([.leading, .trailing]) + .navigationBarTitleDisplayMode(.inline) + #endif + TabView(selection: $selection) { + ForEach(DashboardPage.enabledCases) { page in + page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled) + .tag(page) + } + } + } + + @available(iOS 16.0, *) + @ViewBuilder + private var content1: some View { + TabView(selection: $selection) { + ForEach(DashboardPage.enabledCases) { page in + page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled) + .tag(page) + } + } + .toolbar { + ToolbarTitleMenu { + Picker("Page", selection: $selection) { + ForEach(DashboardPage.allCases) { page in + page.label + } + } + } + } + } + private func doReload() async { defer { isLoading = false diff --git a/ApplicationLibrary/Views/Dashboard/ClashModeView.swift b/ApplicationLibrary/Views/Dashboard/ClashModeView.swift index 0c69415..045fe55 100644 --- a/ApplicationLibrary/Views/Dashboard/ClashModeView.swift +++ b/ApplicationLibrary/Views/Dashboard/ClashModeView.swift @@ -5,8 +5,7 @@ import SwiftUI @MainActor public struct ClashModeView: View { @Environment(\.scenePhase) private var scenePhase - @StateObject private var commandClient = CommandClient(.clashMode) - @State private var clashMode = "" + @EnvironmentObject private var commandClient: CommandClient @State private var alert: Alert? public init() {} @@ -14,9 +13,9 @@ public struct ClashModeView: View { VStack { if commandClient.clashModeList.count > 1 { Picker("", selection: Binding(get: { - clashMode + commandClient.clashMode }, set: { newMode in - clashMode = newMode + commandClient.clashMode = newMode Task { await setClashMode(newMode) } @@ -29,9 +28,6 @@ public struct ClashModeView: View { .padding([.top], 8) } } - .onReceive(commandClient.$clashMode) { newMode in - clashMode = newMode - } .padding([.leading, .trailing]) .onAppear { commandClient.connect() diff --git a/ApplicationLibrary/Views/Dashboard/DashboardPage.swift b/ApplicationLibrary/Views/Dashboard/DashboardPage.swift index d0a2245..fc0728c 100644 --- a/ApplicationLibrary/Views/Dashboard/DashboardPage.swift +++ b/ApplicationLibrary/Views/Dashboard/DashboardPage.swift @@ -9,6 +9,22 @@ public enum DashboardPage: Int, CaseIterable, Identifiable { case overview case groups + case connections +} + +public extension DashboardPage { + #if !os(tvOS) + static var enabledCases: [DashboardPage] = [ + .overview, + .groups, + .connections, + ] + #else + static var enabledCases: [DashboardPage] = [ + .overview, + .groups, + ] + #endif } public extension DashboardPage { @@ -18,15 +34,19 @@ public extension DashboardPage { return NSLocalizedString("Overview", comment: "") case .groups: return NSLocalizedString("Groups", comment: "") + case .connections: + return NSLocalizedString("Connections", comment: "") } } var label: some View { switch self { case .overview: - return Label("Overview", systemImage: "text.and.command.macwindow") + return Label(title, systemImage: "text.and.command.macwindow") case .groups: - return Label("Groups", systemImage: "rectangle.3.group.fill") + return Label(title, systemImage: "rectangle.3.group.fill") + case .connections: + return Label(title, systemImage: "list.bullet.rectangle.portrait.fill") } } @@ -38,6 +58,8 @@ public extension DashboardPage { OverviewView(profileList, selectedProfileID, systemProxyAvailable, systemProxyEnabled) case .groups: GroupListView() + case .connections: + ConnectionListView() } } } diff --git a/ApplicationLibrary/Views/Dashboard/OverviewView.swift b/ApplicationLibrary/Views/Dashboard/OverviewView.swift index 059fd75..ff83f60 100644 --- a/ApplicationLibrary/Views/Dashboard/OverviewView.swift +++ b/ApplicationLibrary/Views/Dashboard/OverviewView.swift @@ -36,6 +36,7 @@ public struct OverviewView: View { if ApplicationLibrary.inPreview || profile.status.isConnected { ExtensionStatusView() ClashModeView() + .environmentObject(environments.logClient) } if profileList.isEmpty { Text("Empty profiles") diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index 5b6963f..088b418 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -57,10 +57,11 @@ public struct LogView: View { } else { ScrollViewReader { reader in ScrollView { - VStack(alignment: .leading, spacing: 0) { + LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading, spacing: 0) { ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in Text(it.element) .font(logFont) + .textSelection(.enabled) #if os(tvOS) .focusable() #endif diff --git a/ApplicationLibrary/Views/NavigationPage.swift b/ApplicationLibrary/Views/NavigationPage.swift index a0cec65..346db43 100644 --- a/ApplicationLibrary/Views/NavigationPage.swift +++ b/ApplicationLibrary/Views/NavigationPage.swift @@ -10,6 +10,7 @@ public enum NavigationPage: Int, CaseIterable, Identifiable { case dashboard #if os(macOS) case groups + case connections #endif case logs case profiles @@ -35,6 +36,8 @@ public extension NavigationPage { #if os(macOS) case .groups: return NSLocalizedString("Groups", comment: "") + case .connections: + return NSLocalizedString("Connections", comment: "") #endif case .logs: return NSLocalizedString("Logs", comment: "") @@ -52,6 +55,8 @@ public extension NavigationPage { #if os(macOS) case .groups: return "rectangle.3.group.fill" + case .connections: + return "list.bullet.rectangle.portrait.fill" #endif case .logs: return "doc.text.fill" @@ -71,6 +76,8 @@ public extension NavigationPage { #if os(macOS) case .groups: GroupListView() + case .connections: + ConnectionListView() #endif case .logs: LogView() @@ -89,7 +96,7 @@ public extension NavigationPage { #if os(macOS) func visible(_ profile: ExtensionProfile?) -> Bool { switch self { - case .groups: + case .groups, .connections: return profile?.status.isConnectedStrict == true default: return true diff --git a/ApplicationLibrary/Views/Profile/EditProfileView.swift b/ApplicationLibrary/Views/Profile/EditProfileView.swift index 2f4f605..fc79d2e 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileView.swift @@ -51,7 +51,7 @@ public struct EditProfileView: View { } if profile.type == .remote { Section("Status") { - FormTextItem("Last Updated", profile.lastUpdatedString) + FormTextItem("Last Updated", profile.lastUpdated!.myFormat) } } Section("Action") { diff --git a/ApplicationLibrary/Views/Profile/ProfileView.swift b/ApplicationLibrary/Views/Profile/ProfileView.swift index 2b3b0b1..8d34e17 100644 --- a/ApplicationLibrary/Views/Profile/ProfileView.swift +++ b/ApplicationLibrary/Views/Profile/ProfileView.swift @@ -336,7 +336,7 @@ public struct ProfileView: View { Text(profile.name) if profile.type == .remote { Spacer(minLength: 4) - Text("Last Updated: \(profile.origin.lastUpdatedString)").font(.caption) + Text("Last Updated: \(profile.origin.lastUpdated!.myFormat)").font(.caption) } } HStack { diff --git a/Library/Database/Profile+Date.swift b/Library/Database/Profile+Date.swift index c6f4b71..0486a7c 100644 --- a/Library/Database/Profile+Date.swift +++ b/Library/Database/Profile+Date.swift @@ -1,9 +1,9 @@ import Foundation -public extension Profile { - var lastUpdatedString: String { +public extension Date { + var myFormat: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - return dateFormatter.string(from: lastUpdated!) + return dateFormatter.string(from: self) } } diff --git a/Library/Database/SharedPreferences.swift b/Library/Database/SharedPreferences.swift index a0c2f56..7b93f4e 100644 --- a/Library/Database/SharedPreferences.swift +++ b/Library/Database/SharedPreferences.swift @@ -68,6 +68,11 @@ public enum SharedPreferences { await excludeAPNsRoute.set(nil) } + // Connections Filter + + public static let connectionStateFilter = Preference("connection_state_filter", defaultValue: 0) + public static let connectionSort = Preference("connection_sort", defaultValue: 0) + // On Demand Rules public static let alwaysOn = Preference("always_on", defaultValue: false) diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift index 5f24080..c13942b 100644 --- a/Library/Network/CommandClient.swift +++ b/Library/Network/CommandClient.swift @@ -7,6 +7,7 @@ public class CommandClient: ObservableObject { case groups case log case clashMode + case connections } private let connectionType: ConnectionType @@ -21,6 +22,11 @@ public class CommandClient: ObservableObject { @Published public var clashModeList: [String] @Published public var clashMode: String + @Published public var connectionStateFilter = ConnectionStateFilter.all + @Published public var connectionSort = ConnectionSort.byDate + @Published public var connections: [LibboxConnection]? + public var rawConnections: LibboxConnections? + public init(_ connectionType: ConnectionType, logMaxLines: Int = 300) { self.connectionType = connectionType self.logMaxLines = logMaxLines @@ -53,7 +59,41 @@ public class CommandClient: ObservableObject { } } + public func filterConnectionsNow() { + guard let message = rawConnections else { + return + } + connections = filterConnections(message) + } + + private func filterConnections(_ message: LibboxConnections) -> [LibboxConnection] { + message.filterState(Int32(connectionStateFilter.rawValue)) + switch connectionSort { + case .byDate: + message.sortByDate() + case .byTraffic: + message.sortByTraffic() + case .byTrafficTotal: + message.sortByTrafficTotal() + } + let connectionIterator = message.iterator()! + var connections: [LibboxConnection] = [] + while connectionIterator.hasNext() { + connections.append(connectionIterator.next()!) + } + return connections + } + + private func initializeConnectionFilterState() async { + connectionStateFilter = await .init(rawValue: SharedPreferences.connectionStateFilter.get()) ?? .all + connectionSort = await .init(rawValue: SharedPreferences.connectionSort.get()) ?? .byDate + } + private nonisolated func connect0() async { + if connectionType == .connections { + await initializeConnectionFilterState() + } + let clientOptions = LibboxCommandClientOptions() switch connectionType { case .status: @@ -64,8 +104,15 @@ public class CommandClient: ObservableObject { clientOptions.command = LibboxCommandLog case .clashMode: clientOptions.command = LibboxCommandClashMode + case .connections: + clientOptions.command = LibboxCommandConnections + } + switch connectionType { + case .log: + clientOptions.statusInterval = Int64(500 * NSEC_PER_MSEC) + default: + clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) } - clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) let client = LibboxNewCommandClient(clientHandler(self), clientOptions)! do { for i in 0 ..< 10 { @@ -101,27 +148,32 @@ public class CommandClient: ObservableObject { } } - func disconnected(_: String?) { + func disconnected(_ message: String?) { DispatchQueue.main.async { [self] in commandClient.isConnected = false } + if let message { + NSLog("client disconnected: \(message)") + } } - func clearLog() { + func clearLogs() { DispatchQueue.main.async { [self] in commandClient.logList.removeAll() } } - func writeLog(_ message: String?) { - guard let message else { + func writeLogs(_ messageList: (any LibboxStringIteratorProtocol)?) { + guard let messageList else { return } DispatchQueue.main.async { [self] in - if commandClient.logList.count > commandClient.logMaxLines { - commandClient.logList.removeFirst() + if commandClient.logList.count >= commandClient.logMaxLines { + commandClient.logList.removeSubrange(0 ..< Int(messageList.len())) + } + while messageList.hasNext() { + commandClient.logList.append(messageList.next()) } - commandClient.logList.append(message) } } @@ -156,5 +208,62 @@ public class CommandClient: ObservableObject { commandClient.clashMode = newMode! } } + + func write(_ message: LibboxConnections?) { + guard let message else { + return + } + let connections = commandClient.filterConnections(message) + DispatchQueue.main.async { [self] in + commandClient.rawConnections = message + commandClient.connections = connections + } + } + } +} + +public enum ConnectionStateFilter: Int, CaseIterable, Identifiable { + public var id: Self { + self + } + + case all + case active + case closed +} + +public extension ConnectionStateFilter { + var name: String { + switch self { + case .all: + return NSLocalizedString("All", comment: "") + case .active: + return NSLocalizedString("Active", comment: "") + case .closed: + return NSLocalizedString("Closed", comment: "") + } + } +} + +public enum ConnectionSort: Int, CaseIterable, Identifiable { + public var id: Self { + self + } + + case byDate + case byTraffic + case byTrafficTotal +} + +public extension ConnectionSort { + var name: String { + switch self { + case .byDate: + return NSLocalizedString("Date", comment: "") + case .byTraffic: + return NSLocalizedString("Traffic", comment: "") + case .byTrafficTotal: + return NSLocalizedString("Traffic Total", comment: "") + } } } diff --git a/Library/Network/Extension+Iterator.swift b/Library/Network/Extension+Iterator.swift index b7d57bb..00832d0 100644 --- a/Library/Network/Extension+Iterator.swift +++ b/Library/Network/Extension+Iterator.swift @@ -1,7 +1,7 @@ import Foundation import Libbox -extension LibboxStringIteratorProtocol { +public extension LibboxStringIteratorProtocol { func toArray() -> [String] { var array: [String] = [] while hasNext() { diff --git a/Library/Network/ExtensionProfile.swift b/Library/Network/ExtensionProfile.swift index c738111..1db4ced 100644 --- a/Library/Network/ExtensionProfile.swift +++ b/Library/Network/ExtensionProfile.swift @@ -3,6 +3,8 @@ import Libbox import NetworkExtension public class ExtensionProfile: ObservableObject { + public static let controlKind = "io.nekohasekai.sfa.widget.ServiceToggle" + private let manager: NEVPNManager private var connection: NEVPNConnection private var observer: Any? diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index 54199dd..47792da 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -1,6 +1,9 @@ import Foundation import Libbox import NetworkExtension +#if os(iOS) + import WidgetKit +#endif open class ExtensionProvider: NEPacketTunnelProvider { public var username: String? = nil @@ -48,13 +51,16 @@ open class ExtensionProvider: NEPacketTunnelProvider { } writeMessage("(packet-tunnel): Here I stand") await startService() + #if os(iOS) + if #available(iOS 18.0, *) { + ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind) + } + #endif } func writeMessage(_ message: String) { if let commandServer { commandServer.writeMessage(message) - } else { - NSLog(message) } } @@ -153,6 +159,11 @@ open class ExtensionProvider: NEPacketTunnelProvider { await SharedPreferences.startedByUser.set(reason == .userInitiated) } #endif + #if os(iOS) + if #available(iOS 18.0, *) { + ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind) + } + #endif } override open func handleAppMessage(_ messageData: Data) async -> Data? { diff --git a/Library/Network/NEVPNStatus+isConnected.swift b/Library/Network/NEVPNStatus+isConnected.swift index d5b626b..312a9c8 100644 --- a/Library/Network/NEVPNStatus+isConnected.swift +++ b/Library/Network/NEVPNStatus+isConnected.swift @@ -4,7 +4,16 @@ import NetworkExtension public extension NEVPNStatus { var isEnabled: Bool { switch self { - case .connected, .disconnected, .reasserting: + case .connecting, .connected, .disconnected, .reasserting: + return true + default: + return false + } + } + + var isStarted: Bool { + switch self { + case .connecting, .connected, .reasserting: return true default: return false diff --git a/MacLibrary/MacApplication.swift b/MacLibrary/MacApplication.swift index fe71828..ece7a4b 100644 --- a/MacLibrary/MacApplication.swift +++ b/MacLibrary/MacApplication.swift @@ -46,6 +46,7 @@ public struct MacApplication: Scene { MenuBarExtra(isInserted: $showMenuBarExtra) { MenuView(isMenuPresented: $isMenuPresented) .environmentObject(environments) + .environmentObject(environments.logClient) } label: { Image("MenuIcon") } diff --git a/MacLibrary/MenuView.swift b/MacLibrary/MenuView.swift index ea9764d..3539ed6 100644 --- a/MacLibrary/MenuView.swift +++ b/MacLibrary/MenuView.swift @@ -11,11 +11,14 @@ public struct MenuView: View { @Environment(\.openWindow) private var openWindow private static let sliderWidth: CGFloat = 270 + public static let MenuDisclosureSectionPaddingFix: CGFloat = -14.0 @Binding private var isMenuPresented: Bool @State private var isLoading = true @State private var profile: ExtensionProfile? + + @EnvironmentObject private var commandClient: CommandClient public init(isMenuPresented: Binding) { _isMenuPresented = isMenuPresented @@ -41,6 +44,9 @@ public struct MenuView: View { if let profile { ProfilePicker(profile) } + if( commandClient.clashModeList.count > 0) { + ClashOutboundPicker() + } Divider() MenuCommand { NSApp.setActivationPolicy(.regular) @@ -118,6 +124,7 @@ public struct MenuView: View { @State private var selectedProfileID: Int64 = 0 @State private var reasserting = false @State private var alert: Alert? + @State private var isExpanded = false private var selectedProfileIDLocal: Binding { $selectedProfileID.withSetter { newValue in @@ -140,14 +147,22 @@ public struct MenuView: View { if profileList.isEmpty { Text("Empty profiles") } else { - MenuSection("Profile") - Picker("", selection: selectedProfileIDLocal) { - ForEach(profileList, id: \.id) { profile in - Text(profile.name) + Divider() + MenuDisclosureSection( + profileList.firstIndex(where: { $0.id == selectedProfileIDLocal.wrappedValue }).map { "Profile: \(profileList[$0].name)" } ?? "Profile", + divider: false, + isExpanded: $isExpanded + ) { + Picker("", selection: selectedProfileIDLocal) { + ForEach(profileList, id: \.id) { profile in + Text(profile.name).frame(maxWidth: .infinity, alignment: .leading) + } } + .pickerStyle(.inline) + .disabled(!profile.status.isSwitchable || reasserting) } - .pickerStyle(.inline) - .disabled(!profile.status.isSwitchable || reasserting) + .padding(.leading, MenuView.MenuDisclosureSectionPaddingFix) + .padding(.trailing, MenuView.MenuDisclosureSectionPaddingFix) } } } @@ -204,4 +219,50 @@ public struct MenuView: View { try LibboxNewStandaloneCommandClient()!.serviceReload() } } + + private struct ClashOutboundPicker: View { + @EnvironmentObject private var commandClient: CommandClient + @State private var isclashModeExpanded = false + @State private var alert: Alert? + + private var clashLists: [MenuEntry] { + commandClient.clashModeList.map { stringValue in + MenuEntry(name: stringValue, systemImage: "") + } + } + + var body: some View { + viewBuilder { + Divider() + MenuDisclosureSection("Outbound: " + commandClient.clashMode, divider: false, isExpanded: $isclashModeExpanded) { + MenuScrollView(maxHeight: 135) { + ForEach(commandClient.clashModeList, id: \.self) { it in + MenuCommand { + commandClient.clashMode = it; + } label: { + if it == commandClient.clashMode { + Image(systemName: "play.fill") + } + Text(it) + } + .padding(.leading, MenuView.MenuDisclosureSectionPaddingFix) + .padding(.trailing, MenuView.MenuDisclosureSectionPaddingFix) + } + } + } + .padding(.leading, MenuView.MenuDisclosureSectionPaddingFix) + .padding(.trailing, MenuView.MenuDisclosureSectionPaddingFix) + } + .alertBinding($alert) + } + private nonisolated func setClashMode(_ newMode: String) async { + do { + try LibboxNewStandaloneCommandClient()!.setClashMode(newMode) + } catch { + await MainActor.run { + alert = Alert(error) + } + } + } + } } diff --git a/MacLibrary/SidebarView.swift b/MacLibrary/SidebarView.swift index df83edf..ef24a48 100644 --- a/MacLibrary/SidebarView.swift +++ b/MacLibrary/SidebarView.swift @@ -33,6 +33,7 @@ public struct SidebarView: View { .tint(.textColor) .tag(NavigationPage.dashboard) NavigationPage.groups.label.tag(NavigationPage.groups) + NavigationPage.connections.label.tag(NavigationPage.connections) } Divider() ForEach(NavigationPage.macosDefaultPages, id: \.self) { it in diff --git a/SFI/ApplicationDelegate.swift b/SFI/ApplicationDelegate.swift index 21f0dbe..0120242 100644 --- a/SFI/ApplicationDelegate.swift +++ b/SFI/ApplicationDelegate.swift @@ -11,23 +11,23 @@ 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, false) - Task { - await setup() - } + setup() return true } - private func setup() async { + private func setup() { do { - try await UIProfileUpdateTask.configure() + try UIProfileUpdateTask.configure() NSLog("setup background task success") } catch { NSLog("setup background task error: \(error.localizedDescription)") } - if UIDevice.current.userInterfaceIdiom == .phone { - await requestNetworkPermission() + Task { + if UIDevice.current.userInterfaceIdiom == .phone { + await requestNetworkPermission() + } + await setupBackground() } - await setupBackground() } private nonisolated func setupBackground() async { diff --git a/SFT/ApplicationDelegate.swift b/SFT/ApplicationDelegate.swift index 9fd4cd8..2c1fc83 100644 --- a/SFT/ApplicationDelegate.swift +++ b/SFT/ApplicationDelegate.swift @@ -8,15 +8,13 @@ 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 { - await setup() - } + setup() return true } - private func setup() async { + private func setup() { do { - try await UIProfileUpdateTask.configure() + try UIProfileUpdateTask.configure() NSLog("setup background task success") } catch { NSLog("setup background task error: \(error.localizedDescription)") diff --git a/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json b/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..2305880 100644 --- a/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -4,6 +4,28 @@ "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { diff --git a/WidgetExtension/WidgetExtensionBundle.swift b/WidgetExtension/ExtensionBundle.swift similarity index 51% rename from WidgetExtension/WidgetExtensionBundle.swift rename to WidgetExtension/ExtensionBundle.swift index fab679b..ff0c94b 100644 --- a/WidgetExtension/WidgetExtensionBundle.swift +++ b/WidgetExtension/ExtensionBundle.swift @@ -2,8 +2,8 @@ import SwiftUI import WidgetKit @main -struct WidgetExtensionBundle: WidgetBundle { +struct ExtensionBundle: WidgetBundle { var body: some Widget { - WidgetExtension() + ServiceToggleControl() } } diff --git a/WidgetExtension/Intents.swift b/WidgetExtension/Intents.swift deleted file mode 100644 index 6be7028..0000000 --- a/WidgetExtension/Intents.swift +++ /dev/null @@ -1,46 +0,0 @@ -import AppIntents -import Library -import WidgetKit - -struct ConfigurationIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource = "Configuration" - static var description = IntentDescription("Configuration sing-bix widget.") -} - -struct StartServiceIntent: AppIntent { - static var title: LocalizedStringResource = "Start sing-box" - - static var description = - IntentDescription("Start sing-box servie") - - func perform() async throws -> some IntentResult { - guard let extensionProfile = try await (ExtensionProfile.load()) else { - throw NSError(domain: "NetworkExtension not installed", code: 0) - } - if extensionProfile.status == .connected { - return .result() - } else { - try await extensionProfile.start() - } - return .result() - } -} - -struct StopServiceIntent: AppIntent { - static var title: LocalizedStringResource = "Stop sing-box" - - static var description = - IntentDescription("Stop sing-box service") - - static var parameterSummary: some ParameterSummary { - Summary("Stop sing-box service") - } - - func perform() async throws -> some IntentResult { - guard let extensionProfile = try await (ExtensionProfile.load()) else { - return .result() - } - extensionProfile.stop() - return .result() - } -} diff --git a/WidgetExtension/ServiceToggleControl.swift b/WidgetExtension/ServiceToggleControl.swift new file mode 100644 index 0000000..3de0cc7 --- /dev/null +++ b/WidgetExtension/ServiceToggleControl.swift @@ -0,0 +1,66 @@ +import AppIntents +import Library +import SwiftUI +import WidgetKit + +struct ServiceToggleControl: ControlWidget { + var body: some ControlWidgetConfiguration { + StaticControlConfiguration( + kind: ExtensionProfile.controlKind, + provider: Provider() + ) { value in + ControlWidgetToggle( + "sing-box", + isOn: value, + action: ToggleServiceIntent() + ) { isOn in + Label(isOn ? "Running" : "Stopped", systemImage: "shippingbox.fill") + } + .tint(.init(red: CGFloat(Double(69) / 255), green: CGFloat(Double(90) / 255), blue: CGFloat(Double(100) / 255))) + } + .displayName("Toggle") + .description("Start or stop sing-box service.") + } +} + +extension ServiceToggleControl { + struct Provider: ControlValueProvider { + var previewValue: Bool { + false + } + + func currentValue() async throws -> Bool { + guard let extensionProfile = try await (ExtensionProfile.load()) else { + return false + } + return extensionProfile.status.isStarted + } + } +} + +struct ToggleServiceIntent: SetValueIntent { + static var title: LocalizedStringResource = "Toggle sing-box" + + static var description = + IntentDescription("Toggle sing-box service") + + static var parameterSummary: some ParameterSummary { + Summary("Toggle sing-box service") + } + + @Parameter(title: "Service status") + var value: Bool + + func perform() async throws -> some IntentResult & ReturnsValue { + guard let extensionProfile = try await (ExtensionProfile.load()) else { + return .result(value: false) + } + if value { + try await extensionProfile.start() + return .result(value: true) + } else { + try await extensionProfile.stop() + return .result(value: false) + } + } +} diff --git a/WidgetExtension/WidgetExtension.swift b/WidgetExtension/WidgetExtension.swift deleted file mode 100644 index 5ba8777..0000000 --- a/WidgetExtension/WidgetExtension.swift +++ /dev/null @@ -1,89 +0,0 @@ -import AppIntents -import Libbox -import Library -import SwiftUI -import WidgetKit - -struct Provider: AppIntentTimelineProvider { - func placeholder(in _: Context) -> ExtensionStatus { - ExtensionStatus(date: .now, isConnected: false, profileList: []) - } - - func snapshot(for _: ConfigurationIntent, in _: Context) async -> ExtensionStatus { - var status = ExtensionStatus(date: .now, isConnected: false, profileList: []) - - do { - status.isConnected = try await ExtensionProfile.load()?.status.isStrictConnected ?? false - - let profileList = try ProfileManager.list() - let selectedProfileID = SharedPreferences.selectedProfileID - for profile in profileList { - status.profileList.append(ProfileEntry(profile: profile, isSelected: profile.id == selectedProfileID)) - } - } catch {} - - return status - } - - func timeline(for intent: ConfigurationIntent, in context: Context) async -> Timeline { - await Timeline(entries: [snapshot(for: intent, in: context)], policy: .never) - } -} - -struct ExtensionStatus: TimelineEntry { - var date: Date - var isConnected: Bool - var profileList: [ProfileEntry] -} - -struct ProfileEntry { - let profile: Profile - let isSelected: Bool -} - -struct WidgetView: View { - @Environment(\.widgetFamily) private var family - - var status: ExtensionStatus - - var body: some View { - VStack { - LabeledContent { - Text(LibboxVersion()) - .font(.caption) - } label: { - Text("sing-box") - .font(.headline) - } - VStack { - viewBuilder { - if !status.isConnected { - Button(intent: StartServiceIntent()) { - Image(systemName: "play.fill") - } - } else { - Button(intent: StopServiceIntent()) { - Image(systemName: "stop.fill") - } - } - } - .controlSize(.large) - .invalidatableContent() - } - .frame(maxHeight: .infinity, alignment: .center) - } - .frame(maxHeight: .infinity, alignment: .topLeading) - .containerBackground(.fill.tertiary, for: .widget) - } -} - -struct WidgetExtension: Widget { - @State private var extensionProfile: ExtensionProfile? - - var body: some WidgetConfiguration { - AppIntentConfiguration(kind: "sing-box", intent: ConfigurationIntent.self, provider: Provider()) { status in - WidgetView(status: status) - } - .supportedFamilies([.systemSmall]) - } -} diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtensionExtension.entitlements similarity index 85% rename from WidgetExtension/WidgetExtension.entitlements rename to WidgetExtensionExtension.entitlements index 6524ff1..78e3dbb 100644 --- a/WidgetExtension/WidgetExtension.entitlements +++ b/WidgetExtensionExtension.entitlements @@ -4,7 +4,7 @@ com.apple.security.application-groups - group.org.sagernet.sfa + group.io.nekohasekai.sfa diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index 10bdd39..9c0064f 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 3A27D9002A89BE230031EBCC /* CommandClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D8FF2A89BE230031EBCC /* CommandClient.swift */; }; 3A27D9022A89C6870031EBCC /* ExtensionEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */; }; 3A2EAEED2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */; }; + 3A334ED02C0F621E00E9C577 /* ConnectionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */; }; 3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3A3AA7FF2A4EFDB3002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3A3AB2A72B70C146001815AE /* CoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3AB2A62B70C146001815AE /* CoreView.swift */; }; @@ -67,10 +68,12 @@ 3A60CC272B70880100D2D682 /* PacketTunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A60CC262B70880100D2D682 /* PacketTunnelView.swift */; }; 3A60CC292B70A7C400D2D682 /* Color+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A60CC282B70A7C400D2D682 /* Color+Extension.swift */; }; 3A60CC2B2B70AD6700D2D682 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A60CC2A2B70AD6700D2D682 /* SettingView.swift */; }; + 3A63269E2C0DE12D0076E274 /* ConnectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A63269D2C0DE12D0076E274 /* ConnectionListView.swift */; }; + 3A6326A02C0DE15C0076E274 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A63269F2C0DE15C0076E274 /* Connection.swift */; }; + 3A6326A22C0DE64F0076E274 /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6326A12C0DE64F0076E274 /* ConnectionView.swift */; }; 3A648D2D2A4EEAA600D95A12 /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A648D2C2A4EEAA600D95A12 /* Library.swift */; }; 3A648D542A4EF4C700D95A12 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; }; 3A6CA4542BC19FDE0012B238 /* OnDemandRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */; }; - 3A6CA5A02A71317A0027933B /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3A6CA59F2A71317A0027933B /* MarkdownUI */; }; 3A6CA5A32A713A580027933B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AEECC0A2A6DF9CA006A0E0C /* Assets.xcassets */; }; 3A6CA5A62A713AA10027933B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3A6CA5A52A713AA10027933B /* AppIcon.icns */; }; 3A6CA5A72A713ABA0027933B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3A6CA5A52A713AA10027933B /* AppIcon.icns */; }; @@ -89,6 +92,7 @@ 3A99B42A2A7526990010D4B0 /* NavigationStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */; }; 3A99B42C2A75288C0010D4B0 /* ViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */; }; 3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */; }; + 3A9E6EBF2C0F20B0005061F3 /* ConnectionListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9E6EBE2C0F20B0005061F3 /* ConnectionListPage.swift */; }; 3AABFD432A9CC5A7005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD422A9CC5A7005A24A4 /* Upload.plist */; }; 3AABFD472A9CCC58005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD462A9CCC58005A24A4 /* Upload.plist */; }; 3AB1220B2A70FD500087CD55 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1220A2A70FD500087CD55 /* Alert.swift */; }; @@ -109,6 +113,13 @@ 3AE1719D2A8128DD00393060 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE1719C2A8128DD00393060 /* PacketTunnelProvider.swift */; }; 3AE171A62A81294400393060 /* TVExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3AE171992A8128DD00393060 /* TVExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AE171A92A81297300393060 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; + 3AE395F42C21A5CA00647718 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AE395F32C21A5CA00647718 /* WidgetKit.framework */; }; + 3AE395F62C21A5CA00647718 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AE395F52C21A5CA00647718 /* SwiftUI.framework */; }; + 3AE395F92C21A5CA00647718 /* ExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE395F82C21A5CA00647718 /* ExtensionBundle.swift */; }; + 3AE395FB2C21A5CA00647718 /* ServiceToggleControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE395FA2C21A5CA00647718 /* ServiceToggleControl.swift */; }; + 3AE395FF2C21A5CC00647718 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AE395FE2C21A5CC00647718 /* Assets.xcassets */; }; + 3AE396032C21A5CC00647718 /* WidgetExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3AE395F22C21A5CA00647718 /* WidgetExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3AE396072C21A60C00647718 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 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 */; }; @@ -245,6 +256,20 @@ remoteGlobalIDString = 3AEC211C2A459B4700A63465; remoteInfo = Library; }; + 3AE396012C21A5CC00647718 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AE395F12C21A5CA00647718; + remoteInfo = WidgetExtensionExtension; + }; + 3AE396092C21A60C00647718 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3AEC211C2A459B4700A63465; + remoteInfo = Library; + }; 3AE4D0BA2A6E2C55009FEA9E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; @@ -328,6 +353,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 3AE396032C21A5CC00647718 /* WidgetExtensionExtension.appex in Embed Foundation Extensions */, 3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; @@ -455,6 +481,7 @@ 3A27D8FF2A89BE230031EBCC /* CommandClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandClient.swift; sourceTree = ""; }; 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionEnvironments.swift; sourceTree = ""; }; 3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndependentApplicationDelegate.swift; sourceTree = ""; }; + 3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailsView.swift; sourceTree = ""; }; 3A3AB2A62B70C146001815AE /* CoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreView.swift; sourceTree = ""; }; 3A3AB2A82B70C5F1001815AE /* RequestReviewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestReviewButton.swift; sourceTree = ""; }; 3A3DEBE12A4FFA1A00373BF4 /* ExtensionFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExtensionFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/ExtensionFoundation.framework; sourceTree = DEVELOPER_DIR; }; @@ -479,6 +506,9 @@ 3A60CC262B70880100D2D682 /* PacketTunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelView.swift; sourceTree = ""; }; 3A60CC282B70A7C400D2D682 /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = ""; }; 3A60CC2A2B70AD6700D2D682 /* SettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = ""; }; + 3A63269D2C0DE12D0076E274 /* ConnectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionListView.swift; sourceTree = ""; }; + 3A63269F2C0DE15C0076E274 /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; + 3A6326A12C0DE64F0076E274 /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = ""; }; 3A648D2C2A4EEAA600D95A12 /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = ""; }; 3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandRulesView.swift; sourceTree = ""; }; 3A6CA5A52A713AA10027933B /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = ""; }; @@ -494,6 +524,7 @@ 3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCompat.swift; sourceTree = ""; }; 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCompat.swift; sourceTree = ""; }; 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDestinationCompat.swift; sourceTree = ""; }; + 3A9E6EBE2C0F20B0005061F3 /* ConnectionListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionListPage.swift; sourceTree = ""; }; 3AA1ABB92A4C4054000FD4BA /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; 3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLogView.swift; sourceTree = ""; }; 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileContentView.swift; sourceTree = ""; }; @@ -526,6 +557,14 @@ 3AE1719C2A8128DD00393060 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 3AE1719E2A8128DD00393060 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3AE1719F2A8128DD00393060 /* TVExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TVExtension.entitlements; sourceTree = ""; }; + 3AE395F22C21A5CA00647718 /* WidgetExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 3AE395F32C21A5CA00647718 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 3AE395F52C21A5CA00647718 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 3AE395F82C21A5CA00647718 /* ExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionBundle.swift; sourceTree = ""; }; + 3AE395FA2C21A5CA00647718 /* ServiceToggleControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceToggleControl.swift; sourceTree = ""; }; + 3AE395FE2C21A5CC00647718 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3AE396002C21A5CC00647718 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3AE3960E2C21D11F00647718 /* WidgetExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtensionExtension.entitlements; sourceTree = ""; }; 3AE4D0B62A6E2C01009FEA9E /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 3AE4D0C02A6E4852009FEA9E /* InstallSystemExtensionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallSystemExtensionButton.swift; sourceTree = ""; }; 3AEAEE9C2A4F1A9D0059612D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -587,7 +626,6 @@ buildActionMask = 2147483647; files = ( 3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */, - 3A6CA5A02A71317A0027933B /* MarkdownUI in Frameworks */, 3A4A020D2B53E3DC004EFB87 /* QRCode in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -619,6 +657,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3AE395EF2C21A5CA00647718 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AE396072C21A60C00647718 /* Library.framework in Frameworks */, + 3AE395F62C21A5CA00647718 /* SwiftUI.framework in Frameworks */, + 3AE395F42C21A5CA00647718 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3AEC20F02A459AB400A63465 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -721,6 +769,18 @@ path = Service; sourceTree = ""; }; + 3A63269C2C0DE10B0076E274 /* Connections */ = { + isa = PBXGroup; + children = ( + 3A63269D2C0DE12D0076E274 /* ConnectionListView.swift */, + 3A63269F2C0DE15C0076E274 /* Connection.swift */, + 3A6326A12C0DE64F0076E274 /* ConnectionView.swift */, + 3A9E6EBE2C0F20B0005061F3 /* ConnectionListPage.swift */, + 3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */, + ); + path = Connections; + sourceTree = ""; + }; 3A6CA5A42A713A6C0027933B /* Icons */ = { isa = PBXGroup; children = ( @@ -807,9 +867,21 @@ path = TVExtension; sourceTree = ""; }; + 3AE395F72C21A5CA00647718 /* WidgetExtension */ = { + isa = PBXGroup; + children = ( + 3AE395F82C21A5CA00647718 /* ExtensionBundle.swift */, + 3AE395FA2C21A5CA00647718 /* ServiceToggleControl.swift */, + 3AE395FE2C21A5CC00647718 /* Assets.xcassets */, + 3AE396002C21A5CC00647718 /* Info.plist */, + ); + path = WidgetExtension; + sourceTree = ""; + }; 3AEC20BC2A45991900A63465 = { isa = PBXGroup; children = ( + 3AE3960E2C21D11F00647718 /* WidgetExtensionExtension.entitlements */, 3AEC20DB2A4599D000A63465 /* Libbox.xcframework */, 3AEC20F42A459AB400A63465 /* SFI */, 3AEC210A2A459B1900A63465 /* SFM */, @@ -822,6 +894,7 @@ 3AEECBF32A6DF40A006A0E0C /* SystemExtension */, 3A77016E2A4E6B34008F031F /* IntentsExtension */, 3AE1719B2A8128DD00393060 /* TVExtension */, + 3AE395F72C21A5CA00647718 /* WidgetExtension */, 3AEC20C72A45991900A63465 /* Products */, 3AEC21012A459AE300A63465 /* Frameworks */, ); @@ -841,6 +914,7 @@ 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */, 3AC03B962A72BF3300B7946F /* sing-box.app */, 3AE171992A8128DD00393060 /* TVExtension.appex */, + 3AE395F22C21A5CA00647718 /* WidgetExtensionExtension.appex */, ); name = Products; sourceTree = ""; @@ -867,6 +941,8 @@ 3A3DEBE12A4FFA1A00373BF4 /* ExtensionFoundation.framework */, 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */, 3ABA46D22A6A32A100D8366B /* Messages.framework */, + 3AE395F32C21A5CA00647718 /* WidgetKit.framework */, + 3AE395F52C21A5CA00647718 /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -942,6 +1018,7 @@ 3AEC21732A45B0AC00A63465 /* Views */ = { isa = PBXGroup; children = ( + 3A63269C2C0DE10B0076E274 /* Connections */, 3AF342D22A4AADA5002B34AC /* Abstract */, 3AF342A12A4A9B8D002B34AC /* Dashboard */, 3A1CF2EE2A50E5D5000A8289 /* Groups */, @@ -1088,7 +1165,6 @@ ); name = ApplicationLibrary; packageProductDependencies = ( - 3A6CA59F2A71317A0027933B /* MarkdownUI */, 3A4A020C2B53E3DC004EFB87 /* QRCode */, ); productName = ApplicationLibrary; @@ -1152,6 +1228,24 @@ productReference = 3AE171992A8128DD00393060 /* TVExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 3AE395F12C21A5CA00647718 /* WidgetExtensionExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3AE396042C21A5CC00647718 /* Build configuration list for PBXNativeTarget "WidgetExtensionExtension" */; + buildPhases = ( + 3AE395EE2C21A5CA00647718 /* Sources */, + 3AE395EF2C21A5CA00647718 /* Frameworks */, + 3AE395F02C21A5CA00647718 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3AE3960A2C21A60C00647718 /* PBXTargetDependency */, + ); + name = WidgetExtensionExtension; + productName = WidgetExtensionExtension; + productReference = 3AE395F22C21A5CA00647718 /* WidgetExtensionExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 3AEC20F22A459AB400A63465 /* SFI */ = { isa = PBXNativeTarget; buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */; @@ -1171,6 +1265,7 @@ 3AEAEE9B2A4F16430059612D /* PBXTargetDependency */, 3A8655162A4FA26600B7181F /* PBXTargetDependency */, 3A4EAD3A2A4FEC20005435B3 /* PBXTargetDependency */, + 3AE396022C21A5CC00647718 /* PBXTargetDependency */, ); name = SFI; packageProductDependencies = ( @@ -1300,7 +1395,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1430; TargetAttributes = { 3A096F852A4ED3DE00D4A2ED = { @@ -1320,6 +1415,9 @@ 3AE171982A8128DD00393060 = { CreatedOnToolsVersion = 15.0; }; + 3AE395F12C21A5CA00647718 = { + CreatedOnToolsVersion = 16.0; + }; 3AEC20F22A459AB400A63465 = { CreatedOnToolsVersion = 15.0; }; @@ -1356,7 +1454,6 @@ 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */, 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */, 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */, - 3ADB2D832A71266E00A6517D /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */, ); productRefGroup = 3AEC20C72A45991900A63465 /* Products */; @@ -1372,8 +1469,9 @@ 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */, 3AEECC2E2A6DFDAD006A0E0C /* MacLibrary */, 3A096F852A4ED3DE00D4A2ED /* Extension */, - 3A77016C2A4E6B34008F031F /* IntentsExtension */, 3AE171982A8128DD00393060 /* TVExtension */, + 3A77016C2A4E6B34008F031F /* IntentsExtension */, + 3AE395F12C21A5CA00647718 /* WidgetExtensionExtension */, ); }; /* End PBXProject section */ @@ -1401,6 +1499,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3AE395F02C21A5CA00647718 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AE395FF2C21A5CC00647718 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3AEC20F12A459AB400A63465 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1477,6 +1583,7 @@ 3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */, 3A60CC272B70880100D2D682 /* PacketTunnelView.swift in Sources */, 3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */, + 3A9E6EBF2C0F20B0005061F3 /* ConnectionListPage.swift in Sources */, 3A411CEC2B734959000D9501 /* MacAppView.swift in Sources */, 3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */, 3A4EAD292A4FEB6D005435B3 /* FormItem.swift in Sources */, @@ -1486,6 +1593,7 @@ 3A172D2B2B88E9DB00D98050 /* BackButton.swift in Sources */, 3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */, 3AE4D0C12A6E4852009FEA9E /* InstallSystemExtensionButton.swift in Sources */, + 3A6326A22C0DE64F0076E274 /* ConnectionView.swift in Sources */, 3A4EAD232A4FEB5A005435B3 /* EnvironmentValues.swift in Sources */, 3A4F68B02A97602C003D66D3 /* ClashModeView.swift in Sources */, 3A4EAD282A4FEB65005435B3 /* ActiveDashboardView.swift in Sources */, @@ -1510,8 +1618,11 @@ 3ACA8B332B7E037800B7238F /* DeleteButton.swift in Sources */, 3ACE6DE32ACADF55009D9A8A /* Binding+Setter.swift in Sources */, 3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */, + 3A63269E2C0DE12D0076E274 /* ConnectionListView.swift in Sources */, 3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */, + 3A6326A02C0DE15C0076E274 /* Connection.swift in Sources */, 3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */, + 3A334ED02C0F621E00E9C577 /* ConnectionDetailsView.swift in Sources */, 3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */, 3A0C6D3E2A79D6A600A4DF2B /* DashboardPage.swift in Sources */, 3A3AB2A92B70C5F1001815AE /* RequestReviewButton.swift in Sources */, @@ -1545,6 +1656,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3AE395EE2C21A5CA00647718 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AE395F92C21A5CA00647718 /* ExtensionBundle.swift in Sources */, + 3AE395FB2C21A5CA00647718 /* ServiceToggleControl.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3AEC20EF2A459AB400A63465 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1697,6 +1817,16 @@ target = 3AEC211C2A459B4700A63465 /* Library */; targetProxy = 3AE171AB2A81297300393060 /* PBXContainerItemProxy */; }; + 3AE396022C21A5CC00647718 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AE395F12C21A5CA00647718 /* WidgetExtensionExtension */; + targetProxy = 3AE396012C21A5CC00647718 /* PBXContainerItemProxy */; + }; + 3AE3960A2C21A60C00647718 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3AEC211C2A459B4700A63465 /* Library */; + targetProxy = 3AE396092C21A60C00647718 /* PBXContainerItemProxy */; + }; 3AE4D0BB2A6E2C55009FEA9E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3AEC211C2A459B4700A63465 /* Library */; @@ -2010,7 +2140,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.4; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; SDKROOT = appletvos; @@ -2044,7 +2174,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.4; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; SDKROOT = appletvos; @@ -2126,6 +2256,69 @@ }; name = Release; }; + 3AE396052C21A5CC00647718 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = WidgetExtensionExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3AE396062C21A5CC00647718 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = WidgetExtensionExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z56Z6NYZN2; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 3AEC20CB2A45991900A63465 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2282,7 +2475,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.4; OTHER_CODE_SIGN_FLAGS = "--deep"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; @@ -2322,7 +2515,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.4; OTHER_CODE_SIGN_FLAGS = "--deep"; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; @@ -2345,7 +2538,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 271; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = Z56Z6NYZN2; ENABLE_HARDENED_RUNTIME = YES; @@ -2361,7 +2554,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.4; OTHER_CODE_SIGN_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; @@ -2383,7 +2576,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 271; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = Z56Z6NYZN2; ENABLE_HARDENED_RUNTIME = YES; @@ -2399,7 +2592,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.4; OTHER_CODE_SIGN_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa; PRODUCT_NAME = "sing-box"; @@ -2541,7 +2734,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.0-rc.22; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.system; PRODUCT_NAME = "$(inherited)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2577,7 +2770,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.0-rc.22; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.system; PRODUCT_NAME = "$(inherited)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2617,7 +2810,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.0-rc.22; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.independent; PRODUCT_NAME = SFM; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2656,7 +2849,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.9.0-rc.22; + MARKETING_VERSION = 1.9.4; PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.independent; PRODUCT_NAME = SFM; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2795,6 +2988,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 3AE396042C21A5CC00647718 /* Build configuration list for PBXNativeTarget "WidgetExtensionExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3AE396052C21A5CC00647718 /* Debug */, + 3AE396062C21A5CC00647718 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2882,7 +3084,7 @@ repositoryURL = "https://github.com/orchetect/MacControlCenterUI"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.1; + minimumVersion = 2.0.7; }; }; 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */ = { @@ -2893,14 +3095,6 @@ minimumVersion = 2.0.0; }; }; - 3ADB2D832A71266E00A6517D /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.1.0; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2914,11 +3108,6 @@ package = 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */; productName = QRCode; }; - 3A6CA59F2A71317A0027933B /* MarkdownUI */ = { - isa = XCSwiftPackageProductDependency; - package = 3ADB2D832A71266E00A6517D /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; - productName = MarkdownUI; - }; 3A7E90372A46778E00D53052 /* BinaryCodable */ = { isa = XCSwiftPackageProductDependency; package = 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */; diff --git a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 029a589..90b7169 100644 --- a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "82fb479ec8c73c3348b93e33a9102fc692bb5a0a0d6f14beb4c5f31445c36e81", "pins" : [ { "identity" : "binarycodable", @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/orchetect/MacControlCenterUI", "state" : { - "revision" : "e7f7e0834a146b59a9d86b5751b711eb3a57be69", - "version" : "2.0.1" + "revision" : "6565bcb33bfdb446a4fd81f3c78dce27b0ab7e09", + "version" : "2.0.8" } }, { @@ -32,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/orchetect/MenuBarExtraAccess", "state" : { - "revision" : "8757eb7c2cd708320df92e6ad6572efe90e58f16", - "version" : "1.0.4" + "revision" : "f5896b47e15e114975897354c7e1082c51a2bffd", + "version" : "1.0.5" } }, { @@ -45,15 +46,6 @@ "version" : "17.0.0" } }, - { - "identity" : "swift-markdown-ui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/swift-markdown-ui", - "state" : { - "revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9", - "version" : "2.1.0" - } - }, { "identity" : "swift-qrcode-generator", "kind" : "remoteSourceControl", @@ -73,5 +65,5 @@ } } ], - "version" : 2 + "version" : 3 }