Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/HomeServerListState.swift
T
Qiu Yuzhou 57ce356573 refactor(catalog): 目录树投影收进 CatalogTreeSnapshot
visibleRows(collapsed:)、serverLeafCount 与共享行类型 CatalogTreeRow
移入树快照,服务器管理侧栏、首页目标树与工作区角标三处各自重走的
遍历归一;HomeServerListState.visibleRows 保留为委托门面,视图与
测试零改动。嵌套菜单构造(TargetCascade)与政策查询
(moveDestinations)非投影重复,有意不动。
2026-10-03 14:29:12 +08:00

156 lines
5.0 KiB
Swift

import Combine
import Foundation
/// 首页独立的浏览状态。选择与折叠不改变目录或运行时活动目标。
@MainActor
final class HomeServerListState: ObservableObject {
enum Navigation { case upward, downward, left, right }
@Published private(set) var tree = CatalogTreeSnapshot(roots: [])
@Published private(set) var selection: NodeID?
@Published private(set) var collapsedGroupIDs: Set<NodeID> = []
private var knownGroupIDs: Set<NodeID> = []
private var didInitializeSelection = false
private let defaults: UserDefaults
private static let expansionKey = "homeServerList.expansion"
private struct ExpansionPreferences: Codable {
let known: Set<NodeID>
let collapsed: Set<NodeID>
}
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
if let data = defaults.data(forKey: Self.expansionKey),
let saved = try? JSONDecoder().decode(ExpansionPreferences.self, from: data)
{
knownGroupIDs = saved.known
collapsedGroupIDs = saved.collapsed
}
}
/// 可见行投影委托树快照;折叠集合由本状态的持久化策略持有。
var visibleRows: [CatalogTreeRow] {
tree.visibleRows(collapsed: collapsedGroupIDs)
}
func update(tree: CatalogTreeSnapshot, activeTargetID: NodeID?) {
let previousTree = self.tree
self.tree = tree
var currentGroups: Set<NodeID> = []
func walk(_ nodes: [CatalogTreeNode], depth: Int) {
for node in nodes where node.isGroup {
currentGroups.insert(node.id)
if knownGroupIDs.insert(node.id).inserted, depth > 0 {
collapsedGroupIDs.insert(node.id)
}
walk(node.childNodes, depth: depth + 1)
}
}
walk(tree.roots, depth: 0)
knownGroupIDs.formIntersection(currentGroups)
collapsedGroupIDs.formIntersection(currentGroups)
if !didInitializeSelection, !tree.isEmpty {
selection = activeTargetID.flatMap { tree.containsNode($0) ? $0 : nil }
didInitializeSelection = true
} else if let selection, !tree.containsNode(selection) {
self.selection = nil
} else if let selection, tree != previousTree,
let visibleAncestor = ancestorIDs(for: selection).reversed().first(where: {
collapsedGroupIDs.contains($0)
})
{
self.selection = visibleAncestor
}
saveExpansion()
}
func select(_ id: NodeID) {
guard visibleRows.contains(where: { $0.id == id }) else { return }
selection = id
}
/// 行内按钮与 Return 共用门禁;隐藏、无效和已激活目标均不发出命令。
func activationTarget(
activeTargetID: NodeID?, eligibility: ActivationEligibility?
) -> NodeID? {
guard let selection, selection != activeTargetID,
eligibility?.canActivate == true,
visibleRows.contains(where: { $0.id == selection })
else { return nil }
return selection
}
/// 明确定位才展开祖先;启动时隐藏的活动项保持选中而不自动展开。
func locate(_ id: NodeID) {
guard tree.containsNode(id) else { return }
collapsedGroupIDs.subtract(ancestorIDs(for: id))
selection = id
saveExpansion()
}
func toggleGroup(_ id: NodeID) {
guard tree.node(withID: id)?.isGroup == true else { return }
if !collapsedGroupIDs.insert(id).inserted {
collapsedGroupIDs.remove(id)
} else if let selection, ancestorIDs(for: selection).contains(id) {
self.selection = id
}
saveExpansion()
}
func ancestorIDs(for id: NodeID) -> [NodeID] {
var result: [NodeID] = []
var parent = tree.node(withID: id)?.parentID
while let id = parent {
result.append(id)
parent = tree.node(withID: id)?.parentID
}
return result
}
func navigate(_ direction: Navigation) {
let rows = visibleRows
guard !rows.isEmpty else { return }
guard let selection, let index = rows.firstIndex(where: { $0.id == selection }) else {
if direction == .upward || direction == .downward {
self.selection = direction == .upward ? rows.last?.id : rows.first?.id
}
return
}
let node = rows[index].node
switch direction {
case .upward:
self.selection = rows[max(0, index - 1)].id
case .downward:
self.selection = rows[min(rows.count - 1, index + 1)].id
case .left:
collapseOrSelectParent(node)
case .right:
expandOrSelectChild(node)
}
}
private func collapseOrSelectParent(_ node: CatalogTreeNode) {
if node.isGroup, !collapsedGroupIDs.contains(node.id) {
toggleGroup(node.id)
} else if let parent = node.parentID {
selection = parent
}
}
private func expandOrSelectChild(_ node: CatalogTreeNode) {
if node.isGroup, collapsedGroupIDs.contains(node.id) {
toggleGroup(node.id)
} else if let child = node.childNodes.first {
selection = child.id
}
}
private func saveExpansion() {
let preferences = ExpansionPreferences(known: knownGroupIDs, collapsed: collapsedGroupIDs)
if let data = try? JSONEncoder().encode(preferences) {
defaults.set(data, forKey: Self.expansionKey)
}
}
}