Release GPS2Audio v2.5.44 POI category fix
This commit is contained in:
@@ -12,9 +12,14 @@ 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.Add
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.AudioFile
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.SwapHoriz
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.*
|
||||
@@ -26,9 +31,14 @@ 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.AudioClip
|
||||
import de.waypointaudio.data.PlaybackMode
|
||||
import de.waypointaudio.data.TourPlaybackDefaults
|
||||
import de.waypointaudio.data.Waypoint
|
||||
import de.waypointaudio.data.effectiveClips
|
||||
import de.waypointaudio.data.withClips
|
||||
import de.waypointaudio.license.LicenseFeatureAccess
|
||||
import de.waypointaudio.license.LicenseStatusProvider
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
@@ -69,11 +79,23 @@ fun WaypointEditDialog(
|
||||
prefillTourName: String = Waypoint.DEFAULT_TOUR_NAME,
|
||||
tourDefaults: TourPlaybackDefaults = TourPlaybackDefaults(),
|
||||
onConfirm: (Waypoint) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
/**
|
||||
* v2.5.15 — Optionaler Callback: kopiert den bearbeiteten Wegpunkt
|
||||
* in eine andere Tour. Wenn null oder es keine alternative Tour gibt,
|
||||
* wird die Aktion ausgeblendet. Der Aufrufer ist für ID-Neuvergabe
|
||||
* und Persistenz zuständig (siehe [de.waypointaudio.viewmodel.WaypointViewModel.copyWaypointToTour]).
|
||||
*/
|
||||
onCopyToOtherTour: ((targetTour: String) -> Unit)? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isNew = waypoint == null
|
||||
|
||||
// v2.5.32 — Feature-Gate: Multi-Clip-Waypoints erfordern Profi-Zugang.
|
||||
// Der Wert liest den Compose-State direkt, reagiert reaktiv auf Lizenzimport.
|
||||
val licenseStatus = LicenseStatusProvider.status
|
||||
val canMultiClip = LicenseFeatureAccess.canUseMultiClip(licenseStatus)
|
||||
|
||||
// Coordinate pre-fill: GPS position takes priority for new waypoints
|
||||
val initialLat = when {
|
||||
waypoint != null -> "%.7f".format(Locale.US, waypoint.latitude)
|
||||
@@ -91,8 +113,18 @@ fun WaypointEditDialog(
|
||||
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 ?: "") }
|
||||
|
||||
// v2.5.12 — Multi-Clip-Editor.
|
||||
// Initial aus effectiveClips() befüllen, sodass alte Wegpunkte ohne audioClips
|
||||
// ihren Legacy-soundUri als ersten Clip vorfinden.
|
||||
val clips = remember {
|
||||
mutableStateListOf<AudioClip>().apply {
|
||||
waypoint?.effectiveClips()?.let { addAll(it) }
|
||||
}
|
||||
}
|
||||
// Index des Clips, der durch den nächsten Picker-Treffer ersetzt werden soll.
|
||||
// null = ein neuer Clip wird angehängt.
|
||||
var replaceTargetIndex by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
// Tour name: use existing waypoint's tour, or prefill for new, fallback to default
|
||||
val initialTour = when {
|
||||
@@ -144,7 +176,8 @@ fun WaypointEditDialog(
|
||||
}
|
||||
}
|
||||
|
||||
// Audio file picker
|
||||
// Audio file picker — v2.5.12: erzeugt einen neuen Clip oder ersetzt
|
||||
// die Audio-Quelle eines bestehenden Clips. Steuerung über [replaceTargetIndex].
|
||||
val audioPickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri: Uri? ->
|
||||
@@ -155,9 +188,31 @@ fun WaypointEditDialog(
|
||||
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
soundUri = uri.toString()
|
||||
soundName = uri.lastPathSegment?.substringAfterLast('/')?.substringAfterLast(':')
|
||||
val pickedUri = uri.toString()
|
||||
val pickedName = uri.lastPathSegment?.substringAfterLast('/')?.substringAfterLast(':')
|
||||
?: uri.toString().takeLast(30)
|
||||
val tgt = replaceTargetIndex
|
||||
if (tgt != null && tgt in clips.indices) {
|
||||
val old = clips[tgt]
|
||||
clips[tgt] = old.copy(
|
||||
uri = pickedUri,
|
||||
displayFileName = pickedName,
|
||||
// Titel nur dann automatisch übernehmen, wenn noch keiner gesetzt war
|
||||
title = old.title.ifBlank { pickedName }
|
||||
)
|
||||
} else {
|
||||
clips.add(
|
||||
AudioClip(
|
||||
title = pickedName,
|
||||
uri = pickedUri,
|
||||
displayFileName = pickedName,
|
||||
isActive = true,
|
||||
isPrimary = clips.isEmpty(),
|
||||
orderIndex = clips.size
|
||||
)
|
||||
)
|
||||
}
|
||||
replaceTargetIndex = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +374,23 @@ fun WaypointEditDialog(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// v2.5.16 — Wegpunkt kopieren (nur im Bearbeiten-Modus sichtbar)
|
||||
// Direkt unter der Tour-Zuordnung, damit der Aktionsbereich
|
||||
// ohne Scrollen auffindbar ist. Wird nur gezeigt, wenn der
|
||||
// Aufrufer einen Copy-Callback liefert (Player/Playback nicht).
|
||||
// ---------------------------------------------------------------
|
||||
if (waypoint != null && onCopyToOtherTour != null) {
|
||||
WaypointCopySection(
|
||||
currentTour = tourName,
|
||||
existingTours = existingTours,
|
||||
onCopy = { target ->
|
||||
onCopyToOtherTour(target)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// Coordinates
|
||||
@@ -362,33 +434,236 @@ fun WaypointEditDialog(
|
||||
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))
|
||||
// ---------------------------------------------------------------
|
||||
// v2.5.12 — Audio-Clips (Mehrfach-Auswahl + Reihenfolge)
|
||||
// v2.5.32 — Multi-Clip-Gate: Hauptclip (idx 0) immer frei,
|
||||
// Zusatzclips (idx > 0) nur mit Profi-Zugang bearbeiten.
|
||||
// ---------------------------------------------------------------
|
||||
Text(
|
||||
text = "Audio-Clips",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
// v2.5.32 — Profi-Hinweis: nur anzeigen, wenn Zusatzclips vorhanden
|
||||
// und kein Profi-Zugang. Daten werden nicht gelöscht.
|
||||
if (!canMultiClip && clips.size > 1) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = LicenseFeatureAccess.MSG_MULTI_CLIP,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (soundName.isNotBlank()) {
|
||||
Text(
|
||||
text = soundName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 2
|
||||
)
|
||||
} else {
|
||||
if (clips.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_sound_selected),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f)
|
||||
)
|
||||
} else {
|
||||
clips.forEachIndexed { idx, clip ->
|
||||
// v2.5.32: Zusatzclips (idx > 0) sind ohne Profi-Zugang
|
||||
// lesbar (Daten bleiben erhalten), aber nicht bearbeitbar.
|
||||
val isExtraClip = idx > 0
|
||||
val clipEditable = !isExtraClip || canMultiClip
|
||||
Surface(
|
||||
tonalElevation = 1.dp,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// Kopfzeile: Position + (primary)-Marker
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Clip ${idx + 1}" + if (clip.isPrimary) " · Hauptclip" else "",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (clipEditable) {
|
||||
// Reihenfolge nach oben / unten
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (idx > 0) {
|
||||
val tmp = clips[idx - 1]
|
||||
clips[idx - 1] = clips[idx]
|
||||
clips[idx] = tmp
|
||||
}
|
||||
},
|
||||
enabled = idx > 0
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.ArrowUpward,
|
||||
contentDescription = "Nach oben",
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (idx < clips.size - 1) {
|
||||
val tmp = clips[idx + 1]
|
||||
clips[idx + 1] = clips[idx]
|
||||
clips[idx] = tmp
|
||||
}
|
||||
},
|
||||
enabled = idx < clips.size - 1
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.ArrowDownward,
|
||||
contentDescription = "Nach unten",
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Titel-Feld
|
||||
OutlinedTextField(
|
||||
value = clip.title,
|
||||
onValueChange = { newTitle ->
|
||||
if (clipEditable) clips[idx] = clip.copy(title = newTitle)
|
||||
},
|
||||
label = { Text("Titel") },
|
||||
placeholder = { Text(clip.displayFileName.ifBlank { "Audio-Clip" }) },
|
||||
singleLine = true,
|
||||
readOnly = !clipEditable,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Dateiname (Quelle) als kleiner Hinweis
|
||||
if (clip.displayFileName.isNotBlank()) {
|
||||
Text(
|
||||
text = clip.displayFileName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f),
|
||||
maxLines = 2
|
||||
)
|
||||
} else if (!clip.hasAudio) {
|
||||
Text(
|
||||
text = "Keine Audio-Datei zugewiesen",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
if (clipEditable) {
|
||||
// Aktions-Zeile
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Aktiv-Schalter
|
||||
Text(
|
||||
text = "Aktiv",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = clip.isActive,
|
||||
onCheckedChange = { checked ->
|
||||
clips[idx] = clip.copy(isActive = checked)
|
||||
}
|
||||
)
|
||||
// Audio ersetzen
|
||||
IconButton(onClick = {
|
||||
replaceTargetIndex = idx
|
||||
audioPickerLauncher.launch(arrayOf("audio/*"))
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.SwapHoriz,
|
||||
contentDescription = "Audio ersetzen",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
// Clip löschen
|
||||
IconButton(onClick = {
|
||||
clips.removeAt(idx)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = "Clip löschen",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Als Hauptclip markieren (nur sinnvoll, wenn noch nicht primary)
|
||||
if (!clip.isPrimary && clip.hasAudio) {
|
||||
TextButton(onClick = {
|
||||
// Alle Clips zurücksetzen, dann diesen markieren
|
||||
for (i in clips.indices) {
|
||||
clips[i] = clips[i].copy(isPrimary = (i == idx))
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
text = "Als Hauptclip festlegen",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
} // end clipEditable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "Neuen Clip hinzufügen" – startet Picker im Anhängen-Modus.
|
||||
// v2.5.32: Nur anzeigen wenn Profi-Zugang vorhanden ODER noch kein
|
||||
// Clip (Hauptclip hinzufügen ist immer frei).
|
||||
if (canMultiClip || clips.isEmpty()) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
replaceTargetIndex = null
|
||||
audioPickerLauncher.launch(arrayOf("audio/*"))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
if (clips.isEmpty()) stringResource(R.string.choose_sound)
|
||||
else "Weiteren Clip hinzufügen"
|
||||
)
|
||||
}
|
||||
} else if (clips.isNotEmpty()) {
|
||||
// Hauptclip-Audio-Wechsel bleibt immer möglich (index 0)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
replaceTargetIndex = 0
|
||||
audioPickerLauncher.launch(arrayOf("audio/*"))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.SwapHoriz,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Hauptclip-Audio wechseln")
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
@@ -702,6 +977,9 @@ fun WaypointEditDialog(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f)
|
||||
)
|
||||
}
|
||||
|
||||
// v2.5.16 — Kopiersektion wurde in den oberen Tour-Bereich
|
||||
// verschoben und ist dort gut sichtbar.
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@@ -738,26 +1016,36 @@ fun WaypointEditDialog(
|
||||
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
|
||||
)
|
||||
// v2.5.12 — Clips über withClips() konsistent in audioClips +
|
||||
// Legacy-soundUri/-soundName spiegeln. Filtere Clips ohne URI
|
||||
// (z. B. wenn der User auf "+" geklickt, aber dann den Picker
|
||||
// abgebrochen hat — solche Phantom-Clips entstehen hier zwar
|
||||
// nicht, aber wir bleiben defensiv).
|
||||
val cleanClips = clips.filter { it.hasAudio }
|
||||
val base = (waypoint ?: Waypoint()).copy(
|
||||
name = name.trim(),
|
||||
latitude = lat!!,
|
||||
longitude = lng!!,
|
||||
radiusMeters = radius!!,
|
||||
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
|
||||
)
|
||||
val withAudio = if (cleanClips.isEmpty()) {
|
||||
// Keine Clips mehr — Legacy-Felder leeren, damit kein
|
||||
// Geister-Audio aus alten Daten zurückbleibt.
|
||||
base.copy(audioClips = emptyList(), soundUri = "", soundName = "")
|
||||
} else {
|
||||
base.withClips(cleanClips)
|
||||
}
|
||||
onConfirm(withAudio)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.save))
|
||||
@@ -770,3 +1058,107 @@ fun WaypointEditDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.5.16 — Kopiersektion für den Bearbeiten-Dialog.
|
||||
*
|
||||
* Stellt zwei Aktionen bereit:
|
||||
* - "In andere Tour kopieren" mit Drop-down-Auswahl (alle Touren außer der
|
||||
* aktuellen).
|
||||
* - "In dieser Tour duplizieren" als zweite Aktion. Hängt eine 1:1-Kopie
|
||||
* ans Ende der aktuellen Tour. Auch nutzbar, wenn nur eine Tour existiert.
|
||||
*
|
||||
* Wird vom [WaypointEditDialog] nur eingebunden, wenn ein bestehender
|
||||
* Wegpunkt bearbeitet wird und ein onCopyToOtherTour-Callback vorhanden ist
|
||||
* — also nie in der Player-/Playback-Ansicht.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WaypointCopySection(
|
||||
currentTour: String,
|
||||
existingTours: List<String>,
|
||||
onCopy: (targetTour: String) -> Unit,
|
||||
) {
|
||||
val effectiveCurrent = currentTour.ifBlank { Waypoint.DEFAULT_TOUR_NAME }
|
||||
val otherTours = remember(existingTours, effectiveCurrent) {
|
||||
existingTours.filter { it.isNotBlank() && it != effectiveCurrent }
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Text(
|
||||
text = "Wegpunkt kopieren",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "Erzeugt eine eigenständige Kopie mit allen Clips. Das Original " +
|
||||
"bleibt unverändert. Die Kopie wird ans Ende der Zieltour gehängt.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f)
|
||||
)
|
||||
|
||||
if (otherTours.isNotEmpty()) {
|
||||
var copyTargetMenuExpanded by remember { mutableStateOf(false) }
|
||||
var copyTarget by remember(otherTours) { mutableStateOf(otherTours.first()) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = copyTargetMenuExpanded,
|
||||
onExpandedChange = { copyTargetMenuExpanded = it },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = copyTarget,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Ziel-Tour") },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = copyTargetMenuExpanded
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = copyTargetMenuExpanded,
|
||||
onDismissRequest = { copyTargetMenuExpanded = false }
|
||||
) {
|
||||
otherTours.forEach { t ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(t) },
|
||||
onClick = {
|
||||
copyTarget = t
|
||||
copyTargetMenuExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FilledTonalButton(onClick = { onCopy(copyTarget) }) {
|
||||
Text("Kopieren")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "Es ist noch keine weitere Tour vorhanden. Du kannst diesen " +
|
||||
"Wegpunkt aber in der aktuellen Tour „$effectiveCurrent“ duplizieren.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f)
|
||||
)
|
||||
}
|
||||
|
||||
// Zweite Aktion: Duplizieren in derselben Tour. Immer verfügbar, auch
|
||||
// wenn keine andere Tour existiert — deckt den Single-Tour-Fall ab.
|
||||
OutlinedButton(
|
||||
onClick = { onCopy(effectiveCurrent) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("In dieser Tour duplizieren („$effectiveCurrent“)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user