mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-13 04:05:06 +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 阶段。
109 lines
4.0 KiB
Swift
109 lines
4.0 KiB
Swift
import Foundation
|
|
|
|
/// 侧栏底部代理状态卡的呈现模型(issue #60,架构评审候选④):从代理控制
|
|
/// 工作流的整体 snapshot 派生卡片数据——行结构(哪些行、条件 detail、顺序)、
|
|
/// 活动目标回退文案与语义色调政策全部在此决定;文本复用 StatusMenuModel 的
|
|
/// 同一映射(配色与文本同源)。不持有 SwiftUI/AppKit 类型,纯逻辑可测;视觉
|
|
/// 细节(tone→语义色、排版)在 StatusCardView 呈现,不建自动测试。
|
|
enum StatusCardModel {
|
|
/// 语义色调:呈现层映射为系统语义色(positive=green、attention=orange、
|
|
/// failure=red、neutral=secondary)。
|
|
enum Tone: Equatable {
|
|
case positive
|
|
case attention
|
|
case failure
|
|
case neutral
|
|
}
|
|
|
|
struct Row: Equatable {
|
|
let label: String
|
|
let text: String
|
|
let tone: Tone
|
|
}
|
|
|
|
struct DetailLine: Equatable {
|
|
let text: String
|
|
let tone: Tone
|
|
}
|
|
|
|
/// 活动目标行:正文与悬停/辅助说明(未激活时按模式区分回退文案)。
|
|
struct TargetPresentation: Equatable {
|
|
let text: String
|
|
let help: String
|
|
}
|
|
|
|
struct Card: Equatable {
|
|
let runtimeRow: Row
|
|
/// agent 侧点名原因(运行失败或激活拒绝);无则 nil。
|
|
let runtimeDetail: DetailLine?
|
|
let systemProxyRow: Row
|
|
/// 系统代理写入/清理失败点名;非失败态为 nil。
|
|
let systemProxyDetail: DetailLine?
|
|
let target: TargetPresentation
|
|
/// 模式行文本(规则模式带子选项,与状态菜单同口径)。
|
|
let modeText: String
|
|
}
|
|
|
|
static func card(from snapshot: ProxyControlSnapshot) -> Card {
|
|
let summary = StatusMenuModel.summary(from: snapshot)
|
|
let runtimeTone = runtimeTone(for: snapshot.runtime.status)
|
|
return Card(
|
|
runtimeRow: Row(label: "后台代理", text: summary.status, tone: runtimeTone),
|
|
runtimeDetail: summary.detail.map {
|
|
DetailLine(text: $0, tone: runtimeDetailTone(snapshot: snapshot, runtimeTone: runtimeTone))
|
|
},
|
|
systemProxyRow: Row(
|
|
label: "系统代理设置",
|
|
text: summary.systemProxyStateLabel,
|
|
tone: systemProxyTone(for: snapshot.systemProxyApplication)),
|
|
systemProxyDetail: summary.systemProxyDetail.map {
|
|
DetailLine(text: $0, tone: .failure)
|
|
},
|
|
target: targetPresentation(summary: summary, proxyMode: snapshot.proxyMode),
|
|
modeText: summary.modeLabel)
|
|
}
|
|
|
|
private static func runtimeTone(for status: ProxyRuntimeStatus) -> Tone {
|
|
switch status {
|
|
case .running: .positive
|
|
case .starting, .off: .neutral
|
|
case .firewallBlocked, .requiresApproval: .attention
|
|
case .launchFailed, .serviceFailed: .failure
|
|
}
|
|
}
|
|
|
|
/// agent 点名原因色调:运行失败跟随运行状态色调;激活点名失败独立显红;
|
|
/// 其余中性。
|
|
private static func runtimeDetailTone(snapshot: ProxyControlSnapshot, runtimeTone: Tone) -> Tone {
|
|
if snapshot.runtime.failure != nil {
|
|
return runtimeTone
|
|
}
|
|
return snapshot.activationFailure == nil ? .neutral : .failure
|
|
}
|
|
|
|
private static func systemProxyTone(for application: SystemProxyApplicationFacts) -> Tone {
|
|
switch application {
|
|
case .idle: .neutral
|
|
case .pending, .changed, .applying, .repairing, .paused: .attention
|
|
case .applied: .positive
|
|
case .failed, .repairFailed, .clearFailed, .unreadable: .failure
|
|
}
|
|
}
|
|
|
|
/// 活动目标回退:有目标用路径;未激活时直连模式无需目标,其余模式提示
|
|
/// 未激活。
|
|
private static func targetPresentation(
|
|
summary: StatusMenuModel.Summary, proxyMode: ProxyMode
|
|
) -> TargetPresentation {
|
|
if let targetPath = summary.targetPath {
|
|
return TargetPresentation(text: targetPath, help: "活动目标:\(targetPath)")
|
|
}
|
|
if proxyMode == .direct {
|
|
return TargetPresentation(
|
|
text: "直连模式(无需服务器)", help: "直连模式无需选择活动目标")
|
|
}
|
|
return TargetPresentation(
|
|
text: "未激活", help: "未设置活动目标;在首页或服务器目录中激活")
|
|
}
|
|
}
|