From 23e4a8b8e047ad52ad9713beb1e5b9fba05a40dd Mon Sep 17 00:00:00 2001 From: gentpan Date: Sun, 13 Sep 2026 07:04:14 +0500 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=B9=B6=E5=85=A5=E4=B8=BB?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=EF=BC=9A=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=AE=BE=E7=BD=AE=E5=88=86=E7=BB=84=EF=BC=8C=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E3=80=81=E9=A3=8E=E6=89=87=E3=80=81=E5=90=88=E7=9B=96?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=94=BE=E5=88=B0=E5=AF=B9=E5=BA=94=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 侧边栏“设置”分组:通用、菜单栏、辅助工具、关于;导航较多时侧边栏可滚动 - 网络设置(连接探测、公网 IP、IP 归属地数据库)放在网络页底部,风扇安全温度放在温度与风扇页,合盖电量下限放在防休眠页 - 弹窗齿轮、菜单栏右键“设置…”与 ⌘, 打开主窗口的通用设置;删除独立设置窗口,偏好设置不变 - 去掉不再使用的设置窗口尺寸 token,侧边栏宽度改名为 sidebarWidth;截图渲染改为按页面输出 --- CHANGELOG.md | 9 +- .../OpenStatsUI/AppKit/AppController.swift | 44 +---- .../Sources/OpenStatsUI/AppKit/MainMenu.swift | 5 +- .../AppKit/MainWindowController.swift | 6 +- .../Sources/OpenStatsUI/Design/Tokens.swift | 10 +- .../OpenStatsUI/Panel/KeepAwakePage.swift | 1 + .../OpenStatsUI/Panel/MainWindowView.swift | 51 ++++-- .../OpenStatsUI/Panel/ProcessesPage.swift | 6 +- .../OpenStatsUI/Panel/ThermalPage.swift | 1 + ...SettingsView.swift => SettingsPages.swift} | 162 ++++-------------- .../Snapshot/SnapshotRenderer.swift | 4 - .../OpenStatsUI/State/AppSettings.swift | 15 ++ README.md | 13 +- README.zh-CN.md | 15 +- web/index.html | 4 +- 15 files changed, 135 insertions(+), 211 deletions(-) rename Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/{SettingsView.swift => SettingsPages.swift} (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a97bc..215c04b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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;每种风格内部只用一套字号,指标之间留出更宽的间距。 - 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift index 4a86d07..ddbbda4 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift @@ -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) diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainMenu.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainMenu.swift index 2d629a3..533e624 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainMenu.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainMenu.swift @@ -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") diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift index 494abff..0803cfb 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MainWindowController.swift @@ -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) } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift index 8400fca..03ed919 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift @@ -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 } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/KeepAwakePage.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/KeepAwakePage.swift index d73c957..e2d65c8 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/KeepAwakePage.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/KeepAwakePage.swift @@ -16,6 +16,7 @@ struct KeepAwakePage: View { LidCard().frame(maxWidth: .infinity) } .fixedSize(horizontal: false, vertical: true) + LidBatterySettings() } } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift index 1b13a73..5433ba5 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift @@ -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: 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) diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ProcessesPage.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ProcessesPage.swift index 00bc8cc..5e317e3 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ProcessesPage.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ProcessesPage.swift @@ -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) } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ThermalPage.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ThermalPage.swift index 272a5dc..fc048ca 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ThermalPage.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/ThermalPage.swift @@ -12,6 +12,7 @@ struct ThermalPage: View { FanCard().frame(maxWidth: .infinity) } .fixedSize(horizontal: false, vertical: true) + FanSafetySettings() } } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift similarity index 83% rename from Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift rename to Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift index 47c89be..7bbc34d 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsView.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift @@ -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: 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: 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: View { +struct SettingsGroup: View { var caption: String? @ViewBuilder var content: Content @@ -135,7 +35,7 @@ private struct SettingsGroup: View { } /// 分组内的一行,自带内边距与分隔线 -private struct GroupRow: View { +struct GroupRow: View { var showsDivider = true @ViewBuilder var content: Content @@ -151,7 +51,7 @@ private struct GroupRow: 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 diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift index b874905..dff2d37 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Snapshot/SnapshotRenderer.swift @@ -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) diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift index ed294ad..9ab1330 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift @@ -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" } } diff --git a/README.md b/README.md index 1f50c9f..c7234a5 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,10 @@ Developed and tested on Apple Silicon. The interface is in Simplified Chinese. -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)
-2026-09-13 · Unreleased · 30 added · 10 style · 6 fixed +2026-09-13 · Unreleased · 30 added · 11 style · 6 fixed **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;每种风格内部只用一套字号,指标之间留出更宽的间距。 - 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。 - 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。 diff --git a/README.zh-CN.md b/README.zh-CN.md index 8b2a1fa..a18df0f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -44,10 +44,10 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/ -尚未发布正式版本 · 开发中 **46** 项改动尚未发布 · [完整更新日志](CHANGELOG.md) +尚未发布正式版本 · 开发中 **47** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
-2026-09-13 · 未发布 · 新增 30 · 样式 10 · 修复 6 +2026-09-13 · 未发布 · 新增 30 · 样式 11 · 修复 6 **新增** @@ -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)。 diff --git a/web/index.html b/web/index.html index e313b3e..f47763f 100644 --- a/web/index.html +++ b/web/index.html @@ -348,7 +348,7 @@
未发布
-

新增 30 · 样式 10 · 修复 6

+

新增 30 · 样式 11 · 修复 6

  • 新增菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。
  • 新增菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。
  • @@ -359,7 +359,7 @@
  • 新增网络详情:上下镜像的流量历史;连接探测格子(每 1 / 2 / 5 秒 ping 一次 Cloudflare、Google、阿里云、腾讯或路由器,显示延迟、抖动、丢包);接口、物理地址、Wi-Fi 信号与速率、VPN / 代理;本地 IPv4 / IPv6、路由器、公网 IPv4 / IPv6,点击即可拷贝;归属地显示 SVG 国旗、城市、ASN 与网络运营方;各进程上传下载速率。
  • 新增DNS:显示正在使用的 DNS 与配置方式,一键刷新 DNS 缓存,一键切换为自动、Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写地址(逐个校验为 IPv4 / IPv6)。流量经过 VPN / 代理时提示 DNS 可能被接管。
- 还有 38 项 → + 还有 39 项 →