mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
* Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net>
183 lines
6.6 KiB
Swift
183 lines
6.6 KiB
Swift
import Foundation
|
||
import Combine
|
||
import SwiftUI
|
||
import SwiftProtobuf
|
||
import MapKit
|
||
import DatadogCore
|
||
import OSLog
|
||
|
||
struct AppSettings: View {
|
||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||
@Environment(\.managedObjectContext) var context
|
||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||
@State var totalDownloadedTileSize = ""
|
||
@State private var isPresentingCoreDataResetConfirm = false
|
||
@State private var isPresentingDeleteMapTilesConfirm = false
|
||
@State private var isPresentingAppIconSheet = false
|
||
@State private var purgeStaleNodes: Bool = false
|
||
@State private var showAutoConnect: Bool = false
|
||
@AppStorage("purgeStaleNodeDays") private var purgeStaleNodeDays: Double = 0
|
||
@AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true
|
||
@AppStorage("enableAdministration") private var enableAdministration: Bool = false
|
||
@AppStorage("usageDataAndCrashReporting") private var usageDataAndCrashReporting: Bool = true
|
||
|
||
let autoconnectBinding = Binding<Bool>(get: {
|
||
return UserDefaults.autoconnectOnDiscovery
|
||
}, set: { newValue in
|
||
UserDefaults.autoconnectOnDiscovery = newValue
|
||
})
|
||
var body: some View {
|
||
VStack {
|
||
Form {
|
||
Section(header: Text("App Settings")) {
|
||
Button("Open Settings", systemImage: "gear") {
|
||
// Get the settings URL and open it
|
||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||
UIApplication.shared.open(url)
|
||
}
|
||
}
|
||
Toggle(isOn: $enableAdministration) {
|
||
Label("Administration", systemImage: "gearshape.2")
|
||
}
|
||
.tint(.accentColor)
|
||
Text("PKI based node administration, requires firmware version 2.5+")
|
||
.foregroundStyle(.secondary)
|
||
.font(.caption)
|
||
Toggle(isOn: $usageDataAndCrashReporting) {
|
||
Label("Usage and Crash Data", systemImage: "pencil.and.list.clipboard")
|
||
}
|
||
.tint(.accentColor)
|
||
Text("Provide anonymous usage statistics and crash reports.")
|
||
.foregroundStyle(.secondary)
|
||
.font(.caption)
|
||
if showAutoConnect {
|
||
Toggle(isOn: autoconnectBinding) {
|
||
Label("Automatically Connect", systemImage: "app.connected.to.app.below.fill")
|
||
}
|
||
.tint(.accentColor)
|
||
}
|
||
#if targetEnvironment(macCatalyst)
|
||
// App Icon Picker is disabled on macOS Catalyst
|
||
#else
|
||
Button {
|
||
isPresentingAppIconSheet.toggle()
|
||
} label: {
|
||
Label("App Icon", systemImage: "app")
|
||
}
|
||
.sheet(isPresented: $isPresentingAppIconSheet) {
|
||
AppIconPicker(isPresenting: self.$isPresentingAppIconSheet)
|
||
.presentationDetents([.medium])
|
||
}
|
||
#endif
|
||
}
|
||
Section(header: Text("Environment")) {
|
||
VStack(alignment: .leading) {
|
||
Toggle(isOn: $environmentEnableWeatherKit) {
|
||
Label("Weather Conditions", systemImage: "cloud.sun")
|
||
}
|
||
.tint(.accentColor)
|
||
}
|
||
}
|
||
Section(header: Text("App Data")) {
|
||
Toggle(isOn: $purgeStaleNodes ) {
|
||
Label {
|
||
Text("Clear Stale Nodes")
|
||
} icon: {
|
||
Image(systemName: "list.bullet.circle")
|
||
}
|
||
}
|
||
.onFirstAppear {
|
||
purgeStaleNodes = purgeStaleNodeDays > 0
|
||
Logger.services.info("ℹ️ Purge Stale Nodes toggle initialized to \(purgeStaleNodes)")
|
||
#if DEBUG
|
||
showAutoConnect = true
|
||
#else
|
||
if Bundle.main.isTestFlight {
|
||
showAutoConnect = true
|
||
}
|
||
#endif
|
||
}
|
||
.onChange(of: usageDataAndCrashReporting) { oldUsageDataAndCrashReporting, newUsageDataAndCrashReporting in
|
||
if !newUsageDataAndCrashReporting {
|
||
Datadog.set(trackingConsent: .notGranted)
|
||
}
|
||
}
|
||
.onChange(of: purgeStaleNodes) { _, newValue in
|
||
purgeStaleNodeDays = purgeStaleNodeDays > 0 ? purgeStaleNodeDays : 7
|
||
purgeStaleNodeDays = newValue ? purgeStaleNodeDays : 0
|
||
Logger.services.info("ℹ️ Purge Stale Nodes changed to \(purgeStaleNodeDays)")
|
||
}
|
||
.tint(.accentColor)
|
||
|
||
.listRowSeparator(purgeStaleNodes ? .hidden : .visible)
|
||
if purgeStaleNodes {
|
||
VStack(alignment: .leading) {
|
||
Text(String(localized: "After \(Int(purgeStaleNodeDays)) Days"))
|
||
Slider(value: $purgeStaleNodeDays, in: 1...180, step: 1) {
|
||
} minimumValueLabel: {
|
||
Text("1")
|
||
} maximumValueLabel: {
|
||
Text("180")
|
||
}
|
||
}
|
||
Text("Favorited and ignored nodes are always retained. Other nodes are cleared from the app database on the schedule set by the user. (Nodes with PKC keys are always retained for at least 7 days.) This feature only purges nodes from the app that are not stored in the device node database.")
|
||
.foregroundStyle(.secondary)
|
||
.font(idiom == .phone ? .caption : .callout)
|
||
}
|
||
Button {
|
||
isPresentingCoreDataResetConfirm = true
|
||
} label: {
|
||
Label("Clear App Data", systemImage: "trash")
|
||
.foregroundColor(.red)
|
||
}
|
||
.confirmationDialog(
|
||
"Are you sure?",
|
||
isPresented: $isPresentingCoreDataResetConfirm,
|
||
titleVisibility: .visible
|
||
) {
|
||
Button("Erase all app data?", role: .destructive) {
|
||
Task {
|
||
try await accessoryManager.disconnect()
|
||
|
||
/// Delete any database backups too
|
||
if var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||
url = url.appendingPathComponent("backup").appendingPathComponent(String(UserDefaults.preferredPeripheralNum))
|
||
do {
|
||
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite"))
|
||
/// Delete -shm file
|
||
do {
|
||
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-wal"))
|
||
do {
|
||
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-shm"))
|
||
} catch {
|
||
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-shm file \(error, privacy: .public)")
|
||
}
|
||
} catch {
|
||
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-wal file \(error, privacy: .public)")
|
||
}
|
||
} catch {
|
||
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite file \(error, privacy: .public)")
|
||
}
|
||
}
|
||
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: true)
|
||
clearNotifications()
|
||
context.refreshAllObjects()
|
||
}
|
||
}
|
||
}
|
||
Button {
|
||
UserDefaults.standard.reset()
|
||
} label: {
|
||
Label("Reset App Settings", systemImage: "arrow.counterclockwise.circle")
|
||
.foregroundColor(.red)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("App Settings")
|
||
.navigationBarItems(trailing:
|
||
ZStack {
|
||
ConnectedDevice(deviceConnected: accessoryManager.isConnected, name: accessoryManager.activeConnection?.device.shortName ?? "?")
|
||
})
|
||
}
|
||
}
|