设置并入主窗口:侧边栏新增设置分组,网络、风扇、合盖设置放到对应页面

- 侧边栏“设置”分组:通用、菜单栏、辅助工具、关于;导航较多时侧边栏可滚动
- 网络设置(连接探测、公网 IP、IP 归属地数据库)放在网络页底部,风扇安全温度放在温度与风扇页,合盖电量下限放在防休眠页
- 弹窗齿轮、菜单栏右键“设置…”与 ⌘, 打开主窗口的通用设置;删除独立设置窗口,偏好设置不变
- 去掉不再使用的设置窗口尺寸 token,侧边栏宽度改名为 sidebarWidth;截图渲染改为按页面输出
This commit is contained in:
gentpan committed 2026-09-13 07:04:14 +05:00
1 parent 0829f425cb
commit 23e4a8b8e0
15 files changed
+135 -211

No files matched your search

+5 -4
View File
@@ -36,17 +36,18 @@
- 清理的安全边界:只清理白名单目录,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过,执行前逐项重新校验。
- 系统维护:一键刷新 DNS 缓存、释放内存,已安装辅助工具时直接执行,否则请求一次管理员授权。
- 进程页按 CPU 或内存排序,辅助进程归并显示为所属应用。
- 设置窗口:外观、登录时启动、刷新频率、温度单位、菜单栏布局 / 显示项 / 样式 / 弹窗内容、连接探测与公网 IP 查询、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 统一外观:浅色为白底蓝色、深色为黑底蓝色,主窗口、设置窗口与弹窗共用同一底色;侧边栏、标题栏与页面不分栏着色,红绿灯按钮与页面标题对齐;卡片为浅灰 / 深灰实色。侧边栏的太阳 / 月亮按钮一键切换,也可以跟随系统。
- 设置(主窗口侧边栏的“设置”分组):外观、登录时启动、刷新频率、温度单位、菜单栏布局 / 显示项 / 样式 / 弹窗内容、连接探测与公网 IP 查询、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 统一外观:浅色为白底蓝色、深色为黑底蓝色,主窗口与弹窗共用同一底色;侧边栏、标题栏与页面不分栏着色,红绿灯按钮与页面标题对齐;卡片为浅灰 / 深灰实色。侧边栏的太阳 / 月亮按钮一键切换,也可以跟随系统。
- 截图命令 `OpenStats --snapshot <目录>`:用本机实时数据渲染主窗口各页、详情弹窗、设置和菜单栏的浅色与深色截图,IP 与硬件地址替换为示例值。
#### 样式
- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
- 设置并入主窗口:侧边栏新增“设置”分组(通用、菜单栏、辅助工具、关于);网络设置(连接探测、公网 IP、IP 归属地数据库)放到“网络”页底部,风扇安全温度放到“温度与风扇”页底部,合盖运行电量下限放到“防休眠”页底部;弹窗齿轮、菜单栏右键“设置…”与 ⌘, 都打开主窗口的通用设置;不再有单独的设置窗口,已有偏好全部保留;导航较多时侧边栏可滚动。
- 主窗口侧边栏的选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 主窗口默认 1072×720,可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。
- 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。
@@ -3,11 +3,10 @@ import Metrics
import SwiftUI
@MainActor
public final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate {
public final class AppController: NSObject, NSApplicationDelegate {
private let model = AppModel()
private var menuBar: MenuBarController!
private var mainWindow: MainWindowController!
private var settingsWindow: NSWindow?
private var workspaceObservers: [NSObjectProtocol] = []
public override init() {
@@ -15,13 +14,17 @@ public final class AppController: NSObject, NSApplicationDelegate, NSWindowDeleg
}
public func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.mainMenu = MainMenu.make()
NSApp.mainMenu = MainMenu.make(settingsTarget: self, settingsAction: #selector(openSettingsFromMenu))
menuBar = MenuBarController(model: model)
mainWindow = MainWindowController(model: model)
mainWindow.onVisibilityChange = { [weak self] _ in self?.updateActivationPolicy() }
menuBar.update()
model.openSettings = { [weak self] in self?.showSettings() }
// 设置已并入主窗口:所有“设置…”入口都打开主窗口的通用设置
model.openSettings = { [weak self] in
self?.menuBar.dismissPopovers()
self?.mainWindow.show(tab: .settingsGeneral)
}
model.openMainWindow = { [weak self] tab in
self?.menuBar.dismissPopovers()
self?.mainWindow.show(tab: tab)
@@ -88,40 +91,11 @@ public final class AppController: NSObject, NSApplicationDelegate, NSWindowDeleg
return true
}
// MARK: 设置窗口
private func showSettings() {
menuBar.dismissPopovers()
if settingsWindow == nil {
let visible = NSScreen.main?.visibleFrame.size ?? DS.Size.settingsDefaultSize
let window = NSWindow(contentRect: NSRect(x: 0, y: 0,
width: min(DS.Size.settingsDefaultSize.width, visible.width - DS.Space.s12),
height: min(DS.Size.settingsDefaultSize.height, visible.height - DS.Space.s12)),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false)
window.title = "OpenStats 设置"
WindowChrome.apply(to: window)
window.isReleasedWhenClosed = false
window.delegate = self
window.contentView = NSHostingView(rootView: SettingsView().environment(model))
window.contentMinSize = NSSize(width: DS.Size.settingsWidth, height: DS.Size.settingsHeight)
window.setFrameAutosaveName("OpenStatsSettingsWindow")
if window.frame.width < DS.Size.settingsDefaultSize.width { window.center() }
settingsWindow = window
}
NSApp.activate()
settingsWindow?.makeKeyAndOrderFront(nil)
updateActivationPolicy()
}
public func windowWillClose(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in self?.updateActivationPolicy() }
}
@objc private func openSettingsFromMenu() { model.openSettings() }
/// 有窗口打开时显示在程序坞与 ⌘Tab 里,全部关闭后回到仅菜单栏
private func updateActivationPolicy() {
let hasWindow = mainWindow.isVisible || settingsWindow?.isVisible == true
let hasWindow = mainWindow.isVisible
let policy: NSApplication.ActivationPolicy = hasWindow ? .regular : .accessory
guard NSApp.activationPolicy() != policy else { return }
NSApp.setActivationPolicy(policy)
@@ -3,12 +3,15 @@ import AppKit
/// 应用菜单。仅菜单栏运行时不显示,但仍负责 ⌘C / ⌘V / ⌘W 等快捷键
@MainActor
enum MainMenu {
static func make() -> NSMenu {
static func make(settingsTarget: AnyObject, settingsAction: Selector) -> NSMenu {
let main = NSMenu()
let app = NSMenu(title: "OpenStats")
app.addItem(withTitle: "关于 OpenStats", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
app.addItem(.separator())
let settings = app.addItem(withTitle: "设置…", action: settingsAction, keyEquivalent: ",")
settings.target = settingsTarget
app.addItem(.separator())
app.addItem(withTitle: "隐藏 OpenStats", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
app.addItem(.separator())
app.addItem(withTitle: "退出 OpenStats", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
@@ -29,8 +29,8 @@ final class MainWindowController: NSObject, NSWindowDelegate {
}
private func makeWindow() -> NSWindow {
let width = DS.Size.settingsSidebar + DS.Size.panelWidth
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: DS.Size.settingsSidebar + DS.Size.windowDefaultContentWidth,
let width = DS.Size.sidebarWidth + DS.Size.panelWidth
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: DS.Size.sidebarWidth + DS.Size.windowDefaultContentWidth,
height: DS.Size.panelMinHeight * 2),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
@@ -56,7 +56,7 @@ final class MainWindowController: NSObject, NSWindowDelegate {
let height = min(measuring.fittingSize.height, visible.height - DS.Space.s12)
// 至少 720pt 高(屏幕放得下时),页面较短也不会开成一个小窗口
let size = NSSize(width: min(window.frame.width, visible.width - DS.Space.s12),
height: min(max(height, DS.Size.settingsDefaultSize.height), visible.height - DS.Space.s12))
height: min(max(height, DS.Size.windowDefaultHeight), visible.height - DS.Space.s12))
return NSRect(x: visible.midX - size.width / 2, y: visible.midY - size.height / 2,
width: size.width, height: size.height)
}
@@ -130,15 +130,11 @@ extension DS {
static let panelGap: CGFloat = 4
/// 窗口顶栏高度,与紧凑工具栏的标题栏同高,红绿灯按钮与页面标题在同一水平线上
static let windowHeader: CGFloat = 40
/// 窄控件的固定宽度(分段控件等)
static let tileWidth: CGFloat = (panelWidth - Space.s3 * 4) / 3
/// 设置窗口的最小尺寸;默认打开时更大,可自由调整
static let settingsWidth: CGFloat = 720
static let settingsHeight: CGFloat = 520
static let settingsDefaultSize = CGSize(width: 920, height: 720)
/// 主窗口首次打开的最小高度(屏幕放得下时)
static let windowDefaultHeight: CGFloat = 720
/// 主窗口默认内容区宽度(侧边栏另计)
static let windowDefaultContentWidth: CGFloat = 880
static let settingsSidebar: CGFloat = 192
static let sidebarWidth: CGFloat = 192
static let valueColumn: CGFloat = 64
static let labelColumn: CGFloat = 48
}
@@ -16,6 +16,7 @@ struct KeepAwakePage: View {
LidCard().frame(maxWidth: .infinity)
}
.fixedSize(horizontal: false, vertical: true)
LidBatterySettings()
}
}
}
@@ -11,9 +11,9 @@ public struct MainWindowView: View {
public init() {}
public var body: some View {
HStack(spacing: 0) {
HStack(alignment: .top, spacing: 0) {
MainSidebar()
.frame(width: DS.Size.settingsSidebar)
.frame(width: DS.Size.sidebarWidth)
.frame(maxHeight: .infinity)
.background(alignment: .top) {
WindowDragArea().frame(height: DS.Size.windowHeader)
@@ -28,11 +28,19 @@ public struct MainWindowView: View {
case .cpu: DetailPage { CPUPopover() }
case .gpu: DetailPage { GPUPopover() }
case .memory: DetailPage { MemoryPopover() }
case .network: DetailPage { NetworkPopover() }
case .network:
DetailPage {
NetworkPopover()
NetworkSettings()
}
case .thermal: ThermalPage()
case .processes: ProcessesPage()
case .keepAwake: KeepAwakePage()
case .cleaner: CleanerPage()
case .settingsGeneral: SettingsTabPage { GeneralSettings() }
case .settingsMenuBar: SettingsTabPage { MenuBarSettings() }
case .settingsHelper: SettingsTabPage { HelperSettings() }
case .settingsAbout: SettingsTabPage { AboutSettings() }
}
}
.frame(maxWidth: .infinity, maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
@@ -42,7 +50,7 @@ public struct MainWindowView: View {
.frame(minWidth: DS.Size.panelWidth, maxWidth: .infinity)
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
}
.frame(width: isSnapshot ? DS.Size.settingsSidebar + DS.Size.panelWidth : nil)
.frame(width: isSnapshot ? DS.Size.sidebarWidth + DS.Size.panelWidth : nil)
.fixedSize(horizontal: false, vertical: isSnapshot)
.background(DS.Palette.background)
// 内容延伸到透明标题栏下方,由顶栏高度留出红绿灯按钮的位置
@@ -63,27 +71,44 @@ private struct DetailPage<Content: View>: View {
private struct MainSidebar: View {
@Environment(AppModel.self) private var model
var body: some View {
VStack(alignment: .leading, spacing: DS.Space.s1) {
group("监控", PanelTab.monitors)
group("工具", PanelTab.tools)
@Environment(\.isSnapshot) private var isSnapshot
Spacer(minLength: DS.Space.s3)
var body: some View {
VStack(alignment: .leading, spacing: DS.Space.s3) {
// 三组导航加起来较高,窗口矮时侧边栏自己滚动,底部按钮始终可见
if isSnapshot {
navigation
} else {
ScrollView { navigation.overlayScrollers() }
.scrollBounceBehavior(.basedOnSize)
}
HStack(spacing: DS.Space.s1) {
ThemeToggle()
IconButton(systemName: "gearshape", help: "设置") { model.openSettings() }
Spacer(minLength: 0)
IconButton(systemName: "power", help: "退出 OpenStats") { model.quit() }
}
.padding(.leading, DS.Space.s2)
}
.sidebarGlider()
.padding(.horizontal, DS.Space.s3)
.padding(.leading, DS.Space.s1)
.padding(.trailing, DS.Space.s3)
// 顶部留出窗口红绿灯按钮的位置
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
.padding(.bottom, DS.Space.s3)
}
private var navigation: some View {
VStack(alignment: .leading, spacing: DS.Space.s1) {
group("监控", PanelTab.monitors)
group("工具", PanelTab.tools)
group("设置", PanelTab.settings)
}
.sidebarGlider()
// 光条的发光向左溢出几个点,留出空间避免被滚动区域裁掉
.padding(.leading, DS.Space.s2)
.frame(maxWidth: .infinity, alignment: .leading)
}
@ViewBuilder
private func group(_ title: String, _ tabs: [PanelTab]) -> some View {
Text(title)
@@ -107,7 +132,7 @@ private struct PageHeader: View {
let tab = settings.panelTab
HStack(spacing: DS.Space.s3) {
Text(tab.title)
Text(tab.headerTitle)
.dsFont(.base, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
Spacer(minLength: DS.Space.s3)
@@ -118,14 +118,14 @@ struct ProcessesPage: View {
}
.padding(.horizontal, DS.Space.s2)
.frame(height: DS.Size.controlHeight)
.frame(maxWidth: DS.Size.settingsSidebar + DS.Space.s16)
.frame(maxWidth: DS.Size.sidebarWidth + DS.Space.s16)
.background(DS.Palette.elevated, in: RoundedRectangle(cornerRadius: DS.Radius.md))
.overlay(RoundedRectangle(cornerRadius: DS.Radius.md).strokeBorder(DS.Palette.neutral300, lineWidth: DS.Size.stroke))
SegmentedControl(selection: $scope, options: Scope.allCases.map { ($0, $0.title) })
.frame(width: DS.Size.settingsSidebar)
.frame(width: DS.Size.sidebarWidth)
SegmentedControl(selection: $grouping, options: Grouping.allCases.map { ($0, $0.title) })
.frame(width: DS.Size.settingsSidebar - DS.Space.s8)
.frame(width: DS.Size.sidebarWidth - DS.Space.s8)
Spacer(minLength: 0)
}
}
@@ -12,6 +12,7 @@ struct ThermalPage: View {
FanCard().frame(maxWidth: .infinity)
}
.fixedSize(horizontal: false, vertical: true)
FanSafetySettings()
}
}
}
@@ -3,121 +3,21 @@ import HelperShared
import Metrics
import SwiftUI
enum SettingsSection: String, CaseIterable, Identifiable {
case general, menuBar, network, thermal, helper, about
var id: String { rawValue }
var title: String {
switch self {
case .general: "通用"
case .menuBar: "菜单栏"
case .network: "网络"
case .thermal: "散热与防休眠"
case .helper: "辅助工具"
case .about: "关于"
}
}
var symbol: String {
switch self {
case .general: "gearshape"
case .menuBar: "menubar.rectangle"
case .network: "network"
case .thermal: "fan"
case .helper: "lock.shield"
case .about: "info.circle"
}
}
}
public struct SettingsView: View {
/// 主窗口里的设置页:与其他页面同样的滚动容器,打开时刷新登录项与辅助工具状态
struct SettingsTabPage<Content: View>: View {
@Environment(AppModel.self) private var model
@Environment(\.isSnapshot) private var isSnapshot
@State private var section: SettingsSection
public init() {
_section = State(initialValue: .general)
}
init(section: SettingsSection) {
_section = State(initialValue: section)
}
public var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: DS.Space.s1) {
ForEach(SettingsSection.allCases) { item in
SidebarButton(title: item.title, symbol: item.symbol, isSelected: item == section) { section = item }
}
Spacer()
}
.sidebarGlider()
.padding(.horizontal, DS.Space.s3)
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
.padding(.bottom, DS.Space.s3)
.frame(width: DS.Size.settingsSidebar)
.frame(maxHeight: .infinity)
.background(alignment: .top) {
WindowDragArea().frame(height: DS.Size.windowHeader)
}
VStack(alignment: .leading, spacing: 0) {
Text(section.title)
.dsFont(.base, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
.padding(.horizontal, DS.Space.s3 + DS.Space.s1)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: DS.Size.windowHeader)
.background(WindowDragArea())
SettingsPage {
switch section {
case .general: GeneralSettings()
case .menuBar: MenuBarSettings()
case .network: NetworkSettings()
case .thermal: ThermalSettings()
case .helper: HelperSettings()
case .about: AboutSettings()
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.frame(width: isSnapshot ? DS.Size.settingsWidth : nil)
.frame(minWidth: DS.Size.settingsWidth, maxWidth: .infinity,
minHeight: isSnapshot ? nil : DS.Size.settingsHeight, maxHeight: isSnapshot ? nil : .infinity)
.fixedSize(horizontal: false, vertical: isSnapshot)
.background(DS.Palette.background)
.ignoresSafeArea()
.onAppear {
model.refreshLaunchAtLogin()
model.helper.refreshStatus()
}
}
}
private struct SettingsPage<Content: View>: View {
@ViewBuilder var content: Content
@Environment(\.isSnapshot) private var isSnapshot
var body: some View {
let stack = VStack(alignment: .leading, spacing: DS.Space.s4) {
content
}
.padding(.horizontal, DS.Space.s3 + DS.Space.s1)
.padding(.bottom, DS.Space.s6)
.frame(maxWidth: .infinity, alignment: .leading)
if isSnapshot {
stack
} else {
ScrollView { stack.overlayScrollers() }
.scrollBounceBehavior(.basedOnSize)
}
PageScroll { content }
.onAppear {
model.refreshLaunchAtLogin()
model.helper.refreshStatus()
}
}
}
private struct SettingsGroup<Content: View>: View {
struct SettingsGroup<Content: View>: View {
var caption: String?
@ViewBuilder var content: Content
@@ -135,7 +35,7 @@ private struct SettingsGroup<Content: View>: View {
}
/// 分组内的一行,自带内边距与分隔线
private struct GroupRow<Content: View>: View {
struct GroupRow<Content: View>: View {
var showsDivider = true
@ViewBuilder var content: Content
@@ -151,7 +51,7 @@ private struct GroupRow<Content: View>: View {
// MARK: - 通用
private struct GeneralSettings: View {
struct GeneralSettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@@ -159,10 +59,10 @@ private struct GeneralSettings: View {
SettingsGroup {
GroupRow(showsDivider: false) {
SettingRow(title: "外观", subtitle: "面板与设置窗口的配色;菜单栏始终跟随系统") {
SettingRow(title: "外观", subtitle: "主窗口与弹窗的配色;菜单栏始终跟随系统") {
SegmentedControl(selection: $settings.appearance,
options: AppearanceMode.allCases.map { ($0, $0.title) })
.frame(width: DS.Size.settingsSidebar + DS.Space.s12)
.frame(width: DS.Size.sidebarWidth + DS.Space.s12)
}
}
GroupRow {
@@ -176,13 +76,13 @@ private struct GeneralSettings: View {
SettingRow(title: "刷新频率", subtitle: "只显示菜单栏时的采样间隔;打开弹窗或主窗口时固定为 1 秒") {
SegmentedControl(selection: $settings.refreshSeconds,
options: AppSettings.refreshOptions.map { ($0, "\($0) 秒") })
.frame(width: DS.Size.settingsSidebar + DS.Space.s12)
.frame(width: DS.Size.sidebarWidth + DS.Space.s12)
}
}
GroupRow {
SettingRow(title: "温度单位") {
SegmentedControl(selection: $settings.useFahrenheit, options: [(false, "°C"), (true, "°F")])
.frame(width: DS.Size.settingsSidebar / 2 + DS.Space.s6)
.frame(width: DS.Size.sidebarWidth / 2 + DS.Space.s6)
}
}
}
@@ -191,7 +91,7 @@ private struct GeneralSettings: View {
// MARK: - 菜单栏
private struct MenuBarSettings: View {
struct MenuBarSettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@@ -207,7 +107,7 @@ private struct MenuBarSettings: View {
: "所有指标合成一个图标,点击打开主窗口") {
SegmentedControl(selection: $settings.menuBarLayout,
options: MenuBarLayout.allCases.map { ($0, $0.title) })
.frame(width: DS.Size.settingsSidebar + DS.Space.s6)
.frame(width: DS.Size.sidebarWidth + DS.Space.s6)
}
}
}
@@ -262,7 +162,7 @@ private struct MenuBarSettings: View {
if item == .network {
SegmentedControl(selection: $settings.networkStyle,
options: NetworkMenuStyle.allCases.map { ($0, $0.title) })
.frame(width: DS.Size.settingsSidebar + DS.Space.s6)
.frame(width: DS.Size.sidebarWidth + DS.Space.s6)
} else {
Picker("风格", selection: Binding(get: { settings.styleOverrides[item] },
set: { settings.setStyleOverride($0, for: item) })) {
@@ -415,7 +315,7 @@ private struct MenuBarPreview: View {
// MARK: - 网络
private struct NetworkSettings: View {
struct NetworkSettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@@ -431,7 +331,7 @@ private struct NetworkSettings: View {
SettingRow(title: "探测间隔") {
SegmentedControl(selection: $settings.probeSeconds,
options: AppSettings.probeOptions.map { ($0, "\($0) 秒") })
.frame(width: DS.Size.settingsSidebar)
.frame(width: DS.Size.sidebarWidth)
}
}
.disabled(!settings.probeEnabled)
@@ -548,28 +448,38 @@ private struct GeoDatabaseSettings: View {
// MARK: - 散热与防休眠
private struct ThermalSettings: View {
/// 放在“温度与风扇”页底部
struct FanSafetySettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@Bindable var settings = model.settings
SettingsGroup(caption: "风扇") {
SettingsGroup(caption: "风扇设置") {
GroupRow(showsDivider: false) {
SettingRow(title: "安全温度", subtitle: "自定义转速时,CPU 达到该温度自动恢复系统控制") {
SegmentedControl(selection: $settings.fanSafetyTemperature,
options: AppSettings.fanSafetyOptions.map { ($0, "\($0)°C") })
.frame(width: DS.Size.settingsSidebar + DS.Space.s12)
.frame(width: DS.Size.sidebarWidth + DS.Space.s12)
}
}
}
}
}
SettingsGroup(caption: "合盖运行") {
/// 放在“防休眠”页底部
struct LidBatterySettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@Bindable var settings = model.settings
SettingsGroup(caption: "合盖运行设置") {
GroupRow(showsDivider: false) {
SettingRow(title: "电量下限", subtitle: "使用电池且电量低于该值时,自动关闭合盖运行") {
SegmentedControl(selection: $settings.lidModeBatteryFloor,
options: AppSettings.batteryFloorOptions.map { ($0, "\($0)%") })
.frame(width: DS.Size.settingsSidebar + DS.Space.s12)
.frame(width: DS.Size.sidebarWidth + DS.Space.s12)
}
}
}
@@ -578,7 +488,7 @@ private struct ThermalSettings: View {
// MARK: - 辅助工具
private struct HelperSettings: View {
struct HelperSettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@@ -662,7 +572,7 @@ private struct CapabilityLine: View {
// MARK: - 关于
private struct AboutSettings: View {
struct AboutSettings: View {
var body: some View {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "开发版"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
@@ -52,10 +52,6 @@ enum SnapshotRenderer {
write(PopoverRootView(item: item), model: model, appearance: appearance,
to: outputDirectory.appendingPathComponent("popover-\(item.rawValue)-\(suffix).png"))
}
for section in SettingsSection.allCases {
write(SettingsView(section: section), model: model, appearance: appearance,
to: outputDirectory.appendingPathComponent("settings-\(section.rawValue)-\(suffix).png"))
}
}
let menuBar = MenuBarRenderer.image(for: model)
@@ -220,11 +220,18 @@ public enum AppearanceMode: String, CaseIterable, Identifiable, Sendable {
/// 主窗口侧边栏的页面
public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case overview, system, cpu, gpu, memory, network, thermal, processes, keepAwake, cleaner
case settingsGeneral, settingsMenuBar, settingsHelper, settingsAbout
public var id: String { rawValue }
static let monitors: [PanelTab] = [.overview, .system, .cpu, .gpu, .memory, .network, .thermal]
static let tools: [PanelTab] = [.processes, .keepAwake, .cleaner]
static let settings: [PanelTab] = [.settingsGeneral, .settingsMenuBar, .settingsHelper, .settingsAbout]
var isSettings: Bool { Self.settings.contains(self) }
/// 页面顶栏的标题:设置页带上分组名
var headerTitle: String { isSettings ? "设置 · \(title)" : title }
var title: String {
switch self {
@@ -238,6 +245,10 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case .processes: "进程"
case .keepAwake: "防休眠"
case .cleaner: "清理"
case .settingsGeneral: "通用"
case .settingsMenuBar: "菜单栏"
case .settingsHelper: "辅助工具"
case .settingsAbout: "关于"
}
}
@@ -253,6 +264,10 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case .processes: "list.bullet.rectangle"
case .keepAwake: "cup.and.saucer"
case .cleaner: "sparkles"
case .settingsGeneral: "gearshape"
case .settingsMenuBar: "menubar.rectangle"
case .settingsHelper: "lock.shield"
case .settingsAbout: "info.circle"
}
}
+7 -6
View File
@@ -46,10 +46,10 @@ Developed and tested on Apple Silicon. The interface is in Simplified Chinese.
<!-- changelog:start -->
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
Not released yet · **46** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
Not released yet · **47** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
<details open>
<summary><b>2026-09-13</b> · Unreleased · 30 added · 10 style · 6 fixed</summary>
<summary><b>2026-09-13</b> · Unreleased · 30 added · 11 style · 6 fixed</summary>
**Added**
@@ -80,17 +80,18 @@ Not released yet · **46** changes in development · [full changelog](CHANGELOG.
- 清理的安全边界:只清理白名单目录,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过,执行前逐项重新校验。
- 系统维护:一键刷新 DNS 缓存、释放内存,已安装辅助工具时直接执行,否则请求一次管理员授权。
- 进程页按 CPU 或内存排序,辅助进程归并显示为所属应用。
- 设置窗口:外观、登录时启动、刷新频率、温度单位、菜单栏布局 / 显示项 / 样式 / 弹窗内容、连接探测与公网 IP 查询、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 统一外观:浅色为白底蓝色、深色为黑底蓝色,主窗口、设置窗口与弹窗共用同一底色;侧边栏、标题栏与页面不分栏着色,红绿灯按钮与页面标题对齐;卡片为浅灰 / 深灰实色。侧边栏的太阳 / 月亮按钮一键切换,也可以跟随系统。
- 设置(主窗口侧边栏的“设置”分组):外观、登录时启动、刷新频率、温度单位、菜单栏布局 / 显示项 / 样式 / 弹窗内容、连接探测与公网 IP 查询、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 统一外观:浅色为白底蓝色、深色为黑底蓝色,主窗口与弹窗共用同一底色;侧边栏、标题栏与页面不分栏着色,红绿灯按钮与页面标题对齐;卡片为浅灰 / 深灰实色。侧边栏的太阳 / 月亮按钮一键切换,也可以跟随系统。
- 截图命令 `OpenStats --snapshot <目录>`:用本机实时数据渲染主窗口各页、详情弹窗、设置和菜单栏的浅色与深色截图,IP 与硬件地址替换为示例值。
**Style**
- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
- 设置并入主窗口:侧边栏新增“设置”分组(通用、菜单栏、辅助工具、关于);网络设置(连接探测、公网 IP、IP 归属地数据库)放到“网络”页底部,风扇安全温度放到“温度与风扇”页底部,合盖运行电量下限放到“防休眠”页底部;弹窗齿轮、菜单栏右键“设置…”与 ⌘, 都打开主窗口的通用设置;不再有单独的设置窗口,已有偏好全部保留;导航较多时侧边栏可滚动。
- 主窗口侧边栏的选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 主窗口默认 1072×720,可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。
- 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
+8 -7
View File
@@ -44,10 +44,10 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/
<!-- changelog:start -->
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
尚未发布正式版本 · 开发中 **46** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
尚未发布正式版本 · 开发中 **47** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
<details open>
<summary><b>2026-09-13</b> · 未发布 · 新增 30 · 样式 10 · 修复 6</summary>
<summary><b>2026-09-13</b> · 未发布 · 新增 30 · 样式 11 · 修复 6</summary>
**新增**
@@ -78,17 +78,18 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/
- 清理的安全边界:只清理白名单目录,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过,执行前逐项重新校验。
- 系统维护:一键刷新 DNS 缓存、释放内存,已安装辅助工具时直接执行,否则请求一次管理员授权。
- 进程页按 CPU 或内存排序,辅助进程归并显示为所属应用。
- 设置窗口:外观、登录时启动、刷新频率、温度单位、菜单栏布局 / 显示项 / 样式 / 弹窗内容、连接探测与公网 IP 查询、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 统一外观:浅色为白底蓝色、深色为黑底蓝色,主窗口、设置窗口与弹窗共用同一底色;侧边栏、标题栏与页面不分栏着色,红绿灯按钮与页面标题对齐;卡片为浅灰 / 深灰实色。侧边栏的太阳 / 月亮按钮一键切换,也可以跟随系统。
- 设置(主窗口侧边栏的“设置”分组):外观、登录时启动、刷新频率、温度单位、菜单栏布局 / 显示项 / 样式 / 弹窗内容、连接探测与公网 IP 查询、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 统一外观:浅色为白底蓝色、深色为黑底蓝色,主窗口与弹窗共用同一底色;侧边栏、标题栏与页面不分栏着色,红绿灯按钮与页面标题对齐;卡片为浅灰 / 深灰实色。侧边栏的太阳 / 月亮按钮一键切换,也可以跟随系统。
- 截图命令 `OpenStats --snapshot <目录>`:用本机实时数据渲染主窗口各页、详情弹窗、设置和菜单栏的浅色与深色截图,IP 与硬件地址替换为示例值。
**样式**
- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
- 设置并入主窗口:侧边栏新增“设置”分组(通用、菜单栏、辅助工具、关于);网络设置(连接探测、公网 IP、IP 归属地数据库)放到“网络”页底部,风扇安全温度放到“温度与风扇”页底部,合盖运行电量下限放到“防休眠”页底部;弹窗齿轮、菜单栏右键“设置…”与 ⌘, 都打开主窗口的通用设置;不再有单独的设置窗口,已有偏好全部保留;导航较多时侧边栏可滚动。
- 主窗口侧边栏的选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 主窗口默认 1072×720,可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。
- 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
@@ -260,7 +261,7 @@ SKIP_NOTARIZE=1 make release # 生成未公证的 DMG,仅供本机测
以及只采集屏幕上需要的指标的 `MetricsHub`。
- `Packages/OpenStatsKit/Sources/Cleaner`——清理规则、安全守卫、扫描与执行。
- `Packages/OpenStatsKit/Sources/HelperShared`——XPC 协议与系统维护命令。
- `Packages/OpenStatsKit/Sources/OpenStatsUI`——设计 token、面板、设置窗口、菜单栏绘制。
- `Packages/OpenStatsKit/Sources/OpenStatsUI`——设计 token、主窗口各页与设置页、详情弹窗、菜单栏绘制。
- `Helper/`——特权辅助工具及其 launchd 配置;`App/`——应用入口。
更多内容见 [ARCHITECTURE.md](ARCHITECTURE.md)。应用的每一项改动都按日期记录在 [CHANGELOG.md](CHANGELOG.md)。
+2 -2
View File
@@ -348,7 +348,7 @@
<div class="log">
<article class="log__day">
<div class="log__head"><time datetime="2026-09-13">2026-09-13</time><span class="log__tag">未发布</span></div>
<p class="log__counts">新增 30 · 样式 10 · 修复 6</p>
<p class="log__counts">新增 30 · 样式 11 · 修复 6</p>
<ul class="log__list">
<li><span class="log__kind">新增</span>菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。</li>
<li><span class="log__kind">新增</span>菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。</li>
@@ -359,7 +359,7 @@
<li><span class="log__kind">新增</span>网络详情:上下镜像的流量历史;连接探测格子(每 1 / 2 / 5 秒 ping 一次 Cloudflare、Google、阿里云、腾讯或路由器,显示延迟、抖动、丢包);接口、物理地址、Wi-Fi 信号与速率、VPN / 代理;本地 IPv4 / IPv6、路由器、公网 IPv4 / IPv6,点击即可拷贝;归属地显示 SVG 国旗、城市、ASN 与网络运营方;各进程上传下载速率。</li>
<li><span class="log__kind">新增</span>DNS:显示正在使用的 DNS 与配置方式,一键刷新 DNS 缓存,一键切换为自动、Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写地址(逐个校验为 IPv4 / IPv6)。流量经过 VPN / 代理时提示 DNS 可能被接管。</li>
</ul>
<a class="log__more" href="https://github.com/gentpan/OpenStats/blob/main/CHANGELOG.md">还有 38 项 →</a>
<a class="log__more" href="https://github.com/gentpan/OpenStats/blob/main/CHANGELOG.md">还有 39 项 →</a>
</article>
</div>
<!-- changelog:end -->