设置并入主窗口:侧边栏新增设置分组,网络、风扇、合盖设置放到对应页面
- 侧边栏“设置”分组:通用、菜单栏、辅助工具、关于;导航较多时侧边栏可滚动 - 网络设置(连接探测、公网 IP、IP 归属地数据库)放在网络页底部,风扇安全温度放在温度与风扇页,合盖电量下限放在防休眠页 - 弹窗齿轮、菜单栏右键“设置…”与 ⌘, 打开主窗口的通用设置;删除独立设置窗口,偏好设置不变 - 去掉不再使用的设置窗口尺寸 token,侧边栏宽度改名为 sidebarWidth;截图渲染改为按页面输出
This commit is contained in:
1 parent
0829f425cb
commit
23e4a8b8e0
15 files changed
+135
-211
No files matched your search
+5
-4
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-126
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 -->
|
||||
|
||||
Reference in new issue
Block a user