mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-13 07:05:05 +00:00
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。 按「目录即分层」重组为 Composition / Application / Presentation / UI / PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、 Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、 StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有 散落文件。 配套改动: - project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。 XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后 旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与 entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何 非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。 - SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免 前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时 适配器放进 Application/ 即自动落在扫描范围外,无需再登记。 - README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。 验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫); task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型 warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位 后签名与 entitlements 语义不变。 注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist 会被当作普通资源扫进 Resources 阶段。
165 lines
6.7 KiB
Swift
165 lines
6.7 KiB
Swift
import SwiftUI
|
|
|
|
/// The draft belongs to this presentation; only an explicit save enters the Workflow.
|
|
struct CustomRuleEditorSheet: View {
|
|
@ObservedObject var workflow: RulesWorkflow
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var draft: CustomRuleDraft
|
|
@State private var preview: CustomRulePreview?
|
|
@State private var isSaving = false
|
|
@State private var saveFailure: CustomRuleUpdateOutcome?
|
|
|
|
init(workflow: RulesWorkflow, draft: CustomRuleDraft) {
|
|
self.workflow = workflow
|
|
_draft = State(initialValue: draft)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(RulesCopy.text(draft.editingID == nil ? "新增规则" : "编辑规则"))
|
|
.font(.title2)
|
|
Form {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Picker(RulesCopy.text("类型"), selection: $draft.kind) {
|
|
ForEach(CustomRuleDraft.Kind.allCases, id: \.self) { kind in
|
|
Text(verbatim: kind.label).tag(kind)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
TextField(RulesCopy.text("匹配内容"), text: $draft.content)
|
|
.autocorrectionDisabled()
|
|
Picker(RulesCopy.text("行动"), selection: $draft.action) {
|
|
Text(RulesCopy.text("代理")).tag(RuleAction.proxy)
|
|
Text(RulesCopy.text("直连")).tag(RuleAction.direct)
|
|
}
|
|
.pickerStyle(.segmented)
|
|
}
|
|
}.disabled(isSaving || workflow.snapshot.isCommitting)
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
if let preview {
|
|
if let content = preview.displayContent {
|
|
LabeledContent(RulesCopy.text("规范化预览")) {
|
|
Text(verbatim: content).textSelection(.enabled)
|
|
}
|
|
}
|
|
if let failure = preview.failure {
|
|
Text(verbatim: failure.message).foregroundStyle(.orange)
|
|
} else if let row = preview.row {
|
|
if !row.sources.subtracting([.custom]).isEmpty {
|
|
Text(RulesCopy.text("将与已有来源合并显示。"))
|
|
Text(verbatim: row.sourceLabels).foregroundStyle(.secondary)
|
|
}
|
|
if preview.inheritsDisablement {
|
|
Text(RulesCopy.text("新身份将继承禁用状态;旧禁用记录保留。"))
|
|
} else if !row.isEnabled {
|
|
Text(RulesCopy.text("此身份已禁用;保存不会重新启用。"))
|
|
}
|
|
RulesRelationshipsContent(row: row)
|
|
Text(RulesCopy.text("覆盖关系仅解释已启用规则之间的关系,不代表当前流量行为。"))
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
} else {
|
|
ProgressView().controlSize(.small)
|
|
}
|
|
if let saveFailure {
|
|
Label(saveFailure.rulesMessage, systemImage: "exclamationmark.triangle")
|
|
.foregroundStyle(.orange)
|
|
if let detail = saveFailure.failureDetail {
|
|
Text(verbatim: detail).textSelection(.enabled)
|
|
}
|
|
if let nextStep = saveFailure.nextStep { Text(RulesCopy.text(nextStep)) }
|
|
}
|
|
}.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
// 预览区随内容伸缩,上限封顶,避免初始 ProgressView 阶段撑出大片空白、
|
|
// 长预览把 sheet 无限拉高。
|
|
.frame(maxHeight: 210)
|
|
HStack {
|
|
if isSaving || workflow.snapshot.isCommitting {
|
|
ProgressView().controlSize(.small)
|
|
Text(RulesCopy.text("正在更新规则…")).font(.caption)
|
|
}
|
|
Spacer()
|
|
Button("取消", role: .cancel) { dismiss() }
|
|
.keyboardShortcut(.cancelAction)
|
|
.disabled(isSaving || workflow.snapshot.isCommitting)
|
|
Button("保存") { save() }
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(preview?.document == nil || isSaving || !workflow.snapshot.isComplete)
|
|
}
|
|
}
|
|
.onChange(of: draft) { _, _ in saveFailure = nil }
|
|
// 预览内容高度天然波动(失败提示一两行,合法预览可达上限),尺寸变化
|
|
// 只发生在合法↔失败、关系行增减这类语义边界;平缓过渡避免 sheet 跳动。
|
|
.animation(.snappy(duration: 0.2), value: preview)
|
|
.animation(.snappy(duration: 0.2), value: saveFailure)
|
|
.padding(20)
|
|
.frame(width: 530)
|
|
.interactiveDismissDisabled(isSaving || workflow.snapshot.isCommitting)
|
|
.task(
|
|
id: PreviewRequest(
|
|
draft: draft, version: workflow.snapshot.version, isComplete: workflow.snapshot.isComplete)
|
|
) {
|
|
// 重算期间保留上一次预览:清空会让预览区塌缩成进度条,sheet 随
|
|
// 每个按键先缩后胀;新结果就绪后原位替换(旧任务由 task(id:) 取消)。
|
|
let result = await workflow.previewCustomRule(draft)
|
|
guard !Task.isCancelled else { return }
|
|
preview = result
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
guard !isSaving, workflow.snapshot.isComplete else { return }
|
|
isSaving = true
|
|
saveFailure = nil
|
|
Task {
|
|
let result = await workflow.saveCustomRule(draft)
|
|
isSaving = false
|
|
switch result {
|
|
case .unavailable(let failure): preview = CustomRulePreview(failure: failure)
|
|
case .committed(let outcome):
|
|
if outcome.isSuccess { dismiss() } else { saveFailure = outcome }
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct PreviewRequest: Equatable {
|
|
let draft: CustomRuleDraft
|
|
let version: String
|
|
let isComplete: Bool
|
|
}
|
|
}
|
|
|
|
extension CustomRuleDraft.Kind {
|
|
var label: String {
|
|
switch self {
|
|
case .ipAddress: RulesCopy.text("IP 地址")
|
|
case .cidr: RulesCopy.text("CIDR 范围")
|
|
case .domainSuffix: RulesCopy.text("域名后缀")
|
|
case .domainExact: RulesCopy.text("精确域名")
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CustomRulePreview.Failure {
|
|
var message: String {
|
|
switch self {
|
|
case .incompleteCollection: RulesCopy.text("集合不完整")
|
|
case .busy: RulesCopy.text("正在更新规则…")
|
|
case .staleDraft: RulesCopy.text("规则集合已变化,请重新打开编辑器。")
|
|
case .duplicate: RulesCopy.text("相同匹配条件和行动的自定义规则已存在。")
|
|
case .fixedPolicyDisablement:
|
|
RulesCopy.text("无法更新规则") + " · " + RulesCopy.text("固定策略优先,以下范围始终直连。")
|
|
case .fixedLocalConflict: RulesCopy.text("代理规则与固定本地策略冲突,无法保存。")
|
|
case .invalidInput(let kind):
|
|
switch kind {
|
|
case .ipAddress: RulesCopy.text("请输入有效的 IPv4 或 IPv6 地址。")
|
|
case .cidr: RulesCopy.text("请输入有效的 IPv4 或 IPv6 CIDR 范围。")
|
|
case .domainExact, .domainSuffix:
|
|
RulesCopy.text("请输入有效的 ASCII 域名;不支持通配符、URL 或路径。")
|
|
}
|
|
}
|
|
}
|
|
}
|