侧边栏选中改为滑动的蓝色光条:细线底轨、发光指示与渐淡高亮,切换带回弹动画
This commit is contained in:
1 parent
0342074489
commit
0829f425cb
7 files changed
+80
-12
No files matched your search
@@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
#### 样式
|
#### 样式
|
||||||
|
|
||||||
|
- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
|
||||||
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
|
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
|
||||||
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
|
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
|
||||||
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
|
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ struct IconButton: View {
|
|||||||
|
|
||||||
// MARK: - 侧边栏
|
// MARK: - 侧边栏
|
||||||
|
|
||||||
|
/// 侧边栏的一项。选中态不填色:由容器上的 `sidebarGlider()` 画左侧滑动的蓝色光条与淡淡的高亮
|
||||||
struct SidebarButton: View {
|
struct SidebarButton: View {
|
||||||
let title: String
|
let title: String
|
||||||
let symbol: String
|
let symbol: String
|
||||||
@@ -227,20 +228,82 @@ struct SidebarButton: View {
|
|||||||
Text(title).dsFont(.sm, weight: isSelected ? .semibold : .regular)
|
Text(title).dsFont(.sm, weight: isSelected ? .semibold : .regular)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.foregroundStyle(isSelected ? DS.Palette.onPrimary : DS.Palette.textPrimary)
|
.foregroundStyle(isSelected ? DS.Palette.primary : hovering ? DS.Palette.textPrimary : DS.Palette.textSecondary)
|
||||||
.padding(.horizontal, DS.Space.s2)
|
.padding(.leading, DS.Space.s3)
|
||||||
|
.padding(.trailing, DS.Space.s2)
|
||||||
.frame(height: DS.Size.controlHeight)
|
.frame(height: DS.Size.controlHeight)
|
||||||
.background(background, in: RoundedRectangle(cornerRadius: DS.Radius.md))
|
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
.animation(DS.Motion.quick, value: isSelected)
|
||||||
|
.animation(DS.Motion.quick, value: hovering)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.onHover { hovering = $0 }
|
.onHover { hovering = $0 }
|
||||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||||
|
.anchorPreference(key: SidebarRowsKey.self, value: .bounds) { [SidebarRow(isSelected: isSelected, bounds: $0)] }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var background: Color {
|
struct SidebarRow {
|
||||||
if isSelected { return DS.Palette.primary }
|
let isSelected: Bool
|
||||||
return hovering ? DS.Palette.surfaceHover : .clear
|
let bounds: Anchor<CGRect>
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SidebarRowsKey: PreferenceKey {
|
||||||
|
static let defaultValue: [SidebarRow] = []
|
||||||
|
static func reduce(value: inout [SidebarRow], nextValue: () -> [SidebarRow]) { value += nextValue() }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// 放在包含 SidebarButton 的容器上:左侧一条两端渐隐的细线,选中项位置有一段发光的蓝色光条,
|
||||||
|
/// 切换时带回弹地滑过去,光条右侧拖出一片渐淡的蓝色高亮
|
||||||
|
func sidebarGlider() -> some View {
|
||||||
|
overlayPreferenceValue(SidebarRowsKey.self) { rows in
|
||||||
|
GeometryReader { proxy in
|
||||||
|
SidebarGlider(frames: rows.map { proxy[$0.bounds] },
|
||||||
|
selected: rows.first(where: \.isSelected).map { proxy[$0.bounds] })
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SidebarGlider: View {
|
||||||
|
let frames: [CGRect]
|
||||||
|
let selected: CGRect?
|
||||||
|
|
||||||
|
private static let barWidth: CGFloat = 2
|
||||||
|
private static let glowWidth = DS.Space.s1 + DS.Space.s1 / 2
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let first = frames.min(by: { $0.minY < $1.minY }), let last = frames.max(by: { $0.maxY < $1.maxY }) {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
// 底轨:整列的细线,上下两端渐隐
|
||||||
|
Rectangle()
|
||||||
|
.fill(LinearGradient(colors: [.clear, DS.Palette.border, DS.Palette.border, .clear], startPoint: .top, endPoint: .bottom))
|
||||||
|
.frame(width: DS.Size.stroke, height: last.maxY - first.minY)
|
||||||
|
.offset(x: first.minX, y: first.minY)
|
||||||
|
|
||||||
|
if let selected {
|
||||||
|
Rectangle()
|
||||||
|
.fill(LinearGradient(colors: [DS.Palette.primary.opacity(0.14), DS.Palette.primary.opacity(0)],
|
||||||
|
startPoint: .leading, endPoint: .trailing))
|
||||||
|
.frame(width: selected.width, height: selected.height)
|
||||||
|
.offset(x: selected.minX, y: selected.minY)
|
||||||
|
Capsule()
|
||||||
|
.fill(DS.Palette.primary)
|
||||||
|
.frame(width: Self.glowWidth, height: selected.height * 0.6)
|
||||||
|
.blur(radius: DS.Space.s2)
|
||||||
|
.offset(x: selected.minX - Self.glowWidth / 2, y: selected.midY - selected.height * 0.3)
|
||||||
|
Rectangle()
|
||||||
|
.fill(LinearGradient(colors: [DS.Palette.primary.opacity(0), DS.Palette.primary, DS.Palette.primary.opacity(0)],
|
||||||
|
startPoint: .top, endPoint: .bottom))
|
||||||
|
.frame(width: Self.barWidth, height: selected.height)
|
||||||
|
.offset(x: selected.minX - Self.barWidth / 2, y: selected.minY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
.animation(.spring(response: 0.45, dampingFraction: 0.6), value: selected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ private struct MainSidebar: View {
|
|||||||
IconButton(systemName: "power", help: "退出 OpenStats") { model.quit() }
|
IconButton(systemName: "power", help: "退出 OpenStats") { model.quit() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sidebarGlider()
|
||||||
.padding(.horizontal, DS.Space.s3)
|
.padding(.horizontal, DS.Space.s3)
|
||||||
// 顶部留出窗口红绿灯按钮的位置
|
// 顶部留出窗口红绿灯按钮的位置
|
||||||
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
|
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.sidebarGlider()
|
||||||
.padding(.horizontal, DS.Space.s3)
|
.padding(.horizontal, DS.Space.s3)
|
||||||
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
|
.padding(.top, DS.Size.windowHeader + DS.Space.s2)
|
||||||
.padding(.bottom, DS.Space.s3)
|
.padding(.bottom, DS.Space.s3)
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ Developed and tested on Apple Silicon. The interface is in Simplified Chinese.
|
|||||||
<!-- changelog:start -->
|
<!-- changelog:start -->
|
||||||
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
|
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
|
||||||
|
|
||||||
Not released yet · **45** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
|
Not released yet · **46** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><b>2026-09-13</b> · Unreleased · 30 added · 9 style · 6 fixed</summary>
|
<summary><b>2026-09-13</b> · Unreleased · 30 added · 10 style · 6 fixed</summary>
|
||||||
|
|
||||||
**Added**
|
**Added**
|
||||||
|
|
||||||
@@ -86,6 +86,7 @@ Not released yet · **45** changes in development · [full changelog](CHANGELOG.
|
|||||||
|
|
||||||
**Style**
|
**Style**
|
||||||
|
|
||||||
|
- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
|
||||||
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
|
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
|
||||||
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
|
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
|
||||||
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
|
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
|
||||||
|
|||||||
+3
-2
@@ -44,10 +44,10 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/
|
|||||||
<!-- changelog:start -->
|
<!-- changelog:start -->
|
||||||
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
|
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
|
||||||
|
|
||||||
尚未发布正式版本 · 开发中 **45** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
|
尚未发布正式版本 · 开发中 **46** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><b>2026-09-13</b> · 未发布 · 新增 30 · 样式 9 · 修复 6</summary>
|
<summary><b>2026-09-13</b> · 未发布 · 新增 30 · 样式 10 · 修复 6</summary>
|
||||||
|
|
||||||
**新增**
|
**新增**
|
||||||
|
|
||||||
@@ -84,6 +84,7 @@ OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https:/
|
|||||||
|
|
||||||
**样式**
|
**样式**
|
||||||
|
|
||||||
|
- 主窗口与设置窗口的侧边栏选中样式:左侧一条两端渐隐的细线,选中项有发光的蓝色光条,切换时带回弹地滑过去,右侧拖出渐淡的蓝色高亮;不再整块填蓝。
|
||||||
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
|
- 仪表盘“快捷开关”改为开关样式:防休眠、合盖运行、散热模式各一行,右侧开关,下面一行小字说明当前状态。
|
||||||
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
|
- 进程表按窗口宽度自动隐藏次要列,窄窗口里名称列不会被挤没。
|
||||||
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
|
- 内存详情的“释放内存”移到标题栏,执行中与完成后在按钮上显示进度和结果;主窗口内存页顶栏同样提供。
|
||||||
|
|||||||
+2
-2
@@ -348,7 +348,7 @@
|
|||||||
<div class="log">
|
<div class="log">
|
||||||
<article class="log__day">
|
<article class="log__day">
|
||||||
<div class="log__head"><time datetime="2026-09-13">2026-09-13</time><span class="log__tag">未发布</span></div>
|
<div class="log__head"><time datetime="2026-09-13">2026-09-13</time><span class="log__tag">未发布</span></div>
|
||||||
<p class="log__counts">新增 30 · 样式 9 · 修复 6</p>
|
<p class="log__counts">新增 30 · 样式 10 · 修复 6</p>
|
||||||
<ul class="log__list">
|
<ul class="log__list">
|
||||||
<li><span class="log__kind">新增</span>菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。</li>
|
<li><span class="log__kind">新增</span>菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。</li>
|
||||||
<li><span class="log__kind">新增</span>菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。</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>网络详情:上下镜像的流量历史;连接探测格子(每 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>
|
<li><span class="log__kind">新增</span>DNS:显示正在使用的 DNS 与配置方式,一键刷新 DNS 缓存,一键切换为自动、Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写地址(逐个校验为 IPv4 / IPv6)。流量经过 VPN / 代理时提示 DNS 可能被接管。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a class="log__more" href="https://github.com/gentpan/OpenStats/blob/main/CHANGELOG.md">还有 37 项 →</a>
|
<a class="log__more" href="https://github.com/gentpan/OpenStats/blob/main/CHANGELOG.md">还有 38 项 →</a>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<!-- changelog:end -->
|
<!-- changelog:end -->
|
||||||
|
|||||||
Reference in new issue
Block a user