mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-26 00:21:55 +08:00
feat: add third-party proxy mode and improve onboarding
This commit is contained in:
+247
-21
@@ -4,26 +4,71 @@ struct SettingsView: View {
|
||||
@ObservedObject var setup: SetupCoordinator
|
||||
@ObservedObject var actions: LocationActionCoordinator
|
||||
@ObservedObject private var proxy = ProxyManager.shared
|
||||
@ObservedObject private var runtimeMode = ProxyRuntimeModeStore.shared
|
||||
@ObservedObject private var thirdPartyProxy = ThirdPartyProxyManager.shared
|
||||
@ObservedObject private var thirdPartyClient = ThirdPartyProxyClientStore.shared
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var activeTip: TipKind?
|
||||
@State private var proxyOperationError = ""
|
||||
@State private var proxyOperationAlertTitle = "代理操作失败"
|
||||
@State private var modeOperationRunning = false
|
||||
@State private var copiedClient: ThirdPartyProxyClient?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("状态") {
|
||||
HStack {
|
||||
Label("代理", systemImage: proxy.isRunning ? "play.circle.fill" : "stop.circle")
|
||||
Spacer()
|
||||
Toggle("", isOn: proxyBinding).labelsHidden()
|
||||
.tint(.blue)
|
||||
Section("运行模式") {
|
||||
Picker("模式", selection: runtimeModeBinding) {
|
||||
ForEach(ProxyRuntimeMode.allCases) { mode in
|
||||
Text(mode.displayName).tag(mode)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Label("虚拟定位", systemImage: actions.virtualLocationEnabled ? "location.fill" : "location.slash")
|
||||
Spacer()
|
||||
Text(actions.virtualLocationEnabled ? "已开启" : "已关闭").foregroundStyle(.secondary)
|
||||
.pickerStyle(.inline)
|
||||
.disabled(modeOperationRunning || actions.state.isBusy || thirdPartyProxy.isRequesting)
|
||||
|
||||
if runtimeMode.mode == .thirdParty {
|
||||
Label("测试模式:仅 Shadowrocket 当前可测试", systemImage: "testtube.2")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
Section("说明") {
|
||||
Section("状态") {
|
||||
if runtimeMode.mode == .localWiFi {
|
||||
HStack {
|
||||
Label("本机代理", systemImage: proxy.isRunning ? "play.circle.fill" : "stop.circle")
|
||||
Spacer()
|
||||
Toggle("", isOn: proxyBinding).labelsHidden()
|
||||
.tint(.blue)
|
||||
.disabled(actions.state.isBusy)
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Label("第三方模块", systemImage: thirdPartyStatusIcon)
|
||||
Spacer()
|
||||
Text(thirdPartyStatusText).foregroundStyle(.secondary)
|
||||
}
|
||||
Button {
|
||||
detectThirdPartyConnection()
|
||||
} label: {
|
||||
if thirdPartyProxy.isRequesting {
|
||||
HStack { ProgressView(); Text("正在检测…") }
|
||||
} else {
|
||||
Label("检测连接", systemImage: "network")
|
||||
}
|
||||
}
|
||||
.disabled(thirdPartyProxy.isRequesting)
|
||||
}
|
||||
HStack {
|
||||
Label("虚拟定位", systemImage: virtualLocationIsActive ? "location.fill" : "location.slash")
|
||||
Spacer()
|
||||
Text(virtualLocationStatusText).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if runtimeMode.mode == .thirdParty {
|
||||
thirdPartyConfigurationSection
|
||||
} else {
|
||||
Section("说明") {
|
||||
Button {
|
||||
activeTip = .activation
|
||||
} label: {
|
||||
@@ -39,23 +84,22 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
Label("关闭 WiFi 代理", systemImage: "wifi.slash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("工作原理") {
|
||||
Text("""
|
||||
App 在设备本地运行一个代理服务器(127.0.0.1:8888)。
|
||||
|
||||
通过 WiFi 手动代理配置,让系统的定位请求(gs-loc.apple.com/clls/wloc)经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
|
||||
""")
|
||||
Text(workflowDescription)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section("应用") {
|
||||
Button {
|
||||
setup.requestSetup()
|
||||
} label: {
|
||||
Label("进入引导页", systemImage: "arrow.clockwise.circle")
|
||||
if runtimeMode.mode == .localWiFi {
|
||||
Button {
|
||||
setup.requestSetup()
|
||||
} label: {
|
||||
Label("进入引导页", systemImage: "arrow.clockwise.circle")
|
||||
}
|
||||
}
|
||||
valueRow("版本", value: versionText)
|
||||
}
|
||||
@@ -100,6 +144,14 @@ struct SettingsView: View {
|
||||
.sheet(item: $activeTip) { kind in
|
||||
TipSheetView(kind: kind)
|
||||
}
|
||||
.alert(proxyOperationAlertTitle, isPresented: Binding(
|
||||
get: { !proxyOperationError.isEmpty },
|
||||
set: { if !$0 { proxyOperationError = "" } }
|
||||
)) {
|
||||
Button("知道了", role: .cancel) {}
|
||||
} message: {
|
||||
Text(proxyOperationError)
|
||||
}
|
||||
}
|
||||
|
||||
private func valueRow(_ title: String, value: String) -> some View {
|
||||
@@ -116,11 +168,185 @@ struct SettingsView: View {
|
||||
Binding(get: { proxy.isRunning }, set: { on in
|
||||
Task {
|
||||
if on {
|
||||
do { try await proxy.start() } catch { proxy.error = error.localizedDescription }
|
||||
do {
|
||||
try await proxy.start()
|
||||
} catch {
|
||||
proxy.error = error.localizedDescription
|
||||
proxyOperationAlertTitle = "代理操作失败"
|
||||
proxyOperationError = error.localizedDescription
|
||||
}
|
||||
} else {
|
||||
if actions.virtualLocationEnabled {
|
||||
actions.clear()
|
||||
RuntimeLogger.info("APP", "Settings", "关闭代理前已同步关闭虚拟定位")
|
||||
}
|
||||
proxy.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var runtimeModeBinding: Binding<ProxyRuntimeMode> {
|
||||
Binding(
|
||||
get: { runtimeMode.mode },
|
||||
set: { newMode in switchRuntimeMode(to: newMode) }
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var thirdPartyConfigurationSection: some View {
|
||||
Section("第三方代理配置") {
|
||||
Picker("客户端", selection: Binding(
|
||||
get: { thirdPartyClient.selectedClient },
|
||||
set: { thirdPartyClient.select($0) }
|
||||
)) {
|
||||
ForEach(ThirdPartyProxyClient.allCases) { client in
|
||||
Text(client.name).tag(client)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("验证状态")
|
||||
Spacer()
|
||||
Text(thirdPartyClient.selectedClient.verificationText)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(thirdPartyClient.selectedClient == .shadowrocket ? .green : .orange)
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = thirdPartyClient.selectedClient.subscriptionURL.absoluteString
|
||||
copiedClient = thirdPartyClient.selectedClient
|
||||
} label: {
|
||||
Label(copiedClient == thirdPartyClient.selectedClient ? "已复制订阅链接" : "复制订阅链接", systemImage: "doc.on.doc")
|
||||
}
|
||||
|
||||
Button {
|
||||
openThirdPartyClient(thirdPartyClient.selectedClient)
|
||||
} label: {
|
||||
Label("打开 \(thirdPartyClient.selectedClient.name)", systemImage: "arrow.up.forward.app")
|
||||
}
|
||||
|
||||
Button {
|
||||
setup.requestThirdPartySetup()
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("重新打开配置引导", systemImage: "arrow.clockwise.circle")
|
||||
}
|
||||
|
||||
if thirdPartyClient.selectedClient == .egern {
|
||||
Text("Egern 直接使用 Surge 的 .sgmodule 模块。")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
} else if thirdPartyClient.selectedClient == .stash {
|
||||
Text("Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text("复制链接后,在对应代理客户端中添加模块/重写订阅,并启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var thirdPartyStatusIcon: String {
|
||||
switch thirdPartyProxy.connectionState {
|
||||
case .unknown: return "questionmark.circle"
|
||||
case .connected: return "checkmark.circle.fill"
|
||||
case .failed: return "xmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private var thirdPartyStatusText: String {
|
||||
switch thirdPartyProxy.connectionState {
|
||||
case .unknown: return "未检测"
|
||||
case .connected(let active): return active ? "已连接,有坐标" : "已连接,无坐标"
|
||||
case .failed: return "连接失败"
|
||||
}
|
||||
}
|
||||
|
||||
private var virtualLocationStatusText: String {
|
||||
if runtimeMode.mode == .localWiFi {
|
||||
return actions.virtualLocationEnabled ? "已开启" : "已关闭"
|
||||
}
|
||||
if case .connected(let active) = thirdPartyProxy.connectionState {
|
||||
return active ? "第三方已保存" : "未保存"
|
||||
}
|
||||
return "未知"
|
||||
}
|
||||
|
||||
private var workflowDescription: String {
|
||||
if runtimeMode.mode == .thirdParty {
|
||||
return "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。"
|
||||
}
|
||||
return """
|
||||
App 在设备本地运行一个代理服务器(127.0.0.1:8888)。
|
||||
|
||||
通过 WiFi 手动代理配置,让系统的定位请求(gs-loc.apple.com/clls/wloc)经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。
|
||||
"""
|
||||
}
|
||||
|
||||
private func switchRuntimeMode(to newMode: ProxyRuntimeMode) {
|
||||
guard newMode != runtimeMode.mode, !modeOperationRunning else { return }
|
||||
modeOperationRunning = true
|
||||
Task { @MainActor in
|
||||
defer { modeOperationRunning = false }
|
||||
switch newMode {
|
||||
case .thirdParty:
|
||||
if actions.virtualLocationEnabled { actions.clear() }
|
||||
proxy.stop()
|
||||
setup.completeSetup()
|
||||
runtimeMode.setMode(.thirdParty)
|
||||
setup.requestThirdPartySetup()
|
||||
proxyOperationAlertTitle = "模式已切换"
|
||||
proxyOperationError = "已切换到第三方代理模式。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,并按引导导入第三方配置。"
|
||||
case .localWiFi:
|
||||
do {
|
||||
try await thirdPartyProxy.clear()
|
||||
} catch {
|
||||
RuntimeLogger.warning("APP", "Mode", "切换 APP 模式前无法清除第三方坐标", details: [
|
||||
"错误": error.localizedDescription
|
||||
])
|
||||
}
|
||||
runtimeMode.setMode(.localWiFi)
|
||||
await setup.prepareLocalServices()
|
||||
setup.requestSetup()
|
||||
proxyOperationAlertTitle = "模式已切换"
|
||||
proxyOperationError = "已切换到 APP 模式。请停用第三方 WLOC 模块或代理连接,避免双重拦截。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func detectThirdPartyConnection() {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let response = try await thirdPartyProxy.query()
|
||||
if !response.success, response.error?.contains("无已保存") != true {
|
||||
proxyOperationAlertTitle = "检测失败"
|
||||
proxyOperationError = response.error ?? "第三方代理模块返回失败"
|
||||
}
|
||||
} catch {
|
||||
proxyOperationAlertTitle = "检测失败"
|
||||
proxyOperationError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openThirdPartyClient(_ client: ThirdPartyProxyClient) {
|
||||
guard let url = client.launchURL else { return }
|
||||
UIApplication.shared.open(url, options: [:]) { opened in
|
||||
guard !opened else { return }
|
||||
Task { @MainActor in
|
||||
proxyOperationAlertTitle = "无法打开客户端"
|
||||
proxyOperationError = "无法打开 \(client.name),请确认客户端已安装后手动打开。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var virtualLocationIsActive: Bool {
|
||||
if runtimeMode.mode == .localWiFi {
|
||||
return actions.virtualLocationEnabled
|
||||
}
|
||||
if case .connected(let active) = thirdPartyProxy.connectionState {
|
||||
return active
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user