Push missing 3.1.3 commits and hopefully add better error clarity

This commit is contained in:
Stephen 2026-05-20 09:02:20 -04:00
parent 8a7b39dbe2
commit 4b20cd3d1e
9 changed files with 459 additions and 96 deletions

View file

@ -239,8 +239,12 @@ struct ScriptListView: View {
guard !newFileName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
var filename = newFileName.trimmingCharacters(in: .whitespacesAndNewlines)
if !filename.hasSuffix(".js") { filename += ".js" }
guard let filename = ScriptStore.normalizedScriptFileName(filename) else {
presentError(title: "Failed to Create New Script", message: "Use a simple .js filename without folders.")
return
}
do {
let newURL = try scriptsDirectory().appendingPathComponent(filename)
let newURL = try ScriptStore.scriptURL(named: filename)
guard !FileManager.default.fileExists(atPath: newURL.path) else {
presentError(title: "Failed to Create New Script", message: "A script with the same name already exists.")
return
@ -270,16 +274,25 @@ struct ScriptListView: View {
isBusy = true
DispatchQueue.global(qos: .userInitiated).async {
defer { DispatchQueue.main.async { self.isBusy = false } }
let accessing = fileURL.startAccessingSecurityScopedResource()
defer {
if accessing {
fileURL.stopAccessingSecurityScopedResource()
}
}
do {
guard let fileName = ScriptStore.normalizedScriptFileName(fileURL.lastPathComponent) else {
throw CocoaError(.fileReadInvalidFileName)
}
let directory = try self.scriptsDirectory()
let dest = directory.appendingPathComponent(fileURL.lastPathComponent)
let dest = directory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: dest.path) {
try FileManager.default.removeItem(at: dest)
}
try FileManager.default.copyItem(at: fileURL, to: dest)
DispatchQueue.main.async {
self.loadScripts()
self.presentSuccess(title: "Imported", message: fileURL.lastPathComponent)
self.presentSuccess(title: "Imported", message: fileName)
}
} catch {
DispatchQueue.main.async {

View file

@ -317,7 +317,7 @@ func startTunnelInBackground(showErrorUI: Bool = true) {
} catch {
let err2 = error as NSError
let code = err2.code
LogManager.shared.addErrorLog("\(error.localizedDescription) (Code: \(code))")
LogManager.shared.addErrorLog(tunnelConnectionLogMessage(for: err2))
guard showErrorUI else { return }
DispatchQueue.main.async {
if code == -9 {
@ -340,7 +340,7 @@ func startTunnelInBackground(showErrorUI: Bool = true) {
} else {
showAlert(
title: "Connection Error",
message: "\(error.localizedDescription)\n\nMake sure Wi‑Fi and LocalDevVPN are connected and that the device is reachable.",
message: tunnelConnectionAlertMessage(for: err2),
showOk: false,
showTryAgain: true
) { shouldTryAgain in
@ -358,6 +358,82 @@ func startTunnelInBackground(showErrorUI: Bool = true) {
}
private func tunnelConnectionLogMessage(for error: NSError) -> String {
let target = "\(DeviceConnectionContext.targetIPAddress):49152"
return "Tunnel connection failed for \(target): \(error.localizedDescription) (Domain: \(error.domain), Code: \(error.code), Raw: \(String(describing: error)))"
}
private func tunnelConnectionAlertMessage(for error: NSError) -> String {
let targetIP = DeviceConnectionContext.targetIPAddress
let rawMessage = error.localizedDescription
let lowercasedMessage = rawMessage.lowercased()
let likelyCause: String
let recoverySteps: [String]
if error.code == 48 || lowercasedMessage.contains("address already in use") || lowercasedMessage.contains("port already in use") {
likelyCause = "A port needed for the tunnel is already in use."
recoverySteps = [
"Close other JIT, debugging, proxy, or VPN apps that may be using the tunnel.",
"Disconnect and reconnect LocalDevVPN.",
"Restart StikDebug, then try again.",
"If it keeps happening, reboot the device to clear the stuck port."
]
} else if error.code == 54 || lowercasedMessage.contains("connection reset") {
likelyCause = "The device or VPN closed the tunnel connection before setup finished."
recoverySteps = [
"Open LocalDevVPN and confirm the VPN is connected.",
"Make sure LocalDevVPN is using the default \(DeviceConnectionContext.defaultTargetIPAddress) address.",
"Reconnect Wi-Fi and LocalDevVPN, then try again.",
"If this keeps happening, select a fresh pairing file."
]
} else if error.code == -18 || lowercasedMessage.contains("parse target ip") {
likelyCause = "The configured target IP address is not valid."
recoverySteps = [
"Open Settings and check the target IP address.",
"Use the default \(DeviceConnectionContext.defaultTargetIPAddress)."
]
} else if lowercasedMessage.contains("timed out") || lowercasedMessage.contains("timeout") {
likelyCause = "The app could not reach the device before the connection timed out."
recoverySteps = [
"Confirm Wi-Fi and LocalDevVPN are both connected.",
"Wake and unlock the target device.",
"Confirm LocalDevVPN is exposing the device at \(targetIP)."
]
} else if lowercasedMessage.contains("network is unreachable") || lowercasedMessage.contains("no route") {
likelyCause = "The VPN route to the device is not available."
recoverySteps = [
"Disconnect and reconnect LocalDevVPN.",
"Confirm iOS shows the VPN indicator.",
"Try switching Wi-Fi off and on."
]
} else {
likelyCause = "The tunnel could not be created."
recoverySteps = [
"Confirm Wi-Fi and LocalDevVPN are connected.",
"Wake and unlock the target device.",
"Reconnect LocalDevVPN, then try again."
]
}
let steps = recoverySteps.enumerated()
.map { "\($0.offset + 1). \($0.element)" }
.joined(separator: "\n")
return """
\(likelyCause)
Target: \(targetIP):49152
Expected LocalDevVPN IP: \(DeviceConnectionContext.defaultTargetIPAddress)
Try this:
\(steps)
Technical details:
Code \(error.code): \(rawMessage)
"""
}
func checkDeviceConnection(callback: @escaping (Bool, String?) -> Void) {
let targetIP = DeviceConnectionContext.targetIPAddress
let host = NWEndpoint.Host(targetIP)
@ -463,20 +539,33 @@ private let ddiDownloadItems: [DDIDownloadItem] = [
enum DDIDownloadError: LocalizedError {
case invalidURL(String)
case invalidResponse
case badStatus(Int)
var errorDescription: String? {
switch self {
case .invalidURL(let string):
return "Invalid download URL: \(string)"
case .invalidResponse:
return "The DDI server returned an invalid response."
case .badStatus(let statusCode):
return "The DDI server returned HTTP \(statusCode)."
}
}
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
guard let url = URL(string: urlString) else {
guard let url = URL(string: urlString),
url.scheme?.lowercased() == "https" else {
throw DDIDownloadError.invalidURL(urlString)
}
let (tempLocalUrl, _) = try await URLSession.shared.download(from: url)
let (tempLocalUrl, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw DDIDownloadError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw DDIDownloadError.badStatus(httpResponse.statusCode)
}
let fileManager = FileManager.default
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(),
withIntermediateDirectories: true)

View file

@ -8,11 +8,9 @@
import Foundation
enum DeviceConnectionContext {
static let defaultTargetIPAddress = "10.7.0.1"
static var targetIPAddress: String {
let stored = UserDefaults.standard.string(forKey: "customTargetIP")
if let stored, !stored.isEmpty {
return stored
}
return "10.7.0.1"
defaultTargetIPAddress
}
}

View file

@ -26,21 +26,33 @@ enum PairingFileStore {
]
static var url: URL {
URL.documentsDirectory.appendingPathComponent(fileName)
directoryURL.appendingPathComponent(fileName)
}
private static var legacyURL: URL {
private static var directoryURL: URL {
FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Pairing", isDirectory: true)
}
private static var legacyURLs: [URL] {
[
URL.documentsDirectory.appendingPathComponent(fileName),
URL.documentsDirectory.appendingPathComponent(legacyFileName)
]
}
@discardableResult
static func prepareURL(fileManager: FileManager = .default) -> URL {
let destination = url
guard !fileManager.fileExists(atPath: destination.path),
fileManager.fileExists(atPath: legacyURL.path) else {
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
guard !fileManager.fileExists(atPath: destination.path) else {
removeLegacyCopies(fileManager: fileManager)
return destination
}
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
do {
try fileManager.moveItem(at: legacyURL, to: destination)
} catch {
@ -49,6 +61,9 @@ enum PairingFileStore {
try? fileManager.removeItem(at: legacyURL)
}
}
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destination.path)
break
}
return destination
}
@ -58,9 +73,7 @@ enum PairingFileStore {
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
if fileManager.fileExists(atPath: legacyURL.path) {
try? fileManager.removeItem(at: legacyURL)
}
removeLegacyCopies(fileManager: fileManager)
try fileManager.copyItem(at: sourceURL, to: destination)
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destination.path)
}
@ -85,7 +98,11 @@ enum PairingFileStore {
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
if fileManager.fileExists(atPath: legacyURL.path) {
removeLegacyCopies(fileManager: fileManager)
}
private static func removeLegacyCopies(fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
try? fileManager.removeItem(at: legacyURL)
}
}
@ -131,6 +148,9 @@ enum ScriptStore {
}
static func scriptURL(named scriptName: String, fileManager: FileManager = .default) throws -> URL {
guard let scriptName = normalizedScriptFileName(scriptName) else {
throw CocoaError(.fileReadInvalidFileName)
}
let directory = try prepareDirectory(fileManager: fileManager)
return directory.appendingPathComponent(scriptName)
}
@ -141,8 +161,8 @@ enum ScriptStore {
static func updateAssignedScriptName(_ scriptName: String?, for bundleID: String, defaults: UserDefaults = .standard) {
var mapping = assignedScriptMap(defaults: defaults)
if let scriptName {
mapping[bundleID] = scriptName
if let scriptName, let normalizedName = normalizedScriptFileName(scriptName) {
mapping[bundleID] = normalizedName
} else {
mapping.removeValue(forKey: bundleID)
}
@ -208,7 +228,28 @@ enum ScriptStore {
}
private static func assignedScriptMap(defaults: UserDefaults) -> [String: String] {
defaults.dictionary(forKey: assignmentKey) as? [String: String] ?? [:]
let rawMap = defaults.dictionary(forKey: assignmentKey) as? [String: String] ?? [:]
return rawMap.compactMapValues(normalizedScriptFileName)
}
static func normalizedScriptFileName(_ candidate: String) -> String? {
let trimmed = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
!trimmed.contains("/"),
!trimmed.contains("\\"),
!trimmed.contains("\0") else {
return nil
}
let fileName = URL(fileURLWithPath: trimmed).lastPathComponent
guard fileName == trimmed,
fileName != ".",
fileName != "..",
URL(fileURLWithPath: fileName).pathExtension.lowercased() == "js" else {
return nil
}
return fileName
}
}

View file

@ -15,6 +15,78 @@ struct JITEnableConfiguration {
var scriptName : String? = nil
}
private enum ExternalURLAction: Identifiable {
case enableJIT(JITEnableConfiguration)
case killProcess(Int)
case launchApp(String)
var id: String {
switch self {
case .enableJIT(let config):
return "enable-\(config.bundleID ?? "")-\(config.pid ?? 0)-\(config.scriptName ?? "")"
case .killProcess(let pid):
return "kill-\(pid)"
case .launchApp(let bundleID):
return "launch-\(bundleID)"
}
}
var title: String {
switch self {
case .enableJIT:
return "Enable JIT?"
case .killProcess:
return "Kill Process?"
case .launchApp:
return "Launch App?"
}
}
var message: String {
switch self {
case .enableJIT(let config):
let scriptText = config.scriptData == nil ? "" : " and run a script"
return "An external link wants to enable JIT\(scriptText) for \(targetDescription(for: config))."
case .killProcess(let pid):
return "An external link wants to kill process \(pid)."
case .launchApp(let bundleID):
return "An external link wants to launch \(bundleID)."
}
}
var confirmationTitle: String {
switch self {
case .enableJIT(let config):
return config.scriptData == nil ? "Enable JIT" : "Enable and Run Script"
case .killProcess:
return "Kill Process"
case .launchApp:
return "Launch App"
}
}
var role: ButtonRole? {
switch self {
case .enableJIT(let config):
return config.scriptData == nil ? nil : .destructive
case .killProcess:
return .destructive
case .launchApp:
return nil
}
}
private func targetDescription(for config: JITEnableConfiguration) -> String {
if let bundleID = config.bundleID {
return bundleID
}
if let pid = config.pid {
return "process \(pid)"
}
return "the requested app"
}
}
private final class DebugKeepAliveLease {
private let stateLock = NSLock()
private var isActive = false
@ -81,6 +153,7 @@ struct HomeView: View {
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
@State private var isShowingPairingFilePicker = false
@State private var debugFeedback: DebugFeedback?
@State private var pendingExternalURLAction: ExternalURLAction?
@State var scriptViewShow = false
@State private var isShowingConsole = false
@ -133,64 +206,30 @@ struct HomeView: View {
}
}
.onOpenURL { url in
guard let host = url.host()?.lowercased() else { return }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
switch host {
case "enable-jit":
var config = JITEnableConfiguration()
if let pidStr = queryValue(["pid"], in: components), let pid = Int(pidStr) {
config.pid = pid
handleExternalURL(url)
}
if let bundleId = queryValue(["bundle-id", "bundleID", "bundle_id", "bundleId"], in: components) {
config.bundleID = bundleId
}
if let scriptBase64URL = queryValue(["script-data", "scriptData", "script_data"], in: components)?.removingPercentEncoding {
let base64 = base64URLToBase64(scriptBase64URL)
if let scriptData = Data(base64Encoded: base64) {
config.scriptData = scriptData
.confirmationDialog(
pendingExternalURLAction?.title ?? "External Request",
isPresented: Binding(
get: { pendingExternalURLAction != nil },
set: { isPresented in
if !isPresented {
pendingExternalURLAction = nil
}
}
if let scriptName = queryValue(["script-name", "scriptName", "script_name"], in: components) {
config.scriptName = scriptName
),
titleVisibility: .visible,
presenting: pendingExternalURLAction
) { action in
Button(action.confirmationTitle, role: action.role) {
performExternalURLAction(action)
pendingExternalURLAction = nil
}
if config.scriptData == nil, let bundleID = config.bundleID,
let scriptInfo = ScriptStore.preferredScript(for: bundleID) {
config.scriptData = scriptInfo.data
config.scriptName = scriptInfo.name
}
if viewDidAppeared {
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
} else {
pendingJITEnableConfiguration = config
}
case "kill-process":
if let pidStr = queryValue(["pid"], in: components), let pid = Int(pidStr) {
pubTunnelConnected = false
startTunnelInBackground(showErrorUI: false)
DispatchQueue.global(qos: .userInitiated).async {
sleep(1)
do {
try JITEnableContext.shared.killProcess(withPID: Int32(pid))
DispatchQueue.main.async {
LogManager.shared.addInfoLog("Killed process \(pid) via URL scheme")
}
} catch {
DispatchQueue.main.async {
LogManager.shared.addErrorLog("Failed to kill process \(pid): \(error.localizedDescription)")
}
}
}
}
case "launch-app":
if let bundleId = queryValue(["bundle-id", "bundleID", "bundle_id", "bundleId"], in: components) {
HapticFeedbackHelper.trigger()
DispatchQueue.global(qos: .userInitiated).async {
let _ = JITEnableContext.shared.launchAppWithoutDebug(bundleId, logger: nil)
}
}
default:
break
Button("Cancel", role: .cancel) {
pendingExternalURLAction = nil
}
} message: { action in
Text(action.message)
}
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: PairingFileStore.supportedContentTypes) { result in
switch result {
@ -256,6 +295,85 @@ struct HomeView: View {
return nil
}
private func handleExternalURL(_ url: URL) {
guard let host = url.host()?.lowercased() else { return }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
switch host {
case "enable-jit":
var config = JITEnableConfiguration()
if let pidStr = queryValue(["pid"], in: components), let pid = Int(pidStr) {
config.pid = pid
}
if let bundleId = queryValue(["bundle-id", "bundleID", "bundle_id", "bundleId"], in: components) {
config.bundleID = bundleId
}
if let scriptBase64URL = queryValue(["script-data", "scriptData", "script_data"], in: components)?.removingPercentEncoding {
let base64 = base64URLToBase64(scriptBase64URL)
if let scriptData = Data(base64Encoded: base64) {
config.scriptData = scriptData
}
}
if let scriptName = queryValue(["script-name", "scriptName", "script_name"], in: components) {
config.scriptName = scriptName
}
if config.scriptData == nil, let bundleID = config.bundleID,
let scriptInfo = ScriptStore.preferredScript(for: bundleID) {
config.scriptData = scriptInfo.data
config.scriptName = scriptInfo.name
}
pendingExternalURLAction = .enableJIT(config)
case "kill-process":
if let pidStr = queryValue(["pid"], in: components), let pid = Int(pidStr) {
pendingExternalURLAction = .killProcess(pid)
}
case "launch-app":
if let bundleId = queryValue(["bundle-id", "bundleID", "bundle_id", "bundleId"], in: components) {
pendingExternalURLAction = .launchApp(bundleId)
}
default:
break
}
}
private func performExternalURLAction(_ action: ExternalURLAction) {
switch action {
case .enableJIT(let config):
if viewDidAppeared {
startJITInBackground(
bundleID: config.bundleID,
pid: config.pid,
scriptData: config.scriptData,
scriptName: config.scriptName,
triggeredByURLScheme: true
)
} else {
pendingJITEnableConfiguration = config
}
case .killProcess(let pid):
pubTunnelConnected = false
startTunnelInBackground(showErrorUI: false)
DispatchQueue.global(qos: .userInitiated).async {
sleep(1)
do {
try JITEnableContext.shared.killProcess(withPID: Int32(pid))
DispatchQueue.main.async {
LogManager.shared.addInfoLog("Killed process \(pid) via URL scheme")
}
} catch {
DispatchQueue.main.async {
LogManager.shared.addErrorLog("Failed to kill process \(pid): \(error.localizedDescription)")
}
}
}
case .launchApp(let bundleID):
HapticFeedbackHelper.trigger()
DispatchQueue.global(qos: .userInitiated).async {
let _ = JITEnableContext.shared.launchAppWithoutDebug(bundleID, logger: nil)
}
}
}
private func debugFeedbackView(_ feedback: DebugFeedback) -> some View {
HStack(spacing: 10) {
if feedback.isWorking {

View file

@ -1060,13 +1060,25 @@ enum AppIconRepository {
guard let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
return nil
}
guard let fileName = cacheFileName(for: bundleID) else {
return nil
}
let directory = container.appendingPathComponent("icons", isDirectory: true)
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
} catch {
return nil
}
return directory.appendingPathComponent("\(bundleID).png")
return directory.appendingPathComponent(fileName)
}
private static func cacheFileName(for bundleID: String) -> String? {
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: ".-_"))
let sanitized = String(bundleID.unicodeScalars.map { scalar in
allowed.contains(scalar) ? Character(scalar) : "_"
})
guard !sanitized.isEmpty else { return nil }
return "\(sanitized).png"
}
private static func memoryCost(for image: UIImage) -> Int {

View file

@ -12,11 +12,53 @@ extension Notification.Name {
static let switchToTab = Notification.Name("MainTabSwitchNotification")
}
private enum ExternalLocationAction: Identifiable {
case simulate(URL, Double, Double)
case clear
var id: String {
switch self {
case .simulate(let url, _, _):
return "simulate-\(url.absoluteString)"
case .clear:
return "clear-location"
}
}
var title: String {
switch self {
case .simulate:
return "Simulate Location?"
case .clear:
return "Clear Location?"
}
}
var message: String {
switch self {
case .simulate(_, let latitude, let longitude):
return String(format: "An external link wants to set the simulated location to %.6f, %.6f.", latitude, longitude)
case .clear:
return "An external link wants to clear the simulated location."
}
}
var confirmationTitle: String {
switch self {
case .simulate:
return "Set Location"
case .clear:
return "Clear Location"
}
}
}
struct MainTabView: View {
@AppStorage("primaryTabSelection") private var selection: String = AppFeature.home.id
@State private var switchObserver: Any?
@State private var detachedFeature: AppFeature?
@State private var didSetInitialHome = false
@State private var pendingLocationAction: ExternalLocationAction?
var body: some View {
ZStack {
@ -50,6 +92,29 @@ struct MainTabView: View {
.onOpenURL { url in
handleURL(url)
}
.confirmationDialog(
pendingLocationAction?.title ?? "External Location Request",
isPresented: Binding(
get: { pendingLocationAction != nil },
set: { isPresented in
if !isPresented {
pendingLocationAction = nil
}
}
),
titleVisibility: .visible,
presenting: pendingLocationAction
) { action in
Button(action.confirmationTitle, role: .destructive) {
performLocationAction(action)
pendingLocationAction = nil
}
Button("Cancel", role: .cancel) {
pendingLocationAction = nil
}
} message: { action in
Text(action.message)
}
.sheet(item: $detachedFeature) { feature in
NavigationStack {
feature.destination
@ -78,15 +143,15 @@ struct MainTabView: View {
switch host {
case "simulate-location", "set-location":
simulateLocation(from: url)
confirmSimulatedLocation(from: url)
case "location", "location-simulation":
if coordinate(from: url) == nil {
openFeature(id: AppFeature.location.id)
} else {
simulateLocation(from: url)
confirmSimulatedLocation(from: url)
}
case "clear-location", "stop-location":
clearSimulatedLocation()
pendingLocationAction = .clear
default:
break
}
@ -104,6 +169,37 @@ struct MainTabView: View {
}
}
private func confirmSimulatedLocation(from url: URL) {
guard let coordinate = coordinate(from: url) else {
showAlert(
title: "Invalid Location URL",
message: "Use stikdebug://simulate-location?lat=37.3349&lon=-122.0090",
showOk: true
)
return
}
guard coordinateIsValid(latitude: coordinate.latitude, longitude: coordinate.longitude) else {
showAlert(
title: "Invalid Coordinates",
message: "Latitude must be between -90 and 90. Longitude must be between -180 and 180.",
showOk: true
)
return
}
pendingLocationAction = .simulate(url, coordinate.latitude, coordinate.longitude)
}
private func performLocationAction(_ action: ExternalLocationAction) {
switch action {
case .simulate(let url, _, _):
simulateLocation(from: url)
case .clear:
clearSimulatedLocation()
}
}
private func simulateLocation(from url: URL) {
guard let coordinate = coordinate(from: url) else {
showAlert(

View file

@ -776,8 +776,7 @@ struct LocationSimulationView: View {
}
private var deviceIP: String {
let stored = UserDefaults.standard.string(forKey: "customTargetIP") ?? ""
return stored.isEmpty ? "10.7.0.1" : stored
DeviceConnectionContext.targetIPAddress
}
private var routeStartCoordinate: CLLocationCoordinate2D? {

View file

@ -17,7 +17,6 @@ struct SettingsView: View {
@AppStorage(UserDefaults.Keys.txmOverride) private var overrideTXMDetection = false
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
@AppStorage("customTargetIP") private var customTargetIP = ""
@State private var isShowingPairingFilePicker = false
@State private var isImportingFile = false
@ -126,10 +125,8 @@ struct SettingsView: View {
HStack {
Text("Target Device IP")
Spacer()
TextField("10.7.0.1", text: $customTargetIP)
.multilineTextAlignment(.trailing)
.keyboardType(.numbersAndPunctuation)
.submitLabel(.done)
Text(DeviceConnectionContext.defaultTargetIPAddress)
.foregroundStyle(.secondary)
}
Button { openAppFolder() } label: {
Label("App Folder", systemImage: "folder")