diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 30e6245e..3a8d12cf 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -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" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e3504190..5038f261 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -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 = ""; }; 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; 9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKDataPackageGenerator.swift; sourceTree = ""; }; + AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = ""; }; ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconButton.swift; sourceTree = ""; }; ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; @@ -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"; diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index d6d2c997..c9fab38a 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -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)" } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 54e1661f..4c7ab01e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -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 { } } } - diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 936f23ad..e0cd2473 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -184,4 +184,3 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { Logger.mqtt.debug("📲 [MQTT Client Proxy] pong") } } - diff --git a/Meshtastic/Helpers/TAK/CoTXMLParser.swift b/Meshtastic/Helpers/TAK/CoTXMLParser.swift index 7f9325e2..eada8793 100644 --- a/Meshtastic/Helpers/TAK/CoTXMLParser.swift +++ b/Meshtastic/Helpers/TAK/CoTXMLParser.swift @@ -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) } diff --git a/Meshtastic/Helpers/TAK/TAKConnection.swift b/Meshtastic/Helpers/TAK/TAKConnection.swift index b4678f06..57340154 100644 --- a/Meshtastic/Helpers/TAK/TAKConnection.swift +++ b/Meshtastic/Helpers/TAK/TAKConnection.swift @@ -494,4 +494,3 @@ enum TAKConnectionError: LocalizedError { } } } - diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents index a6e5465f..bc7e7d0b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents @@ -490,10 +490,12 @@ + + diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index b66b1b59..84e90c6e 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -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 { } } } - diff --git a/Meshtastic/Views/Connect/InvalidVersion.swift b/Meshtastic/Views/Connect/InvalidVersion.swift index 5d475756..d6030139 100644 --- a/Meshtastic/Views/Connect/InvalidVersion.swift +++ b/Meshtastic/Views/Connect/InvalidVersion.swift @@ -62,3 +62,7 @@ struct InvalidVersion: View { } } } + +#Preview { + InvalidVersion(minimumVersion: "2.5.4", version: "2.3.0") +} diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index a68b3597..7e83940f 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 60c1307a..aed8472b 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift index 2621311a..facc07cb 100644 --- a/Meshtastic/Views/Helpers/ChannelLock.swift +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Helpers/CompassView.swift b/Meshtastic/Views/Helpers/CompassView.swift index 1e58b224..c7185acf 100644 --- a/Meshtastic/Views/Helpers/CompassView.swift +++ b/Meshtastic/Views/Helpers/CompassView.swift @@ -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( diff --git a/Meshtastic/Views/Helpers/DateTimeText.swift b/Meshtastic/Views/Helpers/DateTimeText.swift index 38e386f3..34ffcdcf 100644 --- a/Meshtastic/Views/Helpers/DateTimeText.swift +++ b/Meshtastic/Views/Helpers/DateTimeText.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 6c717140..099728c9 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -51,3 +51,8 @@ struct MeshtasticLogo: View { #endif } } + +#Preview { + MeshtasticLogo() + .frame(width: 200, height: 44) +} diff --git a/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift b/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift index e604b564..2173c5c5 100644 --- a/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift +++ b/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift @@ -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) +} diff --git a/Meshtastic/Views/Helpers/PowerMetrics.swift b/Meshtastic/Views/Helpers/PowerMetrics.swift index e85c0f6a..6009e5a6 100644 --- a/Meshtastic/Views/Helpers/PowerMetrics.swift +++ b/Meshtastic/Views/Helpers/PowerMetrics.swift @@ -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") + } + } +} diff --git a/Meshtastic/Views/Helpers/RXTXIndicatorView.swift b/Meshtastic/Views/Helpers/RXTXIndicatorView.swift index 860b3734..f31ae179 100644 --- a/Meshtastic/Views/Helpers/RXTXIndicatorView.swift +++ b/Meshtastic/Views/Helpers/RXTXIndicatorView.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Helpers/RateLimitedButton.swift b/Meshtastic/Views/Helpers/RateLimitedButton.swift index 30c5d667..894bd526 100644 --- a/Meshtastic/Views/Helpers/RateLimitedButton.swift +++ b/Meshtastic/Views/Helpers/RateLimitedButton.swift @@ -45,6 +45,16 @@ public struct RateLimitedButton: 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 { diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index 687cc6fe..bbffe662 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -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)) + } +} diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index def3ae6c..967c2c2b 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -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)) +} diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift index fd4d7d92..f8f8e8d0 100644 --- a/Meshtastic/Views/Layouts/TraceRoute.swift +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -22,6 +22,13 @@ struct TraceRouteComponent: View { } } +#Preview { + TraceRouteComponent { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.title) + } +} + struct TraceRoute: Layout { var animatableData: AnimatablePair { get { diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 0d8843ef..8c5a301b 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -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) diff --git a/Meshtastic/Views/Messages/TapbackInputView.swift b/Meshtastic/Views/Messages/TapbackInputView.swift index 36a1e9b0..03c6597c 100644 --- a/Meshtastic/Views/Messages/TapbackInputView.swift +++ b/Meshtastic/Views/Messages/TapbackInputView.swift @@ -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 } } - diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index f86b9acb..ae57cab0 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 90e8d119..09bca3e7 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 84148d2e..ca19ebee 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift index 624ca183..020855e7 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index 70ffc217..a7b096b3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index f303f21e..c71e6b87 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift index 321b1532..595eec4f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift index 83bac1d3..d3a54864 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index c15c69d1..51a8801b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift index 403d1b98..ec130098 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift index 663d2900..179fb1af 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift index 11a14460..62e696d4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift index 6868f499..245142c9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift @@ -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 diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 480a5cba..39e42baa 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -177,6 +177,7 @@ struct MeshMapContent: MapContent { } } } + .annotationTitles(.automatic) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index ca17c6fa..b284d49b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -228,3 +228,13 @@ struct MapSettingsForm: View { } } + +#Preview { + MapSettingsForm( + traffic: .constant(false), + pointsOfInterest: .constant(true), + mapLayer: .constant(.standard), + meshMap: .constant(true), + enabledOverlayConfigs: .constant(Set()) + ) +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index b441bfb4..6054e4af 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -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).. 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).. 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.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.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)") + } + } + } } + + diff --git a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift index e456ce31..e2061044 100644 --- a/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/Metrics Columns/MetricsColumnDetail.swift @@ -99,3 +99,10 @@ struct MetricsColumnDetail: View { .interactiveDismissDisabled(false) } } + +#Preview { + MetricsColumnDetail( + columnList: MetricsColumnList(columns: []), + seriesList: MetricsSeriesList() + ) +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 3236f0d3..9f1c03c1 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -208,3 +208,7 @@ struct NodeListFilter: View { .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } + +#Preview { + NodeListFilter(filters: NodeFilterParameters()) +} diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 8e1c17d5..6414eb3f 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -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) } diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 482e0d69..c4a93e59 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index af307f20..f667f5ee 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/PowerMetricsLog.swift b/Meshtastic/Views/Nodes/PowerMetricsLog.swift index b4578a59..b43b1c47 100644 --- a/Meshtastic/Views/Nodes/PowerMetricsLog.swift +++ b/Meshtastic/Views/Nodes/PowerMetricsLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 3b84aec3..a4c4c91e 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -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) +} diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index d95d6977..f0c9f1f1 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -454,3 +454,8 @@ struct DeviceOnboarding: View { } } + +#Preview { + DeviceOnboarding() + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 98355864..9d95c472 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -67,3 +67,9 @@ struct AboutMeshtastic: View { .navigationBarTitleDisplayMode(.inline) } } + +#Preview { + NavigationView { + AboutMeshtastic() + } +} diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index aa7ac625..16034020 100644 --- a/Meshtastic/Views/Settings/AppData.swift +++ b/Meshtastic/Views/Settings/AppData.swift @@ -143,3 +143,10 @@ struct AppData: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return AppData() + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift index 08c09664..77f90440 100644 --- a/Meshtastic/Views/Settings/AppLog.swift +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -271,3 +271,7 @@ extension AppLog { } extension OSLogEntry: @retroactive Identifiable { } + +#Preview { + AppLog() +} diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 3579b938..78f85344 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -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) + ) +} diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index a5955caa..b1934df3 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index 1331235d..c8ed4f91 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -37,3 +37,13 @@ struct ConfigHeader: View { } } } + +#Preview { + ConfigHeader( + title: "Bluetooth Configuration", + config: \NodeInfoEntity.bluetoothConfig, + node: nil, + onAppear: { } + ) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 6bfd711b..7d0814dc 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -345,3 +345,10 @@ struct DeviceConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return DeviceConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index ddc67d0f..a9240da0 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 17e18dc7..4129e133 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 515ca933..db2c98c2 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index cc0a87eb..54a9a6bd 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 02afcc3a..a56d42b1 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 23f4ef40..b3694ee3 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -283,3 +283,9 @@ struct ExternalNotificationConfig: View { } } +#Preview { + let context = PersistenceController.preview.container.viewContext + return ExternalNotificationConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 5336c5ef..a3417650 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -464,3 +464,10 @@ struct MQTTConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return MQTTConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index edc46ed0..90b83f73 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index ed65ce4e..74386285 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index d052bf7e..6f45103a 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index a773190d..ff070c09 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index bc35258d..577ef4b5 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 44a38cbd..caae19c3 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 92c5cc5e..044e2a2f 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 3b7f65ec..68c8e9bd 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -562,3 +562,10 @@ struct PositionConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return PositionConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 2ad978b5..737bd7f7 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -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) +} diff --git a/Meshtastic/Views/Settings/Config/SaveConfigButton.swift b/Meshtastic/Views/Settings/Config/SaveConfigButton.swift index 43fcb66a..e21f6a1b 100644 --- a/Meshtastic/Views/Settings/Config/SaveConfigButton.swift +++ b/Meshtastic/Views/Settings/Config/SaveConfigButton.swift @@ -58,3 +58,8 @@ struct SaveConfigButton: View { } } } + +#Preview { + SaveConfigButton(node: nil, hasChanges: .constant(true), onConfirmation: { }) + .environmentObject(AccessoryManager.shared) +} diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index c6a6a6d3..d8c2a969 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -428,3 +428,10 @@ struct SecurityConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return SecurityConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 490f7e01..3869d145 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -206,3 +206,10 @@ struct Firmware: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return Firmware(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/GPSStatus.swift b/Meshtastic/Views/Settings/GPSStatus.swift index c92a647c..f138f980 100644 --- a/Meshtastic/Views/Settings/GPSStatus.swift +++ b/Meshtastic/Views/Settings/GPSStatus.swift @@ -65,3 +65,7 @@ struct GPSStatus: View { } } } + +#Preview { + GPSStatus() +} diff --git a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift index 52927149..8b0bfbb3 100644 --- a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift +++ b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift @@ -169,3 +169,7 @@ struct AppLogFilter: View { .presentationBackgroundInteraction(.enabled(upThrough: .medium)) } } + +#Preview { + AppLogFilter(categories: .constant(Set()), levels: .constant(Set())) +} diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index cb8e3666..7e8b6502 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -424,9 +424,7 @@ struct TAKServerConfig: View { } } - // MARK: - Channel Label - @ViewBuilder private func channelLabel(_ channel: ChannelEntity) -> some View { if channel.name?.isEmpty ?? false { diff --git a/Meshtastic/Views/Settings/UpdateIntervalPicker.swift b/Meshtastic/Views/Settings/UpdateIntervalPicker.swift index c8601624..46e59088 100644 --- a/Meshtastic/Views/Settings/UpdateIntervalPicker.swift +++ b/Meshtastic/Views/Settings/UpdateIntervalPicker.swift @@ -55,3 +55,11 @@ struct UpdateIntervalPicker: View { } } } + +#Preview { + UpdateIntervalPicker( + config: .broadcastShort, + pickerLabel: "Update Interval", + selectedInterval: .constant(UpdateInterval(from: 30)) + ) +} diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index ef0c1ffb..aab5a127 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -253,3 +253,10 @@ struct UserConfig: View { } } } + +#Preview { + let context = PersistenceController.preview.container.viewContext + return UserConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/MeshtasticTests/ConnectViewTests.swift b/MeshtasticTests/ConnectViewTests.swift index cbbcd331..450591ad 100644 --- a/MeshtasticTests/ConnectViewTests.swift +++ b/MeshtasticTests/ConnectViewTests.swift @@ -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) diff --git a/MeshtasticTests/RouterTests.swift b/MeshtasticTests/RouterTests.swift index 1175dc59..11af217c 100644 --- a/MeshtasticTests/RouterTests.swift +++ b/MeshtasticTests/RouterTests.swift @@ -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(