释放内存移到标题栏;主窗口与设置窗口默认更大且可调整;滚动条只在滚动时显示

This commit is contained in:
gentpan committed 2026-09-13 06:18:50 +05:00
1 parent 3caf690987
commit 958d566870
13 files changed
+106 -30

No files matched your search

+3
View File
@@ -39,6 +39,9 @@
#### 样式
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。
- 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
@@ -92,8 +92,11 @@ public final class AppController: NSObject, NSApplicationDelegate, NSWindowDeleg
private func showSettings() {
menuBar.dismissPopovers()
if settingsWindow == nil {
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: DS.Size.settingsWidth, height: DS.Size.settingsHeight),
styleMask: [.titled, .closable, .miniaturizable],
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 设置"
@@ -101,7 +104,9 @@ public final class AppController: NSObject, NSApplicationDelegate, NSWindowDeleg
window.isReleasedWhenClosed = false
window.delegate = self
window.contentView = NSHostingView(rootView: SettingsView().environment(model))
window.center()
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()
@@ -30,7 +30,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: width, height: DS.Size.panelMinHeight * 2),
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: DS.Size.settingsSidebar + DS.Size.windowDefaultContentWidth,
height: DS.Size.panelMinHeight * 2),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false)
@@ -39,7 +40,8 @@ final class MainWindowController: NSObject, NSWindowDelegate {
window.isReleasedWhenClosed = false
window.delegate = self
window.contentMinSize = NSSize(width: width, height: DS.Size.panelMinHeight * 1.5)
window.setFrameAutosaveName("OpenStatsMainWindow")
// 默认尺寸调大后换一个保存名,旧的小窗口尺寸不再沿用
window.setFrameAutosaveName("OpenStatsMainWindow.v2")
window.contentView = NSHostingView(rootView: MainWindowView().environment(model))
return window
}
@@ -52,7 +54,9 @@ final class MainWindowController: NSObject, NSWindowDelegate {
}
let measuring = NSHostingView(rootView: MainWindowView().environment(model).environment(\.isSnapshot, true))
let height = min(measuring.fittingSize.height, visible.height - DS.Space.s12)
let size = NSSize(width: window.frame.width, height: max(height, DS.Size.panelMinHeight * 1.5))
// 至少 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))
return NSRect(x: visible.midX - size.width / 2, y: visible.midY - size.height / 2,
width: size.width, height: size.height)
}
@@ -496,8 +496,7 @@ struct PageScroll<Content: View>: View {
stack
} else {
// 内容放得下时不回弹,避免点击时整页轻微抖动
ScrollView { stack }
.scrollIndicators(.never)
ScrollView { stack.overlayScrollers() }
.scrollBounceBehavior(.basedOnSize)
}
}
@@ -32,3 +32,47 @@ struct WindowDragArea: NSViewRepresentable {
func makeNSView(context: Context) -> DragView { DragView() }
func updateNSView(_ nsView: DragView, context: Context) {}
}
/// 滚动条只在滚动时出现:把所在的 NSScrollView 固定为浮层样式。
/// 系统设置为“始终显示滚动条”或接了鼠标时,SwiftUI 默认会一直显示一条占位的滚动条
struct OverlayScrollers: NSViewRepresentable {
final class Probe: NSView {
private var observer: NSObjectProtocol?
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
guard window != nil else {
if let observer { NotificationCenter.default.removeObserver(observer) }
observer = nil
return
}
apply()
guard observer == nil else { return }
// 系统切换滚动条样式时会重置,收到通知后重新设置
observer = NotificationCenter.default.addObserver(forName: NSScroller.preferredScrollerStyleDidChangeNotification,
object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.apply() }
}
}
func apply() {
DispatchQueue.main.async { [weak self] in
MainActor.assumeIsolated {
guard let scrollView = self?.enclosingScrollView else { return }
scrollView.scrollerStyle = .overlay
scrollView.autohidesScrollers = true
}
}
}
}
func makeNSView(context: Context) -> Probe { Probe() }
func updateNSView(_ nsView: Probe, context: Context) { nsView.apply() }
}
extension View {
/// 放在 ScrollView 的内容里:滚动条改为浮层,只在滚动时显示
func overlayScrollers() -> some View {
background(OverlayScrollers().frame(width: 0, height: 0))
}
}
@@ -132,8 +132,12 @@ extension DS {
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 windowDefaultContentWidth: CGFloat = 880
static let settingsSidebar: CGFloat = 192
static let valueColumn: CGFloat = 64
static let labelColumn: CGFloat = 48
@@ -112,6 +112,7 @@ private struct PageHeader: View {
if model.keepAwake.isActive {
Chip(text: "防休眠已开启", icon: "cup.and.saucer.fill", tone: .primary)
}
if tab == .memory { PurgeMemoryButton() }
if let item = tab.menuBarItem {
Text("在菜单栏显示").dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
DSToggle(isOn: Binding(get: { settings.isEnabled(item) }, set: { settings.setEnabled(item, $0) }),
@@ -193,7 +193,6 @@ struct MemoryPopover: View {
}
}
}
PurgeMemoryRow()
}
case .memoryCompression:
SectionCard(title: section.title) {
@@ -397,25 +396,35 @@ private struct ExplainableRow<Content: View>: View {
}
}
private struct PurgeMemoryRow: View {
/// 标题栏里的“释放内存”:执行中显示进度,完成后短暂显示结果再恢复
struct PurgeMemoryButton: View {
@Environment(AppModel.self) private var model
@State private var result: MaintenanceController.Outcome?
var body: some View {
let maintenance = model.maintenance
HStack(spacing: DS.Space.s2) {
Button(maintenance.running == .purgeMemory ? "正在释放…" : "释放内存") {
Task { await maintenance.run(.purgeMemory) }
let running = maintenance.running == .purgeMemory
Button {
Task {
await maintenance.run(.purgeMemory)
result = maintenance.outcomes[.purgeMemory]
try? await Task.sleep(for: .seconds(3))
result = nil
}
.buttonStyle(DSButtonStyle(kind: .secondary))
.disabled(maintenance.running != nil)
if let outcome = maintenance.outcomes[.purgeMemory] {
Text(outcome.text)
.dsFont(.xs)
.foregroundStyle(outcome.isError ? DS.Palette.error : DS.Palette.textSecondary)
.lineLimit(2)
} label: {
HStack(spacing: DS.Space.s1) {
if running {
ProgressView().controlSize(.mini)
} else {
Image(systemName: result == nil ? "wind" : result!.isError ? "exclamationmark.triangle" : "checkmark")
}
Text(running ? "正在释放…" : result?.text ?? "释放内存").lineLimit(1)
}
Spacer(minLength: 0)
.foregroundStyle(result?.isError == true ? DS.Palette.error : result != nil ? DS.Palette.success : DS.Palette.textPrimary)
}
.buttonStyle(DSButtonStyle(kind: .secondary))
.disabled(maintenance.running != nil)
.help("清理可回收的缓存内存(需要管理员授权或辅助工具)")
}
}
@@ -49,6 +49,7 @@ private struct PopoverHeader: View {
.dsFont(.base, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
Spacer(minLength: DS.Space.s2)
if item == .memory { PurgeMemoryButton() }
IconButton(systemName: "macwindow", help: "打开 OpenStats 主窗口") { model.openMainWindow(nil) }
IconButton(systemName: "gearshape", help: "设置") { model.openSettings() }
}
@@ -81,7 +81,9 @@ public struct SettingsView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.frame(width: DS.Size.settingsWidth, height: isSnapshot ? nil : DS.Size.settingsHeight)
.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()
@@ -107,8 +109,8 @@ private struct SettingsPage<Content: View>: View {
if isSnapshot {
stack
} else {
ScrollView { stack }
.scrollIndicators(.automatic)
ScrollView { stack.overlayScrollers() }
.scrollBounceBehavior(.basedOnSize)
}
}
}
+4 -2
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 · **38** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
Not released yet · **40** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
<details open>
<summary><b>2026-09-13</b> · Unreleased · 27 added · 5 style · 6 fixed</summary>
<summary><b>2026-09-13</b> · Unreleased · 27 added · 7 style · 6 fixed</summary>
**Added**
@@ -83,6 +83,8 @@ Not released yet · **38** changes in development · [full changelog](CHANGELOG.
**Style**
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。
- 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
+4 -2
View File
@@ -44,10 +44,10 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/
<!-- changelog:start -->
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
尚未发布正式版本 · 开发中 **38** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
尚未发布正式版本 · 开发中 **40** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
<details open>
<summary><b>2026-09-13</b> · 未发布 · 新增 27 · 样式 5 · 修复 6</summary>
<summary><b>2026-09-13</b> · 未发布 · 新增 27 · 样式 7 · 修复 6</summary>
**新增**
@@ -81,6 +81,8 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/
**样式**
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
- 主窗口默认 1072×720、设置窗口默认 920×720,两者都可自由调整大小;滚动条改为浮层样式,只在滚动时出现。
- 菜单栏字号缩小并统一:两行布局为 7pt 标签加 10pt 数值,单行布局 11pt,网速两行 9pt;每种风格内部只用一套字号,指标之间留出更宽的间距。
- 柱状历史去掉满高的灰色底槽,只保留一条淡基线,负载起伏更清楚。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
+2 -2
View File
@@ -347,7 +347,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">新增 27 · 样式 5 · 修复 6</p>
<p class="log__counts">新增 27 · 样式 7 · 修复 6</p>
<ul class="log__list">
<li><span class="log__kind">新增</span>菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。</li>
<li><span class="log__kind">新增</span>菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。</li>
@@ -358,7 +358,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">还有 30 项 →</a>
<a class="log__more" href="https://github.com/gentpan/OpenStats/blob/main/CHANGELOG.md">还有 32 项 →</a>
</article>
</div>
<!-- changelog:end -->