mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-09 03:44:51 +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 阶段。
107 lines
3.7 KiB
Swift
107 lines
3.7 KiB
Swift
import SwiftUI
|
||
|
||
struct RulesAddressTestView: View {
|
||
@ObservedObject var workflow: RulesWorkflow
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
HStack {
|
||
TextField(
|
||
RulesCopy.text("地址(域名、IP 或 URL)"),
|
||
text: Binding(
|
||
get: { workflow.snapshot.addressTest.target },
|
||
set: { workflow.setTestTarget($0) })
|
||
)
|
||
.textFieldStyle(.roundedBorder)
|
||
.autocorrectionDisabled()
|
||
.onSubmit { submit() }
|
||
Button(RulesCopy.text("测试")) { submit() }
|
||
.disabled(
|
||
workflow.snapshot.isLoading || workflow.snapshot.isCommitting
|
||
|| workflow.snapshot.addressTest.isTesting)
|
||
}
|
||
if workflow.snapshot.addressTest.isTesting {
|
||
ProgressView(RulesCopy.text("测试中…")).controlSize(.small)
|
||
}
|
||
if let failure = workflow.snapshot.addressTest.failure {
|
||
Label(
|
||
RulesCopy.text(
|
||
failure == .invalidTarget
|
||
? "请输入有效的域名、IP 或带主机的 URL。" : "集合不完整,无法测试地址。"),
|
||
systemImage: "exclamationmark.triangle")
|
||
}
|
||
if let result = workflow.snapshot.addressTest.result {
|
||
resultSummary(result)
|
||
ScrollView {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
evidence(result.deciding, title: "决定结果的规则", explanation: nil)
|
||
evidence(
|
||
result.otherMatches, title: "其他命中规则",
|
||
explanation: result.explanation == .fixedLocal
|
||
? "固定本地策略优先。" : result.explanation.label)
|
||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||
}.frame(maxHeight: 140)
|
||
}
|
||
}.padding()
|
||
}
|
||
|
||
private func submit() {
|
||
guard !workflow.snapshot.isLoading, !workflow.snapshot.isCommitting,
|
||
!workflow.snapshot.addressTest.isTesting
|
||
else { return }
|
||
Task { await workflow.testAddress() }
|
||
}
|
||
|
||
private func resultSummary(_ result: OfflineRuleMatcher.Result) -> some View {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
HStack {
|
||
Text(result.outcome.label).font(.headline)
|
||
Text(verbatim: result.target).textSelection(.enabled)
|
||
}
|
||
if result.domainWithoutDNS {
|
||
Text(RulesCopy.text("未经 DNS 解析,未测试 IP 规则")).font(.caption)
|
||
}
|
||
if let explanation = result.explanation.label {
|
||
Text(RulesCopy.text(explanation)).font(.caption)
|
||
}
|
||
Text(RulesCopy.text("显示顺序不代表路由优先级。"))
|
||
.font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func evidence(_ rows: [RulesRow], title: String, explanation: String?) -> some View {
|
||
if !rows.isEmpty {
|
||
Text(RulesCopy.text(title)).font(.subheadline.bold())
|
||
ForEach(rows) { row in
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(verbatim: "\(row.displayContent) · \(row.matchType) · \(row.actionLabel)")
|
||
Text(verbatim: row.sourceLabels).foregroundStyle(.secondary)
|
||
if let explanation { Text(RulesCopy.text(explanation)) }
|
||
}.font(.caption).textSelection(.enabled)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
extension OfflineRuleMatcher.Outcome {
|
||
var label: String {
|
||
switch self {
|
||
case .proxy: RulesCopy.text("代理")
|
||
case .direct: RulesCopy.text("直连")
|
||
case .unmatched: RulesCopy.text("未匹配规则")
|
||
}
|
||
}
|
||
}
|
||
|
||
extension OfflineRuleMatcher.Explanation {
|
||
var label: String? {
|
||
switch self {
|
||
case .fixedLocal: "固定本地策略优先。"
|
||
case .domainProxy: "域名同时命中两种行动时,代理优先。"
|
||
case .ipDirect: "IP 同时命中两种行动时,直连优先。"
|
||
case .singleAction, .noMatch: nil
|
||
}
|
||
}
|
||
}
|