mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge branch '2.7.10' into copilot/improve-routing-speed
Resolve merge conflicts in: - Localizable.xcstrings (new translation keys from 2.7.10) - MeshtasticTests/RouterTests.swift (trailing comma style) - MeshtasticTests/ConnectViewTests.swift (trailing comma style) Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
commit
eb9b0ff2c9
82 changed files with 1095 additions and 388 deletions
|
|
@ -2,7 +2,6 @@
|
|||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"" : {
|
||||
"shouldTranslate" : false,
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
"stringUnit" : {
|
||||
|
|
@ -10,7 +9,8 @@
|
|||
"value" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"\t%@" : {
|
||||
"localizations" : {
|
||||
|
|
@ -225,12 +225,12 @@
|
|||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
" : %@" : {
|
||||
": %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
|
|
@ -242,42 +242,42 @@
|
|||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %@"
|
||||
"value" : ": %@"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
" : %d" : {
|
||||
": %d" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
|
|
@ -289,31 +289,31 @@
|
|||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
},
|
||||
"zh-Hant-TW" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : " : %d"
|
||||
"value" : ": %d"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -3018,7 +3018,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {},
|
||||
"A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {
|
||||
|
||||
},
|
||||
"A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -3863,7 +3865,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Add CA" : {},
|
||||
"Add CA" : {
|
||||
|
||||
},
|
||||
"Add Channel" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -11484,8 +11488,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Client CA Certificate" : {},
|
||||
"Client Configuration" : {},
|
||||
"Client CA Certificate" : {
|
||||
|
||||
},
|
||||
"Client Configuration" : {
|
||||
|
||||
},
|
||||
"Client Hidden" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -12186,7 +12194,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Configuration" : {},
|
||||
"Configuration" : {
|
||||
|
||||
},
|
||||
"Configuration for: %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -13804,6 +13814,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Created by:" : {
|
||||
|
||||
},
|
||||
"Created: %@" : {
|
||||
"localizations" : {
|
||||
|
|
@ -14570,7 +14583,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Delete All" : {},
|
||||
"Delete All" : {
|
||||
|
||||
},
|
||||
"Delete all config, keys and BLE bonds? " : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -18174,7 +18189,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Download TAK Server Data Package" : {},
|
||||
"Download TAK Server Data Package" : {
|
||||
|
||||
},
|
||||
"Drag & Drop Firmware Update" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -18961,7 +18978,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Enable TAK Server" : {},
|
||||
"Enable TAK Server" : {
|
||||
|
||||
},
|
||||
"Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -19728,8 +19747,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Enter P12 Password" : {},
|
||||
"Enter the password for the PKCS#12 file" : {},
|
||||
"Enter P12 Password" : {
|
||||
|
||||
},
|
||||
"Enter the password for the PKCS#12 file" : {
|
||||
|
||||
},
|
||||
"environment" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -23771,7 +23794,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Generate a data package (.zip) to configure TAK clients to connect to this server." : {},
|
||||
"Generate a data package (.zip) to configure TAK clients to connect to this server." : {
|
||||
|
||||
},
|
||||
"Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -27266,10 +27291,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Import" : {},
|
||||
"Import .pem" : {},
|
||||
"Import Custom .p12" : {},
|
||||
"Import Error" : {},
|
||||
"Import" : {
|
||||
|
||||
},
|
||||
"Import .pem" : {
|
||||
|
||||
},
|
||||
"Import Custom .p12" : {
|
||||
|
||||
},
|
||||
"Import Error" : {
|
||||
|
||||
},
|
||||
"Import Route" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -28388,6 +28421,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Last updated by:" : {
|
||||
|
||||
},
|
||||
"Later" : {
|
||||
"comment" : "A button that dismisses an alert without taking any action.",
|
||||
|
|
@ -31354,6 +31390,10 @@
|
|||
"comment" : "A description of the read-only mode feature in TAK Server.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : {
|
||||
"comment" : "Privacy policy text for Meshtastic.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -32997,7 +33037,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"mTLS" : {},
|
||||
"mTLS" : {
|
||||
|
||||
},
|
||||
"Multiplier" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -39159,7 +39201,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Port" : {},
|
||||
"Port" : {
|
||||
|
||||
},
|
||||
"Position" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -42816,7 +42860,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Reload Bundled Certificates" : {},
|
||||
"Reload Bundled Certificates" : {
|
||||
|
||||
},
|
||||
"Remote administration for: %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -43623,7 +43669,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Reset to Default" : {},
|
||||
"Reset to Default" : {
|
||||
|
||||
},
|
||||
"Restart" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -43676,7 +43724,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Restart Server" : {},
|
||||
"Restart Server" : {
|
||||
|
||||
},
|
||||
"Restart to the node you are connected to" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -46448,8 +46498,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Secure mTLS connection on port 8089. Both server and client certificates are required." : {},
|
||||
|
||||
"Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent." : {
|
||||
"comment" : "A footer for the TAK Server configuration section.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
|
@ -49143,7 +49191,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Server Certificate" : {},
|
||||
"Server Certificate" : {
|
||||
|
||||
},
|
||||
"Server Option" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -49190,7 +49240,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Server Status" : {},
|
||||
"Server Status" : {
|
||||
|
||||
},
|
||||
"Set" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -49237,6 +49289,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Set a channel name" : {
|
||||
"comment" : "A label for a button that sets a channel name.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Set LoRa Region" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -49856,6 +49912,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Share with TAK Buddies" : {
|
||||
"comment" : "A button that shares the QR code with TAK buddies.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Share your location in real-time and keep your group coordinated with integrated GPS features." : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -52018,7 +52078,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Status" : {},
|
||||
"Status" : {
|
||||
|
||||
},
|
||||
"Stay Connected Anywhere" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
|
|
@ -52658,7 +52720,17 @@
|
|||
}
|
||||
|
||||
},
|
||||
"TAK Server" : {},
|
||||
"TAK Cannot Be Used on Public Channel" : {
|
||||
"comment" : "A warning displayed when the user's primary channel is public.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"TAK Channel Index" : {
|
||||
"comment" : "A label for the TAK channel index.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"TAK Server" : {
|
||||
|
||||
},
|
||||
"TAK Tracker" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -55989,7 +56061,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"TLS Certificates" : {},
|
||||
"TLS Certificates" : {
|
||||
|
||||
},
|
||||
"TLS Enabled" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@
|
|||
8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; };
|
||||
9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; };
|
||||
A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; };
|
||||
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; };
|
||||
ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; };
|
||||
ABB99DEB2E2EA1C500CFBD05 /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */; };
|
||||
B16C760DB291CFAB5335EADB /* TAKCertificateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */; };
|
||||
|
|
@ -434,6 +435,7 @@
|
|||
8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetrics.swift; sourceTree = "<group>"; };
|
||||
8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = "<group>"; };
|
||||
9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKDataPackageGenerator.swift; sourceTree = "<group>"; };
|
||||
AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = "<group>"; };
|
||||
ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconButton.swift; sourceTree = "<group>"; };
|
||||
ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = "<group>"; };
|
||||
B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1988,6 +1990,9 @@
|
|||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -2010,6 +2015,9 @@
|
|||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.MeshtasticTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -2174,7 +2182,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
MARKETING_VERSION = 2.7.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
|
|
@ -2213,7 +2221,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
MARKETING_VERSION = 2.7.10;
|
||||
OTHER_LDFLAGS = (
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
|
|
@ -2249,12 +2257,14 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
MARKETING_VERSION = 2.7.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -2282,12 +2292,14 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
MARKETING_VERSION = 2.7.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
|||
|
|
@ -59,8 +59,7 @@ extension MessageEntity {
|
|||
let users = try context.fetch(request)
|
||||
|
||||
// If exactly one match is found, return its name
|
||||
if users.count == 1, let name = users.first?.longName, !name.isEmpty
|
||||
{
|
||||
if users.count == 1, let name = users.first?.longName, !name.isEmpty {
|
||||
return "\(name)"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1175,7 +1175,7 @@ actor MeshPackets {
|
|||
// Fetch waypoint by waypointMessage.id, not packet.id
|
||||
let fetchWaypointRequest = WaypointEntity.fetchRequest()
|
||||
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id))
|
||||
|
||||
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
|
||||
// Fetch the node info to get the short name
|
||||
var nodeShortName: String = "?"
|
||||
|
|
@ -1199,6 +1199,7 @@ actor MeshPackets {
|
|||
waypoint.longitudeI = waypointMessage.longitudeI
|
||||
waypoint.icon = Int64(waypointMessage.icon)
|
||||
waypoint.locked = Int64(waypointMessage.lockedTo)
|
||||
waypoint.createdBy = Int64(packet.from)
|
||||
if waypointMessage.expire >= 1 {
|
||||
waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
} else {
|
||||
|
|
@ -1254,6 +1255,7 @@ actor MeshPackets {
|
|||
existingWaypoint.longitudeI = waypointMessage.longitudeI
|
||||
existingWaypoint.icon = Int64(waypointMessage.icon)
|
||||
existingWaypoint.locked = Int64(waypointMessage.lockedTo)
|
||||
existingWaypoint.lastUpdatedBy = Int64(packet.from)
|
||||
if waypointMessage.expire >= 1 {
|
||||
existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
} else {
|
||||
|
|
@ -1278,4 +1280,3 @@ actor MeshPackets {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -184,4 +184,3 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
|
|||
Logger.mqtt.debug("📲 [MQTT Client Proxy] pong")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,10 +71,7 @@ final class CoTXMLParser: NSObject, XMLParserDelegate {
|
|||
}
|
||||
|
||||
// MARK: - XMLParserDelegate
|
||||
|
||||
func parser(_ parser: XMLParser, didStartElement elementName: String,
|
||||
namespaceURI: String?, qualifiedName qName: String?,
|
||||
attributes attributeDict: [String: String] = [:]) {
|
||||
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
|
||||
elementStack.append(elementName)
|
||||
currentElement = elementName
|
||||
currentText = ""
|
||||
|
|
@ -138,8 +135,7 @@ final class CoTXMLParser: NSObject, XMLParserDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, didEndElement elementName: String,
|
||||
namespaceURI: String?, qualifiedName qName: String?) {
|
||||
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||
if elementName == "remarks" {
|
||||
remarksText = currentText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -494,4 +494,3 @@ enum TAKConnectionError: LocalizedError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -490,10 +490,12 @@
|
|||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="createdBy" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastUpdatedBy" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
|
|
|
|||
|
|
@ -565,18 +565,18 @@ struct DeviceConnectRow: View {
|
|||
}
|
||||
// Show transport type
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
HStack(alignment: .center){
|
||||
HStack(alignment: .center) {
|
||||
TransportIcon(transportType: device.transportType)
|
||||
if device.isManualConnection && (device.longName != nil || device.shortName != nil) {
|
||||
VStack (alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Last seen device:")
|
||||
Text("\(String(describing: device))")
|
||||
}
|
||||
}
|
||||
}.padding(.top, 3.0)
|
||||
#else
|
||||
//Different alignment for Mac
|
||||
HStack(alignment: .firstTextBaseline){
|
||||
// Different alignment for Mac
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
TransportIcon(transportType: device.transportType)
|
||||
if device.isManualConnection && (device.longName != nil || device.shortName != nil) {
|
||||
Text("Last seen device: \(String(describing: device))")
|
||||
|
|
@ -609,4 +609,3 @@ struct DeviceConnectRow: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,3 +62,7 @@ struct InvalidVersion: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
InvalidVersion(minimumVersion: "2.5.4", version: "2.3.0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,3 +94,11 @@ enum BLESignalStrength: Int {
|
|||
case normal = 1
|
||||
case strong = 2
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HStack(spacing: 16) {
|
||||
SignalStrengthIndicator(signalStrength: .weak)
|
||||
SignalStrengthIndicator(signalStrength: .normal)
|
||||
SignalStrengthIndicator(signalStrength: .strong)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,3 +111,15 @@ struct BatteryCompact: View {
|
|||
} ?? "Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 12) {
|
||||
BatteryCompact(batteryLevel: 75, font: .caption, iconFont: .caption, color: .gray)
|
||||
BatteryCompact(batteryLevel: 50, font: .caption, iconFont: .caption, color: .gray)
|
||||
BatteryCompact(batteryLevel: 25, font: .caption, iconFont: .caption, color: .gray)
|
||||
BatteryCompact(batteryLevel: 10, font: .caption, iconFont: .caption, color: .gray)
|
||||
BatteryCompact(batteryLevel: 100, font: .caption, iconFont: .caption, color: .gray)
|
||||
BatteryCompact(batteryLevel: 101, font: .caption, iconFont: .caption, color: .gray)
|
||||
BatteryCompact(batteryLevel: nil, font: .caption, iconFont: .caption, color: .gray)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,15 @@ struct ChannelLock: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let encryptedChannel = ChannelEntity(context: context)
|
||||
encryptedChannel.psk = Data([0x01, 0x02, 0x03, 0x04])
|
||||
let unencryptedChannel = ChannelEntity(context: context)
|
||||
unencryptedChannel.psk = Data()
|
||||
return HStack(spacing: 16) {
|
||||
ChannelLock(channel: encryptedChannel)
|
||||
ChannelLock(channel: unencryptedChannel)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ struct CompassView: View {
|
|||
}
|
||||
|
||||
// Trigger a vibration if aligned with waypoint
|
||||
private func checkAlignment(bearing: Double,heading: Double) {
|
||||
private func checkAlignment(bearing: Double, heading: Double) {
|
||||
// Compute minimal angular difference between heading and bearing in [0, 180]
|
||||
let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360)
|
||||
let diff = min(rawDiff, 360 - rawDiff)
|
||||
|
|
@ -53,7 +53,6 @@ struct CompassView: View {
|
|||
inAlignment = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func distanceToWaypoint() -> CLLocationDistance? {
|
||||
guard
|
||||
|
|
@ -76,7 +75,6 @@ struct CompassView: View {
|
|||
return formatter.string(from: measurement)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 15) {
|
||||
|
|
@ -88,14 +86,14 @@ struct CompassView: View {
|
|||
.foregroundColor(color)
|
||||
|
||||
if let wp = waypointLocation {
|
||||
HStack{
|
||||
HStack {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
if let distance = distanceToWaypoint() {
|
||||
HStack{
|
||||
HStack {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
Text("Distance: \(formatDistance(distance))")
|
||||
.font(.subheadline)
|
||||
|
|
@ -137,7 +135,7 @@ struct CompassView: View {
|
|||
)
|
||||
// Move waypoint marker outside compass
|
||||
.onChange(of: locationsHandler.heading) { _, _ in
|
||||
checkAlignment(bearing: bearing,heading:locationsHandler.heading)
|
||||
checkAlignment(bearing: bearing, heading:locationsHandler.heading)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,9 +157,7 @@ struct CompassView: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Waypoint Marker View
|
||||
|
||||
struct WaypointMarkerView: View {
|
||||
let bearing: Double
|
||||
let compassDegrees: Double
|
||||
|
|
@ -177,9 +173,7 @@ struct WaypointMarkerView: View {
|
|||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Bearing Calculator
|
||||
|
||||
struct BearingCalculator {
|
||||
|
||||
static func bearingBetween(
|
||||
|
|
@ -205,9 +199,7 @@ struct BearingCalculator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Marker Model
|
||||
|
||||
struct Marker: Hashable {
|
||||
let degrees: Double
|
||||
let label: String
|
||||
|
|
@ -239,9 +231,7 @@ struct Marker: Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Compass Marker View
|
||||
|
||||
struct CompassMarkerView: View {
|
||||
let marker: Marker
|
||||
let compassDegrees: Double
|
||||
|
|
@ -281,9 +271,7 @@ struct CompassMarkerView: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
struct CompassView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CompassView(
|
||||
|
|
|
|||
|
|
@ -28,3 +28,11 @@ struct DateTimeText: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
DateTimeText(dateTime: Date())
|
||||
DateTimeText(dateTime: Calendar.current.date(byAdding: .day, value: -1, to: Date()))
|
||||
DateTimeText(dateTime: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,8 @@ struct MeshtasticLogo: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MeshtasticLogo()
|
||||
.frame(width: 200, height: 44)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,3 +36,15 @@ struct MessageTemplate: View {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test User"
|
||||
user.shortName = "TU"
|
||||
let message = MessageEntity(context: context)
|
||||
message.messagePayload = "Hello, World!"
|
||||
message.messageTimestamp = Int32(Date().timeIntervalSince1970)
|
||||
message.replyID = 0
|
||||
return MessageTemplate(user: user, message: message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,3 +94,15 @@ struct PowerMetricCompactWidget: View {
|
|||
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
|
||||
Form {
|
||||
LazyVGrid(columns: gridItemLayout) {
|
||||
PowerMetricCompactWidget(type: .voltage, value: 3.72, title: "Channel 1 Voltage")
|
||||
PowerMetricCompactWidget(type: .current, value: 125.3, title: "Channel 1 Current")
|
||||
PowerMetricCompactWidget(type: .voltage, value: 5.01, title: "Channel 2 Voltage")
|
||||
PowerMetricCompactWidget(type: .current, value: 42.7, title: "Channel 2 Current")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,3 +119,12 @@ struct LEDIndicator: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HStack(spacing: 12) {
|
||||
LEDIndicator(flash: .constant(1), color: .green)
|
||||
.frame(width: 10, height: 10)
|
||||
LEDIndicator(flash: .constant(0), color: .red)
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ public struct RateLimitedButton<Content: View>: View {
|
|||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
RateLimitedButton(key: "preview", rateLimit: 30, action: { }) { rateLimitInfo in
|
||||
if let info = rateLimitInfo {
|
||||
Label("\(Int(info.secondsRemaining))s", systemImage: "clock")
|
||||
} else {
|
||||
Label("Send", systemImage: "paperplane")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To store the time an action occured (name by a key) and the time limit
|
||||
// Does not persist across app launches
|
||||
class RateLimitStorage: ObservableObject {
|
||||
|
|
|
|||
|
|
@ -69,3 +69,11 @@ struct SecureInput: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
SecureInput("Password", text: .constant("s3cretP@ss"), isValid: .constant(true))
|
||||
SecureInput("Invalid Key", text: .constant("short"), isValid: .constant(false))
|
||||
SecureInput("Empty", text: .constant(""), isValid: .constant(true))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,3 +118,7 @@ func calculateDewPoint(temp: Float, relativeHumidity: Float, convertToLocale: Bo
|
|||
}
|
||||
return dewPointUnit.converted(to: format).value
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalWeatherConditions(location: CLLocation(latitude: 47.6062, longitude: -122.3321))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ struct TraceRouteComponent<V: View>: View {
|
|||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TraceRouteComponent {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.font(.title)
|
||||
}
|
||||
}
|
||||
|
||||
struct TraceRoute: Layout {
|
||||
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||
get {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ struct MessageContextMenuItems: View {
|
|||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
|
||||
// Compute a relay display string if relayNode is present
|
||||
|
||||
|
||||
VStack {
|
||||
Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))")
|
||||
.foregroundColor(.gray)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,12 @@ struct TapbackInputView: View {
|
|||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TapbackInputView(text: .constant(""), isPresented: .constant(true)) { emoji in
|
||||
print("Selected: \(emoji)")
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
var firstResponder: UIView? {
|
||||
guard !isFirstResponder else { return self }
|
||||
|
|
@ -90,4 +96,3 @@ extension UIView {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,3 +141,16 @@ struct DetectionSensorLog: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return DetectionSensorLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,3 +254,16 @@ struct DeviceMetricsLog: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return DeviceMetricsLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,3 +186,16 @@ struct EnvironmentMetricsLog: View {
|
|||
return lower...upper
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return EnvironmentMetricsLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,3 +40,13 @@ struct ClientHistoryButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
connectedNode.num = 987654321
|
||||
return ClientHistoryButton(connectedNode: connectedNode, node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,3 +64,14 @@ struct DeleteNodeButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
connectedNode.num = 987654321
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
return DeleteNodeButton(connectedNode: connectedNode, node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,3 +61,13 @@ struct ExchangePositionsButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
connectedNode.num = 987654321
|
||||
return ExchangePositionsButton(node: node, connectedNode: connectedNode)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,3 +59,13 @@ struct ExchangeUserInfoButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let connectedNode = NodeInfoEntity(context: context)
|
||||
connectedNode.num = 987654321
|
||||
return ExchangeUserInfoButton(node: node, connectedNode: connectedNode)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,3 +79,16 @@ struct FavoriteNodeButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return FavoriteNodeButton(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,12 @@ struct IgnoreNodeButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
return IgnoreNodeButton(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,3 +54,15 @@ struct NavigateToButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
user.num = 123456789
|
||||
node.user = user
|
||||
return NavigateToButton(node: node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,3 +31,14 @@ struct NodeAlertsButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return NodeAlertsButton(context: context, node: node, user: user)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,3 +43,15 @@ struct TraceRouteButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return TraceRouteButton(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ struct AnimatedNodePin: View, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 20) {
|
||||
AnimatedNodePin(nodeColor: .systemBlue, shortName: "TN", hasDetectionSensorMetrics: false, isOnline: true, calculatedDelay: 0.0)
|
||||
AnimatedNodePin(nodeColor: .systemGreen, shortName: "AB", hasDetectionSensorMetrics: true, isOnline: true, calculatedDelay: 0.2)
|
||||
AnimatedNodePin(nodeColor: .systemRed, shortName: "XY", hasDetectionSensorMetrics: false, isOnline: false, calculatedDelay: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
struct PulsingCircle: View {
|
||||
let nodeColor: UIColor
|
||||
let calculatedDelay: Double
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,3 +228,13 @@ struct MapSettingsForm: View {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MapSettingsForm(
|
||||
traffic: .constant(false),
|
||||
pointsOfInterest: .constant(true),
|
||||
mapLayer: .constant(.standard),
|
||||
meshMap: .constant(true),
|
||||
enabledOverlayConfigs: .constant(Set<UUID>())
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import MapKit
|
|||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct WaypointForm: View {
|
||||
|
||||
|
|
@ -31,134 +32,218 @@ struct WaypointForm: View {
|
|||
@State private var lockedTo: Int64 = 0
|
||||
@State private var selectedDetent: PresentationDetent = .medium
|
||||
@State private var waypointFailedAlert: Bool = false
|
||||
@State private var createdByNode : NodeInfoEntity? = nil
|
||||
@State private var lastUpdatedByNode : NodeInfoEntity? = nil
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
if editMode {
|
||||
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
|
||||
.font(.largeTitle)
|
||||
Divider()
|
||||
Form {
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
Group {
|
||||
if editMode {
|
||||
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
|
||||
.font(.largeTitle)
|
||||
Divider()
|
||||
Form {
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
HStack {
|
||||
Text("Location:")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
Button {
|
||||
waypoint.coordinate.longitude = cl.longitude
|
||||
waypoint.coordinate.latitude = cl.latitude
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Location:")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
Button {
|
||||
waypoint.coordinate.longitude = cl.longitude
|
||||
waypoint.coordinate.latitude = cl.latitude
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Use my Location")
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
.accessibilityLabel("Set to current location")
|
||||
HStack {
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
DistanceText(meters: distance)
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
Text("Use my Location")
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("Waypoint Options")) {
|
||||
.accessibilityLabel("Set to current location")
|
||||
HStack {
|
||||
Text("Name")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Name",
|
||||
text: $name,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
waypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
DistanceText(meters: distance)
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Description",
|
||||
text: $description,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description) {
|
||||
var totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
description = String(description.dropLast())
|
||||
totalBytes = description.utf8.count
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
Spacer()
|
||||
TextField("Select an emoji", text: $icon)
|
||||
.keyboardType(.emoji)
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) { _, value in
|
||||
// If a second emoji is entered delete the first one
|
||||
if value.count >= 1 {
|
||||
if value.count > 1 {
|
||||
let index = value.index(value.startIndex, offsetBy: 1)
|
||||
icon = String(value[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $expires) {
|
||||
Label("Expires", systemImage: "clock.badge.xmark")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if expires {
|
||||
DatePicker("Expire", selection: $expire, in: Date.now...)
|
||||
.datePickerStyle(.compact)
|
||||
.font(.callout)
|
||||
}
|
||||
Toggle(isOn: $locked) {
|
||||
Label("Locked", systemImage: "lock")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.warning("Send waypoint failed: No deviceNum")
|
||||
return
|
||||
}
|
||||
if accessoryManager.isConnected {
|
||||
/// Send a new or exiting waypoint
|
||||
var newWaypoint = Waypoint()
|
||||
if waypoint.id == 0 {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
waypoint.id = Int64(newWaypoint.id)
|
||||
} else {
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
Section(header: Text("Waypoint Options")) {
|
||||
HStack {
|
||||
Text("Name")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Name",
|
||||
text: $name,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: name) {
|
||||
var totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 30 {
|
||||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
waypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Description")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Description",
|
||||
text: $description,
|
||||
axis: .vertical
|
||||
)
|
||||
.foregroundColor(Color.gray)
|
||||
.onChange(of: description) {
|
||||
var totalBytes = description.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > 100 {
|
||||
description = String(description.dropLast())
|
||||
totalBytes = description.utf8.count
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Text("Icon")
|
||||
Spacer()
|
||||
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
|
||||
.font(.title)
|
||||
.focused($iconIsFocused)
|
||||
.onChange(of: icon) {
|
||||
// If it contains non-emoji characters, clear it
|
||||
if !icon.onlyEmojis() {
|
||||
icon = ""
|
||||
return
|
||||
}
|
||||
|
||||
// If multiple emojis are entered or pasted, keep only the last one
|
||||
if icon.count > 1 {
|
||||
icon = String(icon.suffix(1))
|
||||
}
|
||||
iconIsFocused = false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Toggle(isOn: $expires) {
|
||||
Label("Expires", systemImage: "clock.badge.xmark")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if expires {
|
||||
DatePicker("Expire", selection: $expire, in: Date.now...)
|
||||
.datePickerStyle(.compact)
|
||||
.font(.callout)
|
||||
}
|
||||
Toggle(isOn: $locked) {
|
||||
Label("Locked", systemImage: "lock")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.warning("Send waypoint failed: No deviceNum")
|
||||
return
|
||||
}
|
||||
if accessoryManager.isConnected {
|
||||
/// Send a new or exiting waypoint
|
||||
var newWaypoint = Waypoint()
|
||||
if waypoint.id == 0 {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
waypoint.createdBy = Int64(deviceNum)
|
||||
waypoint.id = Int64(newWaypoint.id)
|
||||
} else {
|
||||
waypoint.lastUpdatedBy = Int64(deviceNum)
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
}
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(deviceNum)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
|
||||
dismiss()
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed: \(error)")
|
||||
Task { @MainActor in
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.disabled(!accessoryManager.isConnected)
|
||||
.padding(.bottom)
|
||||
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Cancel", systemImage: "x.circle")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
|
||||
if waypoint.id > 0 && accessoryManager.isConnected {
|
||||
|
||||
Menu {
|
||||
Button("For me", action: {
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss() })
|
||||
Button("For everyone", action: {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.error("Unable to set waypoint: No Device num")
|
||||
return
|
||||
}
|
||||
var newWaypoint = Waypoint()
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
|
|
@ -171,101 +256,28 @@ struct WaypointForm: View {
|
|||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
|
||||
newWaypoint.expire = UInt32(1)
|
||||
Task {
|
||||
do {
|
||||
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
|
||||
dismiss()
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed: \(error)")
|
||||
Task { @MainActor in
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
Task {@MainActor in
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
})
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.disabled(!accessoryManager.isConnected)
|
||||
.padding(.bottom)
|
||||
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Cancel", systemImage: "x.circle")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
|
||||
if waypoint.id > 0 && accessoryManager.isConnected {
|
||||
|
||||
Menu {
|
||||
Button("For me", action: {
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss() })
|
||||
Button("For everyone", action: {
|
||||
guard let deviceNum = accessoryManager.activeDeviceNum else {
|
||||
Logger.mesh.error("Unable to set waypoint: No Device num")
|
||||
return
|
||||
}
|
||||
var newWaypoint = Waypoint()
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(deviceNum)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
newWaypoint.expire = UInt32(1)
|
||||
Task {
|
||||
do {
|
||||
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
|
||||
Task { @MainActor in
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} catch {
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
Task {@MainActor in
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
|
|
@ -274,130 +286,167 @@ struct WaypointForm: View {
|
|||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
|
||||
Spacer()
|
||||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
Button {
|
||||
editMode = true
|
||||
selectedDetent = .fraction(0.85)
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil" )
|
||||
.font(.largeTitle)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
|
||||
Spacer()
|
||||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
Button {
|
||||
editMode = true
|
||||
selectedDetent = .fraction(0.85)
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil" )
|
||||
.font(.largeTitle)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(.accentColor)
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
// Nodes who created/modified
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let created = createdByNode {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Created by:")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
CircleText(
|
||||
text: created.user?.shortName ?? "?",
|
||||
color: Color(UIColor(hex: UInt32(created.user?.num ?? 0x808080)))
|
||||
)
|
||||
Text(created.user?.longName ?? "Unknown")
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let updated = lastUpdatedByNode {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Last updated by:")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
CircleText(
|
||||
text: updated.user?.shortName ?? "?",
|
||||
color: Color(UIColor(hex: UInt32(updated.user?.num ?? 0x808080)))
|
||||
)
|
||||
Text(updated.user?.longName ?? "Unknown")
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
// Description
|
||||
if (waypoint.longDescription ?? "").count > 0 {
|
||||
Label {
|
||||
Text(waypoint.longDescription ?? "")
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} icon: {
|
||||
Image(systemName: "doc.plaintext")
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
/// Coordinate
|
||||
.padding(.bottom)
|
||||
|
||||
// Description
|
||||
if (waypoint.longDescription ?? "").count > 0 {
|
||||
Label {
|
||||
Text("Coordinates:")
|
||||
Text(waypoint.longDescription ?? "")
|
||||
.foregroundColor(.primary)
|
||||
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.circle")
|
||||
Image(systemName: "doc.plaintext")
|
||||
}
|
||||
.padding(.bottom)
|
||||
// Drop Maps Pin
|
||||
Button(action: {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
/// Coordinate
|
||||
Label {
|
||||
Text("Coordinates:")
|
||||
.foregroundColor(.primary)
|
||||
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.circle")
|
||||
}
|
||||
.padding(.bottom)
|
||||
// Drop Maps Pin
|
||||
Button(action: {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Created
|
||||
}) {
|
||||
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Created
|
||||
Label {
|
||||
Text("Created: \(waypoint.created?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Updated
|
||||
if waypoint.lastUpdated != nil {
|
||||
Label {
|
||||
Text("Created: \(waypoint.created?.formatted() ?? "?")")
|
||||
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Updated
|
||||
if waypoint.lastUpdated != nil {
|
||||
Label {
|
||||
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
/// Expires
|
||||
if waypoint.expire != nil {
|
||||
Label {
|
||||
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "hourglass.bottomhalf.filled")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
/// Expires
|
||||
if waypoint.expire != nil {
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
if cl.distance(from: cl) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: cl)
|
||||
Label {
|
||||
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "hourglass.bottomhalf.filled")
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if let cl = LocationsHandler.currentLocation {
|
||||
if cl.distance(from: cl) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: cl)
|
||||
Label {
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
.padding(.top)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) {
|
||||
Button("OK", role: .cancel) {
|
||||
context.delete(waypoint)
|
||||
|
|
@ -421,6 +470,9 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await fetchNodeInfo()
|
||||
}
|
||||
.onAppear {
|
||||
if waypoint.id > 0 {
|
||||
let waypoint = getWaypoint(id: Int64(waypoint.id), context: context)
|
||||
|
|
@ -453,4 +505,37 @@ struct WaypointForm: View {
|
|||
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
private func fetchNodeInfo() async {
|
||||
// --- Fetch createdBy node ---
|
||||
if waypoint.createdBy != 0 {
|
||||
let createdByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy))
|
||||
createdByFetch.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(createdByFetch)
|
||||
createdByNode = nodes.first
|
||||
} catch {
|
||||
Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fetch lastUpdatedBy node (only if different from createdBy) ---
|
||||
if waypoint.lastUpdatedBy != 0,
|
||||
waypoint.lastUpdatedBy != waypoint.createdBy {
|
||||
let updatedByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
|
||||
updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy))
|
||||
updatedByFetch.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let nodes = try context.fetch(updatedByFetch)
|
||||
lastUpdatedByNode = nodes.first
|
||||
} catch {
|
||||
Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -99,3 +99,10 @@ struct MetricsColumnDetail: View {
|
|||
.interactiveDismissDisabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MetricsColumnDetail(
|
||||
columnList: MetricsColumnList(columns: []),
|
||||
seriesList: MetricsSeriesList()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,3 +208,7 @@ struct NodeListFilter: View {
|
|||
.presentationBackgroundInteraction(.enabled(upThrough: .large))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NodeListFilter(filters: NodeFilterParameters())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,12 +120,17 @@ struct MeshMap: View {
|
|||
}
|
||||
.sheet(item: $selectedWaypoint) { selection in
|
||||
WaypointForm(waypoint: selection)
|
||||
.presentationDetents([.large])
|
||||
.padding()
|
||||
.presentationDetents([.large]) // full screen
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(item: $editingWaypoint) { selection in
|
||||
WaypointForm(waypoint: selection, editMode: true)
|
||||
.padding()
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
.sheet(isPresented: $editingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,3 +224,16 @@ struct PaxCounterLog: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return PaxCounterLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,3 +181,16 @@ struct PositionLog: View {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return PositionLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,3 +297,16 @@ struct PowerMetricsLog: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return PowerMetricsLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,3 +287,16 @@ func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity]
|
|||
array.append(trh8)
|
||||
return array
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
let node = NodeInfoEntity(context: context)
|
||||
node.num = 123456789
|
||||
let user = UserEntity(context: context)
|
||||
user.longName = "Test Node"
|
||||
user.shortName = "TN"
|
||||
node.user = user
|
||||
return TraceRouteLog(node: node)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -454,3 +454,8 @@ struct DeviceOnboarding: View {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DeviceOnboarding()
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,3 +67,9 @@ struct AboutMeshtastic: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
AboutMeshtastic()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,3 +143,10 @@ struct AppData: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return AppData()
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,3 +271,7 @@ extension AppLog {
|
|||
}
|
||||
|
||||
extension OSLogEntry: @retroactive Identifiable { }
|
||||
|
||||
#Preview {
|
||||
AppLog()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,3 +250,21 @@ struct ChannelForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChannelForm(
|
||||
channelIndex: .constant(0),
|
||||
channelName: .constant("LongFast"),
|
||||
channelKeySize: .constant(32),
|
||||
channelKey: .constant("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="),
|
||||
channelRole: .constant(1),
|
||||
uplink: .constant(false),
|
||||
downlink: .constant(false),
|
||||
positionPrecision: .constant(14),
|
||||
preciseLocation: .constant(false),
|
||||
positionsEnabled: .constant(true),
|
||||
hasChanges: .constant(false),
|
||||
hasValidKey: .constant(true),
|
||||
supportedVersion: .constant(true)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,3 +147,10 @@ struct BluetoothConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return BluetoothConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,3 +37,13 @@ struct ConfigHeader<T>: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ConfigHeader(
|
||||
title: "Bluetooth Configuration",
|
||||
config: \NodeInfoEntity.bluetoothConfig,
|
||||
node: nil,
|
||||
onAppear: { }
|
||||
)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -345,3 +345,10 @@ struct DeviceConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return DeviceConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,3 +235,10 @@ struct DisplayConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return DisplayConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,3 +321,10 @@ struct LoRaConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return LoRaConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,3 +134,10 @@ struct AmbientLightingConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return AmbientLightingConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,3 +355,10 @@ struct CannedMessagesConfig: View {
|
|||
self.hasMessagesChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return CannedMessagesConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,3 +261,10 @@ struct DetectionSensorConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return DetectionSensorConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,3 +283,9 @@ struct ExternalNotificationConfig: View {
|
|||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return ExternalNotificationConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -464,3 +464,10 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return MQTTConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,3 +123,10 @@ struct PaxCounterConfig: View {
|
|||
paxcounterUpdateInterval = UpdateInterval(from: Int(node?.paxCounterConfig?.updateInterval ?? 1800))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return PaxCounterConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,3 +143,10 @@ struct RangeTestConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return RangeTestConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,3 +114,10 @@ struct RtttlConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return RtttlConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,3 +205,10 @@ struct SerialConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return SerialConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,3 +197,10 @@ struct StoreForwardConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return StoreForwardConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,3 +241,10 @@ struct TelemetryConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return TelemetryConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,3 +209,10 @@ struct NetworkConfig: View {
|
|||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return NetworkConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -562,3 +562,10 @@ struct PositionConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return PositionConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,3 +226,10 @@ private struct FloatField: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return PowerConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,3 +58,8 @@ struct SaveConfigButton: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SaveConfigButton(node: nil, hasChanges: .constant(true), onConfirmation: { })
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -428,3 +428,10 @@ struct SecurityConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return SecurityConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,3 +206,10 @@ struct Firmware: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return Firmware(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,3 +65,7 @@ struct GPSStatus: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GPSStatus()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,3 +169,7 @@ struct AppLogFilter: View {
|
|||
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AppLogFilter(categories: .constant(Set<Int>()), levels: .constant(Set<Int>()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,9 +424,7 @@ struct TAKServerConfig: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Channel Label
|
||||
|
||||
@ViewBuilder
|
||||
private func channelLabel(_ channel: ChannelEntity) -> some View {
|
||||
if channel.name?.isEmpty ?? false {
|
||||
|
|
|
|||
|
|
@ -55,3 +55,11 @@ struct UpdateIntervalPicker: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
UpdateIntervalPicker(
|
||||
config: .broadcastShort,
|
||||
pickerLabel: "Update Interval",
|
||||
selectedInterval: .constant(UpdateInterval(from: 30))
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,3 +253,10 @@ struct UserConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let context = PersistenceController.preview.container.viewContext
|
||||
return UserConfig(node: nil)
|
||||
.environmentObject(AccessoryManager.shared)
|
||||
.environment(\.managedObjectContext, context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ struct DeviceTests {
|
|||
(-80, BLESignalStrength.normal),
|
||||
(-84, BLESignalStrength.normal),
|
||||
(-85, BLESignalStrength.weak),
|
||||
(-100, BLESignalStrength.weak),
|
||||
(-100, BLESignalStrength.weak)
|
||||
])
|
||||
func signalStrength(rssi: Int, expected: BLESignalStrength) {
|
||||
let device = Device(
|
||||
|
|
@ -209,7 +209,7 @@ struct TransportTypeTests {
|
|||
@Test(arguments: [
|
||||
(TransportType.ble, "BLE"),
|
||||
(TransportType.tcp, "TCP"),
|
||||
(TransportType.serial, "Serial"),
|
||||
(TransportType.serial, "Serial")
|
||||
])
|
||||
func rawValues(type: TransportType, expected: String) {
|
||||
#expect(type.rawValue == expected)
|
||||
|
|
@ -307,7 +307,7 @@ struct NavigationStateTests {
|
|||
NavigationState.Tab.connect,
|
||||
NavigationState.Tab.nodes,
|
||||
NavigationState.Tab.map,
|
||||
NavigationState.Tab.settings,
|
||||
NavigationState.Tab.settings
|
||||
])
|
||||
func tabRawValues(tab: NavigationState.Tab) {
|
||||
#expect(NavigationState.Tab(rawValue: tab.rawValue) == tab)
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ struct RouterTests {
|
|||
("debugLogs", SettingsNavigationState.debugLogs),
|
||||
("appFiles", SettingsNavigationState.appFiles),
|
||||
("firmwareUpdates", SettingsNavigationState.firmwareUpdates),
|
||||
("tak", SettingsNavigationState.tak),
|
||||
("tak", SettingsNavigationState.tak)
|
||||
])
|
||||
func routeSettingsPage(path: String, expected: SettingsNavigationState) async throws {
|
||||
try await assertRoute(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue