Add connections dashboard
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,23 +50,24 @@ public struct ActiveDashboardView: View {
|
|||||||
VStack {
|
VStack {
|
||||||
#if os(iOS) || os(tvOS)
|
#if os(iOS) || os(tvOS)
|
||||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
|
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
|
||||||
Picker("Page", selection: $selection) {
|
viewBuilder {
|
||||||
ForEach(DashboardPage.allCases) { page in
|
#if os(iOS)
|
||||||
page.label
|
if #available(iOS 16.0, *) {
|
||||||
|
content1
|
||||||
|
} else {
|
||||||
|
content0
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
content0
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.padding([.leading, .trailing])
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#endif
|
#endif
|
||||||
TabView(selection: $selection) {
|
.onAppear {
|
||||||
ForEach(DashboardPage.allCases) { page in
|
UIScrollView.appearance().isScrollEnabled = false
|
||||||
page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
|
||||||
.tag(page)
|
|
||||||
}
|
}
|
||||||
}
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
|
||||||
} else {
|
} else {
|
||||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||||
}
|
}
|
||||||
@@ -90,6 +91,45 @@ public struct ActiveDashboardView: View {
|
|||||||
.alertBinding($alert)
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
private func doReload() async {
|
||||||
defer {
|
defer {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|||||||
@@ -9,6 +9,22 @@ public enum DashboardPage: Int, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
case overview
|
case overview
|
||||||
case groups
|
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 {
|
public extension DashboardPage {
|
||||||
@@ -18,15 +34,19 @@ public extension DashboardPage {
|
|||||||
return NSLocalizedString("Overview", comment: "")
|
return NSLocalizedString("Overview", comment: "")
|
||||||
case .groups:
|
case .groups:
|
||||||
return NSLocalizedString("Groups", comment: "")
|
return NSLocalizedString("Groups", comment: "")
|
||||||
|
case .connections:
|
||||||
|
return NSLocalizedString("Connections", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var label: some View {
|
var label: some View {
|
||||||
switch self {
|
switch self {
|
||||||
case .overview:
|
case .overview:
|
||||||
return Label("Overview", systemImage: "text.and.command.macwindow")
|
return Label(title, systemImage: "text.and.command.macwindow")
|
||||||
case .groups:
|
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)
|
OverviewView(profileList, selectedProfileID, systemProxyAvailable, systemProxyEnabled)
|
||||||
case .groups:
|
case .groups:
|
||||||
GroupListView()
|
GroupListView()
|
||||||
|
case .connections:
|
||||||
|
ConnectionListView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public struct LogView: View {
|
|||||||
} else {
|
} else {
|
||||||
ScrollViewReader { reader in
|
ScrollViewReader { reader in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading, spacing: 0) {
|
||||||
ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in
|
ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in
|
||||||
Text(it.element)
|
Text(it.element)
|
||||||
.font(logFont)
|
.font(logFont)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public enum NavigationPage: Int, CaseIterable, Identifiable {
|
|||||||
case dashboard
|
case dashboard
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case groups
|
case groups
|
||||||
|
case connections
|
||||||
#endif
|
#endif
|
||||||
case logs
|
case logs
|
||||||
case profiles
|
case profiles
|
||||||
@@ -35,6 +36,8 @@ public extension NavigationPage {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case .groups:
|
case .groups:
|
||||||
return NSLocalizedString("Groups", comment: "")
|
return NSLocalizedString("Groups", comment: "")
|
||||||
|
case .connections:
|
||||||
|
return NSLocalizedString("Connections", comment: "")
|
||||||
#endif
|
#endif
|
||||||
case .logs:
|
case .logs:
|
||||||
return NSLocalizedString("Logs", comment: "")
|
return NSLocalizedString("Logs", comment: "")
|
||||||
@@ -52,6 +55,8 @@ public extension NavigationPage {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case .groups:
|
case .groups:
|
||||||
return "rectangle.3.group.fill"
|
return "rectangle.3.group.fill"
|
||||||
|
case .connections:
|
||||||
|
return "list.bullet.rectangle.portrait.fill"
|
||||||
#endif
|
#endif
|
||||||
case .logs:
|
case .logs:
|
||||||
return "doc.text.fill"
|
return "doc.text.fill"
|
||||||
@@ -71,6 +76,8 @@ public extension NavigationPage {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case .groups:
|
case .groups:
|
||||||
GroupListView()
|
GroupListView()
|
||||||
|
case .connections:
|
||||||
|
ConnectionListView()
|
||||||
#endif
|
#endif
|
||||||
case .logs:
|
case .logs:
|
||||||
LogView()
|
LogView()
|
||||||
@@ -89,7 +96,7 @@ public extension NavigationPage {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
func visible(_ profile: ExtensionProfile?) -> Bool {
|
func visible(_ profile: ExtensionProfile?) -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .groups:
|
case .groups, .connections:
|
||||||
return profile?.status.isConnectedStrict == true
|
return profile?.status.isConnectedStrict == true
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public struct EditProfileView: View {
|
|||||||
}
|
}
|
||||||
if profile.type == .remote {
|
if profile.type == .remote {
|
||||||
Section("Status") {
|
Section("Status") {
|
||||||
FormTextItem("Last Updated", profile.lastUpdatedString)
|
FormTextItem("Last Updated", profile.lastUpdated!.myFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section("Action") {
|
Section("Action") {
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ public struct ProfileView: View {
|
|||||||
Text(profile.name)
|
Text(profile.name)
|
||||||
if profile.type == .remote {
|
if profile.type == .remote {
|
||||||
Spacer(minLength: 4)
|
Spacer(minLength: 4)
|
||||||
Text("Last Updated: \(profile.origin.lastUpdatedString)").font(.caption)
|
Text("Last Updated: \(profile.origin.lastUpdated!.myFormat)").font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public extension Profile {
|
public extension Date {
|
||||||
var lastUpdatedString: String {
|
var myFormat: String {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
return dateFormatter.string(from: lastUpdated!)
|
return dateFormatter.string(from: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public enum SharedPreferences {
|
|||||||
await excludeAPNsRoute.set(nil)
|
await excludeAPNsRoute.set(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connections Filter
|
||||||
|
|
||||||
|
public static let connectionStateFilter = Preference<Int>("connection_state_filter", defaultValue: 0)
|
||||||
|
public static let connectionSort = Preference<Int>("connection_sort", defaultValue: 0)
|
||||||
|
|
||||||
// On Demand Rules
|
// On Demand Rules
|
||||||
|
|
||||||
public static let alwaysOn = Preference<Bool>("always_on", defaultValue: false)
|
public static let alwaysOn = Preference<Bool>("always_on", defaultValue: false)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public class CommandClient: ObservableObject {
|
|||||||
case groups
|
case groups
|
||||||
case log
|
case log
|
||||||
case clashMode
|
case clashMode
|
||||||
|
case connections
|
||||||
}
|
}
|
||||||
|
|
||||||
private let connectionType: ConnectionType
|
private let connectionType: ConnectionType
|
||||||
@@ -21,6 +22,11 @@ public class CommandClient: ObservableObject {
|
|||||||
@Published public var clashModeList: [String]
|
@Published public var clashModeList: [String]
|
||||||
@Published public var clashMode: 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) {
|
public init(_ connectionType: ConnectionType, logMaxLines: Int = 300) {
|
||||||
self.connectionType = connectionType
|
self.connectionType = connectionType
|
||||||
self.logMaxLines = logMaxLines
|
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 {
|
private nonisolated func connect0() async {
|
||||||
|
if connectionType == .connections {
|
||||||
|
await initializeConnectionFilterState()
|
||||||
|
}
|
||||||
|
|
||||||
let clientOptions = LibboxCommandClientOptions()
|
let clientOptions = LibboxCommandClientOptions()
|
||||||
switch connectionType {
|
switch connectionType {
|
||||||
case .status:
|
case .status:
|
||||||
@@ -64,6 +104,8 @@ public class CommandClient: ObservableObject {
|
|||||||
clientOptions.command = LibboxCommandLog
|
clientOptions.command = LibboxCommandLog
|
||||||
case .clashMode:
|
case .clashMode:
|
||||||
clientOptions.command = LibboxCommandClashMode
|
clientOptions.command = LibboxCommandClashMode
|
||||||
|
case .connections:
|
||||||
|
clientOptions.command = LibboxCommandConnections
|
||||||
}
|
}
|
||||||
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
|
clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC)
|
||||||
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)!
|
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)!
|
||||||
@@ -101,10 +143,13 @@ public class CommandClient: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnected(_: String?) {
|
func disconnected(_ message: String?) {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
commandClient.isConnected = false
|
commandClient.isConnected = false
|
||||||
}
|
}
|
||||||
|
if let message {
|
||||||
|
NSLog("client disconnected: \(message)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearLog() {
|
func clearLog() {
|
||||||
@@ -156,5 +201,62 @@ public class CommandClient: ObservableObject {
|
|||||||
commandClient.clashMode = newMode!
|
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: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
import Libbox
|
||||||
|
|
||||||
extension LibboxStringIteratorProtocol {
|
public extension LibboxStringIteratorProtocol {
|
||||||
func toArray() -> [String] {
|
func toArray() -> [String] {
|
||||||
var array: [String] = []
|
var array: [String] = []
|
||||||
while hasNext() {
|
while hasNext() {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public struct SidebarView: View {
|
|||||||
.tint(.textColor)
|
.tint(.textColor)
|
||||||
.tag(NavigationPage.dashboard)
|
.tag(NavigationPage.dashboard)
|
||||||
NavigationPage.groups.label.tag(NavigationPage.groups)
|
NavigationPage.groups.label.tag(NavigationPage.groups)
|
||||||
|
NavigationPage.connections.label.tag(NavigationPage.connections)
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
ForEach(NavigationPage.macosDefaultPages, id: \.self) { it in
|
ForEach(NavigationPage.macosDefaultPages, id: \.self) { it in
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
3A27D9022A89C6870031EBCC /* ExtensionEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */; };
|
3A27D9022A89C6870031EBCC /* ExtensionEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */; };
|
||||||
3A2EAEED2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */; };
|
3A2EAEED2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */; };
|
||||||
3A2F29EB2C998A5D007E024C /* Export.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3A2F29EA2C998A5D007E024C /* Export.plist */; };
|
3A2F29EB2C998A5D007E024C /* Export.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3A2F29EA2C998A5D007E024C /* Export.plist */; };
|
||||||
|
3A334ED02C0F621E00E9C577 /* ConnectionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */; };
|
||||||
3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
|
||||||
3A3AA7FF2A4EFDB3002F78AB /* 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 */; };
|
3A3AB2A72B70C146001815AE /* CoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3AB2A62B70C146001815AE /* CoreView.swift */; };
|
||||||
@@ -68,6 +69,9 @@
|
|||||||
3A60CC272B70880100D2D682 /* PacketTunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A60CC262B70880100D2D682 /* PacketTunnelView.swift */; };
|
3A60CC272B70880100D2D682 /* PacketTunnelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A60CC262B70880100D2D682 /* PacketTunnelView.swift */; };
|
||||||
3A60CC292B70A7C400D2D682 /* Color+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A60CC282B70A7C400D2D682 /* Color+Extension.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 */; };
|
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 */; };
|
3A648D2D2A4EEAA600D95A12 /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A648D2C2A4EEAA600D95A12 /* Library.swift */; };
|
||||||
3A648D542A4EF4C700D95A12 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
|
3A648D542A4EF4C700D95A12 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
|
||||||
3A6CA4542BC19FDE0012B238 /* OnDemandRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */; };
|
3A6CA4542BC19FDE0012B238 /* OnDemandRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */; };
|
||||||
@@ -89,6 +93,7 @@
|
|||||||
3A99B42A2A7526990010D4B0 /* NavigationStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */; };
|
3A99B42A2A7526990010D4B0 /* NavigationStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */; };
|
||||||
3A99B42C2A75288C0010D4B0 /* ViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */; };
|
3A99B42C2A75288C0010D4B0 /* ViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */; };
|
||||||
3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.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 */; };
|
3AABFD432A9CC5A7005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD422A9CC5A7005A24A4 /* Upload.plist */; };
|
||||||
3AABFD472A9CCC58005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD462A9CCC58005A24A4 /* Upload.plist */; };
|
3AABFD472A9CCC58005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD462A9CCC58005A24A4 /* Upload.plist */; };
|
||||||
3AB1220B2A70FD500087CD55 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1220A2A70FD500087CD55 /* Alert.swift */; };
|
3AB1220B2A70FD500087CD55 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1220A2A70FD500087CD55 /* Alert.swift */; };
|
||||||
@@ -456,6 +461,7 @@
|
|||||||
3A27D8FF2A89BE230031EBCC /* CommandClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandClient.swift; sourceTree = "<group>"; };
|
3A27D8FF2A89BE230031EBCC /* CommandClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandClient.swift; sourceTree = "<group>"; };
|
||||||
3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionEnvironments.swift; sourceTree = "<group>"; };
|
3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionEnvironments.swift; sourceTree = "<group>"; };
|
||||||
3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndependentApplicationDelegate.swift; sourceTree = "<group>"; };
|
3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndependentApplicationDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailsView.swift; sourceTree = "<group>"; };
|
||||||
3A2F29EA2C998A5D007E024C /* Export.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Export.plist; sourceTree = "<group>"; };
|
3A2F29EA2C998A5D007E024C /* Export.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Export.plist; sourceTree = "<group>"; };
|
||||||
3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailsView.swift; sourceTree = "<group>"; };
|
3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailsView.swift; sourceTree = "<group>"; };
|
||||||
3A3AB2A62B70C146001815AE /* CoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreView.swift; sourceTree = "<group>"; };
|
3A3AB2A62B70C146001815AE /* CoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreView.swift; sourceTree = "<group>"; };
|
||||||
@@ -482,6 +488,9 @@
|
|||||||
3A60CC262B70880100D2D682 /* PacketTunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelView.swift; sourceTree = "<group>"; };
|
3A60CC262B70880100D2D682 /* PacketTunnelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelView.swift; sourceTree = "<group>"; };
|
||||||
3A60CC282B70A7C400D2D682 /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = "<group>"; };
|
3A60CC282B70A7C400D2D682 /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = "<group>"; };
|
||||||
3A60CC2A2B70AD6700D2D682 /* SettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = "<group>"; };
|
3A60CC2A2B70AD6700D2D682 /* SettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = "<group>"; };
|
||||||
|
3A63269D2C0DE12D0076E274 /* ConnectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionListView.swift; sourceTree = "<group>"; };
|
||||||
|
3A63269F2C0DE15C0076E274 /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = "<group>"; };
|
||||||
|
3A6326A12C0DE64F0076E274 /* ConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = "<group>"; };
|
||||||
3A648D2C2A4EEAA600D95A12 /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = "<group>"; };
|
3A648D2C2A4EEAA600D95A12 /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = "<group>"; };
|
||||||
3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandRulesView.swift; sourceTree = "<group>"; };
|
3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandRulesView.swift; sourceTree = "<group>"; };
|
||||||
3A6CA5A52A713AA10027933B /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
3A6CA5A52A713AA10027933B /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
||||||
@@ -497,6 +506,7 @@
|
|||||||
3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCompat.swift; sourceTree = "<group>"; };
|
3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCompat.swift; sourceTree = "<group>"; };
|
||||||
3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCompat.swift; sourceTree = "<group>"; };
|
3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCompat.swift; sourceTree = "<group>"; };
|
||||||
3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDestinationCompat.swift; sourceTree = "<group>"; };
|
3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDestinationCompat.swift; sourceTree = "<group>"; };
|
||||||
|
3A9E6EBE2C0F20B0005061F3 /* ConnectionListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionListPage.swift; sourceTree = "<group>"; };
|
||||||
3AA1ABB92A4C4054000FD4BA /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
|
3AA1ABB92A4C4054000FD4BA /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
|
||||||
3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLogView.swift; sourceTree = "<group>"; };
|
3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLogView.swift; sourceTree = "<group>"; };
|
||||||
3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileContentView.swift; sourceTree = "<group>"; };
|
3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileContentView.swift; sourceTree = "<group>"; };
|
||||||
@@ -724,6 +734,18 @@
|
|||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3A63269C2C0DE10B0076E274 /* Connections */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3A63269D2C0DE12D0076E274 /* ConnectionListView.swift */,
|
||||||
|
3A63269F2C0DE15C0076E274 /* Connection.swift */,
|
||||||
|
3A6326A12C0DE64F0076E274 /* ConnectionView.swift */,
|
||||||
|
3A9E6EBE2C0F20B0005061F3 /* ConnectionListPage.swift */,
|
||||||
|
3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */,
|
||||||
|
);
|
||||||
|
path = Connections;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3A6CA5A42A713A6C0027933B /* Icons */ = {
|
3A6CA5A42A713A6C0027933B /* Icons */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -946,6 +968,7 @@
|
|||||||
3AEC21732A45B0AC00A63465 /* Views */ = {
|
3AEC21732A45B0AC00A63465 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
3A63269C2C0DE10B0076E274 /* Connections */,
|
||||||
3AF342D22A4AADA5002B34AC /* Abstract */,
|
3AF342D22A4AADA5002B34AC /* Abstract */,
|
||||||
3AF342A12A4A9B8D002B34AC /* Dashboard */,
|
3AF342A12A4A9B8D002B34AC /* Dashboard */,
|
||||||
3A1CF2EE2A50E5D5000A8289 /* Groups */,
|
3A1CF2EE2A50E5D5000A8289 /* Groups */,
|
||||||
@@ -1481,6 +1504,7 @@
|
|||||||
3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */,
|
3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */,
|
||||||
3A60CC272B70880100D2D682 /* PacketTunnelView.swift in Sources */,
|
3A60CC272B70880100D2D682 /* PacketTunnelView.swift in Sources */,
|
||||||
3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */,
|
3A4EAD222A4FEB54005435B3 /* NavigationPage.swift in Sources */,
|
||||||
|
3A9E6EBF2C0F20B0005061F3 /* ConnectionListPage.swift in Sources */,
|
||||||
3A411CEC2B734959000D9501 /* MacAppView.swift in Sources */,
|
3A411CEC2B734959000D9501 /* MacAppView.swift in Sources */,
|
||||||
3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */,
|
3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */,
|
||||||
3A4EAD292A4FEB6D005435B3 /* FormItem.swift in Sources */,
|
3A4EAD292A4FEB6D005435B3 /* FormItem.swift in Sources */,
|
||||||
@@ -1490,6 +1514,7 @@
|
|||||||
3A172D2B2B88E9DB00D98050 /* BackButton.swift in Sources */,
|
3A172D2B2B88E9DB00D98050 /* BackButton.swift in Sources */,
|
||||||
3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */,
|
3A4EAD242A4FEB65005435B3 /* InstallProfileButton.swift in Sources */,
|
||||||
3AE4D0C12A6E4852009FEA9E /* InstallSystemExtensionButton.swift in Sources */,
|
3AE4D0C12A6E4852009FEA9E /* InstallSystemExtensionButton.swift in Sources */,
|
||||||
|
3A6326A22C0DE64F0076E274 /* ConnectionView.swift in Sources */,
|
||||||
3A4EAD232A4FEB5A005435B3 /* EnvironmentValues.swift in Sources */,
|
3A4EAD232A4FEB5A005435B3 /* EnvironmentValues.swift in Sources */,
|
||||||
3A4F68B02A97602C003D66D3 /* ClashModeView.swift in Sources */,
|
3A4F68B02A97602C003D66D3 /* ClashModeView.swift in Sources */,
|
||||||
3A4EAD282A4FEB65005435B3 /* ActiveDashboardView.swift in Sources */,
|
3A4EAD282A4FEB65005435B3 /* ActiveDashboardView.swift in Sources */,
|
||||||
@@ -1514,8 +1539,11 @@
|
|||||||
3ACA8B332B7E037800B7238F /* DeleteButton.swift in Sources */,
|
3ACA8B332B7E037800B7238F /* DeleteButton.swift in Sources */,
|
||||||
3ACE6DE32ACADF55009D9A8A /* Binding+Setter.swift in Sources */,
|
3ACE6DE32ACADF55009D9A8A /* Binding+Setter.swift in Sources */,
|
||||||
3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */,
|
3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */,
|
||||||
|
3A63269E2C0DE12D0076E274 /* ConnectionListView.swift in Sources */,
|
||||||
3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */,
|
3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */,
|
||||||
|
3A6326A02C0DE15C0076E274 /* Connection.swift in Sources */,
|
||||||
3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */,
|
3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */,
|
||||||
|
3A334ED02C0F621E00E9C577 /* ConnectionDetailsView.swift in Sources */,
|
||||||
3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */,
|
3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */,
|
||||||
3A0C6D3E2A79D6A600A4DF2B /* DashboardPage.swift in Sources */,
|
3A0C6D3E2A79D6A600A4DF2B /* DashboardPage.swift in Sources */,
|
||||||
3A3AB2A92B70C5F1001815AE /* RequestReviewButton.swift in Sources */,
|
3A3AB2A92B70C5F1001815AE /* RequestReviewButton.swift in Sources */,
|
||||||
@@ -2885,7 +2913,7 @@
|
|||||||
repositoryURL = "https://github.com/orchetect/MacControlCenterUI";
|
repositoryURL = "https://github.com/orchetect/MacControlCenterUI";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 2.0.1;
|
minimumVersion = 2.0.7;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */ = {
|
3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */ = {
|
||||||
|
|||||||
Reference in New Issue
Block a user