This commit is contained in:
anlicheng 2026-03-23 14:58:32 +08:00
parent e86cf5e422
commit 03a26b2f31
9 changed files with 335 additions and 19 deletions

View File

@ -13,6 +13,11 @@ struct SystemConfig {
static let version_name = "1.1"
static let build: Int = 123
//
static let channel = "appstore"
static let serverHost = "punchnet.s5s8.com"
// stunip

View File

@ -0,0 +1,90 @@
//
// SDLAPIClient+App.swift
// punchnet
//
// Created by on 2026/3/21.
//
import Foundation
extension SDLAPIClient {
struct AppPoliciesInfo: Codable {
let privacyPolicyUrl: String
let termsOfServiceUrl: String
let privacyPolicyVersion: String
let termsVersion: String
enum CodingKeys: String, CodingKey {
case privacyPolicyUrl = "privacy_policy_url"
case termsOfServiceUrl = "terms_of_service_url"
case privacyPolicyVersion = "privacy_policy_version"
case termsVersion = "terms_version"
}
}
//
struct AppUpgradeInfo: Codable, Identifiable {
var id = UUID().uuidString
let hasUpdate: Bool
let latestVersion: String
let latestBuild: Int
// url
let forceUpdateUrl: String?
let releaseNotes: String
let minSupportedVersion: String
let publishTime: Int
//
var forceUpdate: Bool {
return forceUpdateUrl != nil
}
enum CodingKeys: String, CodingKey {
case hasUpdate = "has_update"
case latestVersion = "latest_version"
case latestBuild = "latest_build"
case forceUpdateUrl = "force_update"
case releaseNotes = "release_notes"
case minSupportedVersion = "min_supported_version"
case publishTime = "publish_time"
}
}
//
static func appIssue(accessToken: String, contact: String, content: String) async throws -> String {
let params: [String: Any] = [
"access_token": accessToken,
"contact": contact,
"platform": SystemConfig.systemInfo,
"content": content,
"client_id": SystemConfig.getClientId(),
"mac": SystemConfig.macAddressString(mac: SystemConfig.getMacAddress())
]
return try await SDLAPIClient.doPost(path: "/app/issue", params: params, as: String.self)
}
//
static func appPolicies() async throws -> AppPoliciesInfo {
let params: [String: Any] = [
"platform": "macos",
"client_id": SystemConfig.getClientId()
]
return try await SDLAPIClient.doPost(path: "/app/policies", params: params, as: AppPoliciesInfo.self)
}
// app
static func appCheckUpdate() async throws -> AppUpgradeInfo {
let params: [String: Any] = [
"app_id": "Punchnet",
"platform": "macos",
"version": SystemConfig.systemInfo,
"build": SystemConfig.build,
"channel": SystemConfig.channel,
"client_id": SystemConfig.getClientId()
]
return try await SDLAPIClient.doPost(path: "/app/checkUpdate", params: params, as: AppUpgradeInfo.self)
}
}

View File

@ -9,16 +9,50 @@ import SwiftUI
struct RootView: View {
@Environment(UserContext.self) var userContext
@State private var updateManager = AppUpdateManager.shared
var body: some View {
Group {
if userContext.isLogined {
NetworkView()
} else {
LoginView()
ZStack {
//
Group {
if userContext.isLogined {
NetworkView()
} else {
LoginView()
}
}
//
if updateManager.showUpdateOverlay, let info = updateManager.updateInfo {
//
Color.black.opacity(0.4)
.ignoresSafeArea()
.onTapGesture {
if !info.forceUpdate {
updateManager.showUpdateOverlay = false
}
}
//
AppUpdateView(info: info) {
updateManager.showUpdateOverlay = false
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.3), radius: 20)
.transition(.asymmetric(
insertion: .scale(scale: 0.9).combined(with: .opacity),
removal: .opacity
))
}
}
.animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay)
.task {
//
let checkUpdateResult = await updateManager.checkUpdate(isManual: false)
NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)")
}
}
}
#Preview {

View File

@ -10,6 +10,12 @@ struct SettingsAboutView: View {
@Environment(\.openURL) private var openURL
@State private var isShowingFeedbackSheet = false
@State private var appPoliciesInfo: SDLAPIClient.AppPoliciesInfo?
//
@State private var updateManager = AppUpdateManager.shared
@State private var showNoUpdateAlert = false
@State private var manualUpdateInfo: SDLAPIClient.AppUpgradeInfo?
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
@ -52,10 +58,14 @@ struct SettingsAboutView: View {
AboutRow(title: "检查更新", icon: "arrow.clockwise.circle") {
Button("立即检查") {
//
Task {@MainActor in
await self.checkAppUpgrade()
}
}
.buttonStyle(.plain)
.foregroundColor(.blue)
.font(.subheadline.bold())
.disabled(updateManager.isChecking)
}
Divider().padding(.leading, 44)
@ -81,7 +91,9 @@ struct SettingsAboutView: View {
.foregroundColor(.secondary)
}
.onTapGesture {
/* */
if let privacyPolicyUrl = self.appPoliciesInfo?.privacyPolicyUrl, let privacyUrl = URL(string: privacyPolicyUrl) {
openURL(privacyUrl)
}
}
Divider().padding(.leading, 44)
@ -92,7 +104,9 @@ struct SettingsAboutView: View {
.foregroundColor(.secondary)
}
.onTapGesture {
/* */
if let termsOfServiceUrl = self.appPoliciesInfo?.termsOfServiceUrl, let termsUrl = URL(string: termsOfServiceUrl) {
openURL(termsUrl)
}
}
}
.background(Color.primary.opacity(0.03))
@ -127,7 +141,32 @@ struct SettingsAboutView: View {
}
.frame(width: 500, height: 600) //
}
// Sheet
.sheet(item: $manualUpdateInfo) { info in
AppUpdateView(info: info) {
self.manualUpdateInfo = nil
}
}
.alert(isPresented: $showNoUpdateAlert) {
Alert(title: Text("检查更新"), message: Text("您当前使用的是最新版本。"))
}
.task {
self.appPoliciesInfo = try? await SDLAPIClient.appPolicies()
_ = try? await SDLAPIClient.appCheckUpdate()
}
}
private func checkAppUpgrade() async {
let hasUpdate = await updateManager.checkUpdate(isManual: true)
if hasUpdate {
// sheet
self.manualUpdateInfo = updateManager.updateInfo
} else {
self.showNoUpdateAlert = true
}
}
}
// MARK: -

