191 lines
7.4 KiB
Kotlin
191 lines
7.4 KiB
Kotlin
package de.waypointaudio.data
|
||
|
||
import java.util.UUID
|
||
|
||
/**
|
||
* Abspiel-Modus für einen Wegpunkt.
|
||
*
|
||
* EVERY_ENTRY – Standardverhalten: Ton bei jedem erneuten Betreten.
|
||
* ONCE – Ton nur beim ersten Betreten (playCount == 0).
|
||
* LIMITED_COUNT – Ton maximal [maxPlayCount] Mal insgesamt.
|
||
*/
|
||
enum class PlaybackMode {
|
||
EVERY_ENTRY,
|
||
ONCE,
|
||
LIMITED_COUNT
|
||
}
|
||
|
||
/**
|
||
* Datenmodell für einen Wegpunkt.
|
||
*
|
||
* @param id Eindeutige ID (UUID)
|
||
* @param name Anzeigename des Wegpunkts
|
||
* @param latitude Breitengrad in Dezimalgrad
|
||
* @param longitude Längengrad in Dezimalgrad
|
||
* @param radiusMeters Erkennungsradius in Metern
|
||
* @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").
|
||
*
|
||
* --- Abspielregeln (optional, Standardwerte = bisheriges Verhalten) ---
|
||
*
|
||
* @param playbackMode Abspiel-Modus (Standard: EVERY_ENTRY = bisheriges Verhalten)
|
||
* @param maxPlayCount Maximale Abspielanzahl für LIMITED_COUNT; null = kein Limit
|
||
* @param playCount Bisherige Abspielanzahl (persistent, wird vom Service inkrementiert)
|
||
* @param scheduleEnabled Ob der Zeitplan aktiv ist
|
||
* @param scheduleStartMillis Frühestes Datum/Uhrzeit (Unix-Millisekunden), ab dem Abspielen erlaubt ist
|
||
* @param scheduleEndMillis Spätestes Datum/Uhrzeit (Unix-Millisekunden), bis zu dem Abspielen erlaubt ist
|
||
* @param allowedStartMinutes Tagesminute (0–1439), ab der das tägliche Zeitfenster beginnt
|
||
* @param allowedEndMinutes Tagesminute (0–1439), 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(),
|
||
val name: String = "",
|
||
val latitude: Double = 0.0,
|
||
val longitude: Double = 0.0,
|
||
val radiusMeters: Float = 50f,
|
||
val soundUri: String = "",
|
||
val soundName: String = "",
|
||
val isActive: Boolean = true,
|
||
|
||
// Tour-Zuordnung – safe default "Standard" für Rückwärtskompatibilität
|
||
val tourName: String = DEFAULT_TOUR_NAME,
|
||
|
||
// Abspielregeln – alle optional, Standardwerte entsprechen bisherigem Verhalten
|
||
val playbackMode: PlaybackMode = PlaybackMode.EVERY_ENTRY,
|
||
val maxPlayCount: Int? = null,
|
||
val playCount: Int = 0,
|
||
val scheduleEnabled: Boolean = false,
|
||
val scheduleStartMillis: Long? = null,
|
||
val scheduleEndMillis: Long? = null,
|
||
val allowedStartMinutes: 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
|
||
)
|
||
}
|