diff --git a/CHANGELOG.md b/CHANGELOG.md index 06bb339..8c4258a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ ## 未发布 +### 2026-10-01 + +#### 新增 + +- 菜单栏布局多一种“仅图标”:菜单栏只显示 OpenStats 标志,不画任何数值,点击弹出完整面板。原来要把所有显示项目都关掉才能只剩一个图标,而且显示的是通用的心电图符号;现在关掉所有项目时也显示 OpenStats 标志。仅图标模式下“整体风格”和“显示项目”暂不生效,已开启的项目会保留,切回其他布局后恢复。 +- “合并显示”和“仅图标”点开的是完整面板,指标、进程与常用工具集中在一页:顶部是健康评分与芯片、内存、系统、运行时间标签,下面两列卡片显示 CPU、GPU、内存、磁盘、网络、风扇(可直接切换自动 / 降温 / 强冷),再往下是电池、高占用进程,以及摄像头和麦克风是否正在使用、防休眠与清理入口、累计清理记录(已清理空间、已卸载应用、已优化次数)。顶部标签列出这台 Mac 上的全部项目,不再只列菜单栏里开启的。 +- 风扇从停转起步时显示“正在启动风扇…”。Apple 芯片的风扇固件从停转起步要先低速拖转约 5 秒,期间转速读数一直是 0,之前看起来像是点了降温没反应。 + +#### 调整 + +- “合并为一个”改名为“合并显示”。 +- 仪表盘的芯片、内存、系统、运行时间、机型标签始终排成一行,放不下时依次省去机型、把运行时间缩成最大单位。 + ### 2026-09-24 #### 新增 diff --git a/Packages/OpenStatsKit/Sources/Localization/Translations.swift b/Packages/OpenStatsKit/Sources/Localization/Translations.swift index cbd5976..9bde0ad 100644 --- a/Packages/OpenStatsKit/Sources/Localization/Translations.swift +++ b/Packages/OpenStatsKit/Sources/Localization/Translations.swift @@ -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 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift index 1e6c09e..48d2371 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift @@ -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) } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarController.swift index 6ca0a8e..b7eadab 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarController.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarController.swift @@ -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) } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift index 5f6c453..7208a3d 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift @@ -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])) diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift index f90ef07..b4b3b20 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift @@ -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 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift index 2c2281b..acb9c5e 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift @@ -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)")) } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift index f08474d..d686354 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift @@ -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 } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift index bfa48a5..e14f992 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift @@ -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 {