Meshtastic-Apple/Meshtastic/Views/Helpers/RXTXIndicatorView.swift
Copilot c388bf9b40 Add missing SwiftUI #Preview blocks across 65 views (#1649)
* Add SwiftUI previews for simple helper views

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Add previews for action buttons, ChannelForm, MetricsColumnDetail, and DeviceOnboarding

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Add previews for config views, log views, AppLog, Firmware, AppData, and UserConfig

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Add preview for PositionConfig

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/a2a43e8c-24fd-443a-8a98-13b678770edd

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Fix formatting bugs in #Preview blocks: restore missing .environmentObject/.environment modifiers and add proper tab indentation

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/7eeb7a54-7928-466f-8e39-b00d0012a09d

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Linting fixes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com>
2026-04-16 06:06:55 +00:00

130 lines
3.6 KiB
Swift

//
// RXTXIndicatorView.swift
// Meshtastic
//
// Created by jake on 8/5/25.
//
import Foundation
import SwiftUI
import OSLog
struct RXTXIndicatorWidget: View {
@EnvironmentObject var accessoryManager: AccessoryManager
@State private var isPopoverOpen = false
let fontSize: CGFloat = 7.0
var body: some View {
Button( action: {
if !isPopoverOpen && accessoryManager.isConnected {
Task {
// TODO: replace with a heartbeat when the heartbeat works
try await Task.sleep(for: .seconds(0.5)) // little delay for user affordance
if accessoryManager.checkIsVersionSupported(forVersion: "2.7.4") {
Logger.transport.debug("[RXTXIndicator] sending heartbeat (2.7.4+)")
try await accessoryManager.sendHeartbeat()
} else {
Logger.transport.debug("[RXTXIndicator] sending metadata request (pre 2.7.4 does not support heartbeat nonce)")
_ = try await accessoryManager.requestDeviceMetadata()
}
}
}
#if targetEnvironment(macCatalyst)
if #available(macOS 26.0, *) {
// Don't show popover that crashes on mac 26
} else {
self.isPopoverOpen.toggle()
}
#else
self.isPopoverOpen.toggle()
#endif
}) {
VStack(spacing: 3.0) {
HStack(spacing: 2.0) {
Image(systemName: "arrow.up")
.font(.system(size: fontSize))
LEDIndicator(flash: $accessoryManager.packetsSent, color: .green)
}.frame(maxHeight: fontSize)
HStack(spacing: 2.0) {
Image(systemName: "arrow.down")
.font(.system(size: fontSize))
LEDIndicator(flash: $accessoryManager.packetsReceived, color: .red)
}.frame(maxHeight: fontSize)
}
.contentShape(Rectangle()) // Make sure the whole thing is tappable
.popover(isPresented: self.$isPopoverOpen,
attachmentAnchor: .rect(.bounds),
arrowEdge: .top) {
Button(action: {
self.isPopoverOpen = false
}) {
VStack(spacing: 0.5) {
Text("Packet Count")
.font(.caption)
.bold()
.padding(2.0)
Divider()
VStack(alignment: .leading) {
HStack(spacing: 3.0) {
HStack(spacing: 2.0) {
LEDIndicator(flash: $accessoryManager.packetsSent, color: .green)
.frame(maxHeight: fontSize)
Image(systemName: "arrow.up")
.font(.system(size: fontSize))
}
Text("To Radio (TX): \(accessoryManager.packetsSent)")
.font(.caption2)
Spacer()
}
HStack(spacing: 3.0) {
HStack(spacing: 2.0) {
LEDIndicator(flash: $accessoryManager.packetsReceived, color: .red)
.frame(maxHeight: fontSize)
Image(systemName: "arrow.down")
.font(.system(size: fontSize))
}
Text("From Radio (RX): \(accessoryManager.packetsReceived)")
.font(.caption2)
Spacer()
}
}.padding(2.0)
}.padding(10)
.contentShape(Rectangle()) // Make sure the whole thing is tappable
}.buttonStyle(.plain)
.presentationCompactAdaptation(.popover)
}
}.buttonStyle(.borderless)
}
}
struct LEDIndicator: View {
@Environment(\.colorScheme) var colorScheme
@Binding var flash: Int
let color: Color
@State private var brightness: Double = 0.0
var body: some View {
Circle()
.foregroundColor(color.opacity(brightness))
.overlay(
Circle()
.stroke(colorScheme == .light ? Color.black : Color.white, lineWidth: 0.5)
).onChange(of: flash) { _, _ in
brightness = 1.0
withAnimation(.easeOut(duration: 0.3)) {
brightness = 0.0
}
}
}
}
#Preview {
HStack(spacing: 12) {
LEDIndicator(flash: .constant(1), color: .green)
.frame(width: 10, height: 10)
LEDIndicator(flash: .constant(0), color: .red)
.frame(width: 10, height: 10)
}
}