释放内存移到标题栏;主窗口与设置窗口默认更大且可调整;滚动条只在滚动时显示
This commit is contained in:
1 parent
3caf690987
commit
958d566870
13 files changed
+106
-30
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 -->
|
||||
|
||||
Reference in new issue
Block a user