mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(datetime): Standardize date/time formatting with DateUtils (#4164)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
1422217303
commit
02cf1f1034
15 changed files with 131 additions and 136 deletions
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -251,6 +251,10 @@
|
|||
<string name="debug_filter_add">Add filter</string>
|
||||
<string name="debug_filter_included">Filter included</string>
|
||||
<string name="debug_filter_clear">Clear all filters</string>
|
||||
<string name="debug_filter_add_custom">Add custom filter</string>
|
||||
<string name="debug_filter_preset_title">Preset Filters</string>
|
||||
<string name="debug_store_logs_title">Store mesh logs</string>
|
||||
<string name="debug_store_logs_summary">Disable to skip writing mesh logs to disk</string>
|
||||
<string name="debug_clear">Clear Logs</string>
|
||||
<string name="match_any">Match Any | All</string>
|
||||
<string name="match_all">Match All | Any</string>
|
||||
|
|
@ -1082,4 +1086,5 @@
|
|||
<string name="compass_uncertainty">Estimated area: \u00b1%1$s (\u00b1%2$s)</string>
|
||||
<string name="compass_uncertainty_unknown">Estimated area: unknown accuracy</string>
|
||||
<string name="mark_as_read">Mark as read</string>
|
||||
<string name="now">now</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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.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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Packet>, selectedWaypointId: Int?): List<MarkerWithLabel> {
|
||||
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}"
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<MeshProtos.Position>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<TracerouteDialog?>(null) }
|
||||
var errorMessageRes by remember { mutableStateOf<StringResource?>(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") }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}) {
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue