mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-30 22:23:43 +00:00
refactor(servers): remove list sorting
This commit is contained in:
@@ -121,7 +121,7 @@ struct CatalogTreeSnapshot: Equatable {
|
||||
extension CatalogTreeSnapshot {
|
||||
/// 折叠投影后的可见行(深度优先):收起分组的子树不出现,其余保持目录
|
||||
/// 序。行序与深度是纯树事实;折叠集合由调用方按自己的持久化策略持有。
|
||||
func visibleRows(collapsed: Set<NodeID>) -> [CatalogTreeRow] {
|
||||
func visibleRows(source: NodeSource? = nil, collapsed: Set<NodeID>) -> [CatalogTreeRow] {
|
||||
var rows: [CatalogTreeRow] = []
|
||||
func walk(_ nodes: [CatalogTreeNode], depth: Int) {
|
||||
for node in nodes {
|
||||
@@ -131,7 +131,8 @@ extension CatalogTreeSnapshot {
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(roots, depth: 0)
|
||||
// 来源不跨越子树,根层筛选保留完整层级与目录顺序。
|
||||
walk(source.map { source in roots.filter { $0.source == source } } ?? roots, depth: 0)
|
||||
return rows
|
||||
}
|
||||
|
||||
|
||||
@@ -1178,28 +1178,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"创建时间": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Date Created"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"修改时间": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Date Modified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"无": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
@@ -1211,17 +1189,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"排序": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Sort"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"收藏": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// 展示排序不改写目录顺序;未知时间在两个方向都排在最后。
|
||||
struct ServerTableSort: SortComparator {
|
||||
enum Field: CaseIterable {
|
||||
case name, createdAt, updatedAt
|
||||
}
|
||||
|
||||
let field: Field
|
||||
var order: SortOrder
|
||||
|
||||
init(_ field: Field, order: SortOrder = .forward) {
|
||||
self.field = field
|
||||
self.order = order
|
||||
}
|
||||
|
||||
func compare(_ lhs: CatalogTreeNode, _ rhs: CatalogTreeNode) -> ComparisonResult {
|
||||
let result: ComparisonResult
|
||||
switch field {
|
||||
case .name:
|
||||
result = lhs.name.localizedStandardCompare(rhs.name)
|
||||
case .createdAt, .updatedAt:
|
||||
let left = field == .createdAt ? lhs.createdAt : lhs.updatedAt
|
||||
let right = field == .createdAt ? rhs.createdAt : rhs.updatedAt
|
||||
switch (left, right) {
|
||||
case (nil, nil): return .orderedSame
|
||||
case (nil, _): return .orderedDescending
|
||||
case (_, nil): return .orderedAscending
|
||||
case (.some(let left), .some(let right)): result = left.compare(right)
|
||||
}
|
||||
}
|
||||
guard order == .reverse else { return result }
|
||||
switch result {
|
||||
case .orderedAscending: return .orderedDescending
|
||||
case .orderedDescending: return .orderedAscending
|
||||
case .orderedSame: return .orderedSame
|
||||
}
|
||||
}
|
||||
|
||||
static func sorted(_ nodes: [CatalogTreeNode], by sortOrder: [Self]) -> [CatalogTreeNode] {
|
||||
guard let comparator = sortOrder.first else { return nodes }
|
||||
return nodes.enumerated().sorted { lhs, rhs in
|
||||
let result = comparator.compare(lhs.element, rhs.element)
|
||||
return result == .orderedSame ? lhs.offset < rhs.offset : result == .orderedAscending
|
||||
}.map(\.element)
|
||||
}
|
||||
}
|
||||
|
||||
extension CatalogTreeSnapshot {
|
||||
/// 来源不跨越子树,因此根层筛选保留完整层级。
|
||||
func roots(for source: NodeSource, sortedBy sortOrder: [ServerTableSort]) -> [CatalogTreeNode] {
|
||||
ServerTableSort.sorted(roots.filter { $0.source == source }, by: sortOrder)
|
||||
}
|
||||
|
||||
/// 每层单独排序,再按展开状态生成 Table 行;不改变子树归属。
|
||||
func visibleRows(
|
||||
source: NodeSource, sortedBy sortOrder: [ServerTableSort], collapsed: Set<NodeID>
|
||||
) -> [CatalogTreeRow] {
|
||||
var rows: [CatalogTreeRow] = []
|
||||
func append(_ nodes: [CatalogTreeNode], depth: Int) {
|
||||
for node in ServerTableSort.sorted(nodes, by: sortOrder) {
|
||||
rows.append(CatalogTreeRow(node: node, depth: depth))
|
||||
if !collapsed.contains(node.id) {
|
||||
append(node.childNodes, depth: depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
append(roots.filter { $0.source == source }, depth: 0)
|
||||
return rows
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ struct ServersView: View {
|
||||
let activeTargetID: NodeID?
|
||||
@Binding var selection: NodeID?
|
||||
@Binding var section: ServerListSection
|
||||
@Binding var sortOrder: [ServerTableSort]
|
||||
/// 服务器管理的展开状态跨工作区页面切换保留,不落盘。
|
||||
@ObservedObject var expansion: CatalogExpansionState
|
||||
let clipboard: any TextClipboard
|
||||
@@ -132,14 +131,13 @@ struct ServersView: View {
|
||||
|
||||
private var serverTable: some View {
|
||||
let rows = workflow.tree.visibleRows(
|
||||
source: section.source ?? .manual, sortedBy: sortOrder, collapsed: expansion.collapsedGroupIDs
|
||||
source: section.source ?? .manual, collapsed: expansion.collapsedGroupIDs
|
||||
)
|
||||
let depths = Dictionary(uniqueKeysWithValues: rows.map { ($0.id, $0.depth) })
|
||||
return Table(
|
||||
of: CatalogTreeNode.self, selection: $selection,
|
||||
sortOrder: Binding(get: { sortOrder }, set: { sortOrder = Array($0.prefix(1)) })
|
||||
of: CatalogTreeNode.self, selection: $selection
|
||||
) {
|
||||
TableColumn("名称", sortUsing: ServerTableSort(.name)) { node in
|
||||
TableColumn("名称") { node in
|
||||
HStack(spacing: 4) {
|
||||
if node.isGroup && !node.childNodes.isEmpty {
|
||||
Button {
|
||||
@@ -309,28 +307,6 @@ extension ServersView {
|
||||
module: module, targets: workflow.connectivityTargets(source: section.source))
|
||||
}
|
||||
}
|
||||
if section != .favorites {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
Picker(
|
||||
"排序",
|
||||
selection: Binding<ServerTableSort.Field?>(
|
||||
get: { sortOrder.first?.field },
|
||||
set: { field in sortOrder = field.map { [ServerTableSort($0)] } ?? [] })
|
||||
) {
|
||||
Text("无").tag(nil as ServerTableSort.Field?)
|
||||
Text("名称").tag(ServerTableSort.Field.name as ServerTableSort.Field?)
|
||||
Text("创建时间").tag(ServerTableSort.Field.createdAt as ServerTableSort.Field?)
|
||||
Text("修改时间").tag(ServerTableSort.Field.updatedAt as ServerTableSort.Field?)
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
} label: {
|
||||
Label("排序", systemImage: "arrow.up.arrow.down")
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.help("排序")
|
||||
}
|
||||
}
|
||||
if section == .manual {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
|
||||
@@ -38,7 +38,6 @@ struct MainWindowView: View {
|
||||
@State private var tabSelection: WorkspaceDestination = WorkspaceRoute.initialDestination
|
||||
@State private var selection: NodeID?
|
||||
@State private var serverSection: ServerListSection = .manual
|
||||
@State private var serverSortOrder: [ServerTableSort] = []
|
||||
/// 全局导入面板由窗口壳持有。
|
||||
@State private var presentedWorkspaceSheet: WorkspaceSheet?
|
||||
|
||||
@@ -132,7 +131,6 @@ struct MainWindowView: View {
|
||||
activeTargetID: control.snapshot.activeTarget?.id,
|
||||
selection: $selection,
|
||||
section: $serverSection,
|
||||
sortOrder: $serverSortOrder,
|
||||
expansion: expansion,
|
||||
clipboard: clipboard,
|
||||
imageClipboard: imageClipboard,
|
||||
|
||||
@@ -114,9 +114,9 @@ struct SubscriptionRootGroupClassificationTests {
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerTableOrderingTests {
|
||||
struct ServerTableDirectoryOrderTests {
|
||||
@Test
|
||||
func hierarchySortsEachSiblingSetAndCollapseHidesOnlyThatSubtree() throws {
|
||||
func hierarchyKeepsDirectoryOrderAndCollapseHidesOnlyThatSubtree() throws {
|
||||
var catalog = ConfigurationCatalog()
|
||||
let outer = try catalog.addGroup("Z 外层")
|
||||
let root = try catalog.addGroup("A 根层")
|
||||
@@ -125,42 +125,51 @@ struct ServerTableOrderingTests {
|
||||
let leaf = try catalog.addGroup("孙节点", to: childA)
|
||||
let tree = CatalogTreeSnapshot.build(
|
||||
from: catalog, credentials: InMemoryCredentialStore(), plugins: NoManagedPluginProvider())
|
||||
let rows = tree.visibleRows(source: .manual, sortedBy: [ServerTableSort(.name)], collapsed: [])
|
||||
let rows = tree.visibleRows(source: .manual, collapsed: [])
|
||||
|
||||
#expect(rows.map(\.id) == [root, outer, childA, leaf, childZ])
|
||||
#expect(rows.map(\.depth) == [0, 0, 1, 2, 1])
|
||||
#expect(rows.map(\.id) == [outer, childZ, childA, leaf, root])
|
||||
#expect(rows.map(\.depth) == [0, 1, 1, 2, 0])
|
||||
#expect(
|
||||
tree.visibleRows(
|
||||
source: .manual, sortedBy: [ServerTableSort(.name)], collapsed: [outer]
|
||||
).map(\.id) == [root, outer])
|
||||
source: .manual, collapsed: [outer]
|
||||
).map(\.id) == [outer, root])
|
||||
#expect(tree.roots.map(\.id) == [outer, root])
|
||||
#expect(tree.node(withID: outer)?.childNodes.map(\.id) == [childZ, childA])
|
||||
}
|
||||
|
||||
@Test(arguments: [SortOrder.forward, .reverse])
|
||||
func sourceFilteringAndTimeSortingKeepUnknownLastAndEqualValuesStable(order: SortOrder) {
|
||||
func node(_ id: String, source: NodeSource = .manual, date: Date?) -> CatalogTreeNode {
|
||||
@Test
|
||||
func sourceFilteringKeepsDirectoryOrderAndCompleteSubtrees() {
|
||||
func node(
|
||||
_ id: String, source: NodeSource = .manual, parentID: NodeID? = nil,
|
||||
children: [CatalogTreeNode] = []
|
||||
) -> CatalogTreeNode {
|
||||
CatalogTreeNode(
|
||||
id: NodeID(rawValue: id), name: id, isGroup: true, source: source, parentID: nil,
|
||||
createdAt: date, updatedAt: date, invalidReasons: [], childCount: 0,
|
||||
subtreeNodeCount: 0, invalidDescendantCount: 0, children: [])
|
||||
id: NodeID(rawValue: id), name: id, isGroup: true, source: source, parentID: parentID,
|
||||
createdAt: nil, updatedAt: nil, invalidReasons: [], childCount: children.count,
|
||||
subtreeNodeCount: children.count, invalidDescendantCount: 0, children: children)
|
||||
}
|
||||
let subscription = node(
|
||||
"subscription", source: .subscription,
|
||||
children: [
|
||||
node(
|
||||
"subscription-child", source: .subscription,
|
||||
parentID: NodeID(rawValue: "subscription"))
|
||||
])
|
||||
let tree = CatalogTreeSnapshot(roots: [
|
||||
node("unknown", date: nil),
|
||||
node("newer", date: Date(timeIntervalSince1970: 200)),
|
||||
node("equal-first", date: Date(timeIntervalSince1970: 100)),
|
||||
node("equal-second", date: Date(timeIntervalSince1970: 100)),
|
||||
node("subscription", source: .subscription, date: Date(timeIntervalSince1970: 50)),
|
||||
node("Z-first"), subscription, node("A-last"),
|
||||
])
|
||||
let sorted = tree.roots(for: .manual, sortedBy: [ServerTableSort(.createdAt, order: order)])
|
||||
#expect(
|
||||
sorted.map(\.id.rawValue)
|
||||
== (order == .forward
|
||||
? ["equal-first", "equal-second", "newer", "unknown"]
|
||||
: ["newer", "equal-first", "equal-second", "unknown"]))
|
||||
tree.visibleRows(source: .manual, collapsed: []).map(\.id.rawValue)
|
||||
== ["Z-first", "A-last"])
|
||||
let subscriptionRows = tree.visibleRows(source: .subscription, collapsed: [])
|
||||
#expect(subscriptionRows.map(\.id.rawValue) == ["subscription", "subscription-child"])
|
||||
#expect(subscriptionRows.map(\.depth) == [0, 1])
|
||||
#expect(
|
||||
tree.roots(for: .manual, sortedBy: []).map(\.id.rawValue)
|
||||
== ["unknown", "newer", "equal-first", "equal-second"])
|
||||
tree.visibleRows(source: .subscription, collapsed: [subscription.id]).map(\.id.rawValue)
|
||||
== ["subscription"])
|
||||
#expect(
|
||||
tree.visibleRows(collapsed: []).map(\.id.rawValue)
|
||||
== ["Z-first", "subscription", "subscription-child", "A-last"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user