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.
|
|
@ -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(
|
||||
|
|
@ -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 = {},
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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),
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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(),
|
||||
) {
|
||||
|
|
@ -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 =
|
||||
|
|
@ -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") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 842 B |
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||