diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt index 4886f333f..05ff8018a 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.model.util import android.widget.EditText @@ -69,15 +68,14 @@ fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } @Suppress("MagicNumber") fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String { - val currentTime = (currentTimeMillis / 1000).toInt() - val diffMin = (currentTime - lastSeenUnix) / 60 - return when { - diffMin < 1 -> "now" - diffMin < 60 -> diffMin.toString() + " min" - diffMin < 2880 -> (diffMin / 60).toString() + " h" - diffMin < 1440000 -> (diffMin / (60 * 24)).toString() + " d" - else -> "?" - } + val timeInMillis = lastSeenUnix * 1000L + return android.text.format.DateUtils.getRelativeTimeSpanString( + timeInMillis, + currentTimeMillis, + android.text.format.DateUtils.MINUTE_IN_MILLIS, + android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE, + ) + .toString() } private const val MPS_TO_KMPH = 3.6f diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index 82684dff5..af3f2b548 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -251,6 +251,10 @@ Add filter Filter included Clear all filters + Add custom filter + Preset Filters + Store mesh logs + Disable to skip writing mesh logs to disk Clear Logs Match Any | All Match All | Any @@ -1082,4 +1086,5 @@ Estimated area: \u00b1%1$s (\u00b1%2$s) Estimated area: unknown accuracy Mark as read + now diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt new file mode 100644 index 000000000..c76d09bbf --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt @@ -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 . + */ +package org.meshtastic.core.ui.util + +import android.text.format.DateUtils +import com.meshtastic.core.strings.getString +import org.meshtastic.core.strings.Res +import org.meshtastic.core.strings.now + +@Suppress("MagicNumber") +fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String { + val timeInMillis = lastSeenUnix * 1000L + val diff = currentTimeMillis - timeInMillis + + return if (diff < 60_000L) { + getString(Res.string.now) + } else { + DateUtils.getRelativeTimeSpanString( + timeInMillis, + currentTimeMillis, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ) + .toString() + } +} diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt index 8d080a5bd..71ab1b46a 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,10 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.ui.util +import android.text.format.DateUtils import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.unknown_age @@ -28,13 +29,12 @@ import org.meshtastic.proto.MeshProtos.MeshPacket import org.meshtastic.proto.MeshProtos.Position import org.meshtastic.proto.channel import org.meshtastic.proto.channelSettings -import java.text.DateFormat import kotlin.time.Duration.Companion.days private const val SECONDS_TO_MILLIS = 1000L @Composable -fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String { +fun MeshProtos.Position.formatPositionTime(): String { val currentTime = System.currentTimeMillis() val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds val isOlderThanSixMonths = time * SECONDS_TO_MILLIS < sixMonthsAgo @@ -42,7 +42,11 @@ fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String { if (isOlderThanSixMonths) { stringResource(Res.string.unknown_age) } else { - dateFormat.format(time * SECONDS_TO_MILLIS) + DateUtils.formatDateTime( + LocalContext.current, + time * SECONDS_TO_MILLIS, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL, + ) } return timeText } diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt index e87821579..895f60768 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt @@ -18,6 +18,7 @@ package org.meshtastic.feature.map import android.Manifest // Added for Accompanist import android.graphics.Paint +import android.text.format.DateUtils import androidx.appcompat.content.res.AppCompatResources import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -88,7 +89,6 @@ import org.meshtastic.core.common.hasGps import org.meshtastic.core.database.entity.Packet import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.model.util.formatAgo import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.calculating import org.meshtastic.core.strings.cancel @@ -123,6 +123,7 @@ import org.meshtastic.core.strings.you import org.meshtastic.core.ui.component.BasicListItem import org.meshtastic.core.ui.component.ListItem import org.meshtastic.core.ui.theme.TracerouteColors +import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer import org.meshtastic.feature.map.component.CacheLayout @@ -157,7 +158,6 @@ import org.osmdroid.views.overlay.Polyline import org.osmdroid.views.overlay.infowindow.InfoWindow import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay import java.io.File -import java.text.DateFormat import kotlin.math.abs import kotlin.math.asin import kotlin.math.atan2 @@ -514,34 +514,32 @@ fun MapView( @Suppress("MagicNumber") fun MapView.onWaypointChanged(waypoints: Collection, selectedWaypointId: Int?): List { - val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) return waypoints.mapNotNull { waypoint -> val pt = waypoint.data.waypoint ?: return@mapNotNull null if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState val lock = if (pt.lockedTo != 0) "\uD83D\uDD12" else "" - val time = dateFormat.format(waypoint.received_time) + val time = + DateUtils.formatDateTime( + context, + waypoint.received_time, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL, + ) val label = pt.name + " " + formatAgo((waypoint.received_time / 1000).toInt()) val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon)) - val timeLeft = pt.expire * 1000L - System.currentTimeMillis() + val now = System.currentTimeMillis() + val expireTimeMillis = pt.expire * 1000L val expireTimeStr = when { pt.expire == 0 || pt.expire == Int.MAX_VALUE -> "Never" - timeLeft <= 0 -> "Expired" - timeLeft < 60_000 -> "${timeLeft / 1000} seconds" - timeLeft < 3_600_000 -> "${timeLeft / 60_000} minute${if (timeLeft / 60_000 != 1L) "s" else ""}" - timeLeft < 86_400_000 -> { - val hours = (timeLeft / 3_600_000).toInt() - val minutes = ((timeLeft % 3_600_000) / 60_000).toInt() - if (minutes >= 30) { - "${hours + 1} hour${if (hours + 1 != 1) "s" else ""}" - } else if (minutes > 0) { - "$hours hour${if (hours != 1) "s" else ""}, $minutes minute${if (minutes != 1) "s" else ""}" - } else { - "$hours hour${if (hours != 1) "s" else ""}" - } - } - - else -> "${timeLeft / 86_400_000} day${if (timeLeft / 86_400_000 != 1L) "s" else ""}" + expireTimeMillis <= now -> "Expired" + else -> + DateUtils.getRelativeTimeSpanString( + expireTimeMillis, + now, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ) + .toString() } MarkerWithLabel(this, label, emoji).apply { id = "${pt.id}" diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt index 611781d81..63c9d4c23 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.map.component import android.app.DatePickerDialog @@ -82,9 +81,7 @@ import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.proto.MeshProtos.Waypoint import org.meshtastic.proto.copy import org.meshtastic.proto.waypoint -import java.text.SimpleDateFormat import java.util.Calendar -import java.util.Locale @Suppress("LongMethod") @OptIn(ExperimentalLayoutApi::class) @@ -119,21 +116,10 @@ fun EditWaypointDialog( val minute = calendar.get(Calendar.MINUTE) // Determine locale-specific date format - val locale = Locale.getDefault() - val dateFormat = - if (locale.country == "US") { - SimpleDateFormat("MM/dd/yyyy", locale) - } else { - SimpleDateFormat("dd/MM/yyyy", locale) - } + val dateFormat = android.text.format.DateFormat.getDateFormat(context) // Check if 24-hour format is preferred val is24Hour = android.text.format.DateFormat.is24HourFormat(context) - val timeFormat = - if (is24Hour) { - SimpleDateFormat("HH:mm", locale) - } else { - SimpleDateFormat("hh:mm a", locale) - } + val timeFormat = android.text.format.DateFormat.getTimeFormat(context) // State to hold selected date and time var selectedDate by remember { mutableStateOf(dateFormat.format(calendar.time)) } @@ -205,12 +191,9 @@ fun EditWaypointDialog( DatePickerDialog( context, { _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int -> - selectedDate = "$selectedDay/${selectedMonth + 1}/$selectedYear" calendar.set(selectedYear, selectedMonth, selectedDay) epochTime = calendar.timeInMillis - if (epochTime != null) { - selectedDate = dateFormat.format(calendar.time) - } + selectedDate = dateFormat.format(calendar.time) }, year, month, @@ -221,8 +204,6 @@ fun EditWaypointDialog( android.app.TimePickerDialog( context, { _: TimePicker, selectedHour: Int, selectedMinute: Int -> - selectedTime = - String.format(Locale.getDefault(), "%02d:%02d", selectedHour, selectedMinute) calendar.set(Calendar.HOUR_OF_DAY, selectedHour) calendar.set(Calendar.MINUTE, selectedMinute) epochTime = calendar.timeInMillis diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt index 5494020ed..2c301302e 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt @@ -92,7 +92,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.model.Node -import org.meshtastic.core.model.util.formatAgo import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.mpsToKmph import org.meshtastic.core.model.util.mpsToMph @@ -109,6 +108,7 @@ import org.meshtastic.core.strings.timestamp import org.meshtastic.core.strings.track_point import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.theme.TracerouteColors +import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.formatPositionTime import org.meshtastic.feature.map.component.ClusterItemsListDialog import org.meshtastic.feature.map.component.CustomMapLayersSheet @@ -124,7 +124,6 @@ import org.meshtastic.proto.MeshProtos.Position import org.meshtastic.proto.MeshProtos.Waypoint import org.meshtastic.proto.copy import org.meshtastic.proto.waypoint -import java.text.DateFormat import kotlin.math.abs import kotlin.math.max @@ -487,9 +486,6 @@ fun MapView( ?.let { focusedNode -> sortedPositions.forEachIndexed { index, position -> val markerState = rememberUpdatedMarkerState(position = position.toLatLng()) - val dateFormat = remember { - DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - } val alpha = (index.toFloat() / (sortedPositions.size.toFloat() - 1)) val color = Color(focusedNode.colors.second).copy(alpha = alpha) if (index == sortedPositions.lastIndex) { @@ -501,11 +497,7 @@ fun MapView( snippet = formatAgo(position.time), zIndex = alpha, infoContent = { - PositionInfoWindowContent( - position = position, - dateFormat = dateFormat, - displayUnits = displayUnits, - ) + PositionInfoWindowContent(position = position, displayUnits = displayUnits) }, ) { Icon( @@ -759,11 +751,7 @@ fun Uri.getFileName(context: android.content.Context): String { @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @Suppress("LongMethod") -private fun PositionInfoWindowContent( - position: Position, - dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM), - displayUnits: DisplayUnits = DisplayUnits.METRIC, -) { +private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayUnits = DisplayUnits.METRIC) { @Composable fun PositionRow(label: String, value: String) { Row(modifier = Modifier.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) { @@ -796,7 +784,7 @@ private fun PositionInfoWindowContent( value = "%.0f°".format(position.groundTrack * HEADING_DEG), ) - PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime(dateFormat)) + PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime()) } } } diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt index d8a588f73..cde9825ab 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.map.component import android.app.DatePickerDialog @@ -77,9 +76,7 @@ import org.meshtastic.core.strings.waypoint_new import org.meshtastic.core.ui.emoji.EmojiPickerDialog import org.meshtastic.proto.MeshProtos.Waypoint import org.meshtastic.proto.copy -import java.text.SimpleDateFormat import java.util.Calendar -import java.util.Locale import java.util.TimeZone @OptIn(ExperimentalMaterial3Api::class) @@ -108,22 +105,8 @@ fun EditWaypointDialog( mutableStateOf(waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) } - val locale = Locale.getDefault() - val dateFormat = remember { - if (locale.country.equals("US", ignoreCase = true)) { - SimpleDateFormat("MM/dd/yyyy", locale) - } else { - SimpleDateFormat("dd/MM/yyyy", locale) - } - } - val timeFormat = remember { - val is24Hour = android.text.format.DateFormat.is24HourFormat(context) - if (is24Hour) { - SimpleDateFormat("HH:mm", locale) - } else { - SimpleDateFormat("hh:mm a", locale) - } - } + val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) } + val timeFormat = remember { android.text.format.DateFormat.getTimeFormat(context) } dateFormat.timeZone = TimeZone.getDefault() timeFormat.timeZone = TimeZone.getDefault() diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt index e46a9aac1..64c333cfd 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.node.component import androidx.compose.material3.MaterialTheme @@ -25,11 +24,11 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.PreviewLightDark import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.model.util.formatAgo import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.node_sort_last_heard import org.meshtastic.core.ui.R import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.util.formatAgo @Composable fun LastHeardInfo( diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt index edc0e5a0d..84a6084e2 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.node.component import android.content.ActivityNotFoundException @@ -37,7 +36,6 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.util.GPSFormat -import org.meshtastic.core.model.util.formatAgo import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.toString import org.meshtastic.core.strings.Res @@ -46,6 +44,7 @@ import org.meshtastic.core.strings.last_position_update import org.meshtastic.core.ui.component.BasicListItem import org.meshtastic.core.ui.component.icon import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.showToast import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits import java.net.URLEncoder diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index 9d1314465..1159f445d 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.database.model.Node -import org.meshtastic.core.model.util.formatAgo import org.meshtastic.core.model.util.formatUptime import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.details @@ -61,6 +60,7 @@ import org.meshtastic.core.strings.role import org.meshtastic.core.strings.short_name import org.meshtastic.core.strings.uptime import org.meshtastic.core.strings.user_id +import org.meshtastic.core.ui.util.formatAgo @Composable fun NodeDetailsSection(node: Node, modifier: Modifier = Modifier) { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt index 07086c27f..d1916722c 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.node.metrics import android.app.Activity @@ -49,7 +48,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -79,7 +77,6 @@ import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.formatPositionTime import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits import org.meshtastic.proto.MeshProtos -import java.text.DateFormat @Composable private fun RowScope.PositionText(text: String, weight: Float) { @@ -116,7 +113,7 @@ const val DEG_D = 1e-7 const val HEADING_DEG = 1e-5 @Composable -fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateFormat: DateFormat, system: DisplayUnits) { +fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, system: DisplayUnits) { Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, @@ -129,7 +126,7 @@ fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateForma PositionText("${position.groundSpeed} Km/h", WEIGHT_15) PositionText("%.0f°".format(position.groundTrack * HEADING_DEG), WEIGHT_15) } - PositionText(position.formatPositionTime(dateFormat), WEIGHT_40) + PositionText(position.formatPositionTime(), WEIGHT_40) } } @@ -233,10 +230,8 @@ private fun ColumnScope.PositionList( positions: List, displayUnits: DisplayUnits, ) { - val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) } - LazyColumn(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) { - items(positions) { position -> PositionItem(compactWidth, position, dateFormat, displayUnits) } + items(positions) { position -> PositionItem(compactWidth, position, displayUnits) } } } @@ -255,14 +250,7 @@ private val testPosition = @Preview(showBackground = true) @Composable private fun PositionItemPreview() { - AppTheme { - PositionItem( - compactWidth = false, - position = testPosition, - dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM), - system = DisplayUnits.METRIC, - ) - } + AppTheme { PositionItem(compactWidth = false, position = testPosition, system = DisplayUnits.METRIC) } } @PreviewScreenSizes diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt index a77af41fd..edd92dab3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,9 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.node.metrics +import android.text.format.DateUtils import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -88,7 +89,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusYellow import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC import org.meshtastic.proto.MeshProtos -import java.text.DateFormat private data class TracerouteDialog( val message: AnnotatedString, @@ -107,12 +107,12 @@ fun TracerouteLogScreen( onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> }, ) { val state by viewModel.state.collectAsStateWithLifecycle() - val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) } fun getUsername(nodeNum: Int): String = with(viewModel.getUser(nodeNum)) { "$longName ($shortName)" } var showDialog by remember { mutableStateOf(null) } var errorMessageRes by remember { mutableStateOf(null) } + val context = LocalContext.current TracerouteLogDialogs( dialog = showDialog, @@ -150,7 +150,12 @@ fun TracerouteLogScreen( } val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery } - val time = dateFormat.format(log.received_date) + val time = + DateUtils.formatDateTime( + context, + log.received_date, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL, + ) val (text, icon) = route.getTextAndIcon() var expanded by remember { mutableStateOf(false) } @@ -359,8 +364,11 @@ fun annotateTraceroute(inString: String?): AnnotatedString { @PreviewLightDark @Composable private fun TracerouteItemPreview() { - val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - AppTheme { - TracerouteItem(icon = Icons.Default.Group, text = "${dateFormat.format(System.currentTimeMillis())} - Direct") - } + val time = + DateUtils.formatDateTime( + LocalContext.current, + System.currentTimeMillis(), + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL, + ) + AppTheme { TracerouteItem(icon = Icons.Default.Group, text = "$time - Direct") } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt index 97ba58510..327d888b7 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt @@ -87,11 +87,14 @@ import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.debug_clear import org.meshtastic.core.strings.debug_clear_logs_confirm import org.meshtastic.core.strings.debug_decoded_payload +import org.meshtastic.core.strings.debug_default_search import org.meshtastic.core.strings.debug_export_failed import org.meshtastic.core.strings.debug_export_success import org.meshtastic.core.strings.debug_filters import org.meshtastic.core.strings.debug_logs_export import org.meshtastic.core.strings.debug_panel +import org.meshtastic.core.strings.debug_store_logs_summary +import org.meshtastic.core.strings.debug_store_logs_title import org.meshtastic.core.strings.log_retention_days import org.meshtastic.core.strings.log_retention_days_quantity import org.meshtastic.core.strings.log_retention_days_summary @@ -250,11 +253,11 @@ private fun DebugLogSettings(viewModel: DebugViewModel) { ) SwitchPreference( - title = "Store mesh logs", + title = stringResource(Res.string.debug_store_logs_title), enabled = true, checked = loggingEnabled, onCheckedChange = { viewModel.setLoggingEnabled(it) }, - summary = "Disable to skip writing mesh logs to disk", + summary = stringResource(Res.string.debug_store_logs_summary), ) } } @@ -691,7 +694,7 @@ private fun DebugScreenEmptyPreview() { value = "", onValueChange = {}, modifier = Modifier.weight(1f).padding(end = 8.dp), - placeholder = { Text("Search in logs...") }, + placeholder = { Text(stringResource(Res.string.debug_default_search)) }, singleLine = true, ) TextButton(onClick = {}) { diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt index c4c72cc62..98d8cf757 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.settings.debugging import androidx.compose.foundation.background @@ -59,8 +58,10 @@ import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.debug_active_filters import org.meshtastic.core.strings.debug_filter_add +import org.meshtastic.core.strings.debug_filter_add_custom import org.meshtastic.core.strings.debug_filter_clear import org.meshtastic.core.strings.debug_filter_included +import org.meshtastic.core.strings.debug_filter_preset_title import org.meshtastic.core.strings.debug_filters import org.meshtastic.core.strings.match_all import org.meshtastic.core.strings.match_any @@ -79,7 +80,7 @@ fun DebugCustomFilterInput( value = customFilterText, onValueChange = onCustomFilterTextChange, modifier = Modifier.weight(1f), - placeholder = { Text("Add custom filter") }, + placeholder = { Text(stringResource(Res.string.debug_filter_add_custom)) }, singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = @@ -125,7 +126,7 @@ internal fun DebugPresetFilters( } Column(modifier = modifier) { Text( - text = "Preset Filters", + text = stringResource(Res.string.debug_filter_preset_title), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(vertical = 4.dp), )