From a25ca56d1cc67a27e6f1855876004ea5cd372b30 Mon Sep 17 00:00:00 2001 From: mrFq1 <1xxbx0il0@mozmail.com> Date: Fri, 8 Sep 2023 22:17:43 +0800 Subject: [PATCH 1/2] misc: swift difference --- .../xcshareddata/swiftpm/Package.resolved | 9 -- Package.resolved | 9 -- Package.swift | 2 - .../Models/DBConnectionSnapShot.swift | 6 +- .../Views/ClashApiDatasStorage.swift | 15 +- .../Connections/ConnectionsTableView.swift | 148 +++++------------- .../ContentTabs/Logs/LogsTableView.swift | 69 ++++---- .../Views/NSTableViewExtension.swift | 72 +++++++++ 8 files changed, 155 insertions(+), 175 deletions(-) create mode 100644 Sources/ClashX Dashboard/Views/NSTableViewExtension.swift diff --git a/Examples/ClashX Dashboard Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/ClashX Dashboard Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 52314e6..71d61bc 100644 --- a/Examples/ClashX Dashboard Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/ClashX Dashboard Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,15 +18,6 @@ "version" : "3.8.0" } }, - { - "identity" : "differencekit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ra1028/DifferenceKit", - "state" : { - "revision" : "073b9671ce2b9b5b96398611427a1f929927e428", - "version" : "1.3.0" - } - }, { "identity" : "dsfsparkline", "kind" : "remoteSourceControl", diff --git a/Package.resolved b/Package.resolved index 5122eae..ff8ee7a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,15 +18,6 @@ "version" : "3.8.0" } }, - { - "identity" : "differencekit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ra1028/DifferenceKit.git", - "state" : { - "revision" : "073b9671ce2b9b5b96398611427a1f929927e428", - "version" : "1.3.0" - } - }, { "identity" : "dsfsparkline", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 34f6915..7dfcc25 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,6 @@ let package = Package( .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"), .package(url: "https://github.com/daltoniam/Starscream.git", exact: "3.1.1"), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", from: "3.0.0"), - .package(url: "https://github.com/ra1028/DifferenceKit.git", from: "1.0.0"), .package(url: "https://github.com/dagronf/DSFSparkline.git", from: "4.0.0"), .package(url: "https://github.com/siteline/swiftui-introspect", from: "0.10.0"), ], @@ -33,7 +32,6 @@ let package = Package( "Alamofire", "CocoaLumberjack", .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), - "DifferenceKit", "DSFSparkline", .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"), "Starscream", diff --git a/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift b/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift index bdb4372..92ab954 100644 --- a/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift +++ b/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift @@ -5,7 +5,6 @@ // import Cocoa -import DifferenceKit struct DBConnectionSnapShot: Codable { let downloadTotal: Int @@ -49,7 +48,7 @@ struct DBMetaConnectionData: Codable, Hashable { } -class DBConnectionObject: NSObject, Differentiable { +class DBConnectionObject: NSObject { @objc let id: String @objc let host: String @objc let sniffHost: String @@ -67,9 +66,6 @@ class DBConnectionObject: NSObject, Differentiable { @objc let destinationIP: String? @objc let type: String - var differenceIdentifier: String { - return id - } func isContentEqual(to source: DBConnectionObject) -> Bool { download == source.download && diff --git a/Sources/ClashX Dashboard/Views/ClashApiDatasStorage.swift b/Sources/ClashX Dashboard/Views/ClashApiDatasStorage.swift index addab5a..b8b317a 100644 --- a/Sources/ClashX Dashboard/Views/ClashApiDatasStorage.swift +++ b/Sources/ClashX Dashboard/Views/ClashApiDatasStorage.swift @@ -7,7 +7,6 @@ import Cocoa import SwiftUI import CocoaLumberjackSwift -import DifferenceKit class ClashApiDatasStorage: NSObject, ObservableObject { @@ -126,25 +125,23 @@ class ClashOverviewData: ObservableObject, Identifiable { class ClashLogStorage: ObservableObject { @Published var logs = [ClashLog]() - class ClashLog: NSObject, ObservableObject, Identifiable, Differentiable { + class ClashLog: NSObject, ObservableObject { let id: String - var differenceIdentifier: String { - return id - } let date: Date let level: ClashLogLevel - @objc let log: String + let log: String let levelColor: NSColor - @objc let levelString: String + let levelString: String init(level: String, log: String) { - self.date = Date() + id = UUID().uuidString + date = Date() + self.level = .init(rawValue: level) ?? .unknow self.log = log - id = "\(date)" + log self.levelString = level switch self.level { case .info: diff --git a/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift b/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift index ee31825..dc44563 100644 --- a/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift +++ b/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift @@ -5,7 +5,6 @@ // import SwiftUI import AppKit -import DifferenceKit struct ConnectionsTableView: NSViewRepresentable { @@ -62,7 +61,7 @@ struct ConnectionsTableView: NSViewRepresentable { TableColumn.allCases.forEach { - let tableColumn = NSTableColumn(identifier: .init($0.rawValue)) + let tableColumn = NSTableColumn(identifier: .init("ConnectionsTableView." + $0.rawValue)) tableColumn.title = $0.rawValue tableColumn.isEditable = false @@ -139,22 +138,24 @@ struct ConnectionsTableView: NSViewRepresentable { return } - let target = updateSorts(data.map(DBConnectionObject.init), tableView: tableView) + var conns = data.map(DBConnectionObject.init) - let source = context.coordinator.conns - let changeset = StagedChangeset(source: source, target: target) - - - tableView.reload(using: changeset) { data in - context.coordinator.conns = data } + + conns = updateSorts(conns, tableView: tableView) + context.coordinator.updateConns(conns, for: tableView) } func updateSorts(_ objects: [DBConnectionObject], tableView: NSTableView) -> [DBConnectionObject] { var re = objects - var sortDescriptors = tableView.sortDescriptors + 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) @@ -188,6 +189,22 @@ struct ConnectionsTableView: NSViewRepresentable { self.parent = parent } + func updateConns(_ conns: [DBConnectionObject], for tableView: NSTableView) { + let changes = conns.difference(from: self.conns) { + $0.id == $1.id + } + 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 @@ -196,14 +213,18 @@ struct ConnectionsTableView: NSViewRepresentable { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - guard let cellView = tableView.createCellView(with: "ConnsTableCellView"), - let s = tableColumn?.identifier.rawValue.split(separator: ".").last, - let tc = TableColumn(rawValue: String(s)) + 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] - cellView.textField?.objectValue = { + tf.objectValue = { switch tc { case .host: return conn.host @@ -247,102 +268,3 @@ struct ConnectionsTableView: NSViewRepresentable { } } - - -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( - using stagedChangeset: StagedChangeset, - interrupt: ((Changeset) -> 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.. NSTableCellView? { - // https://stackoverflow.com/a/27624927 - - var cellView: NSTableCellView? - if let spareView = makeView(withIdentifier: .init(identifier), - owner: self) as? NSTableCellView { - - // We can use an old cell - no need to do anything. - cellView = spareView - - } else { - - // Create a text field for the cell - let textField = NSTextField() - textField.backgroundColor = NSColor.clear - textField.translatesAutoresizingMaskIntoConstraints = false - textField.isBordered = false - textField.font = .systemFont(ofSize: 13) - textField.lineBreakMode = .byTruncatingTail - - // Create a cell - let newCell = NSTableCellView() - newCell.identifier = .init(identifier) - newCell.addSubview(textField) - newCell.textField = textField - - // Constrain the text field within the cell - newCell.addConstraints( - NSLayoutConstraint.constraints(withVisualFormat: "H:|[textField]|", - options: [], - metrics: nil, - views: ["textField" : textField])) - - newCell.addConstraint(.init(item: textField, attribute: .centerY, relatedBy: .equal, toItem: newCell, attribute: .centerY, multiplier: 1, constant: 0)) - - - textField.bind(NSBindingName.value, - to: newCell, - withKeyPath: "objectValue", - options: nil) - - cellView = newCell - } - - return cellView - } - -} diff --git a/Sources/ClashX Dashboard/Views/ContentTabs/Logs/LogsTableView.swift b/Sources/ClashX Dashboard/Views/ContentTabs/Logs/LogsTableView.swift index b7ea20c..3f346dd 100644 --- a/Sources/ClashX Dashboard/Views/ContentTabs/Logs/LogsTableView.swift +++ b/Sources/ClashX Dashboard/Views/ContentTabs/Logs/LogsTableView.swift @@ -6,7 +6,6 @@ import Cocoa import SwiftUI -import DifferenceKit struct LogsTableView: NSViewRepresentable { @@ -41,7 +40,7 @@ struct LogsTableView: NSViewRepresentable { tableView.dataSource = context.coordinator TableColumn.allCases.forEach { - let tableColumn = NSTableColumn(identifier: .init($0.rawValue)) + let tableColumn = NSTableColumn(identifier: .init("LogsTableView." + $0.rawValue)) tableColumn.title = $0.rawValue tableColumn.isEditable = false @@ -69,19 +68,11 @@ struct LogsTableView: NSViewRepresentable { func updateNSView(_ nsView: NSScrollView, context: Context) { context.coordinator.parent = self guard let tableView = nsView.documentView as? NSTableView, - let data = data as? [ClashLogStorage.ClashLog] else { + var data = data as? [ClashLogStorage.ClashLog] else { return } - - let target = updateSorts(data, tableView: tableView) - - let source = context.coordinator.logs - let changeset = StagedChangeset(source: source, target: target) - - - tableView.reload(using: changeset) { data in - context.coordinator.logs = data - } + data = updateSorts(data, tableView: tableView) + context.coordinator.updateLogs(data, for: tableView) } func updateSorts(_ objects: [ClashLogStorage.ClashLog], @@ -114,12 +105,28 @@ struct LogsTableView: NSViewRepresentable { df.dateFormat = "MM/dd HH:mm:ss.SSS" return df }() - init(parent: LogsTableView) { self.parent = parent } + func updateLogs(_ logs: [ClashLogStorage.ClashLog], for tableView: NSTableView) { + + let changes = logs.difference(from: self.logs) { + $0.id == $1.id + } + + guard let partialChanges = self.logs.applying(changes) else { return } + + self.logs = partialChanges + + let indicesToReload = IndexSet(zip(partialChanges, logs).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 { logs.count @@ -128,31 +135,37 @@ struct LogsTableView: NSViewRepresentable { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - guard let cellView = tableView.createCellView(with: "LogsTableCellView"), - let s = tableColumn?.identifier.rawValue.split(separator: ".").last, - let tc = TableColumn(rawValue: String(s)) + 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 < logs.count, + let tf = cellView.textField else { return nil } let log = logs[row] - let tf = cellView.textField + + tf.isEditable = false + tf.isSelectable = false switch tc { case .date: - tf?.lineBreakMode = .byTruncatingHead - tf?.textColor = .orange - tf?.stringValue = dateFormatter.string(from: log.date) + tf.lineBreakMode = .byTruncatingHead + tf.textColor = .orange + tf.stringValue = dateFormatter.string(from: log.date) case .level: - tf?.lineBreakMode = .byTruncatingTail - tf?.textColor = log.levelColor - tf?.stringValue = log.levelString + tf.lineBreakMode = .byTruncatingTail + tf.textColor = log.levelColor + tf.stringValue = log.levelString case .log: - tf?.lineBreakMode = .byTruncatingTail - tf?.textColor = .labelColor - tf?.stringValue = log.log + tf.lineBreakMode = .byTruncatingTail + tf.textColor = .labelColor + tf.stringValue = log.log + tf.isSelectable = true } return cellView } - } } diff --git a/Sources/ClashX Dashboard/Views/NSTableViewExtension.swift b/Sources/ClashX Dashboard/Views/NSTableViewExtension.swift new file mode 100644 index 0000000..4f1e025 --- /dev/null +++ b/Sources/ClashX Dashboard/Views/NSTableViewExtension.swift @@ -0,0 +1,72 @@ +// +// NSTableViewExtension.swift +// +// + +// + +import Cocoa + +extension NSTableView { + func makeCellView(with identifier: String, owner: Any?) -> NSTableCellView? { + // https://stackoverflow.com/a/27624927 + + var cellView: NSTableCellView? + if let spareView = makeView(withIdentifier: .init(identifier), + owner: owner) as? NSTableCellView { + + // We can use an old cell - no need to do anything. + cellView = spareView + + } else { + + // Create a text field for the cell + let textField = NSTextField() + textField.backgroundColor = NSColor.clear + textField.translatesAutoresizingMaskIntoConstraints = false + textField.isBordered = false + textField.font = .systemFont(ofSize: 13) + textField.lineBreakMode = .byTruncatingTail + + // Create a cell + let newCell = NSTableCellView() + newCell.identifier = .init(identifier) + newCell.addSubview(textField) + newCell.textField = textField + + // Constrain the text field within the cell + newCell.addConstraints( + NSLayoutConstraint.constraints(withVisualFormat: "H:|[textField]|", + options: [], + metrics: nil, + views: ["textField" : textField])) + + newCell.addConstraint(.init(item: textField, attribute: .centerY, relatedBy: .equal, toItem: newCell, attribute: .centerY, multiplier: 1, constant: 0)) + + + textField.bind(NSBindingName.value, + to: newCell, + withKeyPath: "objectValue", + options: nil) + + cellView = newCell + } + + return cellView + } + + + func reloadData(_ changes: CollectionDifference, indexs: IndexSet) { + beginUpdates() + for change in changes { + switch change { + case .insert(let offset, _, _): + insertRows(at: IndexSet(integer: offset)) + case .remove(let offset, _, _): + removeRows(at: IndexSet(integer: offset)) + } + } + reloadData(forRowIndexes: indexs, columnIndexes: IndexSet(tableColumns.indices)) + endUpdates() + } +} From 217999724f2a87df756d04f57bcc11fff0e5cbee Mon Sep 17 00:00:00 2001 From: mrFq1 <1xxbx0il0@mozmail.com> Date: Fri, 8 Sep 2023 22:27:30 +0800 Subject: [PATCH 2/2] feat: connections speeds --- .../Models/DBConnectionSnapShot.swift | 39 +++++++++++++++++++ .../Connections/ConnectionsTableView.swift | 31 ++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift b/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift index 92ab954..78a84eb 100644 --- a/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift +++ b/Sources/ClashX Dashboard/Models/DBConnectionSnapShot.swift @@ -66,6 +66,11 @@ class DBConnectionObject: NSObject { @objc let destinationIP: String? @objc let type: String + @objc var downloadSpeed: Int64 + @objc var uploadSpeed: Int64 + var downloadSpeedString: String + var uploadSpeedString: String + func isContentEqual(to source: DBConnectionObject) -> Bool { download == source.download && @@ -99,6 +104,40 @@ class DBConnectionObject: NSObject { metadata.host].first(where: { $0 != "" }) type = "\(metadata.type)(\(metadata.network))" + + downloadSpeed = 0 + uploadSpeed = 0 + downloadSpeedString = "" + uploadSpeedString = "" } + + func updateSpeeds(_ old: (download: Int64, upload: Int64)?) { + guard let old = old else { + downloadSpeed = 0 + uploadSpeed = 0 + downloadSpeedString = "" + uploadSpeedString = "" + return + } + + let byteCountFormatter = ByteCountFormatter() + + downloadSpeed = download - old.download + uploadSpeed = upload - old.upload + + if downloadSpeed >= 0 { + downloadSpeedString = byteCountFormatter.string(fromByteCount: downloadSpeed) + "/s" + } else { + downloadSpeed = 0 + downloadSpeedString = "" + } + + if uploadSpeed >= 0 { + uploadSpeedString = byteCountFormatter.string(fromByteCount: uploadSpeed) + "/s" + } else { + uploadSpeed = 0 + uploadSpeedString = "" + } + } } diff --git a/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift b/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift index dc44563..32bc246 100644 --- a/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift +++ b/Sources/ClashX Dashboard/Views/ContentTabs/Connections/ConnectionsTableView.swift @@ -12,6 +12,8 @@ struct ConnectionsTableView: NSViewRepresentable { 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" @@ -80,6 +82,10 @@ struct ConnectionsTableView: NSViewRepresentable { 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: @@ -96,8 +102,6 @@ struct ConnectionsTableView: NSViewRepresentable { sort = .init(keyPath: \DBConnectionObject.destinationIP, ascending: true) case .type: sort = .init(keyPath: \DBConnectionObject.type, ascending: true) - default: - sort = nil } tableColumn.sortDescriptorPrototype = sort @@ -140,6 +144,9 @@ struct ConnectionsTableView: NSViewRepresentable { var conns = data.map(DBConnectionObject.init) + let connHistorys = context.coordinator.connHistorys + conns.forEach { + $0.updateSpeeds(connHistorys[$0.id]) } conns = updateSorts(conns, tableView: tableView) @@ -184,6 +191,7 @@ struct ConnectionsTableView: NSViewRepresentable { var parent: ConnectionsTableView var conns = [DBConnectionObject]() + var connHistorys = [String: (download: Int64, upload: Int64)]() init(parent: ConnectionsTableView) { self.parent = parent @@ -193,6 +201,19 @@ struct ConnectionsTableView: NSViewRepresentable { 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 } @@ -232,6 +253,12 @@ struct ConnectionsTableView: NSViewRepresentable { 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: