feat: show runtime status dots in menu bar

This commit is contained in:
Qiu Yuzhou
2026-10-28 18:56:07 +08:00
parent b1b9fcfd37
commit 3e3749e3a6
3 changed files with 53 additions and 32 deletions
+1 -18
View File
@@ -62,7 +62,7 @@ struct ShadowsocksXNG2App: App {
ProxyStatusMenu(
control: proxyControl, statusDisplay: statusDisplay)
} label: {
StatusMenuLabel(control: proxyControl)
StatusMenuLabel(control: proxyControl, statusDisplay: statusDisplay)
}
// .menuBarExtraStyle(.window)
@@ -148,23 +148,6 @@ private final class NSAppWindowActivationApplier: WindowActivationPolicyApplying
}
}
/// 状态菜单的标签视图(状态栏图标)。图标为静态 SF Symbol,不随代理状态
/// 变化,故不观察 control——control 仅承载启动 resync。挂载时机与菜单内容
/// 视图不同:内容视图首次点开才创建,而标签在启动时即挂载——启动 resync
/// 只能挂在这里,挂在菜单内容上等于从不执行。主窗口的启动呈现由 Window
/// scene 的 defaultLaunchBehavior 负责,与本任务无时序耦合。
private struct StatusMenuLabel: View {
let control: ProxyControlWorkflow
var body: some View {
Image(systemName: "paperplane.fill")
.accessibilityLabel("ShadowsocksX-NG2")
.task {
await control.resyncOnLaunch()
}
}
}
/// 组合根装配:目录提交协调器与全部生产运行时适配器只在此接线一次,各 scene
/// 不再重复设置提交回调;目录工作流的实现依赖经 CatalogWorkflowDependencies
/// 显式装配(workflow 不自行创建生产默认实现);Legacy 导入后的 2.0 运行时
@@ -64,10 +64,7 @@ struct RuntimeControlCard: View {
private func statusRow(_ row: StatusCardModel.Row) -> some View {
HStack(spacing: 6) {
Circle()
.fill(statusColor(row.tone))
.frame(width: 8, height: 8)
.accessibilityHidden(true)
RuntimeStatusDot(tone: row.tone, diameter: 8)
Text(row.text)
.font(.body.weight(.medium))
.foregroundStyle(.secondary)
@@ -75,15 +72,6 @@ struct RuntimeControlCard: View {
}
}
private func statusColor(_ tone: StatusCardModel.Tone) -> Color {
switch tone {
case .positive: .green
case .attention: .orange
case .failure: .red
case .neutral: .secondary
}
}
private var agentBinding: Binding<Bool> {
Binding(
get: { control.snapshot.agentIntentEnabled },
@@ -1,3 +1,4 @@
import AppKit
import SwiftUI
/// 菜单栏面板代理状态卡。状态行经 StatusDisplayModel 的瞬态门控呈现
@@ -36,8 +37,21 @@ struct StatusCardView: View {
.lineLimit(2)
.help(detail.text)
}
}
private func color(_ tone: StatusCardModel.Tone) -> Color {
/// 首页与状态栏共用圆点及色调映射,尺寸由显示位置决定。
struct RuntimeStatusDot: View {
let tone: StatusCardModel.Tone
let diameter: CGFloat
var body: some View {
Circle()
.fill(color)
.frame(width: diameter, height: diameter)
.accessibilityHidden(true)
}
private var color: Color {
switch tone {
case .positive: .green
case .attention: .orange
@@ -46,3 +60,39 @@ struct StatusCardView: View {
}
}
}
/// 标签在启动时即挂载;启动 resync 必须留在此处,不能移入首次点开才创建的菜单内容。
struct StatusMenuLabel: View {
let control: ProxyControlWorkflow
let statusDisplay: StatusDisplayModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.displayScale) private var displayScale
var body: some View {
Image(nsImage: statusImage)
.renderingMode(.original)
.accessibilityLabel("ShadowsocksX-NG2")
.task {
await control.resyncOnLaunch()
}
}
/// MenuBarExtra 使用单张图像承载主图标和圆点,保留圆点的原始颜色。
private var statusImage: NSImage {
let renderer = ImageRenderer(
content: HStack(spacing: 2) {
Image(systemName: "paperplane.fill")
.font(.system(size: 16))
.foregroundStyle(.primary)
.frame(width: 18, height: 18)
VStack(spacing: 5) {
RuntimeStatusDot(tone: statusDisplay.agent.row.tone, diameter: 4)
RuntimeStatusDot(tone: statusDisplay.systemProxy.row.tone, diameter: 4)
}
}
.environment(\.colorScheme, colorScheme))
renderer.scale = displayScale
return renderer.nsImage ?? NSImage(
systemSymbolName: "paperplane.fill", accessibilityDescription: nil) ?? NSImage(size: .zero)
}
}