菜单栏布局新增“仅图标”:只显示 OpenStats 标志,点击弹出完整面板;关掉所有项目时也显示标志;“合并为一个”改名“合并显示”;更新日志补上今天的改动

This commit is contained in:
gentpan committed 2026-10-01 17:35:24 +05:00
1 parent 83c5ece4b2
commit 26d52cbe45
9 files changed
+128 -57

No files matched your search

@@ -186,6 +186,8 @@ npm 缓存 npm Cache
云端只保存邮箱、姓名与设置文档 The cloud only keeps your email, name and the settings document
云端已有 {} 在 {} 保存的设置,和这台 Mac 上的不一样。要用哪一份? {1} saved settings to the cloud {2}, and they differ from this Mac's. Which one should be used?
交换区 Swap
仅图标 Icon Only
仅图标模式下菜单栏不显示数值,下面的风格与显示项目暂不生效;切回其他布局后恢复。 In Icon Only mode the menu bar shows no values, so the style and item settings below are paused. They come back when you switch to another layout.
仅系统不休眠 Keep system awake
介质错误 Media errors
从左侧选择应用,或把应用拖到这里 Choose an app on the left, or drop one here
@@ -357,6 +359,7 @@ npm 缓存 npm Cache
只在菜单栏显示 GPU 或打开 GPU 相关页面时记录 Recorded only when GPU is in the menu bar or a GPU page is open
只在菜单栏显示温度、开启过热通知或打开温度页面时记录 Recorded only when temperature is in the menu bar, overheating alerts are on, or a temperature page is open
只在菜单栏显示电池或打开电池页面时记录 Recorded only while Battery is in the menu bar or the Battery page is open
只显示 OpenStats 标志,点击弹出完整面板 Shows only the OpenStats logo; click for the full panel
只显示菜单栏时的采样间隔;打开弹窗或主窗口时为 1 秒(进程页 2 秒) Sampling interval when only the menu bar is shown; 1 second with a popover or window open (2 seconds on Processes)
只查找以该应用包名命名的文件,以及 Application Support、Logs 下与应用同名的目录;钥匙串与其他应用共享的数据不会动。程序坞里的图标会一并移除 Only files named after the app's bundle ID, plus folders with the app's name in Application Support and Logs, are included. Keychains and data shared with other apps are never touched. The Dock icon is removed as well
只设置了系统代理:浏览器等应用走代理,命令行工具、游戏等不读代理设置的程序仍在直连。要接管全部流量,可以开启增强模式或 TUN 模式。 Only a system proxy is set: browsers and most apps use it, but command-line tools, games, and other programs that ignore proxy settings still connect directly. To route all traffic, turn on enhanced mode or TUN mode.
@@ -376,7 +379,7 @@ npm 缓存 npm Cache
各核心负载 Core Load
各项指标正常 Everything looks good
合上屏幕时 Mac 不进入睡眠,下载、渲染、远程连接不中断。 Your Mac stays awake with the lid closed, so downloads, renders and remote sessions continue.
合并为一个 Combined
合并显示 Combined
合盖后继续运行 Keep Running with Lid Closed
合盖电量下限 Lid Mode Battery Floor
合盖睡眠 Lid sleep
@@ -277,7 +277,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
}
private func updateNetworkVisibility() {
model.network.setVisibility(inMenuBar: model.settings.menuBarItems.contains(.network),
model.network.setVisibility(inMenuBar: model.settings.drawnMenuBarItems.contains(.network),
detailVisible: model.isNetworkDetailVisible)
}
@@ -26,8 +26,8 @@ final class MenuBarController: NSObject {
/// 布局或显示项目变化时重建;否则只重绘图片
func update() {
let settings = model.settings
let enabled = settings.orderedMenuBarItems
// 没有任何项目时保留一个图标,确保仍能打开面板与主窗口
let enabled = settings.drawnMenuBarItems
// 没有任何项目时保留一个图标(OpenStats 标志),确保仍能打开面板与主窗口
let signature: [MenuBarItem?] = settings.menuBarLayout == .separate && !enabled.isEmpty ? enabled : [nil]
if signature != layoutSignature {
rebuild(signature)
@@ -76,7 +76,7 @@ final class MenuBarController: NSObject {
let settings = model.settings
let reading = MenuBarReading(model: model)
for (index, entry) in items.enumerated() {
let itemsToDraw = entry.key.map { [$0] } ?? settings.orderedMenuBarItems
let itemsToDraw = entry.key.map { [$0] } ?? settings.drawnMenuBarItems
var entryReading = reading
// 防休眠标记只画在最左侧的图标里
entryReading.keepAwake = reading.keepAwake && index == 0
@@ -86,7 +86,7 @@ final class MenuBarController: NSObject {
networkStyle: settings.networkStyle,
colorizeHighLoad: settings.colorizeHighLoad,
fahrenheit: settings.useFahrenheit)
entry.statusItem.button?.toolTip = reading.tooltip(items: itemsToDraw, fahrenheit: settings.useFahrenheit)
entry.statusItem.button?.toolTip = itemsToDraw.isEmpty ? "OpenStats" : reading.tooltip(items: itemsToDraw, fahrenheit: settings.useFahrenheit)
}
}
@@ -125,6 +125,9 @@ enum MenuBarRenderer {
static let upperBaseline: CGFloat = 13
static let lowerBaseline: CGFloat = 3
static let symbolSize: CGFloat = 11
/// OpenStats 标志的外框与线宽
static let logoSize = NSSize(width: 22, height: 14)
static let logoLine: CGFloat = 1.75
static let itemGap = DS.Space.s3
static let innerGap = DS.Space.s1
static let historyCount = 10
@@ -154,7 +157,7 @@ enum MenuBarRenderer {
static func image(for model: AppModel) -> NSImage {
let settings = model.settings
return image(reading: MenuBarReading(model: model),
items: settings.orderedMenuBarItems,
items: settings.drawnMenuBarItems,
style: { settings.style(for: $0) },
networkStyle: settings.networkStyle,
colorizeHighLoad: settings.colorizeHighLoad,
@@ -173,7 +176,8 @@ enum MenuBarRenderer {
segments.append(segment(for: item, reading: reading, style: style(item), networkStyle: networkStyle,
colorizeHighLoad: colorizeHighLoad, fahrenheit: fahrenheit))
}
if segments.isEmpty { segments.append(symbolSegment("waveform.path.ecg")) }
// 仅图标或没有开启任何项目:画 OpenStats 标志;防休眠时杯子标记仍在左边
if items.isEmpty { segments.append(logoSegment()) }
let width = segments.reduce(0) { $0 + $1.width } + CGFloat(segments.count - 1) * Metrics.itemGap
let image = NSImage(size: NSSize(width: ceil(width), height: Metrics.barHeight), flipped: false) { rect in
@@ -507,6 +511,27 @@ enum MenuBarRenderer {
return segment
}
/// OpenStats 标志:与 App 图标同一条脉搏折线,单色绘制,随菜单栏浅色 / 深色变化
private static func logoSegment() -> Segment {
let size = Metrics.logoSize
return Segment(width: size.width) { rect in
let box = NSRect(x: rect.minX, y: rect.midY - size.height / 2, width: size.width, height: size.height)
.insetBy(dx: Metrics.logoLine / 2, dy: Metrics.logoLine / 2)
// 折线顶点按 App 图标量取:横向 0…1 从左到右,纵向 0…1 从上到下
let vertices: [(x: CGFloat, y: CGFloat)] = [(0, 0.54), (0.2, 0.54), (0.41, 0), (0.51, 1), (0.7, 0.27), (0.8, 0.54), (1, 0.54)]
let path = NSBezierPath()
for (index, vertex) in vertices.enumerated() {
let point = NSPoint(x: box.minX + vertex.x * box.width, y: box.maxY - vertex.y * box.height)
if index == 0 { path.move(to: point) } else { path.line(to: point) }
}
path.lineWidth = Metrics.logoLine
path.lineCapStyle = .round
path.lineJoinStyle = .round
NSColor.labelColor.setStroke()
path.stroke()
}
}
private static func symbolSegment(_ name: String) -> Segment {
let configuration = NSImage.SymbolConfiguration(pointSize: Metrics.symbolSize, weight: .medium)
.applying(.init(paletteColors: [.labelColor]))
@@ -284,57 +284,65 @@ struct MenuBarSettings: View {
SettingsGroup(caption: tr("布局")) {
GroupRow(showsDivider: false) {
SettingRow(title: tr("菜单栏图标"),
subtitle: settings.menuBarLayout == .separate
? tr("每个指标一个图标,点击弹出该项详情")
: tr("所有指标合成一个图标,点击弹出完整面板:指标、进程与常用工具集中在一页")) {
SettingRow(title: tr("菜单栏图标"), subtitle: layoutSubtitle(settings.menuBarLayout)) {
SegmentedControl(selection: $settings.menuBarLayout,
options: MenuBarLayout.allCases.map { ($0, $0.title) })
.frame(width: DS.Size.sidebarWidth + DS.Space.s6)
.frame(width: DS.Size.sidebarWidth + DS.Space.s16)
}
}
}
SettingsGroup(caption: tr("整体风格")) {
LazyVGrid(columns: [GridItem(.flexible(), spacing: DS.Space.s3), GridItem(.flexible(), spacing: DS.Space.s3)],
spacing: DS.Space.s3) {
ForEach(MenuBarStyle.allCases) { style in
StyleCard(style: style, isSelected: settings.menuBarStyle == style) {
settings.menuBarStyle = style
let iconOnly = settings.menuBarLayout == .iconOnly
if iconOnly {
InfoBanner(icon: "info.circle",
text: tr("仅图标模式下菜单栏不显示数值,下面的风格与显示项目暂不生效;切回其他布局后恢复。"))
}
Group {
// 仅图标时菜单栏不画数值,风格与显示项目保留设置但暂不生效
SettingsGroup(caption: tr("整体风格")) {
LazyVGrid(columns: [GridItem(.flexible(), spacing: DS.Space.s3), GridItem(.flexible(), spacing: DS.Space.s3)],
spacing: DS.Space.s3) {
ForEach(MenuBarStyle.allCases) { style in
StyleCard(style: style, isSelected: settings.menuBarStyle == style) {
settings.menuBarStyle = style
}
}
}
.padding(DS.Space.s3)
HairlineDivider()
Text(tr("整体风格套用到所有项目。想让某个项目不一样,在下面“显示项目”里给它单独选一种,比如 CPU 用圆环、风扇用数字。"))
.dsFont(.xs)
.foregroundStyle(DS.Palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, DS.Space.s4)
.padding(.vertical, DS.Space.s3)
}
.padding(DS.Space.s3)
HairlineDivider()
Text(tr("整体风格套用到所有项目。想让某个项目不一样,在下面“显示项目”里给它单独选一种,比如 CPU 用圆环、风扇用数字。"))
.dsFont(.xs)
.foregroundStyle(DS.Palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, DS.Space.s4)
.padding(.vertical, DS.Space.s3)
}
SettingsGroup(caption: tr("显示项目")) {
GroupRow(showsDivider: false) {
InfoBanner(icon: "hand.draw",
text: tr("菜单栏里的图标可以调整顺序:按住 ⌘ 键拖动任意一个,松开后位置会一直保留。新开启的项目由系统安排位置,可能离其他图标较远,拖一下就能挪到一起。"))
}
ForEach(Array(MenuBarItem.allCases.enumerated()), id: \.element) { index, item in
GroupRow {
VStack(alignment: .leading, spacing: DS.Space.s3) {
SettingRow(title: item.title, subtitle: itemSubtitle(item), icon: item.symbol) {
DSToggle(isOn: Binding(get: { settings.isEnabled(item) },
set: { settings.setEnabled(item, $0) }),
label: item.title)
}
if settings.isEnabled(item) {
ItemStyleRow(item: item)
PopoverSectionPicker(item: item)
SettingsGroup(caption: tr("显示项目")) {
GroupRow(showsDivider: false) {
InfoBanner(icon: "hand.draw",
text: tr("菜单栏里的图标可以调整顺序:按住 ⌘ 键拖动任意一个,松开后位置会一直保留。新开启的项目由系统安排位置,可能离其他图标较远,拖一下就能挪到一起。"))
}
ForEach(Array(MenuBarItem.allCases.enumerated()), id: \.element) { index, item in
GroupRow {
VStack(alignment: .leading, spacing: DS.Space.s3) {
SettingRow(title: item.title, subtitle: itemSubtitle(item), icon: item.symbol) {
DSToggle(isOn: Binding(get: { settings.isEnabled(item) },
set: { settings.setEnabled(item, $0) }),
label: item.title)
}
if settings.isEnabled(item) {
ItemStyleRow(item: item)
PopoverSectionPicker(item: item)
}
}
}
}
}
}
.disabled(iconOnly)
.opacity(iconOnly ? 0.5 : 1)
SettingsGroup(caption: tr("其他")) {
GroupRow(showsDivider: false) {
@@ -351,6 +359,14 @@ struct MenuBarSettings: View {
}
}
private func layoutSubtitle(_ layout: MenuBarLayout) -> String {
switch layout {
case .separate: tr("每个指标一个图标,点击弹出该项详情")
case .combined: tr("所有指标合成一个图标,点击弹出完整面板:指标、进程与常用工具集中在一页")
case .iconOnly: tr("只显示 OpenStats 标志,点击弹出完整面板")
}
}
/// 没有电池的 Mac 上,电池项目改为显示蓝牙设备电量
private func itemSubtitle(_ item: MenuBarItem) -> String {
item == .battery && model.store.battery == nil
@@ -89,16 +89,22 @@ enum SnapshotRenderer {
}
let menuBar = MenuBarRenderer.image(for: model)
for dark in [false, true] {
let preview = MenuBarRenderer.preview(menuBar, dark: dark)
// 垫上菜单栏底色,便于查看
let padded = NSImage(size: NSSize(width: preview.size.width + DS.Space.s4, height: preview.size.height), flipped: false) { rect in
NSColor(hex: dark ? 0x1F2937 : 0xE5E7EB).setFill()
rect.fill()
preview.draw(in: NSRect(x: DS.Space.s2, y: 0, width: preview.size.width, height: preview.size.height))
return true
let layout = settings.menuBarLayout
settings.menuBarLayout = .iconOnly
let iconOnly = MenuBarRenderer.image(for: model)
settings.menuBarLayout = layout
for (image, name) in [(menuBar, "menubar"), (iconOnly, "menubar-icon-only")] {
for dark in [false, true] {
let preview = MenuBarRenderer.preview(image, dark: dark)
// 垫上菜单栏底色,便于查看
let padded = NSImage(size: NSSize(width: preview.size.width + DS.Space.s4, height: preview.size.height), flipped: false) { rect in
NSColor(hex: dark ? 0x1F2937 : 0xE5E7EB).setFill()
rect.fill()
preview.draw(in: NSRect(x: DS.Space.s2, y: 0, width: preview.size.width, height: preview.size.height))
return true
}
writePNG(padded, scale: 2, to: outputDirectory.appendingPathComponent("\(name)-\(dark ? "dark" : "light").png"))
}
writePNG(padded, scale: 2, to: outputDirectory.appendingPathComponent("menubar-\(dark ? "dark" : "light").png"))
}
print(tr("截图已输出到 \(outputDirectory.path)"))
}
@@ -90,7 +90,7 @@ public final class AppModel {
let tab = settings.panelTab
let window = isMainWindowVisible
let popover = openPopover
let menu = settings.menuBarItems
let menu = Set(settings.drawnMenuBarItems)
let thermalPopover = popover == .temperature || popover == .fan
// 主窗口仪表盘或合并面板的总览
let overview = (window && tab == .overview) || (isCombinedPopoverOpen && combinedPopoverTab == nil)
@@ -129,7 +129,7 @@ public final class AppModel {
/// 菜单栏开着电池项且要做低电量提示、或这台 Mac 没有电池时每 5 分钟
var bluetoothDemand: BluetoothController.Demand {
if (isMainWindowVisible && [.battery, .system].contains(settings.panelTab)) || openPopover == .battery { return .foreground }
if settings.menuBarItems.contains(.battery) && (settings.bluetoothLowBatteryInMenuBar || store.battery == nil) { return .background }
if settings.drawnMenuBarItems.contains(.battery) && (settings.bluetoothLowBatteryInMenuBar || store.battery == nil) { return .background }
return .off
}
@@ -141,15 +141,18 @@ public enum PopoverSection: String, CaseIterable, Identifiable, Sendable {
public enum MenuBarLayout: String, CaseIterable, Identifiable, Sendable {
/// 每个指标一个图标,点击弹出该项详情
case separate
/// 所有指标合成一个图标,点击弹出状态总览,可切到各项详情
/// 所有指标合成一个图标,点击弹出完整面板
case combined
/// 只显示 OpenStats 标志,不画任何数值,点击弹出完整面板
case iconOnly
public var id: String { rawValue }
var title: String {
switch self {
case .separate: tr("每项独立")
case .combined: tr("合并为一个")
case .combined: tr("合并显示")
case .iconOnly: tr("仅图标")
}
}
}
@@ -564,6 +567,11 @@ public final class AppSettings {
MenuBarItem.allCases.filter(menuBarItems.contains)
}
/// 菜单栏上实际画出来的项目:“仅图标”时一项都不画,开着的项目保留,切回其他布局时恢复
var drawnMenuBarItems: [MenuBarItem] {
menuBarLayout == .iconOnly ? [] : orderedMenuBarItems
}
func isEnabled(_ item: MenuBarItem) -> Bool { menuBarItems.contains(item) }
func style(for item: MenuBarItem) -> MenuBarStyle {