主窗口改用液态玻璃(macOS 26),分段切换改为苹果标准样式,新增全局加载框
- 新增 Design/LiquidGlass.swift:dsGlass 修饰器(macOS 26 用 glassEffect,更早系统与离屏截图退回实色)、isInsideGlass 环境值避免玻璃叠玻璃、LoadingHUD 全局加载框 - 侧边栏是浮在窗口里的玻璃面板,选中项为滑动的淡蓝圆角底;顶栏“在菜单栏显示”一组收进玻璃胶囊 - DSButtonStyle:主按钮蓝色玻璃胶囊、次要按钮透明玻璃;IconButton / MiniIconButton 为圆形玻璃;ChipButton 为玻璃胶囊;DSToggle 在 macOS 26 用系统开关 - SegmentedControl 统一为苹果标准样式:灰色圆角底槽、等宽分段、选中段实心品牌蓝白字并滑动;PillSwitch 同样灰底 - 清理、卸载、导出诊断、修改 DNS 进行中显示全局加载框 - 没有用 safeAreaBar 让页面从顶栏下滚过:macOS 上交界处会画一条分隔线,柔和或隐藏滚动边缘都去不掉
This commit is contained in:
1 parent
f3c0f860cb
commit
d68fbce42a
10 files changed
+421
-160
No files matched your search
@@ -3,6 +3,19 @@
|
||||
记录 OpenStats 应用的功能新增、样式调整和问题修复,按版本与日期倒序排列。
|
||||
官网、构建与发布脚本等不影响应用本身的改动不在此记录。
|
||||
|
||||
## 未发布
|
||||
|
||||
### 2026-09-16
|
||||
|
||||
#### 样式
|
||||
|
||||
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
|
||||
- 分段切换改为苹果标准样式:灰色圆角底槽,各段等宽铺满,选中的一段是实心蓝色、白字,切换时蓝块滑过去;网络详情的 IPv4 / IPv6 切换同样是灰底蓝选中。
|
||||
|
||||
#### 新增
|
||||
|
||||
- 全局加载框:清理、卸载应用、导出诊断信息、修改 DNS 进行中,整个主窗口压暗,中间浮一块加载框写明正在做什么(macOS 26 上是液态玻璃),期间不能误点其他操作,完成后自动消失。
|
||||
|
||||
## 0.3.2 · 2026-09-16
|
||||
|
||||
### 2026-09-16
|
||||
|
||||
@@ -680,6 +680,7 @@ npm 缓存 npm Cache
|
||||
正在同步… Syncing…
|
||||
正在安装,完成后 OpenStats 会自动重启。 Installing. OpenStats will relaunch when it's done.
|
||||
正在导出… Exporting…
|
||||
正在导出诊断信息… Exporting diagnostics…
|
||||
正在扫描应用… Scanning apps…
|
||||
正在收集走势… Collecting trend…
|
||||
正在本机生成… Generating on this Mac…
|
||||
|
||||
@@ -331,8 +331,31 @@ private struct DSButtonBody: View {
|
||||
let kind: DSButtonStyle.Kind
|
||||
@State private var hovering = false
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
@Environment(\.isInsideGlass) private var isInsideGlass
|
||||
|
||||
var body: some View {
|
||||
if DS.Glass.isAvailable {
|
||||
glassBody
|
||||
} else {
|
||||
solidBody
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 26:胶囊形玻璃按钮。主按钮是蓝色玻璃,次要按钮是透明玻璃,文字按钮不加玻璃
|
||||
private var glassBody: some View {
|
||||
configuration.label
|
||||
.dsFont(.sm, weight: .medium)
|
||||
.foregroundStyle(foreground)
|
||||
.padding(.horizontal, DS.Space.s4)
|
||||
.frame(height: DS.Size.controlHeight)
|
||||
.contentShape(Capsule())
|
||||
.modifier(GlassButtonSurface(kind: kind, active: active, isInsideGlass: isInsideGlass))
|
||||
.opacity(isEnabled ? 1 : 0.5)
|
||||
.onHover { hovering = $0 }
|
||||
.animation(DS.Motion.quick, value: hovering)
|
||||
}
|
||||
|
||||
private var solidBody: some View {
|
||||
configuration.label
|
||||
.dsFont(.sm, weight: .medium)
|
||||
.foregroundStyle(foreground)
|
||||
@@ -374,15 +397,18 @@ struct IconButton: View {
|
||||
let help: String
|
||||
let action: () -> Void
|
||||
@State private var hovering = false
|
||||
@Environment(\.isInsideGlass) private var isInsideGlass
|
||||
|
||||
var body: some View {
|
||||
// macOS 26 上是圆形玻璃按钮(与系统设置的前进后退一致);已经在玻璃面板里时只有悬停底色
|
||||
let glass = DS.Glass.isAvailable && !isInsideGlass
|
||||
Button(action: action) {
|
||||
Image(systemName: systemName)
|
||||
.font(.system(size: DS.TextSize.sm.rawValue, weight: .medium))
|
||||
.foregroundStyle(hovering ? DS.Palette.primary : DS.Palette.textSecondary)
|
||||
.foregroundStyle(hovering ? DS.Palette.primary : glass ? DS.Palette.textPrimary : DS.Palette.textSecondary)
|
||||
.frame(width: DS.Size.controlHeight, height: DS.Size.controlHeight)
|
||||
.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.md))
|
||||
.contentShape(Rectangle())
|
||||
.modifier(IconButtonSurface(hovering: hovering, glass: glass))
|
||||
.contentShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hovering = $0 }
|
||||
@@ -391,6 +417,39 @@ struct IconButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 玻璃按钮的底:主按钮蓝色玻璃,次要按钮透明玻璃;文字按钮、或已在玻璃面板里的次要按钮只有悬停底色
|
||||
private struct GlassButtonSurface: ViewModifier {
|
||||
let kind: DSButtonStyle.Kind
|
||||
let active: Bool
|
||||
let isInsideGlass: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
switch kind {
|
||||
case .primary:
|
||||
content.dsGlass(in: Capsule(), tint: DS.Palette.primary, interactive: true)
|
||||
case .secondary where !isInsideGlass:
|
||||
content.dsGlass(in: Capsule(), interactive: true)
|
||||
default:
|
||||
content.background(active ? DS.Palette.surfaceHover : .clear, in: Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct IconButtonSurface: ViewModifier {
|
||||
let hovering: Bool
|
||||
let glass: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if glass {
|
||||
content.dsGlass(in: Circle(), interactive: true)
|
||||
} else if DS.Glass.isAvailable {
|
||||
content.background(hovering ? DS.Palette.surfaceHover : .clear, in: Circle())
|
||||
} else {
|
||||
content.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.md))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 侧边栏
|
||||
|
||||
/// 侧边栏的一项。选中态不填色:由容器上的 `sidebarGlider()` 画左侧滑动的蓝色光条与淡淡的高亮
|
||||
@@ -442,15 +501,32 @@ struct SidebarRowsKey: PreferenceKey {
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// 放在包含 SidebarButton 的容器上:左侧一条两端渐隐的细线,选中项位置有一段发光的蓝色光条,
|
||||
/// 切换时带回弹地滑过去,光条右侧拖出一片渐淡的蓝色高亮
|
||||
/// 放在包含 SidebarButton 的容器上,画出选中态。
|
||||
/// macOS 26:选中项垫一块淡蓝色圆角底,切换时滑过去(侧边栏本身是玻璃面板,里面不再叠玻璃);
|
||||
/// 更早的系统:左侧一条两端渐隐的细线,选中项位置有一段发光的蓝色光条,带回弹地滑过去
|
||||
@ViewBuilder
|
||||
func sidebarGlider() -> some View {
|
||||
overlayPreferenceValue(SidebarRowsKey.self) { rows in
|
||||
GeometryReader { proxy in
|
||||
SidebarGlider(frames: rows.map { proxy[$0.bounds] },
|
||||
selected: rows.first(where: \.isSelected).map { proxy[$0.bounds] })
|
||||
if DS.Glass.isAvailable {
|
||||
backgroundPreferenceValue(SidebarRowsKey.self) { rows in
|
||||
GeometryReader { proxy in
|
||||
if let selected = rows.first(where: \.isSelected).map({ proxy[$0.bounds] }) {
|
||||
RoundedRectangle(cornerRadius: DS.Radius.md, style: .continuous)
|
||||
.fill(DS.Palette.sidebarSelected)
|
||||
.frame(width: selected.width, height: selected.height)
|
||||
.offset(x: selected.minX, y: selected.minY)
|
||||
.animation(.spring(response: 0.35, dampingFraction: 0.8), value: selected)
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
} else {
|
||||
overlayPreferenceValue(SidebarRowsKey.self) { rows in
|
||||
GeometryReader { proxy in
|
||||
SidebarGlider(frames: rows.map { proxy[$0.bounds] },
|
||||
selected: rows.first(where: \.isSelected).map { proxy[$0.bounds] })
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,9 +573,14 @@ private struct SidebarGlider: View {
|
||||
|
||||
// MARK: - 分段控件
|
||||
|
||||
/// 分段切换(苹果标准样式,与系统设置一致):一条灰色圆角底槽,各段等宽铺满,
|
||||
/// 选中的一段是实心品牌蓝、白字,切换时蓝块滑过去
|
||||
struct SegmentedControl<Value: Hashable>: View {
|
||||
@Binding var selection: Value
|
||||
let options: [(value: Value, title: String)]
|
||||
@Namespace private var namespace
|
||||
|
||||
private static var radius: CGFloat { DS.Radius.md - DS.Space.s1 / 2 }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
@@ -509,16 +590,17 @@ struct SegmentedControl<Value: Hashable>: View {
|
||||
selection = option.value
|
||||
} label: {
|
||||
Text(option.title)
|
||||
.dsFont(.sm, weight: selected ? .semibold : .regular)
|
||||
.foregroundStyle(selected ? DS.Palette.textPrimary : DS.Palette.textSecondary)
|
||||
.dsFont(.sm)
|
||||
.foregroundStyle(selected ? DS.Palette.onPrimary : DS.Palette.textPrimary)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, DS.Space.s2)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: DS.Size.segmentHeight)
|
||||
.frame(height: DS.Size.segmentHeight + DS.Space.s1)
|
||||
.background {
|
||||
if selected {
|
||||
RoundedRectangle(cornerRadius: DS.Radius.md - DS.Space.s1)
|
||||
.fill(DS.Palette.elevated)
|
||||
.dsShadow(DS.Shadow.level1)
|
||||
RoundedRectangle(cornerRadius: Self.radius, style: .continuous)
|
||||
.fill(DS.Palette.primary)
|
||||
.matchedGeometryEffect(id: "selection", in: namespace)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
@@ -527,9 +609,8 @@ struct SegmentedControl<Value: Hashable>: View {
|
||||
.accessibilityAddTraits(selected ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
.padding(DS.Space.s1)
|
||||
.background(DS.Palette.track, in: RoundedRectangle(cornerRadius: DS.Radius.md))
|
||||
.animation(DS.Motion.quick, value: selection)
|
||||
.background(DS.Palette.track, in: RoundedRectangle(cornerRadius: Self.radius, style: .continuous))
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: selection)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,8 +619,22 @@ struct SegmentedControl<Value: Hashable>: View {
|
||||
struct DSToggle: View {
|
||||
@Binding var isOn: Bool
|
||||
var label: String = ""
|
||||
@Environment(\.isSnapshot) private var isSnapshot
|
||||
|
||||
var body: some View {
|
||||
// 离屏截图时窗口不在前台,系统开关会画成灰色,截图里用自绘的蓝色开关
|
||||
if DS.Glass.isAvailable, !isSnapshot {
|
||||
// macOS 26 的系统开关自带液态玻璃(拖动时滑块变成玻璃),颜色沿用品牌蓝
|
||||
Toggle(isOn: $isOn) { Text(label) }
|
||||
.labelsHidden()
|
||||
.toggleStyle(.switch)
|
||||
.tint(DS.Palette.primary)
|
||||
} else {
|
||||
solidBody
|
||||
}
|
||||
}
|
||||
|
||||
private var solidBody: some View {
|
||||
Button {
|
||||
isOn.toggle()
|
||||
} label: {
|
||||
@@ -785,6 +880,27 @@ struct ChipButton: View {
|
||||
@State private var hovering = false
|
||||
|
||||
var body: some View {
|
||||
if DS.Glass.isAvailable {
|
||||
// macOS 26:每个选项是一颗玻璃胶囊,选中的是蓝色玻璃
|
||||
Button(action: action) {
|
||||
Text(title)
|
||||
.dsFont(.xs, weight: isSelected ? .semibold : .medium)
|
||||
.foregroundStyle(isSelected ? DS.Palette.onPrimary : DS.Palette.textPrimary)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: DS.Size.segmentHeight)
|
||||
.contentShape(Capsule())
|
||||
.dsGlass(in: Capsule(), tint: isSelected ? DS.Palette.primary : nil, interactive: true)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.animation(DS.Motion.quick, value: isSelected)
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
} else {
|
||||
solidBody
|
||||
}
|
||||
}
|
||||
|
||||
private var solidBody: some View {
|
||||
Button(action: action) {
|
||||
Text(title)
|
||||
.dsFont(.xs, weight: isSelected ? .semibold : .medium)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import Localization
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - 液态玻璃
|
||||
|
||||
/// macOS 26 起,侧边栏、顶栏控件、按钮与切换控件用系统的液态玻璃;更早的系统退回原来的实色样式。
|
||||
/// 玻璃只给浮在内容之上的导航与控件用,卡片、设置分组这类内容本身保持实色(与系统设置一致)。
|
||||
extension EnvironmentValues {
|
||||
/// 已经在一块玻璃里面(比如侧边栏面板):里面的控件不再叠一层玻璃,改用普通的悬停底色
|
||||
@Entry var isInsideGlass = false
|
||||
}
|
||||
|
||||
extension DS {
|
||||
enum Glass {
|
||||
/// 当前系统是否支持液态玻璃
|
||||
static var isAvailable: Bool {
|
||||
if #available(macOS 26.0, *) { true } else { false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// 给控件加一层液态玻璃。`tint` 为品牌色时是蓝色玻璃(主按钮、选中项);
|
||||
/// `interactive` 为真时按下会有玻璃的形变反馈。不支持玻璃的系统上、以及离屏截图时(拍不出玻璃)画 `fallback` 实色
|
||||
func dsGlass<S: InsettableShape>(in shape: S, tint: Color? = nil, interactive: Bool = false,
|
||||
fallback: Color = DS.Palette.elevated) -> some View {
|
||||
modifier(GlassSurface(shape: shape, tint: tint, interactive: interactive, fallback: fallback))
|
||||
}
|
||||
}
|
||||
|
||||
private struct GlassSurface<S: InsettableShape>: ViewModifier {
|
||||
let shape: S
|
||||
let tint: Color?
|
||||
let interactive: Bool
|
||||
let fallback: Color
|
||||
@Environment(\.isSnapshot) private var isSnapshot
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(macOS 26.0, *), !isSnapshot {
|
||||
content.glassEffect(DS.Glass.style(tint: tint, interactive: interactive), in: shape)
|
||||
} else {
|
||||
content.background(tint ?? fallback, in: shape)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
extension DS.Glass {
|
||||
static func style(tint: Color?, interactive: Bool) -> SwiftUI.Glass {
|
||||
var glass = SwiftUI.Glass.regular
|
||||
if let tint { glass = glass.tint(tint) }
|
||||
if interactive { glass = glass.interactive() }
|
||||
return glass
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 全局加载
|
||||
|
||||
/// 需要等待、期间不宜继续操作的任务(清理、卸载、导出诊断、修改 DNS):
|
||||
/// 整个窗口压暗,中间浮一块玻璃加载框,写明正在做什么;任务结束自动消失
|
||||
struct LoadingHUD: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 压暗并拦住点击,避免任务进行中又触发别的操作
|
||||
Rectangle()
|
||||
.fill(DS.Palette.scrim)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {}
|
||||
|
||||
HStack(spacing: DS.Space.s3) {
|
||||
ProgressView().controlSize(.regular)
|
||||
Text(verbatim: message)
|
||||
.dsFont(.base, weight: .medium)
|
||||
.foregroundStyle(DS.Palette.textPrimary)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, DS.Space.s8)
|
||||
.padding(.vertical, DS.Space.s6)
|
||||
.frame(minWidth: DS.Size.sidebarWidth + DS.Space.s16)
|
||||
.modifier(HUDBackground())
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityAddTraits(.updatesFrequently)
|
||||
}
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
|
||||
private struct HUDBackground: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
if #available(macOS 26.0, *) {
|
||||
content.glassEffect(.regular, in: RoundedRectangle(cornerRadius: DS.Radius.xl, style: .continuous))
|
||||
} else {
|
||||
content
|
||||
.background(DS.Palette.elevated, in: RoundedRectangle(cornerRadius: DS.Radius.xl, style: .continuous))
|
||||
.dsShadow(DS.Shadow.level2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// 有文字时在上方盖一层全局加载框
|
||||
func loadingHUD(_ message: String?) -> some View {
|
||||
overlay {
|
||||
if let message { LoadingHUD(message: message) }
|
||||
}
|
||||
.animation(DS.Motion.quick, value: message)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,10 @@ extension DS {
|
||||
static let neutral500 = Color.dynamic(light: 0x6B7280, dark: 0x4B5563)
|
||||
/// 浅一档的品牌蓝,用来区分同一图表里的第二类数据(如性能核)
|
||||
static let primarySoft = Color.dynamic(light: 0x93C5FD, dark: 0x60A5FA)
|
||||
/// 侧边栏选中项的淡蓝底
|
||||
static let sidebarSelected = Color.dynamic(light: 0x2563EB, lightAlpha: 0.12, dark: 0x3B82F6, darkAlpha: 0.22)
|
||||
/// 全局加载时压暗整个窗口
|
||||
static let scrim = Color.dynamic(light: 0x000000, lightAlpha: 0.12, dark: 0x000000, darkAlpha: 0.40)
|
||||
|
||||
/// 核心类型配色:最高性能档为品牌蓝,其次浅蓝,能效核灰色
|
||||
static func cluster(_ id: Int) -> Color {
|
||||
@@ -184,6 +188,7 @@ extension DS {
|
||||
}
|
||||
|
||||
static let level1 = Level(color: .black.opacity(0.08), radius: 3, y: 1)
|
||||
static let level2 = Level(color: .black.opacity(0.10), radius: 12, y: 4)
|
||||
}
|
||||
|
||||
enum Motion {
|
||||
|
||||
@@ -3,8 +3,9 @@ import Localization
|
||||
import Metrics
|
||||
import SwiftUI
|
||||
|
||||
/// 主窗口:侧边栏、标题栏与页面在同一张底色上,不分栏着色,也不画分隔线。
|
||||
/// 左侧切换页面,右侧是仪表盘、各指标详情与工具页;窗口宽高都可调整
|
||||
/// 主窗口。左侧切换页面,右侧是仪表盘、各指标详情与工具页;窗口宽高都可调整。
|
||||
/// macOS 26 起侧边栏是一块浮在窗口里的液态玻璃面板,顶栏控件是玻璃胶囊;
|
||||
/// 更早的系统上侧边栏、标题栏与页面在同一张底色上,不分栏着色,也不画分隔线
|
||||
public struct MainWindowView: View {
|
||||
@Environment(AppModel.self) private var model
|
||||
@Environment(\.isSnapshot) private var isSnapshot
|
||||
@@ -19,55 +20,93 @@ public struct MainWindowView: View {
|
||||
.background(alignment: .top) {
|
||||
WindowDragArea().frame(height: DS.Size.windowHeader)
|
||||
}
|
||||
.modifier(SidebarPanel())
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 页面的滚动视图会自动向上延伸到标题栏下面,与顶栏重叠;顶栏必须在它之上,否则开关收不到点击
|
||||
PageHeader()
|
||||
.zIndex(1)
|
||||
Group {
|
||||
switch model.settings.panelTab {
|
||||
case .overview: OverviewPage()
|
||||
case .system: SystemInfoPage()
|
||||
case .history: HistoryPage()
|
||||
case .cpu: DetailPage { CPUPopover() }
|
||||
case .gpu: DetailPage { GPUPopover() }
|
||||
case .memory: DetailPage { MemoryPopover() }
|
||||
case .disk: DiskPage()
|
||||
case .network:
|
||||
DetailPage {
|
||||
NetworkPopover()
|
||||
NetworkSettings()
|
||||
}
|
||||
case .thermal: ThermalPage()
|
||||
case .battery: DetailPage { BatteryPopover() }
|
||||
case .processes: ProcessesPage()
|
||||
case .keepAwake: KeepAwakePage()
|
||||
case .cleaner: CleanerPage()
|
||||
case .uninstaller: UninstallerPage()
|
||||
case .startupItems: StartupItemsPage()
|
||||
case .settingsGeneral: SettingsTabPage { GeneralSettings() }
|
||||
case .settingsMenuBar: SettingsTabPage { MenuBarSettings() }
|
||||
case .settingsNotifications: SettingsTabPage { NotificationSettings() }
|
||||
case .settingsAccount: SettingsTabPage { AccountSettings() }
|
||||
case .settingsHelper: SettingsTabPage { HelperSettings() }
|
||||
case .settingsAbout: SettingsTabPage { AboutSettings() }
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
|
||||
// 截图时页面取自身高度:侧边栏更高时,多出的高度不能分给页面里可伸展的卡片
|
||||
.fixedSize(horizontal: false, vertical: isSnapshot)
|
||||
}
|
||||
.frame(minWidth: DS.Size.panelWidth, maxWidth: .infinity)
|
||||
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
|
||||
content
|
||||
.frame(minWidth: DS.Size.panelWidth, maxWidth: .infinity)
|
||||
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
|
||||
}
|
||||
.frame(width: isSnapshot ? DS.Size.sidebarWidth + DS.Size.panelWidth : nil)
|
||||
.fixedSize(horizontal: false, vertical: isSnapshot)
|
||||
.background(DS.Palette.background)
|
||||
// 需要等待的任务进行中,整个窗口压暗并显示加载框
|
||||
.loadingHUD(isSnapshot ? nil : busyMessage)
|
||||
// 内容延伸到透明标题栏下方,由顶栏高度留出红绿灯按钮的位置
|
||||
.ignoresSafeArea()
|
||||
// 文案在各视图计算时翻译好,切换语言后整棵视图重建
|
||||
.id(model.settings.language)
|
||||
}
|
||||
|
||||
/// 顶栏在上、页面在下。没有用 macOS 26 的 safeAreaBar 让页面从顶栏下面滚过:
|
||||
/// 它在 macOS 上会在交界处画一条分隔线,滚动边缘样式设成柔和或隐藏都去不掉
|
||||
private var content: some View {
|
||||
VStack(spacing: 0) {
|
||||
// 页面的滚动视图会自动向上延伸到标题栏下面,与顶栏重叠;顶栏必须在它之上,否则开关收不到点击
|
||||
PageHeader()
|
||||
.zIndex(1)
|
||||
page
|
||||
}
|
||||
}
|
||||
|
||||
/// 需要等待、期间不宜继续操作的任务
|
||||
private var busyMessage: String? {
|
||||
if model.cleaner.phase == .cleaning { return tr("正在清理…") }
|
||||
if model.uninstaller.isRemoving { return tr("正在移除…") }
|
||||
if model.diagnostics.phase == .collecting { return tr("正在导出诊断信息…") }
|
||||
if model.maintenance.isApplyingDNS { return tr("正在修改 DNS…") }
|
||||
return nil
|
||||
}
|
||||
|
||||
private var page: some View {
|
||||
Group {
|
||||
switch model.settings.panelTab {
|
||||
case .overview: OverviewPage()
|
||||
case .system: SystemInfoPage()
|
||||
case .history: HistoryPage()
|
||||
case .cpu: DetailPage { CPUPopover() }
|
||||
case .gpu: DetailPage { GPUPopover() }
|
||||
case .memory: DetailPage { MemoryPopover() }
|
||||
case .disk: DiskPage()
|
||||
case .network:
|
||||
DetailPage {
|
||||
NetworkPopover()
|
||||
NetworkSettings()
|
||||
}
|
||||
case .thermal: ThermalPage()
|
||||
case .battery: DetailPage { BatteryPopover() }
|
||||
case .processes: ProcessesPage()
|
||||
case .keepAwake: KeepAwakePage()
|
||||
case .cleaner: CleanerPage()
|
||||
case .uninstaller: UninstallerPage()
|
||||
case .startupItems: StartupItemsPage()
|
||||
case .settingsGeneral: SettingsTabPage { GeneralSettings() }
|
||||
case .settingsMenuBar: SettingsTabPage { MenuBarSettings() }
|
||||
case .settingsNotifications: SettingsTabPage { NotificationSettings() }
|
||||
case .settingsAccount: SettingsTabPage { AccountSettings() }
|
||||
case .settingsHelper: SettingsTabPage { HelperSettings() }
|
||||
case .settingsAbout: SettingsTabPage { AboutSettings() }
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
|
||||
// 截图时页面取自身高度:侧边栏更高时,多出的高度不能分给页面里可伸展的卡片
|
||||
.fixedSize(horizontal: false, vertical: isSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 26:侧边栏浮在窗口里,四周留一圈边距,整块是液态玻璃,红绿灯按钮落在面板里
|
||||
private struct SidebarPanel: ViewModifier {
|
||||
@Environment(\.isSnapshot) private var isSnapshot
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if DS.Glass.isAvailable, !isSnapshot {
|
||||
content
|
||||
.environment(\.isInsideGlass, true)
|
||||
.dsGlass(in: RoundedRectangle(cornerRadius: DS.Radius.xl, style: .continuous))
|
||||
.padding(DS.Space.s2)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 指标详情页:复用菜单栏弹窗的内容,显示全部区块
|
||||
@@ -104,11 +143,13 @@ private struct MainSidebar: View {
|
||||
}
|
||||
.padding(.leading, DS.Space.s1)
|
||||
.padding(.trailing, DS.Space.s3)
|
||||
// 顶部留出窗口红绿灯按钮的位置
|
||||
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
|
||||
// 顶部留出窗口红绿灯按钮的位置;玻璃面板本身离窗口顶边已有一圈边距
|
||||
.padding(.top, DS.Size.windowHeader + (usesGlassPanel ? 0 : DS.Space.s2))
|
||||
.padding(.bottom, DS.Space.s3)
|
||||
}
|
||||
|
||||
private var usesGlassPanel: Bool { DS.Glass.isAvailable && !isSnapshot }
|
||||
|
||||
private var navigation: some View {
|
||||
VStack(alignment: .leading, spacing: DS.Space.s1) {
|
||||
group(tr("监控"), PanelTab.monitors)
|
||||
@@ -171,15 +212,19 @@ private struct PageHeader: View {
|
||||
// 每个监控页都能在这里开关自己的菜单栏项目;温度与风扇页有两项,各带一个小标签
|
||||
let items = tab.menuBarItems
|
||||
if !items.isEmpty {
|
||||
Text(tr("在菜单栏显示")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
|
||||
ForEach(items) { item in
|
||||
if items.count > 1 {
|
||||
Text(item.popoverTitle).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
|
||||
// macOS 26 上这一组是一颗玻璃胶囊,与访达工具栏的分组一致
|
||||
HStack(spacing: DS.Space.s3) {
|
||||
Text(tr("在菜单栏显示")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
|
||||
ForEach(items) { item in
|
||||
if items.count > 1 {
|
||||
Text(item.popoverTitle).dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
|
||||
}
|
||||
DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }),
|
||||
label: tr("在菜单栏显示\(item.title)"))
|
||||
.help(tr("开启后图标出现在菜单栏;按住 ⌘ 键拖动图标可以调整位置"))
|
||||
}
|
||||
DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }),
|
||||
label: tr("在菜单栏显示\(item.title)"))
|
||||
.help(tr("开启后图标出现在菜单栏;按住 ⌘ 键拖动图标可以调整位置"))
|
||||
}
|
||||
.modifier(HeaderControlGroup())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, DS.Space.s3 + DS.Space.s1)
|
||||
@@ -187,6 +232,21 @@ private struct PageHeader: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 顶栏右侧的一组控件:macOS 26 上收进一颗玻璃胶囊,更早的系统直接排开
|
||||
private struct HeaderControlGroup: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
if DS.Glass.isAvailable {
|
||||
content
|
||||
.padding(.leading, DS.Space.s4)
|
||||
.padding(.trailing, DS.Space.s2)
|
||||
.frame(height: DS.Size.controlHeight)
|
||||
.dsGlass(in: Capsule())
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 在浅色与深色之间切换;当前跟随系统时,切到与系统相反的那一种
|
||||
private struct ThemeToggle: View {
|
||||
@Environment(AppModel.self) private var model
|
||||
|
||||
@@ -187,8 +187,8 @@ struct MiniIconButton: View {
|
||||
.font(.system(size: DS.TextSize.xs.rawValue, weight: .semibold))
|
||||
.foregroundStyle(hovering ? DS.Palette.textPrimary : DS.Palette.textSecondary)
|
||||
.frame(width: DS.Size.segmentHeight, height: DS.Size.segmentHeight)
|
||||
.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.sm))
|
||||
.contentShape(Rectangle())
|
||||
.modifier(MiniIconSurface(hovering: hovering))
|
||||
.contentShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hovering = $0 }
|
||||
@@ -197,6 +197,19 @@ struct MiniIconButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 26 上是一颗小玻璃圆钮,更早的系统只有悬停底色
|
||||
private struct MiniIconSurface: ViewModifier {
|
||||
let hovering: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if DS.Glass.isAvailable {
|
||||
content.dsGlass(in: Circle(), interactive: true)
|
||||
} else {
|
||||
content.background(hovering ? DS.Palette.surfaceHover : .clear, in: RoundedRectangle(cornerRadius: DS.Radius.sm))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 区块标题栏里的刷新按钮:查询进行中换成系统的转圈,查完自动变回图标
|
||||
struct RefreshButton: View {
|
||||
var loading: Bool
|
||||
|
||||
@@ -59,9 +59,23 @@ 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.3.2** (2026-09-16) · [full changelog](CHANGELOG.md) (kept in Chinese)
|
||||
Latest release **0.3.2** (2026-09-16) · **3** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
|
||||
|
||||
<details open>
|
||||
<summary><b>2026-09-16</b> · Unreleased · 2 style · 1 added</summary>
|
||||
|
||||
**Style**
|
||||
|
||||
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
|
||||
- 分段切换改为苹果标准样式:灰色圆角底槽,各段等宽铺满,选中的一段是实心蓝色、白字,切换时蓝块滑过去;网络详情的 IPv4 / IPv6 切换同样是灰底蓝选中。
|
||||
|
||||
**Added**
|
||||
|
||||
- 全局加载框:清理、卸载应用、导出诊断信息、修改 DNS 进行中,整个主窗口压暗,中间浮一块加载框写明正在做什么(macOS 26 上是液态玻璃),期间不能误点其他操作,完成后自动消失。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>2026-09-16</b> · 0.3.2 · 2 fixed</summary>
|
||||
|
||||
**Fixed**
|
||||
@@ -81,49 +95,6 @@ Latest release **0.3.2** (2026-09-16) · [full changelog](CHANGELOG.md) (kept in
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>2026-09-15</b> · 0.3.0 · 8 added · 14 changed · 3 fixed</summary>
|
||||
|
||||
**Added**
|
||||
|
||||
- 菜单栏新增“电池”项目:电量按九种风格显示(电量条、圆环、饼图、数字等),充电中带闪电,电量低于 20% 按“高负载时着色”变红。点击弹出电池详情:电量与剩余 / 充满时间、适配器功率、电池温度、最近 24 小时电量曲线、功耗、健康度与循环次数、已连接蓝牙设备的电量(AirPods 左右耳与充电盒、妙控键盘 / 鼠标 / 触控板),可跳到系统电池设置;主窗口也有对应的“电池”页,历史页多了电池电量曲线。没有电池的 Mac(mini、Studio、iMac)上这个项目改为显示电量最低的蓝牙设备,弹窗只列蓝牙设备。「设置 · 菜单栏」新增“蓝牙设备电量低时提示”:某个设备低于 20% 时在电池项目旁显示它的图标与电量。
|
||||
- 网络详情“接口”区块右上角新增重置按钮:把开机后下载 / 上传归零、从现在起重新累计,标题旁标出起算时间,两行改叫“重置后下载 / 上传”;重启后自动回到开机后累计,右键按钮可随时改回。
|
||||
- 磁盘页从展示页变成能动手的磁盘工具,新增四张卡片:
|
||||
- 空间占用:统计家目录里每个文件夹占多少,并列出最大的 20 个文件(应用、照片图库等按整体算一个);可在访达中显示,家目录里、Library 之外的文件可以直接移到废纸篓。只读扫描,文件多时需要几十秒,可随时停止。
|
||||
- 文件系统检查:调用系统的 diskutil 对启动盘做一次只读检查,相当于“磁盘工具”的急救但不修改任何东西;记住上次检查的时间与结果,发现问题时提示去磁盘工具修复。
|
||||
- 本地快照:列出 Time Machine 留在本机的快照(它们计入“可清除”空间),可一键全部删除;辅助工具够新时由它执行,否则请求一次管理员授权。辅助工具协议升到第 4 版,已安装的旧版会提示重新安装。
|
||||
- 其他磁盘:列出外置硬盘、U 盘、镜像与网络共享的容量,可在访达中显示或直接推出;接入、拔出时自动刷新。
|
||||
- 网络详情新增「IP 纯净度」区块:CleanIP.io 的纯净度评分与等级画成占满整行的 F 到 A+ 六段色带,分数在色带上方、得分处有标记,同一行右侧是 cleanip.io 字标,点了打开这个 IP 的完整报告;下面列出风险评分、命中的风险标记(VPN、代理、Tor、机房、滥用记录等)与一句评价。IP 地址区块用 cleanip.io 样式的徽章标出原生 / 广播、住宅 / 机房与运营商类型(ISP 等),徽章样式作为通用组件收进设计系统。
|
||||
- 账号与设置同步(设置 · 账号与同步):用 GitHub、Google 或 Apple 登录后,菜单栏项目与风格、刷新频率、外观与语言、详情弹窗的区块、连接探测、通知、快捷键、风扇安全温度、合盖电量下限会保存到 getopenstats.com,换一台 Mac 登录即自动恢复。设置改动 2 秒后上传,启动、唤醒与每 15 分钟检查一次云端;第一次登录时本机与云端都有内容且不同,会让你选用哪一份,之后以最后写入为准。登录走系统的授权窗口,令牌存在钥匙串,云端只保存邮箱、姓名与设置文档,可随时退出或删除云端数据。不登录时应用行为不变。
|
||||
- CPU 走势时长可选 1 / 3 / 5 分钟:走势线上方一行小字切换,弹窗与主窗口共用;曲线按真实采样时间定位,最新一次采样固定在右边缘,睡眠等超过 10 秒没有采样的地方线条断开。
|
||||
- 菜单栏新增“磁盘”项目:显示启动磁盘已用占比,八种风格都可用;点击弹出磁盘详情(容量与可用空间、读写速度、SSD 健康、读写最多的应用),区块可在设置里逐个隐藏。主窗口磁盘页右上角同样可以直接开关,“温度与风扇”页右上角现在有温度、风扇两个开关。
|
||||
- CPU 详情新增“各核心占用”区块:主窗口里每个核心一个小圆环,中间写百分比,按“核心 1…N”编号并按类型分组,超过 60% 变橙、85% 变红;弹窗里是每核一根柱子的紧凑版,默认关闭,可在“设置 · 菜单栏”的 CPU 弹窗显示里打开。
|
||||
|
||||
**Changed**
|
||||
|
||||
- 公网 IP 的归属地改用 cleanip.io:去掉本地 MaxMind GeoLite2 数据库(含下载、导入、自动更新的整个“IP 归属地数据库”设置区块);归属地、ASN、网络类型与纯净度都由这台 Mac 直接向 cleanip.io 查询、不经过我们的服务器,只发送公网地址。
|
||||
- 「设置 · 菜单栏」每个显示项目下面多了一行“菜单栏风格”:给这一项单独选一种风格(默认跟随整体,菜单里标出整体现在是哪种),旁边是这一项按当前风格、实时读数画出的预览,改了立刻能看到。以前这个选择藏在开关旁一个没有说明的下拉里,不容易发现。温度、风扇只提供文字类的三种风格;原“风格”分组改名“整体风格”并加了说明。
|
||||
- 菜单栏弹窗右上角的两个按钮:左边改为该指标自己的图标(网络是网络图标、磁盘是硬盘),点了直接进主窗口里这个指标的页面;右边的齿轮进总设置。
|
||||
- 公网 IP 双栈支持:IPv4 与 IPv6 各自查询归属地与纯净度、各自缓存;两族都有时 IP 地址区块用胶囊开关切换 IPv4 / IPv6。纯净度区块的标题一行排开:地址族徽章(只有一族时不标)、置信度徽章(高绿、中灰、低黄)、完整报告链接,右侧是刷新按钮;IP 地址区块也有同样的刷新按钮,都是忽略缓存立即重查,查询进行中按钮变成系统的转圈。
|
||||
- 公网 IP 的归属地结果保存在本机:点开网络详情立刻显示上次的结果,后台只向 Cloudflare 核对一下地址;地址没变且不满 7 天就不再查归属地,换了 IP、超过 7 天或点了刷新才重新查。
|
||||
- 磁盘页与磁盘弹窗的“读取 / 写入”两组数值各占一半宽度,数字长短变化时位置不再左右挪动。
|
||||
- 磁盘“读写最多的应用”与网络详情“高占用进程”改为稳定的 5 条榜单:按最近十几秒的平均速率排序和画条,数字仍是当前速率;刚安静下来的应用会在榜上停留几秒再退出,不再随每秒的波动忽隐忽现、上下乱跳。
|
||||
- 磁盘页与磁盘弹窗的容量改为分段条:已用、可清除、可用三段按比例排在一条里,段内直接标名称与百分比,下方图例给出各自的容量;可清除是系统随时可以腾出的缓存,访达的“可用”把它算在内。
|
||||
- 磁盘页“读写最多的应用”与 CPU、内存详情“按应用汇总”的排行条不再画灰色底槽,只保留代表相对占比的蓝色条。
|
||||
- 「设置 · 菜单栏」的显示项目上方加了一条说明:按住 ⌘ 拖动可以调整菜单栏图标的顺序,新开启的项目由系统安排位置;各页面右上角的开关悬停时也有同样的提示。官网常见问题同步补充。
|
||||
- 网络详情的 DNS 切换改为下拉菜单:“配置方式”一行直接是菜单,选 Cloudflare、Google 等预设立即切换,选“手动”展开输入框,自定义地址时旁边有铅笔按钮可再次修改;不再显示六个并排的按钮,省下两行。
|
||||
- 主窗口的 CPU 走势图加上 0–100% 刻度与起点、中点、现在三个时间标签。
|
||||
- CPU 详情的解释性文字不再常驻:热力图怎么看、每类核心是干什么的、平均负载怎么理解、按应用汇总为什么会超过 100%,都收进区块标题旁的 ⓘ,鼠标悬停 ⓘ 或卡片内容时才显示,界面上只留数据。
|
||||
- 核心类型说人话:核心分工的悬停说明写明每类核心是干什么的(最快的处理重活、更省电的负责后台和轻量任务,系统自动分配);各核心占用的分组标题、热力图的行标签都带上核心数量,热力图说明写明“每行一个核心(共 N 个),颜色越深越忙”;最忙的核心改为“核心 7(性能核)”这种统一编号。
|
||||
|
||||
**Fixed**
|
||||
|
||||
- 菜单栏弹窗的内容整体偏左、右边留白更宽:接了鼠标时系统默认常驻滚动条,滚动区域给它预留了一条宽度。现在应用内一律用浮层滚动条,内容占满整个弹窗宽度,主窗口页面同样处理。
|
||||
- 网络详情里的归属地国旗画错:中国、乌兹别克斯坦等 63 面旗子的 SVG 用了嵌套引用,系统渲染器画成一大块白。现在国旗改为预先渲染好的 PNG,全部按参考图核对过。
|
||||
- 各监控页右上角的“在菜单栏显示”开关点不动:页面的滚动区域会自动向上延伸到顶栏底下,把开关的点击截走了;现在顶栏盖在滚动区域之上,GPU、磁盘等页面的开关可以正常点击。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- changelog:end -->
|
||||
|
||||
## Activity
|
||||
|
||||
+15
-44
@@ -55,9 +55,23 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
|
||||
<!-- changelog:start -->
|
||||
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
|
||||
|
||||
最新版本 **0.3.2**(2026-09-16) · [完整更新日志](CHANGELOG.md)
|
||||
最新版本 **0.3.2**(2026-09-16) · 开发中 **3** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
|
||||
|
||||
<details open>
|
||||
<summary><b>2026-09-16</b> · 未发布 · 样式 2 · 新增 1</summary>
|
||||
|
||||
**样式**
|
||||
|
||||
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
|
||||
- 分段切换改为苹果标准样式:灰色圆角底槽,各段等宽铺满,选中的一段是实心蓝色、白字,切换时蓝块滑过去;网络详情的 IPv4 / IPv6 切换同样是灰底蓝选中。
|
||||
|
||||
**新增**
|
||||
|
||||
- 全局加载框:清理、卸载应用、导出诊断信息、修改 DNS 进行中,整个主窗口压暗,中间浮一块加载框写明正在做什么(macOS 26 上是液态玻璃),期间不能误点其他操作,完成后自动消失。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>2026-09-16</b> · 0.3.2 · 修复 2</summary>
|
||||
|
||||
**修复**
|
||||
@@ -77,49 +91,6 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>2026-09-15</b> · 0.3.0 · 新增 8 · 调整 14 · 修复 3</summary>
|
||||
|
||||
**新增**
|
||||
|
||||
- 菜单栏新增“电池”项目:电量按九种风格显示(电量条、圆环、饼图、数字等),充电中带闪电,电量低于 20% 按“高负载时着色”变红。点击弹出电池详情:电量与剩余 / 充满时间、适配器功率、电池温度、最近 24 小时电量曲线、功耗、健康度与循环次数、已连接蓝牙设备的电量(AirPods 左右耳与充电盒、妙控键盘 / 鼠标 / 触控板),可跳到系统电池设置;主窗口也有对应的“电池”页,历史页多了电池电量曲线。没有电池的 Mac(mini、Studio、iMac)上这个项目改为显示电量最低的蓝牙设备,弹窗只列蓝牙设备。「设置 · 菜单栏」新增“蓝牙设备电量低时提示”:某个设备低于 20% 时在电池项目旁显示它的图标与电量。
|
||||
- 网络详情“接口”区块右上角新增重置按钮:把开机后下载 / 上传归零、从现在起重新累计,标题旁标出起算时间,两行改叫“重置后下载 / 上传”;重启后自动回到开机后累计,右键按钮可随时改回。
|
||||
- 磁盘页从展示页变成能动手的磁盘工具,新增四张卡片:
|
||||
- 空间占用:统计家目录里每个文件夹占多少,并列出最大的 20 个文件(应用、照片图库等按整体算一个);可在访达中显示,家目录里、Library 之外的文件可以直接移到废纸篓。只读扫描,文件多时需要几十秒,可随时停止。
|
||||
- 文件系统检查:调用系统的 diskutil 对启动盘做一次只读检查,相当于“磁盘工具”的急救但不修改任何东西;记住上次检查的时间与结果,发现问题时提示去磁盘工具修复。
|
||||
- 本地快照:列出 Time Machine 留在本机的快照(它们计入“可清除”空间),可一键全部删除;辅助工具够新时由它执行,否则请求一次管理员授权。辅助工具协议升到第 4 版,已安装的旧版会提示重新安装。
|
||||
- 其他磁盘:列出外置硬盘、U 盘、镜像与网络共享的容量,可在访达中显示或直接推出;接入、拔出时自动刷新。
|
||||
- 网络详情新增「IP 纯净度」区块:CleanIP.io 的纯净度评分与等级画成占满整行的 F 到 A+ 六段色带,分数在色带上方、得分处有标记,同一行右侧是 cleanip.io 字标,点了打开这个 IP 的完整报告;下面列出风险评分、命中的风险标记(VPN、代理、Tor、机房、滥用记录等)与一句评价。IP 地址区块用 cleanip.io 样式的徽章标出原生 / 广播、住宅 / 机房与运营商类型(ISP 等),徽章样式作为通用组件收进设计系统。
|
||||
- 账号与设置同步(设置 · 账号与同步):用 GitHub、Google 或 Apple 登录后,菜单栏项目与风格、刷新频率、外观与语言、详情弹窗的区块、连接探测、通知、快捷键、风扇安全温度、合盖电量下限会保存到 getopenstats.com,换一台 Mac 登录即自动恢复。设置改动 2 秒后上传,启动、唤醒与每 15 分钟检查一次云端;第一次登录时本机与云端都有内容且不同,会让你选用哪一份,之后以最后写入为准。登录走系统的授权窗口,令牌存在钥匙串,云端只保存邮箱、姓名与设置文档,可随时退出或删除云端数据。不登录时应用行为不变。
|
||||
- CPU 走势时长可选 1 / 3 / 5 分钟:走势线上方一行小字切换,弹窗与主窗口共用;曲线按真实采样时间定位,最新一次采样固定在右边缘,睡眠等超过 10 秒没有采样的地方线条断开。
|
||||
- 菜单栏新增“磁盘”项目:显示启动磁盘已用占比,八种风格都可用;点击弹出磁盘详情(容量与可用空间、读写速度、SSD 健康、读写最多的应用),区块可在设置里逐个隐藏。主窗口磁盘页右上角同样可以直接开关,“温度与风扇”页右上角现在有温度、风扇两个开关。
|
||||
- CPU 详情新增“各核心占用”区块:主窗口里每个核心一个小圆环,中间写百分比,按“核心 1…N”编号并按类型分组,超过 60% 变橙、85% 变红;弹窗里是每核一根柱子的紧凑版,默认关闭,可在“设置 · 菜单栏”的 CPU 弹窗显示里打开。
|
||||
|
||||
**调整**
|
||||
|
||||
- 公网 IP 的归属地改用 cleanip.io:去掉本地 MaxMind GeoLite2 数据库(含下载、导入、自动更新的整个“IP 归属地数据库”设置区块);归属地、ASN、网络类型与纯净度都由这台 Mac 直接向 cleanip.io 查询、不经过我们的服务器,只发送公网地址。
|
||||
- 「设置 · 菜单栏」每个显示项目下面多了一行“菜单栏风格”:给这一项单独选一种风格(默认跟随整体,菜单里标出整体现在是哪种),旁边是这一项按当前风格、实时读数画出的预览,改了立刻能看到。以前这个选择藏在开关旁一个没有说明的下拉里,不容易发现。温度、风扇只提供文字类的三种风格;原“风格”分组改名“整体风格”并加了说明。
|
||||
- 菜单栏弹窗右上角的两个按钮:左边改为该指标自己的图标(网络是网络图标、磁盘是硬盘),点了直接进主窗口里这个指标的页面;右边的齿轮进总设置。
|
||||
- 公网 IP 双栈支持:IPv4 与 IPv6 各自查询归属地与纯净度、各自缓存;两族都有时 IP 地址区块用胶囊开关切换 IPv4 / IPv6。纯净度区块的标题一行排开:地址族徽章(只有一族时不标)、置信度徽章(高绿、中灰、低黄)、完整报告链接,右侧是刷新按钮;IP 地址区块也有同样的刷新按钮,都是忽略缓存立即重查,查询进行中按钮变成系统的转圈。
|
||||
- 公网 IP 的归属地结果保存在本机:点开网络详情立刻显示上次的结果,后台只向 Cloudflare 核对一下地址;地址没变且不满 7 天就不再查归属地,换了 IP、超过 7 天或点了刷新才重新查。
|
||||
- 磁盘页与磁盘弹窗的“读取 / 写入”两组数值各占一半宽度,数字长短变化时位置不再左右挪动。
|
||||
- 磁盘“读写最多的应用”与网络详情“高占用进程”改为稳定的 5 条榜单:按最近十几秒的平均速率排序和画条,数字仍是当前速率;刚安静下来的应用会在榜上停留几秒再退出,不再随每秒的波动忽隐忽现、上下乱跳。
|
||||
- 磁盘页与磁盘弹窗的容量改为分段条:已用、可清除、可用三段按比例排在一条里,段内直接标名称与百分比,下方图例给出各自的容量;可清除是系统随时可以腾出的缓存,访达的“可用”把它算在内。
|
||||
- 磁盘页“读写最多的应用”与 CPU、内存详情“按应用汇总”的排行条不再画灰色底槽,只保留代表相对占比的蓝色条。
|
||||
- 「设置 · 菜单栏」的显示项目上方加了一条说明:按住 ⌘ 拖动可以调整菜单栏图标的顺序,新开启的项目由系统安排位置;各页面右上角的开关悬停时也有同样的提示。官网常见问题同步补充。
|
||||
- 网络详情的 DNS 切换改为下拉菜单:“配置方式”一行直接是菜单,选 Cloudflare、Google 等预设立即切换,选“手动”展开输入框,自定义地址时旁边有铅笔按钮可再次修改;不再显示六个并排的按钮,省下两行。
|
||||
- 主窗口的 CPU 走势图加上 0–100% 刻度与起点、中点、现在三个时间标签。
|
||||
- CPU 详情的解释性文字不再常驻:热力图怎么看、每类核心是干什么的、平均负载怎么理解、按应用汇总为什么会超过 100%,都收进区块标题旁的 ⓘ,鼠标悬停 ⓘ 或卡片内容时才显示,界面上只留数据。
|
||||
- 核心类型说人话:核心分工的悬停说明写明每类核心是干什么的(最快的处理重活、更省电的负责后台和轻量任务,系统自动分配);各核心占用的分组标题、热力图的行标签都带上核心数量,热力图说明写明“每行一个核心(共 N 个),颜色越深越忙”;最忙的核心改为“核心 7(性能核)”这种统一编号。
|
||||
|
||||
**修复**
|
||||
|
||||
- 菜单栏弹窗的内容整体偏左、右边留白更宽:接了鼠标时系统默认常驻滚动条,滚动区域给它预留了一条宽度。现在应用内一律用浮层滚动条,内容占满整个弹窗宽度,主窗口页面同样处理。
|
||||
- 网络详情里的归属地国旗画错:中国、乌兹别克斯坦等 63 面旗子的 SVG 用了嵌套引用,系统渲染器画成一大块白。现在国旗改为预先渲染好的 PNG,全部按参考图核对过。
|
||||
- 各监控页右上角的“在菜单栏显示”开关点不动:页面的滚动区域会自动向上延伸到顶栏底下,把开关的点击截走了;现在顶栏盖在滚动区域之上,GPU、磁盘等页面的开关可以正常点击。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- changelog:end -->
|
||||
|
||||
## 活跃度
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ settings:
|
||||
SWIFT_VERSION: "6.0"
|
||||
MARKETING_VERSION: "0.3.2"
|
||||
# 构建号四位数字,每次 make build 加一;营销版本只在发布时用 Scripts/version.sh patch / minor 修改
|
||||
CURRENT_PROJECT_VERSION: "0055"
|
||||
CURRENT_PROJECT_VERSION: "0067"
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
# 本地开发使用 ad-hoc 签名;发布时改为 Developer ID(见 docs/PLAN.md)
|
||||
CODE_SIGN_STYLE: Manual
|
||||
|
||||
Reference in new issue
Block a user