mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-26 00:21:55 +08:00
chore: prepare v1.0.5 release
This commit is contained in:
+173
-10
@@ -1,5 +1,22 @@
|
||||
import SwiftUI
|
||||
|
||||
private enum UpdateCheckResult: Identifiable {
|
||||
case current(currentVersion: String, latestVersion: String)
|
||||
case available(AppUpdatePrompt)
|
||||
case failed
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .current(let currentVersion, let latestVersion):
|
||||
return "current-\(currentVersion)-\(latestVersion)"
|
||||
case .available(let prompt):
|
||||
return "available-\(prompt.id)"
|
||||
case .failed:
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var setup: SetupCoordinator
|
||||
@ObservedObject var actions: LocationActionCoordinator
|
||||
@@ -15,8 +32,11 @@ struct SettingsView: View {
|
||||
@State private var proxyOperationAlertTitle = "代理操作失败"
|
||||
@State private var modeOperationRunning = false
|
||||
@State private var copiedClient: ThirdPartyProxyClient?
|
||||
@State private var copiedMITMHostnames = false
|
||||
@State private var showCertificateResetConfirmation = false
|
||||
@State private var githubDestination: SafariDestination?
|
||||
@State private var isCheckingForUpdates = false
|
||||
@State private var updateCheckResult: UpdateCheckResult?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -66,7 +86,11 @@ struct SettingsView: View {
|
||||
|
||||
Section("定位模拟") {
|
||||
Toggle("运动状态模拟", isOn: motionSimulationBinding)
|
||||
.disabled(modeOperationRunning || actions.state.isBusy || thirdPartyProxy.isRequesting)
|
||||
.disabled(
|
||||
modeOperationRunning ||
|
||||
actions.state.isBusy ||
|
||||
thirdPartyProxy.isRequesting
|
||||
)
|
||||
Text("实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -109,6 +133,19 @@ struct SettingsView: View {
|
||||
Label("进入引导页", systemImage: "arrow.clockwise.circle")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
checkForUpdates()
|
||||
} label: {
|
||||
if isCheckingForUpdates {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("正在检查…")
|
||||
}
|
||||
} else {
|
||||
Label("检查更新", systemImage: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
}
|
||||
.disabled(isCheckingForUpdates)
|
||||
valueRow("版本", value: versionText)
|
||||
}
|
||||
|
||||
@@ -205,6 +242,9 @@ struct SettingsView: View {
|
||||
} message: {
|
||||
Text(proxyOperationError)
|
||||
}
|
||||
.alert(item: $updateCheckResult) { result in
|
||||
updateCheckAlert(for: result)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"重置证书?",
|
||||
isPresented: $showCertificateResetConfirmation,
|
||||
@@ -217,6 +257,16 @@ struct SettingsView: View {
|
||||
} message: {
|
||||
Text("当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。")
|
||||
}
|
||||
.task(id: runtimeMode.mode) {
|
||||
guard runtimeMode.mode == .thirdParty, !modeOperationRunning else { return }
|
||||
if !(await thirdPartyProxy.refreshAdvancedFeatureAvailability()) {
|
||||
disableUnsupportedThirdPartyMotionSimulation()
|
||||
}
|
||||
}
|
||||
.onChange(of: thirdPartyProxy.moduleUpdateRecommended) { updateRecommended in
|
||||
guard updateRecommended else { return }
|
||||
disableUnsupportedThirdPartyMotionSimulation()
|
||||
}
|
||||
}
|
||||
|
||||
private func valueRow(_ title: String, value: String) -> some View {
|
||||
@@ -229,6 +279,71 @@ struct SettingsView: View {
|
||||
return "\(v) (\(b))"
|
||||
}
|
||||
|
||||
private func checkForUpdates() {
|
||||
guard !isCheckingForUpdates else { return }
|
||||
isCheckingForUpdates = true
|
||||
Task { @MainActor in
|
||||
defer { isCheckingForUpdates = false }
|
||||
guard let configuration = await AppRemoteConfigurationService.fetch() else {
|
||||
updateCheckResult = .failed
|
||||
return
|
||||
}
|
||||
AppRemoteConfigurationStore.shared.apply(configuration)
|
||||
let currentVersion = Bundle.main.object(
|
||||
forInfoDictionaryKey: "CFBundleShortVersionString"
|
||||
) as? String ?? AppRemoteConfiguration.fallback.latestVersion
|
||||
guard let pendingPrompt = configuration.updatePrompt(currentVersion: currentVersion) else {
|
||||
updateCheckResult = .current(
|
||||
currentVersion: currentVersion,
|
||||
latestVersion: configuration.latestVersion
|
||||
)
|
||||
return
|
||||
}
|
||||
let releaseNotes = await AppRemoteConfigurationService.fetchReleaseNotes(
|
||||
version: pendingPrompt.latestVersion
|
||||
)
|
||||
let prompt = configuration.updatePrompt(
|
||||
currentVersion: currentVersion,
|
||||
releaseNotes: releaseNotes
|
||||
) ?? pendingPrompt
|
||||
updateCheckResult = .available(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCheckAlert(for result: UpdateCheckResult) -> Alert {
|
||||
switch result {
|
||||
case .current(let currentVersion, let latestVersion):
|
||||
return Alert(
|
||||
title: Text("已是最新版本"),
|
||||
message: Text("当前版本 \(currentVersion),远程最新版本 \(latestVersion)。"),
|
||||
dismissButton: .default(Text("知道了"))
|
||||
)
|
||||
case .available(let prompt):
|
||||
let details = prompt.releaseNotes
|
||||
?? "更新说明暂时无法加载,请前往最新 Release 页面查看。"
|
||||
let message: String
|
||||
if prompt.requirement == .required {
|
||||
message = "当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)"
|
||||
} else {
|
||||
message = "当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)。\n\n\(details)"
|
||||
}
|
||||
return Alert(
|
||||
title: Text(prompt.requirement == .required ? "需要更新" : "发现新版本"),
|
||||
message: Text(message),
|
||||
primaryButton: .default(Text("前往更新")) {
|
||||
UIApplication.shared.open(AppRemoteConfigurationService.releasesURL)
|
||||
},
|
||||
secondaryButton: .cancel(Text("稍后"))
|
||||
)
|
||||
case .failed:
|
||||
return Alert(
|
||||
title: Text("检查更新失败"),
|
||||
message: Text("无法获取远程版本信息,请检查网络后重试。"),
|
||||
dismissButton: .default(Text("知道了"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var proxyBinding: Binding<Bool> {
|
||||
Binding(get: { proxy.isRunning }, set: { on in
|
||||
Task {
|
||||
@@ -267,7 +382,19 @@ struct SettingsView: View {
|
||||
return
|
||||
}
|
||||
guard thirdPartyProxy.activeSettings?.success == true else {
|
||||
motionSimulation.setEnabled(enabled)
|
||||
guard enabled else {
|
||||
motionSimulation.setEnabled(false)
|
||||
return
|
||||
}
|
||||
modeOperationRunning = true
|
||||
Task { @MainActor in
|
||||
if await thirdPartyProxy.refreshAdvancedFeatureAvailability() {
|
||||
motionSimulation.setEnabled(true)
|
||||
} else {
|
||||
presentMotionSimulationModuleUpdateAlert()
|
||||
}
|
||||
modeOperationRunning = false
|
||||
}
|
||||
return
|
||||
}
|
||||
modeOperationRunning = true
|
||||
@@ -283,7 +410,11 @@ struct SettingsView: View {
|
||||
error: error,
|
||||
details: ["当前客户端": thirdPartyClient.selectedClient.name]
|
||||
)
|
||||
setup.requestThirdPartySetup(message: error.localizedDescription)
|
||||
if error as? ThirdPartyProxyError == .moduleOutdated {
|
||||
presentMotionSimulationModuleUpdateAlert()
|
||||
} else {
|
||||
setup.requestThirdPartySetup(message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
modeOperationRunning = false
|
||||
}
|
||||
@@ -311,6 +442,12 @@ struct SettingsView: View {
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if thirdPartyProxy.moduleUpdateRecommended {
|
||||
Text("当前模块版本较旧,基础坐标功能仍可继续使用。重新导入最新模块后可使用版本检测和运动状态模拟。")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
|
||||
if let verificationText = thirdPartyClient.selectedClient.verificationText {
|
||||
HStack {
|
||||
Text("验证状态")
|
||||
@@ -328,6 +465,13 @@ struct SettingsView: View {
|
||||
Label(copiedClient == thirdPartyClient.selectedClient ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = ThirdPartyProxyManager.interceptionHostnamesText
|
||||
copiedMITMHostnames = true
|
||||
} label: {
|
||||
Label(copiedMITMHostnames ? "已复制两个解密域名" : "复制两个解密域名", systemImage: "doc.on.doc")
|
||||
}
|
||||
|
||||
Button {
|
||||
openThirdPartyClient(thirdPartyClient.selectedClient)
|
||||
} label: {
|
||||
@@ -349,7 +493,7 @@ struct SettingsView: View {
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text("复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。")
|
||||
Text("复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并为 gs-loc.apple.com 和 gs-loc-cn.apple.com 启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -387,7 +531,7 @@ struct SettingsView: View {
|
||||
return """
|
||||
App 在设备本地运行一个代理服务器(127.0.0.1:8888)。
|
||||
|
||||
通过 WiFi 手动代理配置,让系统的定位请求(gs-loc.apple.com/clls/wloc)经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
|
||||
通过 WiFi 手动代理配置,让系统发往 gs-loc.apple.com 和 gs-loc-cn.apple.com 的定位请求经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -404,8 +548,8 @@ struct SettingsView: View {
|
||||
runtimeMode.setMode(.thirdParty)
|
||||
if runtimeMode.isInitialized(.thirdParty) {
|
||||
do {
|
||||
_ = try await thirdPartyProxy.validateVersion()
|
||||
_ = try await thirdPartyProxy.query()
|
||||
_ = try await thirdPartyProxy.validateConnection()
|
||||
refreshThirdPartyAdvancedFeatures()
|
||||
proxyOperationAlertTitle = "模式已切换"
|
||||
proxyOperationError = "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。"
|
||||
} catch {
|
||||
@@ -447,8 +591,8 @@ struct SettingsView: View {
|
||||
let startedAt = Date()
|
||||
Task { @MainActor in
|
||||
do {
|
||||
_ = try await thirdPartyProxy.validateVersion()
|
||||
_ = try await thirdPartyProxy.query()
|
||||
_ = try await thirdPartyProxy.validateConnection()
|
||||
refreshThirdPartyAdvancedFeatures()
|
||||
RuntimeLogger.info("APP", "ThirdPartyProxy", "设置页第三方连接检测通过", details: [
|
||||
"当前客户端": client.name,
|
||||
"请求动作": "WLOC query",
|
||||
@@ -466,7 +610,7 @@ struct SettingsView: View {
|
||||
"请求动作": "WLOC query",
|
||||
"连接状态": String(describing: thirdPartyProxy.connectionState),
|
||||
"耗时毫秒": String(Int(Date().timeIntervalSince(startedAt) * 1_000)),
|
||||
"处理建议": "检查模块、MITM、证书和代理/VPN连接"
|
||||
"处理建议": ThirdPartyProxyError.recoverySuggestion(for: error)
|
||||
]
|
||||
)
|
||||
openThirdPartySetup(for: error)
|
||||
@@ -474,6 +618,25 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshThirdPartyAdvancedFeatures() {
|
||||
Task { @MainActor in
|
||||
if !(await thirdPartyProxy.refreshAdvancedFeatureAvailability()) {
|
||||
disableUnsupportedThirdPartyMotionSimulation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentMotionSimulationModuleUpdateAlert() {
|
||||
disableUnsupportedThirdPartyMotionSimulation()
|
||||
proxyOperationAlertTitle = "无法开启运动状态模拟"
|
||||
proxyOperationError = "当前模块脚本不支持运动状态模拟,请重新导入最新模块脚本后再开启。基础坐标功能仍可继续使用。"
|
||||
}
|
||||
|
||||
private func disableUnsupportedThirdPartyMotionSimulation() {
|
||||
guard runtimeMode.mode == .thirdParty else { return }
|
||||
motionSimulation.setEnabled(false)
|
||||
}
|
||||
|
||||
private func openThirdPartySetup(for error: Error) {
|
||||
setup.requestThirdPartySetup(message: error.localizedDescription)
|
||||
dismiss()
|
||||
|
||||
Reference in New Issue
Block a user