菜单栏新增双行居中风格:标签比数值宽时数值不再偏到右边

This commit is contained in:
gentpan committed 2026-09-21 20:37:04 +05:00
1 parent 2efebb5008
commit d05b02be4f
8 files changed
+78 -50

No files matched your search

+8
View File
@@ -3,6 +3,14 @@
记录 OpenStats 应用的功能新增、样式调整和问题修复,按版本与日期倒序排列。
官网、构建与发布脚本等不影响应用本身的改动不在此记录。
## 未发布
### 2026-09-21
#### 新增
- 菜单栏多一种“双行居中”风格:两行各自居中。原来的“双行文字”是两行一起靠右对齐,标签比数值宽的时候(风扇 0、温度 54°),数值会被推到右边看着偏。居中之后数值正落在标签下面。设置 · 菜单栏里选整体风格,也可以只给某一项单独用。
## 0.6.2 · 2026-09-21
### 2026-09-21
@@ -334,6 +334,7 @@ npm 缓存 npm Cache
去批准 Approve
去授权 Grant Access
双行圆点 Dots
双行居中 Stacked, Centered
双行文字 Stacked Text
双行箭头 Arrows
反馈问题 Report a Problem
@@ -382,6 +383,7 @@ npm 缓存 npm Cache
合盖运行时散热变差,请勿放入包中。使用电池且电量低于 {}% 时会自动关闭。 Cooling is worse with the lid closed, so don't put your Mac in a bag. Turns off automatically on battery below {}%.
合盖运行设置 Lid Mode
合盖运行需要修改系统睡眠设置,需安装辅助工具(管理员授权一次)。 Lid mode changes system sleep settings and needs the helper (one-time admin approval).
同双行文字,但两行各自居中;标签比数值宽时(风扇、温度)不会偏到一边 Same as stacked, but both lines are centered, so a value narrower than its label (fan, temperature) is not pushed to one side
同步 Sync
同步失败 Sync failed
同步的内容:菜单栏项目与风格、刷新频率、外观与语言、详情弹窗的区块、连接探测、通知、快捷键、风扇安全温度、合盖电量下限。当前页面、辅助工具与历史记录留在本机。 What syncs: menu bar items and styles, refresh rate, appearance and language, popover sections, connection probe, notifications, hotkeys, fan safety temperature and lid-mode battery floor. The current page, the helper and history stay on this Mac.
@@ -450,7 +452,7 @@ npm 缓存 npm Cache
寿命按厂商估算的已用比例计算;写入量越大消耗越快,日常使用通常可用很多年 Life is based on the vendor's wear estimate. Heavy writes wear it faster; typical use lasts many years.
将清理 {} 个项目,共 {}。{} {} items, {} total, will be cleaned. {}
将移到废纸篓 Will move to Trash
小标签在上、数值在下,最紧凑 Small label above the value; most compact
小标签在上、数值在下,两行靠右对齐,数值的右边不会跳 Small label on top, value below, both right-aligned so the value's right edge stays put
尚未检查 Not checked yet
屏幕保持常亮 Keep display on
屏幕可按设置关闭,下载、编译等后台任务继续运行 The display can turn off as usual while downloads and builds keep running
@@ -243,10 +243,11 @@ enum MenuBarRenderer {
let text = Format.percent(fraction)
let alert = colorizeHighLoad && fraction <= Metrics.lowBatteryLevel
let label = MenuBarItem.battery.menuBarLabel
let stacked = stackedText(label: label, value: text, sample: "100%", alert: alert)
let stacked = stackedText(label: label, value: text, sample: "100%", alert: alert,
centered: style == .stackedCenter)
var segment: Segment
switch style {
case .stacked: segment = stacked
case .stacked, .stackedCenter: segment = stacked
case .inline: segment = inlineText(label: label, value: text, sample: "100%", alert: alert)
case .icon:
let symbol = reading.batteryCharging ? "battery.100.bolt" : batterySymbol(fraction)
@@ -283,10 +284,11 @@ enum MenuBarRenderer {
let fraction = min(1, max(0, value ?? 0))
let text = value.map { Format.percent($0) } ?? "—"
let alert = colorizeHighLoad && fraction >= Metrics.highLevel
let stacked = stackedText(label: item.menuBarLabel, value: text, sample: "100%", alert: alert)
let stacked = stackedText(label: item.menuBarLabel, value: text, sample: "100%", alert: alert,
centered: style == .stackedCenter)
switch style {
case .stacked:
case .stacked, .stackedCenter:
return stacked
case .inline:
return inlineText(label: item.menuBarLabel, value: text, sample: "100%", alert: alert)
@@ -312,7 +314,8 @@ enum MenuBarRenderer {
switch style {
case .inline: inlineText(label: item.menuBarLabel, value: value, sample: sample, alert: false)
case .icon: combine([symbolSegment(item.symbol), inlineValue(value, sample: sample, alert: false)])
default: stackedText(label: item.menuBarLabel, value: value, sample: sample, alert: false)
default: stackedText(label: item.menuBarLabel, value: value, sample: sample, alert: false,
centered: style == .stackedCenter)
}
}
@@ -323,14 +326,24 @@ enum MenuBarRenderer {
}
/// 颜色保持动态(labelColor / secondaryLabelColor),在绘制时才按菜单栏外观解析
private static func stackedText(label: String, value: String, sample: String, alert: Bool) -> Segment {
/// 两行文字:宽度按标签、数值与样本里最宽的算,所以数值变长也不会挤动旁边的项目。
/// `centered` 时两行各自居中,标签比数值宽时(风扇、温度)数值不会贴在右边
private static func stackedText(label: String, value: String, sample: String, alert: Bool,
centered: Bool = false) -> Segment {
let labelAttributes: [NSAttributedString.Key: Any] = [.font: Metrics.labelFont, .foregroundColor: NSColor.secondaryLabelColor]
let valueAttributes: [NSAttributedString.Key: Any] = [.font: Metrics.stackedValueFont, .foregroundColor: foreground(alert)]
let width = ceil(max(textWidth(label, labelAttributes), textWidth(value, valueAttributes), textWidth(sample, valueAttributes)))
return Segment(width: width) { rect in
let base = baselineOrigin(in: rect)
drawText(label, labelAttributes, rightEdge: rect.maxX, baseline: base + Metrics.upperBaseline)
drawText(value, valueAttributes, rightEdge: rect.maxX, baseline: base + Metrics.lowerBaseline)
let rows = [(label, labelAttributes, base + Metrics.upperBaseline),
(value, valueAttributes, base + Metrics.lowerBaseline)]
for (text, attributes, baseline) in rows {
if centered {
drawText(text, attributes, centerX: rect.midX, baseline: baseline)
} else {
drawText(text, attributes, rightEdge: rect.maxX, baseline: baseline)
}
}
}
}
@@ -363,6 +376,13 @@ enum MenuBarRenderer {
string.draw(with: NSRect(x: rightEdge - width, y: baseline, width: width, height: 0))
}
/// 居中绘制:起点取整像素,文字才不会发虚
private static func drawText(_ text: String, _ attributes: [NSAttributedString.Key: Any], centerX: CGFloat, baseline: CGFloat) {
let string = NSAttributedString(string: text, attributes: attributes)
let width = string.size().width
string.draw(with: NSRect(x: (centerX - width / 2).rounded(), y: baseline, width: width, height: 0))
}
/// 刘海屏的菜单栏更高,两行内容整体在按钮内垂直居中
private static func baselineOrigin(in rect: NSRect) -> CGFloat {
rect.midY - Metrics.barHeight / 2
@@ -472,8 +472,20 @@ private struct StyleCard: View {
let action: () -> Void
@State private var hovering = false
/// 两种双行风格的差别只在标签比数值宽时看得出来,预览里用风扇(标签宽、数值短)代替 GPU
private var isStacked: Bool { style == .stacked || style == .stackedCenter }
private var previewItems: [MenuBarItem] { isStacked ? [.cpu, .memory, .fan] : [.cpu, .memory, .gpu] }
private var previewReading: MenuBarReading {
guard isStacked else { return .sample }
var reading = MenuBarReading.sample
reading.fanRPM = 0
return reading
}
var body: some View {
let sample = MenuBarRenderer.image(reading: .sample, items: [.cpu, .memory, .gpu],
let sample = MenuBarRenderer.image(reading: previewReading, items: previewItems,
style: { _ in style }, networkStyle: model.settings.networkStyle,
colorizeHighLoad: false, fahrenheit: model.settings.useFahrenheit)
Button(action: action) {
@@ -184,14 +184,14 @@ public enum ProbeTarget: String, CaseIterable, Identifiable, Sendable {
/// 菜单栏风格:整体选一种套用到所有指标,个别指标可以单独指定(AppSettings.styleOverrides);风格内部的排版保持一致
public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
case stacked, inline, icon, ring, pie, history, line, meter, dot
case stacked, stackedCenter, inline, icon, ring, pie, history, line, meter, dot
public var id: String { rawValue }
/// 温度、风扇没有百分比,图形类风格画不出来,只给文字类的三种
static func options(for item: MenuBarItem) -> [MenuBarStyle] {
switch item {
case .temperature, .fan: [.stacked, .inline, .icon]
case .temperature, .fan: [.stacked, .stackedCenter, .inline, .icon]
default: allCases
}
}
@@ -199,6 +199,7 @@ public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
var title: String {
switch self {
case .stacked: tr("双行文字")
case .stackedCenter: tr("双行居中")
case .inline: tr("单行文字")
case .icon: tr("图标")
case .ring: tr("圆环")
@@ -212,7 +213,8 @@ public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
var detail: String {
switch self {
case .stacked: tr("小标签在上、数值在下,最紧凑")
case .stacked: tr("小标签在上、数值在下,两行靠右对齐,数值的右边不会跳")
case .stackedCenter: tr("同双行文字,但两行各自居中;标签比数值宽时(风扇、温度)不会偏到一边")
case .inline: tr("标签与数值同一行,最易读")
case .icon: tr("用系统图标代替文字标签")
case .ring: tr("圆环表示当前占用比例")
+10 -18
View File
@@ -62,9 +62,18 @@ system language by default; switch it in Settings → General → Language.
<!-- changelog:start -->
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
Latest release **0.6.2** (2026-09-21) · [full changelog](CHANGELOG.md) (kept in Chinese)
Latest release **0.6.2** (2026-09-21) · **1** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
<details open>
<summary><b>2026-09-21</b> · Unreleased · 1 added</summary>
**Added**
- 菜单栏多一种“双行居中”风格:两行各自居中。原来的“双行文字”是两行一起靠右对齐,标签比数值宽的时候(风扇 0、温度 54°),数值会被推到右边看着偏。居中之后数值正落在标签下面。设置 · 菜单栏里选整体风格,也可以只给某一项单独用。
</details>
<details>
<summary><b>2026-09-21</b> · 0.6.2 · 2 fixed</summary>
**Fixed**
@@ -83,23 +92,6 @@ Latest release **0.6.2** (2026-09-21) · [full changelog](CHANGELOG.md) (kept in
</details>
<details>
<summary><b>2026-09-18</b> · 0.6.0 · 5 added · 1 style</summary>
**Added**
- 新增出口与分流窗口:网络页顶栏和菜单栏网络弹窗各有一个入口。一次检测约十秒,看清 VPN 与代理有没有生效、各个网站分别从哪个出口出去。
- 结论只用一句话说清:未使用代理、代理出口(地区)、代理没有生效、网络不通四种情况。右侧同时给出经代理、直连、连不上的网站数量。
- 本机网络:物理网卡的出口 IP、归属地、运营商与 IPv6。归属地数据来自 cleanip.io。
- 网站分流:25 个常用网站分别从哪个出口出去。按 AI、影音、社交、开发、国内分类筛选;接入 Cloudflare 的网站能实测出口与边缘节点,其余按国内 / 国际规则推断,每个网站带延迟信号条。
- 代理方式一览:VPN / 隧道、系统代理、自动代理配置、DNS 与检测到的代理软件。DNS 被代理接管时会标出来。
**Style**
- 菜单栏网络弹窗的 DNS 区块重排:网络服务与线路做成两个徽章(Wi-Fi / 有线、直连或经过哪个代理),走隧道时标题旁有叹号提示 DNS 可能被接管,刷新 DNS 缓存改为图标按钮。
</details>
<!-- changelog:end -->
## Activity
+10 -18
View File
@@ -57,9 +57,18 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
<!-- changelog:start -->
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
最新版本 **0.6.2**(2026-09-21) · [完整更新日志](CHANGELOG.md)
最新版本 **0.6.2**(2026-09-21) · 开发中 **1** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
<details open>
<summary><b>2026-09-21</b> · 未发布 · 新增 1</summary>
**新增**
- 菜单栏多一种“双行居中”风格:两行各自居中。原来的“双行文字”是两行一起靠右对齐,标签比数值宽的时候(风扇 0、温度 54°),数值会被推到右边看着偏。居中之后数值正落在标签下面。设置 · 菜单栏里选整体风格,也可以只给某一项单独用。
</details>
<details>
<summary><b>2026-09-21</b> · 0.6.2 · 修复 2</summary>
**修复**
@@ -78,23 +87,6 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
</details>
<details>
<summary><b>2026-09-18</b> · 0.6.0 · 新增 5 · 样式 1</summary>
**新增**
- 新增出口与分流窗口:网络页顶栏和菜单栏网络弹窗各有一个入口。一次检测约十秒,看清 VPN 与代理有没有生效、各个网站分别从哪个出口出去。
- 结论只用一句话说清:未使用代理、代理出口(地区)、代理没有生效、网络不通四种情况。右侧同时给出经代理、直连、连不上的网站数量。
- 本机网络:物理网卡的出口 IP、归属地、运营商与 IPv6。归属地数据来自 cleanip.io。
- 网站分流:25 个常用网站分别从哪个出口出去。按 AI、影音、社交、开发、国内分类筛选;接入 Cloudflare 的网站能实测出口与边缘节点,其余按国内 / 国际规则推断,每个网站带延迟信号条。
- 代理方式一览:VPN / 隧道、系统代理、自动代理配置、DNS 与检测到的代理软件。DNS 被代理接管时会标出来。
**样式**
- 菜单栏网络弹窗的 DNS 区块重排:网络服务与线路做成两个徽章(Wi-Fi / 有线、直连或经过哪个代理),走隧道时标题旁有叹号提示 DNS 可能被接管,刷新 DNS 缓存改为图标按钮。
</details>
<!-- changelog:end -->
## 活跃度
+1 -1
View File
@@ -13,7 +13,7 @@ settings:
SWIFT_VERSION: "6.0"
MARKETING_VERSION: "0.6.2"
# 构建号四位数字,每次 make build 加一;营销版本只在发布时用 Scripts/version.sh patch / minor 修改
CURRENT_PROJECT_VERSION: "0108"
CURRENT_PROJECT_VERSION: "0111"
MACOSX_DEPLOYMENT_TARGET: "14.0"
# 本地开发使用 ad-hoc 签名;发布时改为 Developer ID(见 docs/PLAN.md)
CODE_SIGN_STYLE: Manual