Files
OpenStats/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/UpdateViews.swift
T
gentpan c9d722f20d 账号同步、磁盘工具、IP 归属地数据源与纯净度、弹窗与设置调整
- 账号与同步: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 与译文同步
2026-09-15 03:37:24 +05:00

206 lines
8.8 KiB
Swift

import AppKit
import Localization
import SwiftUI
import Updates
/// 新版本信息:版本号、日期、体积与更新摘要,升级提示窗口与“关于”页共用
struct ReleaseSummary: View {
let release: UpdateRelease
let currentVersion: String
/// 升级提示窗口里限制摘要区高度,超出时滚动;关于页里完整展开
var notesHeight: CGFloat?
var showsHeader = true
var body: some View {
VStack(alignment: .leading, spacing: DS.Space.s3) {
if showsHeader { header }
Text(tr("更新内容")).dsFont(.xs, weight: .medium).foregroundStyle(DS.Palette.textSecondary)
notes
}
}
private var header: some View {
HStack(spacing: DS.Space.s3) {
AppGlyph(size: DS.Space.s12)
VStack(alignment: .leading, spacing: DS.Space.s1) {
Text(verbatim: tr("OpenStats \(release.version) 已发布"))
.dsFont(.lg, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
Text(verbatim: tr("当前版本 \(currentVersion) · \(release.date) · \(ByteCountFormatter.string(fromByteCount: release.size, countStyle: .file))"))
.dsFont(.xs)
.foregroundStyle(DS.Palette.textSecondary)
}
Spacer(minLength: 0)
}
}
@ViewBuilder
private var notes: some View {
let list = VStack(alignment: .leading, spacing: DS.Space.s2) {
ForEach(Array(release.notes.enumerated()), id: \.offset) { _, note in
HStack(alignment: .firstTextBaseline, spacing: DS.Space.s2) {
Circle().fill(DS.Palette.primary).frame(width: DS.Space.s1 + DS.Space.s1 / 2, height: DS.Space.s1 + DS.Space.s1 / 2)
.alignmentGuide(.firstTextBaseline) { $0[VerticalAlignment.center] + DS.Space.s1 }
Text(note).dsFont(.sm).foregroundStyle(DS.Palette.textPrimary)
.fixedSize(horizontal: false, vertical: true)
}
}
if let changelog = release.changelog {
Button { NSWorkspace.shared.open(changelog) } label: {
Text(tr("查看完整更新日志")).dsFont(.sm, weight: .medium).foregroundStyle(DS.Palette.primary)
}
.buttonStyle(.plain)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if let notesHeight {
ViewThatFits(in: .vertical) {
list.padding(DS.Space.s3)
ScrollView { list.padding(DS.Space.s3).overlayScrollers() }
}
.frame(maxHeight: notesHeight)
.background(DS.Palette.surface, in: RoundedRectangle(cornerRadius: DS.Radius.md))
} else {
list
}
}
}
/// 下载 / 校验 / 安装的进度与失败提示
struct UpdateProgress: View {
@Environment(AppModel.self) private var model
var body: some View {
let updates = model.updates
switch updates.phase {
case .downloading(let fraction):
VStack(alignment: .leading, spacing: DS.Space.s2) {
HStack {
Text(tr("正在下载")).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
Spacer()
Text(verbatim: "\(Int((fraction * 100).rounded()))%").dsFont(.xs, weight: .medium).monospacedDigit()
.foregroundStyle(DS.Palette.textPrimary)
}
ProgressTrack(fraction: fraction)
}
case .verifying:
InfoBanner(icon: "checkmark.shield", text: tr("正在核对校验值、开发者签名与 Apple 公证…"))
case .installing:
InfoBanner(icon: "arrow.triangle.2.circlepath", text: tr("正在安装,完成后 OpenStats 会自动重启。"))
case .failed(let message):
InfoBanner(icon: "exclamationmark.triangle", text: message, tone: .error) {
if updates.release != nil {
Button(tr("手动下载")) { updates.openManualDownload() }
.buttonStyle(DSButtonStyle(kind: .secondary))
}
}
default:
EmptyView()
}
}
}
/// 自动检查发现新版本时弹出的窗口
struct UpdatePromptView: View {
@Environment(AppModel.self) private var model
let close: () -> Void
var body: some View {
let updates = model.updates
VStack(alignment: .leading, spacing: DS.Space.s4) {
if let release = updates.release {
ReleaseSummary(release: release, currentVersion: updates.currentVersion, notesHeight: DS.Size.updateNotesHeight)
}
UpdateProgress()
HStack(spacing: DS.Space.s2) {
if case .downloading = updates.phase {
Spacer()
Button(tr("取消")) { updates.cancel() }.buttonStyle(DSButtonStyle(kind: .secondary))
} else {
Button(tr("跳过此版本")) {
updates.skipCurrentRelease()
close()
}
.buttonStyle(DSButtonStyle(kind: .ghost))
Spacer()
Button(tr("以后再说"), action: close).buttonStyle(DSButtonStyle(kind: .secondary))
Button(tr("一键安装")) { updates.install() }
.buttonStyle(DSButtonStyle(kind: .primary))
.keyboardShortcut(.defaultAction)
}
}
.disabled(updates.phase == .verifying || updates.phase == .installing)
}
.padding(.horizontal, DS.Space.s6)
.padding(.bottom, DS.Space.s6)
.padding(.top, DS.Size.windowHeader)
.frame(width: DS.Size.updateWindowWidth)
.background(DS.Palette.background)
.id(model.settings.language)
}
}
/// 设置 · 关于 里的“软件更新”分组
struct UpdateSettings: View {
@Environment(AppModel.self) private var model
var body: some View {
@Bindable var settings = model.settings
let updates = model.updates
SettingsGroup(caption: tr("软件更新")) {
GroupRow(showsDivider: false) {
SettingRow(title: statusTitle, subtitle: statusSubtitle) {
Button(updates.phase == .checking ? tr("正在检查…") : tr("检查更新")) { updates.check(userInitiated: true) }
.buttonStyle(DSButtonStyle(kind: .secondary))
.disabled(updates.isBusy)
}
}
GroupRow {
SettingRow(title: tr("自动检查更新"), subtitle: tr("启动时和之后每天检查一次,发现新版本时显示更新摘要")) {
DSToggle(isOn: $settings.autoCheckUpdates, label: tr("自动检查更新"))
}
}
if let release = updates.release {
GroupRow {
VStack(alignment: .leading, spacing: DS.Space.s3) {
ReleaseSummary(release: release, currentVersion: updates.currentVersion, showsHeader: false)
UpdateProgress()
HStack {
Spacer()
if case .downloading = updates.phase {
Button(tr("取消")) { updates.cancel() }.buttonStyle(DSButtonStyle(kind: .secondary))
} else {
Button(tr("一键安装")) { updates.install() }
.buttonStyle(DSButtonStyle(kind: .primary))
.disabled(updates.isBusy)
}
}
}
}
} else if case .failed = updates.phase {
GroupRow { UpdateProgress() }
}
}
}
private var statusTitle: String {
let updates = model.updates
switch updates.phase {
case .checking: return tr("正在检查更新")
case .upToDate: return tr("已是最新版本")
case .failed where updates.release == nil: return tr("检查更新失败")
default: return updates.release.map { tr("发现新版本 \($0.version)") } ?? tr("当前版本 \(updates.currentVersion)")
}
}
private var statusSubtitle: String {
let checked = model.updates.lastChecked.map { tr("上次检查:\($0.formatted(.relative(presentation: .named).locale(L10n.locale)))") } ?? tr("尚未检查")
guard let release = model.updates.release else { return checked }
let size = ByteCountFormatter.string(fromByteCount: release.size, countStyle: .file)
return tr("当前 \(model.updates.currentVersion) · 发布于 \(release.date) · \(size) · \(checked)")
}
}