fix view
This commit is contained in:
parent
e86cf5e422
commit
03a26b2f31
@ -13,6 +13,11 @@ struct SystemConfig {
|
|||||||
|
|
||||||
static let version_name = "1.1"
|
static let version_name = "1.1"
|
||||||
|
|
||||||
|
static let build: Int = 123
|
||||||
|
|
||||||
|
// 渠道相关
|
||||||
|
static let channel = "appstore"
|
||||||
|
|
||||||
static let serverHost = "punchnet.s5s8.com"
|
static let serverHost = "punchnet.s5s8.com"
|
||||||
|
|
||||||
// stun探测辅助服务器ip
|
// stun探测辅助服务器ip
|
||||||
|
|||||||
90
punchnet/Networking/SDLAPIClient+App.swift
Normal file
90
punchnet/Networking/SDLAPIClient+App.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -9,16 +9,50 @@ import SwiftUI
|
|||||||
|
|
||||||
struct RootView: View {
|
struct RootView: View {
|
||||||
@Environment(UserContext.self) var userContext
|
@Environment(UserContext.self) var userContext
|
||||||
|
@State private var updateManager = AppUpdateManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
ZStack {
|
||||||
if userContext.isLogined {
|
// 主要界面
|
||||||
NetworkView()
|
Group {
|
||||||
} else {
|
if userContext.isLogined {
|
||||||
LoginView()
|
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 {
|
#Preview {
|
||||||
|
|||||||
@ -10,7 +10,13 @@ struct SettingsAboutView: View {
|
|||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
@State private var isShowingFeedbackSheet = false
|
@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 {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: 32) {
|
VStack(alignment: .leading, spacing: 32) {
|
||||||
@ -52,10 +58,14 @@ struct SettingsAboutView: View {
|
|||||||
AboutRow(title: "检查更新", icon: "arrow.clockwise.circle") {
|
AboutRow(title: "检查更新", icon: "arrow.clockwise.circle") {
|
||||||
Button("立即检查") {
|
Button("立即检查") {
|
||||||
// 检查更新逻辑
|
// 检查更新逻辑
|
||||||
|
Task {@MainActor in
|
||||||
|
await self.checkAppUpgrade()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
.font(.subheadline.bold())
|
.font(.subheadline.bold())
|
||||||
|
.disabled(updateManager.isChecking)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider().padding(.leading, 44)
|
Divider().padding(.leading, 44)
|
||||||
@ -81,7 +91,9 @@ struct SettingsAboutView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
/* 跳转隐私协议 */
|
if let privacyPolicyUrl = self.appPoliciesInfo?.privacyPolicyUrl, let privacyUrl = URL(string: privacyPolicyUrl) {
|
||||||
|
openURL(privacyUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider().padding(.leading, 44)
|
Divider().padding(.leading, 44)
|
||||||
@ -92,7 +104,9 @@ struct SettingsAboutView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
/* 跳转服务条款 */
|
if let termsOfServiceUrl = self.appPoliciesInfo?.termsOfServiceUrl, let termsUrl = URL(string: termsOfServiceUrl) {
|
||||||
|
openURL(termsUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.primary.opacity(0.03))
|
.background(Color.primary.opacity(0.03))
|
||||||
@ -127,7 +141,32 @@ struct SettingsAboutView: View {
|
|||||||
}
|
}
|
||||||
.frame(width: 500, height: 600) // 设置弹窗大小
|
.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: - 复用之前的行组件
|
// MARK: - 复用之前的行组件
|
||||||
|
|||||||
@ -18,6 +18,10 @@ struct SettingsUserIssueView: View {
|
|||||||
@State private var isSubmitting: Bool = false
|
@State private var isSubmitting: Bool = false
|
||||||
@State private var showSuccessToast: Bool = false
|
@State private var showSuccessToast: Bool = false
|
||||||
|
|
||||||
|
// 错误提示
|
||||||
|
@State private var showAlert: Bool = false
|
||||||
|
@State private var errorMessage: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// 主滚动视图
|
// 主滚动视图
|
||||||
@ -113,6 +117,9 @@ struct SettingsUserIssueView: View {
|
|||||||
successPopup
|
successPopup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(isPresented: $showAlert) {
|
||||||
|
Alert(title: Text("提示"), message: Text(errorMessage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 提交逻辑
|
// MARK: - 提交逻辑
|
||||||
@ -121,7 +128,7 @@ struct SettingsUserIssueView: View {
|
|||||||
isSubmitting = true
|
isSubmitting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var params: [String: Any] = [
|
let params: [String: Any] = [
|
||||||
"access_token": self.userContext.networkSession?.accessToken ?? "",
|
"access_token": self.userContext.networkSession?.accessToken ?? "",
|
||||||
"contact": self.account,
|
"contact": self.account,
|
||||||
"platform": SystemConfig.systemInfo,
|
"platform": SystemConfig.systemInfo,
|
||||||
@ -132,15 +139,6 @@ struct SettingsUserIssueView: View {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try await SDLAPIClient.doPost(path: "/app/issue", params: params, as: String.self)
|
_ = 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)) {
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
|
||||||
isSubmitting = false
|
isSubmitting = false
|
||||||
showSuccessToast = true
|
showSuccessToast = true
|
||||||
@ -153,7 +151,15 @@ struct SettingsUserIssueView: View {
|
|||||||
showSuccessToast = false
|
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: - 成功弹窗视图
|
// MARK: - 成功弹窗视图
|
||||||
|
|||||||
66
punchnet/Views/Update/AppUpdateManager.swift
Normal file
66
punchnet/Views/Update/AppUpdateManager.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
77
punchnet/Views/Update/AppUpdateView.swift
Normal file
77
punchnet/Views/Update/AppUpdateView.swift
Normal 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -45,8 +45,7 @@ struct punchnetApp: App {
|
|||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup(id: "main") {
|
WindowGroup(id: "main") {
|
||||||
//RootView()
|
RootView()
|
||||||
SettingsUserIssueView()
|
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
.environment(self.appContext)
|
.environment(self.appContext)
|
||||||
.environment(self.userContext)
|
.environment(self.userContext)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user