Release GPS2Audio v2.5.44 POI category fix

This commit is contained in:
Perplexity Computer
2026-05-30 05:42:44 +00:00
parent 18ea77b21d
commit 014e6c1bf1
100 changed files with 28584 additions and 1083 deletions
@@ -23,11 +23,15 @@ enum class PlaybackMode {
* @param latitude Breitengrad in Dezimalgrad
* @param longitude Längengrad in Dezimalgrad
* @param radiusMeters Erkennungsradius in Metern
* @param soundUri URI zur Audiodatei (content:// oder file://)
* @param soundName Anzeigename der Audiodatei
* @param soundUri Legacy-Single-Clip-URI (v2.5.11 und älter). In v2.5.12
* ist [audioClips] die führende Repräsentation; das
* Feld bleibt zur Rückwärtskompatibilität gefüllt
* (typischerweise = primary-Clip-URI), sodass alte
* Lese-Pfade unverändert funktionieren.
* @param soundName Legacy-Dateiname (v2.5.11 und älter). Wie [soundUri]
* aus Kompatibilitätsgründen weiter gepflegt.
* @param isActive Ob der Wegpunkt aktiv überwacht wird
* @param tourName Name der Tour/Route (Standard: "Standard"). Rückwärtskompatibel:
* bestehende Wegpunkte ohne dieses Feld erhalten automatisch "Standard".
* @param tourName Name der Tour/Route (Standard: "Standard").
*
* --- Abspielregeln (optional, Standardwerte = bisheriges Verhalten) ---
*
@@ -39,6 +43,14 @@ enum class PlaybackMode {
* @param scheduleEndMillis Spätestes Datum/Uhrzeit (Unix-Millisekunden), bis zu dem Abspielen erlaubt ist
* @param allowedStartMinutes Tagesminute (01439), ab der das tägliche Zeitfenster beginnt
* @param allowedEndMinutes Tagesminute (01439), bis zu der das tägliche Zeitfenster endet
*
* --- v2.5.12 Multi-Clip ---
*
* @param audioClips Liste der Audio-Clips dieses Wegpunkts. Leer =
* Migration aus Legacy-Feldern [soundUri]/[soundName]
* via [effectiveClips]. Aktive Clips werden in
* Reihenfolge ([AudioClip.orderIndex]) abgespielt;
* inaktive werden übersprungen.
*/
data class Waypoint(
val id: String = UUID.randomUUID().toString(),
@@ -61,10 +73,118 @@ data class Waypoint(
val scheduleStartMillis: Long? = null,
val scheduleEndMillis: Long? = null,
val allowedStartMinutes: Int? = null,
val allowedEndMinutes: Int? = null
val allowedEndMinutes: Int? = null,
// v2.5.12 — Multi-Clip-Erweiterung. Default = leer, damit alte JSONs ohne
// diesen Key weiterhin laden (Gson füllt den Default ein).
val audioClips: List<AudioClip> = emptyList()
) {
companion object {
/** Standard-Tourname für bestehende Wegpunkte ohne tourName-Feld. */
const val DEFAULT_TOUR_NAME = "Standard"
}
}
/**
* v2.5.12 — Liefert die *effektiv anzuwendende* Cliplist für diesen Wegpunkt.
*
* Verhalten:
* - Ist [Waypoint.audioClips] nicht leer, wird diese Liste in stabiler
* Sortierung (orderIndex, dann id) zurückgegeben.
* - Andernfalls wird sofern [Waypoint.soundUri] vorhanden ist ein
* synthetischer primary-Clip aus [Waypoint.soundUri]/[Waypoint.soundName]
* erzeugt. Damit funktionieren alle alten Wegpunkte ohne Migration
* transparent weiter.
* - Hat ein Wegpunkt weder [Waypoint.audioClips] noch [Waypoint.soundUri],
* ist die Rückgabe leer.
*
* Die ID des synthetischen Legacy-Clips wird deterministisch aus der Waypoint-
* ID abgeleitet, damit sich Skip-/UI-Zustände über mehrere Aufrufe hinweg
* stabil referenzieren lassen.
*/
fun Waypoint.effectiveClips(): List<AudioClip> {
if (audioClips.isNotEmpty()) {
return audioClips.sortedWith(
compareBy({ it.orderIndex }, { it.id })
)
}
if (soundUri.isBlank()) return emptyList()
return listOf(
AudioClip(
id = "legacy-$id",
title = soundName,
uri = soundUri,
displayFileName = soundName,
isActive = true,
isPrimary = true,
orderIndex = 0
)
)
}
/**
* Alle aktiven, abspielbaren Clips (für Tour-Play). Reihenfolge wie
* [effectiveClips]; ohne deaktivierte und ohne leere URIs.
*/
fun Waypoint.activeClips(): List<AudioClip> =
effectiveClips().filter { it.isActive && it.hasAudio }
/**
* Primärer Clip (Vorschau, Listenansicht, Legacy-Fallback).
*
* Priorität:
* 1. Erster Clip in [effectiveClips] mit isPrimary = true (sofern abspielbar).
* 2. Erster *aktiver* Clip mit URI.
* 3. Erster Clip mit URI überhaupt.
* 4. null, falls kein Audio existiert.
*/
fun Waypoint.primaryClip(): AudioClip? {
val clips = effectiveClips()
return clips.firstOrNull { it.isPrimary && it.hasAudio }
?: clips.firstOrNull { it.isActive && it.hasAudio }
?: clips.firstOrNull { it.hasAudio }
}
/**
* URI des primären Clips, oder Legacy-[Waypoint.soundUri] als Fallback.
* Praktisch für alle Stellen, die früher direkt `waypoint.soundUri` lasen.
*/
fun Waypoint.primarySoundUri(): String =
primaryClip()?.uri?.ifBlank { soundUri } ?: soundUri
/**
* Anzeigename des primären Clips, oder Legacy-[Waypoint.soundName].
*/
fun Waypoint.primarySoundName(): String =
primaryClip()?.effectiveTitle?.ifBlank { soundName } ?: soundName
/**
* v2.5.12 — Hilfsfunktion zum Setzen der Clipliste mit konsistenten
* Legacy-Spiegel-Feldern. Die Felder [Waypoint.soundUri] und
* [Waypoint.soundName] werden auf den primären Clip gesetzt, sodass
* - alte Lese-Pfade weiter den Hauptclip sehen,
* - JSON-Exports (legacy-Format) ein sinnvolles soundUri-Feld enthalten,
* - der Tour-Play (multi-clip-aware) trotzdem alle Clips kennt.
*
* orderIndex wird beim Speichern auf die Listen-Position normalisiert,
* damit die Reihenfolge stabil aus der UI übernommen werden kann.
*/
fun Waypoint.withClips(newClips: List<AudioClip>): Waypoint {
if (newClips.isEmpty()) {
return copy(audioClips = emptyList())
}
val normalized = newClips.mapIndexed { idx, c -> c.copy(orderIndex = idx) }
val primary = normalized.firstOrNull { it.isPrimary && it.hasAudio }
?: normalized.firstOrNull { it.isActive && it.hasAudio }
?: normalized.firstOrNull { it.hasAudio }
?: normalized.first()
// Genau einen Clip als primary markieren (idempotent)
val finalized = normalized.map { c ->
c.copy(isPrimary = (c.id == primary.id))
}
return copy(
audioClips = finalized,
soundUri = primary.uri,
soundName = primary.effectiveTitle
)
}