Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/SidebarRow.swift
T
Qiu Yuzhou 7bb327ba1d docs(domain): CONTEXT.md 改名 GLOSSARY.md 并对齐引用
domain-modeling skill 升级后单上下文约定文件名为 GLOSSARY.md:
git mv 保历史、内容零改动;AGENTS.md、docs/agents/domain.md 及
NG2 源码注释中的活引用随之对齐。docs/design、docs/research、ADR
中的旧名引用属历史记录,不改写。
2026-10-04 15:06:41 +08:00

101 lines
3.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
/// 侧栏树行(issue #41):来源标识、有效性提示、活动目标标记与右键菜单。
/// 数据来自目录工作流 module 的树 projection;激活经 seam 的 typed command,
/// 活动目标标记由父视图传入(运行时事实,不进目录 projection)。删除一律
/// 交父视图确认弹窗(空手动组单次、非空二次,GLOSSARY.md)。
struct SidebarRow: View {
let node: CatalogTreeNode
let workflow: CatalogWorkflow
/// 激活反馈共享状态:命令经它发出(单飞互斥、结果记录在会话反馈里)。
let activation: ActivationFeedbackState
let activeTargetID: NodeID?
let errors: ErrorAlertPresenter
let onRename: (NodeID) -> Void
let onNewGroup: (NodeID?) -> Void
let onMove: (NodeID) -> Void
let onDelete: (NodeID) -> Void
let onExport: (NodeID) -> Void
private var isSubscription: Bool { node.source == .subscription }
var body: some View {
HStack(spacing: 6) {
Image(systemName: node.isGroup ? "folder" : "server.rack")
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 1) {
Text(node.name)
.foregroundStyle(node.isInvalid ? .secondary : .primary)
if node.isGroup {
// 分组行尾数量说明(票 #55):原型「手动分组 · N 项」口径。
Text("\(node.isManual ? "手动分组" : "订阅分组") · \(node.childCount) 项")
.font(.caption)
.foregroundStyle(.tertiary)
}
}
if node.isInvalid {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
.help(
node.invalidReasons.first.map {
AppPresentation.message(
for: ActivationFailure.invalidLeaf(node: node.id, reason: $0))
} ?? "服务器存在已知阻塞问题")
} else if node.invalidDescendantCount > 0 {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.orange)
.help("包含 \(node.invalidDescendantCount) 个存在已知阻塞问题的服务器")
}
if isSubscription {
Image(systemName: "arrow.triangle.2.circlepath")
.foregroundStyle(.tertiary)
.help("订阅节点:由远端管理")
}
if activeTargetID == node.id {
Image(systemName: "bolt.fill")
.foregroundStyle(.orange)
.help("活动目标")
}
}
.contextMenu { contextMenu }
}
@ViewBuilder
private var contextMenu: some View {
Button("激活") {
Task { @MainActor in
do {
let outcome = try await activation.activate(node.id, via: workflow)
// 右键菜单即关、无内联反馈面:原子拒绝点名弹窗(typed 原因随
// 结果返回);单飞忽略(nil)不呈现。
if case .rejectedActivation(let failure)? = outcome {
errors.present(failure)
}
} catch {
errors.present(error)
}
}
}
if node.isGroup {
Divider()
Button("导出为 SIP-008 JSON…") { onExport(node.id) }
.disabled(!node.containsServerConfiguration)
}
if !isSubscription {
Divider()
if node.isGroup {
Button("重命名…") { onRename(node.id) }
}
Button("新建分组…") {
onNewGroup(node.isGroup ? node.id : node.parentID)
}
Divider()
Button("移动到…") { onMove(node.id) }
Divider()
Button("删除…", role: .destructive) {
onDelete(node.id)
}
}
}
}