Initial release of GPS2Audio
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package de.waypointaudio.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Globale Audio-Routing-Einstellungen für Live/PTT.
|
||||
*
|
||||
* Gerätekennungen (AudioDeviceInfo.id) sind Sitzungs-spezifisch und können sich nach
|
||||
* einem Neustart oder nach dem Trennen/Verbinden von Bluetooth-Geräten ändern.
|
||||
* Wenn ein gespeichertes Gerät nicht mehr verfügbar ist, wird automatisch der
|
||||
* Systemstandard verwendet.
|
||||
*
|
||||
* @param selectedInputDeviceId ID des Eingabegeräts, null = Systemstandard
|
||||
* @param selectedOutputDeviceId ID des Ausgabegeräts, null = Systemstandard
|
||||
*/
|
||||
data class AudioRoutingSettings(
|
||||
val selectedInputDeviceId: Int? = null,
|
||||
val selectedOutputDeviceId: Int? = null
|
||||
)
|
||||
|
||||
private val Context.audioRoutingDataStore by preferencesDataStore(name = "audio_routing_settings")
|
||||
|
||||
/**
|
||||
* Persistenz-Schicht für Audio-Routing-Einstellungen via DataStore.
|
||||
*/
|
||||
class AudioRoutingStore(private val context: Context) {
|
||||
|
||||
private companion object {
|
||||
val KEY_INPUT_DEVICE_ID = intPreferencesKey("selected_input_device_id")
|
||||
val KEY_OUTPUT_DEVICE_ID = intPreferencesKey("selected_output_device_id")
|
||||
// Sentinel: -1 means "use system default" (null cannot be stored as Int)
|
||||
const val NO_DEVICE = -1
|
||||
}
|
||||
|
||||
val settings: Flow<AudioRoutingSettings> = context.audioRoutingDataStore.data
|
||||
.map { prefs ->
|
||||
AudioRoutingSettings(
|
||||
selectedInputDeviceId = prefs[KEY_INPUT_DEVICE_ID].takeIf { it != null && it != NO_DEVICE },
|
||||
selectedOutputDeviceId = prefs[KEY_OUTPUT_DEVICE_ID].takeIf { it != null && it != NO_DEVICE }
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun save(settings: AudioRoutingSettings) {
|
||||
context.audioRoutingDataStore.edit { prefs ->
|
||||
prefs[KEY_INPUT_DEVICE_ID] = settings.selectedInputDeviceId ?: NO_DEVICE
|
||||
prefs[KEY_OUTPUT_DEVICE_ID] = settings.selectedOutputDeviceId ?: NO_DEVICE
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearInputDevice() {
|
||||
context.audioRoutingDataStore.edit { prefs ->
|
||||
prefs[KEY_INPUT_DEVICE_ID] = NO_DEVICE
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearOutputDevice() {
|
||||
context.audioRoutingDataStore.edit { prefs ->
|
||||
prefs[KEY_OUTPUT_DEVICE_ID] = NO_DEVICE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.waypointaudio.data
|
||||
|
||||
/**
|
||||
* Ein einzelner Punkt eines aufgezeichneten GPS-Tracks.
|
||||
*
|
||||
* @param latitude Breitengrad in Dezimalgrad
|
||||
* @param longitude Längengrad in Dezimalgrad
|
||||
* @param timestamp Unix-Millisekunden zum Zeitpunkt der Aufzeichnung
|
||||
*/
|
||||
data class GpsTrackPoint(
|
||||
val latitude: Double = 0.0,
|
||||
val longitude: Double = 0.0,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.waypointaudio.data
|
||||
|
||||
/**
|
||||
* Quelle der Begleitmusik für eine Tour.
|
||||
*/
|
||||
enum class MusicSourceType {
|
||||
LOCAL_PLAYLIST,
|
||||
STREAM_URL
|
||||
}
|
||||
|
||||
/**
|
||||
* Verhalten der Begleitmusik, wenn ein Wegpunkt-Audio abgespielt wird.
|
||||
*/
|
||||
enum class WaypointMusicBehavior {
|
||||
/** Begleitmusik pausieren, danach fortsetzen. */
|
||||
PAUSE_RESUME,
|
||||
|
||||
/** Begleitmusik ausblenden (Fade-out), Wegpunkt abspielen, danach einblenden (Fade-in). */
|
||||
FADE_OUT_IN,
|
||||
|
||||
/** Begleitmusik auf duckVolume leise stellen, Wegpunkt parallel abspielen, danach wiederherstellen. */
|
||||
DUCK_UNDERLAY,
|
||||
|
||||
/** Begleitmusik unveränderter Lautstärke weiterspielen, Wegpunkt parallel abspielen. */
|
||||
CONTINUE_UNDERLAY
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein einzelnes Element in der lokalen Playlist.
|
||||
*/
|
||||
data class PlaylistItem(
|
||||
val uriString: String,
|
||||
val displayName: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Begleitmusik-Einstellungen für eine Tour.
|
||||
*
|
||||
* @param enabled Ob Begleitmusik für diese Tour aktiv ist.
|
||||
* @param sourceType Quelle: lokale Playlist oder Stream-URL.
|
||||
* @param localPlaylist Liste von Audiodateien (content:// URIs) aus dem lokalen Speicher.
|
||||
* @param streamUrl Direkte HTTP/HTTPS-Stream-URL (z. B. Internetradio).
|
||||
* @param behavior Verhalten wenn Wegpunkt-Audio abgespielt wird.
|
||||
* @param fadeDurationMs Dauer des Fade-Effekts in Millisekunden.
|
||||
* @param duckVolume Lautstärke (0.0–1.0) während des Duck-Modus.
|
||||
* @param shuffle Playlist zufällig abspielen.
|
||||
* @param autoStartAfterWaypoint Nach Wegpunkt-Audio Begleitmusik automatisch starten,
|
||||
* auch wenn sie vorher nicht spielte.
|
||||
*/
|
||||
data class TourAudioSettings(
|
||||
val enabled: Boolean = false,
|
||||
val sourceType: MusicSourceType = MusicSourceType.LOCAL_PLAYLIST,
|
||||
val localPlaylist: List<PlaylistItem> = emptyList(),
|
||||
val streamUrl: String? = null,
|
||||
val behavior: WaypointMusicBehavior = WaypointMusicBehavior.PAUSE_RESUME,
|
||||
val fadeDurationMs: Long = 1500L,
|
||||
val duckVolume: Float = 0.25f,
|
||||
val shuffle: Boolean = false,
|
||||
val autoStartAfterWaypoint: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.waypointaudio.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
// Separater DataStore für Begleitmusik-Einstellungen
|
||||
private val Context.musicDataStore: DataStore<Preferences> by preferencesDataStore(name = "tour_music")
|
||||
|
||||
/**
|
||||
* Persistiert Begleitmusik-Einstellungen pro Tour (Key: Tourname).
|
||||
* Serialisiert als JSON Map<String, TourAudioSettings>.
|
||||
*/
|
||||
class TourMusicStore(private val context: Context) {
|
||||
|
||||
private val gson: Gson = GsonBuilder().create()
|
||||
private val mapType = object : TypeToken<Map<String, TourAudioSettingsJson>>() {}.type
|
||||
|
||||
companion object {
|
||||
private val KEY_MUSIC_SETTINGS = stringPreferencesKey("music_settings_json")
|
||||
}
|
||||
|
||||
/** Flow der Begleitmusik-Einstellungen für alle Touren. */
|
||||
val settingsFlow: Flow<Map<String, TourAudioSettings>> =
|
||||
context.musicDataStore.data.map { prefs ->
|
||||
val json = prefs[KEY_MUSIC_SETTINGS] ?: "{}"
|
||||
runCatching {
|
||||
val raw: Map<String, TourAudioSettingsJson>? = gson.fromJson(json, mapType)
|
||||
raw?.mapValues { (_, v) -> v.toDomain() } ?: emptyMap()
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
/** Liefert die Einstellungen für eine spezifische Tour. */
|
||||
fun settingsForTour(tourName: String): Flow<TourAudioSettings> =
|
||||
settingsFlow.map { map -> map[tourName] ?: TourAudioSettings() }
|
||||
|
||||
/** Speichert oder aktualisiert die Einstellungen für eine Tour. */
|
||||
suspend fun save(tourName: String, settings: TourAudioSettings) {
|
||||
context.musicDataStore.edit { prefs ->
|
||||
val json = prefs[KEY_MUSIC_SETTINGS] ?: "{}"
|
||||
val map: MutableMap<String, TourAudioSettingsJson> = runCatching {
|
||||
gson.fromJson<Map<String, TourAudioSettingsJson>>(json, mapType)?.toMutableMap()
|
||||
}.getOrNull() ?: mutableMapOf()
|
||||
map[tourName] = TourAudioSettingsJson.fromDomain(settings)
|
||||
prefs[KEY_MUSIC_SETTINGS] = gson.toJson(map)
|
||||
}
|
||||
}
|
||||
|
||||
/** Löscht die Einstellungen für eine Tour (z. B. beim Tour-Löschen). */
|
||||
suspend fun delete(tourName: String) {
|
||||
context.musicDataStore.edit { prefs ->
|
||||
val json = prefs[KEY_MUSIC_SETTINGS] ?: "{}"
|
||||
val map: MutableMap<String, TourAudioSettingsJson> = runCatching {
|
||||
gson.fromJson<Map<String, TourAudioSettingsJson>>(json, mapType)?.toMutableMap()
|
||||
}.getOrNull() ?: mutableMapOf()
|
||||
map.remove(tourName)
|
||||
prefs[KEY_MUSIC_SETTINGS] = gson.toJson(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON-Repräsentation (Gson-kompatibel, ohne Kotlin-Enums direkt serialisieren)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
internal data class PlaylistItemJson(
|
||||
val uriString: String = "",
|
||||
val displayName: String = ""
|
||||
) {
|
||||
fun toDomain() = PlaylistItem(uriString, displayName)
|
||||
companion object {
|
||||
fun fromDomain(item: PlaylistItem) = PlaylistItemJson(item.uriString, item.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class TourAudioSettingsJson(
|
||||
val enabled: Boolean = false,
|
||||
val sourceType: String = "LOCAL_PLAYLIST",
|
||||
val localPlaylist: List<PlaylistItemJson> = emptyList(),
|
||||
val streamUrl: String? = null,
|
||||
val behavior: String = "PAUSE_RESUME",
|
||||
val fadeDurationMs: Long = 1500L,
|
||||
val duckVolume: Float = 0.25f,
|
||||
val shuffle: Boolean = false,
|
||||
val autoStartAfterWaypoint: Boolean = false
|
||||
) {
|
||||
fun toDomain() = TourAudioSettings(
|
||||
enabled = enabled,
|
||||
sourceType = runCatching { MusicSourceType.valueOf(sourceType) }
|
||||
.getOrDefault(MusicSourceType.LOCAL_PLAYLIST),
|
||||
localPlaylist = localPlaylist.map { it.toDomain() },
|
||||
streamUrl = streamUrl,
|
||||
behavior = runCatching { WaypointMusicBehavior.valueOf(behavior) }
|
||||
.getOrDefault(WaypointMusicBehavior.PAUSE_RESUME),
|
||||
fadeDurationMs = fadeDurationMs,
|
||||
duckVolume = duckVolume,
|
||||
shuffle = shuffle,
|
||||
autoStartAfterWaypoint = autoStartAfterWaypoint
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromDomain(s: TourAudioSettings) = TourAudioSettingsJson(
|
||||
enabled = s.enabled,
|
||||
sourceType = s.sourceType.name,
|
||||
localPlaylist = s.localPlaylist.map { PlaylistItemJson.fromDomain(it) },
|
||||
streamUrl = s.streamUrl,
|
||||
behavior = s.behavior.name,
|
||||
fadeDurationMs = s.fadeDurationMs,
|
||||
duckVolume = s.duckVolume,
|
||||
shuffle = s.shuffle,
|
||||
autoStartAfterWaypoint = s.autoStartAfterWaypoint
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.waypointaudio.data
|
||||
|
||||
/**
|
||||
* Tour-weite Abspiel-Vorgaben, die bei neuen Wegpunkten als Standardwerte verwendet werden
|
||||
* und per Massen-Aktion auf alle vorhandenen Wegpunkte einer Tour angewendet werden können.
|
||||
*
|
||||
* @param tourName Name der Tour
|
||||
* @param playbackMode Standard-Abspiel-Modus für neue und alle vorhandenen Wegpunkte
|
||||
* @param maxPlayCount Standard-Maximale Abspielanzahl (nur für LIMITED_COUNT)
|
||||
*/
|
||||
data class TourPlaybackDefaults(
|
||||
val tourName: String = Waypoint.DEFAULT_TOUR_NAME,
|
||||
val playbackMode: PlaybackMode = PlaybackMode.EVERY_ENTRY,
|
||||
val maxPlayCount: Int = 3
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
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 URI zur Audiodatei (content:// oder file://)
|
||||
* @param soundName Anzeigename der Audiodatei
|
||||
* @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".
|
||||
*
|
||||
* --- 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
|
||||
*/
|
||||
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
|
||||
) {
|
||||
companion object {
|
||||
/** Standard-Tourname für bestehende Wegpunkte ohne tourName-Feld. */
|
||||
const val DEFAULT_TOUR_NAME = "Standard"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package de.waypointaudio.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
// DataStore-Instanz als Extension Property (Singleton pro Context)
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "waypoints")
|
||||
|
||||
/**
|
||||
* Lokaler persistenter Speicher für Wegpunkte und Touren.
|
||||
* Nutzt Jetpack DataStore mit JSON-Serialisierung via Gson.
|
||||
*
|
||||
* tourList speichert die geordnete Liste aller Tournamen (inkl. Standardtour).
|
||||
* Touren werden zusätzlich aus den Wegpunkt-tourName-Feldern abgeleitet,
|
||||
* damit auch leere Touren (ohne Wegpunkte) persistiert werden.
|
||||
*
|
||||
* tourPlaybackDefaults speichert die tour-weiten Abspiel-Vorgaben als JSON-Map.
|
||||
*/
|
||||
class WaypointStore(private val context: Context) {
|
||||
|
||||
private val gson: Gson = GsonBuilder().create()
|
||||
private val waypointListType = object : TypeToken<List<Waypoint>>() {}.type
|
||||
private val stringListType = object : TypeToken<List<String>>() {}.type
|
||||
private val defaultsMapType = object : TypeToken<Map<String, TourPlaybackDefaults>>() {}.type
|
||||
|
||||
companion object {
|
||||
private val KEY_WAYPOINTS = stringPreferencesKey("waypoints_json")
|
||||
private val KEY_TOURS = stringPreferencesKey("tours_json")
|
||||
private val KEY_TOUR_DEFAULTS = stringPreferencesKey("tour_playback_defaults_json")
|
||||
private val KEY_GPS_TRACKS = stringPreferencesKey("gps_tracks_json")
|
||||
}
|
||||
|
||||
/** Liefert einen Flow mit der aktuellen Wegpunktliste. */
|
||||
val waypointsFlow: Flow<List<Waypoint>> = context.dataStore.data.map { prefs ->
|
||||
val json = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
runCatching {
|
||||
gson.fromJson<List<Waypoint>>(json, waypointListType) ?: emptyList()
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert einen Flow mit der persistierten Tourliste.
|
||||
* Die Liste enthält mindestens die Standardtour.
|
||||
* Touren aus Wegpunkten werden ggf. ergänzt, aber die Reihenfolge
|
||||
* (und leere Touren) stammen aus diesem Key.
|
||||
*/
|
||||
val toursFlow: Flow<List<String>> = context.dataStore.data.map { prefs ->
|
||||
val json = prefs[KEY_TOURS] ?: "[]"
|
||||
val stored = runCatching {
|
||||
gson.fromJson<List<String>>(json, stringListType) ?: emptyList()
|
||||
}.getOrDefault(emptyList())
|
||||
// Immer mindestens die Standardtour enthalten
|
||||
if (stored.isEmpty()) listOf(Waypoint.DEFAULT_TOUR_NAME) else stored
|
||||
}
|
||||
|
||||
/** Liefert einen Flow mit der Map tour → TourPlaybackDefaults. */
|
||||
val tourDefaultsFlow: Flow<Map<String, TourPlaybackDefaults>> =
|
||||
context.dataStore.data.map { prefs ->
|
||||
val json = prefs[KEY_TOUR_DEFAULTS] ?: "{}"
|
||||
runCatching {
|
||||
gson.fromJson<Map<String, TourPlaybackDefaults>>(json, defaultsMapType) ?: emptyMap()
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
/** GPS-Track-Flow: Map tour → Liste von TrackPoint. */
|
||||
val gpsTracksFlow: Flow<Map<String, List<GpsTrackPoint>>> =
|
||||
context.dataStore.data.map { prefs ->
|
||||
val json = prefs[KEY_GPS_TRACKS] ?: "{}"
|
||||
runCatching {
|
||||
val mapType = object : TypeToken<Map<String, List<GpsTrackPoint>>>() {}.type
|
||||
gson.fromJson<Map<String, List<GpsTrackPoint>>>(json, mapType) ?: emptyMap()
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
/** Speichert die gesamte Wegpunktliste. */
|
||||
suspend fun saveAll(waypoints: List<Waypoint>) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(waypoints)
|
||||
}
|
||||
}
|
||||
|
||||
/** Speichert die gesamte Tourliste (Reihenfolge bleibt erhalten). */
|
||||
suspend fun saveTours(tours: List<String>) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[KEY_TOURS] = gson.toJson(tours)
|
||||
}
|
||||
}
|
||||
|
||||
/** Speichert oder aktualisiert die Abspiel-Vorgaben einer Tour. */
|
||||
suspend fun saveTourDefaults(tourName: String, defaults: TourPlaybackDefaults) {
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_TOUR_DEFAULTS] ?: "{}"
|
||||
val map: MutableMap<String, TourPlaybackDefaults> = runCatching {
|
||||
gson.fromJson<Map<String, TourPlaybackDefaults>>(json, defaultsMapType)
|
||||
?.toMutableMap()
|
||||
}.getOrNull() ?: mutableMapOf()
|
||||
map[tourName] = defaults
|
||||
prefs[KEY_TOUR_DEFAULTS] = gson.toJson(map)
|
||||
}
|
||||
}
|
||||
|
||||
/** Liest die Abspiel-Vorgaben einer Tour einmalig (ohne Flow). */
|
||||
suspend fun getTourDefaults(tourName: String): TourPlaybackDefaults {
|
||||
val json = context.dataStore.data.first()[KEY_TOUR_DEFAULTS] ?: "{}"
|
||||
val map: Map<String, TourPlaybackDefaults> = runCatching {
|
||||
gson.fromJson<Map<String, TourPlaybackDefaults>>(json, defaultsMapType) ?: emptyMap()
|
||||
}.getOrDefault(emptyMap())
|
||||
return map[tourName] ?: TourPlaybackDefaults(tourName = tourName)
|
||||
}
|
||||
|
||||
/** Speichert den GPS-Track einer Tour. */
|
||||
suspend fun saveGpsTrack(tourName: String, points: List<GpsTrackPoint>) {
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_GPS_TRACKS] ?: "{}"
|
||||
val mapType = object : TypeToken<Map<String, List<GpsTrackPoint>>>() {}.type
|
||||
val map: MutableMap<String, List<GpsTrackPoint>> = runCatching {
|
||||
gson.fromJson<Map<String, List<GpsTrackPoint>>>(json, mapType)?.toMutableMap()
|
||||
}.getOrNull() ?: mutableMapOf()
|
||||
map[tourName] = points
|
||||
prefs[KEY_GPS_TRACKS] = gson.toJson(map)
|
||||
}
|
||||
}
|
||||
|
||||
/** Löscht den GPS-Track einer Tour. */
|
||||
suspend fun clearGpsTrack(tourName: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_GPS_TRACKS] ?: "{}"
|
||||
val mapType = object : TypeToken<Map<String, List<GpsTrackPoint>>>() {}.type
|
||||
val map: MutableMap<String, List<GpsTrackPoint>> = runCatching {
|
||||
gson.fromJson<Map<String, List<GpsTrackPoint>>>(json, mapType)?.toMutableMap()
|
||||
}.getOrNull() ?: mutableMapOf()
|
||||
map.remove(tourName)
|
||||
prefs[KEY_GPS_TRACKS] = gson.toJson(map)
|
||||
}
|
||||
}
|
||||
|
||||
/** Fügt einen Wegpunkt hinzu oder aktualisiert einen vorhandenen (matching id). */
|
||||
suspend fun upsert(waypoint: Waypoint) {
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
val list: MutableList<Waypoint> = runCatching {
|
||||
gson.fromJson<List<Waypoint>>(json, waypointListType)?.toMutableList()
|
||||
}.getOrNull() ?: mutableListOf()
|
||||
|
||||
val index = list.indexOfFirst { it.id == waypoint.id }
|
||||
if (index >= 0) list[index] = waypoint else list.add(waypoint)
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(list)
|
||||
}
|
||||
}
|
||||
|
||||
/** Löscht einen Wegpunkt anhand seiner ID. */
|
||||
suspend fun delete(id: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
val list: MutableList<Waypoint> = runCatching {
|
||||
gson.fromJson<List<Waypoint>>(json, waypointListType)?.toMutableList()
|
||||
}.getOrNull() ?: mutableListOf()
|
||||
list.removeAll { it.id == id }
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(list)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inkrementiert den playCount eines Wegpunkts atomar.
|
||||
* Wird vom WaypointLocationService nach erfolgreicher Wiedergabe aufgerufen.
|
||||
* @return den neuen Wegpunkt mit inkrementiertem playCount, oder null wenn nicht gefunden.
|
||||
*/
|
||||
suspend fun incrementPlayCount(waypointId: String): Waypoint? {
|
||||
var updated: Waypoint? = null
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
val list: MutableList<Waypoint> = runCatching {
|
||||
gson.fromJson<List<Waypoint>>(json, waypointListType)?.toMutableList()
|
||||
}.getOrNull() ?: mutableListOf()
|
||||
|
||||
val index = list.indexOfFirst { it.id == waypointId }
|
||||
if (index >= 0) {
|
||||
val wp = list[index].copy(playCount = list[index].playCount + 1)
|
||||
list[index] = wp
|
||||
updated = wp
|
||||
}
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(list)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den playCount aller Wegpunkte einer Tour auf 0 zurück.
|
||||
* @return Anzahl der zurückgesetzten Wegpunkte.
|
||||
*/
|
||||
suspend fun resetPlayCountsForTour(tourName: String): Int {
|
||||
var count = 0
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
val list: MutableList<Waypoint> = runCatching {
|
||||
gson.fromJson<List<Waypoint>>(json, waypointListType)?.toMutableList()
|
||||
}.getOrNull() ?: mutableListOf()
|
||||
|
||||
for (i in list.indices) {
|
||||
val wp = list[i]
|
||||
if (wp.tourName.ifBlank { Waypoint.DEFAULT_TOUR_NAME } == tourName && wp.playCount != 0) {
|
||||
list[i] = wp.copy(playCount = 0)
|
||||
count++
|
||||
}
|
||||
}
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(list)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet einen Abspiel-Modus auf alle Wegpunkte einer Tour an.
|
||||
* Optional: setzt auch maxPlayCount, optional: setzt playCount zurück.
|
||||
* @return Anzahl der aktualisierten Wegpunkte.
|
||||
*/
|
||||
suspend fun applyPlaybackModeForTour(
|
||||
tourName: String,
|
||||
mode: PlaybackMode,
|
||||
maxPlayCount: Int?,
|
||||
resetCounts: Boolean
|
||||
): Int {
|
||||
var count = 0
|
||||
context.dataStore.edit { prefs ->
|
||||
val json = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
val list: MutableList<Waypoint> = runCatching {
|
||||
gson.fromJson<List<Waypoint>>(json, waypointListType)?.toMutableList()
|
||||
}.getOrNull() ?: mutableListOf()
|
||||
|
||||
for (i in list.indices) {
|
||||
val wp = list[i]
|
||||
if (wp.tourName.ifBlank { Waypoint.DEFAULT_TOUR_NAME } == tourName) {
|
||||
list[i] = wp.copy(
|
||||
playbackMode = mode,
|
||||
maxPlayCount = if (mode == PlaybackMode.LIMITED_COUNT) maxPlayCount else null,
|
||||
playCount = if (resetCounts) 0 else wp.playCount
|
||||
)
|
||||
count++
|
||||
}
|
||||
}
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(list)
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user