mirror of
https://github.com/yJason/ClashX-Dashboard.git
synced 2026-02-04 10:02:26 +08:00
misc: swift difference
This commit is contained in:
@@ -18,15 +18,6 @@
|
|||||||
"version" : "3.8.0"
|
"version" : "3.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "differencekit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/ra1028/DifferenceKit",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "073b9671ce2b9b5b96398611427a1f929927e428",
|
|
||||||
"version" : "1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "dsfsparkline",
|
"identity" : "dsfsparkline",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -18,15 +18,6 @@
|
|||||||
"version" : "3.8.0"
|
"version" : "3.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "differencekit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/ra1028/DifferenceKit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "073b9671ce2b9b5b96398611427a1f929927e428",
|
|
||||||
"version" : "1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "dsfsparkline",
|
"identity" : "dsfsparkline",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
|
.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/daltoniam/Starscream.git", exact: "3.1.1"),
|
||||||
.package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", from: "3.0.0"),
|
.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/dagronf/DSFSparkline.git", from: "4.0.0"),
|
||||||
.package(url: "https://github.com/siteline/swiftui-introspect", from: "0.10.0"),
|
.package(url: "https://github.com/siteline/swiftui-introspect", from: "0.10.0"),
|
||||||
],
|
],
|
||||||
@@ -33,7 +32,6 @@ let package = Package(
|
|||||||
"Alamofire",
|
"Alamofire",
|
||||||
"CocoaLumberjack",
|
"CocoaLumberjack",
|
||||||
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
|
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
|
||||||
"DifferenceKit",
|
|
||||||
"DSFSparkline",
|
"DSFSparkline",
|
||||||
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
|
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
|
||||||
"Starscream",
|
"Starscream",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import DifferenceKit
|
|
||||||
|
|
||||||
struct DBConnectionSnapShot: Codable {
|
struct DBConnectionSnapShot: Codable {
|
||||||
let downloadTotal: Int
|
let downloadTotal: Int
|
||||||
@@ -49,7 +48,7 @@ struct DBMetaConnectionData: Codable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DBConnectionObject: NSObject, Differentiable {
|
class DBConnectionObject: NSObject {
|
||||||
@objc let id: String
|
@objc let id: String
|
||||||
@objc let host: String
|
@objc let host: String
|
||||||
@objc let sniffHost: String
|
@objc let sniffHost: String
|
||||||
@@ -67,9 +66,6 @@ class DBConnectionObject: NSObject, Differentiable {
|
|||||||
@objc let destinationIP: String?
|
@objc let destinationIP: String?
|
||||||
@objc let type: String
|
@objc let type: String
|
||||||
|
|
||||||
var differenceIdentifier: String {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func isContentEqual(to source: DBConnectionObject) -> Bool {
|
func isContentEqual(to source: DBConnectionObject) -> Bool {
|
||||||
download == source.download &&
|
download == source.download &&
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CocoaLumberjackSwift
|
import CocoaLumberjackSwift
|
||||||
import DifferenceKit
|
|
||||||
|
|
||||||
class ClashApiDatasStorage: NSObject, ObservableObject {
|
class ClashApiDatasStorage: NSObject, ObservableObject {
|
||||||
|
|
||||||
@@ -126,25 +125,23 @@ class ClashOverviewData: ObservableObject, Identifiable {
|
|||||||
class ClashLogStorage: ObservableObject {
|
class ClashLogStorage: ObservableObject {
|
||||||
@Published var logs = [ClashLog]()
|
@Published var logs = [ClashLog]()
|
||||||
|
|
||||||
class ClashLog: NSObject, ObservableObject, Identifiable, Differentiable {
|
class ClashLog: NSObject, ObservableObject {
|
||||||
let id: String
|
let id: String
|
||||||
var differenceIdentifier: String {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
let date: Date
|
let date: Date
|
||||||
let level: ClashLogLevel
|
let level: ClashLogLevel
|
||||||
@objc let log: String
|
let log: String
|
||||||
|
|
||||||
let levelColor: NSColor
|
let levelColor: NSColor
|
||||||
@objc let levelString: String
|
let levelString: String
|
||||||
|
|
||||||
init(level: String, log: String) {
|
init(level: String, log: String) {
|
||||||
self.date = Date()
|
id = UUID().uuidString
|
||||||
|
date = Date()
|
||||||
|
|
||||||
self.level = .init(rawValue: level) ?? .unknow
|
self.level = .init(rawValue: level) ?? .unknow
|
||||||
self.log = log
|
self.log = log
|
||||||
|
|
||||||
id = "\(date)" + log
|
|
||||||
self.levelString = level
|
self.levelString = level
|
||||||
switch self.level {
|
switch self.level {
|
||||||
case .info:
|
case .info:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
//
|
//
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AppKit
|
import AppKit
|
||||||
import DifferenceKit
|
|
||||||
|
|
||||||
struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
|
|
||||||
|
|
||||||
TableColumn.allCases.forEach {
|
TableColumn.allCases.forEach {
|
||||||
let tableColumn = NSTableColumn(identifier: .init($0.rawValue))
|
let tableColumn = NSTableColumn(identifier: .init("ConnectionsTableView." + $0.rawValue))
|
||||||
tableColumn.title = $0.rawValue
|
tableColumn.title = $0.rawValue
|
||||||
tableColumn.isEditable = false
|
tableColumn.isEditable = false
|
||||||
|
|
||||||
@@ -139,22 +138,24 @@ struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
return
|
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],
|
func updateSorts(_ objects: [DBConnectionObject],
|
||||||
tableView: NSTableView) -> [DBConnectionObject] {
|
tableView: NSTableView) -> [DBConnectionObject] {
|
||||||
var re = objects
|
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))
|
sortDescriptors.append(.init(keyPath: \DBConnectionObject.id, ascending: true))
|
||||||
re = re.sorted(descriptors: sortDescriptors)
|
re = re.sorted(descriptors: sortDescriptors)
|
||||||
|
|
||||||
@@ -188,6 +189,22 @@ struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
self.parent = parent
|
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 {
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||||
conns.count
|
conns.count
|
||||||
@@ -196,14 +213,18 @@ struct ConnectionsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
|
|
||||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||||
|
|
||||||
guard let cellView = tableView.createCellView(with: "ConnsTableCellView"),
|
guard let identifier = tableColumn?.identifier,
|
||||||
let s = tableColumn?.identifier.rawValue.split(separator: ".").last,
|
let cellView = tableView.makeCellView(with: identifier.rawValue, owner: self),
|
||||||
let tc = TableColumn(rawValue: String(s))
|
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 }
|
else { return nil }
|
||||||
|
|
||||||
let conn = conns[row]
|
let conn = conns[row]
|
||||||
|
|
||||||
cellView.textField?.objectValue = {
|
tf.objectValue = {
|
||||||
switch tc {
|
switch tc {
|
||||||
case .host:
|
case .host:
|
||||||
return conn.host
|
return conn.host
|
||||||
@@ -247,102 +268,3 @@ struct ConnectionsTableView<Item: Hashable>: 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<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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func createCellView(with identifier: String) -> 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import DifferenceKit
|
|
||||||
|
|
||||||
struct LogsTableView<Item: Hashable>: NSViewRepresentable {
|
struct LogsTableView<Item: Hashable>: NSViewRepresentable {
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ struct LogsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
tableView.dataSource = context.coordinator
|
tableView.dataSource = context.coordinator
|
||||||
|
|
||||||
TableColumn.allCases.forEach {
|
TableColumn.allCases.forEach {
|
||||||
let tableColumn = NSTableColumn(identifier: .init($0.rawValue))
|
let tableColumn = NSTableColumn(identifier: .init("LogsTableView." + $0.rawValue))
|
||||||
tableColumn.title = $0.rawValue
|
tableColumn.title = $0.rawValue
|
||||||
tableColumn.isEditable = false
|
tableColumn.isEditable = false
|
||||||
|
|
||||||
@@ -69,19 +68,11 @@ struct LogsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
func updateNSView(_ nsView: NSScrollView, context: Context) {
|
func updateNSView(_ nsView: NSScrollView, context: Context) {
|
||||||
context.coordinator.parent = self
|
context.coordinator.parent = self
|
||||||
guard let tableView = nsView.documentView as? NSTableView,
|
guard let tableView = nsView.documentView as? NSTableView,
|
||||||
let data = data as? [ClashLogStorage.ClashLog] else {
|
var data = data as? [ClashLogStorage.ClashLog] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
data = updateSorts(data, tableView: tableView)
|
||||||
let target = updateSorts(data, tableView: tableView)
|
context.coordinator.updateLogs(data, for: tableView)
|
||||||
|
|
||||||
let source = context.coordinator.logs
|
|
||||||
let changeset = StagedChangeset(source: source, target: target)
|
|
||||||
|
|
||||||
|
|
||||||
tableView.reload(using: changeset) { data in
|
|
||||||
context.coordinator.logs = data
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSorts(_ objects: [ClashLogStorage.ClashLog],
|
func updateSorts(_ objects: [ClashLogStorage.ClashLog],
|
||||||
@@ -115,11 +106,27 @@ struct LogsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
return df
|
return df
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
||||||
init(parent: LogsTableView) {
|
init(parent: LogsTableView) {
|
||||||
self.parent = parent
|
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 {
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||||
logs.count
|
logs.count
|
||||||
@@ -128,31 +135,37 @@ struct LogsTableView<Item: Hashable>: NSViewRepresentable {
|
|||||||
|
|
||||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||||
|
|
||||||
guard let cellView = tableView.createCellView(with: "LogsTableCellView"),
|
guard let identifier = tableColumn?.identifier,
|
||||||
let s = tableColumn?.identifier.rawValue.split(separator: ".").last,
|
let cellView = tableView.makeCellView(with: identifier.rawValue, owner: self),
|
||||||
let tc = TableColumn(rawValue: String(s))
|
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 }
|
else { return nil }
|
||||||
|
|
||||||
let log = logs[row]
|
let log = logs[row]
|
||||||
let tf = cellView.textField
|
|
||||||
|
tf.isEditable = false
|
||||||
|
tf.isSelectable = false
|
||||||
|
|
||||||
switch tc {
|
switch tc {
|
||||||
case .date:
|
case .date:
|
||||||
tf?.lineBreakMode = .byTruncatingHead
|
tf.lineBreakMode = .byTruncatingHead
|
||||||
tf?.textColor = .orange
|
tf.textColor = .orange
|
||||||
tf?.stringValue = dateFormatter.string(from: log.date)
|
tf.stringValue = dateFormatter.string(from: log.date)
|
||||||
case .level:
|
case .level:
|
||||||
tf?.lineBreakMode = .byTruncatingTail
|
tf.lineBreakMode = .byTruncatingTail
|
||||||
tf?.textColor = log.levelColor
|
tf.textColor = log.levelColor
|
||||||
tf?.stringValue = log.levelString
|
tf.stringValue = log.levelString
|
||||||
case .log:
|
case .log:
|
||||||
tf?.lineBreakMode = .byTruncatingTail
|
tf.lineBreakMode = .byTruncatingTail
|
||||||
tf?.textColor = .labelColor
|
tf.textColor = .labelColor
|
||||||
tf?.stringValue = log.log
|
tf.stringValue = log.log
|
||||||
|
tf.isSelectable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return cellView
|
return cellView
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
Sources/ClashX Dashboard/Views/NSTableViewExtension.swift
Normal file
72
Sources/ClashX Dashboard/Views/NSTableViewExtension.swift
Normal file
@@ -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<C>(_ changes: CollectionDifference<C>, 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user