mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
Add English as an overlay localization following the device language,
keeping Simplified Chinese as the base/development language. Untranslated
strings fall back to Chinese automatically.
- project.yml: set developmentLanguage zh-Hans and knownRegions (zh-Hans/en/Base)
- Resources/en.lproj/{Localizable,InfoPlist}.strings: ~230 English strings
- Resources/zh-Hans.lproj/InfoPlist.strings: Chinese permission prompts
- Restructure ternary Text/Label and String-typed UI messages so they
localize (String(localized:), LocalizedStringKey helper params); replace
a .contains("通过") success check with a dedicated flag
- Localize user-facing model/coordinator/error messages across App and Shared
Internal RuntimeLogger logs and diagnostic/report bodies remain in Chinese.
215 lines
9.0 KiB
Swift
215 lines
9.0 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
struct ContentView: View {
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@StateObject private var setup = SetupCoordinator()
|
|
@ObservedObject private var runtimeMode = ProxyRuntimeModeStore.shared
|
|
@ObservedObject private var remoteConfiguration = AppRemoteConfigurationStore.shared
|
|
@State private var phase: AppPhase = .splash
|
|
@State private var updatePrompt: AppUpdatePrompt?
|
|
@State private var requiredUpdatePrompt: AppUpdatePrompt?
|
|
|
|
enum AppPhase { case splash, setup, map }
|
|
|
|
var body: some View {
|
|
Group {
|
|
switch phase {
|
|
case .splash:
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "location.fill")
|
|
.font(.system(size: 48)).foregroundStyle(.blue)
|
|
ProgressView()
|
|
(runtimeMode.hasSelectedMode && runtimeMode.mode == .localWiFi
|
|
? Text("正在初始化地图与本地代理…")
|
|
: Text("正在初始化地图…"))
|
|
.font(.subheadline).foregroundStyle(.secondary)
|
|
}
|
|
case .setup:
|
|
FirstSetupView(setup: setup, onComplete: finishInitialSetup)
|
|
case .map:
|
|
NavigationView {
|
|
MapHomeView(setup: setup)
|
|
}
|
|
.fullScreenCover(isPresented: $setup.needsSetup) {
|
|
FirstSetupView(setup: setup, onComplete: finishPresentedSetup)
|
|
}
|
|
}
|
|
}
|
|
.task { await bootstrap() }
|
|
.task { await checkForUpdates() }
|
|
.onChange(of: scenePhase) { newPhase in
|
|
guard newPhase == .active, let requiredUpdatePrompt else { return }
|
|
updatePrompt = requiredUpdatePrompt
|
|
}
|
|
.alert(item: $updatePrompt) { prompt in
|
|
updateAlert(for: prompt)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func bootstrap() async {
|
|
guard runtimeMode.hasSelectedMode else {
|
|
ProxyManager.shared.stop()
|
|
RuntimeLogger.info("APP", "Startup", "尚未选择运行模式,跳过本地 CA 和代理初始化")
|
|
setup.requestModeSelection()
|
|
phase = .setup
|
|
return
|
|
}
|
|
|
|
let launchMode = runtimeMode.mode
|
|
guard runtimeMode.isInitialized(launchMode) else {
|
|
if launchMode == .localWiFi {
|
|
await setup.prepareLocalServices()
|
|
setup.requestSetup()
|
|
} else {
|
|
ProxyManager.shared.stop()
|
|
BackgroundKeepAlive.shared.stop()
|
|
setup.requestThirdPartyOnboarding()
|
|
}
|
|
phase = .setup
|
|
return
|
|
}
|
|
|
|
if launchMode == .localWiFi {
|
|
await setup.prepareLocalServices()
|
|
} else {
|
|
ProxyManager.shared.stop()
|
|
BackgroundKeepAlive.shared.stop()
|
|
RuntimeLogger.info("APP", "Startup", "第三方代理测试模式:跳过本地 CA、代理和环境检测")
|
|
}
|
|
do {
|
|
try CoordinateStorageMigration.migrateIfNeeded(favorites: FavoriteLocationStore())
|
|
} catch {
|
|
RuntimeLogger.error("APP", "Startup", "旧坐标数据迁移失败,将在下次启动重试", error: error)
|
|
}
|
|
|
|
// MapHomeView is intentionally constructed only after this required
|
|
// coordinate-system gate resolves, so cached pins are never replayed
|
|
// into an unknown Apple Maps coordinate system.
|
|
let mapCoordinateSystem = await CoordinateConverter.resolveInitialMapCoordinateSystem()
|
|
guard !Task.isCancelled else { return }
|
|
RuntimeLogger.info("APP", "Startup", "地图坐标标准初始化完成,开始后续启动流程", details: [
|
|
"地图标准": mapCoordinateSystem.rawValue,
|
|
"使用兜底": String(CoordinateConverter.initialMapCoordinateSystemUsedFallback)
|
|
])
|
|
|
|
// Resolve the first map center before constructing MapHomeView. This
|
|
// prevents a Shenzhen/cache frame followed by a second realtime frame.
|
|
if LastCoordinateStore.load() == nil {
|
|
RuntimeLogger.info("APP", "Startup", "没有持久化图钉,地图创建前请求实时定位")
|
|
if let realtime = await RealtimeLocationManager.shared.requestLocation() {
|
|
let mapCoordinateSystemChange = CoordinateConverter.correctMapCoordinateSystemUsingRealtime(realtime)
|
|
let pair = CoordinateConverter.coordinatePair(
|
|
lat: realtime.latitude,
|
|
lon: realtime.longitude,
|
|
mapCoordinateSystem: .wgs84
|
|
)
|
|
LastCoordinateStore.save(coordinatePair: pair, zoomMeters: 1_000)
|
|
RuntimeLogger.info("APP", "Startup", "已使用实时定位准备唯一初始地图状态", details: [
|
|
"地图标准": CoordinateConverter.currentMapCoordinateSystem.rawValue,
|
|
"修正兜底标准": String(mapCoordinateSystemChange != nil),
|
|
"缩放米": "1000"
|
|
])
|
|
RealtimeLocationTrace.coordinate(
|
|
"地图创建前取得的初始实时位置(WGS-84)",
|
|
coordinate: realtime
|
|
)
|
|
} else {
|
|
RuntimeLogger.warning("APP", "Startup", "地图创建前无法取得实时定位,唯一初始位置使用深圳", details: [
|
|
"地图标准": mapCoordinateSystem.rawValue,
|
|
"缩放米": "1000"
|
|
])
|
|
}
|
|
} else {
|
|
RuntimeLogger.info("APP", "Startup", "已找到持久化图钉,直接准备唯一初始地图状态")
|
|
}
|
|
guard !Task.isCancelled else { return }
|
|
|
|
setup.completeSetup()
|
|
RuntimeLogger.info("APP", "Startup", "启动门禁全部完成,现在创建 MapHomeView")
|
|
phase = .map
|
|
}
|
|
|
|
private func finishInitialSetup() {
|
|
let completedMode = runtimeMode.mode
|
|
runtimeMode.markInitialized(completedMode)
|
|
setup.completeSetup()
|
|
phase = .splash
|
|
Task { await bootstrap() }
|
|
}
|
|
|
|
private func finishPresentedSetup() {
|
|
runtimeMode.markInitialized(runtimeMode.mode)
|
|
setup.completeSetup()
|
|
}
|
|
|
|
@MainActor
|
|
private func checkForUpdates() async {
|
|
let currentVersion = Bundle.main.object(
|
|
forInfoDictionaryKey: "CFBundleShortVersionString"
|
|
) as? String ?? AppRemoteConfiguration.fallback.latestVersion
|
|
let configuration: AppRemoteConfiguration
|
|
if let remote = await AppRemoteConfigurationService.fetch() {
|
|
remoteConfiguration.apply(remote)
|
|
configuration = remote
|
|
} else {
|
|
configuration = remoteConfiguration.configuration
|
|
}
|
|
guard let pendingPrompt = configuration.updatePrompt(currentVersion: currentVersion) else { return }
|
|
let releaseNotes = await AppRemoteConfigurationService.fetchReleaseNotes(
|
|
version: pendingPrompt.latestVersion
|
|
)
|
|
guard !Task.isCancelled,
|
|
let prompt = configuration.updatePrompt(
|
|
currentVersion: currentVersion,
|
|
releaseNotes: releaseNotes
|
|
) else {
|
|
return
|
|
}
|
|
if prompt.requirement == .required {
|
|
requiredUpdatePrompt = prompt
|
|
}
|
|
updatePrompt = prompt
|
|
}
|
|
|
|
private func updateAlert(for prompt: AppUpdatePrompt) -> Alert {
|
|
switch prompt.requirement {
|
|
case .required:
|
|
let details = prompt.releaseNotes
|
|
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
|
return Alert(
|
|
title: Text("需要更新"),
|
|
message: Text(
|
|
"当前版本 \(prompt.currentVersion) 已停止支持,请更新到 \(prompt.latestVersion) 后继续使用。\n\n\(details)"
|
|
),
|
|
dismissButton: .default(Text("立即更新")) {
|
|
openUpdatePage(requiredPrompt: prompt)
|
|
}
|
|
)
|
|
case .recommended:
|
|
let details = prompt.releaseNotes
|
|
?? String(localized: "更新说明暂时无法加载,请前往最新 Release 页面查看。")
|
|
return Alert(
|
|
title: Text("发现新版本"),
|
|
message: Text(
|
|
"当前版本 \(prompt.currentVersion),最新版本 \(prompt.latestVersion)。\n\n\(details)"
|
|
),
|
|
primaryButton: .default(Text("前往更新")) {
|
|
openUpdatePage(requiredPrompt: nil)
|
|
},
|
|
secondaryButton: .cancel(Text("稍后"))
|
|
)
|
|
}
|
|
}
|
|
|
|
private func openUpdatePage(requiredPrompt: AppUpdatePrompt?) {
|
|
UIApplication.shared.open(AppRemoteConfigurationService.releasesURL)
|
|
guard requiredPrompt != nil else { return }
|
|
Task { @MainActor in
|
|
try? await Task.sleep(nanoseconds: 800_000_000)
|
|
updatePrompt = self.requiredUpdatePrompt
|
|
}
|
|
}
|
|
}
|