204 lines
8.6 KiB
Swift
204 lines
8.6 KiB
Swift
import AppKit
|
|
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("更新内容").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: "OpenStats \(release.version) 已发布")
|
|
.dsFont(.lg, weight: .semibold)
|
|
.foregroundStyle(DS.Palette.textPrimary)
|
|
Text(verbatim: "当前版本 \(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("查看完整更新日志").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("正在下载").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: "正在核对校验值、开发者签名与 Apple 公证…")
|
|
case .installing:
|
|
InfoBanner(icon: "arrow.triangle.2.circlepath", text: "正在安装,完成后 OpenStats 会自动重启。")
|
|
case .failed(let message):
|
|
InfoBanner(icon: "exclamationmark.triangle", text: message, tone: .error) {
|
|
if updates.release != nil {
|
|
Button("手动下载") { 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("取消") { updates.cancel() }.buttonStyle(DSButtonStyle(kind: .secondary))
|
|
} else {
|
|
Button("跳过此版本") {
|
|
updates.skipCurrentRelease()
|
|
close()
|
|
}
|
|
.buttonStyle(DSButtonStyle(kind: .ghost))
|
|
Spacer()
|
|
Button("以后再说", action: close).buttonStyle(DSButtonStyle(kind: .secondary))
|
|
Button("一键安装") { 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)
|
|
}
|
|
}
|
|
|
|
/// 设置 · 关于 里的“软件更新”分组
|
|
struct UpdateSettings: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
@Bindable var settings = model.settings
|
|
let updates = model.updates
|
|
|
|
SettingsGroup(caption: "软件更新") {
|
|
GroupRow(showsDivider: false) {
|
|
SettingRow(title: statusTitle, subtitle: statusSubtitle) {
|
|
Button(updates.phase == .checking ? "正在检查…" : "检查更新") { updates.check(userInitiated: true) }
|
|
.buttonStyle(DSButtonStyle(kind: .secondary))
|
|
.disabled(updates.isBusy)
|
|
}
|
|
}
|
|
GroupRow {
|
|
SettingRow(title: "自动检查更新", subtitle: "启动时和之后每天检查一次,发现新版本时显示更新摘要") {
|
|
DSToggle(isOn: $settings.autoCheckUpdates, label: "自动检查更新")
|
|
}
|
|
}
|
|
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("取消") { updates.cancel() }.buttonStyle(DSButtonStyle(kind: .secondary))
|
|
} else {
|
|
Button("一键安装") { 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 "正在检查更新"
|
|
case .upToDate: return "已是最新版本"
|
|
case .failed where updates.release == nil: return "检查更新失败"
|
|
default: return updates.release.map { "发现新版本 \($0.version)" } ?? "当前版本 \(updates.currentVersion)"
|
|
}
|
|
}
|
|
|
|
private var statusSubtitle: String {
|
|
let checked = model.updates.lastChecked.map { "上次检查:\($0.formatted(.relative(presentation: .named)))" } ?? "尚未检查"
|
|
guard let release = model.updates.release else { return checked }
|
|
let size = ByteCountFormatter.string(fromByteCount: release.size, countStyle: .file)
|
|
return "当前 \(model.updates.currentVersion) · 发布于 \(release.date) · \(size) · \(checked)"
|
|
}
|
|
}
|