feat: add waypoint expiration picker (#2051)

This commit is contained in:
Benjamin Faershtein 2025-06-07 13:50:30 -07:00 committed by GitHub
parent 89462be97a
commit 5b6c8483e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 170 additions and 9 deletions

View file

@ -370,6 +370,8 @@ fun MapView(
model.getUser(id).longName
}
@Composable
@Suppress("MagicNumber")
fun MapView.onWaypointChanged(waypoints: Collection<Packet>): List<MarkerWithLabel> {
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
return waypoints.mapNotNull { waypoint ->
@ -378,10 +380,29 @@ fun MapView(
val time = dateFormat.format(waypoint.received_time)
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 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 ""}"
}
MarkerWithLabel(this, label, emoji).apply {
id = "${pt.id}"
title = "${pt.name} (${getUsername(waypoint.data.from)}$lock)"
snippet = "[$time] " + pt.description
snippet = "[$time] ${pt.description} " + stringResource(R.string.expires) + ": $expireTimeStr"
position = GeoPoint(pt.latitudeI * 1e-7, pt.longitudeI * 1e-7)
setVisible(false)
setOnLongClickListener {
@ -642,7 +663,8 @@ fun MapView(
model.sendWaypoint(
waypoint.copy {
if (id == 0) id = model.generatePacketId() ?: return@EditWaypointDialog
expire = Int.MAX_VALUE // TODO add expire picker
if (name == "") name = "Dropped Pin"
if (expire == 0) expire = Int.MAX_VALUE
lockedTo = if (waypoint.lockedTo != 0) model.myNodeNum ?: 0 else 0
}
)

View file

@ -17,6 +17,9 @@
package com.geeksville.mesh.ui.map.components
import android.app.DatePickerDialog
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -33,6 +36,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@ -49,6 +53,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
@ -64,6 +69,9 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.waypoint
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@ -77,9 +85,45 @@ internal fun EditWaypointDialog(
) {
var waypointInput by remember { mutableStateOf(waypoint) }
val title = if (waypoint.id == 0) R.string.waypoint_new else R.string.waypoint_edit
@Suppress("MagicNumber")
val emoji = if (waypointInput.icon == 0) 128205 else waypointInput.icon
var showEmojiPickerView by remember { mutableStateOf(false) }
// State to hold selected date and time
var selectedDate by remember { mutableStateOf("") }
var selectedTime by remember { mutableStateOf("") }
var epochTime by remember { mutableStateOf<Long?>(null) }
// Get current context for dialogs
val context = LocalContext.current
val calendar = Calendar.getInstance()
val currentTime = System.currentTimeMillis()
calendar.timeInMillis = currentTime
@Suppress("MagicNumber")
calendar.add(Calendar.HOUR_OF_DAY, 8)
// Current time for initializing pickers
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val day = calendar.get(Calendar.DAY_OF_MONTH)
val hour = calendar.get(Calendar.HOUR_OF_DAY)
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)
}
// 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)
}
if (!showEmojiPickerView) {
AlertDialog(
onDismissRequest = onDismissRequest,
@ -99,7 +143,7 @@ internal fun EditWaypointDialog(
EditTextPreference(
title = stringResource(R.string.name),
value = waypointInput.name,
maxSize = 29, // name max_size:30
maxSize = 29,
enabled = true,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
@ -123,7 +167,7 @@ internal fun EditWaypointDialog(
EditTextPreference(
title = stringResource(R.string.description),
value = waypointInput.description,
maxSize = 99, // description max_size:100
maxSize = 99,
enabled = true,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
@ -149,13 +193,103 @@ internal fun EditWaypointDialog(
.wrapContentWidth(Alignment.End),
checked = waypointInput.lockedTo != 0,
onCheckedChange = {
waypointInput =
waypointInput.copy { lockedTo = if (it) 1 else 0 }
waypointInput = waypointInput.copy { lockedTo = if (it) 1 else 0 }
}
)
}
}
},
val datePickerDialog = 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)
}
}, year, month, day
)
val timePickerDialog = 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
selectedTime = timeFormat.format(calendar.time)
@Suppress("MagicNumber")
waypointInput = waypointInput.copy { expire = (epochTime!! / 1000).toInt() }
}, hour, minute, is24Hour
)
Row(
modifier = Modifier
.fillMaxWidth()
.size(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
imageVector = Icons.Default.CalendarMonth,
contentDescription = stringResource(R.string.expires),
)
Text(stringResource(R.string.expires))
Switch(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
checked = waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0,
onCheckedChange = { isChecked ->
waypointInput = waypointInput.copy {
expire = if (isChecked) {
@Suppress("MagicNumber")
calendar.timeInMillis / 1000
} else {
Int.MAX_VALUE
}.toInt()
}
if (isChecked) {
selectedDate = dateFormat.format(calendar.time)
selectedTime = timeFormat.format(calendar.time)
} else {
selectedDate = ""
selectedTime = ""
}
}
)
}
if (waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { datePickerDialog.show() }) {
Text(stringResource(R.string.date))
}
Text(
modifier = Modifier.padding(top = 4.dp),
text = "$selectedDate",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { timePickerDialog.show() }) {
Text(stringResource(R.string.time))
}
Text(
modifier = Modifier.padding(top = 4.dp),
text = "$selectedTime",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
}
}
}
} },
confirmButton = {
FlowRow(
modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 16.dp),
@ -176,7 +310,7 @@ internal fun EditWaypointDialog(
Button(
modifier = modifier.weight(1f),
onClick = { onSendClicked(waypointInput) },
enabled = waypointInput.name.isNotEmpty(),
enabled = true,
) { Text(stringResource(R.string.send)) }
}
},
@ -191,6 +325,7 @@ internal fun EditWaypointDialog(
@Preview(showBackground = true)
@Composable
@Suppress("MagicNumber")
private fun EditWaypointFormPreview() {
AppTheme {
EditWaypointDialog(
@ -199,6 +334,7 @@ private fun EditWaypointFormPreview() {
name = "Test 123"
description = "This is only a test"
icon = 128169
expire = (System.currentTimeMillis() / 1000 + 8 * 3600).toInt()
},
onSendClicked = { },
onDeleteClicked = { },