mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:24:50 +00:00
163 lines
6.6 KiB
Swift
163 lines
6.6 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()
|
|
LabeledContent(RulesCopy.text("行动")) {
|
|
Text(RulesCopy.text(draft.action == .proxy ? "代理" : "直连"))
|
|
}
|
|
}
|
|
}.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 或路径。")
|
|
}
|
|
}
|
|
}
|
|
}
|