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
@@ -38,8 +38,21 @@ class WaypointStore(private val context: Context) {
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")
private val KEY_TRACK_DRAFTS = stringPreferencesKey("track_drafts_json")
// v2.4.6 — persistente Tourrouten (tourName → TourRoute, JSON).
// Eigene Schlüssel-Datei: bestehende Backups/JSON ohne diesen Key
// bleiben gültig (Default = leere Map).
private val KEY_TOUR_ROUTES = stringPreferencesKey("tour_routes_json")
// v2.5.5 — Persistente Tour-Cover (tourName → TourCover, JSON).
// Optional pro Tour; fehlt der Eintrag, bleibt die deterministische
// Standard-Optik (Akzentfarbe + Icon-Heuristik aus Tournamen) erhalten.
private val KEY_TOUR_COVERS = stringPreferencesKey("tour_covers_json")
}
private val draftListType = object : TypeToken<List<TrackDraft>>() {}.type
private val tourRoutesMapType = object : TypeToken<Map<String, TourRoute>>() {}.type
private val tourCoversMapType = object : TypeToken<Map<String, TourCover>>() {}.type
/** Liefert einen Flow mit der aktuellen Wegpunktliste. */
val waypointsFlow: Flow<List<Waypoint>> = context.dataStore.data.map { prefs ->
val json = prefs[KEY_WAYPOINTS] ?: "[]"
@@ -72,6 +85,33 @@ class WaypointStore(private val context: Context) {
}.getOrDefault(emptyMap())
}
/**
* v2.4.6 — Persistente Tourrouten als Map (tourName → TourRoute).
* Rückwärtskompatibel: fehlt der Key komplett, ist die Map leer.
* Auch korrupte JSON werden defensiv auf leer abgefangen, damit eine
* einzelne kaputte Route nicht den Rest der App blockiert.
*/
val tourRoutesFlow: Flow<Map<String, TourRoute>> =
context.dataStore.data.map { prefs ->
val json = prefs[KEY_TOUR_ROUTES] ?: "{}"
runCatching {
gson.fromJson<Map<String, TourRoute>>(json, tourRoutesMapType) ?: emptyMap()
}.getOrDefault(emptyMap())
}
/**
* v2.5.5 — Persistente Tour-Cover als Map (tourName → TourCover).
* Rückwärtskompatibel: fehlt der Key, ist die Map leer und Touren
* bekommen ihre bisherige deterministische Hero-Optik.
*/
val tourCoversFlow: Flow<Map<String, TourCover>> =
context.dataStore.data.map { prefs ->
val json = prefs[KEY_TOUR_COVERS] ?: "{}"
runCatching {
gson.fromJson<Map<String, TourCover>>(json, tourCoversMapType) ?: emptyMap()
}.getOrDefault(emptyMap())
}
/** GPS-Track-Flow: Map tour → Liste von TrackPoint. */
val gpsTracksFlow: Flow<Map<String, List<GpsTrackPoint>>> =
context.dataStore.data.map { prefs ->
@@ -82,6 +122,59 @@ class WaypointStore(private val context: Context) {
}.getOrDefault(emptyMap())
}
/**
* Reaktiver Flow der archivierten Track-Drafts.
* Drafts sind eigenständige, persistierte Aufzeichnungen — neue
* Aufzeichnungen werden als zusätzliche Einträge gespeichert,
* niemals durch Überschreiben.
*/
val trackDraftsFlow: Flow<List<TrackDraft>> = context.dataStore.data.map { prefs ->
val json = prefs[KEY_TRACK_DRAFTS] ?: "[]"
runCatching {
gson.fromJson<List<TrackDraft>>(json, draftListType) ?: emptyList()
}.getOrDefault(emptyList())
}
/** Fügt einen neuen Track-Draft hinzu (immer als neuen Eintrag). */
suspend fun addTrackDraft(draft: TrackDraft) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TRACK_DRAFTS] ?: "[]"
val list: MutableList<TrackDraft> = runCatching {
gson.fromJson<List<TrackDraft>>(json, draftListType)?.toMutableList()
}.getOrNull() ?: mutableListOf()
list.add(draft)
prefs[KEY_TRACK_DRAFTS] = gson.toJson(list)
}
}
/**
* Aktualisiert einen vorhandenen Draft anhand seiner ID.
* Falls noch nicht vorhanden, wird er hinzugefügt (defensiv).
*/
suspend fun upsertTrackDraft(draft: TrackDraft) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TRACK_DRAFTS] ?: "[]"
val list: MutableList<TrackDraft> = runCatching {
gson.fromJson<List<TrackDraft>>(json, draftListType)?.toMutableList()
}.getOrNull() ?: mutableListOf()
val idx = list.indexOfFirst { it.id == draft.id }
if (idx >= 0) list[idx] = draft else list.add(draft)
prefs[KEY_TRACK_DRAFTS] = gson.toJson(list)
}
}
/** Löscht einen Track-Draft anhand seiner ID. */
suspend fun deleteTrackDraft(id: String) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TRACK_DRAFTS] ?: "[]"
val list: MutableList<TrackDraft> = runCatching {
gson.fromJson<List<TrackDraft>>(json, draftListType)?.toMutableList()
}.getOrNull() ?: mutableListOf()
list.removeAll { it.id == id }
prefs[KEY_TRACK_DRAFTS] = gson.toJson(list)
}
}
/** Speichert die gesamte Wegpunktliste. */
suspend fun saveAll(waypoints: List<Waypoint>) {
context.dataStore.edit { prefs ->
@@ -131,6 +224,125 @@ class WaypointStore(private val context: Context) {
}
}
/**
* v2.4.6 — Speichert (oder ersetzt) die gespeicherte Route einer Tour.
* Setzt [TourRoute.updatedAtMillis] auf die aktuelle Zeit, sofern das
* übergebene Objekt nicht selbst einen neueren Zeitstempel mitbringt.
*/
suspend fun saveTourRoute(tourName: String, route: TourRoute) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_ROUTES] ?: "{}"
val map: MutableMap<String, TourRoute> = runCatching {
gson.fromJson<Map<String, TourRoute>>(json, tourRoutesMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
val stamped = route.copy(
updatedAtMillis = if (route.updatedAtMillis > 0) route.updatedAtMillis
else System.currentTimeMillis()
)
map[tourName] = stamped
prefs[KEY_TOUR_ROUTES] = gson.toJson(map)
}
}
/** Entfernt die gespeicherte Route einer Tour (falls vorhanden). */
suspend fun deleteTourRoute(tourName: String) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_ROUTES] ?: "{}"
val map: MutableMap<String, TourRoute> = runCatching {
gson.fromJson<Map<String, TourRoute>>(json, tourRoutesMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
if (map.remove(tourName) != null) {
prefs[KEY_TOUR_ROUTES] = gson.toJson(map)
}
}
}
/**
* Benennt die Tour einer gespeicherten Route um. Wird aus
* [WaypointViewModel.renameTour]/[deleteTour] aufgerufen, damit die
* Route nicht verwaist zurückbleibt.
*/
suspend fun renameTourRouteKey(oldTour: String, newTour: String) {
if (oldTour == newTour) return
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_ROUTES] ?: "{}"
val map: MutableMap<String, TourRoute> = runCatching {
gson.fromJson<Map<String, TourRoute>>(json, tourRoutesMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
val existing = map.remove(oldTour) ?: return@edit
map[newTour] = existing.copy(updatedAtMillis = System.currentTimeMillis())
prefs[KEY_TOUR_ROUTES] = gson.toJson(map)
}
}
/** Bulk-Schreiben der Routen (für Backup-Import). Vorhandene Keys werden überschrieben. */
suspend fun mergeTourRoutes(routes: Map<String, TourRoute>) {
if (routes.isEmpty()) return
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_ROUTES] ?: "{}"
val map: MutableMap<String, TourRoute> = runCatching {
gson.fromJson<Map<String, TourRoute>>(json, tourRoutesMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
map.putAll(routes)
prefs[KEY_TOUR_ROUTES] = gson.toJson(map)
}
}
/** v2.5.5 — Speichert das Cover einer Tour (überschreibt vorhandenes). */
suspend fun saveTourCover(tourName: String, cover: TourCover) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_COVERS] ?: "{}"
val map: MutableMap<String, TourCover> = runCatching {
gson.fromJson<Map<String, TourCover>>(json, tourCoversMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
map[tourName] = cover
prefs[KEY_TOUR_COVERS] = gson.toJson(map)
}
}
/** v2.5.5 — Entfernt das Cover einer Tour (falls vorhanden). */
suspend fun deleteTourCover(tourName: String) {
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_COVERS] ?: "{}"
val map: MutableMap<String, TourCover> = runCatching {
gson.fromJson<Map<String, TourCover>>(json, tourCoversMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
if (map.remove(tourName) != null) {
prefs[KEY_TOUR_COVERS] = gson.toJson(map)
}
}
}
/**
* v2.5.5 — Tour-Umbenennung: Cover-Eintrag mitziehen, damit er nicht
* unter einem verwaisten Tournamen liegen bleibt.
*/
suspend fun renameTourCoverKey(oldTour: String, newTour: String) {
if (oldTour == newTour) return
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_COVERS] ?: "{}"
val map: MutableMap<String, TourCover> = runCatching {
gson.fromJson<Map<String, TourCover>>(json, tourCoversMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
val existing = map.remove(oldTour) ?: return@edit
map[newTour] = existing
prefs[KEY_TOUR_COVERS] = gson.toJson(map)
}
}
/** v2.5.5 — Bulk-Schreiben der Cover (für Backup-Import). */
suspend fun mergeTourCovers(covers: Map<String, TourCover>) {
if (covers.isEmpty()) return
context.dataStore.edit { prefs ->
val json = prefs[KEY_TOUR_COVERS] ?: "{}"
val map: MutableMap<String, TourCover> = runCatching {
gson.fromJson<Map<String, TourCover>>(json, tourCoversMapType)?.toMutableMap()
}.getOrNull() ?: mutableMapOf()
map.putAll(covers)
prefs[KEY_TOUR_COVERS] = gson.toJson(map)
}
}
/** Löscht den GPS-Track einer Tour. */
suspend fun clearGpsTrack(tourName: String) {
context.dataStore.edit { prefs ->