Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/UI/Rules/RulesOperationStatusView.swift
Qiu Yuzhou fb428f12bc refactor(app): 按分层重组 App/ 源码目录
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 阶段。
2026-10-05 19:56:18 +08:00

106 lines
3.5 KiB
Swift

import SwiftUI
/// A reserved toolbar row keeps feedback from resizing or replacing the rule table.
struct RulesOperationStatusView: View {
@ObservedObject var workflow: RulesWorkflow
@State private var showsDetails = false
var body: some View {
HStack(spacing: 8) {
Spacer(minLength: 0)
switch workflow.snapshot.operationStatus {
case .idle, .initialLoading:
EmptyView()
case .updating:
progress("正在更新规则…")
case .refreshing:
progress("正在加载规则…")
case .collectionIncomplete:
Label(RulesCopy.text("集合不完整"), systemImage: "exclamationmark.triangle")
.foregroundStyle(.orange)
detailsButton
refreshButton
case .feedback(let feedback):
Label {
Text(verbatim: feedback.summary).lineLimit(1).truncationMode(.middle)
} icon: {
Image(
systemName: feedback.outcome.isSuccess ? "checkmark.circle" : "exclamationmark.triangle"
)
.foregroundStyle(feedback.outcome.isSuccess ? Color.secondary : Color.orange)
}
.help(feedback.summary)
if !feedback.outcome.isSuccess {
detailsButton
Button(RulesCopy.text("关闭"), systemImage: "xmark") {
workflow.dismissFeedback()
}.labelStyle(.iconOnly)
}
}
}
.font(.caption)
.controlSize(.small)
.frame(maxWidth: .infinity, minHeight: 22, maxHeight: 22)
.onChange(of: workflow.snapshot.operationStatus) { _, _ in showsDetails = false }
}
private func progress(_ key: String) -> some View {
HStack(spacing: 6) {
ProgressView().controlSize(.mini)
Text(RulesCopy.text(key))
}.accessibilityElement(children: .combine)
}
private var detailsButton: some View {
Button(RulesCopy.text("查看详情")) { showsDetails = true }
.popover(isPresented: $showsDetails) { details }
}
private var refreshButton: some View {
Button(RulesCopy.text("刷新规则")) {
showsDetails = false
Task { await workflow.refresh(retryFailedSources: true) }
}
}
private var details: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
if let feedback = workflow.snapshot.commitFeedback {
Text(verbatim: feedback.summary).font(.headline)
if let detail = feedback.outcome.failureDetail {
Text(verbatim: detail).textSelection(.enabled)
}
if let nextStep = feedback.outcome.nextStep {
Text(RulesCopy.text(nextStep))
}
}
if !workflow.snapshot.issues.isEmpty {
Text(RulesCopy.text("规则列表未更新,请刷新规则。"))
RulesCollectionIssuesView(issues: workflow.snapshot.issues)
}
}.frame(maxWidth: .infinity, alignment: .leading).padding()
}.frame(width: 380).frame(maxHeight: 300)
}
}
struct RulesCollectionIssuesView: View {
let issues: [RulesPageSnapshot.Issue]
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(RulesCopy.text("集合不完整时,覆盖解释仅基于已加载来源。"))
ForEach(issues, id: \.self) { issue in
Text(verbatim: description(issue)).textSelection(.enabled)
}
}.font(.caption)
}
private func description(_ issue: RulesPageSnapshot.Issue) -> String {
switch issue {
case .userDocument(let detail): "\(RulesCopy.text("自定义规则")): \(detail)"
case .builtin(let source, let detail): "\(source.label): \(detail)"
}
}
}