菜单栏弹窗改为紧凑排版:去掉卡片底色改细分隔线,纯净度与 DNS 默认收起

- 新增 isPopover 环境值,PopoverRootView 打开;Card 在弹窗里不画灰底,只在顶部画一条细分隔线,上下 8pt、行距 6pt;PageScroll 在弹窗里区块间不再留间距
- 弹窗顶栏的两个按钮改用 24pt 的 MiniIconButton
- 网络弹窗:IP 纯净度、DNS 默认收起成一行(CollapsedSection,显示分数等级 / 正在使用的 DNS),点开后标题栏右侧有收起按钮;展开状态存在 AppSettings.expandedPopoverSections,变化时刷新弹窗高度
- 流量图高度 72pt → 56pt,连接探测改为 30 列 2 行
- 网络弹窗 1519pt → 952pt,其余弹窗矮 9%~21%;主窗口详情页不变
This commit is contained in:
gentpan committed 2026-09-16 18:52:04 +05:00
1 parent 9e4f92e366
commit 4099ec636e
10 files changed
+153 -16

No files matched your search

+1
View File
@@ -10,6 +10,7 @@
#### 样式
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
- 菜单栏弹窗改为紧凑排版,参照 Stats:区块不再是灰底卡片,改成一条细分隔线隔开,行距收紧,顶栏按钮缩小;网络弹窗的 IP 纯净度与 DNS 默认收起成一行(纯净度显示分数与等级,DNS 显示正在使用的地址),点一下展开、标题栏右侧的箭头收起,状态会记住;流量图变矮,连接探测改为两行细格。网络弹窗从约 1520pt 降到约 950pt,其他弹窗矮了一到两成。主窗口的详情页保持原样,照常完整显示。
- 连接探测只用来看网络通不通:格子通了是绿色、不通是红色(不再用橙色表示延迟偏高),固定 60 格,弹窗排成 3 行、主窗口排成 2 行;去掉格子下面的延迟、抖动、丢包三项。打开网络详情时固定每秒探测一次,「设置 · 网络」里的“探测间隔”选项随之去掉,后台低频探测仍是每 10 秒。
- 分段切换改为苹果标准样式:灰色圆角底槽,各段等宽铺满,选中的一段是实心蓝色、白字,切换时蓝块滑过去;网络详情的 IPv4 / IPv6 切换同样是灰底蓝选中。
@@ -412,6 +412,7 @@ npm 缓存 npm Cache
屏幕保持常亮 Keep display on
屏幕可按设置关闭,下载、编译等后台任务继续运行 The display can turn off as usual while downloads and builds keep running
屏幕和系统都不会因闲置而关闭或休眠 Neither the display nor the system sleeps when idle
展开{} Expand {}
峰值 Peak
峰值 {} Peak {}
工具 Tools
@@ -553,6 +554,7 @@ npm 缓存 npm Cache
搜索名称、PID 或用户 Search name, PID or user
搜索应用 Search apps
撤销 Undo
收起 Collapse
改回开机后累计 Back to since-boot totals
政府 Government
教育网 Education
@@ -183,6 +183,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
_ = model.settings.colorizeHighLoad
_ = model.settings.useFahrenheit
_ = model.settings.hiddenPopoverSections
_ = model.settings.expandedPopoverSections
_ = model.keepAwake.isActive
_ = model.settings.appearance
_ = model.isNetworkDetailVisible
@@ -7,6 +7,8 @@ extension EnvironmentValues {
@Entry var isSnapshot = false
/// 在主窗口里显示指标详情:显示全部区块,图表更高
@Entry var isDetailPage = false
/// 菜单栏弹窗:区块去掉卡片底色,用细分隔线隔开,排得更紧凑
@Entry var isPopover = false
}
// MARK: - 卡片
@@ -15,8 +17,23 @@ struct Card<Content: View>: View {
var padding: CGFloat = DS.Space.s4
var spacing: CGFloat = DS.Space.s3
@ViewBuilder var content: Content
@Environment(\.isPopover) private var isPopover
var body: some View {
if isPopover {
// 菜单栏弹窗:不画卡片,区块顶上一条细分隔线,上下各留一点空,行距收紧
VStack(alignment: .leading, spacing: min(spacing, DS.Space.s1 + DS.Space.s1 / 2)) {
content
}
.padding(.vertical, DS.Space.s2)
.frame(maxWidth: .infinity, alignment: .topLeading)
.overlay(alignment: .top) { HairlineDivider() }
} else {
cardBody
}
}
private var cardBody: some View {
VStack(alignment: .leading, spacing: spacing) {
content
}
@@ -833,11 +850,15 @@ struct DSSlider: View {
struct PageScroll<Content: View>: View {
@Environment(\.isSnapshot) private var isSnapshot
@Environment(\.isPopover) private var isPopover
@ViewBuilder var content: Content
var body: some View {
let stack = VStack(alignment: .leading, spacing: DS.Space.s3) { content }
.padding(DS.Space.s3)
// 弹窗里区块之间靠分隔线隔开,不再额外留间距
let stack = VStack(alignment: .leading, spacing: isPopover ? 0 : DS.Space.s3) { content }
.padding(.horizontal, DS.Space.s3)
.padding(.top, isPopover ? 0 : DS.Space.s3)
.padding(.bottom, isPopover ? DS.Space.s1 : DS.Space.s3)
if isSnapshot {
stack
} else {
@@ -75,7 +75,7 @@ private struct TrafficHistorySection: View {
.monospacedDigit()
}) {
MirroredRateChart(upload: upload, download: download,
height: isDetailPage ? DS.Size.chartHeight * 3 : DS.Size.chartHeight + DS.Space.s6)
height: isDetailPage ? DS.Size.chartHeight * 3 : DS.Size.chartHeight + DS.Space.s2)
}
}
@@ -104,9 +104,8 @@ private struct ProbeSection: View {
}
}) {
if settings.probeEnabled {
// 只用来看网络通不通:60 格是最近 60 次探测,主窗口里更宽,排成两行
let columns = isDetailPage ? 30 : 20
ProbeGrid(samples: network.probes.elements, columns: columns, rows: NetworkController.probeCapacity / columns)
// 只用来看网络通不通:60 格是最近 60 次探测,排成两行细格
ProbeGrid(samples: network.probes.elements, columns: NetworkController.probeCapacity / 2, rows: 2)
} else {
HStack {
Text(tr("定时 ping 一个地址,记录网络是否通畅")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
@@ -335,11 +334,35 @@ private struct AddressSection: View {
/// cleanip.io 给出的纯净度评分与风险标记
private struct PuritySection: View {
@Environment(AppModel.self) private var model
@Environment(\.isPopover) private var isPopover
/// 标题行里“查看完整报告”链接的三种长度
enum ReportStyle { case full, short, icon }
var body: some View {
// 菜单栏弹窗里默认收起成一行,只显示分数与等级
if isPopover, !model.settings.isExpanded(.networkPurity) {
let summary = collapsedSummary
CollapsedSection(title: PopoverSection.networkPurity.title, summary: summary.text, summaryColor: summary.color) {
withAnimation(DS.Motion.quick) { model.settings.setExpanded(.networkPurity, true) }
}
} else {
card
}
}
private var collapsedSummary: (text: String, color: Color) {
let network = model.network
if let purity = network.publicAddresses?.purity {
return ("\(purity.score) \(purity.grade)", DS.Grade.color(for: purity.score))
}
if !model.settings.publicIPLookup { return (tr("查询已关闭"), DS.Palette.textTertiary) }
if network.isLookingUpPublic { return (tr("正在查询…"), DS.Palette.textTertiary) }
return ("—", DS.Palette.textTertiary)
}
@ViewBuilder
private var card: some View {
let settings = model.settings
let network = model.network
let addresses = network.publicAddresses
@@ -419,8 +442,15 @@ private struct PuritySection: View {
ReportLink(url: url, style: report).padding(.leading, DS.Space.s1)
}
Spacer(minLength: 0)
RefreshButton(loading: network.isLookingUpPublic, help: tr("强制刷新:忽略缓存,立即重新查询公网 IP 与归属地")) {
network.lookUpPublicAddresses(force: true)
if isPopover {
RefreshButton(loading: network.isLookingUpPublic, help: tr("强制刷新:忽略缓存,立即重新查询公网 IP 与归属地")) {
network.lookUpPublicAddresses(force: true)
}
.collapseButton(.networkPurity, settings: model.settings)
} else {
RefreshButton(loading: network.isLookingUpPublic, help: tr("强制刷新:忽略缓存,立即重新查询公网 IP 与归属地")) {
network.lookUpPublicAddresses(force: true)
}
}
}
}
@@ -461,10 +491,25 @@ private struct PuritySection: View {
private struct DNSSection: View {
@Environment(AppModel.self) private var model
@Environment(\.isPopover) private var isPopover
@State private var editingManual = false
@State private var manualText = ""
var body: some View {
// 菜单栏弹窗里默认收起成一行,只显示正在使用的 DNS
if isPopover, !model.settings.isExpanded(.networkDNS) {
let servers = model.network.details?.dnsServers ?? []
CollapsedSection(title: PopoverSection.networkDNS.title,
summary: servers.first.map { servers.count > 1 ? "\($0) +\(servers.count - 1)" : $0 } ?? "—") {
withAnimation(DS.Motion.quick) { model.settings.setExpanded(.networkDNS, true) }
}
} else {
card
}
}
@ViewBuilder
private var card: some View {
let network = model.network
let maintenance = model.maintenance
let physical = network.details?.physical
@@ -472,7 +517,11 @@ private struct DNSSection: View {
let matched = DNSPreset.matching(manual)
SectionCard(title: PopoverSection.networkDNS.title, trailing: {
if let physical { Text(verbatim: physical.serviceName) }
if isPopover {
Text(verbatim: physical?.serviceName ?? "").collapseButton(.networkDNS, settings: model.settings)
} else if let physical {
Text(verbatim: physical.serviceName)
}
}) {
InfoRow(label: tr("正在使用")) {
Text(verbatim: network.details.map { $0.dnsServers.isEmpty ? "—" : $0.dnsServers.joined(separator: "\n") } ?? "—")
@@ -27,6 +27,7 @@ struct PopoverRootView: View {
}
}
.frame(maxWidth: .infinity, maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
.environment(\.isPopover, true)
}
.frame(width: DS.Size.popoverWidth)
.frame(maxHeight: isSnapshot ? nil : .infinity, alignment: .top)
@@ -55,8 +56,8 @@ private struct PopoverHeader: View {
if item == .memory { PurgeMemoryButton() }
// 左边的按钮用该指标自己的图标,点进主窗口里它的页面;右边的齿轮进总设置
let page = PanelTab(item: item)
IconButton(systemName: page.symbol, help: tr("在主窗口打开“\(page.title)”")) { model.openMainWindow(page) }
IconButton(systemName: "gearshape", help: tr("设置")) { model.openSettings() }
MiniIconButton(systemName: page.symbol, help: tr("在主窗口打开“\(page.title)”")) { model.openMainWindow(page) }
MiniIconButton(systemName: "gearshape", help: tr("设置")) { model.openSettings() }
}
}
}
@@ -117,6 +118,54 @@ extension View {
}
}
/// 弹窗里默认收起的区块:只占一行,左边标题、右边摘要,点一下展开
struct CollapsedSection: View {
let title: String
let summary: String
var summaryColor: Color = DS.Palette.textPrimary
let expand: () -> Void
var body: some View {
Card {
Button(action: expand) {
HStack(spacing: DS.Space.s2) {
Text(title)
.dsFont(.xs, weight: .semibold)
.foregroundStyle(DS.Palette.textSecondary)
.lineLimit(1)
.fixedSize()
Spacer(minLength: DS.Space.s2)
Text(verbatim: summary)
.dsFont(.xs, weight: .medium)
.foregroundStyle(summaryColor)
.monospacedDigit()
.lineLimit(1)
.truncationMode(.middle)
Image(systemName: "chevron.down")
.font(.system(size: DS.TextSize.xs.rawValue, weight: .semibold))
.foregroundStyle(DS.Palette.textTertiary)
}
.frame(height: DS.Size.segmentHeight)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.help(tr("展开\(title)"))
}
}
}
extension View {
/// 弹窗里可收起区块的收起按钮,放在标题栏最右边
func collapseButton(_ section: PopoverSection, settings: AppSettings) -> some View {
HStack(spacing: DS.Space.s1) {
self
MiniIconButton(systemName: "chevron.up", help: tr("收起")) {
withAnimation(DS.Motion.quick) { settings.setExpanded(section, false) }
}
}
}
}
/// 左侧标签、右侧数值的一行
struct InfoRow<Value: View>: View {
let label: String
@@ -428,6 +428,10 @@ public final class AppSettings {
public var hiddenPopoverSections: Set<PopoverSection> {
didSet { defaults.set(hiddenPopoverSections.map(\.rawValue).sorted(), forKey: Keys.hiddenPopoverSections) }
}
/// 弹窗里默认收起、被用户展开的区块(IP 纯净度、DNS);只影响菜单栏弹窗,主窗口始终完整显示
public var expandedPopoverSections: Set<PopoverSection> {
didSet { defaults.set(expandedPopoverSections.map(\.rawValue).sorted(), forKey: Keys.expandedPopoverSections) }
}
public var probeEnabled: Bool {
didSet { defaults.set(probeEnabled, forKey: Keys.probeEnabled) }
}
@@ -518,6 +522,7 @@ public final class AppSettings {
appearance = defaults.string(forKey: Keys.appearance).flatMap(AppearanceMode.init(rawValue:)) ?? .system
cleanPrefersTrash = defaults.bool(forKey: Keys.cleanPrefersTrash)
menuBarLayout = defaults.string(forKey: Keys.menuBarLayout).flatMap(MenuBarLayout.init(rawValue:)) ?? .separate
expandedPopoverSections = Set((defaults.stringArray(forKey: Keys.expandedPopoverSections) ?? []).compactMap(PopoverSection.init(rawValue:)))
hiddenPopoverSections = defaults.stringArray(forKey: Keys.hiddenPopoverSections)
.map { Set($0.compactMap(PopoverSection.init(rawValue:))) } ?? PopoverSection.hiddenByDefault
probeEnabled = defaults.object(forKey: Keys.probeEnabled) as? Bool ?? true
@@ -558,6 +563,12 @@ public final class AppSettings {
func isVisible(_ section: PopoverSection) -> Bool { !hiddenPopoverSections.contains(section) }
func isExpanded(_ section: PopoverSection) -> Bool { expandedPopoverSections.contains(section) }
func setExpanded(_ section: PopoverSection, _ expanded: Bool) {
if expanded { expandedPopoverSections.insert(section) } else { expandedPopoverSections.remove(section) }
}
func setVisible(_ section: PopoverSection, _ visible: Bool) {
if visible { hiddenPopoverSections.remove(section) } else { hiddenPopoverSections.insert(section) }
}
@@ -582,6 +593,7 @@ public final class AppSettings {
static let cleanPrefersTrash = "cleanPrefersTrash"
static let menuBarLayout = "menuBarLayout"
static let hiddenPopoverSections = "hiddenPopoverSections"
static let expandedPopoverSections = "expandedPopoverSections"
static let probeEnabled = "probeEnabled"
static let probeInBackground = "probeInBackground"
static let probeTarget = "probeTarget"
+3 -2
View File
@@ -59,14 +59,15 @@ system language by default; switch it in Settings → General → Language.
<!-- changelog:start -->
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
Latest release **0.3.2** (2026-09-16) · **4** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
Latest release **0.3.2** (2026-09-16) · **5** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
<details open>
<summary><b>2026-09-16</b> · Unreleased · 3 style · 1 added</summary>
<summary><b>2026-09-16</b> · Unreleased · 4 style · 1 added</summary>
**Style**
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
- 菜单栏弹窗改为紧凑排版,参照 Stats:区块不再是灰底卡片,改成一条细分隔线隔开,行距收紧,顶栏按钮缩小;网络弹窗的 IP 纯净度与 DNS 默认收起成一行(纯净度显示分数与等级,DNS 显示正在使用的地址),点一下展开、标题栏右侧的箭头收起,状态会记住;流量图变矮,连接探测改为两行细格。网络弹窗从约 1520pt 降到约 950pt,其他弹窗矮了一到两成。主窗口的详情页保持原样,照常完整显示。
- 连接探测只用来看网络通不通:格子通了是绿色、不通是红色(不再用橙色表示延迟偏高),固定 60 格,弹窗排成 3 行、主窗口排成 2 行;去掉格子下面的延迟、抖动、丢包三项。打开网络详情时固定每秒探测一次,「设置 · 网络」里的“探测间隔”选项随之去掉,后台低频探测仍是每 10 秒。
- 分段切换改为苹果标准样式:灰色圆角底槽,各段等宽铺满,选中的一段是实心蓝色、白字,切换时蓝块滑过去;网络详情的 IPv4 / IPv6 切换同样是灰底蓝选中。
+3 -2
View File
@@ -55,14 +55,15 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
<!-- changelog:start -->
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
最新版本 **0.3.2**(2026-09-16) · 开发中 **4** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
最新版本 **0.3.2**(2026-09-16) · 开发中 **5** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
<details open>
<summary><b>2026-09-16</b> · 未发布 · 样式 3 · 新增 1</summary>
<summary><b>2026-09-16</b> · 未发布 · 样式 4 · 新增 1</summary>
**样式**
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
- 菜单栏弹窗改为紧凑排版,参照 Stats:区块不再是灰底卡片,改成一条细分隔线隔开,行距收紧,顶栏按钮缩小;网络弹窗的 IP 纯净度与 DNS 默认收起成一行(纯净度显示分数与等级,DNS 显示正在使用的地址),点一下展开、标题栏右侧的箭头收起,状态会记住;流量图变矮,连接探测改为两行细格。网络弹窗从约 1520pt 降到约 950pt,其他弹窗矮了一到两成。主窗口的详情页保持原样,照常完整显示。
- 连接探测只用来看网络通不通:格子通了是绿色、不通是红色(不再用橙色表示延迟偏高),固定 60 格,弹窗排成 3 行、主窗口排成 2 行;去掉格子下面的延迟、抖动、丢包三项。打开网络详情时固定每秒探测一次,「设置 · 网络」里的“探测间隔”选项随之去掉,后台低频探测仍是每 10 秒。
- 分段切换改为苹果标准样式:灰色圆角底槽,各段等宽铺满,选中的一段是实心蓝色、白字,切换时蓝块滑过去;网络详情的 IPv4 / IPv6 切换同样是灰底蓝选中。
+1 -1
View File
@@ -13,7 +13,7 @@ settings:
SWIFT_VERSION: "6.0"
MARKETING_VERSION: "0.3.2"
# 构建号四位数字,每次 make build 加一;营销版本只在发布时用 Scripts/version.sh patch / minor 修改
CURRENT_PROJECT_VERSION: "0068"
CURRENT_PROJECT_VERSION: "0070"
MACOSX_DEPLOYMENT_TARGET: "14.0"
# 本地开发使用 ad-hoc 签名;发布时改为 Developer ID(见 docs/PLAN.md)
CODE_SIGN_STYLE: Manual