- 账号与同步:GitHub / Google / Apple 登录后把菜单栏项目与风格等偏好同步到 getopenstats.com,换一台 Mac 登录即恢复(AccountSync 模块、SyncController、SettingsDocument、账号设置页,openstats:// 回调) - 磁盘页成为工具页:已用 / 可清除 / 可用分段容量条、空间分析、文件系统检查、本地快照清理(辅助工具协议 v4)、其他磁盘推出 - 公网 IP:归属地数据源可选 CleanIP.io / ipapi.is / DB-IP / ipinfo.io / 本地 GeoLite2,每台 Mac 自己查、本地缓存 7 天;IPv4 / IPv6 双栈各自查询与切换;弹窗显示 ASN、运营方、原生 / 住宅等徽章与纯净度评分色带;接口区块可重置上传 / 下载统计 - 弹窗:右上角按钮改为该指标自己的图标 + 总设置;主窗口页面标题栏的开关点不动已修复;滚动条改为悬浮式,内容不再偏左;网络弹窗卡片被标题行撑宽的问题已修复 - 设置:⌘ 拖动排序说明;每个菜单栏项目单独选风格并带实时预览;网络与磁盘的进程榜单固定 5 行并平滑排序;CPU 持续高负载通知 - 设计系统:分段条、评分色带、标签徽章、胶囊切换、刷新按钮等组件与徽章配色 token - 更新日志、架构说明、README 与译文同步
83 lines
3.4 KiB
Swift
83 lines
3.4 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
/// 弹窗窗口的底板:圆角裁切的普通容器,背景由 SwiftUI 内容绘制
|
|
@MainActor
|
|
enum PanelContainer {
|
|
/// 容器必须从内容的尺寸开始:若从 0 开始,窗口把它撑满时自动调整会把内容再放大一倍
|
|
static func make(containing content: NSView, cornerRadius: CGFloat) -> NSView {
|
|
let container = NSView(frame: content.frame)
|
|
container.wantsLayer = true
|
|
container.layer?.cornerRadius = cornerRadius
|
|
container.layer?.cornerCurve = .continuous
|
|
container.layer?.masksToBounds = true
|
|
content.autoresizingMask = [.width, .height]
|
|
container.addSubview(content)
|
|
return container
|
|
}
|
|
}
|
|
|
|
/// 内容铺满标题栏后,标题栏区域由 SwiftUI 接收点击;放在顶栏背后,按住空白处可以拖动窗口
|
|
struct WindowDragArea: NSViewRepresentable {
|
|
final class DragView: NSView {
|
|
override func mouseDown(with event: NSEvent) {
|
|
if event.clickCount == 2 {
|
|
window?.performZoom(nil)
|
|
} else {
|
|
window?.performDrag(with: event)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 }
|
|
guard scrollView.scrollerStyle != .overlay || !scrollView.autohidesScrollers else { return }
|
|
scrollView.scrollerStyle = .overlay
|
|
scrollView.autohidesScrollers = true
|
|
// 样式改了之后重新排版,让内容占满原来留给滚动条的那一条
|
|
scrollView.tile()
|
|
scrollView.documentView?.needsLayout = 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))
|
|
}
|
|
}
|