diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index ce7109d7..0d6c3898 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; }; + DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; }; DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */; }; @@ -98,6 +99,7 @@ DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; + DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; }; DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; }; DDCFF601285453A7005FA625 /* localonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCFF600285453A7005FA625 /* localonly.pb.swift */; }; DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; }; @@ -186,6 +188,7 @@ DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; + DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAnnotation.swift; sourceTree = ""; }; @@ -222,6 +225,8 @@ DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = ""; }; + DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = ""; }; DDCFF600285453A7005FA625 /* localonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = localonly.pb.swift; sourceTree = ""; }; DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; @@ -302,13 +307,14 @@ isa = PBXGroup; children = ( DD97E96728EFE9A00056DDA4 /* About.swift */, - DD3501882852FC3B000FC853 /* Settings.swift */, - DD4A911D2708C65400501B7E /* AppSettings.swift */, - DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, - DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */, - DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */, - DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */, DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, + DD4A911D2708C65400501B7E /* AppSettings.swift */, + DDA0B6B1294CDC55001356EC /* Channels.swift */, + DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, + DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */, + DD3501882852FC3B000FC853 /* Settings.swift */, + DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */, + DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */, DD61937A2863876A00E59241 /* Config */, ); path = Settings; @@ -401,6 +407,7 @@ DDC2E14B26CE248E0042C5E4 = { isa = PBXGroup; children = ( + DDCDC6CD29481FCC004C1DDA /* Localizable.strings */, DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */, DDC2E15626CE248E0042C5E4 /* Meshtastic */, DDC2E16D26CE248F0042C5E4 /* MeshtasticTests */, @@ -641,6 +648,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + de, Base, ); mainGroup = DDC2E14B26CE248E0042C5E4; @@ -665,6 +673,7 @@ buildActionMask = 2147483647; files = ( DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */, + DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */, DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -731,6 +740,7 @@ DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, + DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */, @@ -835,11 +845,24 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + DDCDC6CD29481FCC004C1DDA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + DDCDC6CC29481FCC004C1DDA /* en */, + DDCDC6CE294821AD004C1DDA /* de */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ DDC2E17C26CE248F0042C5E4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -901,6 +924,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -976,7 +1000,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.7; + MARKETING_VERSION = 2.0.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1009,7 +1033,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.7; + MARKETING_VERSION = 2.0.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/Meshtastic/Enums/BluetoothModes.swift b/Meshtastic/Enums/BluetoothModes.swift index c631defa..69f926e6 100644 --- a/Meshtastic/Enums/BluetoothModes.swift +++ b/Meshtastic/Enums/BluetoothModes.swift @@ -4,6 +4,7 @@ // // Copyright(c) Garth Vander Houwen 8/19/22. // +import Foundation enum BluetoothModes: Int, CaseIterable, Identifiable { @@ -16,11 +17,11 @@ enum BluetoothModes: Int, CaseIterable, Identifiable { get { switch self { case .randomPin: - return "Random PIN" + return NSLocalizedString("bluetooth.mode.randompin", comment: "Random PIN") case .fixedPin: - return "Fixed PIN" + return NSLocalizedString("bluetooth.mode.fixedpin", comment: "Fixed PIN") case .noPin: - return "No PIN (Just Works)" + return NSLocalizedString("bluetooth.mode.nopin", comment: "No PIN (Just Works)") } } } diff --git a/Meshtastic/Enums/CannedMessagesConfigEnums.swift b/Meshtastic/Enums/CannedMessagesConfigEnums.swift index 217bcd60..565e375d 100644 --- a/Meshtastic/Enums/CannedMessagesConfigEnums.swift +++ b/Meshtastic/Enums/CannedMessagesConfigEnums.swift @@ -4,6 +4,7 @@ // // Copyright(c) Garth Vander Houwen 9/10/22. // +import Foundation // Default of 0 is unset enum ConfigPresets : Int, CaseIterable, Identifiable { @@ -18,11 +19,11 @@ enum ConfigPresets : Int, CaseIterable, Identifiable { switch self { case .unset: - return "Manual Configuration" + return NSLocalizedString("canned.messages.preset.manual", comment: "Manual Configuration") case .rakRotaryEncoder: - return "RAK Rotary Encoder Module" + return NSLocalizedString("canned.messages.preset.rakrotary", comment: "RAK Rotary Encoder Module") case .cardKB: - return "M5 Stack Card KB / RAK Keypad" + return NSLocalizedString("canned.messages.preset.cardkb", comment: "M5 Stack Card KB / RAK Keypad") } } } @@ -46,21 +47,21 @@ enum InputEventChars: Int, CaseIterable, Identifiable { switch self { case .none: - return "None" + return NSLocalizedString("inputevent.none", comment: "None") case .up: - return "Up" + return NSLocalizedString("inputevent.up", comment: "Up") case .down: - return "Down" + return NSLocalizedString("inputevent.down", comment: "Down") case .left: - return "Left" + return NSLocalizedString("inputevent.left", comment: "Left") case .right: - return "Right" + return NSLocalizedString("inputevent.right", comment: "Right") case .select: - return "Select" + return NSLocalizedString("inputevent.select", comment: "Select") case .back: - return "Back" + return NSLocalizedString("inputevent.back", comment: "Back") case .cancel: - return "Cancel" + return NSLocalizedString("inputevent.cancel", comment: "Cancel") } } } diff --git a/Meshtastic/Enums/ChannelRoles.swift b/Meshtastic/Enums/ChannelRoles.swift index ac5ad574..83bb30a7 100644 --- a/Meshtastic/Enums/ChannelRoles.swift +++ b/Meshtastic/Enums/ChannelRoles.swift @@ -4,6 +4,7 @@ // // Copyright(c) Garth Vander Houwen 9/21/22. // +import Foundation // Default of 0 is Client enum ChannelRoles: Int, CaseIterable, Identifiable { @@ -18,11 +19,11 @@ enum ChannelRoles: Int, CaseIterable, Identifiable { switch self { case .disabled: - return "Disabled" + return NSLocalizedString("channel.role.disabled", comment: "Disabled") case .primary: - return "Primary" + return NSLocalizedString("channel.role.primary", comment: "Primary") case .secondary: - return "Secondary" + return NSLocalizedString("channel.role.secondary", comment: "Secondary") } } } diff --git a/Meshtastic/Enums/DeviceRoles.swift b/Meshtastic/Enums/DeviceRoles.swift index ba793bd3..c1e8d18c 100644 --- a/Meshtastic/Enums/DeviceRoles.swift +++ b/Meshtastic/Enums/DeviceRoles.swift @@ -21,13 +21,13 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { switch self { case .client: - return "Client (default) - App connected client." + return NSLocalizedString("device.role.client", comment: "Client (default) - App connected client.") case .clientMute: - return "Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh." + return NSLocalizedString("device.role.clientmute", comment: "Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh.") case .router: - return "Router - Mesh packets will prefer to be routed over this node. This node will not be used by client apps. The wifi/ble radios and the oled screen will be put to sleep." + return NSLocalizedString("device.role.router", comment: "Router - Mesh packets will prefer to be routed over this node. This node will not be used by client apps. The wifi/ble radios and the oled screen will be put to sleep.") case .routerClient: - return "Router Client - Mesh packets will prefer to be routed over this node. The Router Client can be used as both a Router and an app connected Client." + return NSLocalizedString("device.role.routerclient", comment: "Router Client - Mesh packets will prefer to be routed over this node. The Router Client can be used as both a Router and an app connected Client.") } } } diff --git a/Meshtastic/Enums/DisplayEnums.swift b/Meshtastic/Enums/DisplayEnums.swift index a1a6103b..46110532 100644 --- a/Meshtastic/Enums/DisplayEnums.swift +++ b/Meshtastic/Enums/DisplayEnums.swift @@ -49,23 +49,24 @@ enum ScreenOnIntervals: Int, CaseIterable, Identifiable { get { switch self { case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") case .fiveMinutes: - return "Five Minutes" + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") case .tenMinutes: - return "Ten Minutes" + return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes") case .fifteenMinutes: - return "Fifteen Minutes" + return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes") case .thirtyMinutes: - return "Thirty Minutes" + return NSLocalizedString("interval.thirty.minutes", comment: "Thirty Minutes") case .oneHour: - return "One Hour" + return NSLocalizedString("interval.one.hour", comment: "One Hour") case .max: - return "Always On" + return NSLocalizedString("always.on", comment: "Always On") } } } } + // Default of 0 is off enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable { @@ -81,17 +82,17 @@ enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable { get { switch self { case .off: - return "Off" + return NSLocalizedString("off", comment: "Off") case .thirtySeconds: - return "Thirty Seconds" + return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds") case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") case .fiveMinutes: - return "Five Minutes" + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") case .tenMinutes: - return "Ten Minutes" + return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes") case .fifteenMinutes: - return "Fifteen Minutes" + return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes") } } } @@ -109,7 +110,7 @@ enum OledTypes: Int, CaseIterable, Identifiable { get { switch self { case .auto: - return "Automatic Detection" + return NSLocalizedString("automatic.detection", comment: "Automatic Detection") case .ssd1306: return "SSD 1306" case .sh1106: diff --git a/Meshtastic/Enums/MessagingEnums.swift b/Meshtastic/Enums/MessagingEnums.swift index 251fbb6f..9b9f145d 100644 --- a/Meshtastic/Enums/MessagingEnums.swift +++ b/Meshtastic/Enums/MessagingEnums.swift @@ -4,6 +4,7 @@ // // Copyright(c) Garth Vander Houwen 9/30/22. // +import Foundation enum BubblePosition { case left @@ -45,19 +46,19 @@ enum Tapbacks: Int, CaseIterable, Identifiable { get { switch self { case .heart: - return "Heart" + return NSLocalizedString("tapback.heart", comment: "Heart") case .thumbsUp: - return "Thumbs Up" + return NSLocalizedString("tapback.thumbsup", comment: "Thumbs Up") case .thumbsDown: - return "Thumbs Down" + return NSLocalizedString("tapback.thumbsdown", comment: "Thumbs Down") case .haHa: - return "HaHa" + return NSLocalizedString("tapback.haha", comment: "HaHa") case .exclamation: - return "Exclamation Mark" + return NSLocalizedString("tapback.exclamation", comment: "Exclamation Mark") case .question: - return "Question Mark" + return NSLocalizedString("tapback.question", comment: "Question Mark") case .poop: - return "Poop" + return NSLocalizedString("tapback.poop", comment: "Poop") } } } diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index b70ed62a..081420f0 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -27,27 +27,27 @@ enum PositionBroadcastIntervals: Int, CaseIterable, Identifiable { switch self { case .fifteenSeconds: - return "Fifteen Seconds" + return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds") case .thirtySeconds: - return "Thirty Seconds" + return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds") case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") case .fiveMinutes: - return "Five Minutes" + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") case .tenMinutes: - return "Ten Minutes" + return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes") case .fifteenMinutes: - return "Fifteen Minutes" + return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes") case .thirtyMinutes: - return "Thirty Minutes" + return NSLocalizedString("interval.thirty.minutes", comment: "Thirty Minutes") case .oneHour: - return "One Hour" + return NSLocalizedString("interval.one.hour", comment: "One Hour") case .sixHours: - return "Six Hours" + return NSLocalizedString("interval.six.hours", comment: "Six Hours") case .twelveHours: - return "Twelve Hours" + return NSLocalizedString("interval.twelve.hours", comment: "Twelve Hours") case .twentyFourHours: - return "Twenty Four Hours" + return NSLocalizedString("interval.twentyfour.hours", comment: "Twenty Four Hours") } } } @@ -67,17 +67,17 @@ enum GpsFormats: Int, CaseIterable, Identifiable { get { switch self { case .gpsFormatDec: - return "Decimal Degrees Format" + return NSLocalizedString("gpsformat.dec", comment: "Decimal Degrees Format") case .gpsFormatDms: - return "Degrees Minutes Seconds" + return NSLocalizedString("gpsformat.dms", comment: "Degrees Minutes Seconds") case .gpsFormatUtm: - return "Universal Transverse Mercator" + return NSLocalizedString("gpsformat.utm", comment: "Universal Transverse Mercator") case .gpsFormatMgrs: - return "Military Grid Reference System" + return NSLocalizedString("gpsformat.mgrs", comment: "Military Grid Reference System") case .gpsFormatOlc: - return "Open Location Code (aka Plus Codes)" + return NSLocalizedString("gpsformat.olc", comment: "Open Location Code (aka Plus Codes)") case .gpsFormatOsgr: - return "Ordnance Survey Grid Reference" + return NSLocalizedString("gpsformat.osgr", comment: "Ordnance Survey Grid Reference") } } } @@ -128,39 +128,39 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { switch self { case .fiveSeconds: - return "Five Seconds" + return NSLocalizedString("interval.five.seconds", comment: "Five Seconds") case .tenSeconds: - return "Ten Seconds" + return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds") case .fifteenSeconds: - return "Fifteen Seconds" + return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds") case .twentySeconds: - return "Twenty Seconds" + return NSLocalizedString("interval.twenty.seconds", comment: "Twenty Seconds") case .twentyFiveSeconds: - return "Twenty Five Seconds" + return NSLocalizedString("interval.twentyfive.seconds", comment: "Twenty Five Seconds") case .thirtySeconds: - return "Thirty Seconds" + return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds") case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") case .twoMinutes: - return "Two Minutes" + return NSLocalizedString("interval.two.minutes", comment: "Two Minutes") case .fiveMinutes: - return "Five Minutes" + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") case .tenMinutes: - return "Ten Minutes" + return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes") case .fifteenMinutes: - return "Fifteen Minutes" + return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes") case .thirtyMinutes: - return "Thirty Minutes" + return NSLocalizedString("interval.thirty.minutes", comment: "Thirty Minutes") case .oneHour: - return "One Hour" + return NSLocalizedString("interval.one.hour", comment: "One Hour") case .sixHours: - return "Six Hours" + return NSLocalizedString("interval.six.hours", comment: "Six Hours") case .twelveHours: - return "Twelve Hours" + return NSLocalizedString("interval.twelve.hours", comment: "Twelve Hours") case .twentyFourHours: - return "Twenty Four Hours" + return NSLocalizedString("interval.twentyfour.hours", comment: "Twenty Four Hours") case .maxInt32: - return "On Boot Only" + return NSLocalizedString("on.boot", comment: "On Boot Only") } } } @@ -168,10 +168,7 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { enum GpsAttemptTimes: Int, CaseIterable, Identifiable { - case oneSecond = 1 case twoSeconds = 2 - case threeSeconds = 3 - case fourSeconds = 4 case fiveSeconds = 5 case tenSeconds = 10 case fifteenSeconds = 15 @@ -179,6 +176,7 @@ enum GpsAttemptTimes: Int, CaseIterable, Identifiable { case twentyFiveSeconds = 25 case thirtySeconds = 30 case oneMinute = 60 + case twoMinutes = 120 case fiveMinutes = 300 var id: Int { self.rawValue } @@ -186,30 +184,26 @@ enum GpsAttemptTimes: Int, CaseIterable, Identifiable { get { switch self { - case .oneSecond: - return "One Seconds" case .twoSeconds: - return "Two Seconds" - case .threeSeconds: - return "Three Seconds" - case .fourSeconds: - return "Four Seconds" + return NSLocalizedString("interval.two.seconds", comment: "Two Seconds") case .fiveSeconds: - return "Five Seconds" + return NSLocalizedString("interval.five.seconds", comment: "Five Seconds") case .tenSeconds: - return "Ten Seconds" + return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds") case .fifteenSeconds: - return "Fifteen Seconds" + return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds") case .twentySeconds: - return "Twenty Seconds" + return NSLocalizedString("interval.twenty.seconds", comment: "Twenty Seconds") case .twentyFiveSeconds: - return "Twenty Five Seconds" + return NSLocalizedString("interval.twentyfive.seconds", comment: "Twenty Five Seconds") case .thirtySeconds: - return "Thirty Seconds" + return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds") case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") + case .twoMinutes: + return NSLocalizedString("interval.two.minutes", comment: "Two Minutes") case .fiveMinutes: - return "Five Minutes" + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") } } } diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index b242f45f..390ad5a6 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -4,6 +4,7 @@ // // Copyright(c) Garth Vander Houwen 8/4/22. // +import Foundation enum RoutingError: Int, CaseIterable, Identifiable { @@ -16,6 +17,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case noChannel = 6 case tooLarge = 7 case noResponse = 8 + case dutyCycleLimit = 9 case badRequest = 32 case notAuthorized = 33 @@ -25,56 +27,29 @@ enum RoutingError: Int, CaseIterable, Identifiable { switch self { case .none: - return "No Error." + return NSLocalizedString("routing.acknowledged", comment: "Acknowledged") case .noRoute: - return "No Route" + return NSLocalizedString("routing.noroute", comment: "No Route") case .gotNak: - return "Received a nak" + return NSLocalizedString("routing.gotnak", comment: "Received a negative acknowledgment") case .timeout: - return "Timeout" + return NSLocalizedString("routing.timeout", comment: "Timeout") case .noInterface: - return "No Interface" + return NSLocalizedString("routing.nointerface", comment: "No Interface") case .maxRetransmit: - return "Max Retransmission Reached" + return NSLocalizedString("routing.maxretransmit", comment: "Max Retransmission Reached") case .noChannel: - return "No Channel" + return NSLocalizedString("routing.nochannel", comment: "No Channel") case .tooLarge: - return "The packet is too large" + return NSLocalizedString("routing.toolarge", comment: "The packet is too large") case .noResponse: - return "No Response" + return NSLocalizedString("routing.noresponse", comment: "No Response") + case .dutyCycleLimit: + return NSLocalizedString("routing.dutycyclelimit", comment: "Regional Duty Cycle Limit Reached") case .badRequest: - return "Bad Request" + return NSLocalizedString("routing.badRequest", comment: "Bad Request") case .notAuthorized: - return "Not Authorized" - } - } - } - var description: String { - get { - switch self { - - case .none: - return "This message is not a failure." - case .noRoute: - return "Our node doesn't have a route to the requested destination anymore." - case .gotNak: - return "We received a nak while trying to forward on your behalf." - case .timeout: - return "We timed out while attempting to route this packet." - case .noInterface: - return "No suitable interface could be found for delivering this packet." - case .maxRetransmit: - return "We reached the max retransmission count (Hop Limit) and have received no responses." - case .noChannel: - return "No suitable channel was found for sending this packet (i.e. was requested channel index disabled?)." - case .tooLarge: - return "The packet was too big for sending (exceeds interface MTU after encoding)." - case .noResponse: - return "The request had want_response set, the request reached the destination node, but no service on that node wants to send a response (possibly due to bad channel permissions)." - case .badRequest: - return "The application layer service on the remote node received your request, but considered your request somehow invalid." - case .notAuthorized: - return "The application layer service on the remote node received your request, but considered your request not authorized (i.e you did not send the request on the required bound channel)." + return NSLocalizedString("routing.notauthorized", comment: "Not Authorized") } } } @@ -100,10 +75,13 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Routing.Error.tooLarge case .noResponse: return Routing.Error.noResponse + case .dutyCycleLimit: + return Routing.Error.dutyCycleLimit case .badRequest: return Routing.Error.badRequest case .notAuthorized: return Routing.Error.notAuthorized + } } } diff --git a/Meshtastic/Enums/SerialConfigEnums.swift b/Meshtastic/Enums/SerialConfigEnums.swift index 81e4d5b6..f6c4dded 100644 --- a/Meshtastic/Enums/SerialConfigEnums.swift +++ b/Meshtastic/Enums/SerialConfigEnums.swift @@ -4,6 +4,7 @@ // // Copyright(c) Garth Vander Houwen 9/10/22. // +import Foundation enum SerialBaudRates: Int, CaseIterable, Identifiable { @@ -30,7 +31,7 @@ enum SerialBaudRates: Int, CaseIterable, Identifiable { switch self { case .baudDefault: - return "Baud Default" + return NSLocalizedString("default", comment: "Default") case .baud110: return "110 Baud" case .baud300: @@ -118,15 +119,15 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable { get { switch self { case .default: - return "Default" + return NSLocalizedString("serial.mode.default", comment: "Default") case .simple: - return "Simple" + return NSLocalizedString("serial.mode.simple", comment: "Simple") case .proto: - return "Protobufs" + return NSLocalizedString("serial.mode.proto", comment: "Protobufs") case .txtmsg: - return "Text Message" + return NSLocalizedString("serial.mode.txtmsg", comment: "Text Message") case .nmea: - return "NMEA Positions" + return NSLocalizedString("serial.mode.nmea", comment: "NMEA Positions") } } } @@ -166,20 +167,19 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable { case .unset: return "Unset" case .oneSecond: - return "One Second" + return NSLocalizedString("interval.one.second", comment: "One Second") case .fiveSeconds: - return "Five Seconds" + return NSLocalizedString("interval.five.seconds", comment: "Five Seconds") case .tenSeconds: - return "Ten Seconds" + return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds") case .fifteenSeconds: - return "Fifteen Seconds" + return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds") case .thirtySeconds: - return "Thirty Seconds" + return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds") case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") case .fiveMinutes: - return "Five Minutes" - + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") } } } diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 9edc9929..b0de58cc 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -23,7 +23,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.airUtilTx) csvString += ", " - csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age" + csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? NSLocalizedString("unknown.age", comment: "") } } } else if metricsType == 1 { @@ -44,7 +44,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.current) csvString += ", " - csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age" + csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? NSLocalizedString("unknown.age", comment: "") } } } @@ -73,7 +73,7 @@ func PositionToCsvFile(positions: [PositionEntity]) -> String { csvString += ", " csvString += String(pos.snr) csvString += ", " - csvString += pos.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age" + csvString += pos.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? NSLocalizedString("unknown.age", comment: "") } return csvString } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 3057e4d7..acd11c67 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -458,7 +458,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) + _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) } } } @@ -490,7 +490,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .rangeTestApp: MeshLogger.log("ℹ️ MESH PACKET received for Range Test App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .telemetryApp: - if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, context: context!) } + if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } case .textMessageCompressedApp: MeshLogger.log("ℹ️ MESH PACKET received for Text Message Compressed App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .zpsApp: @@ -527,19 +527,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { MeshLogger.log("💥 Error Deleting the All - Broadcast User") } - // MARK: Share Location Position Update Timer - // Use context to pass the radio name with the timer - // Use a RunLoop to prevent the timer from running on the main UI thread - if userSettings?.provideLocation ?? false { - if positionTimer != nil { - positionTimer!.invalidate() - } - positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) - if positionTimer != nil { - RunLoop.current.add(positionTimer!, forMode: .common) - } - } - if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { invalidVersion = false lastConnectionError = "" @@ -548,6 +535,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { MeshLogger.log("🤜 BLE Config Complete Packet Id: \(decodedInfo.configCompleteID)") peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again + // MARK: Share Location Position Update Timer + // Use context to pass the radio name with the timer + // Use a RunLoop to prevent the timer from running on the main UI thread + if userSettings?.provideLocation ?? false { + if positionTimer != nil { + positionTimer!.invalidate() + } + positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + if positionTimer != nil { + RunLoop.current.add(positionTimer!, forMode: .common) + } + } + return } @@ -711,7 +711,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return success } - public func sendPosition(destNum: Int64, wantAck: Bool) -> Bool { + public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num @@ -734,14 +734,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = 0 // Send 0 as from from phone to device to avoid warning about client trying to set node num - meshPacket.wantAck = wantAck var dataMessage = DataMessage() dataMessage.payload = try! positionPacket.serializedData() dataMessage.portnum = PortNum.positionApp - if destNum != emptyNodeNum { - dataMessage.wantResponse = true - } + //if destNum != emptyNodeNum { + dataMessage.wantResponse = wantResponse + //} meshPacket.decoded = dataMessage var toRadio: ToRadio! @@ -765,7 +764,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // Send a position out to the mesh if "share location with the mesh" is enabled in settings if userSettings!.provideLocation { - let success = sendPosition(destNum: connectedPeripheral.num, wantAck: false) + let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false) if !success { print("Failed to send positon to device") @@ -940,6 +939,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return success } + public func saveChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setChannel = channel + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = 0 //UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { if isConnected { @@ -957,7 +983,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { fetchedMyInfo[0].channels = mutableChannels do { try context!.save() - } catch { print("Failed to clear existing channels from local app database") } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 67eb547f..f947ecbf 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -78,11 +78,15 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 newDeviceConfig.role = Int32(config.device.role.rawValue) newDeviceConfig.serialEnabled = config.device.serialEnabled newDeviceConfig.debugLogEnabled = config.device.debugLogEnabled + newDeviceConfig.buttonGpio = Int32(config.device.buttonGpio) + newDeviceConfig.buzzerGpio = Int32(config.device.buzzerGpio) fetchedNode[0].deviceConfig = newDeviceConfig } else { fetchedNode[0].deviceConfig?.role = Int32(config.device.role.rawValue) fetchedNode[0].deviceConfig?.serialEnabled = config.device.serialEnabled fetchedNode[0].deviceConfig?.debugLogEnabled = config.device.debugLogEnabled + fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.device.buttonGpio) + fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.device.buzzerGpio) } do { @@ -752,6 +756,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo if fetchedMyInfo.count == 1 { let newChannel = ChannelEntity(context: context) + newChannel.id = Int32(channel.index) newChannel.index = Int32(channel.index) newChannel.uplinkEnabled = channel.settings.uplinkEnabled newChannel.downlinkEnabled = channel.settings.downlinkEnabled @@ -1038,16 +1043,8 @@ func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - //print(packet.payloadVariant.debugDescription) - if let messages = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { - //let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!, wantResponse: true) - //if adminMessageId > 0 { - - //} - //print(messages) - } else { - //print(try! packet.decoded.jsonString()) + print(messages) } } @@ -1197,7 +1194,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana } } -func telemetryPacket(packet: MeshPacket, context: NSManagedObjectContext) { +func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { @@ -1243,7 +1240,10 @@ func telemetryPacket(packet: MeshPacket, context: NSManagedObjectContext) { } try context.save() - MeshLogger.log("💾 Telemetry Saved for Node: \(packet.from)") + // Only log telemetery from the mesh not the connected device + if connectedNode != Int64(packet.from) { + MeshLogger.log("💾 Telemetry Saved for Node: \(packet.from)") + } } catch { context.rollback() @@ -1314,19 +1314,30 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") } else if newMessage.fromUser != nil && newMessage.toUser == nil { - - - // Create an iOS Notification for the received private channel message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown")", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", - content: messageText) - ] - manager.schedule() - MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") + let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel && !channel.mute { + // Create an iOS Notification for the received private channel message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown")", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", + content: messageText) + ] + manager.schedule() + MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") + } + } + } catch { + + + } } } } catch { diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 98958a3e..84ae1e6a 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -66,6 +66,12 @@ struct MeshtasticAppleApp: App { let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false) if !self.saveChannels { + + //tell the system we want the file please + guard url.startAccessingSecurityScopedResource() else { + return + } + //do we need to delete an old one? if (fileManager.fileExists(atPath: destination.path)) { print("ℹ️ Found an old map file. Deleting it") diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a189e58e..bd752922 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -60,9 +60,9 @@ public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManage } } -public func deleteChannelMessages(channelIndex: Int32, context: NSManagedObjectContext) { +public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { let fetchChannelMessagesRequest = NSFetchRequest(entityName: "MessageEntity") - fetchChannelMessagesRequest.predicate = NSPredicate(format: "channel == %i AND toUser == nil AND admin == false", Int32(channelIndex)) + fetchChannelMessagesRequest.predicate = NSPredicate(format: "channel == %i AND toUser == nil AND admin == false", Int32(channel.id)) fetchChannelMessagesRequest.includesPropertyValues = false do { let objects = try context.fetch(fetchChannelMessagesRequest) as! [NSManagedObject] @@ -70,7 +70,6 @@ public func deleteChannelMessages(channelIndex: Int32, context: NSManagedObjectC context.delete(object) } try context.save() - context.refreshAllObjects() } catch let error as NSError { print("Error: \(error.localizedDescription)") } @@ -87,7 +86,6 @@ public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext context.delete(object) } try context.save() - context.refresh(user, mergeChanges: true) } catch let error as NSError { print("Error: \(error.localizedDescription)") } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index f2875e5e..cfa38d5e 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -28,7 +28,7 @@ struct Connect: View { VStack { List { if bleManager.isSwitchedOn { - Section(header: Text("Connected Radio").font(.title)) { + Section(header: Text("connected.radio").font(.title)) { if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected { HStack { Image(systemName: "antenna.radiowaves.left.and.right") @@ -39,25 +39,26 @@ struct Connect: View { if node != nil { Text(bleManager.connectedPeripheral.longName).font(.title2) } - Text("BLE Name: ").font(.caption)+Text(bleManager.connectedPeripheral.peripheral.name ?? "Unknown") + Text("ble.name").font(.caption)+Text(": \(bleManager.connectedPeripheral.peripheral.name ?? "Unknown")") .font(.caption).foregroundColor(Color.gray) if node != nil { - Text("FW Version: ").font(.caption)+Text(node?.myInfo?.firmwareVersion ?? "Unknown") + Text("firmware.version").font(.caption)+Text(": \(node?.myInfo?.firmwareVersion ?? "Unknown")") .font(.caption).foregroundColor(Color.gray) } if bleManager.isSubscribed { - Text("Subscribed to mesh").font(.caption) + Text("subscribed").font(.caption) .foregroundColor(.green) } else { - Text("Communicating with device. . . ").font(.caption) + Text("communicating").font(.caption) .foregroundColor(.orange) } } Spacer() VStack(alignment: .center) { - Text("Preferred").font(.caption2) - Text("Radio").font(.caption2) - Toggle("Preferred Radio", isOn: $bleManager.preferredPeripheral) + Text("preferred.radio").font(.caption2) + .multilineTextAlignment(.center) + .frame(width: 75) + Toggle("preferred.radio", isOn: $bleManager.preferredPeripheral) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .labelsHidden() .onChange(of: bleManager.preferredPeripheral) { value in @@ -91,7 +92,7 @@ struct Connect: View { isPreferredRadio = false } } label: { - Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") + Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } } .contextMenu{ @@ -112,7 +113,7 @@ struct Connect: View { NavigationLink { LoRaConfig(node: node) } label: { - Label("Set LoRa Region", systemImage: "globe.americas.fill") + Label("set.region", systemImage: "globe.americas.fill") .foregroundColor(.red) .font(.title) } @@ -127,7 +128,7 @@ struct Connect: View { .imageScale(.large).foregroundColor(.orange) .padding(.trailing) if bleManager.timeoutTimerCount == 0 { - Text("Connecting . . .") + Text("connecting") .font(.title3) .foregroundColor(.orange) } else { @@ -151,7 +152,7 @@ struct Connect: View { .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.red) .padding(.trailing) - Text("No device connected").font(.title3) + Text("not.connected").font(.title3) } .padding() } @@ -160,7 +161,7 @@ struct Connect: View { .textCase(nil) if !self.bleManager.isConnected { - Section(header: Text("Available Radios").font(.title)) { + Section(header: Text("available.radios").font(.title)) { ForEach(bleManager.peripherals.filter({ $0.peripheral.state == CBPeripheralState.disconnected }).sorted(by: { $0.name > $1.name })) { peripheral in HStack { Image(systemName: "circle.fill") @@ -215,7 +216,7 @@ struct Connect: View { }) { - Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") + Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") } .buttonStyle(.bordered) @@ -228,7 +229,7 @@ struct Connect: View { } .padding(.bottom, 10) } - .navigationTitle("Bluetooth") + .navigationTitle("bluetooth") .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index ba37fb99..a3f99164 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -29,28 +29,28 @@ struct ContentView: View { Contacts() .tabItem { - Label("Messages", systemImage: "message") + Label("messages", systemImage: "message") } .tag(Tab.contacts) } Connect() .tabItem { - Label("Bluetooth", systemImage: "antenna.radiowaves.left.and.right") + Label("bluetooth", systemImage: "antenna.radiowaves.left.and.right") } .tag(Tab.ble) NodeList() .tabItem { - Label("Nodes", systemImage: "flipphone") + Label("nodes", systemImage: "flipphone") } .tag(Tab.nodes) NodeMap() .tabItem { - Label("Mesh Map", systemImage: "map") + Label("map", systemImage: "map") } .tag(Tab.map) Settings() .tabItem { - Label("Settings", systemImage: "gear") + Label("settings", systemImage: "gear") } .tag(Tab.settings) } diff --git a/Meshtastic/Views/Helpers/DateTimeText.swift b/Meshtastic/Views/Helpers/DateTimeText.swift index e88a46e6..068664fe 100644 --- a/Meshtastic/Views/Helpers/DateTimeText.swift +++ b/Meshtastic/Views/Helpers/DateTimeText.swift @@ -24,7 +24,7 @@ struct DateTimeText: View { } else { - Text("Unknown Age") + Text("unknown.age") } } } diff --git a/Meshtastic/Views/Helpers/DistanceText.swift b/Meshtastic/Views/Helpers/DistanceText.swift index 59d58bb8..6028c1e5 100644 --- a/Meshtastic/Views/Helpers/DistanceText.swift +++ b/Meshtastic/Views/Helpers/DistanceText.swift @@ -16,7 +16,7 @@ struct DistanceText: View { var body: some View { let distanceFormatter = MKDistanceFormatter() - Text("Distance: \(distanceFormatter.string(fromDistance: Double(meters)))") + Text("distance")+Text(": \(distanceFormatter.string(fromDistance: Double(meters)))") } } struct DistanceText_Previews: PreviewProvider { diff --git a/Meshtastic/Views/Helpers/LastHeardText.swift b/Meshtastic/Views/Helpers/LastHeardText.swift index 36bfaa9d..d4a877e9 100644 --- a/Meshtastic/Views/Helpers/LastHeardText.swift +++ b/Meshtastic/Views/Helpers/LastHeardText.swift @@ -7,17 +7,22 @@ import SwiftUI // struct LastHeardText: View { var lastHeard: Date? - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - var body: some View { if (lastHeard != nil && lastHeard! >= sixMonthsAgo!){ - - Text("Heard: \(lastHeard!, style: .relative) ago") - + Text("heard")+Text(": \(lastHeard!, style: .relative) ")+Text("ago") } else { - - Text("Unknown Age") + Text("unknown.age") } } } +struct LastHeardText_Previews: PreviewProvider { + static var previews: some View { + LastHeardText(lastHeard: Date()) + .previewLayout(.fixed(width: 300, height: 100)) + .environment(\.locale, .init(identifier: "en")) + LastHeardText(lastHeard: Date()) + .previewLayout(.fixed(width: 300, height: 100)) + .environment(\.locale, .init(identifier: "de")) + } +} diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 6b61a9da..68f407e3 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -67,9 +67,9 @@ struct ChannelMessageList: View { .cornerRadius(15) .contextMenu { VStack{ - Text("Channel: \(message.channel)") + Text("channel")+Text(": \(message.channel)") } - Menu("Tapback response") { + Menu("tapback") { ForEach(Tapbacks.allCases) { tb in Button(action: { if bleManager.sendMessage(message: tb.emojiString, toUserNum: 0, channel: channel.index, isEmoji: true, replyID: message.messageId) { @@ -89,16 +89,16 @@ struct ChannelMessageList: View { self.focusedField = .messageText print("I want to reply to \(message.messageId)") }) { - Text("Reply") + Text("reply") Image(systemName: "arrowshape.turn.up.left.2.fill") } Button(action: { UIPasteboard.general.string = message.messagePayload }) { - Text("Copy") + Text("copy") Image(systemName: "doc.on.doc") } - Menu("Message Details") { + Menu("message.details") { VStack { let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) @@ -110,11 +110,11 @@ struct ChannelMessageList: View { } if currentUser && message.receivedACK { VStack { - Text("Received Ack \(message.receivedACK ? "✔️" : "")") + Text("received.ack")+Text(" \(message.receivedACK ? "✔️" : "")") } } else if currentUser && message.ackError == 0 { // Empty Error - Text("Waiting. . .") + Text("waiting") } else if currentUser && message.ackError > 0 { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) @@ -126,7 +126,7 @@ struct ChannelMessageList: View { if ackDate >= sixMonthsAgo! { Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) } else { - Text("Unknown Age").font(.caption2).foregroundColor(.gray) + Text("unknown.age").font(.caption2).foregroundColor(.gray) } } } @@ -144,7 +144,7 @@ struct ChannelMessageList: View { self.deleteMessageId = message.messageId print(deleteMessageId) }) { - Text("Delete") + Text("delete") Image(systemName: "trash") } } @@ -243,12 +243,12 @@ struct ChannelMessageList: View { } } label: { - Text("Share Position") + Text("share.position") Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.accentColor) } - ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + ProgressView("\(NSLocalizedString("bytes", comment: "")): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) @@ -260,7 +260,7 @@ struct ChannelMessageList: View { ZStack { let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) - TextField("Message", text: $typingMessage, axis: .vertical) + TextField("message", text: $typingMessage, axis: .vertical) .onChange(of: typingMessage, perform: { value in totalBytes = value.utf8.count // Only mess with the value if it is too big @@ -277,7 +277,7 @@ struct ChannelMessageList: View { .keyboardType(kbType!) .toolbar { ToolbarItemGroup(placement: .keyboard) { - Button("Dismiss Keyboard") { + Button("dismiss.keyboard") { focusedField = nil } .font(.subheadline) @@ -300,7 +300,7 @@ struct ChannelMessageList: View { .imageScale(.large).foregroundColor(.accentColor) } - ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + ProgressView("\(NSLocalizedString("bytes", comment: "")): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) @@ -311,9 +311,22 @@ struct ChannelMessageList: View { .focused($focusedField, equals: .messageText) .multilineTextAlignment(.leading) .frame(minHeight: 50) - + .keyboardShortcut(.defaultAction) + .onSubmit { + #if targetEnvironment(macCatalyst) + if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) { + typingMessage = "" + focusedField = nil + replyMessageId = 0 + if sendPositionWithMessage { + if bleManager.sendPosition(destNum: Int64(channel.index), wantAck: true) { + print("Location Sent") + } + } + } + #endif + } Text(typingMessage).opacity(0).padding(.all, 0) - } .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) .padding(.bottom, 15) @@ -323,7 +336,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantAck: true) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 54b00d9e..308e4188 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -28,88 +28,85 @@ struct Contacts: View { NavigationSplitView { List { - Section(header: Text("Channels (groups)")) { + Section(header: Text("channels")) { // Display Contacts for the rest of the non admin channels if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil { ForEach(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" && channel.name?.lowercased() ?? "" != "serial" { - VStack { - NavigationLink(destination: ChannelMessageList(channel: channel)) { - - let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index }) - let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) - let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 - let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 - VStack(alignment: .leading) { - HStack { - CircleText(text: String(channel.index), color: .accentColor, circleSize: 52, fontSize: 40, brightness: 0.1) - .padding(.trailing, 5) - VStack { - HStack { - if channel.name?.isEmpty ?? false { - if channel.role == 1 { - Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) - } else { - Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) - } + NavigationLink(destination: ChannelMessageList(channel: channel)) { + + let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index }) + let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) + let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 + let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 + VStack(alignment: .leading) { + HStack { + CircleText(text: String(channel.index), color: .accentColor, circleSize: 52, fontSize: 40, brightness: 0.1) + .padding(.trailing, 5) + VStack { + HStack { + if channel.name?.isEmpty ?? false { + if channel.role == 1 { + Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) } else { - Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) - } - Spacer() - if channel.allPrivateMessages.count > 0 { - VStack (alignment: .trailing) { - if lastMessageDay == currentDay { - Text(lastMessageTime, style: .time ) - .font(.subheadline) - } else if lastMessageDay == (currentDay - 1) { - Text("Yesterday") - .font(.subheadline) - } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { - Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) - .font(.subheadline) - } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) - .font(.subheadline) - } - } - .brightness(-0.20) + Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) } + } else { + Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) } + Spacer() if channel.allPrivateMessages.count > 0 { - HStack(alignment: .top) { - Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") - .truncationMode(.tail) - .frame(maxWidth: .infinity, alignment: .leading) - .brightness(-0.20) - .font(.body) + VStack (alignment: .trailing) { + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.subheadline) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.subheadline) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) + .font(.subheadline) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) + .font(.subheadline) + } } + .brightness(-0.20) + } + } + if channel.allPrivateMessages.count > 0 { + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + .brightness(-0.20) + .font(.body) } } - .frame(maxWidth: .infinity, alignment: .leading) } + .frame(maxWidth: .infinity, alignment: .leading) } } } .frame(maxWidth: .infinity, maxHeight: 80, alignment: .leading) .contextMenu { - if false { // Hide mute channel menu item until I can check it in notifications - Button { - channel.mute = !channel.mute - - do { - try context.save() - // Would rather not do this but the merge changes on - // A single object is only working on mac GVH - context.refreshAllObjects() - //context.refresh(channel, mergeChanges: true) - } catch { - context.rollback() - print("💥 Save Channel Mute Error") - } - } label: { - Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") + Button { + channel.mute = !channel.mute + + do { + try context.save() + // Would rather not do this but the merge changes on + // A single object is only working on mac GVH + context.refreshAllObjects() + //context.refresh(channel, mergeChanges: true) + } catch { + context.rollback() + print("💥 Save Channel Mute Error") } + } label: { + Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") } + if channel.allPrivateMessages.count > 0 { Button(role: .destructive) { isPresentingDeleteChannelMessagesConfirm = true @@ -121,14 +118,15 @@ struct Contacts: View { .confirmationDialog( "This conversation will be deleted.", isPresented: $isPresentingDeleteChannelMessagesConfirm, + titleVisibility: .visible ) { Button(role: .destructive) { - deleteChannelMessages(channelIndex: channel.index, context: context) - context.refreshAllObjects() + deleteChannelMessages(channel: channel, context: context) + context.refresh(node!.myInfo!, mergeChanges: true) } label: { - Text("Delete") + Text("delete") } } } @@ -137,7 +135,7 @@ struct Contacts: View { } } - Section(header: Text("Direct Messages")) { + Section(header: Text("direct.messages")) { ForEach(users) { (user: UserEntity) in if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 { NavigationLink(destination: UserMessageList(user: user)) { @@ -212,9 +210,9 @@ struct Contacts: View { Button(role: .destructive) { deleteUserMessages(user: user, context: context) - + context.refresh(node!.user!, mergeChanges: true) } label: { - Text("Delete") + Text("delete") } } } @@ -227,7 +225,7 @@ struct Contacts: View { } } } - .navigationTitle("Contacts") + .navigationTitle("contacts") .navigationBarItems(leading: MeshtasticLogo() ) @@ -260,7 +258,7 @@ struct Contacts: View { UserMessageList(user:user) } else { - Text("Select a Contact") + Text("select.contact") } } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index f2c4f98e..6dda3220 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -68,9 +68,9 @@ struct UserMessageList: View { .cornerRadius(15) .contextMenu { VStack{ - Text("Channel: \(message.channel)") + Text("channel")+Text(": \(message.channel)") } - Menu("Tapback response") { + Menu("tapback") { ForEach(Tapbacks.allCases) { tb in Button(action: { if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, channel: 0, isEmoji: true, replyID: message.messageId) { @@ -90,16 +90,16 @@ struct UserMessageList: View { self.focusedField = .messageText print("I want to reply to \(message.messageId)") }) { - Text("Reply") + Text("reply") Image(systemName: "arrowshape.turn.up.left.2.fill") } Button(action: { UIPasteboard.general.string = message.messagePayload }) { - Text("Copy") + Text("copy") Image(systemName: "doc.on.doc") } - Menu("Message Details") { + Menu("message.details") { VStack { let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) @@ -111,11 +111,11 @@ struct UserMessageList: View { } if currentUser && message.receivedACK { VStack { - Text("Received Ack \(message.receivedACK ? "✔️" : "")") + Text("received.ack")+Text(" \(message.receivedACK ? "✔️" : "")") } } else if currentUser && message.ackError == 0 { // Empty Error - Text("Waiting. . .") + Text("waiting") } else if currentUser && message.ackError > 0 { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) @@ -127,7 +127,7 @@ struct UserMessageList: View { if ackDate >= sixMonthsAgo! { Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) } else { - Text("Unknown Age").font(.caption2).foregroundColor(.gray) + Text("unknown.age").font(.caption2).foregroundColor(.gray) } } } @@ -145,7 +145,7 @@ struct UserMessageList: View { self.deleteMessageId = message.messageId print(deleteMessageId) }) { - Text("Delete") + Text("delete") Image(systemName: "trash") } } @@ -174,14 +174,14 @@ struct UserMessageList: View { } } HStack { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) if currentUser && message.receivedACK { // Ack Received - Text("Acknowledged").font(.caption2).foregroundColor(.gray) + Text("\(ackErrorVal?.display ?? "No Error" )").font(.caption2).foregroundColor(.gray) } else if currentUser && message.ackError == 0 { // Empty Error Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) .font(.caption2).foregroundColor(.red) } @@ -243,12 +243,12 @@ struct UserMessageList: View { } } label: { - Text("Share Position") + Text("share.position") Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.accentColor) } - ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + ProgressView("\(NSLocalizedString("bytes", comment: "")): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) @@ -260,7 +260,7 @@ struct UserMessageList: View { HStack(alignment: .top) { ZStack { let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) - TextField("Message", text: $typingMessage, axis: .vertical) + TextField("message", text: $typingMessage, axis: .vertical) .onChange(of: typingMessage, perform: { value in totalBytes = value.utf8.count // Only mess with the value if it is too big @@ -277,7 +277,7 @@ struct UserMessageList: View { .keyboardType(kbType!) .toolbar { ToolbarItemGroup(placement: .keyboard) { - Button("Dismiss Keyboard") { + Button("dismiss.keyboard") { focusedField = nil } .font(.subheadline) @@ -297,7 +297,7 @@ struct UserMessageList: View { .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.accentColor) } - ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + ProgressView("\(NSLocalizedString("bytes", comment: "")): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) @@ -308,6 +308,21 @@ struct UserMessageList: View { .focused($focusedField, equals: .messageText) .multilineTextAlignment(.leading) .frame(minHeight: 50) + .keyboardShortcut(.defaultAction) + .onSubmit { + #if targetEnvironment(macCatalyst) + if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) { + typingMessage = "" + focusedField = nil + replyMessageId = 0 + if sendPositionWithMessage { + if bleManager.sendPosition(destNum: user.num, wantAck: true) { + print("Location Sent") + } + } + } + #endif + } Text(typingMessage).opacity(0).padding(.all, 0) } .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) @@ -318,7 +333,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantAck: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true) { print("Location Sent") } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index e8e73d5e..198f1f72 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -132,7 +132,7 @@ struct DeviceMetricsLog: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingClearLogConfirm, titleVisibility: .visible ) { @@ -148,7 +148,7 @@ struct DeviceMetricsLog: View { exportString = TelemetryToCsvFile(telemetry: node.telemetries!.array as! [TelemetryEntity], metricsType: 0) isExporting = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index a8d2c2b3..9b8c803a 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -135,7 +135,7 @@ struct EnvironmentMetricsLog: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingClearLogConfirm, titleVisibility: .visible ) { @@ -156,7 +156,7 @@ struct EnvironmentMetricsLog: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 6ab47349..ee0da7d0 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -138,7 +138,7 @@ struct NodeDetail: View { .font(.title) .foregroundColor(.accentColor) .symbolRenderingMode(.hierarchical) - Text("User Id:").font(.title) + Text("user").font(.title)+Text(":").font(.title) } //Text(node.user?.userId ?? "??????").font(.title).foregroundColor(.gray) Text("!\(String(format:"%02x", node.num))") @@ -176,7 +176,7 @@ struct NodeDetail: View { .font(.title) .foregroundColor(.accentColor) .symbolRenderingMode(.hierarchical) - Text("Last Heard: ").font(.title) + Text("heard.last").font(.title)+Text(":").font(.title) } DateTimeText(dateTime: node.lastHeard) @@ -358,7 +358,7 @@ struct NodeDetail: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $showingShutdownConfirm ) { Button("Shutdown Node?", role: .destructive) { @@ -385,7 +385,7 @@ struct NodeDetail: View { .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $showingRebootConfirm ) { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 2845bafe..aaefe178 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -30,13 +30,7 @@ struct NodeList: View { NavigationSplitView { List (nodes, id: \.self, selection: $selection) { node in if nodes.count == 0 { - Text("Scan for Radios").font(.largeTitle) - Text("No Meshtastic Nodes Found").font(.title2) - Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your iPhone, iPad or Mac.") - .font(.body) - Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.") - Text("Views with bluetooth functionality will show an indicator in the upper right hand corner showing if bluetooth is on, and if a device is connected.") - .listRowSeparator(.visible) + Text("no.nodes").font(.title) } else { NavigationLink(value: node) { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == node.num) @@ -51,7 +45,7 @@ struct NodeList: View { Image(systemName: "repeat.circle.fill") .font(.title3) .symbolRenderingMode(.hierarchical) - Text("Currently Connected").font(.subheadline) + Text("connected").font(.subheadline) .foregroundColor(.green) } } @@ -85,7 +79,7 @@ struct NodeList: View { .padding([.top, .bottom]) } } - .navigationTitle("All Nodes") + .navigationTitle("nodes") .navigationBarItems(leading: MeshtasticLogo() ) @@ -97,7 +91,7 @@ struct NodeList: View { if let node = selection { NodeDetail(node:node) } else { - Text("Select a node") + Text("select.node") } } } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index b31e9b2a..46333fe1 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -114,7 +114,7 @@ struct PositionLog: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingClearLogConfirm, titleVisibility: .visible ) { @@ -137,7 +137,7 @@ struct PositionLog: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 8cc59d3c..32745f11 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -50,7 +50,7 @@ struct AboutMeshtastic: View { .font(.caption) } } - .navigationTitle("About") + .navigationTitle("about") .navigationBarTitleDisplayMode(.inline) } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 13a55da4..7a1cd178 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -17,15 +17,15 @@ enum KeyboardType: Int, CaseIterable, Identifiable { get { switch self { case .defaultKeyboard: - return "Default" + return NSLocalizedString("default", comment: "Default Keyboard") case .asciiCapable: - return "ASCII Capable" + return NSLocalizedString("ascii.capable", comment: "ASCII Capable Keyboard") case .twitter: - return "Twitter" + return NSLocalizedString("twitter", comment: "Twitter Keyboard") case .emailAddress: - return "Email Address" + return NSLocalizedString("email.address", comment: "Email Address Keyboard") case .numbersAndPunctuation: - return "Numbers and Punctuation" + return NSLocalizedString("numbers.punctuation", comment: "Numbers and Punctuation Keyboard") } } } @@ -43,11 +43,11 @@ enum MeshMapType: String, CaseIterable, Identifiable { get { switch self { case .satellite: - return "Satellite" + return NSLocalizedString("satellite", comment: "Satellite Map Type") case .standard: - return "Standard" + return NSLocalizedString("standard", comment: "Standard Map Type") case .hybrid: - return "Hybrid" + return NSLocalizedString("hybrid", comment: "Hybrid Map Type") } } } @@ -69,21 +69,21 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable { get { switch self { case .fiveSeconds: - return "Five Seconds" + return NSLocalizedString("interval.five.seconds", comment: "Five Seconds") case .tenSeconds: - return "Ten Seconds" + return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds") case .fifteenSeconds: - return "Fifteen Seconds" + return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds") case .thirtySeconds: - return "Thirty Seconds" + return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds") case .oneMinute: - return "One Minute" + return NSLocalizedString("interval.one.minute", comment: "One Minute") case .fiveMinutes: - return "Five Minutes" + return NSLocalizedString("interval.five.minutes", comment: "Five Minutes") case .tenMinutes: - return "Ten Minutes" + return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes") case .fifteenMinutes: - return "Fifteen Minutes" + return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes") } } } @@ -105,7 +105,7 @@ struct AppSettings: View { var body: some View { VStack { Form { - Section(header: Text("USER DETAILS")) { + Section(header: Text("user.details")) { HStack { Label("Name", systemImage: "person.crop.rectangle.fill") @@ -116,16 +116,16 @@ struct AppSettings: View { .disableAutocorrection(true) .listRowSeparator(.visible) } - Section(header: Text("Options")) { + Section(header: Text("options")) { - Picker("Keyboard Type", selection: $userSettings.keyboardType) { + Picker("keyboard.type", selection: $userSettings.keyboardType) { ForEach(KeyboardType.allCases) { kb in Text(kb.description) } } .pickerStyle(DefaultPickerStyle()) - Picker("Map Type", selection: $userSettings.meshMapType) { + Picker("map.type", selection: $userSettings.meshMapType) { ForEach(MeshMapType.allCases) { map in Text(map.description) } @@ -134,23 +134,23 @@ struct AppSettings: View { } - Section(header: Text("Phone GPS")) { + Section(header: Text("phone.gps")) { Toggle(isOn: $userSettings.provideLocation) { - Label("Provide location to mesh", systemImage: "location.circle.fill") + Label("provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if userSettings.provideLocation { - Picker(" Update Interval", selection: $userSettings.provideLocationInterval) { + Picker("update.interval", selection: $userSettings.provideLocationInterval) { ForEach(LocationUpdateInterval.allCases) { lu in Text(lu.description) } } .pickerStyle(DefaultPickerStyle()) - Text("How frequently your phone will send your location to the device, location updates to the mesh are managed by the device.") + Text("phone.gps.interval.description") .font(.caption) .listRowSeparator(.visible) } @@ -160,7 +160,7 @@ struct AppSettings: View { Button { isPresentingCoreDataResetConfirm = true } label: { - Label("Clear App Data", systemImage: "trash") + Label("clear.app.data", systemImage: "trash") .foregroundColor(.red) } .buttonStyle(.bordered) @@ -168,7 +168,7 @@ struct AppSettings: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingCoreDataResetConfirm, titleVisibility: .visible ) { @@ -179,7 +179,7 @@ struct AppSettings: View { } } } - .navigationTitle("App Settings") + .navigationTitle("app.settings") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift new file mode 100644 index 00000000..7f73dc12 --- /dev/null +++ b/Meshtastic/Views/Settings/Channels.swift @@ -0,0 +1,288 @@ +// +// ShareChannel.swift +// MeshtasticApple +// +// Copyright(c) Garth Vander Houwen 4/8/22. +// +//import SwiftUI +//import CoreData +// +//func generateChannelKey(size: Int) -> String { +// var keyData = Data(count: size) +// _ = keyData.withUnsafeMutableBytes { +// SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!) +// } +// return keyData.base64EncodedString() +//} +// +//struct Channels: View { +// +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// @Environment(\.dismiss) private var goBack +// @Environment(\.sizeCategory) var sizeCategory +// +// +// var node: NodeInfoEntity? +// +// @State var hasChanges = false +// @State private var isPresentingEditView = false +// @State private var isPresentingSaveConfirm: Bool = false +// @State private var channelIndex: Int32 = 0 +// @State private var channelName = "" +// @State private var channelKeySize = 32 +// @State private var channelKey = "AQ==" +// @State private var channelRole = 0 +// @State private var uplink = false +// @State private var downlink = false +// +// var body: some View { +// +// NavigationStack { +// List { +// if node != nil && node?.myInfo != nil { +// ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in +// Button(action: { +// channelIndex = channel.index +// channelRole = Int(channel.role) +// channelKey = channel.psk?.base64EncodedString() ?? "" +// if channelKey.count == 0 { +// channelKeySize = 0 +// } else if channelKey == "AQ==" { +// channelKeySize = -1 +// } else if channelKey.count == 24 { +// channelKeySize = 16 +// } else if channelKey.count == 32 { +// channelKeySize = 24 +// } else if channelKey.count == 44 { +// channelKeySize = 32 +// } +// channelName = channel.name ?? "" +// uplink = channel.uplinkEnabled +// downlink = channel.downlinkEnabled +// isPresentingEditView = true +// hasChanges = false +// }) { +// VStack(alignment: .leading) { +// HStack { +// CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1) +// .padding(.trailing, 5) +// VStack { +// HStack { +// if channel.name?.isEmpty ?? false { +// if channel.role == 1 { +// Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) +// } else { +// Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline) +// } +// } else { +// Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) +// } +// } +// } +// } +// } +// } +// } +// } +// } +// if node?.myInfo?.channels?.array.count ?? 0 < 8 { +// +// Button { +// let key = generateChannelKey(size: 32) +// channelName = "" +// channelIndex = Int32(node!.myInfo!.channels!.array.count) +// channelRole = 2 +// channelKey = key +// uplink = false +// downlink = false +// hasChanges = false +// isPresentingEditView = true +// +// } label: { +// Label("Add Channel", systemImage: "plus.square") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding() +// .sheet(isPresented: $isPresentingEditView) { +// +// #if targetEnvironment(macCatalyst) +// Text("channel") +// .font(.largeTitle) +// .padding() +// #endif +// Form { +// HStack { +// Text("name") +// Spacer() +// TextField( +// "Channel Name", +// text: $channelName +// ) +// .disableAutocorrection(true) +// .keyboardType(.alphabet) +// .foregroundColor(Color.gray) +// .disabled(channelRole == 1 && channelName.count > 0) +// .onChange(of: channelName, perform: { value in +// channelName = channelName.replacing(" ", with: "") +// let totalBytes = channelName.utf8.count +// // Only mess with the value if it is too big +// if totalBytes > 11 { +// let firstNBytes = Data(channelName.utf8.prefix(11)) +// if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { +// // Set the channelName back to the last place where it was the right size +// channelName = maxBytesString +// } +// } +// hasChanges = true +// }) +// } +// HStack { +// Picker("Key Size", selection: $channelKeySize) { +// Text("Empty").tag(0) +// Text("Default").tag(-1) +// Text("1 bit").tag(1) +// Text("128 bit").tag(16) +// Text("192 bit").tag(24) +// Text("256 bit").tag(32) +// } +// .pickerStyle(DefaultPickerStyle()) +// Spacer() +// Button { +// if channelKeySize == -1 { +// channelKey = "AQ==" +// } else { +// let key = generateChannelKey(size: channelKeySize) +// channelKey = key +// } +// } label: { +// Image(systemName: "lock.rotation") +// .font(.title) +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.small) +// } +// HStack (alignment: .top) { +// Text("Key") +// Spacer() +// TextField ( +// "", +// text: $channelKey, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .disabled(true) +// +// } +// .textSelection(.enabled) +// Picker("Channel Role", selection: $channelRole) { +// if channelRole == 1 { +// Text("Primary").tag(1) +// } else{ +// Text("Disabled").tag(0) +// Text("Secondary").tag(2) +// } +// } +// .pickerStyle(DefaultPickerStyle()) +// .disabled(channelRole == 1) +// Toggle("Uplink Enabled", isOn: $uplink) +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// Toggle("Downlink Enabled", isOn: $downlink) +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// } +// .onSubmit { +// //validate(name: channelName) +// } +// .onChange(of: channelName) { newName in +// hasChanges = true +// } +// .onChange(of: channelKeySize) { newKeySize in +// if channelKeySize == -1 { +// channelKey = "AQ==" +// } else { +// let key = generateChannelKey(size: channelKeySize) +// channelKey = key +// } +// hasChanges = true +// } +// .onChange(of: channelKey) { newKey in +// hasChanges = true +// } +// .onChange(of: channelRole) { newRole in +// hasChanges = true +// } +// .onChange(of: uplink) { newUplink in +// hasChanges = true +// } +// .onChange(of: downlink) { newDownlink in +// hasChanges = true +// } +// HStack { +// Button { +// isPresentingSaveConfirm = true +// } label: { +// Label("save", systemImage: "square.and.arrow.down") +// } +// .disabled(bleManager.connectedPeripheral == nil || !hasChanges) +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// .confirmationDialog( +// "are.you.sure", +// isPresented: $isPresentingSaveConfirm, +// titleVisibility: .visible +// ) { +// Button("Save Channel \(channelIndex) to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { +// +// var channel = Channel() +// channel.index = channelIndex +// channel.settings.id = UInt32(channelIndex) +// channel.settings.name = channelName +// channel.settings.psk = Data(base64Encoded: channelKey) ?? Data() +// channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary +// channel.settings.uplinkEnabled = uplink +// channel.settings.downlinkEnabled = downlink +// +// let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) +// +// if adminMessageId > 0 { +// // Should show a saved successfully alert once I know that to be true +// // for now just disable the button after a successful save +// channelName = "" +// hasChanges = false +// isPresentingEditView = false +// bleManager.disconnectPeripheral() +// } +// } +// } +// #if targetEnvironment(macCatalyst) +// Button { +// isPresentingEditView = false +// } label: { +// Label("Close", systemImage: "xmark") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// #endif +// } +// .presentationDetents([.medium, .large]) +// } +// } +// } +// .navigationTitle("channels") +// .navigationSplitViewStyle(.automatic) +// .navigationBarItems(trailing: +// ZStack { +// ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") +// }) +// .onAppear { +// bleManager.context = context +// } +// } +//} diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 412282aa..1516a5a9 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -37,11 +37,11 @@ struct BluetoothConfig: View { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("Enabled", systemImage: "antenna.radiowaves.left.and.right") + Label("enabled", systemImage: "antenna.radiowaves.left.and.right") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -87,7 +87,7 @@ struct BluetoothConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges || shortPin) .buttonStyle(.bordered) @@ -116,7 +116,7 @@ struct BluetoothConfig: View { Text("After bluetooth config saves the node will reboot.") } } - .navigationTitle("Bluetooth (BLE) Config") + .navigationTitle("bluetooth.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 6800a117..1abc2cdf 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -31,7 +31,7 @@ struct DeviceConfig: View { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Picker("Device Role", selection: $deviceRole ) { ForEach(DeviceRoles.allCases) { dr in @@ -96,7 +96,7 @@ struct DeviceConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingNodeDBResetConfirm, titleVisibility: .visible ) { @@ -143,7 +143,7 @@ struct DeviceConfig: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -182,7 +182,7 @@ struct DeviceConfig: View { Spacer() } - .navigationTitle("Device Config") + .navigationTitle("device.config") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 318bb111..13b12138 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -97,7 +97,7 @@ struct DisplayConfig: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -106,7 +106,7 @@ struct DisplayConfig: View { .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm ) { Button("Save Display Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { @@ -131,7 +131,7 @@ struct DisplayConfig: View { } } } - .navigationTitle("Display Config") + .navigationTitle("display.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index e5584f3d..6f5164a3 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -63,7 +63,7 @@ struct LoRaConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -94,7 +94,7 @@ struct LoRaConfig: View { Text("After LoRa config saves the node will reboot.") } } - .navigationTitle("LoRa Config") + .navigationTitle("lora.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 6d83625a..a7b16b39 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -47,11 +47,11 @@ struct CannedMessagesConfig: View { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("Enabled", systemImage: "list.bullet.rectangle.fill") + Label("enabled", systemImage: "list.bullet.rectangle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -212,7 +212,7 @@ struct CannedMessagesConfig: View { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || (!hasChanges && !hasMessagesChanges)) .buttonStyle(.bordered) @@ -220,7 +220,7 @@ struct CannedMessagesConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { @@ -265,7 +265,7 @@ struct CannedMessagesConfig: View { } } } - .navigationTitle("Canned Messages Config") + .navigationTitle("canned.messages.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 5bceaec3..69896e77 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -68,9 +68,9 @@ struct ExternalNotificationConfig: View { VStack { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("Enabled", systemImage: "megaphone") + Label("enabled", systemImage: "megaphone") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $alertBell) { @@ -123,7 +123,7 @@ struct ExternalNotificationConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -131,7 +131,7 @@ struct ExternalNotificationConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { @@ -153,7 +153,7 @@ struct ExternalNotificationConfig: View { } } } - .navigationTitle("External Notification Config") + .navigationTitle("external.notification.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index a8b135d9..89f472dc 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -26,11 +26,11 @@ struct MQTTConfig: View { VStack { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("Enabled", systemImage: "dot.radiowaves.right") + Label("enabled", systemImage: "dot.radiowaves.right") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -78,8 +78,8 @@ struct MQTTConfig: View { .autocorrectionDisabled() HStack { - Label("Username", systemImage: "person.text.rectangle") - TextField("Server Username", text: $username) + Label("mqtt.username", systemImage: "person.text.rectangle") + TextField("mqtt.username", text: $username) .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) @@ -105,8 +105,8 @@ struct MQTTConfig: View { .keyboardType(.default) .scrollDismissesKeyboard(.interactively) HStack { - Label("Password", systemImage: "wallet.pass") - TextField("Server Password", text: $password) + Label("password", systemImage: "wallet.pass") + TextField("password", text: $password) .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) @@ -144,7 +144,7 @@ struct MQTTConfig: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -152,7 +152,7 @@ struct MQTTConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { @@ -174,7 +174,7 @@ struct MQTTConfig: View { } } } - .navigationTitle("MQTT Config") + .navigationTitle("mqtt.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 56ea350f..38ecae08 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -61,10 +61,10 @@ struct RangeTestConfig: View { var body: some View { VStack { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("Enabled", systemImage: "figure.walk") + Label("enabled", systemImage: "figure.walk") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Picker("Sender Interval", selection: $sender ) { @@ -76,7 +76,7 @@ struct RangeTestConfig: View { Text("This device will send out range test messages on the selected interval.") .font(.caption) Toggle(isOn: $save) { - Label("Save", systemImage: "square.and.arrow.down.fill") + Label("save", systemImage: "square.and.arrow.down.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .disabled(!(node != nil && node!.myInfo?.hasWifi ?? false)) @@ -88,7 +88,7 @@ struct RangeTestConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.myInfo?.hasWifi ?? false)) .buttonStyle(.bordered) @@ -96,7 +96,7 @@ struct RangeTestConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { @@ -114,7 +114,7 @@ struct RangeTestConfig: View { } } } - .navigationTitle("Range Test Config") + .navigationTitle("range.test.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 16eea485..349373bd 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -31,30 +31,30 @@ struct SerialConfig: View { Form { - Section(header: Text("Options")) { + Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("Enabled", systemImage: "terminal") + Label("enabled", systemImage: "terminal") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $echo) { - Label("Echo", systemImage: "repeat") + Label("echo", systemImage: "repeat") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("If set, any packets you send will be echoed back to your device.") .font(.caption) - Picker("Baud Rate", selection: $baudRate ) { + Picker("Baud", selection: $baudRate ) { ForEach(SerialBaudRates.allCases) { sbr in Text(sbr.description) } } .pickerStyle(DefaultPickerStyle()) - Picker("Timeout", selection: $timeout ) { + Picker("timeout", selection: $timeout ) { ForEach(SerialTimeoutIntervals.allCases) { sti in Text(sti.description) } @@ -63,7 +63,7 @@ struct SerialConfig: View { Text("The amount of time to wait before we consider your packet as done.") .font(.caption) - Picker("Mode", selection: $mode ) { + Picker("mode", selection: $mode ) { ForEach(SerialModeTypes.allCases) { smt in Text(smt.description) } @@ -113,7 +113,7 @@ struct SerialConfig: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -122,7 +122,7 @@ struct SerialConfig: View { .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { @@ -148,7 +148,7 @@ struct SerialConfig: View { } } - .navigationTitle("Serial Config") + .navigationTitle("serial.config") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index c944d987..0eeb1902 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -86,7 +86,7 @@ struct TelemetryConfig: View { VStack { Form { - Section(header: Text("Update Intervals")) { + Section(header: Text("update.interval")) { Picker("Device Metrics", selection: $deviceUpdateInterval ) { ForEach(UpdateIntervals.allCases) { ui in Text(ui.description) @@ -108,7 +108,7 @@ struct TelemetryConfig: View { Text("Supported I2C Connected sensors will be detected automatically, sensors are BMP280, BME280, BME680, MCP9808, INA219, INA260, LPS22 and SHTC3.") .font(.caption) Toggle(isOn: $environmentMeasurementEnabled) { - Label("Enabled", systemImage: "chart.xyaxis.line") + Label("enabled", systemImage: "chart.xyaxis.line") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $environmentScreenEnabled) { @@ -125,7 +125,7 @@ struct TelemetryConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges || node!.telemetryConfig == nil) .buttonStyle(.bordered) @@ -133,7 +133,7 @@ struct TelemetryConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { @@ -154,7 +154,7 @@ struct TelemetryConfig: View { } } - .navigationTitle("Telemetry Config") + .navigationTitle("telemetry.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 3d3bafb9..c71bfaa0 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -33,12 +33,12 @@ struct NetworkConfig: View { Section(header: Text("WiFi Options (ESP32 Only)")) { Toggle(isOn: $wifiEnabled) { - Label("Enabled", systemImage: "wifi") + Label("enabled", systemImage: "wifi") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) HStack { - Label("SSID", systemImage: "network") - TextField("SSID", text: $wifiSsid) + Label("ssid", systemImage: "network") + TextField("ssid", text: $wifiSsid) .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) @@ -58,8 +58,8 @@ struct NetworkConfig: View { } .keyboardType(.default) HStack { - Label("Password", systemImage: "wallet.pass") - TextField("Password", text: $wifiPsk) + Label("password", systemImage: "wallet.pass") + TextField("password", text: $wifiPsk) .foregroundColor(.gray) .autocapitalization(.none) .disableAutocorrection(true) @@ -84,7 +84,7 @@ struct NetworkConfig: View { .disabled(!(node != nil && node!.myInfo?.hasWifi ?? false)) Section(header: Text("Ethernet Options")) { Toggle(isOn: $ethEnabled) { - Label("Enabled", systemImage: "network") + Label("enabled", systemImage: "network") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("Enabling Ethernet will disable the bluetooth connection to the app.") @@ -96,7 +96,7 @@ struct NetworkConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -128,7 +128,7 @@ struct NetworkConfig: View { Text("After network config saves the node will reboot.") } } - .navigationTitle("Network Config") + .navigationTitle("network.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 361df32d..e800a64f 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -193,7 +193,7 @@ struct PositionConfig: View { } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) @@ -201,14 +201,14 @@ struct PositionConfig: View { .controlSize(.large) .padding() .confirmationDialog( - "Are you sure?", + "are.you.sure", isPresented: $isPresentingSaveConfirm, titleVisibility: .visible ) { Button("Save Position Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { if fixedPosition { - let sendPosition = bleManager.sendPosition(destNum: bleManager.connectedPeripheral.num, wantAck: true) + _ = bleManager.sendPosition(destNum: bleManager.connectedPeripheral.num, wantResponse: false) } var pc = Config.PositionConfig() @@ -240,7 +240,7 @@ struct PositionConfig: View { } } } - .navigationTitle("Position Config") + .navigationTitle("position.config") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 7f375268..c556dcfa 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -32,7 +32,7 @@ struct SaveChannelQRCode: View { } } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .buttonStyle(.bordered) .buttonBorderShape(.capsule) @@ -44,7 +44,7 @@ struct SaveChannelQRCode: View { Button { dismiss() } label: { - Label("Cancel", systemImage: "xmark") + Label("cancel", systemImage: "xmark") } .buttonStyle(.bordered) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index da96b83d..9bb805f7 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -26,16 +26,16 @@ struct Settings: View { Image(systemName: "gearshape") .symbolRenderingMode(.hierarchical) - Text("App Settings") + Text("app.settings") } - Section("Radio Configuration") { + Section("radio.configuration") { NavigationLink { ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "qrcode") .symbolRenderingMode(.hierarchical) - Text("Share Channels QR Code") + Text("share.channels") } NavigationLink { @@ -45,7 +45,7 @@ struct Settings: View { Image(systemName: "person.crop.rectangle.fill") .symbolRenderingMode(.hierarchical) - Text("User") + Text("user") } NavigationLink() { @@ -56,16 +56,27 @@ struct Settings: View { Image(systemName: "dot.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) - Text("LoRa") + Text("lora") } +// NavigationLink() { +// +// Channels(node: nodes.first(where: { $0.num == connectedNodeNum })) +// } label: { +// +// Image(systemName: "fibrechannel") +// .symbolRenderingMode(.hierarchical) +// +// Text("channels") +// } + NavigationLink() { BluetoothConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "antenna.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) - Text("Bluetooth (BLE)") + Text("bluetooth") } NavigationLink { @@ -73,7 +84,7 @@ struct Settings: View { } label: { Image(systemName: "flipphone") .symbolRenderingMode(.hierarchical) - Text("Device") + Text("device") } NavigationLink { @@ -81,7 +92,7 @@ struct Settings: View { } label: { Image(systemName: "display") .symbolRenderingMode(.hierarchical) - Text("Display (Device Screen)") + Text("display") } NavigationLink { @@ -90,7 +101,7 @@ struct Settings: View { Image(systemName: "network") .symbolRenderingMode(.hierarchical) - Text("Network") + Text("network") } NavigationLink { @@ -99,11 +110,11 @@ struct Settings: View { Image(systemName: "location") .symbolRenderingMode(.hierarchical) - Text("Position") + Text("position") } } - Section("Module Configuration") { + Section("module.configuration") { NavigationLink { CannedMessagesConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) @@ -112,7 +123,7 @@ struct Settings: View { Image(systemName: "list.bullet.rectangle.fill") .symbolRenderingMode(.hierarchical) - Text("Canned Messages") + Text("canned.messages") } NavigationLink { @@ -120,44 +131,44 @@ struct Settings: View { } label: { Image(systemName: "megaphone") .symbolRenderingMode(.hierarchical) - Text("External Notification") + Text("external.notification") } NavigationLink { MQTTConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "dot.radiowaves.right") .symbolRenderingMode(.hierarchical) - Text("MQTT") + Text("mqtt") } NavigationLink { RangeTestConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "point.3.connected.trianglepath.dotted") .symbolRenderingMode(.hierarchical) - Text("Range Test (ESP32 Only)") + Text("range.test") } NavigationLink { SerialConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "terminal") .symbolRenderingMode(.hierarchical) - Text("Serial") + Text("serial") } NavigationLink { TelemetryConfig(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { Image(systemName: "chart.xyaxis.line") .symbolRenderingMode(.hierarchical) - Text("Telemetry (Sensors)") + Text("telemetry") } } - Section(header: Text("Logging")) { + Section(header: Text("logging")) { NavigationLink { MeshLog() } label: { Image(systemName: "list.bullet.rectangle") .symbolRenderingMode(.hierarchical) - Text("Mesh Log") + Text("mesh.log") } NavigationLink { let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) @@ -165,11 +176,11 @@ struct Settings: View { } label: { Image(systemName: "building.columns") .symbolRenderingMode(.hierarchical) - Text("Admin Message Log") + Text("admin.log") } } - Section(header: Text("About")) { + Section(header: Text("about")) { NavigationLink { @@ -180,10 +191,9 @@ struct Settings: View { Image(systemName: "questionmark.app") .symbolRenderingMode(.hierarchical) - Text("About Meshtastic") + Text("about.meshtastic") } } - } .onAppear { @@ -192,13 +202,13 @@ struct Settings: View { } .listStyle(GroupedListStyle()) - .navigationTitle("Settings") + .navigationTitle("settings") .navigationBarItems(leading: MeshtasticLogo() ) } detail: { - Text("Select an item from the menu") + Text("select.menu.item") } } } diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 75240770..5810b5cf 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -76,7 +76,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()).fixedSize() + Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()) if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index c8fe32b4..6380430b 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -72,7 +72,7 @@ struct UserConfig: View { Button { isPresentingSaveConfirm = true } label: { - Label("Save", systemImage: "square.and.arrow.down") + Label("save", systemImage: "square.and.arrow.down") } .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings new file mode 100644 index 00000000..bde184d0 --- /dev/null +++ b/de.lproj/Localizable.strings @@ -0,0 +1,183 @@ +/* + Localizable.strings + Meshtastic + + Created by Garth Vander Houwen on 12/12/22. + +*/ +"about"="Über"; +"about.meshtastic"="Über Meshtastic"; +"admin"="admin"; +"admin.log"="Admin Message Log"; +"ago"="her"; +"always.on"="Immer an"; +"app.settings"="App Einstellungen"; +"are.you.sure"="Bist Du sicher?"; +"ascii.capable"="ASCII fähig"; +"available.radios"="Geräte in der Nähe"; +"automatic.detection"="Automatische erkennung"; +"ble.name"="BLE Name"; +"bluetooth"="Bluetooth"; +"bluetooth.config"="Bluetooth Konfiguration"; +"bluetooth.mode.randompin"="Zufällige PIN"; +"bluetooth.mode.fixedpin"="Feste PIN"; +"bluetooth.mode.nopin"="Keine PIN (geht einfach)"; +"bytes"="Bytes"; +"cancel"="Abbrechen"; +"canned.messages"="Canned Messages"; +"canned.messages.config"="Canned Messages Config"; +"canned.messages.preset.manual"="Manualle Konfiguration"; +"canned.messages.preset.rakrotary"="RAK Drehimpulsgeber Modul"; +"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Tastenfeld"; +"channel"="Kanal"; +"channel.role.disabled"="Deaktiviert"; +"channel.role.primary"="Primär"; +"channel.role.secondary"="Sekundär"; +"channels"="Kanäle"; +"clear.app.data"="App Daten löschen"; +"connected.radio"="Verbundenes Gerät"; +"communicating"="Verbinde mit Gerät..."; +"connected"="Derzeit verbunden"; +"connecting"="Verbinde..."; +"contacts"="Kontakte"; +"copy"="Kopieren"; +"default"="Standard"; +"delete"="Löschen"; +"device"="Gerät"; +"device.config"="Gerätekonfiguration"; +"device.role.client"="Client (Standard) - Mit App verbundener Client."; +"device.role.clientmute"="Client Leise - Das selbe wie Client, außer das die Pakete nicht über diesen Node weitergeleitet werden. Nimmt nicht am Mesh-Routing teil."; +"device.role.router"="Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus."; +"device.role.routerclient"="Router Client - Mesh Pakete werden bevorzugt über diesen Node gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden."; +"direct.messages"="Direktnachrichten"; +"dismiss.keyboard"="Dismiss Keyboard"; +"display"="Display (Device Screen)"; +"display.config"="Display Config"; +"distance"="Entfernung"; +"disconnect"="Trennen"; +"echo"="Echo"; +"email.address"="Email Adresse"; +"enabled"="Aktiviert"; +"external.notification"="Externe Benachrichtigung"; +"external.notification.config"="Einstellungen der externen Benachrichtigung"; +"firmware.version"="Firmware Version"; +"gpsformat.dec"="Dezimalgrad Format"; +"gpsformat.dms"="Grad Minuten Sekunden"; +"gpsformat.utm"="Universal Transversal Mercator"; +"gpsformat.mgrs"="Militärisches Gitternetz-Referenzsystem"; +"gpsformat.olc"="Open Location Code (aka Plus Codes)"; +"gpsformat.osgr"="Ordnance Survey Gitterreferenz"; +"heard"="Gehört"; +"heard.last"="Zuletzt gehört"; +"hybrid"="Hybrid"; +"inputevent.none"="Keins"; +"inputevent.up"="Hoch"; +"inputevent.down"="Runter"; +"inputevent.left"="Links"; +"inputevent.right"="Rechts"; +"inputevent.select"="Auswählen"; +"inputevent.back"="Zurück"; +"inputevent.cancel"="Abbrechen"; +"interval.one.second"="Eine Sekunde"; +"interval.two.seconds"="Zwei Sekunden"; +"interval.five.seconds"="Fünf Sekunden"; +"interval.ten.seconds"="Zehn Sekunden"; +"interval.fifteen.seconds"="Fünfzehn Sekunden"; +"interval.twenty.seconds"="Zwanzig Sekunden"; +"interval.twentyfive.seconds"="Fünfundzwanzig Sekunden"; +"interval.thirty.seconds"="Dreißig Sekunden"; +"interval.one.minute"="Eine Minute"; +"interval.two.minutes"="Zwei Minutes"; +"interval.five.minutes"="Fünf Minutes"; +"interval.ten.minutes"="Zehn Minutes"; +"interval.fifteen.minutes"="Fünfzehn Minutes"; +"interval.thirty.minutes"="Dreißig Minutes"; +"interval.one.hour"="Eine Stunde"; +"interval.six.hours"="Sechs Stunden"; +"interval.twelve.hours"="Zwölf Stunden"; +"interval.twentyfour.hours"="Vierundzwanzig Stunden"; +"keyboard.type"="Keyboard Typ"; +"logging"="Logging"; +"lora"="LoRa"; +"lora.config"="LoRa Einstellungen"; +"map"="Mesh Karte"; +"map.type"="kartentyp"; +"mesh.log"="Mesh Log"; +"message"="Nachricht"; +"message.details"="Nachrichtendetails"; +"messages"="Nachrichten"; +"mode"="Modus"; +"module.configuration"="Modul Konfiguration"; +"mqtt"="MQTT"; +"mqtt.config"="MQTT Config"; +"mqtt.username"="Benutzername"; +"name"="Name"; +"network"="Netzwerk"; +"network.config"="Netzwerkeinstellungen"; +"nodes"="Nodes"; +"no.nodes"="Keine Meshtastic Nodes gefunden"; +"not.connected"="Kein Gerät verbunden"; +"numbers.punctuation"="Ziffern und Interpunktion"; +"off"="Aus"; +"on.boot"="Nur beim Starten"; +"options"="Optionen"; +"password"="Passwort"; +"phone.gps"="Telefon GPS"; +"phone.gps.interval.description"="Wie häufig das Telefon den Standort an das Gerät sendet. Standortaktualisierungen an das Mesh werden vom Gerät verwaltet."; +"position"="Position"; +"position.config"="Positionseinstellungen"; +"preferred.radio"="Bevorzugtes Gerät"; +"provide.location"="Standort im Mesh veröffentlichen"; +"radio.configuration"="Geräteeinstellungen"; +"range.test"="Entfernungstest"; +"range.test.config"="Entfernungstest Konfiguration"; +"reply"="Antworten"; +"received.ack"="Empfangsbestätigung"; +"routing.acknowledged"="Bestätigt"; +"routing.noroute"="Keine Route"; +"routing.gotnak"="Negative Empfangsbestätigung empfangen"; +"routing.timeout"="Zeitlimit erreicht"; +"routing.nointerface"="Kein Interface"; +"routing.maxretransmit"="Maximale Wiederholungen erreicht"; +"routing.nochannel"="Kein Kanal"; +"routing.toolarge"="Das Paket ist zu groß"; +"routing.noresponse"="Keine Antwort"; +"routing.dutycyclelimit"="Regionale Einschaltdauergrenze erreicht"; +"routing.badRequest"="Bad Request"; +"routing.notauthorized"="Nicht authorisiert"; +"satellite"="Satellit"; +"save"="Speichern"; +"serial"="Serial"; +"serial.config"="Serial Konfiguration"; +"serial.mode.default"="Standard"; +"serial.mode.simple"="Einfach"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="Text Nachricht"; +"serial.mode.nmea"="NMEA Positionen"; +"settings"="Einstellungen"; +"share.channels"="Kanal QR Code teilen"; +"share.position"="Position teilen"; +"subscribed"="Subscribed to mesh"; +"select.contact"="Kontakt wählen"; +"select.node"="Node auswählen"; +"select.menu.item"="Wähle einen Menüeintrag aus"; +"set.region"="Setze LoRa Region"; +"standard"="Standard"; +"ssid"="SSID"; +"tapback"="Tapback Response"; +"tapback.heart"="Gehört"; +"tapback.thumbsup"="Daumen hoch"; +"tapback.thumbsdown"="Daumen runter"; +"tapback.haha"="HaHa"; +"tapback.exclamation"="Ausrufezeichen"; +"tapback.question"="Fragezeichen"; +"tapback.poop"="Kacke"; +"telemetry"="Telemetrie (Sensoren)"; +"telemetry.config"="Telemetrie Einstellungen"; +"timeout"="Zeitlimit erreicht"; +"twitter"="Twitter"; +"unknown.age"="Unbekanntes alter"; +"update.interval"="Update intervall"; +"user"="Benutzer"; +"user.details"="Benutzer Details"; +"waiting"="Warte..."; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings new file mode 100644 index 00000000..dbe62ed9 --- /dev/null +++ b/en.lproj/Localizable.strings @@ -0,0 +1,183 @@ +/* + Localizable.strings + Meshtastic + + Copyright(c) Garth Vander Houwen on 12/12/22. + +*/ +"about"="About"; +"about.meshtastic"="About Meshtastic"; +"admin"="Admin"; +"admin.log"="Admin Message Log"; +"ago"="ago"; +"always.on"="Always On"; +"app.settings"="App Settings"; +"are.you.sure"="Are you sure?"; +"ascii.capable"="ASCII Capable"; +"available.radios"="Available Radios"; +"automatic.detection"="Automatic Detection"; +"ble.name"="BLE Name"; +"bluetooth"="Bluetooth"; +"bluetooth.config"="Bluetooth Config"; +"bluetooth.mode.randompin"="Random PIN"; +"bluetooth.mode.fixedpin"="Fixed PIN"; +"bluetooth.mode.nopin"="No PIN (Just Works)"; +"bytes"="Bytes"; +"cancel"="Cancel"; +"canned.messages"="Canned Messages"; +"canned.messages.config"="Canned Messages Config"; +"canned.messages.preset.manual"="Manual Configuration"; +"canned.messages.preset.rakrotary"="RAK Rotary Encoder Module"; +"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Keypad"; +"channel"="Channel"; +"channel.role.disabled"="Disabled"; +"channel.role.primary"="Primary"; +"channel.role.secondary"="Secondary"; +"channels"="Channels"; +"clear.app.data"="Clear App Data"; +"connected.radio"="Connected Radio"; +"communicating"="Communicating with device. ."; +"connected"="Currently Connected"; +"connecting"="Connecting . ."; +"contacts"="Contacts"; +"copy"="Copy"; +"default"="Default"; +"delete"="Delete"; +"device"="Device"; +"device.config"="Device Config"; +"device.role.client"="Client (default) - App connected client."; +"device.role.clientmute"="Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh."; +"device.role.router"="Router - Mesh packets will prefer to be routed over this node. This node will not be used by client apps. The wifi/ble radios and the oled screen will be put to sleep."; +"device.role.routerclient"="Router Client - Mesh packets will prefer to be routed over this node. The Router Client can be used as both a Router and an app connected Client."; +"direct.messages"="Direct Messages"; +"dismiss.keyboard"="Dismiss Keyboard"; +"display"="Display (Device Screen)"; +"display.config"="Display Config"; +"distance"="Distance"; +"disconnect"="Disconnect"; +"echo"="Echo"; +"email.address"="Email Address"; +"enabled"="Enabled"; +"external.notification"="External Notification"; +"external.notification.config"="External Notification Config"; +"firmware.version"="Firmware Version"; +"gpsformat.dec"="Decimal Degrees Format"; +"gpsformat.dms"="Degrees Minutes Seconds"; +"gpsformat.utm"="Universal Transverse Mercator"; +"gpsformat.mgrs"="Military Grid Reference System"; +"gpsformat.olc"="Open Location Code (aka Plus Codes)"; +"gpsformat.osgr"="Ordnance Survey Grid Reference"; +"heard"="Heard"; +"heard.last"="Last Heard"; +"hybrid"="Hybrid"; +"inputevent.none"="None"; +"inputevent.up"="Up"; +"inputevent.down"="Down"; +"inputevent.left"="Left"; +"inputevent.right"="Right"; +"inputevent.select"="Select"; +"inputevent.back"="Back"; +"inputevent.cancel"="Cancel"; +"interval.one.second"="One Second"; +"interval.two.seconds"="Two Seconds"; +"interval.five.seconds"="Five Seconds"; +"interval.ten.seconds"="Ten Seconds"; +"interval.fifteen.seconds"="Fifteen Seconds"; +"interval.twenty.seconds"="Twenty Seconds"; +"interval.twentyfive.seconds"="Twenty Five Seconds"; +"interval.thirty.seconds"="Thirty Seconds"; +"interval.one.minute"="One Minute"; +"interval.two.minutes"="Two Minutes"; +"interval.five.minutes"="Five Minutes"; +"interval.ten.minutes"="Ten Minutes"; +"interval.fifteen.minutes"="Fifteen Minutes"; +"interval.thirty.minutes"="Thirty Minutes"; +"interval.one.hour"="One Hour"; +"interval.six.hours"="Six Hours"; +"interval.twelve.hours"="Twelve Hours"; +"interval.twentyfour.hours"="Twenty Four Hours"; +"keyboard.type"="Keyboard Type"; +"logging"="Logging"; +"lora"="LoRa"; +"lora.config"="LoRa Config"; +"map"="Mesh Map"; +"map.type"="Map Type"; +"mesh.log"="Mesh Log"; +"message"="Message"; +"message.details"="Message Details"; +"messages"="Messages"; +"mode"="Mode"; +"module.configuration"="Module Configuration"; +"mqtt"="MQTT"; +"mqtt.config"="MQTT Config"; +"mqtt.username"="Username"; +"name"="Name"; +"network"="Network"; +"network.config"="Network Config"; +"nodes"="Nodes"; +"no.nodes"="No Meshtastic Nodes Found"; +"not.connected"="No device connected"; +"numbers.punctuation"="Numbers and Punctuation"; +"off"="Off"; +"on.boot"="On Boot Only"; +"options"="Options"; +"password"="Password"; +"phone.gps"="Phone GPS"; +"phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; +"position"="Position"; +"position.config"="Position Config"; +"preferred.radio"="Preferred Radio"; +"provide.location"="Provide location to mesh"; +"radio.configuration"="Radio Configuration"; +"range.test"="Range Test"; +"range.test.config"="Range Test Config"; +"reply"="Reply"; +"received.ack"="Received Ack"; +"routing.acknowledged"="Acknowledged"; +"routing.noroute"="No Route"; +"routing.gotnak"="Received a negative acknowledgment"; +"routing.timeout"="Timeout"; +"routing.nointerface"="No Interface"; +"routing.maxretransmit"="Max Retransmission Reached"; +"routing.nochannel"="No Channel"; +"routing.toolarge"="The packet is too large"; +"routing.noresponse"="No Response"; +"routing.dutycyclelimit"="Regional Duty Cycle Limit Reached"; +"routing.badRequest"="Bad Request"; +"routing.notauthorized"="Not Authorized"; +"satellite"="Satellite"; +"save"="Save"; +"serial"="Serial"; +"serial.config"="Serial Config"; +"serial.mode.default"="Default"; +"serial.mode.simple"="Simple"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="Text Message"; +"serial.mode.nmea"="NMEA Positions"; +"settings"="Settings"; +"share.channels"="Share Channels QR Code"; +"share.position"="Share Position"; +"subscribed"="Subscribed to mesh"; +"select.contact"="Select a Contact"; +"select.node"="Select a Node"; +"select.menu.item"="Select an item from the menu"; +"set.region"="Set LoRa Region"; +"standard"="Standard"; +"ssid"="SSID"; +"tapback"="Tapback Response"; +"tapback.heart"="Heart"; +"tapback.thumbsup"="Thumbs Up"; +"tapback.thumbsdown"="Thumbs Down"; +"tapback.haha"="HaHa"; +"tapback.exclamation"="Exclamation Mark"; +"tapback.question"="Question Mark"; +"tapback.poop"="Poop"; +"telemetry"="Telemetry (Sensors)"; +"telemetry.config"="Telemetry Config"; +"timeout"="timeout"; +"twitter"="Twitter"; +"unknown.age"="Unknown Age"; +"update.interval"="Update Interval"; +"user"="User"; +"user.details"="User Details"; +"waiting"="Waiting. . .";