From 4b847b6b088ce10ec2caed3a7a9c12e5c5a14ceb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 5 Apr 2026 08:43:44 -0700 Subject: [PATCH] Fix emojionlytextfield regression by @rcgv1 --- Localizable.xcstrings | 102 +---- Meshtastic/Views/Helpers/CompassView.swift | 369 ++++++++++-------- .../Nodes/Helpers/Map/WaypointForm.swift | 23 +- 3 files changed, 222 insertions(+), 272 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3a8d12cf..e8c21ff6 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -233,12 +233,6 @@ "value" : ": %@" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : " : %@" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -280,12 +274,6 @@ "value" : ": %d" } }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : " : %d" - } - }, "it" : { "stringUnit" : { "state" : "translated", @@ -7774,6 +7762,7 @@ } }, "Bearing: %@" : { + "extractionState" : "stale", "localizations" : { "ru" : { "stringUnit" : { @@ -7784,6 +7773,7 @@ } }, "Bearing: N/A" : { + "extractionState" : "stale", "localizations" : { "ru" : { "stringUnit" : { @@ -17895,6 +17885,7 @@ } }, "Distance: %@" : { + "extractionState" : "stale", "localizations" : { "es" : { "stringUnit" : { @@ -31390,10 +31381,6 @@ "comment" : "A description of the read-only mode feature in TAK Server.", "isCommentAutoGenerated" : true }, - "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { - "comment" : "Privacy policy text for Meshtastic.", - "isCommentAutoGenerated" : true - }, "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { "localizations" : { "es" : { @@ -52718,7 +52705,6 @@ } } } - }, "TAK Cannot Be Used on Public Channel" : { "comment" : "A warning displayed when the user's primary channel is public.", @@ -62963,88 +62949,6 @@ } } } - }, - ": %@" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - } - }, - "shouldTranslate" : false - }, - ": %d" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - } - }, - "shouldTranslate" : false } }, "version" : "1.1" diff --git a/Meshtastic/Views/Helpers/CompassView.swift b/Meshtastic/Views/Helpers/CompassView.swift index c7185acf..c8c0b9bc 100644 --- a/Meshtastic/Views/Helpers/CompassView.swift +++ b/Meshtastic/Views/Helpers/CompassView.swift @@ -13,9 +13,9 @@ struct CompassView: View { /// Single waypoint parameter let waypointLocation: CLLocationCoordinate2D? - + let waypointName: String? - + let color: Color @ObservedObject private var locationsHandler = LocationsHandler.shared @@ -24,6 +24,8 @@ struct CompassView: View { private let alignmentTolerance: Double = 5.0 @State private var inAlignment = false + private let dialRadius: CGFloat = 140 + // Compute bearing from user → waypoint private func bearingToWaypoint() -> Double? { guard @@ -39,10 +41,9 @@ struct CompassView: View { // Trigger a vibration if aligned with waypoint private func checkAlignment(bearing: Double, heading: Double) { - // Compute minimal angular difference between heading and bearing in [0, 180] - let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360) - let diff = min(rawDiff, 360 - rawDiff) - + let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360) + let diff = min(rawDiff, 360 - rawDiff) + if diff <= alignmentTolerance { if !inAlignment { inAlignment = true @@ -53,124 +54,246 @@ struct CompassView: View { inAlignment = false } } - + private func distanceToWaypoint() -> CLLocationDistance? { guard let waypoint = waypointLocation, let user = LocationsHandler.currentLocation else { return nil } - + let userLocation = CLLocation(latitude: user.latitude, longitude: user.longitude) let waypointLocation = CLLocation(latitude: waypoint.latitude, longitude: waypoint.longitude) - + return userLocation.distance(from: waypointLocation) } - // Format distance with localization private func formatDistance(_ distance: CLLocationDistance) -> String { let measurement = Measurement(value: distance, unit: UnitLength.meters) let formatter = MeasurementFormatter() formatter.unitOptions = .naturalScale - formatter.numberFormatter.maximumFractionDigits = 2 + formatter.numberFormatter.maximumFractionDigits = 1 return formatter.string(from: measurement) } - + var body: some View { NavigationStack { - VStack(spacing: 15) { - - VStack(spacing: 8) { - Text(waypointName ?? "Waypoint") - .font(.title2) - .bold() - .foregroundColor(color) - + ZStack { + Color.black.ignoresSafeArea() + + VStack(spacing: 0) { + // Top fixed heading indicator triangle + Image(systemName: "triangle.fill") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.white) + .rotationEffect(.degrees(180)) + .padding(.bottom, 4) + + // Rotating compass dial + ZStack { + // Outer bezel ring + Circle() + .stroke(Color.white.opacity(0.2), lineWidth: 1.5) + .frame(width: dialRadius * 2 + 20, height: dialRadius * 2 + 20) + + // Tick marks + ForEach(0..<360, id: \.self) { degree in + CompassTickMark(degree: Double(degree), radius: dialRadius) + } + + // Cardinal and intercardinal labels + ForEach(CompassLabel.allLabels, id: \.degrees) { label in + CompassLabelView(label: label, radius: dialRadius - 28) + .rotationEffect(.degrees(-locationsHandler.heading)) + } + + // North triangle indicator at 0° + CompassNorthIndicator(radius: dialRadius + 2) + + // Degree readout at center + VStack(spacing: 4) { + Text(headingText()) + .font(.system(size: 42, weight: .light, design: .rounded)) + .foregroundColor(.white) + .monospacedDigit() + + if let distance = distanceToWaypoint() { + Text(formatDistance(distance)) + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundColor(color) + } + + if waypointName != nil || waypointLocation != nil { + Text(waypointName ?? "Waypoint") + .font(.system(size: 13, weight: .medium)) + .foregroundColor(color.opacity(0.8)) + } + } + + // Waypoint bearing indicator + if let bearing = bearingToWaypoint() { + WaypointMarkerView( + bearing: bearing, + radius: dialRadius + 14, + color: color + ) + .onChange(of: locationsHandler.heading) { _, _ in + checkAlignment(bearing: bearing, heading: locationsHandler.heading) + } + } + } + .frame(width: dialRadius * 2 + 40, height: dialRadius * 2 + 40) + .rotationEffect(Angle(degrees: -locationsHandler.heading)) + + // Bottom info if let wp = waypointLocation { - HStack { - Image(systemName: "mappin.and.ellipse") - Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))") - .font(.subheadline) - } - - if let distance = distanceToWaypoint() { - HStack { - Image(systemName: "lines.measurement.horizontal") - Text("Distance: \(formatDistance(distance))") - .font(.subheadline) - .fontWeight(.semibold) + VStack(spacing: 6) { + HStack(spacing: 4) { + Image(systemName: "mappin") + .font(.system(size: 11)) + Text("\(String(format: "%.4f", wp.latitude)), \(String(format: "%.4f", wp.longitude))") + .font(.system(size: 12, design: .monospaced)) } - } - HStack { - Image(systemName: "location.north") + .foregroundColor(.white.opacity(0.5)) + if let bearing = bearingToWaypoint() { - Text("Bearing: \(String(format: "%.0f°", bearing))") - .font(.subheadline) - } else { - Text("Bearing: N/A") - .font(.subheadline) + HStack(spacing: 4) { + Image(systemName: "location.north.fill") + .font(.system(size: 11)) + .rotationEffect(.degrees(bearing)) + Text("\(String(format: "%.0f°", bearing))") + .font(.system(size: 12, weight: .medium, design: .monospaced)) + } + .foregroundColor(color.opacity(0.7)) } } + .padding(.top, 20) } } - .padding() - - Capsule() - .frame(width: 5, height: 50) - ZStack { - - // Cardinal/degree markers - ForEach(Marker.markers(), id: \.self) { marker in - CompassMarkerView( - marker: marker, - compassDegrees: -locationsHandler.heading - ) - } - - // Waypoint bearing indicator - if let bearing = bearingToWaypoint() { - WaypointMarkerView( - bearing: bearing, - compassDegrees: locationsHandler.heading, - color: color - ) - // Move waypoint marker outside compass - .onChange(of: locationsHandler.heading) { _, _ in - checkAlignment(bearing: bearing, heading:locationsHandler.heading) - } - } - - } - .frame(width: 300, height: 300) - .rotationEffect(Angle(degrees: -locationsHandler.heading)) - .statusBar(hidden: true) - .onAppear { - locationsHandler.startHeadingUpdates() - locationsHandler.startLocationUpdates() - } - .onDisappear { - locationsHandler.stopHeadingUpdates() - locationsHandler.stopLocationUpdates() - } - .navigationTitle("Compass") } + .statusBar(hidden: true) + .onAppear { + locationsHandler.startHeadingUpdates() + locationsHandler.startLocationUpdates() + } + .onDisappear { + locationsHandler.stopHeadingUpdates() + locationsHandler.stopLocationUpdates() + } + .navigationTitle("Compass") + .toolbarColorScheme(.dark, for: .navigationBar) } } + + private func headingText() -> String { + let h = Int(locationsHandler.heading.rounded()) % 360 + return "\(h)°" + } +} + +// MARK: - Compass Tick Mark +struct CompassTickMark: View { + let degree: Double + let radius: CGFloat + + var body: some View { + let isCardinal = degree.truncatingRemainder(dividingBy: 90) == 0 + let isMajor = degree.truncatingRemainder(dividingBy: 30) == 0 + let isMinor = degree.truncatingRemainder(dividingBy: 10) == 0 + + let length: CGFloat = isCardinal ? 16 : (isMajor ? 12 : (isMinor ? 8 : 4)) + let width: CGFloat = isCardinal ? 2.5 : (isMajor ? 1.5 : 1) + let tickColor: Color = isCardinal ? .white : (isMajor ? .white.opacity(0.7) : .white.opacity(0.3)) + + // Only draw ticks at 2° intervals + if Int(degree) % 2 == 0 { + Capsule() + .fill(tickColor) + .frame(width: width, height: length) + .offset(y: -(radius - length / 2)) + .rotationEffect(.degrees(degree)) + } + } +} + +// MARK: - North Indicator +struct CompassNorthIndicator: View { + let radius: CGFloat + + var body: some View { + Triangle() + .fill(Color.orange) + .frame(width: 12, height: 10) + .offset(y: -(radius + 8)) + } +} + +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.closeSubpath() + return path + } +} + +// MARK: - Compass Label Model & View +struct CompassLabel { + let degrees: Double + let text: String + let isCardinal: Bool + + static let allLabels: [CompassLabel] = [ + CompassLabel(degrees: 0, text: "N", isCardinal: true), + CompassLabel(degrees: 45, text: "NE", isCardinal: false), + CompassLabel(degrees: 90, text: "E", isCardinal: true), + CompassLabel(degrees: 135, text: "SE", isCardinal: false), + CompassLabel(degrees: 180, text: "S", isCardinal: true), + CompassLabel(degrees: 225, text: "SW", isCardinal: false), + CompassLabel(degrees: 270, text: "W", isCardinal: true), + CompassLabel(degrees: 315, text: "NW", isCardinal: false) + ] +} + +struct CompassLabelView: View { + let label: CompassLabel + let radius: CGFloat + + var body: some View { + Text(label.text) + .font(.system(size: label.isCardinal ? 18 : 13, + weight: label.isCardinal ? .bold : .medium)) + .foregroundColor(label.degrees == 0 ? .orange : .white) + .rotationEffect(.degrees(-label.degrees)) + .offset(y: -radius) + .rotationEffect(.degrees(label.degrees)) + } } // MARK: - Waypoint Marker View struct WaypointMarkerView: View { let bearing: Double - let compassDegrees: Double + let radius: CGFloat let color: Color var body: some View { - Circle() - .frame(width: 20, height: 20) - .foregroundColor(color) - .offset(y: -170) - .rotationEffect(Angle(degrees: bearing)) - } + ZStack { + // Outer glow + Image(systemName: "arrowtriangle.up.fill") + .font(.system(size: 20, weight: .bold)) + .foregroundColor(color.opacity(0.3)) + .offset(y: -(radius + 4)) + .rotationEffect(.degrees(bearing)) + // Arrow + Image(systemName: "arrowtriangle.up.fill") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(color) + .offset(y: -(radius + 5)) + .rotationEffect(.degrees(bearing)) + } + } } // MARK: - Bearing Calculator @@ -199,78 +322,6 @@ struct BearingCalculator { } } -// MARK: - Marker Model -struct Marker: Hashable { - let degrees: Double - let label: String - - init(degrees: Double, label: String = "") { - self.degrees = degrees - self.label = label - } - - func degreeText() -> String { - return String(format: "%.0f", self.degrees) - } - - static func markers() -> [Marker] { - return [ - Marker(degrees: 0, label: "N"), - Marker(degrees: 30), - Marker(degrees: 60), - Marker(degrees: 90, label: "E"), - Marker(degrees: 120), - Marker(degrees: 150), - Marker(degrees: 180, label: "S"), - Marker(degrees: 210), - Marker(degrees: 240), - Marker(degrees: 270, label: "W"), - Marker(degrees: 300), - Marker(degrees: 330) - ] - } -} - -// MARK: - Compass Marker View -struct CompassMarkerView: View { - let marker: Marker - let compassDegrees: Double - - var body: some View { - VStack { - Text(marker.degreeText()) - .fontWeight(.light) - .rotationEffect(textAngle()) - - Capsule() - .frame(width: capsuleWidth(), height: capsuleHeight()) - .foregroundColor(capsuleColor()) - - Text(marker.label) - .fontWeight(.bold) - .rotationEffect(textAngle()) - .padding(.bottom, 180) - } - .rotationEffect(Angle(degrees: marker.degrees)) - } - - private func capsuleWidth() -> CGFloat { - marker.degrees == 0 ? 7 : 3 - } - - private func capsuleHeight() -> CGFloat { - marker.degrees == 0 ? 45 : 30 - } - - private func capsuleColor() -> Color { - marker.degrees == 0 ? .red : .gray - } - - private func textAngle() -> Angle { - Angle(degrees: -compassDegrees - marker.degrees) - } -} - // MARK: - Preview struct CompassView_Previews: PreviewProvider { static var previews: some View { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 6054e4af..1260f4f5 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -114,24 +114,19 @@ struct WaypointForm: View { HStack { Text("Icon") Spacer() - EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") + TextField("Select an emoji", text: $icon) + .keyboardType(.emoji) .font(.title) .focused($iconIsFocused) - .onChange(of: icon) { - // If it contains non-emoji characters, clear it - if !icon.onlyEmojis() { - icon = "" - return + .onChange(of: icon) { _, value in + // If a second emoji is entered delete the first one + if value.count >= 1 { + if value.count > 1 { + let index = value.index(value.startIndex, offsetBy: 1) + icon = String(value[index]) + } } - - // If multiple emojis are entered or pasted, keep only the last one - if icon.count > 1 { - icon = String(icon.suffix(1)) - } - iconIsFocused = false } - - } Toggle(isOn: $expires) { Label("Expires", systemImage: "clock.badge.xmark")