Add GitHub update checker and installer

This commit is contained in:
世界
2026-03-30 23:02:42 +08:00
parent 6b790c7a80
commit 63d1d0fe3f
15 changed files with 1663 additions and 2 deletions
@@ -31,6 +31,12 @@ public struct AppView: View {
@State private var menuBarExtraInBackground = false
@State private var helperStatusLoaded = false
@State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var updateManager: UpdateManager
@State private var updateTrack: UpdateTrack = .stable
@State private var checkUpdateEnabled = false
@State private var cacheSize: Int64 = 0
@State private var cacheSizeText = ""
#endif
@State private var alert: AlertState?
@@ -91,6 +97,111 @@ public struct AppView: View {
}
if Variant.useSystemExtension {
FormTextItem("Cache Size", cacheSizeText)
if cacheSize > 0 {
// Safe: System Extension's working directory is in its own container
// (/var/root/Library/Containers/), not under the app's cacheDirectory.
FormButton(role: .destructive) {
Task.detached {
let cacheDir = FilePath.cacheDirectory
if let contents = try? FileManager.default.contentsOfDirectory(
at: cacheDir,
includingPropertiesForKeys: nil
) {
for item in contents {
try? FileManager.default.removeItem(at: item)
}
}
await MainActor.run {
cacheSize = 0
cacheSizeText = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
}
}
} label: {
Label("Clear Cache", systemImage: "trash")
.foregroundColor(.red)
}
}
}
if Variant.useSystemExtension {
Section("Update Settings") {
Picker("Update Track", selection: $updateTrack) {
Text("Stable").tag(UpdateTrack.stable)
Text("Beta").tag(UpdateTrack.beta)
}
.onChangeCompat(of: updateTrack) { newValue in
Task {
await updateManager.updateTrackChanged(to: newValue)
}
}
Toggle("Automatic Update Check", isOn: $checkUpdateEnabled)
.onChangeCompat(of: checkUpdateEnabled) { newValue in
Task {
await SharedPreferences.checkUpdateEnabled.set(newValue)
}
}
FormButton {
Task {
do {
if try await updateManager.refreshUpdateInfo() != nil {
await updateManager.showUpdateSheet()
} else {
alert = AlertState(
title: String(localized: "Check Update"),
message: String(localized: "No updates available")
)
}
} catch {}
}
} label: {
if updateManager.isChecking {
HStack(spacing: 6) {
ProgressView()
.controlSize(.small)
Text("Checking...")
}
} else {
Label("Check Update", systemImage: "arrow.triangle.2.circlepath")
}
}
.disabled(updateManager.isChecking)
.contextMenu {
Button("Force Show Latest Version as Update") {
Task {
do {
if try await updateManager.refreshUpdateInfo(force: true) != nil {
await updateManager.showUpdateSheet()
} else {
alert = AlertState(
title: String(localized: "Check Update"),
message: String(localized: "No updates available")
)
}
} catch {}
}
}
.disabled(updateManager.isChecking)
}
if let info = updateManager.updateInfo {
FormButton {
Task {
await updateManager.showUpdateSheet()
}
} label: {
HStack {
Label("Update", systemImage: "arrow.down.circle")
Spacer()
Text("v\(info.versionName)")
.foregroundStyle(.secondary)
}
}
}
}
Section("System Extension") {
FormButton {
Task {
@@ -163,7 +274,10 @@ public struct AppView: View {
}
}
.alert($alert)
.navigationTitle("App")
#if os(macOS)
.alert($updateManager.alert)
#endif
.navigationTitle("App")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
@@ -174,12 +288,18 @@ public struct AppView: View {
#if os(macOS)
startAtLogin = SMAppService.mainApp.status == .enabled
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
if Variant.useSystemExtension {
let trackString = await SharedPreferences.updateTrack.get()
updateTrack = UpdateTrack.resolved(from: trackString)
checkUpdateEnabled = await SharedPreferences.checkUpdateEnabled.get()
}
#endif
isLoading = false
#if os(macOS)
if Variant.useSystemExtension {
refreshHelperStatus()
helperStatusLoaded = true
refreshCacheSize()
}
#endif
}
@@ -332,5 +452,32 @@ public struct AppView: View {
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/System Preferences.app"))
}
private func refreshCacheSize() {
Task.detached {
let size = Self.calculateDirSize(FilePath.cacheDirectory)
await MainActor.run {
cacheSize = size
cacheSizeText = ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
}
}
}
private static func calculateDirSize(_ dir: URL) -> Int64 {
guard let enumerator = FileManager.default.enumerator(
at: dir,
includingPropertiesForKeys: [.fileSizeKey],
options: [.skipsHiddenFiles]
) else {
return 0
}
var size: Int64 = 0
for case let fileURL as URL in enumerator {
if let fileSize = try? fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize {
size += Int64(fileSize)
}
}
return size
}
#endif
}