From ea064a5c8ade9154adca37352c3355a0e4a270b1 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Thu, 15 Oct 2026 00:45:52 +0800 Subject: [PATCH] refactor(servers): remove list sorting --- .../CatalogWorkflowModels.swift | 5 +- .../App/Resources/Localizable.xcstrings | 33 --------- .../App/UI/Servers/ServerTableSort.swift | 71 ------------------- .../App/UI/Servers/ServersView.swift | 30 +------- .../App/UI/Workspace/MainWindowView.swift | 2 - .../Catalog/CatalogTreeProjectionTests.swift | 59 ++++++++------- 6 files changed, 40 insertions(+), 160 deletions(-) delete mode 100644 ShadowsocksX-NG2/App/UI/Servers/ServerTableSort.swift diff --git a/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflowModels.swift b/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflowModels.swift index 7f8c2616..80cb3bc2 100644 --- a/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflowModels.swift +++ b/ShadowsocksX-NG2/App/Application/CatalogWorkflow/CatalogWorkflowModels.swift @@ -121,7 +121,7 @@ struct CatalogTreeSnapshot: Equatable { extension CatalogTreeSnapshot { /// 折叠投影后的可见行(深度优先):收起分组的子树不出现,其余保持目录 /// 序。行序与深度是纯树事实;折叠集合由调用方按自己的持久化策略持有。 - func visibleRows(collapsed: Set) -> [CatalogTreeRow] { + func visibleRows(source: NodeSource? = nil, collapsed: Set) -> [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 } diff --git a/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings b/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings index 5f785878..2dd026b3 100644 --- a/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings +++ b/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings @@ -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": { diff --git a/ShadowsocksX-NG2/App/UI/Servers/ServerTableSort.swift b/ShadowsocksX-NG2/App/UI/Servers/ServerTableSort.swift deleted file mode 100644 index a441ca0f..00000000 --- a/ShadowsocksX-NG2/App/UI/Servers/ServerTableSort.swift +++ /dev/null @@ -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 - ) -> [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 - } -} diff --git a/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift b/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift index d712f539..8af0e065 100644 --- a/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift +++ b/ShadowsocksX-NG2/App/UI/Servers/ServersView.swift @@ -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( - 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 { diff --git a/ShadowsocksX-NG2/App/UI/Workspace/MainWindowView.swift b/ShadowsocksX-NG2/App/UI/Workspace/MainWindowView.swift index 0356fbc0..b8b3e8c5 100644 --- a/ShadowsocksX-NG2/App/UI/Workspace/MainWindowView.swift +++ b/ShadowsocksX-NG2/App/UI/Workspace/MainWindowView.swift @@ -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, diff --git a/ShadowsocksX-NG2/Tests/Catalog/CatalogTreeProjectionTests.swift b/ShadowsocksX-NG2/Tests/Catalog/CatalogTreeProjectionTests.swift index b0ba365a..9651ce2c 100644 --- a/ShadowsocksX-NG2/Tests/Catalog/CatalogTreeProjectionTests.swift +++ b/ShadowsocksX-NG2/Tests/Catalog/CatalogTreeProjectionTests.swift @@ -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"]) } }