From 4a918f69cba4d59199a545ca5053b151c3a6d09f Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Tue, 6 Oct 2026 19:19:57 +0800 Subject: [PATCH] feat(rules): move custom rule deletion to toolbar and context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the inline RulesDeletionButton with a toolbar delete button (trash icon next to the add button). It is enabled only when the custom-rules source is selected on the left and the selection contains deletable custom rules; the collection must also be complete and not committing. The table context menu gains a destructive "删除 (N)" item after a divider when the custom-rules source is selected, with the same enablement conditions. Both entries share one confirmation alert now held by RulesView. Also inline the operation status view into the filter row and tighten filter-area padding. --- .../App/Resources/Rules.xcstrings | 22 +++---- .../App/UI/Rules/RulesDeletionButton.swift | 39 ------------- ShadowsocksX-NG2/App/UI/Rules/RulesView.swift | 57 ++++++++++++++++--- 3 files changed, 59 insertions(+), 59 deletions(-) delete mode 100644 ShadowsocksX-NG2/App/UI/Rules/RulesDeletionButton.swift diff --git a/ShadowsocksX-NG2/App/Resources/Rules.xcstrings b/ShadowsocksX-NG2/App/Resources/Rules.xcstrings index d89aa7e2..85b2a91e 100644 --- a/ShadowsocksX-NG2/App/Resources/Rules.xcstrings +++ b/ShadowsocksX-NG2/App/Resources/Rules.xcstrings @@ -1203,17 +1203,6 @@ } } }, - "删除 %lld 条自定义规则…": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Delete %lld Custom Rules…" - } - } - } - }, "删除 %lld 条自定义规则?": { "extractionState": "manual", "localizations": { @@ -1247,6 +1236,17 @@ } } }, + "删除规则": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete Rules" + } + } + } + }, "已删除 %lld 条自定义规则": { "extractionState": "manual", "localizations": { diff --git a/ShadowsocksX-NG2/App/UI/Rules/RulesDeletionButton.swift b/ShadowsocksX-NG2/App/UI/Rules/RulesDeletionButton.swift deleted file mode 100644 index 5afb40a3..00000000 --- a/ShadowsocksX-NG2/App/UI/Rules/RulesDeletionButton.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftUI - -/// A single confirmation captures custom UUIDs before starting the shared transaction. -/// Gate rejections and commit failures surface through the workflow's commit -/// feedback, not a local alert. -struct RulesDeletionButton: View { - @ObservedObject var workflow: RulesWorkflow - @State private var confirmation: CustomRuleDeletion? - - var body: some View { - Button { - confirmation = workflow.prepareCustomRuleDeletion() - } label: { - Text(verbatim: countText("删除 %lld 条自定义规则…", workflow.deletableSelection.count)) - } - .disabled( - workflow.deletableSelection.isEmpty || !workflow.snapshot.isComplete - || workflow.snapshot.isCommitting - ) - .alert( - countText("删除 %lld 条自定义规则?", confirmation?.customIDs.count ?? 0), - isPresented: Binding( - get: { confirmation != nil }, set: { if !$0 { confirmation = nil } }), - presenting: confirmation - ) { captured in - Button(RulesCopy.text("删除"), role: .destructive) { - confirmation = nil - Task { await workflow.deleteCustomRules(captured) } - } - Button(RulesCopy.text("取消"), role: .cancel) { confirmation = nil } - } message: { _ in - Text(RulesCopy.text("只删除所选自定义条目。其他来源的规则和已有禁用记录将保留。")) - } - } - - private func countText(_ key: String, _ count: Int) -> String { - String.localizedStringWithFormat(RulesCopy.text(key), Int64(count)) - } -} diff --git a/ShadowsocksX-NG2/App/UI/Rules/RulesView.swift b/ShadowsocksX-NG2/App/UI/Rules/RulesView.swift index ec3c0352..6277d101 100644 --- a/ShadowsocksX-NG2/App/UI/Rules/RulesView.swift +++ b/ShadowsocksX-NG2/App/UI/Rules/RulesView.swift @@ -7,6 +7,8 @@ struct RulesView: View { /// 规则编辑草稿与编辑 sheet 由本页自持:@State 生命周期绑定在 rules /// destination 分支上,离开分区即丢弃(与壳级清稿等价)。 @State private var ruleDraft: CustomRuleDraft? + /// 删除确认由本页自持,工具栏按钮与右键菜单共用同一确认弹窗。 + @State private var deletionConfirmation: CustomRuleDeletion? var body: some View { VStack(spacing: 0) { @@ -27,14 +29,8 @@ struct RulesView: View { } }.frame(width: 180) Divider() - VStack(spacing: 0) { - VStack(spacing: 6) { - filters - HStack { - RulesDeletionButton(workflow: workflow) - RulesOperationStatusView(workflow: workflow) - } - }.padding() + VStack(spacing: 8) { + filters ruleTable.overlay { emptyState } if let row = workflow.selectedRelationshipRow { Divider() @@ -54,10 +50,30 @@ struct RulesView: View { } .disabled(!workflow.snapshot.isComplete) } + ToolbarItem(placement: .primaryAction) { + Button(RulesCopy.text("删除规则"), systemImage: "trash") { + deletionConfirmation = workflow.prepareCustomRuleDeletion() + } + .disabled(!canDeleteSelection) + } } .sheet(item: $ruleDraft) { draft in CustomRuleEditorSheet(workflow: workflow, draft: draft) } + .alert( + countText("删除 %lld 条自定义规则?", deletionConfirmation?.customIDs.count ?? 0), + isPresented: Binding( + get: { deletionConfirmation != nil }, set: { if !$0 { deletionConfirmation = nil } }), + presenting: deletionConfirmation + ) { captured in + Button(RulesCopy.text("删除"), role: .destructive) { + deletionConfirmation = nil + Task { await workflow.deleteCustomRules(captured) } + } + Button(RulesCopy.text("取消"), role: .cancel) { deletionConfirmation = nil } + } message: { _ in + Text(RulesCopy.text("只删除所选自定义条目。其他来源的规则和已有禁用记录将保留。")) + } } @ViewBuilder @@ -99,12 +115,13 @@ struct RulesView: View { } private var filters: some View { - VStack(spacing: 6) { + VStack(spacing: 8) { HStack { actionFilter .fixedSize() enabledFilter .fixedSize() + RulesOperationStatusView(workflow: workflow) } .frame(maxWidth: .infinity, alignment: .leading) TextField( @@ -115,6 +132,7 @@ struct RulesView: View { ) .textFieldStyle(.roundedBorder) } + .padding(8) } private var actionFilter: some View { @@ -202,6 +220,17 @@ struct RulesView: View { workflow.actionableSelection.isEmpty || !workflow.snapshot.isComplete || workflow.snapshot.isCommitting) } + if workflow.snapshot.query.source == .custom { + Divider() + Button(role: .destructive) { + deletionConfirmation = workflow.prepareCustomRuleDeletion() + } label: { + Text( + RulesCopy.text("删除") + " (" + + String(workflow.deletableSelection.count) + ")") + } + .disabled(!canDeleteSelection) + } } } @@ -218,6 +247,16 @@ struct RulesView: View { }) } + /// 删除只针对自定义条目:左侧须选“自定义规则”,且所选行含自定义规则、页完整。 + private var canDeleteSelection: Bool { + workflow.snapshot.query.source == .custom && workflow.snapshot.isComplete + && !workflow.deletableSelection.isEmpty + } + + private func countText(_ key: String, _ count: Int) -> String { + String.localizedStringWithFormat(RulesCopy.text(key), Int64(count)) + } + // Native controls can write their bindings during a SwiftUI update. Publish // after that pass, and merge each edit into the latest query so queued edits // to different filters do not overwrite one another.