mirror of
https://github.com/yJason/ClashX-Dashboard.git
synced 2026-02-04 10:02:26 +08:00
298 lines
7.6 KiB
Swift
298 lines
7.6 KiB
Swift
//
|
|
// ConnectionsTableView.swift
|
|
// ClashX Dashboard
|
|
//
|
|
//
|
|
import SwiftUI
|
|
import AppKit
|
|
|
|
struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
|
|
|
enum TableColumn: String, CaseIterable {
|
|
case host = "Host"
|
|
case sniffHost = "Sniff Host"
|
|
case process = "Process"
|
|
case dlSpeed = "DL Speed"
|
|
case ulSpeed = "UL Speed"
|
|
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
|
|
scrollView.autohidesScrollers = true
|
|
|
|
let tableView = NonRespondingTableView()
|
|
tableView.usesAlternatingRowBackgroundColors = true
|
|
|
|
tableView.delegate = context.coordinator
|
|
tableView.dataSource = context.coordinator
|
|
|
|
let menu = NSMenu()
|
|
menu.showsStateColumn = true
|
|
tableView.headerView?.menu = menu
|
|
|
|
|
|
TableColumn.allCases.forEach {
|
|
let tableColumn = NSTableColumn(identifier: .init("ConnectionsTableView." + $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: \DBConnectionObject.host, ascending: true)
|
|
case .sniffHost:
|
|
sort = .init(keyPath: \DBConnectionObject.sniffHost, ascending: true)
|
|
case .process:
|
|
sort = .init(keyPath: \DBConnectionObject.process, ascending: true)
|
|
case .dlSpeed:
|
|
sort = .init(keyPath: \DBConnectionObject.downloadSpeed, ascending: true)
|
|
case .ulSpeed:
|
|
sort = .init(keyPath: \DBConnectionObject.uploadSpeed, ascending: true)
|
|
case .dl:
|
|
sort = .init(keyPath: \DBConnectionObject.download, ascending: true)
|
|
case .ul:
|
|
sort = .init(keyPath: \DBConnectionObject.upload, ascending: true)
|
|
case .chain:
|
|
sort = .init(keyPath: \DBConnectionObject.chainString, ascending: true)
|
|
case .rule:
|
|
sort = .init(keyPath: \DBConnectionObject.ruleString, ascending: true)
|
|
case .time:
|
|
sort = .init(keyPath: \DBConnectionObject.startDate, ascending: true)
|
|
case .source:
|
|
sort = .init(keyPath: \DBConnectionObject.source, ascending: true)
|
|
case .destinationIP:
|
|
sort = .init(keyPath: \DBConnectionObject.destinationIP, ascending: true)
|
|
case .type:
|
|
sort = .init(keyPath: \DBConnectionObject.type, ascending: true)
|
|
}
|
|
|
|
tableColumn.sortDescriptorPrototype = sort
|
|
|
|
let item = NSMenuItem(
|
|
title: $0.rawValue,
|
|
action: #selector(context.coordinator.toggleColumn(_:)),
|
|
keyEquivalent: "")
|
|
item.target = context.coordinator
|
|
item.representedObject = tableColumn
|
|
|
|
menu.addItem(item)
|
|
}
|
|
|
|
|
|
if let sort = tableView.tableColumns.first?.sortDescriptorPrototype {
|
|
tableView.sortDescriptors = [sort]
|
|
}
|
|
|
|
|
|
scrollView.documentView = tableView
|
|
|
|
tableView.autosaveName = "ClashX_Dashboard.Connections.TableView"
|
|
tableView.autosaveTableColumns = true
|
|
|
|
menu.items.forEach {
|
|
guard let column = $0.representedObject as? NSTableColumn else { return }
|
|
$0.state = column.isHidden ? .off : .on
|
|
}
|
|
|
|
return scrollView
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSScrollView, context: Context) {
|
|
context.coordinator.parent = self
|
|
guard let tableView = nsView.documentView as? NSTableView,
|
|
let data = data as? [DBConnection] else {
|
|
return
|
|
}
|
|
|
|
var conns = data.map(DBConnectionObject.init)
|
|
|
|
let connHistorys = context.coordinator.connHistorys
|
|
conns.forEach {
|
|
$0.updateSpeeds(connHistorys[$0.id])
|
|
}
|
|
|
|
conns = updateSorts(conns, tableView: tableView)
|
|
context.coordinator.updateConns(conns, for: tableView)
|
|
}
|
|
|
|
func updateSorts(_ objects: [DBConnectionObject],
|
|
tableView: NSTableView) -> [DBConnectionObject] {
|
|
var re = objects
|
|
|
|
var sortDescriptors = [NSSortDescriptor]()
|
|
|
|
if let sort = tableView.sortDescriptors.first {
|
|
sortDescriptors.append(sort)
|
|
}
|
|
|
|
sortDescriptors.append(.init(keyPath: \DBConnectionObject.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: ConnectionsTableView
|
|
|
|
var conns = [DBConnectionObject]()
|
|
var connHistorys = [String: (download: Int64, upload: Int64)]()
|
|
|
|
init(parent: ConnectionsTableView) {
|
|
self.parent = parent
|
|
}
|
|
|
|
func updateConns(_ conns: [DBConnectionObject], for tableView: NSTableView) {
|
|
let changes = conns.difference(from: self.conns) {
|
|
$0.id == $1.id
|
|
}
|
|
|
|
for change in changes {
|
|
switch change {
|
|
case .remove(_, let conn, _):
|
|
connHistorys[conn.id] = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
conns.forEach {
|
|
connHistorys[$0.id] = ($0.download, $0.upload)
|
|
}
|
|
|
|
guard let partialChanges = self.conns.applying(changes) else {
|
|
return
|
|
}
|
|
self.conns = conns
|
|
|
|
let indicesToReload = IndexSet(zip(partialChanges, conns).enumerated().compactMap { index, pair -> Int? in
|
|
(pair.0.id == pair.1.id && pair.0 != pair.1) ? index : nil
|
|
})
|
|
|
|
tableView.reloadData(changes, indexs: indicesToReload)
|
|
}
|
|
|
|
|
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
conns.count
|
|
}
|
|
|
|
|
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
|
|
|
guard let identifier = tableColumn?.identifier,
|
|
let cellView = tableView.makeCellView(with: identifier.rawValue, owner: self),
|
|
let s = identifier.rawValue.split(separator: ".").last,
|
|
let tc = TableColumn(rawValue: String(s)),
|
|
row >= 0,
|
|
row < conns.count,
|
|
let tf = cellView.textField
|
|
else { return nil }
|
|
|
|
let conn = conns[row]
|
|
|
|
tf.objectValue = {
|
|
switch tc {
|
|
case .host:
|
|
return conn.host
|
|
case .sniffHost:
|
|
return conn.sniffHost
|
|
case .process:
|
|
return conn.process
|
|
case .dlSpeed:
|
|
return conn.downloadSpeedString
|
|
// return conn.downloadSpeed
|
|
case .ulSpeed:
|
|
return conn.uploadSpeedString
|
|
// return conn.uploadSpeed
|
|
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 cellView
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
|
|
conns = parent.updateSorts(conns, tableView: tableView)
|
|
tableView.reloadData()
|
|
}
|
|
|
|
@objc func toggleColumn(_ menuItem: NSMenuItem) {
|
|
guard let column = menuItem.representedObject as? NSTableColumn else { return }
|
|
let hide = menuItem.state == .on
|
|
column.isHidden = hide
|
|
menuItem.state = hide ? .off : .on
|
|
}
|
|
|
|
}
|
|
}
|