diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 16941a31..c62f4e6a 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -35346,485 +35346,7 @@ } } } - }, - "ble.signal.strength.weak" : { - "comment" : "VoiceOver value for weak BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke schwach" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength weak" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale debole" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Слаб сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号弱" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號微弱" - } - } - } - }, - "signal_strength" : { - "comment" : "VoiceOver label for signal strength indicator", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Intensità del segnale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јачина сигнала" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强度" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強度" - } - } - } - }, - "message_size" : { - "comment" : "VoiceOver label for message size", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Nachrichtengröße" - } - }, - "en" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Message size" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Dimensione messaggio" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Величина поруке" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "消息大小" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊息大小" - } - } - } - }, - "device_charging" : { - "comment" : "VoiceOver value for charging device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" - } - } - } - }, - "Bluetooth is off.off" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth ist aus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le Bluetooth est arrêté" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "בלוטוס כבוי" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il Bluetooth è spento" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth jest wyłączony" - } - }, - "se" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth är avstängt" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Блутут је искључен" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "蓝牙已关闭" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "藍芽已關閉" - } - } - } - }, - "bytes_used" : { - "comment" : "VoiceOver value for bytes used", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d von %d Bytes verwendet" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d of %d bytes used" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d di %d byte usati" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "%d од %d бајтова искоришћено" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d字节" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "已用%d/%d位元組" - } - } - } - }, - "heading" : { - "comment" : "Heading label for VoiceOver" - }, - "Hide sidebar" : {}, - "bluetooth.not.connected" : { - "comment" : "VoiceOver label for disconnected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Bluetooth device connected" - } - } - } - }, - "device.configuration" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Gerätekonfiguration" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device Configuration" - } - }, - "he" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Configurazione del dispositivo" - } - }, - "pl" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Device Configuration" - } - }, - "se" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Enhetsinställningar" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Подешавања уређаја" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "设备配置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "設備設定" - } - } - } - }, - "ble.signal.strength.strong" : { - "comment" : "VoiceOver value for strong BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke stark" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength strong" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale forte" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Јак сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号强" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號強" - } - } - } - }, - "ble.signal.strength.normal" : { - "comment" : "VoiceOver value for normal BLE signal strength", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Signalstärke normal" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signal strength normal" - } - }, - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Segnale normale" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Нормалан сигнал" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "信号正常" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "訊號正常" - } - } - } - }, - "bluetooth.connected" : { - "comment" : "VoiceOver label for connected Bluetooth icon", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected to Bluetooth device" - } - } - } - }, - "request_position" : { - "comment" : "VoiceOver label for request position button", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Position anfordern" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request position" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Richiedi posizione" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захтевај позицију" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请求位置" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "請求位置" - } - } - } - }, - "distance" : { - "comment" : "Distance label for VoiceOver" - }, - "device_plugged_in" : { - "comment" : "VoiceOver value for plugged in device", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plugged in" - } - } - } - }, - "unknown" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "sconosciuto" - } - }, - "sr" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "непознато" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "未知" - } - } - } } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Extensions/String.swift b/Meshtastic/Extensions/String.swift index 6a57da9e..d2ae1e5a 100644 --- a/Meshtastic/Extensions/String.swift +++ b/Meshtastic/Extensions/String.swift @@ -115,17 +115,6 @@ extension String { .joined() } - /// Formats a short name like "P130" to read as "Node P 130" for VoiceOver - /// This ensures proper pronunciation of alphanumeric node IDs - func formatNodeNameForVoiceOver() -> String { - let spaced = self.replacingOccurrences( - of: #"([A-Za-z])([0-9]+)"#, - with: "$1 $2", - options: .regularExpression - ) - return "Node " + spaced - } - // Adds variation selectors to prefer the graphical form of emoji. // Looks ahead to make sure that the variation selector is not already applied. var addingVariationSelectors: String { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 60bb2072..5e9dd834 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,31 +27,6 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" - private func nodeAccessibilityLabel() -> String { - // Create a battery status string that handles charging and plugged in states - var batteryStatus: String? = nil - if let batteryLevel = node?.latestDeviceMetrics?.batteryLevel { - if batteryLevel > 100 { - // Plugged in state - batteryStatus = NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if batteryLevel == 100 { - // Charging state - batteryStatus = NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - // Normal battery percentage - batteryStatus = "Battery: \(Int(batteryLevel))%" - } - } - - return [ - node?.user?.shortName?.formatNodeNameForVoiceOver() ?? "", - "BLE Name: \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)", - "Firmware Version: \(node?.metadata?.firmwareVersion ?? "unknown".localized)", - bleManager.isSubscribed ? "Subscribed" : nil, - batteryStatus - ].compactMap { $0 }.joined(separator: ", ") - } - init () { let notificationCenter = UNUserNotificationCenter.current() notificationCenter.getNotificationSettings(completionHandler: { (settings) in @@ -111,8 +86,6 @@ struct Connect: View { } } } - .accessibilityElement(children: .ignore) - .accessibilityLabel(nodeAccessibilityLabel()) .font(.caption) .foregroundColor(Color.gray) .padding([.top]) @@ -326,8 +299,6 @@ struct Connect: View { mqttTopic: bleManager.mqttManager.topic ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } .sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) { diff --git a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift index 73d38f98..c5d17f16 100644 --- a/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/BLESignalStrengthIndicator.swift @@ -32,65 +32,47 @@ import Foundation import SwiftUI struct SignalStrengthIndicator: View { - // Accessibility: VoiceOver description - private var accessibilityDescription: String { - switch signalStrength { - case .weak: - return NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") - case .normal: - return NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") - case .strong: - return NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") - } - } + let signalStrength: BLESignalStrength - let signalStrength: BLESignalStrength + var body: some View { + HStack { + ForEach(0..<3) { bar in + RoundedRectangle(cornerRadius: 3) + .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) + .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .frame(width: 8, height: 40) + } + } + } - var body: some View { - Group { - HStack { - ForEach(0..<3) { bar in - RoundedRectangle(cornerRadius: 3) - .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) - .frame(width: 8, height: 40) - accessibilityHidden(true) // Ensures bars are ignored - } - } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("signal_strength", comment: "VoiceOver label for signal strength indicator")) - .accessibilityValue(accessibilityDescription) - } - - private func getColor() -> Color { - switch signalStrength { - case .weak: - return Color.red - case .normal: - return Color.yellow - case .strong: - return Color.green - } - } + private func getColor() -> Color { + switch signalStrength { + case .weak: + return Color.red + case .normal: + return Color.yellow + case .strong: + return Color.green + } + } } struct Divided: Shape { - var amount: CGFloat // Should be in range 0...1 - var shape: S - func path(in rect: CGRect) -> Path { - shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) - } + var amount: CGFloat // Should be in range 0...1 + var shape: S + func path(in rect: CGRect) -> Path { + shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice) + } } extension Shape { - func divided(amount: CGFloat) -> Divided { - return Divided(amount: amount, shape: self) - } + func divided(amount: CGFloat) -> Divided { + return Divided(amount: amount, shape: self) + } } enum BLESignalStrength: Int { - case weak = 0 - case normal = 1 - case strong = 2 + case weak = 0 + case normal = 1 + case strong = 2 } diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index bb9819a2..4ac61d0c 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -13,104 +13,69 @@ struct BatteryCompact: View { var color: Color var body: some View { - // Group the battery icon and label in a single accessible container HStack(alignment: .center, spacing: 0) { if let batteryLevel { - // Check for plugged in state - let isPluggedIn = batteryLevel > 100 - let isCharging = batteryLevel == 100 - - // Battery icon selection based on level - if isPluggedIn { - Image(systemName: "powerplug") - .font(iconFont) - .foregroundColor(color) - .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) // Hide from VoiceOver since container will handle it - } else if isCharging { + if batteryLevel == 100 { Image(systemName: "battery.100.bolt") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 74 { + } else if batteryLevel < 100 && batteryLevel > 74 { Image(systemName: "battery.75") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 49 { + } else if batteryLevel < 75 && batteryLevel > 49 { Image(systemName: "battery.50") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 14 { + } else if batteryLevel < 50 && batteryLevel > 14 { Image(systemName: "battery.25") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else if batteryLevel > 0 { + } else if batteryLevel < 15 && batteryLevel > 0 { Image(systemName: "battery.0") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } else { + } else if batteryLevel == 0 { Image(systemName: "battery.0") .font(iconFont) .foregroundColor(.red) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - } - - // Battery text label - if isPluggedIn { - Text("PWD") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) - } else if isCharging { - Text("CHG") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) - } else { - Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") - .foregroundStyle(.secondary) - .font(font) - .accessibilityHidden(true) + } else if batteryLevel > 100 { + Image(systemName: "powerplug") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.multicolor) } } else { - // Unknown battery state Image(systemName: "battery.0") .font(iconFont) .foregroundColor(color) .symbolRenderingMode(.multicolor) - .accessibilityHidden(true) - + } + if let batteryLevel { + if batteryLevel > 100 { + Text("PWD") + .foregroundStyle(.secondary) + .font(font) + } else if batteryLevel == 100 { + Text("CHG") + .foregroundStyle(.secondary) + .font(font) + } else { + Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%") + .foregroundStyle(.secondary) + .font(font) + } + } else { Text(verbatim: "?") .foregroundStyle(.secondary) .font(font) - .accessibilityHidden(true) } } - // Setup container-level accessibility for VoiceOver - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - // Set appropriate value based on the battery state using a computed property - .accessibilityValue(batteryLevel.map { level in - if level > 100 { - // Plugged in - same as PWD visual indicator - return NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if level == 100 { - // Charging - same as CHG visual indicator - return NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - // Normal battery level - return String(format: NSLocalizedString("battery_level_percent", comment: "VoiceOver value for battery level"), Int(level)) - } - } ?? "Unknown") } } diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 81e81e7e..952c9768 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -18,20 +18,18 @@ struct BatteryGauge: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - // For VoiceOver purposes, detect when device is plugged in (battery > 100%) - let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 - // Use a capped battery level for UI display - let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0)) + let batteryLevel = Double(mostRecent?.batteryLevel ?? 0) VStack { - if isPluggedIn { - // Use a completely standalone view for the plugged in state - // to avoid any VoiceOver confusion - PluggedInIndicator() + if batteryLevel > 100.0 { + // Plugged in + Image(systemName: "powerplug") + .font(.largeTitle) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) } else { let gradient = Gradient(colors: [.red, .orange, .green]) Gauge(value: batteryLevel, in: minValue...maxValue) { - // Accessibility for battery gauge if batteryLevel >= 0.0 && batteryLevel < 10 { Label("Battery Level %", systemImage: "battery.0") } else if batteryLevel >= 10.0 && batteryLevel < 25.00 { @@ -52,8 +50,6 @@ struct BatteryGauge: View { Text(Int(batteryLevel), format: .percent) } } - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel))) .tint(gradient) .gaugeStyle(.accessoryCircular) } @@ -67,23 +63,6 @@ struct BatteryGauge: View { } } -/// A dedicated view for showing a device is plugged in -/// With proper VoiceOver support that matches the visual indication -struct PluggedInIndicator: View { - var body: some View { - // This view is isolated from any battery measurement - // to ensure VoiceOver doesn't pick up any percentages - Image(systemName: "powerplug") - .font(.largeTitle) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - // Override the accessibility to ensure correct VoiceOver announcement - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge")) - .accessibilityValue(NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device")) - } -} - struct BatteryGauge_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 4a46db41..c795b1b0 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -21,46 +21,22 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { if deviceConnected { - // Create an HStack for connected state with proper accessibility - HStack { - if mqttUplinkEnabled || mqttDownlinkEnabled { - MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) - .accessibilityHidden(true) - } - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - .accessibilityHidden(true) - Text(name.addingVariationSelectors) - .font(name.isEmoji() ? .title : .callout) - .foregroundColor(.gray) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.connected".localized + ", " + name.formatNodeNameForVoiceOver()) + if mqttUplinkEnabled || mqttDownlinkEnabled { + MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic) + } + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) } else { - // Create a container for disconnected state - HStack { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.not.connected".localized) + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) } } else { - // Create a container for Bluetooth off state - HStack { - Text("bluetooth.off".localized) - .font(.subheadline) - .foregroundColor(.red) - .accessibilityHidden(true) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.off".localized) + Text("Bluetooth is off").font(.subheadline).foregroundColor(.red) } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index fd166f51..2f1634bc 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -6,7 +6,6 @@ struct RequestPositionButton: View { var body: some View { Button(action: action) { Image(systemName: "mappin.and.ellipse") - .accessibilityLabel(NSLocalizedString("request_position", comment: "VoiceOver label for request position button")) .symbolRenderingMode(.hierarchical) .imageScale(.large) .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index 9839e246..aacbd60d 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -6,8 +6,6 @@ struct TextMessageSize: View { var body: some View { ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .accessibilityLabel(NSLocalizedString("message_size", comment: "VoiceOver label for message size")) - .accessibilityValue(String(format: NSLocalizedString("bytes_used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes)) .frame(width: 130) .padding(5) .font(.subheadline) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 2d73d5c0..84fdf4d3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -40,7 +40,6 @@ struct IgnoreNodeButton: View { Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle") .symbolRenderingMode(.multicolor) } - // Accessibility: Label for VoiceOver } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index c5670e06..081e7adc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -46,8 +46,7 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - .accessibilityElement(children: .combine) - Section("Node") { // Node + Section("Node") { HStack(alignment: .center) { Spacer() CircleText( @@ -68,7 +67,6 @@ struct NodeDetail: View { .foregroundColor(getRssiColor(rssi: node.rssi)) .font(.caption) } - .accessibilityElement(children: .combine) } if node.telemetries?.count ?? 0 > 0 { Spacer() @@ -76,7 +74,6 @@ struct NodeDetail: View { } Spacer() } - .accessibilityElement(children: .combine) .listRowSeparator(.hidden) if let user = node.user { if !user.keyMatch { @@ -89,7 +86,6 @@ struct NodeDetail: View { .foregroundStyle(.secondary) .font(.callout) } - .accessibilityElement(children: .combine) } icon: { Image(systemName: "key.slash.fill") .symbolRenderingMode(.multicolor) @@ -108,7 +104,6 @@ struct NodeDetail: View { Text(String(node.num)) .textSelection(.enabled) } - .accessibilityElement(children: .combine) HStack { Label { @@ -121,7 +116,6 @@ struct NodeDetail: View { Text(node.num.toHex()) .textSelection(.enabled) } - .accessibilityElement(children: .combine) if let metadata = node.metadata { HStack { @@ -135,7 +129,6 @@ struct NodeDetail: View { Text(metadata.firmwareVersion ?? "Unknown".localized) } - .accessibilityElement(children: .combine) } if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { @@ -149,7 +142,6 @@ struct NodeDetail: View { Spacer() Text(deviceRole.name) } - .accessibilityElement(children: .combine) } if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { @@ -169,7 +161,6 @@ struct NodeDetail: View { Text(uptime) .textSelection(.enabled) } - .accessibilityElement(children: .combine) } if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { @@ -188,9 +179,7 @@ struct NodeDetail: View { Text(firstHeard.formatted()) .textSelection(.enabled) } - } - .accessibilityElement(children: .combine) - .onTapGesture { + }.onTapGesture { dateFormatRelative.toggle() } } @@ -214,9 +203,7 @@ struct NodeDetail: View { Text(lastHeard.formatted()) .textSelection(.enabled) } - } - .accessibilityElement(children: .combine) - .onTapGesture { + }.onTapGesture { dateFormatRelative.toggle() } } @@ -229,84 +216,79 @@ struct NodeDetail: View { if node.hasPositions && UserDefaults.environmentEnableWeatherKit || node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) { Section("Environment") { - // Group weather/environment data for better VoiceOver experience - VStack { - if !node.hasEnvironmentMetrics { - LocalWeatherConditions(location: node.latestPosition?.nodeLocation) - } else { - VStack { - if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { - IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) - .padding(.vertical) - } - LazyVGrid(columns: gridItemLayout) { - if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { - WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") - } - if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { - if let temperature = node.latestEnvironmentMetrics?.temperature { - let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) - .formatted(.number.precision(.fractionLength(0))) + "°" - HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) - } else { - HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) - } - } - if let pressure = node.latestEnvironmentMetrics?.barometricPressure { - PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) - } - if let windSpeed = node.latestEnvironmentMetrics?.windSpeed { - let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond) - let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } - let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) - WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), - gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) - } - if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H { - let locale = NSLocale.current as NSLocale - let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) - let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) - RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel) - } - if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H { - let locale = NSLocale.current as NSLocale - let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) - let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches - let unitLabel = usesMetricSystem ? "mm" : "in" - let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) - let decimals = usesMetricSystem ? 0 : 1 - let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) - RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel) - } - if let radiation = node.latestEnvironmentMetrics?.radiation { - RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") - } - if let weight = node.latestEnvironmentMetrics?.weight { - WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") - } - if let distance = node.latestEnvironmentMetrics?.distance { - DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") - } - if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature { - let locale = NSLocale.current as NSLocale - let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) - let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C" - SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit) - } - if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture { - SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") - } - } - .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) + if !node.hasEnvironmentMetrics { + LocalWeatherConditions(location: node.latestPosition?.nodeLocation) + } else { + VStack { + if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 { + IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient) + .padding(.vertical) } + LazyVGrid(columns: gridItemLayout) { + if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() { + WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP") + } + if let humidity = node.latestEnvironmentMetrics?.relativeHumidity { + if let temperature = node.latestEnvironmentMetrics?.temperature { + let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity) + .formatted(.number.precision(.fractionLength(0))) + "°" + HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint) + } else { + HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil) + } + } + if let pressure = node.latestEnvironmentMetrics?.barometricPressure { + PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144) + } + if let windSpeed = node.latestEnvironmentMetrics?.windSpeed { + let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond) + let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } + let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) + WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), + gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) + } + if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H { + let locale = NSLocale.current as NSLocale + let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) + let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) + RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel) + } + if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H { + let locale = NSLocale.current as NSLocale + let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches) + let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches + let unitLabel = usesMetricSystem ? "mm" : "in" + let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters) + let decimals = usesMetricSystem ? 0 : 1 + let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals))) + RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel) + } + if let radiation = node.latestEnvironmentMetrics?.radiation { + RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr") + } + if let weight = node.latestEnvironmentMetrics?.weight { + WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg") + } + if let distance = node.latestEnvironmentMetrics?.distance { + DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm") + } + if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature { + let locale = NSLocale.current as NSLocale + let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) + let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C" + SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit) + } + if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture { + SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%") + } + } + .padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical) } } - // Apply accessibility properties to the environment section - .accessibilityElement(children: .combine) } } if node.hasPowerMetrics && node.latestPowerMetrics != nil { @@ -316,7 +298,6 @@ struct NodeDetail: View { PowerMetrics(metric: metric) } } - .accessibilityElement(children: .combine) } } Section("Logs") { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index eb4c37b0..07f3d92c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -31,7 +31,6 @@ struct NodeInfoItem: View { .foregroundStyle(.gray) .font(.callout) } - .accessibilityElement(children: .combine) Spacer() } VStack(alignment: .center) { @@ -50,11 +49,9 @@ struct NodeInfoItem: View { .cornerRadius(5) } } - .accessibilityElement(children: .combine) } Spacer() } - .accessibilityElement(children: .combine) .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { @@ -82,7 +79,6 @@ struct NodeInfoItem: View { Text(String("incomplete".localized)) } } - .accessibilityElement(children: .combine) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 62dd5fd0..2978ceab 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -7,99 +7,9 @@ import SwiftUI import CoreLocation -import Foundation struct NodeListItem: View { - // Accessibility: Synthesized description for VoiceOver - private var accessibilityDescription: String { - var desc = "" - if let shortName = node.user?.shortName { - // Format the shortName using the String extension method - desc = shortName.formatNodeNameForVoiceOver() - } else if let longName = node.user?.longName { - desc = longName - } else { - desc = "unknown node" - } - if connected { - desc += ", currently connected" - } - if node.favorite { - desc += ", favorite" - } - if node.lastHeard != nil { - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .full - let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date()) - desc += ", last heard " + relative - } - if node.isOnline { - desc += ", online" - } else { - desc += ", offline" - } - let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) - if let roleName = role?.name { - desc += ", role: \(roleName)" - } - if node.hopsAway > 0 { - desc += ", \(node.hopsAway) hops away" - } - if let battery = node.latestDeviceMetrics?.batteryLevel { - // Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge - if battery > 100 { - desc += ", " + NSLocalizedString("device_plugged_in", comment: "VoiceOver value for plugged in device") - } else if battery == 100 { - desc += ", " + NSLocalizedString("device_charging", comment: "VoiceOver value for charging device") - } else { - desc += ", battery \(battery)%" - } - } - // Add distance and heading/bearing if available, but only for non-connected nodes - if !connected, let (lastPosition, myCoord) = locationData { - let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude) - let metersAway = nodeCoord.distance(from: myCoord) - - // Distance information - let distanceFormatter = LengthFormatter() - distanceFormatter.unitStyle = .medium - let formattedDistance = distanceFormatter.string(fromMeters: metersAway) - // For VoiceOver, prepend 'Distance' (localized) - desc += ", " + String(format: "%@: %@", NSLocalizedString("distance", comment: "Distance label for VoiceOver"), formattedDistance) - - // Add bearing/heading information for VoiceOver - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) - let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) - let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))) - // Using a direct format without requiring a new localization key - desc += ", " + NSLocalizedString("heading", comment: "Heading label for VoiceOver") + " " + formattedHeading - } - // Add signal strength if available - if node.snr != 0 && !node.viaMqtt { - let signalStrength: BLESignalStrength - if node.snr < -10 { - signalStrength = .weak - } else if node.snr < 5 { - signalStrength = .normal - } else { - signalStrength = .strong - } - let signalString: String - switch signalStrength { - case .weak: - signalString = NSLocalizedString("ble.signal.strength.weak", comment: "VoiceOver value for weak BLE signal strength") - case .normal: - signalString = NSLocalizedString("ble.signal.strength.normal", comment: "VoiceOver value for normal BLE signal strength") - case .strong: - signalString = NSLocalizedString("ble.signal.strength.strong", comment: "VoiceOver value for strong BLE signal strength") - } - desc += ", " + signalString - } - return desc - } - - @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 @@ -257,10 +167,7 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - // Accessibility: Make the whole row a single element for VoiceOver - .accessibilityElement(children: .ignore) - .accessibilityLabel(accessibilityDescription) - } + } } struct DefaultIcon: View { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 9af36fbd..a17a19d0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -235,8 +235,6 @@ struct NodeList: View { phoneOnly: true ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } content: { if let node = selectedNode { @@ -255,7 +253,6 @@ struct NodeList: View { } label: { Image(systemName: "rectangle") } - .accessibilityLabel("Hide sidebar") } ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, @@ -264,8 +261,6 @@ struct NodeList: View { phoneOnly: true ) } - // Make sure the ZStack passes through accessibility to the ConnectedDevice component - .accessibilityElement(children: .contain) ) } } else {