refactor: Move common UI components to core:ui and add screenshot tests

Relocate messaging and node-related UI components from feature modules to the shared core:ui module to improve reusability across the project. Enable the screenshot testing plugin and add comprehensive baseline tests for shared components.

- **feature/messaging**: Move `MessageItem`, `MessageActions`, `MessageBubble`, and `Reaction` components to core:ui.
- **feature/node**: Move `NodeItem`, `NodeStatusIcons`, `InfoCard`, and `CooldownIconButton` to core:ui.
- **core/ui**: Add new screenshot tests for `MainAppBar`, `NodeItem`, `MessageItem`, and various common UI components.
- **build.gradle.kts**: Configure the screenshot testing plugin and dependencies for node and messaging feature modules.
- Update imports and references across the codebase to reflect relocated components.
This commit is contained in:
James Rich 2026-03-02 10:32:55 -06:00
parent a19c463b37
commit 85847e1144
50 changed files with 332 additions and 138 deletions

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
package org.meshtastic.core.ui.component
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
@ -36,8 +36,8 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
internal const val COOL_DOWN_TIME_MS = 30000L
internal const val REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS = 180000L // 3 minutes
const val COOL_DOWN_TIME_MS = 30000L
const val REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS = 180000L // 3 minutes
@Composable
fun CooldownIconButton(

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@ -33,15 +33,13 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.close
import org.meshtastic.core.resources.relays
import org.meshtastic.core.resources.resend
import org.meshtastic.core.ui.component.MeshtasticDialog
@Suppress("UnusedParameter")
@Composable
fun DeliveryInfo(
title: StringResource,
resendOption: Boolean,
text: StringResource? = null,
relayNodeName: String? = null,
@Suppress("UNUSED_PARAMETER") relayNodeName: String? = null,
relays: Int = 0,
onConfirm: (() -> Unit) = {},
onDismiss: () -> Unit = {},

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
package org.meshtastic.core.ui.component
import android.content.ClipData
import androidx.compose.foundation.ExperimentalFoundationApi
@ -120,6 +120,6 @@ fun InfoCard(
}
@Composable
internal fun DrawableInfoCard(iconRes: DrawableResource, text: String, value: String, rotateIcon: Float = 0f) {
fun DrawableInfoCard(iconRes: DrawableResource, text: String, value: String, rotateIcon: Float = 0f) {
InfoCard(iconRes = iconRes, text = text, value = value, rotateIcon = rotateIcon)
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.component
package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
@ -49,7 +49,7 @@ import org.meshtastic.core.resources.reply
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
@Composable
internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
var showEmojiPickerDialog by remember { mutableStateOf(false) }
if (showEmojiPickerDialog) {
EmojiPickerDialog(
@ -66,7 +66,7 @@ internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
}
@Composable
private fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
onClick = onClick,
content = {
Icon(imageVector = Icons.AutoMirrored.Filled.Reply, contentDescription = stringResource(Res.string.reply))
@ -74,7 +74,7 @@ private fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
)
@Composable
internal fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, fromLocal: Boolean) =
fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, fromLocal: Boolean) =
AnimatedVisibility(visible = fromLocal) {
IconButton(onClick = onStatusClick) {
Crossfade(targetState = status, label = "MessageStatusIcon") { currentStatus ->
@ -98,7 +98,7 @@ internal fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: Message
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun MessageActions(
fun MessageActions(
modifier: Modifier = Modifier,
isLocal: Boolean = false,
status: MessageStatus?,

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.component
package org.meshtastic.core.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.component
package org.meshtastic.core.ui.component
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.RoundedCornerShape
@ -29,7 +29,7 @@ import androidx.compose.ui.unit.dp
* @param hasSamePrev Whether the previous message in the list is from the same sender.
* @param hasSameNext Whether the next message in the list is from the same sender.
*/
internal fun getMessageBubbleShape(
fun getMessageBubbleShape(
cornerRadius: Dp,
isSender: Boolean,
hasSamePrev: Boolean = false,

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.component
package org.meshtastic.core.ui.component
import android.content.ClipData
import androidx.compose.foundation.background
@ -71,11 +71,6 @@ import org.meshtastic.core.resources.filter_message_label
import org.meshtastic.core.resources.message_delivery_status
import org.meshtastic.core.resources.reply
import org.meshtastic.core.resources.sample_message
import org.meshtastic.core.ui.component.AutoLinkText
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.component.TransportIcon
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.emoji.EmojiPicker
import org.meshtastic.core.ui.icon.Acknowledged
@ -92,7 +87,7 @@ import org.meshtastic.core.ui.theme.MessageItemColors
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
internal fun MessageItem(
fun MessageItem(
modifier: Modifier = Modifier,
node: Node,
ourNode: Node,
@ -449,7 +444,7 @@ private fun OriginalMessageSnippet(
@PreviewLightDark
@Composable
private fun MessageItemPreview() {
fun MessageItemPreview() {
val sent =
Message(
text = stringResource(Res.string.sample_message),

View file

@ -16,7 +16,7 @@
*/
@file:Suppress("MagicNumber")
package org.meshtastic.feature.node.component
package org.meshtastic.core.ui.component
import android.content.res.Configuration
import androidx.compose.foundation.combinedClickable
@ -64,6 +64,7 @@ import org.meshtastic.core.resources.elevation_suffix
import org.meshtastic.core.resources.signal_quality
import org.meshtastic.core.resources.unknown_username
import org.meshtastic.core.resources.voltage
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.component.AirQualityInfo
import org.meshtastic.core.ui.component.ChannelInfo
import org.meshtastic.core.ui.component.DistanceInfo
@ -117,7 +118,7 @@ fun NodeItem(
val isFavorite = remember(thatNode) { thatNode.isFavorite }
val isMuted = remember(thatNode) { thatNode.isMuted }
val isIgnored = thatNode.isIgnored
val originalLongName = (thatNode.user.long_name ?: "").ifEmpty { stringResource(Res.string.unknown_username) }
val originalLongName = thatNode.user.long_name.ifEmpty { stringResource(Res.string.unknown_username) }
val isThisNode = remember(thatNode) { thisNode?.num == thatNode.num }
val system =
@ -318,7 +319,7 @@ private fun gatherSensors(node: Node, tempInFahrenheit: Boolean, contentColor: C
val env = node.environmentMetrics
val pax = node.paxcounter
if ((pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0) {
if (pax.ble != 0 || pax.wifi != 0) {
items.add { PaxcountInfo(pax = "B:${pax.ble ?: 0} W:${pax.wifi ?: 0}", contentColor = contentColor) }
}
if ((env.temperature ?: 0f) != 0f) {

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -182,7 +182,7 @@ private fun StatusBadge(
tint: Color = LocalContentColor.current,
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = { PlainTooltip { Text(stringResource(tooltipText)) } },
state = rememberTooltipState(),
) {

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.component
package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
@ -72,14 +72,10 @@ import org.meshtastic.core.resources.message_status_enroute
import org.meshtastic.core.resources.message_status_queued
import org.meshtastic.core.resources.react
import org.meshtastic.core.resources.you
import org.meshtastic.core.ui.component.BottomSheetDialog
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.messaging.DeliveryInfo
import org.meshtastic.proto.User
@Composable
@ -137,7 +133,7 @@ private fun ReactionItem(
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun ReactionRow(
fun ReactionRow(
modifier: Modifier = Modifier,
reactions: List<Reaction> = emptyList(),
myId: String? = null,
@ -193,7 +189,7 @@ private fun AddReactionButton(modifier: Modifier = Modifier, onSendReaction: (St
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
internal fun ReactionDialog(
fun ReactionDialog(
reactions: List<Reaction>,
onDismiss: () -> Unit = {},
myId: String? = null,
@ -334,7 +330,7 @@ private fun ReactionItemPreview() {
@Preview
@Composable
private fun ReactionRowPreview() {
fun ReactionRowPreview() {
AppTheme {
ReactionRow(
reactions =

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest
import org.meshtastic.core.model.Channel
import org.meshtastic.core.ui.component.AdaptiveTwoPane
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.preview.previewNode
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.proto.ChannelSet
class AppScreenshotTest {
@PreviewTest
@Preview(showBackground = true)
@Composable
fun MainAppBarTest() {
AppTheme {
MainAppBar(
title = "Meshtastic",
subtitle = "Connected to Node",
ourNode = previewNode,
showNodeChip = true,
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onClickChip = {}
)
}
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun ScannedQrCodeDialogTest() {
AppTheme {
ScannedQrCodeDialog(
channels = ChannelSet(
settings = listOf(Channel.default.settings),
lora_config = Channel.default.loraConfig
),
incoming = ChannelSet(
settings = listOf(Channel.default.settings),
lora_config = Channel.default.loraConfig
),
onDismiss = {},
onConfirm = {},
)
}
}
@PreviewTest
@Preview(showBackground = true, widthDp = 800)
@Composable
fun AdaptiveTwoPaneExpandedTest() {
AppTheme {
AdaptiveTwoPane(
first = { Text("Left Pane") },
second = { Text("Right Pane") }
)
}
}
@PreviewTest
@Preview(showBackground = true, widthDp = 400)
@Composable
fun AdaptiveTwoPaneCompactTest() {
AppTheme {
AdaptiveTwoPane(
first = { Text("Top Pane") },
second = { Text("Bottom Pane") }
)
}
}
}

View file

@ -12,11 +12,14 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* along with this program. Of not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
@ -29,8 +32,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.android.tools.screenshot.PreviewTest
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Channel
import org.meshtastic.core.ui.component.AutoLinkText
import org.meshtastic.core.ui.component.BatteryInfoPreviewParameterProvider
import org.meshtastic.core.ui.component.ChannelInfo
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.component.DistanceInfo
import org.meshtastic.core.ui.component.ElevationInfo
import org.meshtastic.core.ui.component.HopsInfo
@ -40,13 +46,18 @@ import org.meshtastic.core.ui.component.IndoorAirQuality
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.component.QrDialog
import org.meshtastic.core.ui.component.SatelliteCountInfo
import org.meshtastic.core.ui.component.SecurityIcon
import org.meshtastic.core.ui.component.SecurityState
import org.meshtastic.core.ui.component.SignalInfo
import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.component.TransportIcon
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.Config
import org.meshtastic.proto.MeshPacket
class ComponentScreenshotTest {
@ -170,4 +181,69 @@ class ComponentScreenshotTest {
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Pill)
}
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun AutoLinkTextTest() {
AppTheme {
AutoLinkText("Check out https://meshtastic.org for more info!")
}
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun SecurityIconTest() {
AppTheme {
Column {
SecurityState.entries.forEach { state ->
SecurityIcon(securityState = state)
}
}
}
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun TransportIconTest() {
AppTheme {
Column {
TransportIcon(transport = MeshPacket.TransportMechanism.TRANSPORT_INTERNAL.value, viaMqtt = false)
TransportIcon(transport = MeshPacket.TransportMechanism.TRANSPORT_MQTT.value, viaMqtt = true)
TransportIcon(transport = MeshPacket.TransportMechanism.TRANSPORT_MULTICAST_UDP.value, viaMqtt = false)
}
}
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun ChannelSelectionTest() {
AppTheme {
ChannelSelection(
index = 0,
title = "LongFast",
enabled = true,
isSelected = true,
onSelected = {},
channel = Channel.default
)
}
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun QrDialogTest() {
AppTheme {
QrDialog(
title = "Share Contact",
uri = Uri.parse("https://meshtastic.org/u/dummy"),
qrCode = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888),
onDismiss = {}
)
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest
import org.meshtastic.core.ui.component.MessageItemPreview
import org.meshtastic.core.ui.component.ReactionRowPreview
class MessageItemScreenshotTest {
@PreviewTest
@Preview(showBackground = true)
@Composable
fun MessageItemTest() {
MessageItemPreview()
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun ReactionRowTest() {
ReactionRowPreview()
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.android.tools.screenshot.PreviewTest
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.ui.component.NodeInfoPreview
import org.meshtastic.core.ui.component.NodeInfoSignalPreview
import org.meshtastic.core.ui.component.NodeInfoSimplePreview
import org.meshtastic.core.ui.component.NodeInfoStatusPreview
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
class NodeItemScreenshotTest {
@PreviewTest
@Preview(showBackground = true)
@Composable
fun NodeInfoSimpleTest() {
NodeInfoSimplePreview()
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun NodeInfoStatusTest() {
NodeInfoStatusPreview()
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun NodeInfoSignalTest() {
NodeInfoSignalPreview()
}
@PreviewTest
@Preview(showBackground = true)
@Composable
fun NodeInfoTest(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
NodeInfoPreview(thatNode = node)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -20,9 +20,15 @@ plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.compose)
alias(libs.plugins.meshtastic.hilt)
alias(libs.plugins.screenshot)
}
configure<LibraryExtension> { namespace = "org.meshtastic.feature.messaging" }
configure<LibraryExtension> {
namespace = "org.meshtastic.feature.messaging"
experimentalProperties["android.experimental.enableScreenshotTest"] = true
testOptions { unitTests { isIncludeAndroidResources = true } }
}
dependencies {
implementation(projects.core.analytics)
@ -68,4 +74,10 @@ dependencies {
testImplementation(libs.androidx.work.testing)
testImplementation(libs.androidx.test.core)
testImplementation(libs.robolectric)
screenshotTestImplementation(libs.screenshot.validation.api)
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
screenshotTestImplementation(libs.compose.multiplatform.runtime)
screenshotTestImplementation(libs.compose.multiplatform.resources)
screenshotTestImplementation(projects.core.resources)
}

View file

@ -68,8 +68,9 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.new_messages_below
import org.meshtastic.feature.messaging.component.MessageItem
import org.meshtastic.feature.messaging.component.ReactionDialog
import org.meshtastic.core.ui.component.DeliveryInfo
import org.meshtastic.core.ui.component.MessageItem
import org.meshtastic.core.ui.component.ReactionDialog
internal data class MessageListHandlers(
val onUnreadChanged: (Long, Long) -> Unit,

View file

@ -21,6 +21,7 @@ plugins {
alias(libs.plugins.meshtastic.android.library.flavors)
alias(libs.plugins.meshtastic.android.library.compose)
alias(libs.plugins.meshtastic.hilt)
alias(libs.plugins.screenshot)
}
configure<LibraryExtension> {
@ -29,6 +30,8 @@ configure<LibraryExtension> {
defaultConfig { manifestPlaceholders["MAPS_API_KEY"] = "DEBUG_KEY" }
testOptions { unitTests { isIncludeAndroidResources = true } }
experimentalProperties["android.experimental.enableScreenshotTest"] = true
}
dependencies {
@ -70,4 +73,10 @@ dependencies {
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.robolectric)
debugImplementation(libs.androidx.compose.ui.test.manifest)
screenshotTestImplementation(libs.screenshot.validation.api)
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
screenshotTestImplementation(libs.compose.multiplatform.runtime)
screenshotTestImplementation(libs.compose.multiplatform.resources)
screenshotTestImplementation(projects.core.resources)
}

View file

@ -61,6 +61,8 @@ import org.meshtastic.core.resources.uv_lux
import org.meshtastic.core.resources.voltage
import org.meshtastic.core.resources.weight
import org.meshtastic.core.resources.wind
import org.meshtastic.core.ui.component.DrawableInfoCard
import org.meshtastic.core.ui.component.InfoCard
import org.meshtastic.feature.node.model.DrawableMetricInfo
import org.meshtastic.feature.node.model.VectorMetricInfo
import org.meshtastic.proto.Config

View file

@ -1,93 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Preview(name = "Wind Dir -359°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirectionn359() {
PreviewWindDirectionItem(-359f)
}
@Preview(name = "Wind Dir 0°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection0() {
PreviewWindDirectionItem(0f)
}
@Preview(name = "Wind Dir 45°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection45() {
PreviewWindDirectionItem(45f)
}
@Preview(name = "Wind Dir 90°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection90() {
PreviewWindDirectionItem(90f)
}
@Preview(name = "Wind Dir 180°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection180() {
PreviewWindDirectionItem(180f)
}
@Preview(name = "Wind Dir 225°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection225() {
PreviewWindDirectionItem(225f)
}
@Preview(name = "Wind Dir 270°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection270() {
PreviewWindDirectionItem(270f)
}
@Preview(name = "Wind Dir 315°")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirection315() {
PreviewWindDirectionItem(315f)
}
@Preview(name = "Wind Dir -45")
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirectionN45() {
PreviewWindDirectionItem(-45f)
}
@Suppress("detekt:MagicNumber")
@Composable
private fun PreviewWindDirectionItem(windDirection: Float, windSpeed: String = "5 m/s") {
val normalizedBearing = (windDirection + 180) % 360
InfoCard(icon = Icons.Outlined.Navigation, text = "Wind", value = windSpeed, rotateIcon = normalizedBearing)
}

View file

@ -31,6 +31,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.channel_1
import org.meshtastic.core.resources.channel_2
import org.meshtastic.core.resources.channel_3
import org.meshtastic.core.ui.component.InfoCard
import org.meshtastic.feature.node.model.VectorMetricInfo
/**

View file

@ -55,6 +55,9 @@ import org.meshtastic.core.resources.request_air_quality_metrics
import org.meshtastic.core.resources.request_telemetry
import org.meshtastic.core.resources.telemetry
import org.meshtastic.core.resources.userinfo
import org.meshtastic.core.ui.component.COOL_DOWN_TIME_MS
import org.meshtastic.core.ui.component.CooldownOutlinedIconButton
import org.meshtastic.core.ui.component.REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS
import org.meshtastic.core.ui.icon.AirQuality
import org.meshtastic.core.ui.icon.LineAxis
import org.meshtastic.core.ui.icon.MeshtasticIcons

View file

@ -88,7 +88,7 @@ import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.node.component.NodeFilterTextField
import org.meshtastic.feature.node.component.NodeItem
import org.meshtastic.core.ui.component.NodeItem
import org.meshtastic.proto.SharedContact
@OptIn(ExperimentalMaterial3ExpressiveApi::class)

View file

@ -55,7 +55,7 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
import org.meshtastic.core.ui.util.annotateNeighborInfo
import org.meshtastic.feature.node.component.CooldownIconButton
import org.meshtastic.core.ui.component.CooldownIconButton
import org.meshtastic.feature.node.detail.NodeRequestEffect
@OptIn(ExperimentalFoundationApi::class)

View file

@ -73,7 +73,7 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
import org.meshtastic.core.ui.util.annotateTraceroute
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.feature.node.component.CooldownIconButton
import org.meshtastic.core.ui.component.CooldownIconButton
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.proto.RouteDiscovery