This commit is contained in:
mrFq1
2023-04-25 14:51:23 +08:00
commit f3141fbffe
39 changed files with 3793 additions and 0 deletions
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<tableCellView identifier="CollectionTableCellView" id="E1E-Yw-tO2">
<rect key="frame" x="0.0" y="0.0" width="100" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="EkO-g0-3Rf">
<rect key="frame" x="0.0" y="1" width="100" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="mZD-CG-RaE">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="EkO-g0-3Rf" secondAttribute="trailing" constant="2" id="cZb-2s-5cP"/>
<constraint firstItem="EkO-g0-3Rf" firstAttribute="centerY" secondItem="E1E-Yw-tO2" secondAttribute="centerY" id="g8v-Gi-bjM"/>
<constraint firstItem="EkO-g0-3Rf" firstAttribute="leading" secondItem="E1E-Yw-tO2" secondAttribute="leading" constant="2" id="nDQ-TP-ijK"/>
</constraints>
<accessibility identifier="CollectionTableCellView"/>
<connections>
<outlet property="textField" destination="EkO-g0-3Rf" id="Kzu-HK-Onq"/>
</connections>
<point key="canvasLocation" x="-108" y="-102"/>
</tableCellView>
</objects>
</document>
@@ -0,0 +1,271 @@
//
// CollectionsTableView.swift
// ClashX Dashboard
//
//
import SwiftUI
import AppKit
import DifferenceKit
struct CollectionsTableView<Item: Hashable>: NSViewRepresentable {
enum TableColumn: String, CaseIterable {
case host = "Host"
case sniffHost = "Sniff Host"
case process = "Process"
case dl = "DL"
case ul = "UL"
case chain = "Chain"
case rule = "Rule"
case time = "Time"
case source = "Source"
case destinationIP = "Destination IP"
case type = "Type"
}
var data: [Item]
var filterString: String
var startFormatter: RelativeDateTimeFormatter = {
let startFormatter = RelativeDateTimeFormatter()
startFormatter.unitsStyle = .short
return startFormatter
}()
var byteCountFormatter = ByteCountFormatter()
class NonRespondingScrollView: NSScrollView {
override var acceptsFirstResponder: Bool { false }
}
class NonRespondingTableView: NSTableView {
override var acceptsFirstResponder: Bool { false }
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NonRespondingScrollView()
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = true
let tableView = NonRespondingTableView()
tableView.usesAlternatingRowBackgroundColors = true
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(.init(nibNamed: .init("CollectionTableCellView"), bundle: .main), forIdentifier: .init(rawValue: "CollectionTableCellView"))
TableColumn.allCases.forEach {
let tableColumn = NSTableColumn(identifier: .init($0.rawValue))
tableColumn.title = $0.rawValue
tableColumn.isEditable = false
tableColumn.minWidth = 50
tableColumn.maxWidth = .infinity
tableView.addTableColumn(tableColumn)
var sort: NSSortDescriptor?
switch $0 {
case .host:
sort = .init(keyPath: \ClashConnectionObject.host, ascending: true)
case .sniffHost:
sort = .init(keyPath: \ClashConnectionObject.sniffHost, ascending: true)
case .process:
sort = .init(keyPath: \ClashConnectionObject.process, ascending: true)
case .dl:
sort = .init(keyPath: \ClashConnectionObject.download, ascending: true)
case .ul:
sort = .init(keyPath: \ClashConnectionObject.upload, ascending: true)
case .chain:
sort = .init(keyPath: \ClashConnectionObject.chainString, ascending: true)
case .rule:
sort = .init(keyPath: \ClashConnectionObject.ruleString, ascending: true)
case .time:
sort = .init(keyPath: \ClashConnectionObject.startDate, ascending: true)
case .source:
sort = .init(keyPath: \ClashConnectionObject.source, ascending: true)
case .destinationIP:
sort = .init(keyPath: \ClashConnectionObject.destinationIP, ascending: true)
case .type:
sort = .init(keyPath: \ClashConnectionObject.type, ascending: true)
default:
sort = nil
}
tableColumn.sortDescriptorPrototype = sort
}
if let sort = tableView.tableColumns.first?.sortDescriptorPrototype {
tableView.sortDescriptors = [sort]
}
scrollView.documentView = tableView
return scrollView
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
context.coordinator.parent = self
guard let tableView = nsView.documentView as? NSTableView,
let data = data as? [ClashConnection] else {
return
}
let target = updateSorts(data.map(ClashConnectionObject.init), tableView: tableView)
let source = context.coordinator.conns
let changeset = StagedChangeset(source: source, target: target)
tableView.reload(using: changeset) { data in
context.coordinator.conns = data
}
}
func updateSorts(_ objects: [ClashConnectionObject],
tableView: NSTableView) -> [ClashConnectionObject] {
var re = objects
var sortDescriptors = tableView.sortDescriptors
sortDescriptors.append(.init(keyPath: \ClashConnectionObject.id, ascending: true))
re = re.sorted(descriptors: sortDescriptors)
let filterKeys = [
"host",
"process",
"chainString",
"ruleString",
"source",
"destinationIP",
"type",
]
re = re.filtered(filterString, for: filterKeys)
return re
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
var parent: CollectionsTableView
var conns = [ClashConnectionObject]()
init(parent: CollectionsTableView) {
self.parent = parent
}
func numberOfRows(in tableView: NSTableView) -> Int {
conns.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let cell = tableView.makeView(withIdentifier: .init(rawValue: "CollectionTableCellView"), owner: nil) as? NSTableCellView,
let s = tableColumn?.identifier.rawValue.split(separator: ".").last,
let tc = TableColumn(rawValue: String(s))
else { return nil }
let conn = conns[row]
cell.textField?.objectValue = {
switch tc {
case .host:
return conn.host
case .sniffHost:
return conn.sniffHost
case .process:
return conn.process
case .dl:
return conn.downloadString
case .ul:
return conn.uploadString
case .chain:
return conn.chainString
case .rule:
return conn.ruleString
case .time:
return conn.startString
case .source:
return conn.source
case .destinationIP:
return conn.destinationIP
case .type:
return conn.type
}
}()
return cell
}
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
conns = parent.updateSorts(conns, tableView: tableView)
tableView.reloadData()
}
}
}
extension NSTableView {
/// Applies multiple animated updates in stages using `StagedChangeset`.
///
/// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`.
/// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it.
/// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages.
///
/// - Parameters:
/// - stagedChangeset: A staged set of changes.
/// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated
/// updates should be stopped and performed reloadData. Default is nil.
/// - setData: A closure that takes the collection as a parameter.
/// The collection should be set to data-source of NSTableView.
func reload<C>(
using stagedChangeset: StagedChangeset<C>,
interrupt: ((Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void
) {
if case .none = window, let data = stagedChangeset.last?.data {
setData(data)
return reloadData()
}
for changeset in stagedChangeset {
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
setData(data)
return reloadData()
}
beginUpdates()
setData(changeset.data)
if !changeset.elementDeleted.isEmpty {
removeRows(at: IndexSet(changeset.elementDeleted.map { $0.element }))
}
if !changeset.elementUpdated.isEmpty {
reloadData(forRowIndexes: IndexSet(changeset.elementUpdated.map { $0.element }), columnIndexes: IndexSet(0..<tableColumns.count))
}
if !changeset.elementInserted.isEmpty {
insertRows(at: IndexSet(changeset.elementInserted.map { $0.element }))
}
endUpdates()
}
}
}
@@ -0,0 +1,37 @@
//
// Connections.swift
// ClashX Dashboard
//
//
import Cocoa
class Connections: ObservableObject, Identifiable {
let id = UUID()
@Published var items: [ConnectionItem]
init(_ items: [ConnectionItem]) {
self.items = items
}
}
class ConnectionItem: ObservableObject, Decodable {
let id: String
let host: String
let sniffHost: String
let process: String
let dl: String
let ul: String
let dlSpeed: String
let ulSpeed: String
let chains: String
let rule: String
let time: String
let source: String
let destinationIP: String
let type: String
}
@@ -0,0 +1,32 @@
//
// ConnectionsView.swift
// ClashX Dashboard
//
//
import SwiftUI
struct ConnectionsView: View {
@EnvironmentObject var data: ClashConnsStorage
@State private var searchString: String = ""
var body: some View {
CollectionsTableView(data: data.conns,
filterString: searchString)
.background(.white)
.searchable(text: $searchString)
}
}
struct ConnectionsView_Previews: PreviewProvider {
static var previews: some View {
ConnectionsView()
}
}