Initial release of GPS2Audio

This commit is contained in:
Marcel Mayer
2026-05-05 20:54:05 +02:00
commit b2aa1c167c
48 changed files with 11570 additions and 0 deletions
@@ -0,0 +1,772 @@
package de.waypointaudio.ui
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import java.util.Calendar
import java.util.Locale
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AudioFile
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.KeyboardType
import androidx.compose.ui.unit.dp
import de.waypointaudio.R
import de.waypointaudio.data.PlaybackMode
import de.waypointaudio.data.TourPlaybackDefaults
import de.waypointaudio.data.Waypoint
import java.text.SimpleDateFormat
import java.util.Date
// Display formats
private val DISPLAY_DATE_FORMAT = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.GERMAN)
private val DISPLAY_TIME_FORMAT = SimpleDateFormat("HH:mm", Locale.GERMAN)
/** Formats Long? (milliseconds) as "dd.MM.yyyy HH:mm" or empty. */
private fun Long?.toDisplayDateString(): String =
if (this == null) "" else DISPLAY_DATE_FORMAT.format(Date(this))
/** Formats Int? (day minute) as "HH:mm" or empty. */
private fun Int?.toDisplayTimeString(): String {
if (this == null) return ""
val h = this / 60
val m = this % 60
return "%02d:%02d".format(h, m)
}
/**
* Dialog for creating and editing a waypoint.
*
* @param waypoint Existing waypoint (null = create new)
* @param prefillLatLng Pre-filled GPS coordinates (lat, lon).
* Only used for new waypoints (waypoint == null).
* @param existingTours List of existing tour names for the dropdown.
* @param prefillTourName Tour to pre-select for new waypoints (e.g. currently active tab).
* @param tourDefaults Tour-wide playback defaults inherited by new waypoints.
* @param onConfirm Callback with the configured waypoint
* @param onDismiss Callback on cancel
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WaypointEditDialog(
waypoint: Waypoint?,
prefillLatLng: Pair<Double, Double>? = null,
existingTours: List<String> = emptyList(),
prefillTourName: String = Waypoint.DEFAULT_TOUR_NAME,
tourDefaults: TourPlaybackDefaults = TourPlaybackDefaults(),
onConfirm: (Waypoint) -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val isNew = waypoint == null
// Coordinate pre-fill: GPS position takes priority for new waypoints
val initialLat = when {
waypoint != null -> "%.7f".format(Locale.US, waypoint.latitude)
prefillLatLng != null -> "%.7f".format(Locale.US, prefillLatLng.first)
else -> ""
}
val initialLng = when {
waypoint != null -> "%.7f".format(Locale.US, waypoint.longitude)
prefillLatLng != null -> "%.7f".format(Locale.US, prefillLatLng.second)
else -> ""
}
// Basic field state
var name by remember { mutableStateOf(waypoint?.name ?: "") }
var latStr by remember { mutableStateOf(initialLat) }
var lngStr by remember { mutableStateOf(initialLng) }
var radiusStr by remember { mutableStateOf(waypoint?.radiusMeters?.toInt()?.toString() ?: "50") }
var soundUri by remember { mutableStateOf(waypoint?.soundUri ?: "") }
var soundName by remember { mutableStateOf(waypoint?.soundName ?: "") }
// Tour name: use existing waypoint's tour, or prefill for new, fallback to default
val initialTour = when {
waypoint != null -> waypoint.tourName.ifBlank { Waypoint.DEFAULT_TOUR_NAME }
prefillTourName.isNotBlank() -> prefillTourName
else -> Waypoint.DEFAULT_TOUR_NAME
}
var tourName by remember { mutableStateOf(initialTour) }
var tourMenuExpanded by remember { mutableStateOf(false) }
// Build the selectable tour list: existing tours + current value if not yet in list
val selectableTours = remember(existingTours, tourName) {
val base = if (existingTours.isEmpty()) listOf(Waypoint.DEFAULT_TOUR_NAME) else existingTours
if (tourName !in base) base + tourName else base
}
// Basic field validation errors
var nameError by remember { mutableStateOf(false) }
var latError by remember { mutableStateOf(false) }
var lngError by remember { mutableStateOf(false) }
var radiusError by remember { mutableStateOf(false) }
// --- Playback rules state inherit tour defaults for new waypoints ---
val defaultMode = if (waypoint == null) tourDefaults.playbackMode else waypoint.playbackMode
val defaultMaxCount = if (waypoint == null && tourDefaults.playbackMode == PlaybackMode.LIMITED_COUNT)
tourDefaults.maxPlayCount.toString() else waypoint?.maxPlayCount?.toString() ?: ""
var playbackMode by remember { mutableStateOf(defaultMode) }
var maxPlayCountStr by remember {
mutableStateOf(defaultMaxCount)
}
var maxPlayCountError by remember { mutableStateOf(false) }
var currentPlayCount by remember { mutableStateOf(waypoint?.playCount ?: 0) }
// Scheduler: stored as millis / minutes (nullable = not set)
var scheduleEnabled by remember { mutableStateOf(waypoint?.scheduleEnabled ?: false) }
var scheduleStartMillis by remember { mutableStateOf<Long?>(waypoint?.scheduleStartMillis) }
var scheduleEndMillis by remember { mutableStateOf<Long?>(waypoint?.scheduleEndMillis) }
var allowedStartMinutes by remember { mutableStateOf<Int?>(waypoint?.allowedStartMinutes) }
var allowedEndMinutes by remember { mutableStateOf<Int?>(waypoint?.allowedEndMinutes) }
// Schedule validation errors
var scheduleRangeError by remember { mutableStateOf(false) }
// GPS pre-fill on change
LaunchedEffect(prefillLatLng) {
if (isNew && prefillLatLng != null) {
latStr = "%.7f".format(Locale.US, prefillLatLng.first)
lngStr = "%.7f".format(Locale.US, prefillLatLng.second)
}
}
// Audio file picker
val audioPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
if (uri != null) {
runCatching {
context.contentResolver.takePersistableUriPermission(
uri,
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
soundUri = uri.toString()
soundName = uri.lastPathSegment?.substringAfterLast('/')?.substringAfterLast(':')
?: uri.toString().takeLast(30)
}
}
// Helper: open DatePickerDialog followed by TimePickerDialog, result via callback
fun showDateTimePicker(
initialMillis: Long?,
onResult: (Long) -> Unit
) {
val cal = Calendar.getInstance().also { c ->
if (initialMillis != null) c.timeInMillis = initialMillis
}
DatePickerDialog(
context,
{ _, year, month, day ->
// After date is picked, open time picker
TimePickerDialog(
context,
{ _, hour, minute ->
val result = Calendar.getInstance().apply {
set(Calendar.YEAR, year)
set(Calendar.MONTH, month)
set(Calendar.DAY_OF_MONTH, day)
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
onResult(result.timeInMillis)
},
cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE),
true // 24-hour format
).show()
},
cal.get(Calendar.YEAR),
cal.get(Calendar.MONTH),
cal.get(Calendar.DAY_OF_MONTH)
).show()
}
// Helper: open TimePickerDialog, result in minutes (0..1439)
fun showTimePicker(
initialMinutes: Int?,
onResult: (Int) -> Unit
) {
val h = (initialMinutes ?: 0) / 60
val m = (initialMinutes ?: 0) % 60
TimePickerDialog(
context,
{ _, hour, minute ->
onResult(hour * 60 + minute)
},
h, m,
true // 24-hour format
).show()
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
stringResource(if (isNew) R.string.add_waypoint else R.string.edit_waypoint)
)
},
text = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// GPS pre-fill notice
if (isNew && prefillLatLng != null) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
shape = MaterialTheme.shapes.small
) {
Text(
"📍 Koordinaten aus aktuellem GPS-Standort vorausgefüllt.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
// Tour-Vorgaben-Hinweis für neue Wegpunkte
if (isNew && tourDefaults.playbackMode != de.waypointaudio.data.PlaybackMode.EVERY_ENTRY) {
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = MaterialTheme.shapes.small
) {
Text(
text = stringResource(R.string.waypoint_inherits_tour_defaults),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
// Name
OutlinedTextField(
value = name,
onValueChange = { name = it; nameError = false },
label = { Text(stringResource(R.string.waypoint_name)) },
isError = nameError,
supportingText = if (nameError) {{ Text("Pflichtfeld") }} else null,
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
// ---------------------------------------------------------------
// Tour-Zuordnung
// ---------------------------------------------------------------
Text(
text = stringResource(R.string.tour_section_label),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
// Tour dropdown selectableTours als Auswahlhilfe,
// aber freie Texteingabe ist ebenfalls möglich
ExposedDropdownMenuBox(
expanded = tourMenuExpanded,
onExpandedChange = { tourMenuExpanded = it }
) {
OutlinedTextField(
value = tourName,
onValueChange = { tourName = it },
label = { Text(stringResource(R.string.tour_name_label)) },
placeholder = { Text(Waypoint.DEFAULT_TOUR_NAME) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = tourMenuExpanded)
},
singleLine = true,
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = tourMenuExpanded,
onDismissRequest = { tourMenuExpanded = false }
) {
selectableTours.forEach { t ->
DropdownMenuItem(
text = { Text(t) },
onClick = {
tourName = t
tourMenuExpanded = false
}
)
}
}
}
Text(
text = stringResource(R.string.tour_name_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f)
)
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
// Coordinates
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = latStr,
onValueChange = { latStr = it; latError = false },
label = { Text("Lat") },
isError = latError,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = lngStr,
onValueChange = { lngStr = it; lngError = false },
label = { Text("Lng") },
isError = lngError,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.weight(1f)
)
}
// Coordinates format hint
Text(
"Dezimalgrad, z. B. 48.137154 · 11.576124",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f)
)
// Radius
OutlinedTextField(
value = radiusStr,
onValueChange = { radiusStr = it; radiusError = false },
label = { Text(stringResource(R.string.waypoint_radius)) },
isError = radiusError,
supportingText = if (radiusError) {{ Text("Ganzzahl > 0") }} else null,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
// Audio file selection
OutlinedButton(
onClick = { audioPickerLauncher.launch(arrayOf("audio/*")) },
modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Filled.AudioFile,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.choose_sound))
}
if (soundName.isNotBlank()) {
Text(
text = soundName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
maxLines = 2
)
} else {
Text(
text = stringResource(R.string.no_sound_selected),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f)
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
// ---------------------------------------------------------------
// Playback rules
// ---------------------------------------------------------------
Text(
text = stringResource(R.string.playback_rules_section),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
// Mode selection via DropdownMenu
var modeMenuExpanded by remember { mutableStateOf(false) }
val modeLabel = when (playbackMode) {
PlaybackMode.EVERY_ENTRY -> stringResource(R.string.playback_mode_every_entry)
PlaybackMode.ONCE -> stringResource(R.string.playback_mode_once)
PlaybackMode.LIMITED_COUNT -> stringResource(R.string.playback_mode_limited)
}
ExposedDropdownMenuBox(
expanded = modeMenuExpanded,
onExpandedChange = { modeMenuExpanded = it }
) {
OutlinedTextField(
value = modeLabel,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.playback_mode_label)) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = modeMenuExpanded)
},
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = modeMenuExpanded,
onDismissRequest = { modeMenuExpanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.playback_mode_every_entry)) },
onClick = {
playbackMode = PlaybackMode.EVERY_ENTRY
modeMenuExpanded = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.playback_mode_once)) },
onClick = {
playbackMode = PlaybackMode.ONCE
modeMenuExpanded = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.playback_mode_limited)) },
onClick = {
playbackMode = PlaybackMode.LIMITED_COUNT
modeMenuExpanded = false
}
)
}
}
// Max count only visible for LIMITED_COUNT
if (playbackMode == PlaybackMode.LIMITED_COUNT) {
OutlinedTextField(
value = maxPlayCountStr,
onValueChange = { maxPlayCountStr = it; maxPlayCountError = false },
label = { Text(stringResource(R.string.playback_max_count_label)) },
placeholder = { Text(stringResource(R.string.playback_max_count_hint)) },
isError = maxPlayCountError,
supportingText = if (maxPlayCountError) {
{ Text(stringResource(R.string.playback_max_count_error)) }
} else null,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
// Counter display + reset (edit mode only)
if (!isNew) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.playback_count_info, currentPlayCount),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
TextButton(onClick = { currentPlayCount = 0 }) {
Text(
text = stringResource(R.string.playback_count_reset),
style = MaterialTheme.typography.bodySmall
)
}
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
// ---------------------------------------------------------------
// Scheduler
// ---------------------------------------------------------------
Text(
text = stringResource(R.string.schedule_section),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.schedule_enabled_label),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = scheduleEnabled,
onCheckedChange = { scheduleEnabled = it }
)
}
if (scheduleEnabled) {
// ---- Start date/time ----
Text(
text = stringResource(R.string.schedule_start_label),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = {
showDateTimePicker(scheduleStartMillis) { millis ->
scheduleStartMillis = millis
scheduleRangeError = false
}
},
modifier = Modifier.weight(1f)
) {
Icon(
Icons.Filled.CalendarMonth,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(6.dp))
Text(
text = if (scheduleStartMillis != null)
scheduleStartMillis.toDisplayDateString()
else
stringResource(R.string.schedule_pick_start),
maxLines = 1
)
}
if (scheduleStartMillis != null) {
TextButton(onClick = {
scheduleStartMillis = null
scheduleRangeError = false
}) {
Text(stringResource(R.string.schedule_clear_start))
}
}
}
// ---- End date/time ----
Text(
text = stringResource(R.string.schedule_end_label),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = {
showDateTimePicker(scheduleEndMillis) { millis ->
scheduleEndMillis = millis
scheduleRangeError = false
}
},
modifier = Modifier.weight(1f)
) {
Icon(
Icons.Filled.CalendarMonth,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(6.dp))
Text(
text = if (scheduleEndMillis != null)
scheduleEndMillis.toDisplayDateString()
else
stringResource(R.string.schedule_pick_end),
maxLines = 1
)
}
if (scheduleEndMillis != null) {
TextButton(onClick = {
scheduleEndMillis = null
scheduleRangeError = false
}) {
Text(stringResource(R.string.schedule_clear_end))
}
}
}
// Range validation error
if (scheduleRangeError) {
Text(
text = stringResource(R.string.schedule_end_before_start_error),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
// ---- Daily time window ----
Text(
text = stringResource(R.string.schedule_daily_label),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
// Daily start time
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = {
showTimePicker(allowedStartMinutes) { minutes ->
allowedStartMinutes = minutes
}
},
modifier = Modifier.weight(1f)
) {
Icon(
Icons.Filled.Schedule,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(6.dp))
Text(
text = if (allowedStartMinutes != null)
allowedStartMinutes.toDisplayTimeString()
else
stringResource(R.string.schedule_pick_daily_start),
maxLines = 1
)
}
if (allowedStartMinutes != null) {
TextButton(onClick = { allowedStartMinutes = null }) {
Text(stringResource(R.string.schedule_clear_window))
}
}
}
// Daily end time
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedButton(
onClick = {
showTimePicker(allowedEndMinutes) { minutes ->
allowedEndMinutes = minutes
}
},
modifier = Modifier.weight(1f)
) {
Icon(
Icons.Filled.Schedule,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(6.dp))
Text(
text = if (allowedEndMinutes != null)
allowedEndMinutes.toDisplayTimeString()
else
stringResource(R.string.schedule_pick_daily_end),
maxLines = 1
)
}
if (allowedEndMinutes != null) {
TextButton(onClick = { allowedEndMinutes = null }) {
Text(stringResource(R.string.schedule_clear_window))
}
}
}
// Info: daily window can cross midnight
Text(
text = stringResource(R.string.schedule_daily_midnight_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f)
)
}
}
},
confirmButton = {
TextButton(onClick = {
// --- Basic validation ---
nameError = name.isBlank()
val lat = latStr.replace(',', '.').toDoubleOrNull()
val lng = lngStr.replace(',', '.').toDoubleOrNull()
val radius = radiusStr.replace(',', '.').toFloatOrNull()
latError = lat == null || lat !in -90.0..90.0
lngError = lng == null || lng !in -180.0..180.0
radiusError = radius == null || radius <= 0f
// --- Tour name validation: fallback to default if blank ---
val safeTourName = tourName.trim().ifBlank { Waypoint.DEFAULT_TOUR_NAME }
// --- Playback rule validation ---
var maxCount: Int? = null
if (playbackMode == PlaybackMode.LIMITED_COUNT) {
maxCount = maxPlayCountStr.trim().toIntOrNull()
maxPlayCountError = (maxCount == null || maxCount < 1)
} else {
maxPlayCountError = false
}
// --- Schedule validation ---
scheduleRangeError = if (scheduleEnabled) {
scheduleStartMillis != null &&
scheduleEndMillis != null &&
scheduleEndMillis!! < scheduleStartMillis!!
} else false
val hasError = nameError || latError || lngError || radiusError ||
maxPlayCountError || scheduleRangeError
if (!hasError) {
onConfirm(
(waypoint ?: Waypoint()).copy(
name = name.trim(),
latitude = lat!!,
longitude = lng!!,
radiusMeters = radius!!,
soundUri = soundUri,
soundName = soundName,
tourName = safeTourName,
// Playback rules
playbackMode = playbackMode,
maxPlayCount = if (playbackMode == PlaybackMode.LIMITED_COUNT) maxCount else null,
playCount = currentPlayCount,
scheduleEnabled = scheduleEnabled,
scheduleStartMillis = if (scheduleEnabled) scheduleStartMillis else null,
scheduleEndMillis = if (scheduleEnabled) scheduleEndMillis else null,
allowedStartMinutes = if (scheduleEnabled) allowedStartMinutes else null,
allowedEndMinutes = if (scheduleEnabled) allowedEndMinutes else null
)
)
}
}) {
Text(stringResource(R.string.save))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}