View File

@ -18,6 +18,10 @@ struct SettingsUserIssueView: View {
@State private var isSubmitting: Bool = false
@State private var showSuccessToast: Bool = false
//
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""
var body: some View {
ZStack {
//
@ -113,6 +117,9 @@ struct SettingsUserIssueView: View {
successPopup
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(errorMessage))
}
}
// MARK: -
@ -121,7 +128,7 @@ struct SettingsUserIssueView: View {
isSubmitting = true
}
var params: [String: Any] = [
let params: [String: Any] = [
"access_token": self.userContext.networkSession?.accessToken ?? "",
"contact": self.account,
"platform": SystemConfig.systemInfo,
@ -132,15 +139,6 @@ struct SettingsUserIssueView: View {
do {
_ = try await SDLAPIClient.doPost(path: "/app/issue", params: params, as: String.self)
} catch let err as SDLAPIError {
} catch let err {
}
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
isSubmitting = false
showSuccessToast = true
@ -153,7 +151,15 @@ struct SettingsUserIssueView: View {
showSuccessToast = false
}
}
} catch let err as SDLAPIError {
self.showAlert = true
self.errorMessage = err.message
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
}
self.isSubmitting = false
}
// MARK: -

View File

@ -0,0 +1,66 @@
//
// AppUpdateManager.swift
// punchnet
//
// Created by on 2026/3/23.
//
import SwiftUI
import Observation
@Observable
class AppUpdateManager {
static let shared = AppUpdateManager()
var updateInfo: SDLAPIClient.AppUpgradeInfo?
var isChecking = false
var showUpdateOverlay = false //
@MainActor
func checkUpdate(isManual: Bool = false) async -> Bool {
isChecking = true
defer {
isChecking = false
}
do {
let updateInfo = try await SDLAPIClient.appCheckUpdate()
//
let currentVersion = SystemConfig.version_name
let needsUpdate = VersionComparator.isVersion(currentVersion, olderThan: updateInfo.latestVersion)
if needsUpdate {
self.updateInfo = updateInfo
//
if !isManual {
self.showUpdateOverlay = true
}
return true
}
} catch {
print("Update check failed: \(error)")
}
return false
}
}
struct VersionComparator {
/// current < latest true ()
static func isVersion(_ current: String, olderThan latest: String) -> Bool {
let currentComponents = current.split(separator: ".").map { Int($0) ?? 0 }
let latestComponents = latest.split(separator: ".").map { Int($0) ?? 0 }
let maxLength = max(currentComponents.count, latestComponents.count)
for i in 0..<maxLength {
let currentPart = i < currentComponents.count ? currentComponents[i] : 0
let latestPart = i < latestComponents.count ? latestComponents[i] : 0
if currentPart < latestPart {
return true
}
if currentPart > latestPart {
return false
}
}
return false
}
}

View File

@ -0,0 +1,77 @@
//
// AppUpdateView.swift
// punchnet
//
// Created by on 2026/3/23.
//
import SwiftUI
struct AppUpdateView: View {
let info: SDLAPIClient.AppUpgradeInfo
var dismissAction: () -> Void
var body: some View {
VStack(spacing: 0) {
// Header
VStack(spacing: 12) {
Image(systemName: "arrow.up.rocket.fill")
.font(.system(size: 40))
.foregroundStyle(.blue.gradient)
Text("新版本已就绪")
.font(.title3.bold())
Text("版本号: \(info.latestVersion)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.top, 30)
.padding(.bottom, 20)
.frame(maxWidth: .infinity)
.background(Color.blue.opacity(0.05))
//
VStack(alignment: .leading, spacing: 12) {
Text("更新说明")
.font(.subheadline.bold())
ScrollView {
Text(info.releaseNotes)
.font(.subheadline)
.foregroundColor(.secondary)
.lineSpacing(4)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxHeight: 120)
//
HStack(spacing: 12) {
if !info.forceUpdate {
Button("稍后") {
dismissAction()
}
.buttonStyle(.plain)
.frame(width: 80)
}
Button {
if let forceUpdateUrl = info.forceUpdateUrl, let url = URL(string: forceUpdateUrl) {
NSWorkspace.shared.open(url)
}
} label: {
Text(info.forceUpdate ? "立即更新" : "下载并安装")
.fontWeight(.bold)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.padding(.top, 10)
}
.padding(25)
}
.frame(width: 360)
.background(VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow))
}
}

View File

@ -45,8 +45,7 @@ struct punchnetApp: App {
var body: some Scene {
WindowGroup(id: "main") {
//RootView()
SettingsUserIssueView()
RootView()
.navigationTitle("")
.environment(self.appContext)
.environment(self.userContext)