mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-03 01:23:43 +00:00
refactor(catalog): 目录树投影收进 CatalogTreeSnapshot
visibleRows(collapsed:)、serverLeafCount 与共享行类型 CatalogTreeRow 移入树快照,服务器管理侧栏、首页目标树与工作区角标三处各自重走的 遍历归一;HomeServerListState.visibleRows 保留为委托门面,视图与 测试零改动。嵌套菜单构造(TargetCascade)与政策查询 (moveDestinations)非投影重复,有意不动。
This commit is contained in:
@@ -61,6 +61,15 @@ extension CatalogTreeNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// 侧栏树的可见行投影:节点 + 呈现深度;身份即节点身份(折叠只影响可见
|
||||
/// 集合,不影响行身份与选中)。服务器管理侧栏与首页目标树共用同一类型,
|
||||
/// 折叠集合由各自的浏览状态 owner 给出。
|
||||
struct CatalogTreeRow: Identifiable {
|
||||
let node: CatalogTreeNode
|
||||
let depth: Int
|
||||
var id: NodeID { node.id }
|
||||
}
|
||||
|
||||
/// 目录树快照:侧栏、活动目标级联、移动目的地与删除确认共用的同一份
|
||||
/// 非敏感 projection;UI 不再持有原始目录副本(issue #41)。
|
||||
struct CatalogTreeSnapshot: Equatable {
|
||||
@@ -97,6 +106,32 @@ struct CatalogTreeSnapshot: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension CatalogTreeSnapshot {
|
||||
/// 折叠投影后的可见行(深度优先):收起分组的子树不出现,其余保持目录
|
||||
/// 序。行序与深度是纯树事实;折叠集合由调用方按自己的持久化策略持有。
|
||||
func visibleRows(collapsed: Set<NodeID>) -> [CatalogTreeRow] {
|
||||
var rows: [CatalogTreeRow] = []
|
||||
func walk(_ nodes: [CatalogTreeNode], depth: Int) {
|
||||
for node in nodes {
|
||||
rows.append(CatalogTreeRow(node: node, depth: depth))
|
||||
if node.isGroup, !collapsed.contains(node.id) {
|
||||
walk(node.childNodes, depth: depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(roots, depth: 0)
|
||||
return rows
|
||||
}
|
||||
|
||||
/// 全树服务器叶子数(工作区侧栏角标)。
|
||||
var serverLeafCount: Int {
|
||||
func leaves(_ nodes: [CatalogTreeNode]) -> Int {
|
||||
nodes.reduce(0) { $0 + ($1.isGroup ? leaves($1.childNodes) : 1) }
|
||||
}
|
||||
return leaves(roots)
|
||||
}
|
||||
}
|
||||
|
||||
extension CatalogTreeSnapshot {
|
||||
/// 从已提交目录构建 projection(module 内部推导;测试同 target 可直接调用)。
|
||||
/// 每个服务器叶子求值一次激活校验(与既有口径一致,读凭据存储)。
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
struct HomeServerTreeRow: Identifiable {
|
||||
let node: CatalogTreeNode
|
||||
let depth: Int
|
||||
var id: NodeID { node.id }
|
||||
}
|
||||
|
||||
/// 首页独立的浏览状态。选择与折叠不改变目录或运行时活动目标。
|
||||
@MainActor
|
||||
final class HomeServerListState: ObservableObject {
|
||||
@@ -34,18 +28,9 @@ final class HomeServerListState: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
var visibleRows: [HomeServerTreeRow] {
|
||||
var rows: [HomeServerTreeRow] = []
|
||||
func walk(_ nodes: [CatalogTreeNode], depth: Int) {
|
||||
for node in nodes {
|
||||
rows.append(HomeServerTreeRow(node: node, depth: depth))
|
||||
if node.isGroup, !collapsedGroupIDs.contains(node.id) {
|
||||
walk(node.childNodes, depth: depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(tree.roots, depth: 0)
|
||||
return rows
|
||||
/// 可见行投影委托树快照;折叠集合由本状态的持久化策略持有。
|
||||
var visibleRows: [CatalogTreeRow] {
|
||||
tree.visibleRows(collapsed: collapsedGroupIDs)
|
||||
}
|
||||
|
||||
func update(tree: CatalogTreeSnapshot, activeTargetID: NodeID?) {
|
||||
|
||||
@@ -192,7 +192,7 @@ struct TargetTreeCard: View {
|
||||
|
||||
private struct HomeServerListRow: View {
|
||||
@Environment(\.controlActiveState) private var controlActiveState
|
||||
let row: HomeServerTreeRow
|
||||
let row: CatalogTreeRow
|
||||
let eligibility: ActivationEligibility?
|
||||
let isSelected: Bool
|
||||
let isActive: Bool
|
||||
|
||||
@@ -340,10 +340,7 @@ extension MainWindowView {
|
||||
}
|
||||
|
||||
private var serverLeafCount: Int {
|
||||
func leaves(_ nodes: [CatalogTreeNode]) -> Int {
|
||||
nodes.reduce(0) { $0 + ($1.isGroup ? leaves($1.childNodes) : 1) }
|
||||
}
|
||||
return leaves(workflow.tree.roots)
|
||||
workflow.tree.serverLeafCount
|
||||
}
|
||||
|
||||
// MARK: - 底部代理状态卡
|
||||
|
||||
@@ -91,7 +91,7 @@ struct ServersView: View {
|
||||
|
||||
private var serverSidebar: some View {
|
||||
List(selection: $selection) {
|
||||
ForEach(visibleRows) { row in
|
||||
ForEach(workflow.tree.visibleRows(collapsed: expansion.collapsedGroupIDs)) { row in
|
||||
treeRow(row)
|
||||
}
|
||||
}
|
||||
@@ -113,21 +113,6 @@ struct ServersView: View {
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
|
||||
/// 折叠投影后的可见行(深度优先):收起分组的子树不出现,其余保持目录序。
|
||||
private var visibleRows: [CatalogTreeRow] {
|
||||
var rows: [CatalogTreeRow] = []
|
||||
func walk(_ nodes: [CatalogTreeNode], depth: Int) {
|
||||
for node in nodes {
|
||||
rows.append(CatalogTreeRow(node: node, depth: depth))
|
||||
if node.isGroup, !expansion.isCollapsed(node.id) {
|
||||
walk(node.childNodes, depth: depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(workflow.tree.roots, depth: 0)
|
||||
return rows
|
||||
}
|
||||
|
||||
private func treeRow(_ row: CatalogTreeRow) -> some View {
|
||||
let node = row.node
|
||||
return HStack(spacing: 4) {
|
||||
@@ -354,11 +339,3 @@ private struct MoveContext: Identifiable {
|
||||
let nodeID: NodeID
|
||||
var id: NodeID { nodeID }
|
||||
}
|
||||
|
||||
/// 侧栏树的可见行投影:节点 + 呈现深度;身份即节点身份(折叠只影响可见集合,
|
||||
/// 不影响行身份与选中)。
|
||||
private struct CatalogTreeRow: Identifiable {
|
||||
let node: CatalogTreeNode
|
||||
let depth: Int
|
||||
var id: NodeID { node.id }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import XCTest
|
||||
|
||||
@testable import ShadowsocksX_NG2
|
||||
|
||||
/// CatalogTreeSnapshot 树投影的纯单元测试:折叠可见行与服务器叶子计数。
|
||||
/// 直接构造 snapshot(memberwise init),不经工作流。
|
||||
final class CatalogTreeProjectionTests: XCTestCase {
|
||||
private func server(_ id: String) -> CatalogTreeNode {
|
||||
CatalogTreeNode(
|
||||
id: NodeID(rawValue: id), name: id, isGroup: false, source: .manual, parentID: nil,
|
||||
invalidReasons: [], childCount: 0, subtreeNodeCount: 0, invalidDescendantCount: 0,
|
||||
children: nil)
|
||||
}
|
||||
|
||||
private func group(_ id: String, children: [CatalogTreeNode]) -> CatalogTreeNode {
|
||||
CatalogTreeNode(
|
||||
id: NodeID(rawValue: id), name: id, isGroup: true, source: .manual, parentID: nil,
|
||||
invalidReasons: [], childCount: children.count,
|
||||
subtreeNodeCount: children.reduce(0) { $0 + 1 + $1.subtreeNodeCount },
|
||||
invalidDescendantCount: 0, children: children)
|
||||
}
|
||||
|
||||
/// 两层嵌套 + 根叶子 + 空组:覆盖深度、子树跳过与空组语义。
|
||||
private func nestedTree() -> CatalogTreeSnapshot {
|
||||
CatalogTreeSnapshot(roots: [
|
||||
server("root-server"),
|
||||
group(
|
||||
"outer",
|
||||
children: [
|
||||
group("inner", children: [server("deep-leaf")]),
|
||||
server("outer-leaf"),
|
||||
]),
|
||||
group("empty", children: []),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - 可见行折叠投影
|
||||
|
||||
func testVisibleRowsWalkDepthFirstWithDepth() {
|
||||
let rows = nestedTree().visibleRows(collapsed: [])
|
||||
XCTAssertEqual(
|
||||
rows.map(\.id.rawValue),
|
||||
["root-server", "outer", "inner", "deep-leaf", "outer-leaf", "empty"])
|
||||
XCTAssertEqual(rows.map(\.depth), [0, 0, 1, 2, 1, 0])
|
||||
XCTAssertEqual(rows.map(\.node.name), rows.map(\.id.rawValue), "行身份即节点身份")
|
||||
}
|
||||
|
||||
func testVisibleRowsSkipCollapsedSubtrees() {
|
||||
let tree = nestedTree()
|
||||
XCTAssertEqual(
|
||||
tree.visibleRows(collapsed: [NodeID(rawValue: "outer")]).map(\.id.rawValue),
|
||||
["root-server", "outer", "empty"], "收起组的子树不出现,组行自身保留可再展开")
|
||||
XCTAssertEqual(
|
||||
tree.visibleRows(collapsed: [NodeID(rawValue: "inner")]).map(\.id.rawValue),
|
||||
["root-server", "outer", "inner", "outer-leaf", "empty"])
|
||||
}
|
||||
|
||||
func testVisibleRowsTreatUnknownAndLeafIDsAsInert() {
|
||||
let collapsed: Set<NodeID> = [NodeID(rawValue: "nope"), NodeID(rawValue: "root-server")]
|
||||
XCTAssertEqual(
|
||||
nestedTree().visibleRows(collapsed: collapsed).map(\.id.rawValue),
|
||||
["root-server", "outer", "inner", "deep-leaf", "outer-leaf", "empty"])
|
||||
}
|
||||
|
||||
// MARK: - 服务器叶子计数
|
||||
|
||||
func testServerLeafCountCountsLeavesAcrossNesting() {
|
||||
XCTAssertEqual(nestedTree().serverLeafCount, 3)
|
||||
XCTAssertEqual(CatalogTreeSnapshot(roots: []).serverLeafCount, 0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user