Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ba9adbe48 | |||
| a3382bb0aa |
@@ -0,0 +1,40 @@
|
||||
# GPS2Audio v2.7.39 — Handoff
|
||||
|
||||
## Ziel
|
||||
|
||||
Stabilitätsfix für Samsung-Geräte: Die in v2.7.37 eingeführte feste Stille-Pause zwischen manuellen Clips/Wegpunkten wurde entfernt. Der Nutzen der Pause war rein kosmetisch, während sie auf einigen Geräten offenbar zu uneindeutigen Player-Zuständen und der sichtbaren Fehlermeldung „Audio konnte nicht abgespielt werden“ beitragen konnte.
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
- `app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt`
|
||||
- `manualGapJob` und `MANUAL_GAP_MS` wurden entfernt.
|
||||
- Der Sonderfall „Play/Pause während der Stille-Pause“ wurde entfernt.
|
||||
- Der manuelle Auto-Advance startet den nächsten aktiven Clip/Wegpunkt jetzt wieder direkt.
|
||||
- `cancelManualGap()` bleibt als No-op bestehen, damit bestehende Stop-, Skip- und Tourwechsel-Pfade ohne strukturelles Risiko gleich bleiben.
|
||||
|
||||
- `app/build.gradle.kts`
|
||||
- `versionCode` von `144` auf `145` erhöht.
|
||||
- `versionName` von `2.7.38` auf `2.7.39` erhöht.
|
||||
- Changelog-Kommentar auf v2.7.39 aktualisiert.
|
||||
|
||||
- `RELEASES.md`
|
||||
- v2.7.39 als Audio-Stabilitätsfix dokumentiert.
|
||||
|
||||
## Entscheidungslogik
|
||||
|
||||
Die Pause wurde nicht als Benutzeroption beibehalten, sondern vollständig aus dem aktiven Ablauf entfernt. Dadurch gibt es keinen verzögerten Coroutine-Job mehr, der nach einem Tourwechsel, Stop/Pause oder auf Samsung-Geräten in einen veralteten Player-Zustand laufen kann. Der manuelle Modus spielt weiterhin alle aktivierten Clips und Waypoints der Reihe nach, nur ohne künstliche Pause.
|
||||
|
||||
Die GPS-Wiedergabe nutzt weiterhin ihren eigenen Pfad und wurde in diesem Fix bewusst nicht verändert.
|
||||
|
||||
## Build-Status in dieser Sandbox
|
||||
|
||||
Der Quellcode-Fix ist vorbereitet. In der aktuellen Sandbox fehlen jedoch Android-SDK/Gradle-Toolchain und der Release-Keystore, daher konnte hier keine signierte updatefähige APK gebaut werden. Für den finalen Release-Build muss die bekannte GPS2Audio-Release-Buildumgebung beziehungsweise der Keystore erneut verfügbar sein.
|
||||
|
||||
## Empfohlene Tests
|
||||
|
||||
- Manueller Tour-Play mit mehreren Waypoints und mehreren Clips pro Waypoint.
|
||||
- Manuelles Weiter/Zurück zwischen Clips und Waypoints.
|
||||
- GPS-Trigger bei aktiver GPS-Wiedergabe auf einem Samsung-Tablet.
|
||||
- Ungültige oder gelöschte Audioquelle: Fehlermeldung darf weiterhin erscheinen.
|
||||
- PTT während laufender Waypoint-Wiedergabe.
|
||||
- Atmo-Pause/Resume während Waypoint-Audio.
|
||||
+228
@@ -35,6 +35,16 @@ Release: https://gitea.nesohub.org/marcel/GPS2Audio/releases/tag/v2.1.3
|
||||
| `v2.5.36` | MapLibre Phantom-Layer-Fix | Root-Cause-Fix der weißen Phantom-GPX-Linie im neutralen Standard-Explorer (0 Wegpunkte). Ursache: `currentTrack.isNotEmpty()` in `visibleTrack` feuerte vor der `isNeutralStandard`-Prüfung, weil `loadPersistedTrackForCurrentTour` den TrackRecordingManager auch für DEFAULT_TOUR_NAME befüllte. Fix: (1) `isNeutralStandard`-Guard vor `currentTrack.isNotEmpty()` verschoben; (2) ViewModel befüllt den Manager-Track nicht mehr für die neutrale Standardtour. Alle echten Touren, laufende Aufzeichnungen und aktive Drafts unverändert. Versionscode 90. |
|
||||
| `v2.4.6` | Persistente Tourrouten | Reiner Datenmodell-/Persistenzschritt. Eine Tour kann eine dauerhaft gespeicherte Route besitzen (Start, Ziel, Via, Geometrie, Profil, Endpunkt/Quelle, Distanz/Dauer, Zeitstempel). Routenplaner kann die berechnete Route oder die Planlinie an die aktuelle Tour binden („Tourroute speichern/ersetzen/löschen"). Clipabschnitte folgen primär der frisch berechneten Route, dann der gespeicherten Tourroute, dann dem Track, zuletzt einer Planlinie — keine freien Linien. Wegpunkte bleiben Bausteine ohne fest verdrahtete Route und lassen sich später in andere Touren kopieren. Rückwärtskompatibel: bestehende JSON/Backups ohne Tourroute laden weiterhin; Backup-Export/Import nehmen Tourrouten mit. Versionscode 53. |
|
||||
|
||||
| `v2.6.2` | Route-Persistenz-Fix | Berechnete Route/Tourlinie bleibt beim Workspace-Wechsel erhalten. Root Cause: `routeLine` lebte nur im Compose-State (remember) und wurde beim Verlassen des Explorers verworfen. Fix: (1) `currentSessionRoute` StateFlow im ViewModel speichert die zuletzt berechnete Route tour-gebunden. (2) Nach erfolgreicher OSRM-Berechnung wird die Route via `setCurrentSessionRoute()` im ViewModel gesichert und gleichzeitig dauerhaft als `TourRoute` persistiert. (3) `LaunchedEffect(selectedTour)` stellt beim Zurückkehren die Route wieder her — statt des bisherigen `LaunchedEffect(selectedTour, activeTourRoute?.id)`, das beim Auto-Speichern sofort wieder zurücksetzte. (4) Planlinie und Tourverlauf-Begriffe aus v2.6.1/v2.6.0 unverändert. versionCode 105. |
|
||||
| `v2.7.0` | Tourverlauf-Editor | Neues TourverlaufEditorSheet (v2.7.0): Verlaufspunkte manuell auf der Karte setzen (Tipp-Modus oder Kartenmitte), letzten Punkt entfernen, Verlauf löschen, Verlaufspunkt als Audio-Wegpunkt erstellen. Einklappbarer Online-Routing-Abschnitt. Race-Guard (v2.7.1) gegen zu frühes Auto-Save (manualRouteInitialized). versionCode 106/107. |
|
||||
| `v2.7.2` | Tourverlauf-Persistenz-Fix | Root Cause: routePlan (Start/Via/Ziel des RoutePlannerSheet) lebte nur im lokalen Compose-State und wurde beim Workspace-Wechsel verloren. Planlinie („Planlinie zeigen“) wurde nicht als Session-Route persistiert. Fix: (1) Neuer currentSessionPlan StateFlow im ViewModel — speichert den RoutePlan tour-gebunden und überlebt Workspace-Wechsel. (2) onPlanChange → viewModel.setCurrentSessionPlan(). (3) onShowPlanline → buildTourRouteFromState() + setCurrentSessionRoute() (wie OSRM). (4) LaunchedEffect(selectedTour): restauriert Plan aus Session, kein blindes Reset. (5) Kalt-Start-Restore: activeTourRoute mit Start/Destination wird als RoutePlan wiederhergestellt. (6) manualRouteInitialized-Guard aus v2.7.1 unverändert. versionCode 108. |
|
||||
| `v2.7.9` | Explorer State Isolation | v2.7.9 — Explorer-State-Isolation: LaunchedEffect(selectedTour) im ExplorerScreenMapLibre räumt alle Waypoint-Dialog-States (actionMenuWaypoint, editDialogWaypoint, showEditDialog, deleteConfirm, tapMode) bei Tour-Wechsel sofort auf, damit keine alte Wegpunkt-Referenz in den Dialog der neuen Tour gelangen kann. Marker der Vorgänger-Tour werden explizit vor dem waypoints-Redraw-Effekt gelöscht. versionCode 115. |
|
||||
| `v2.7.10` | Waypoint-Persistence-Fix (Unbenannte Tour) | Root Cause: selectDefaultTour() setzte bei jedem Einstieg in den Explorer aus der Audio-Bibliothek/Home blind auf DEFAULT_TOUR_NAME („Standard“). War zuvor eine spontane „Unbenannte Tour“ angelegt worden, filterten Waypoints und Explorer-Karte nach „Standard“ und zeigten leere Ansicht. Fix: selectDefaultTour() prüft synchron, ob echte (nicht-Standard) Touren in _tourList vorhanden sind. Ist das der Fall, wird sofort zur ersten echten Tour gewechselt – statt auf DEFAULT zu fallen und auf den asynchronen combine-Kollektor (v2.7.5) zu warten, der den Reset dann ggf. rückabwickelte. Keine Regressionen: Lizenz, Backup, Multiclip, PTT, Atmo, Bibliothek, MapLibre, POIs, Tourverlauf, GPS-Track, Explorer-State-Isolation. versionCode 116. |
|
||||
| `v2.7.8` | Unbenannte Tour + Planlinie Auto-Render | (1) 'Spontane Tour' → 'Unbenannte Tour': UI-String explorer_spontane_tour und createSpontaneousTour()-Basis geändert, Schlüssel/Funktionsname intern beibehalten. (2) Root Cause Planlinie: savedRouteLine wurde korrekt befüllt, aber nicht in routeLine übertragen; routeLine ist die primäre Zeichnungsquelle im draw-LaunchedEffect. Bei Kaltstart und ohne Button-Druck fehlte der Transfer. Fix 1: Neues LaunchedEffect(savedRouteLine, selectedTour) überträgt savedRouteLine → routeLine, wenn routeLine leer und keine Session-Route vorhanden ist. Fix 2: Auto-Persistenz-Effekt (LaunchedEffect routePlanline) ruft zusätzlich setCurrentSessionRoute() auf, statt nur saveTourRouteFor() — füllt Session-Route auch ohne Button. Fix 3: activeTourRoute als zusätzlicher Key im Zeichen-LaunchedEffect. Keine Regressionen. versionCode 114. |
|
||||
|
||||
| `v2.7.26` | Persistent Map Preferences | Globale Kartenpräferenzen dauerhaft gespeichert: Wegpunkt-Marker, Audio-Radius/Reichweite, GPS-Track/Draft-Linien, POIs-automatisch-nachladen jetzt persistent (MapStyleStore). Bestehende Persistenz (baseStyle, showAudioLength, showAudioLines, POI-Kategorien, POI-Namen, perspectiveTilt, Standort-Folgen) unverändert erweitert. Standort-Folgen entschärft: Jede manuelle Kartenverschiebung deaktiviert Follow sofort (statt nur bei >40m Distanz). Kein Auto-Zentrieren beim nächsten GPS-Fix. Beim Öffnen des Explorers: Kamera auf Tour/Wegpunkte, nicht auf Live-Standort (sofern Follow aus). Keine Regressionen. versionCode 132. |
|
||||
| `v2.7.39` | Audio-Stabilitätsfix ohne manuelle Zwangspause | Die in v2.7.37 eingeführte feste Stille-Pause zwischen Clips/Wegpunkten im manuellen Tour-Play wurde entfernt, weil sie auf Samsung-Geräten zu uneindeutigem Player-State und der Meldung „Audio konnte nicht abgespielt werden“ führen konnte. Manueller Auto-Advance startet den nächsten aktiven Clip/Wegpunkt wieder direkt. GPS-Wiedergabe bleibt getrennt und wurde nicht verändert. Keine Änderung an Lizenz, Backup, Karten/Explorer, PTT, Atmo oder Hintergrundwiedergabe. versionCode 145. |
|
||||
|
||||
## Release-Links
|
||||
|
||||
- `v1.6.2-osm-layout`: https://gitea.nesohub.org/marcel/GPS2Audio/releases/tag/v1.6.2-osm-layout
|
||||
@@ -63,3 +73,221 @@ Die hochgeladenen Release-Pakete enthalten APK, Quellcode-ZIP und Handoff bezieh
|
||||
## Empfohlene Nutzung
|
||||
|
||||
Für neue Nutzer ist `v2.1.3` der beste Einstieg. Ältere Versionen bleiben als Archiv erhalten, damit Entwicklungsschritte, Fehlerbehebungen und Zwischenstände nachvollziehbar bleiben.
|
||||
|
||||
## v2.7.17 — Route-first Explorer (2026)
|
||||
|
||||
**GPS2Audio v2.7.17** führt den konservativen UI-/Bedienlogik-Umbau „Route-first Explorer" ein.
|
||||
|
||||
### Was ist neu
|
||||
- **Route zuerst**: Der mentale Ablauf ist jetzt „Route planen → Tourpunkte setzen → Audio hinzufügen".
|
||||
- **Tourpunkt-Konzept**: Verlaufspunkte werden als „Tourpunkte" bezeichnet. Ein Tourpunkt kann zunächst nur die Route formen; Audio wird bewusst nachträglich hinzugefügt.
|
||||
- **Klarere Labels**: „Route planen" statt „Tourverlauf planen", „Routenlinie anzeigen" statt „Planlinie zeigen", „Audio hier hinzufügen" statt technischem „Als Audio-Wegpunkt erstellen".
|
||||
- **Route bearbeiten**: Der Tourverlauf-Editor heißt nun „Route bearbeiten" – klarer, weniger abschreckend.
|
||||
- **Abwärtskompatibel**: Bestehende Audio-Wegpunkte und Verlaufslinien aus v2.7.16 bleiben vollständig erhalten.
|
||||
|
||||
### Geschützte Funktionen
|
||||
Keine Änderungen an: MapLibre-Rendering, Waypoint-Rendering, routeRenderKey/waypointRenderKey/overlayRenderNonce, Planlinie-Autorender, Explorer-Kontext-Isolation, POI-Autoload, Kartenstile, Lizenzsystem, Backup/Restore, Multiclip, manuelle Wiedergabe, GPS-Aufzeichnung.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 123
|
||||
- versionName: 2.7.17
|
||||
|
||||
## v2.7.18 — Unified Tour Layers (2026)
|
||||
|
||||
**GPS2Audio v2.7.18** konsolidiert die Karten-Darstellung zu einem einheitlichen Tour-System.
|
||||
|
||||
### Was ist neu
|
||||
- **Einheitliche Tourlinie**: Routing, manuelles Zeichnen und gespeicherte Tourlinie wirken jetzt als ein System. Eine manuell gezeichnete Route wird im gespeicherten Zustand als grüne Tourlinie dargestellt (wie eine OSRM-berechnete Route) — keine verwirrende Doppellinie mehr.
|
||||
- **Bearbeitungs-Vorschau nur im Zeichenmodus**: Die violette Vorschaulinie erscheint nur noch, während der Nutzer aktiv Verlaufspunkte zeichnet (manualRouteMode=true). Im Normalbetrieb ist die Karte übersichtlicher.
|
||||
- **Verlaufspunkt-Marker reduziert**: Kleine Punkt-Marker für Verlaufspunkte erscheinen ebenfalls nur im Bearbeitungsmodus — reduziert visuelles Rauschen im Explorer.
|
||||
- **Planungs-Vorschau bleibt erkennbar**: Die orange/gedimmte Planlinie (Routing-Werkzeug) bleibt visuell als Werkzeug-Vorschau erkennbar.
|
||||
- **Abwärtskompatibel**: Kein Datenmodell-Umbau, bestehende Routen/Wegpunkte unverändert.
|
||||
|
||||
### Geschützte Funktionen
|
||||
Keine Änderungen an: experimentellem Routing/Route berechnen, Route planen, Routenlinie anzeigen, Route bearbeiten/manuelles Zeichnen, Audio-Waypoints, Multiclip, manuelle Wiedergabe, PTT, Atmo, MapLibre Initial-Render-Fixes, Planlinie-Autorender, routeRenderKey/waypointRenderKey/overlayRenderNonce, Explorer-Kontext-Isolation, POI-Autoload, Kartenstile, Lizenzsystem, Backup/Restore.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 124
|
||||
- versionName: 2.7.18
|
||||
|
||||
## v2.7.19 — Tourplaner Integration Fix (2026)
|
||||
|
||||
**GPS2Audio v2.7.19** behebt den Verlust von Start-, Ziel- und Via-Punkten im Tourplaner und integriert das manuelle Zeichnen als Werkzeug des Tourplaners.
|
||||
|
||||
### Was ist neu
|
||||
- **Fix: Start/Ziel/Via dauerhaft gespeichert**: Der Tourplaner schreibt den Routenplan jetzt sofort in die Datenbank, sobald Start und Ziel gesetzt sind. Bisher wurde nur die In-Memory-Session gespeichert — nach einem Prozess-Kill (App-Neustart) war der Plan verloren. Jetzt bleibt er auch nach Neustart vollständig erhalten.
|
||||
- **Manuelles Zeichnen im Tourplaner**: Die Schaltfläche „Zeichenmodus öffnen“ ist jetzt direkt im Tourplaner (RoutePlannerSheet) eingebettet. Routing und manuelles Zeichnen sind gleichwertige Bearbeitungsmethoden im selben Planer.
|
||||
- **Kein Rückschritt beim Doppellinien-Fix**: Die violette Bearbeitungs-Vorschau erscheint weiterhin nur im aktiven Zeichenmodus (wie in v2.7.18).
|
||||
- **Abwärtskompatibel**: Kein Datenmodell-Umbau, bestehende Routen/Wegpunkte unverändert.
|
||||
|
||||
### Geschützte Funktionen
|
||||
Keine Änderungen an: experimentellem Routing/Route berechnen, Routenlinie anzeigen, Route bearbeiten/manuelles Zeichnen, Audio-Waypoints, Multiclip, manuelle Wiedergabe, PTT, Atmo, MapLibre Initial-Render-Fixes, Planlinie-Autorender, routeRenderKey/waypointRenderKey/overlayRenderNonce, Explorer-Kontext-Isolation, POI-Autoload, Kartenstile, Lizenzsystem, Backup/Restore.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 125
|
||||
- versionName: 2.7.19
|
||||
|
||||
## v2.7.20 — Flexible Route Shaping (2026)
|
||||
|
||||
**GPS2Audio v2.7.20** führt flexible Routenformung per Karten-Tap ein.
|
||||
|
||||
### Was ist neu
|
||||
- **AddViaPoint-Modus**: Neuer TapMode für direkte Zwischenpunkt-Setzung per Karten-Tap aus dem Routenplaner heraus.
|
||||
- **Intuitiver Fallback**: 1. Tap = Start, 2. Tap = Ziel, weitere Taps = Zwischenpunkte.
|
||||
- **Via-Sektion**: Im Tourplaner klar als „Zwischenpunkte (Route formen)" bezeichnet.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 126
|
||||
- versionName: 2.7.20
|
||||
|
||||
## v2.7.21 — Waypoint Preservation Fix (2026)
|
||||
|
||||
**GPS2Audio v2.7.21** behebt kritischen Datenverlust: Audio-Wegpunkte innerhalb einer Tour wurden beim Öffnen des Routenplaners unter bestimmten Bedingungen gelöscht.
|
||||
|
||||
### Root Cause
|
||||
Race-Condition in `duplicateTour()`, `deleteTour()` und `renameTour()`: Diese Funktionen lasen `_waypoints.value` aus dem In-Memory-State (MutableStateFlow). Wenn der DataStore-Erstladevorgang noch nicht abgeschlossen war, enthielt `_waypoints.value` noch die initiale leere Liste. Ein `saveAll(emptyList() + newWps)` überschrieb dann alle bestehenden Wegpunkte aller Touren.
|
||||
|
||||
Zusätzlich: Kein globaler Schutz vor einem `saveAll([])` bei nicht-leerer DataStore-Wegpunktliste.
|
||||
|
||||
### Fixes
|
||||
1. **`WaypointStore.saveAll()` Preservation-Guard**: Wenn die übergebene Liste leer ist, aber die DataStore-Wegpunktliste nicht-leer ist, wird der Schreibvorgang blockiert (atomarer Check in `dataStore.edit`). Verhindert versehentlichen Totalverlust.
|
||||
2. **`WaypointStore.saveAllForced()`**: Neue Methode für explizite Lösch-/Restore-Operationen, die den Preservation-Guard umgehen dürfen.
|
||||
3. **Race-Condition-Fix in `duplicateTour`/`deleteTour`/`renameTour`**: Lesen jetzt mit `repository.waypoints.first()` direkt aus DataStore statt aus dem möglicherweise noch leeren `_waypoints.value`.
|
||||
4. **AddViaPoint-Pfad (v2.7.20) bestätigt sicher**: Kein Waypoint-Speicherpfad wird durch Route-Planen, AddViaPoint oder Planlinie-Auto-Persistenz ausgelöst.
|
||||
5. **Architektonische Invariante dokumentiert**: `saveTourRoute`/`saveTourRouteFor` schreiben ausschließlich in `KEY_TOUR_ROUTES`, nie in `KEY_WAYPOINTS`.
|
||||
|
||||
### Geschützte Funktionen
|
||||
Alle Funktionen aus v2.7.20 unverändert: AddViaPoint, flexible Routenformung, v2.7.19 Start/Via/Ziel-Persistenz, v2.7.18 Doppellinie-Fix, Audio-Waypoints, Multiclip, manuelle Wiedergabe, PTT, Atmo, MapLibre-Render-Fixes, Planlinie-Autorender, routeRenderKey/waypointRenderKey/overlayRenderNonce, POI-Autoload, Kartenstile, Lizenzsystem, Backup/Restore.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 127
|
||||
- versionName: 2.7.21
|
||||
|
||||
---
|
||||
|
||||
## v2.7.22 — Saved Route Render Fix
|
||||
|
||||
**Problem**: Ein gespeicherter Tourverlauf (manuell gezeichnet, Quelle MANUAL) war im RoutePlannerSheet sichtbar (Karte mit Buttons "Tourverlauf ersetzen/löschen"), wurde aber visuell nicht als Linie auf der Karte angezeigt. Zusätzlich zeigte der Tourplaner "Noch keine Route. Routenlinie anzeigen genügt — Online-Routing ist optional."
|
||||
|
||||
**Root Cause**: `savedRouteLine` (die Hilfsliste, aus der die Kartenrenderschicht die grüne Tourlinie zeichnet) schloss MANUAL-Routen explizit mit `emptyList()` aus (Zeile 290, v2.5.3-Kommentar). Die ursprüngliche Intention war, Doppellinien zu verhindern (v2.7.18). Aber v2.7.18 hat die Bearbeitungs-Vorschau (manualRoutePolylines, violett) bereits auf `manualRouteMode == true` beschränkt. Damit war der Ausschluss in `savedRouteLine` überflüssig und verhinderte die korrekte Darstellung.
|
||||
|
||||
**Fixes (ExplorerScreenMapLibre.kt)**:
|
||||
1. **Fix 1** — `savedRouteLine`-Berechnung: MANUAL nicht mehr pauschal mit `emptyList()` ausgeschlossen. Stattdessen gleiches Verhalten wie OSRM: Route wird als Punkte zurückgegeben, Phantom-Guard auf Standardtour bleibt erhalten.
|
||||
2. **Fix 2** — `LaunchedEffect(savedRouteLine)`: MANUAL-Quellen überspringen (wie PLANLINE), damit MANUAL via `hasSavedRoute`-Pfad grün gezeichnet wird (nicht blau als OSRM-Route). Kein `routeLine`-Load für MANUAL.
|
||||
3. **Fix 3** — RoutePlannerSheet-Aufruf: `hasRoute = routeLine.size >= 2 || savedRouteLine.size >= 2` — "Noch keine Route" erscheint nicht mehr, wenn eine gespeicherte MANUAL- oder OSRM-TourRoute vorhanden ist.
|
||||
|
||||
### Geschützte Funktionen
|
||||
Alle Funktionen aus v2.7.21 unverändert: Waypoint-Preservation-Guard, AddViaPoint, flexible Routenformung, v2.7.19 Start/Via/Ziel-Persistenz, v2.7.18 Doppellinie-Fix (manualRouteMode-Guard bleibt), Audio-Waypoints, Multiclip, manuelle Wiedergabe, PTT, Atmo, MapLibre-Render-Fixes, Planlinie-Autorender, routeRenderKey/waypointRenderKey/overlayRenderNonce, POI-Autoload, Kartenstile, Lizenzsystem, Backup/Restore.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 128
|
||||
- versionName: 2.7.22
|
||||
|
||||
---
|
||||
|
||||
## v2.7.23 — Route Planner Flow Fix
|
||||
|
||||
**Problem**: Der Routenplaner zeigte eine violette/orange Planlinie auf der Karte, aber diese konnte nicht sinnvoll als Tourverlauf gespeichert oder mit dem Tourverlauf verknüpft werden. Audio-Wegpunkte ließen sich zwar als Start/Ziel/Via wählen, aber der Weg von der Planlinie zum persistierten Tourverlauf war unklar.
|
||||
|
||||
**Root Cause**: Es fehlte ein direkt zugänglicher, klar benannter "Als Tourverlauf übernehmen"-Button in der Sticky Action Bar des Tourplaners. Die vorhandene `TourRouteStatusCard` im scrollbaren Bereich war nicht sichtbar genug, und die Buttons "Speichern"/"Ersetzen" dort waren bei bestehenden Routen in der Karte verborgen. Zusätzlich nutzte `onShowPlanline` und `onSaveTourRoute` `routePlanline` aus der Compose-Remember-Chain, ohne einen Inline-Fallback für den Fall, dass die Recomposition noch nicht stattgefunden hatte.
|
||||
|
||||
**Fixes**:
|
||||
1. **Neuer "Als Tourverlauf übernehmen"-Button** in der Sticky Action Bar (neben "Routenlinie anzeigen"). Aktiviert wenn `hasPlanline || hasRoute`. Speichert direkt — kein zusätzliches Scrolling nötig.
|
||||
2. **`onAdoptAsTourRoute`-Callback** in ExplorerScreenMapLibre: Wählt OSRM-Route (wenn vorhanden) oder Planlinie als TourRoute-Quelle, schreibt in DB und Session-Route, zeigt Statusmeldung.
|
||||
3. **Inline Planline-Fallback**: `onShowPlanline`, `onSaveTourRoute`, `onReplaceTourRoute` berechnen die Planline-Koordinaten inline aus `routePlan.orderedStops()` als Fallback, falls `routePlanline` (remember-computed) noch leer ist.
|
||||
4. **setCurrentSessionRoute zusätzlich bei Save/Replace**: Session-Route wird nach Speichern auch im ViewModel gesetzt, damit Workspace-Wechsel sofort die neue Route kennt.
|
||||
5. **TourRouteStatusCard-Hinweis**: Zeigt proaktiv `route_planner_adopt_planline_hint` wenn Planlinie vorhanden aber noch kein gespeicherter Verlauf.
|
||||
|
||||
### Audio-Wegpunkte als Planpunkte
|
||||
- Auswahl eines Audio-Wegpunkts für Start/Ziel/Via: unverändert über WaypointPickerDialog → applyStopToRole() → onPlanChange().
|
||||
- Keine Waypoint-Löschung, keine Duplikation (Waypoint-Preservation-Guard aus v2.7.21 bleibt aktiv).
|
||||
- Nach Auswahl: routePlanline auto-computed, hasPlanline=true, Buttons aktiv.
|
||||
|
||||
### Geschützte Funktionen
|
||||
Alle Funktionen aus v2.7.22 unverändert: Saved Route Render Fix, Waypoint-Preservation-Guard, AddViaPoint, flexible Routenformung, v2.7.19 Start/Via/Ziel-Persistenz, v2.7.18 Doppellinie-Fix, Audio-Waypoints, Multiclip, manuelle Wiedergabe, PTT, Atmo, MapLibre-Render-Fixes, Planlinie-Autorender, routeRenderKey/waypointRenderKey/overlayRenderNonce, POI-Autoload, Kartenstile, Lizenzsystem, Backup/Restore.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 129
|
||||
- versionName: 2.7.23
|
||||
|
||||
---
|
||||
|
||||
## v2.7.24 — Tourverlauf UX Polish
|
||||
|
||||
Tourverlauf-UX-Polish: Verbesserte Nutzererfahrung beim Verwalten gespeicherter Tourverlauf-Linien im Explorer. Keine neue Funktionalität — nur UX-Feinschliff und Code-Qualität.
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 130
|
||||
- versionName: 2.7.24
|
||||
|
||||
---
|
||||
|
||||
## v2.7.25 — Tourline Initial Render Fix
|
||||
|
||||
**Problem**: Die gespeicherte Tourlinie erschien beim ersten Öffnen des Explorers nicht sofort auf der Karte. Erst eine spätere UI-Interaktion (z.B. Abspielen eines Clips) löste den Render aus.
|
||||
|
||||
**Root Cause**: Zwei zusammenwirkende Timing-Probleme:
|
||||
|
||||
1. **Doppelter Style-Load**: `LaunchedEffect(layerState.baseStyle, mapStyleSettings.perspectiveTilt, styleReady)` enthielt `styleReady` als Key. Wenn der initiale Style-Load (im `AndroidView` factory Callback) `styleReady=true` setzte, feuerte dieser Effect erneut und rief `ml.setStyle()` ein zweites Mal auf. Das wischte alle bereits gezeichneten Polylines weg und trigerred einen zweiten asynchronen Style-Load.
|
||||
|
||||
2. **Late activeTourRoute**: `activeTourRoute` wird asynchron aus der Datenbank geladen (via `StateFlow` im ViewModel). Kam der zweite Style-Load vor dem DB-Load ab, war `savedRouteLine` noch leer. Der `overlayRenderNonce`-Mechanismus hatte bereits gefeuert, bevor die Route verfügbar war. Obwohl `savedRouteLine` als Key im Overlay-`LaunchedEffect` gelistet ist, konnten Compose-Snapshot-Caching-Effekte das erneute Feuern verhindern.
|
||||
|
||||
**Fixes**:
|
||||
1. **`styleReady` aus Style-Change-LaunchedEffect-Keys entfernt** (v2.7.25): Verhindert den Doppel-Reload. Der Effect feuert jetzt nur noch bei echten `baseStyle`- oder `perspectiveTilt`-Änderungen.
|
||||
2. **Dedizierter `LaunchedEffect(savedRouteLine.size, styleReady, mapLibreMap)`** (v2.7.25): Feuert wenn `savedRouteLine` mindestens 2 Punkte hat UND Style bereit UND Map initialisiert. Inkrementiert `overlayRenderNonce`, sodass der Zeichnungseffekt garantiert neu startet — unabhängig davon, wann `activeTourRoute` aus der DB ankommt.
|
||||
|
||||
**Nicht geändert**:
|
||||
- Speicherlogik (keine Änderungen an `saveTourRouteFor`, `activeTourRoute`, `_tourRoutes`)
|
||||
- v2.7.24 Tourverlauf UX Polish
|
||||
- v2.7.23 'Als Tourverlauf übernehmen' Flow Fix
|
||||
- v2.7.22 Saved Route Render Fix
|
||||
- v2.7.21 Waypoint Preservation Guard
|
||||
- v2.7.18 Doppellinie-Fix (manualRouteMode-Guard)
|
||||
- Alle anderen Funktionen (AddViaPoint, OSRM, PLANLINE, MANUAL, Audio-Waypoints, PTT, Atmo, Backup, Lizenzsystem)
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 131
|
||||
- versionName: 2.7.25
|
||||
|
||||
---
|
||||
|
||||
## v2.7.30 — MapLibre Overlay Rebuild Fix
|
||||
|
||||
**Problem (Nutzerbericht nach v2.7.29)**: "nein das hat nichts verändert. Das ist der Live Standort und die Waypoints bzw. Planlinie wird nicht angezeigt." Screenshot zeigte nur einen hellen/weißen Kreis als Live-Standort, keine Waypoint-Marker, keine Planlinie.
|
||||
|
||||
**Root-Cause-Analyse (warum v2.7.29 nicht wirkte)**:
|
||||
|
||||
1. **yield() zu kurz**: Das einzige `yield()` (Wartedauer: 1 Frame ≈ 16ms) nach `styleReady` in den Overlay-LaunchedEffects war nicht ausreichend für MapLibres GL-Thread und AnnotationManager-Initialisierung nach `setStyle()`. `addMarker()` und `addPolyline()` schlugen still fehl — keine Exception, aber Rückgabewert null/leer.
|
||||
|
||||
2. **Kein Retry-Mechanismus**: Wenn der erste Render-Versuch (150ms nach Style-Load) scheiterte, gab es keinen weiteren Versuch. Nur Nutzerinteraktion (State-Änderung) löste einen neuen Render aus.
|
||||
|
||||
3. **runCatching fehlte**: Fehlschlagende `addMarker()`/`addPolyline()`-Aufrufe warfen keine Exception in die Coroutine, aber gaben null zurück. Ohne `runCatching` konnte ein unerwarteter Fehler die Coroutine abbrechen und alle nachfolgenden Zeichenoperationen verhindern.
|
||||
|
||||
4. **clearAllAnnotationsCaches Lücke**: `planlinePolylines` und `routeStopMarkers` wurden beim Style-Cleanup (vor `setStyle()`) nicht geleert — veraltete Referenzen blieben im Cache und führten zu nutzlosen `removePolyline()`-Aufrufen auf ungültigen Objekten.
|
||||
|
||||
5. **v2.7.29 Puck-Fix: Early-Return-Problem**: `lastLat ?: return@LaunchedEffect` vor `yield()` bedeutete: Hatte der Nutzer beim Style-Load noch keinen GPS-Fix, blieb der Puck dauerhaft leer (bis zum nächsten GPS-Fix). Wurde der Fix vor GPS-Empfang aufgerufen, war `lastLat` null → sofortiger Return → kein Puck gezeichnet.
|
||||
|
||||
**Fixes v2.7.30**:
|
||||
|
||||
1. **delay(150ms) statt yield()** in beiden Overlay-LaunchedEffects (Waypoints/Tracks + Routen/Planline): Gibt dem AnnotationManager ausreichend Zeit nach Style-Load.
|
||||
|
||||
2. **Zentraler Retry-Mechanismus (500ms)**: `LaunchedEffect(styleReady, mapLibreMap)` wartet 500ms nach jedem Style-Ready und inkrementiert `overlayRenderNonce` ein zweites Mal. Ergebnis: Zwei Render-Versuche — bei 150ms und bei 650ms nach Style-Load. Der zweite ist garantiert erfolgreich, da der AnnotationManager nach 500ms sicher bereit ist.
|
||||
|
||||
3. **delay(200ms) + runCatching im Puck-Rehydrate-LaunchedEffect**: Puck-Marker wird robuster gezeichnet, Fehler werden abgefangen.
|
||||
|
||||
4. **runCatching um alle addMarker/addPolyline-Aufrufe**: In `drawExplorerOverlaysMapLibre` und der Route-LaunchedEffect — verhindert silent-Crashes und gibt dem 500ms-Retry die Chance, alle fehlgeschlagenen Overlays nachzuzeichnen.
|
||||
|
||||
5. **clearAllAnnotationsCaches erweitert**: `planlinePolylines` und `routeStopMarkers` werden jetzt ebenfalls beim Style-Cleanup geleert (neue optionale Parameter).
|
||||
|
||||
**Nicht geändert**:
|
||||
- v2.7.28 Map Style Apply Fix (appliedBaseStyle/appliedPerspectiveTilt guard)
|
||||
- v2.7.27 Kartenpräferenz-Persistenz
|
||||
- v2.7.26 Standort-Folgen-Fix
|
||||
- v2.7.25 Initial-Render-Fix (savedRouteLine.size LaunchedEffect)
|
||||
- v2.7.24 UX Polish, v2.7.23 Tourverlauf übernehmen
|
||||
- v2.7.21 Waypoint Preservation Guard
|
||||
- Alle Funktionen: Routenplanung, POIs, Kartenstile, Audio-Waypoints, Multiclip, PTT, Atmo, Lizenzsystem, Backup/Restore
|
||||
|
||||
### Build-Details
|
||||
- versionCode: 136
|
||||
- versionName: 2.7.30
|
||||
|
||||
+289
-2
@@ -157,8 +157,295 @@ android {
|
||||
// • Keine Regressionen: Phantom-Layer-Fix, Lizenzsystem, Multi-Clip,
|
||||
// PTT, Tour-Zähler, Backup/Cover, GPX-Import, Explorer/Routing,
|
||||
// Tour-/Waypoint-Kopien, Hintergrundwiedergabe.
|
||||
versionCode = 102
|
||||
versionName = "2.5.48"
|
||||
// v2.6.0 — Tourverlauf/Storyboard-Konsolidierung:
|
||||
// • Routenplaner → "Tourverlauf" (deutsch, klar, kein Auto-Navi-Gefühl).
|
||||
// • Storyboard-Panel übersichtlicher beschriftet und mit Erklärtext versehen.
|
||||
// • Routepunkt vs. Wegpunkt (Audio) klar unterschieden in Labels.
|
||||
// • Online-Routing als optional/sekundär markiert, nicht ausgebaut.
|
||||
// • Keine Änderung an Logik, Backup, Lizenz, Playback, GPS-Aufzeichnung.
|
||||
// v2.6.1 — Waypoint-Dialog-Polish: Formatierungsfehler RoutePlannerSheet
|
||||
// behoben, WaypointEditDialog neu gestaltet (freundlichere Cards).
|
||||
// Keine Logik-/Datenmodell-Änderungen.
|
||||
// v2.7.0 — Tourverlauf-Editor:
|
||||
// • Neues TourverlaufEditorSheet als dedizierter Bearbeitungsmodus.
|
||||
// • Verlaufspunkte über Kartenmitte oder Tipp-Modus setzen.
|
||||
// • "Als Audio-Wegpunkt erstellen" aus Verlaufspunkt heraus.
|
||||
// • Online-Routing in RoutePlannerSheet sekundär (OutlinedButton).
|
||||
// • Status-Bar zeigt Verlaufspunkt-Anzahl im Editor-Modus.
|
||||
// • Persistenz/Speicherung über bestehende TourRoute/MANUAL-Infrastruktur.
|
||||
// • Keine Regressionen: Playback, Backup, Lizenz, GPS-Aufzeichnung unverändert.
|
||||
// v2.7.1 — Tourverlauf-Linie-Fix:
|
||||
// • Behebt: Manuelle Tourverlauf-Linie wurde dauerhaft nicht angezeigt bzw.
|
||||
// verschwand nach Öffnen/Schließen des Editors, Workspace-Wechsel oder
|
||||
// Kartenstil-Reload.
|
||||
// • Root Cause 1 (Race-Condition): Beim initialen Laden der Composable feuerte
|
||||
// der Auto-Save-LaunchedEffect mit leerem manualRoutePoints, bevor der
|
||||
// Load-Effekt die Punkte aus der DB geladen hatte. Bei leerem Liste UND
|
||||
// wasManual=true wurde deleteTourRouteForCurrent() aufgerufen — die
|
||||
// gespeicherte Route wurde gelöscht. Fix: manualRouteInitialized-Guard.
|
||||
// • Root Cause 2 (Zeichnungs-LaunchedEffect verpasst Map-Init):
|
||||
// mapLibreMap als Key im Drawing-LaunchedEffect fehlte. Wenn die Karte nach
|
||||
// einem Workspace-Wechsel asynchron initialisiert wurde, feuerte das Effect
|
||||
// nicht erneut (manualRoutePoints unverändert). Fix: mapLibreMap als
|
||||
// zusätzlicher Key.
|
||||
// • Root Cause 3 (Load-Effect-Keys zu breit):
|
||||
// activeTourRoute?.id im Load-Effect führte dazu, dass jedes Auto-Save
|
||||
// (neue updatedAtMillis) den Load neu auslöste. Fix: nur source als Key,
|
||||
// kein id.
|
||||
// • Verbesserung: Strichstärke der manuellen Tourverlauf-Linie erhöht
|
||||
// (10/7 statt 8/5) für bessere Sichtbarkeit.
|
||||
// • Keine Regressionen: v2.7.0 Editor, v2.6.2 Route-Persistenz, v2.6.1
|
||||
// Wegpunktdialog, v2.5.48 TrackDraft-Backup, POI, 3D, Playback, Backup,
|
||||
// Lizenzsystem unverändert.
|
||||
// v2.7.3 — Planlinie Auto-Show-Fix:
|
||||
// • Root Cause: "Planlinie zeigen" persistierte die Planroute nur in die
|
||||
// ViewModel-Session (RAM), nicht in die DB. Nach App-Neustart (Prozess-Kill)
|
||||
// war kein activeTourRoute-Eintrag vorhanden, savedRouteLine blieb leer →
|
||||
// keine Linie sichtbar, Button erneut nötig.
|
||||
// • Fix 1 (Persistenz): onShowPlanline ruft jetzt zusätzlich
|
||||
// viewModel.saveTourRouteFor() auf. PLANLINE wird in der DB gespeichert.
|
||||
// activeTourRoute.polyline ist ab dem nächsten App-Start verfügbar.
|
||||
// • Fix 2 (Zeichnungs-Priorität): savedRouteLine hat jetzt Vorrang vor
|
||||
// routePlanline. routeLine > savedRouteLine > routePlanline. Doppelte
|
||||
// Überlagerung (PLANLINE-Quelle) beseitigt.
|
||||
// • Fix 3 (Clear-Symmetrie): onClear löscht die DB-PLANLINE-Route, wenn
|
||||
// der Nutzer explizit den Planer leert. OSRM/MANUAL bleiben unangetastet.
|
||||
// • Keine Änderungen an Datenmodell, Backup, Playback, Lizenz.
|
||||
// v2.7.4 — Planlinie Auto Render:
|
||||
// • Root Cause 1 behoben: savedRouteLine-Phantom-Guard schloss PLANLINE-Routen auf
|
||||
// der Standardtour aus (isNeutralStandard). PLANLINE ist bewusste Nutzeraktion,
|
||||
// kein Phantom — Ausnahme eingefügt.
|
||||
// • Root Cause 2 behoben: mapLibreMap fehlte als Key im route-drawing-LaunchedEffect.
|
||||
// Nach Map-Neuinitialisierung (Stylewechsel, Workspace-Rückkehr) wurde die Linie
|
||||
// nicht neu gezeichnet, obwohl routePlanline befüllt war.
|
||||
// • Auto-Persistenz: routePlanline ≥ 2 Stopps → automatisch als PLANLINE-TourRoute
|
||||
// in DB gespeichert, ohne Button-Druck. Button „Planlinie zeigen“ ist nur noch
|
||||
// Kamera-Fokus.
|
||||
// • Zeichnungsfarbe: PLANLINE aus savedRouteLine erhält orangene Farbe (wie routePlanline),
|
||||
// kein falsches Grün mehr.
|
||||
// • Priorität v2.7.4: routeLine (OSRM) > routePlanline (wenn PLANLINE-Quelle) >
|
||||
// savedRouteLine (OSRM/andere) > routePlanline (fallback) > keine Linie.
|
||||
// • Keine Änderungen an Datenmodell, Backup, Playback, Lizenz.
|
||||
//
|
||||
// v2.7.5 — Standard-Tour intern (Konzeptkorrektur):
|
||||
// • Root Cause: DEFAULT_TOUR_NAME="Standard" war bisher in der Audio-Bibliothek
|
||||
// als vollwertige bearbeitbare Tour sichtbar, was Phantom-/Kontextprobleme
|
||||
// erzeugen konnte. Standard-Tour ist eine System-Blaupause, kein Nutzerdatensatz.
|
||||
// • visibleTourList (neu im ViewModel): filtert Standard-Tour aus der UI-Liste,
|
||||
// sobald mindestens eine echte Tour existiert. tourList intern unverändert.
|
||||
// • Audio-Bibliothek (HomeScreen): zeigt Standard-Tour nicht mehr im Cover-Grid.
|
||||
// • Menü (WaypointListScreen): Umbenennen + Duplizieren für Standard-Tour deaktiviert.
|
||||
// Löschen war bereits gesperrt (unverändert).
|
||||
// • Auto-Selektion: Beim App-Start mit echten Touren wird automatisch die erste
|
||||
// echte Tour gewählt statt "Standard".
|
||||
// • Explorer isNeutralStandard, Backup, Datenmodell, Lizenz: unverändert.
|
||||
// v2.7.7 — Spontane Tour Autocreate:
|
||||
// • Root Cause: ExplorerScreenMapLibre.kt zeigte in der Topbar weiterhin
|
||||
// "Standard" (selectedTour direkt), da die UI-Entkopplung aus v2.7.6
|
||||
// (displayTourLabel) dort nicht übernommen wurde. Fix: displayTourLabel
|
||||
// analog zu ExplorerScreen.kt implementiert.
|
||||
// • Automatische Tourerstellung beim ersten Wegpunkt im neutralen Explorer:
|
||||
// upsertFromNeutralExplorer() im ViewModel prüft, ob tourName ==
|
||||
// DEFAULT_TOUR_NAME. Falls ja, erzeugt createSpontaneousTour() eine
|
||||
// eindeutige "Spontane Tour" (mit Suffix falls Name existiert), ruft
|
||||
// addTour() auf (inkl. selectedTour-Wechsel) und speichert den Wegpunkt
|
||||
// in der neuen Tour. Keine DB-Migration, keine neue Tabelle.
|
||||
// • Betroffen: neuer Wegpunkt per Tap (PlaceNew), POI als Wegpunkt,
|
||||
// POI zu Storyboard, Explorer-POI-Karte. Bearbeiten bestehender
|
||||
// Wegpunkte: normaler upsert-Pfad (keine Auto-Tour).
|
||||
// • Standard-Tour bleibt intern; visibleTourList (v2.7.5) blendet
|
||||
// sie weiterhin aus, sobald echte Touren existieren.
|
||||
// • Keine Änderungen an Backup, Lizenz, Playback, PTT, Atmo, GPS-Track.
|
||||
// v2.7.8 — Unbenannte Tour + Planlinie Auto-Render:
|
||||
// • 'Spontane Tour' → 'Unbenannte Tour': UI-String explorer_spontane_tour
|
||||
// und createSpontaneousTour()-Basisname geändert. Schlüssel bleibt für
|
||||
// risikoarmes Deployment; Funktionsname intern unverändert.
|
||||
// • Root Cause Planlinie: Beim ersten Rendern nach App-Neustart oder
|
||||
// Workspace-Rückkehr war savedRouteLine befüllt, wurde aber nicht in
|
||||
// routeLine übertragen. routeLine ist die primäre Zeichnungsquelle.
|
||||
// Folge: LaunchedEffect-Zeichnung lief, zog aber savedRouteLine nur
|
||||
// als sekundären Fallback (grüne Linie). Die sichtbarkeitsblockierende
|
||||
// Hürde: manuelle Sitzung (Session-Route) fehlte bei Kaltstart.
|
||||
// • Fix 1: Neues LaunchedEffect(savedRouteLine, selectedTour) überträgt
|
||||
// savedRouteLine → routeLine, wenn routeLine leer und keine Session-Route
|
||||
// vorhanden. Damit ist routeLine immer die aktive Zeichnungsquelle.
|
||||
// • Fix 2: Auto-Persistenz (LaunchedEffect routePlanline) ruft nun
|
||||
// viewModel.setCurrentSessionRoute() auf, statt nur saveTourRouteFor().
|
||||
// Das füllt currentSessionRoute für Workspace-Rückkehr ohne Button-Druck.
|
||||
// • Fix 3: activeTourRoute als zusätzlicher Key im Zeichen-LaunchedEffect
|
||||
// (belt-and-suspenders gegen strukturgleiche List-Vergleiche).
|
||||
// • Keine Regressionen: Lizenz, Backup, Multiclip, PTT, Atmo, Bibliothek,
|
||||
// MapLibre, POIs, Tourverlauf, GPS-Track, Spontane-Tour-Autocreate.
|
||||
// v2.7.10 — Waypoint-Persistence-Fix (Unbenannte Tour bleibt nach Verlassen der Karte):
|
||||
// • Root Cause: selectDefaultTour() wurde bei jedem Einstieg in den Explorer
|
||||
// aus der Audio-Bibliothek/Home blind auf DEFAULT_TOUR_NAME („Standard“)
|
||||
// gesetzt. Wenn zuvor eine spontane „Unbenannte Tour“ angelegt worden war,
|
||||
// filterten Waypoints und Explorer-Karte nach „Standard“ → leere Ansicht.
|
||||
// • Fix: selectDefaultTour() prüft jetzt vor dem Reset, ob echte (nicht-Standard)
|
||||
// Touren in _tourList vorhanden sind. Ist das der Fall, wird sofort synchron
|
||||
// zur ersten echten Tour gewechselt – statt auf DEFAULT zu fallen und auf den
|
||||
// asynchronen combine-Kollektor (v2.7.5) zu warten, der den Reset dann ggf.
|
||||
// rückabwickelt. Das vermeidet die Race-Condition, bei der die Explorer-UI
|
||||
// in einem Recomposition-Zyklus mit leerem Waypoint-Filter gerendert wurde.
|
||||
// • Keine Regressionen: Lizenz, Backup, Multiclip, PTT, Atmo, Bibliothek,
|
||||
// MapLibre, POIs, Tourverlauf, GPS-Track, Explorer-State-Isolation (v2.7.9).
|
||||
// Bestehende echte Touren werden beim Explorer-Öffnen korrekt vorausgewählt
|
||||
// (war bereits so via async combine-Autoswitch – jetzt deterministisch/synchron).
|
||||
// v2.7.14 — Explorer Entry Context Fix (Navigations-Bug):
|
||||
// • Bug: Übersicht → Explorer öffnete nicht neutral, sondern die zuletzt
|
||||
// bekannte echte Tour. Grund: selectDefaultTour() wählte bei vorhandenen
|
||||
// echten Touren die erste echte Tour statt DEFAULT_TOUR_NAME.
|
||||
// • Fix: Neue Funktion selectNeutralTourForNewExplorer() mit _neutralExplorerMode-
|
||||
// Flag verhindert den automatischen Upgrade durch den combine-Kollektor (v2.7.5)
|
||||
// und erzwingt bewusst einen leeren Explorer-Kontext aus der Übersicht.
|
||||
// • Touransicht → Explorer: bleibt unverändert bei der aktiven Tour.
|
||||
// • selectDefaultTour() für Legacy-Pfade erhalten. Keine Regressionen.
|
||||
// v2.7.13 Planlinie, v2.7.12 overlayRenderNonce, v2.7.11 waypointRenderKey,
|
||||
// v2.7.10 Persistenz, v2.7.9 State Isolation alle unberührt.
|
||||
// v2.7.20 — Flexible Routenformung:
|
||||
// • Route formen = Start/Ziel/Zwischenpunkte setzen, dann Planlinie oder Online-Route.
|
||||
// • Neu: Zwischenpunkt direkt per Karten-Tap setzen (neuer AddViaPoint-TapMode).
|
||||
// • Intuitiver Fallback: 1. Tap = Start, 2. Tap = Ziel, weitere = Zwischenpunkte.
|
||||
// • Via-Sektion im Tourplaner klar als "Zwischenpunkte (Route formen)" bezeichnet.
|
||||
// • Freie Linie klar als Fallback gekennzeichnet (UI-Text, kein Verhaltensänderung).
|
||||
// • Keine Regressionen: v2.7.19 Start/Via/Ziel-Persistenz, v2.7.18 Doppellinie-Fix,
|
||||
// Audio-Waypoints, PTT, Atmo, Backup, Lizenz, MapLibre-Render-Fixes unverändert.
|
||||
// v2.7.21 — Waypoint Preservation Fix:
|
||||
// • Root Cause: Race-Condition in duplicateTour()/deleteTour()/renameTour() liest
|
||||
// _waypoints.value (könnte noch emptyList() sein vor DataStore-Erstladen) und ruft
|
||||
// saveAll(leere_liste + newWps) auf — überschreibt alle bestehenden Wegpunkte.
|
||||
// • Fix 1: saveAll() hat jetzt Preservation-Guard: leere Liste überschreibt niemals
|
||||
// nicht-leere DataStore-Liste (atomarer Check innerhalb dataStore.edit).
|
||||
// • Fix 2: saveAllForced() für explizite Lösch-/Restore-Operationen (deleteTour,
|
||||
// renameTour, Backup-Restore) — bewusster Vollersatz.
|
||||
// • Fix 3: duplicateTour/deleteTour/renameTour lesen jetzt mit repository.waypoints.first()
|
||||
// direkt aus DataStore statt aus _waypoints.value.
|
||||
// • Planlinie/Route-Speicherpfade (saveTourRouteFor) berhren KEY_WAYPOINTS nie.
|
||||
// • AddViaPoint (v2.7.20) bestätigt sicher: kein Waypoint-Speicherpfad wird ausgelöst.
|
||||
// • Keine Regressionen: v2.7.20 AddViaPoint, v2.7.19 Start/Via/Ziel-Persistenz,
|
||||
// v2.7.18 Doppellinie-Fix, Audio-Waypoints, PTT, Atmo, Backup, Lizenz unverändert.
|
||||
// v2.7.22 — Saved Route Render Fix:
|
||||
// • Root Cause: savedRouteLine schloss MANUAL-Routen explizit aus (emptyList()),
|
||||
// damit der v2.7.18 Doppellinien-Fix keine Konflikte erzeugt. Da aber
|
||||
// manualRoutePolylines (Bearbeitungsvorschau) nur bei manualRouteMode=true
|
||||
// gezeichnet wird, war das Ausschluss-Kriterium in savedRouteLineüberfüssig
|
||||
// und verhinderte die Darstellung gespeicherter MANUAL-Routen.
|
||||
// • Fix 1: MANUAL-Quellen in savedRouteLine eingeschlossen (wie OSRM); Phantom-Guard
|
||||
// auf Standardtour bleibt.
|
||||
// • Fix 2: LaunchedEffect(savedRouteLine) überspringt MANUAL (wie PLANLINE), damit
|
||||
// MANUAL via hasSavedRoute-Pfad grün gezeichnet wird (nicht blau als OSRM).
|
||||
// • Fix 3: hasRoute im RoutePlannerSheet-Aufruf berücksichtigt savedRouteLine.size,
|
||||
// damit "Noch keine Route" nicht erscheint wenn eine gespeicherte Route vorhanden ist.
|
||||
// • Keine Regressionen: v2.7.21 Waypoint-Preservation-Guard, v2.7.18 Doppellinie-Fix,
|
||||
// PLANLINE, OSRM, AddViaPoint, Audio-Waypoints, PTT, Atmo, Backup, Lizenz unverändert.
|
||||
// v2.7.26 — Persistent Map Preferences:
|
||||
// • Globale Kartenpräferenzen dauerhaft gespeichert: Wegpunkt-Marker, Audio-Radius,
|
||||
// GPS-Track/Draft-Linien, POIs-automatisch-nachladen (autoPoiLoad) jetzt persistent.
|
||||
// • Bestehende Persistenz (baseStyle, showAudioLength, showAudioLines, POI-Kategorien,
|
||||
// POI-Namen, Standort-Folgen, perspectiveTilt) unverändert beibehalten und erweitert.
|
||||
// • Standort-Folgen entschärft: Jede manuelle Kartenverschiebung deaktiviert Standort
|
||||
// folgen sofort (statt nur bei >40m Distanz). Kein Auto-Rücksprung beim nächsten
|
||||
// GPS-Fix. Explizite Aktivierung nur per Toggle-Switch oder MyLocation-Button.
|
||||
// • Beim Öffnen des Explorers mit Tourlinie/Wegpunkten: Kamera auf Tour zentriert,
|
||||
// nicht auf Live-Standort (sofern Standort folgen aus).
|
||||
// • Keine Regressionen: v2.7.25 Initial-Render-Fix, v2.7.24 UX Polish, v2.7.23
|
||||
// Tourverlauf übernehmen, v2.7.22 Saved Route Render, v2.7.21 Waypoint
|
||||
// Preservation Guard alle unberührt.
|
||||
// v2.7.29 — Overlay Rehydrate Fix:
|
||||
// • Root Cause: Nach setStyle() loescht MapLibre alle Annotations (Marker,
|
||||
// Polylines, Polygons). overlayRenderNonce++ loest Waypoint-Redraw aus,
|
||||
// aber myLocationMarker/accuracyPolygon wurden auf null gesetzt und nur durch
|
||||
// eingehende GPS-Updates (LocationCallback) neu gesetzt — fehlende Interaktion
|
||||
// bis zum naechsten GPS-Fix fuehrte zu fehlendem/hellem Default-Marker.
|
||||
// • Fix 1 (Live-Standort): Neuer LaunchedEffect(overlayRenderNonce, styleReady,
|
||||
// mapLibreMap) zeichnet den Puck sofort aus lastLat/lastLng neu, ohne auf
|
||||
// naechsten GPS-Fix zu warten. Genauigkeitskreis wird ebenfalls rehydriert.
|
||||
// • Fix 2 (POI-Marker): overlayRenderNonce als Key im explorerPoiResults-
|
||||
// LaunchedEffect sichert POI-Redraw nach Style-Load.
|
||||
// • Alle v2.7.28 Fixes (Map Style Apply Fix, appliedBaseStyle/appliedPerspectiveTilt)
|
||||
// bleiben vollstaendig erhalten.
|
||||
// • Keine Regressionen: v2.7.27 Kartenpraeferenz-Persistenz, v2.7.26
|
||||
// Standort-Folgen-Fix, v2.7.25 Initial-Render-Fix, v2.7.24 UX Polish,
|
||||
// v2.7.23 Tourverlauf uebernehmen, v2.7.21 Waypoint Preservation Guard,
|
||||
// Routenplanung, POIs, Kartenstile, Audio-Waypoints, Multiclip, PTT, Atmo,
|
||||
// Lizenzsystem, Backup/Restore alle unberuehrt.
|
||||
// v2.7.32 — Zwei Regressionsfixes:
|
||||
// • Touren ordnen: Erste und letzte SICHTBARE Tour ließen sich nicht
|
||||
// mehr verschieben. Ursache: TourReorderDialog arbeitet auf
|
||||
// visibleTourList (Standard-Tour ausgeblendet), WaypointViewModel.moveTour
|
||||
// operierte aber auf _tourList inkl. DEFAULT_TOUR_NAME → Off-by-one,
|
||||
// wenn die Standard-Tour an Index 0 lag. Fix: moveTour interpretiert
|
||||
// Indizes im sichtbaren Raum und merged zurück, wobei die ausgeblendete
|
||||
// Standard-Tour ihre relative Position behält. Hidden-Standard-Verhalten
|
||||
// bleibt erhalten; echte Touren sind alle frei sortierbar.
|
||||
// • Audioplayer: Der etablierte GPS2Audio-Player (ClipDetailSheet/
|
||||
// -Fullscreen) erschien beim manuellen Start eines Tracks/Clips nicht
|
||||
// mehr. Fix: Neuer LaunchedEffect öffnet die Player-Ansicht automatisch
|
||||
// für den aktiv abgespielten Wegpunkt, sobald Wiedergabe beginnt.
|
||||
// • Keine Regressionen: Multi-Clip, Clip-Navigation, Countdown, maximierter
|
||||
// Detailplayer, Hintergrundwiedergabe, PTT-Pause, Tour-Wiedergabe-
|
||||
// Persistenz, Explorer/MapLibre, Routing, POIs, Backup, Lizenz unberührt.
|
||||
// v2.7.33 — UI-Politur Begleitmusik-Dialog (rein gestalterisch):
|
||||
// • Begleitmusik fühlt sich jetzt wie eine integrierte Musik-Karte der
|
||||
// Tour an statt wie ein technisches Formular.
|
||||
// • Neue Status-/Musik-Karte (MusicStatusCard) im Teal/Dark-Stil:
|
||||
// Tour-Kontext, Aktiv-Status, Kurz-Zusammenfassung + Aktivieren-Schalter.
|
||||
// • Kompakte Quellen-Chips (Lokale Playlist / Stream), primäre Aktion
|
||||
// „Musik auswählen" als gefüllter Button, einladender Empty-State
|
||||
// (MusicEmptyState) statt technischem Text.
|
||||
// • Verhaltensoptionen mit Klartext-Titeln („Bei Ansagen pausieren",
|
||||
// „Leiser im Hintergrund", „Weiterlaufen lassen", „Sanft aus-/einblenden")
|
||||
// unter zusammengefasstem Abschnitt „Verhalten & Optionen".
|
||||
// • KEINE Änderung an Datenmodell, Speicherung, Backup/Import, Lizenz,
|
||||
// Playback-Service, PTT, Atmo, Waypoint-/Hintergrundwiedergabe, MapLibre/
|
||||
// Explorer oder Routing. validateAndSave + alle Optionen unverändert.
|
||||
// • Keine Regression zu v2.7.32 (Player-Auto-Open, Tour-Reihenfolge).
|
||||
// v2.7.34 — UI-Politur „Wegpunkt bearbeiten" (rein gestalterisch):
|
||||
// • Ruhigeres Layout: kompakte Status-/Kopfkarte oben (Name, Tour,
|
||||
// Kurzüberblick Position/Audio).
|
||||
// • „Details" (Name + Tour) immer sichtbar; Position, Audio, Auslösung,
|
||||
// Wiedergabe-Regeln, Zeitplan und „Kopieren & duplizieren" als
|
||||
// einklappbare Abschnitte mit Kurz-Zusammenfassung.
|
||||
// • Kopieren/Duplizieren nach ganz unten verschoben (nicht mehr zwischen
|
||||
// Basisfeldern und Position); Koordinaten nur bei Bedarf aufgeklappt.
|
||||
// • Abschnitte mit Validierungsfehlern öffnen sich automatisch.
|
||||
// • KEINE Änderung an Datenmodell, Speicherung, Backup/Import, Lizenz,
|
||||
// Playback, Multi-Clip, PTT, Atmo, MapLibre/Explorer oder Routing.
|
||||
// Validation + Save (confirmButton) und alle Felder unverändert.
|
||||
// v2.7.35 — UI-Politur Hauptseite/Audio-Bibliothek (rein gestalterisch):
|
||||
// • Abschnittstitel „Meine Touren" mit Anzahl als Chip rechts.
|
||||
// • Tour-Karten: feste Textblock-Höhe (gleichmäßige Karten), klare
|
||||
// Metazeile statt abgeschnittenem Text („Route ge…").
|
||||
// • Leere Touren wirken bewusst („Noch leer") statt „0 Wegpunkte".
|
||||
// • Chips immer sichtbar: Wegpunkt-Zahl bzw. „Noch leer" + „Route" /
|
||||
// „Keine Route".
|
||||
// • Live/PTT-Modul optisch zurückgenommen (Einrückung/kompakter) –
|
||||
// KEINE Verhaltensänderung an LivePttCard.
|
||||
// • KEINE Änderung an Datenmodell, Speicherung, Backup/Import, Lizenz,
|
||||
// Playback, Multi-Clip, PTT/Atmo-Verhalten, MapLibre/Explorer, Routing,
|
||||
// Tour-Reihenfolge oder Player-Auto-Open.
|
||||
// v2.7.36 — Bugfix: Absturz/ANR nach Backup-Erstellung.
|
||||
// • Ursache: exportBackupZip() führte Flow-Reads, ZIP-Aufbau und das
|
||||
// Kopieren aller Mediendateien im viewModelScope auf dem Main-Thread
|
||||
// aus → ANR ("App reagiert nicht"), obwohl die Datei geschrieben war.
|
||||
// • Fix: schwere IO läuft jetzt auf Dispatchers.IO (wie der Import-Pfad);
|
||||
// State-Updates für Erfolg/Fehler bleiben auf Main. Zusätzliche
|
||||
// Absicherung: unerwartete Fehler werden als nicht-fatale Meldung
|
||||
// gezeigt statt abzustürzen.
|
||||
// • KEINE Änderung am Backup-Format/-Inhalt, an der Import-Logik oder an
|
||||
// anderen Funktionen.
|
||||
// v2.7.39 — Audio-Stabilitätsfix:
|
||||
// • Die in v2.7.37 eingeführte feste Stille-Pause zwischen Clips und
|
||||
// Wegpunkten im manuellen Tour-Play wurde entfernt.
|
||||
// • Manueller Auto-Advance startet den nächsten aktiven Clip/Wegpunkt
|
||||
// wieder direkt. Dadurch gibt es keinen verzögerten Coroutine-Job
|
||||
// mehr, der auf Samsung-Geräten oder nach Tour-/GPS-State-Wechseln in
|
||||
// veraltete Player-Zustände laufen kann.
|
||||
// • Keine Änderung an GPS-Wiedergabe, PTT, Atmo, Einzelclip-Vorschau,
|
||||
// Hintergrund-Playback, Backup/Import, Lizenz, Karten/Explorer oder
|
||||
// Player-Auto-Open.
|
||||
versionCode = 145
|
||||
versionName = "2.7.39"
|
||||
}
|
||||
|
||||
val storeFilePath = resolveSigningValue("storeFile", "GPS2AUDIO_RELEASE_STORE_FILE")
|
||||
|
||||
@@ -165,15 +165,18 @@ class MainActivity : ComponentActivity() {
|
||||
// direkt die bestehende Tour-/Waypoint-Ansicht.
|
||||
navController.navigate("list")
|
||||
},
|
||||
// v2.5.34 — Explorer-Kontext-Fix:
|
||||
// Einstieg aus Audio-Bibliothek/Home muss immer neutral
|
||||
// öffnen. selectDefaultTour() setzt selectedTour auf
|
||||
// DEFAULT_TOUR_NAME, bevor der Explorer gerendert wird.
|
||||
// Einstieg aus WaypointListScreen (Tour-Ansicht) behält
|
||||
// die jeweils aktive Tour — dort wird selectDefaultTour()
|
||||
// nicht aufgerufen.
|
||||
// v2.7.14 — Explorer-Kontext-Fix (neutral):
|
||||
// Einstieg aus Audio-Bibliothek/Home öffnet immer einen
|
||||
// leeren, neutralen Kontext ("Unbenannte Tour").
|
||||
// selectNeutralTourForNewExplorer() setzt selectedTour auf
|
||||
// DEFAULT_TOUR_NAME und aktiviert den Neutral-Explorer-Flag,
|
||||
// der verhindert, dass der combine-Kollektor automatisch
|
||||
// zur ersten echten Tour wechselt.
|
||||
// Einstieg aus WaypointListScreen (Touransicht) behält
|
||||
// die jeweils aktive Tour — dort wird keine tour-Reset-Funktion
|
||||
// aufgerufen.
|
||||
onOpenExplorer = {
|
||||
viewModel.selectDefaultTour()
|
||||
viewModel.selectNeutralTourForNewExplorer()
|
||||
navController.navigate("explorer")
|
||||
},
|
||||
// v2.5.19 — „Track Editor" wurde aus dem Hauptmenü
|
||||
|
||||
@@ -113,7 +113,24 @@ data class PoiLayerSettings(
|
||||
)
|
||||
|
||||
/**
|
||||
* Gespeicherte Karten-Stil-Einstellungen — v2.5.38.
|
||||
* Persistierte Layer-Overlay-Einstellungen — v2.7.26.
|
||||
* Alle Overlay-Toggles aus dem MapLayerPanel, die bisher nur im
|
||||
* Compose-State lebten und bei App-Neustart zurückgesetzt wurden.
|
||||
*
|
||||
* @param showMarkers Wegpunkt-Marker anzeigen.
|
||||
* @param showRadius Audio-Radius/Reichweite anzeigen.
|
||||
* @param showTrack GPS-Track/Draft-Linien anzeigen.
|
||||
* @param autoPoiLoadEnabled POIs automatisch nachladen (bei Kamerabewegung).
|
||||
*/
|
||||
data class MapLayerOverlaySettings(
|
||||
val showMarkers: Boolean = true,
|
||||
val showRadius: Boolean = true,
|
||||
val showTrack: Boolean = true,
|
||||
val autoPoiLoadEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
/**
|
||||
* Gespeicherte Karten-Stil-Einstellungen — v2.5.38 / v2.7.26.
|
||||
*
|
||||
* @param baseStyle gewählter Basis-Stil (wird persistent gespeichert)
|
||||
* @param showAudioLength ob die visuelle Audio-/Trigger-Länge als
|
||||
@@ -122,6 +139,7 @@ data class PoiLayerSettings(
|
||||
* @param showAudioLines Clip-Längen als projizierte Liniensegmente.
|
||||
* @param routingEndpoint optionaler OSRM-Routing-Endpoint.
|
||||
* @param poiLayers POI-Kategorien-Sichtbarkeit (persistent).
|
||||
* @param overlayLayers Weitere Layer-Toggles (Marker, Radius, Track, Auto-POI-Load).
|
||||
*
|
||||
* Legacy-Felder (customSatelliteUrl, customTopoUrl, customTerrainDemUrl) werden
|
||||
* weiterhin gelesen, aber nicht mehr in der UI angeboten — für Rückwärts-
|
||||
@@ -137,6 +155,8 @@ data class MapStyleSettings(
|
||||
val showAudioLines: Boolean = true,
|
||||
val routingEndpoint: String = "",
|
||||
val poiLayers: PoiLayerSettings = PoiLayerSettings(),
|
||||
// v2.7.26 — Persistente Layer-Overlay-Einstellungen
|
||||
val overlayLayers: MapLayerOverlaySettings = MapLayerOverlaySettings(),
|
||||
)
|
||||
|
||||
private val Context.mapStyleDataStore by preferencesDataStore(name = "map_style_settings")
|
||||
@@ -159,6 +179,11 @@ class MapStyleStore(private val context: Context) {
|
||||
val KEY_POI_MOBILITY = booleanPreferencesKey("poi_mobility")
|
||||
// v2.5.42
|
||||
val KEY_POI_SHOW_NAMES = booleanPreferencesKey("poi_show_names")
|
||||
// v2.7.26 — Layer-Overlay-Persistenz
|
||||
val KEY_SHOW_MARKERS = booleanPreferencesKey("show_markers")
|
||||
val KEY_SHOW_RADIUS = booleanPreferencesKey("show_radius")
|
||||
val KEY_SHOW_TRACK = booleanPreferencesKey("show_track")
|
||||
val KEY_AUTO_POI_LOAD = booleanPreferencesKey("auto_poi_load")
|
||||
}
|
||||
|
||||
val settings: Flow<MapStyleSettings> = context.mapStyleDataStore.data.map { prefs ->
|
||||
@@ -179,6 +204,13 @@ class MapStyleStore(private val context: Context) {
|
||||
showGastronomy = prefs[KEY_POI_GASTRONOMY] ?: true,
|
||||
showMobility = prefs[KEY_POI_MOBILITY] ?: true,
|
||||
showPoiNames = prefs[KEY_POI_SHOW_NAMES] ?: false,
|
||||
),
|
||||
// v2.7.26 — Layer-Overlay-Persistenz
|
||||
overlayLayers = MapLayerOverlaySettings(
|
||||
showMarkers = prefs[KEY_SHOW_MARKERS] ?: true,
|
||||
showRadius = prefs[KEY_SHOW_RADIUS] ?: true,
|
||||
showTrack = prefs[KEY_SHOW_TRACK] ?: true,
|
||||
autoPoiLoadEnabled = prefs[KEY_AUTO_POI_LOAD] ?: true,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -212,6 +244,16 @@ class MapStyleStore(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// v2.7.26 — Layer-Overlay-Persistenz
|
||||
suspend fun setOverlayLayers(overlay: MapLayerOverlaySettings) {
|
||||
context.mapStyleDataStore.edit {
|
||||
it[KEY_SHOW_MARKERS] = overlay.showMarkers
|
||||
it[KEY_SHOW_RADIUS] = overlay.showRadius
|
||||
it[KEY_SHOW_TRACK] = overlay.showTrack
|
||||
it[KEY_AUTO_POI_LOAD] = overlay.autoPoiLoadEnabled
|
||||
}
|
||||
}
|
||||
|
||||
// v2.5.37 — Legacy-Stubs für Aufrufstellen aus v2.4.x die
|
||||
// noch customSatelliteUrl / customTopoUrl / customTerrainDemUrl schreiben.
|
||||
// Diese Werte werden weiterhin gelesen (Preferences bleiben),
|
||||
|
||||
@@ -175,8 +175,45 @@ class WaypointStore(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Speichert die gesamte Wegpunktliste. */
|
||||
/**
|
||||
* Speichert die gesamte Wegpunktliste.
|
||||
*
|
||||
* v2.7.21 — Waypoint-Preservation-Guard:
|
||||
* Wenn [waypoints] leer ist, aber im DataStore bereits Wegpunkte vorhanden
|
||||
* sind, wird der Schreibvorgang NICHT ausgeführt. Das verhindert, dass eine
|
||||
* versehentlich leere oder veraltete In-Memory-Liste (`_waypoints.value`
|
||||
* aus einem Race-Condition-Fenster) bestehende Wegpunkte überschreibt.
|
||||
*
|
||||
* Explizites Leeren (Tour-Löschung, Migration) muss [saveAllForced] verwenden.
|
||||
*/
|
||||
suspend fun saveAll(waypoints: List<Waypoint>) {
|
||||
context.dataStore.edit { prefs ->
|
||||
// v2.7.21 — Preservation Guard: nie eine leere Liste über eine
|
||||
// nicht-leere Liste schreiben. Schutz gegen Race-Condition-Verlust.
|
||||
if (waypoints.isEmpty()) {
|
||||
val existingJson = prefs[KEY_WAYPOINTS] ?: "[]"
|
||||
val existingList = runCatching {
|
||||
gson.fromJson<List<Waypoint>>(existingJson, waypointListType)
|
||||
}.getOrNull()
|
||||
if (!existingList.isNullOrEmpty()) {
|
||||
// Vorhandene Wegpunkte schützen — leere Liste wird NICHT geschrieben.
|
||||
android.util.Log.w(
|
||||
"WaypointStore",
|
||||
"saveAll() mit leerer Liste blockiert: ${existingList.size} bestehende Wegpunkte bleiben erhalten."
|
||||
)
|
||||
return@edit
|
||||
}
|
||||
}
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(waypoints)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.21 — Forciertes Speichern ohne Preservation-Guard.
|
||||
* Nur für explizite Lösch- und Migrationsoperationen verwenden,
|
||||
* bei denen eine leere Liste das gewünschte Ergebnis ist.
|
||||
*/
|
||||
suspend fun saveAllForced(waypoints: List<Waypoint>) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[KEY_WAYPOINTS] = gson.toJson(waypoints)
|
||||
}
|
||||
@@ -228,6 +265,12 @@ 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.
|
||||
*
|
||||
* v2.7.21 — Waypoint-Preservation-Invariante:
|
||||
* Diese Funktion schreibt AUSSCHLIESSLICH in [KEY_TOUR_ROUTES].
|
||||
* [KEY_WAYPOINTS] (Audio-Wegpunkte) wird durch diesen Aufruf NIEMALS
|
||||
* verändert. Tourrouten und Audio-Wegpunkte sind vollständig getrennte
|
||||
* Speicherbereiche.
|
||||
*/
|
||||
suspend fun saveTourRoute(tourName: String, route: TourRoute) {
|
||||
context.dataStore.edit { prefs ->
|
||||
|
||||
@@ -73,9 +73,16 @@ class WaypointRepository(context: Context) {
|
||||
/** Wegpunkt löschen. */
|
||||
suspend fun delete(id: String) = store.delete(id)
|
||||
|
||||
/** Alle Wegpunkte ersetzen. */
|
||||
/** Alle Wegpunkte ersetzen (mit Preservation-Guard gegen leere Liste). */
|
||||
suspend fun saveAll(waypoints: List<Waypoint>) = store.saveAll(waypoints)
|
||||
|
||||
/**
|
||||
* v2.7.21 — Alle Wegpunkte forciert ersetzen (kein Preservation-Guard).
|
||||
* Nur für explizite Lösch-/Migrations-Operationen (z. B. Tour löschen mit
|
||||
* Reassignment auf DEFAULT_TOUR_NAME, Backup-Restore-Vollersatz).
|
||||
*/
|
||||
suspend fun saveAllForced(waypoints: List<Waypoint>) = store.saveAllForced(waypoints)
|
||||
|
||||
/** Tourliste persistieren. */
|
||||
suspend fun saveTours(tours: List<String>) = store.saveTours(tours)
|
||||
|
||||
|
||||
@@ -82,10 +82,12 @@ class ManualAudioPlayer {
|
||||
fun play(
|
||||
context: Context,
|
||||
soundUri: String,
|
||||
onError: () -> Unit = {},
|
||||
onCompletion: () -> Unit = {}
|
||||
) {
|
||||
if (soundUri.isBlank()) {
|
||||
Log.w(TAG, "Keine Audiodatei angegeben.")
|
||||
onError()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,14 +102,14 @@ class ManualAudioPlayer {
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "Fehler beim Fortsetzen von $soundUri", e)
|
||||
releasePlayer()
|
||||
loadAndPlay(context, soundUri, onCompletion)
|
||||
loadAndPlay(context, soundUri, onCompletion, onError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// New or different track → fresh load
|
||||
releasePlayer()
|
||||
loadAndPlay(context, soundUri, onCompletion)
|
||||
loadAndPlay(context, soundUri, onCompletion, onError)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -168,12 +170,12 @@ class ManualAudioPlayer {
|
||||
) {
|
||||
isSingleMode = true
|
||||
singleWaypointId = waypointId
|
||||
loadAndPlay(context, soundUri) {
|
||||
loadAndPlay(context, soundUri, onCompletion = {
|
||||
// Natural completion: clear single state, then notify caller
|
||||
isSingleMode = false
|
||||
singleWaypointId = null
|
||||
onCompletion()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -183,7 +185,8 @@ class ManualAudioPlayer {
|
||||
private fun loadAndPlay(
|
||||
context: Context,
|
||||
soundUri: String,
|
||||
onCompletion: () -> Unit
|
||||
onCompletion: () -> Unit,
|
||||
onError: () -> Unit = {}
|
||||
) {
|
||||
runCatching {
|
||||
val uri = Uri.parse(soundUri)
|
||||
@@ -200,9 +203,14 @@ class ManualAudioPlayer {
|
||||
onCompletion()
|
||||
}
|
||||
setOnErrorListener { mp, what, extra ->
|
||||
// v2.7.38 — Asynchroner MediaPlayer-Fehler (z. B. Samsung-
|
||||
// spezifische Codec-/SAF-Probleme): Ressourcen freigeben und
|
||||
// den Aufrufer informieren, damit der "spielt"-Zustand nicht
|
||||
// ohne Ton hängen bleibt.
|
||||
Log.e(TAG, "MediaPlayer-Fehler: what=$what extra=$extra")
|
||||
mp.release()
|
||||
releasePlayer()
|
||||
onError()
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -217,8 +225,14 @@ class ManualAudioPlayer {
|
||||
tour = pendingTour
|
||||
)
|
||||
}.onFailure { e ->
|
||||
// v2.7.38 — Synchroner Lade-/Prepare-/Start-Fehler. Auf manchen
|
||||
// Geräten (insb. Samsung-Tablets) kann prepare()/start() mit
|
||||
// content://-SAF-URIs fehlschlagen. Bisher blieb der UI-Zustand
|
||||
// dann fälschlich auf "spielt" ohne Audio. Jetzt: sauber freigeben
|
||||
// und den Aufrufer benachrichtigen.
|
||||
Log.e(TAG, "Fehler beim Abspielen von $soundUri", e)
|
||||
releasePlayer()
|
||||
onError()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,35 +152,31 @@ fun BegleitmusikDialog(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Tour-Kontext
|
||||
Text(
|
||||
stringResource(R.string.music_for_tour, tourName),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
// ─── Status-/Musik-Karte (v2.7.33) ───────────────────────────────
|
||||
// Integrierte Karte statt technischem Formular-Kopf: Tour-Kontext,
|
||||
// Aktiv-Status, Kurz-Zusammenfassung und Aktivieren-Schalter in
|
||||
// einer visuell zusammenhängenden Einheit.
|
||||
val summaryText = when {
|
||||
sourceType == MusicSourceType.LOCAL_PLAYLIST && localPlaylist.size == 1 ->
|
||||
stringResource(R.string.music_summary_local_one)
|
||||
sourceType == MusicSourceType.LOCAL_PLAYLIST && localPlaylist.isNotEmpty() ->
|
||||
stringResource(R.string.music_summary_local, localPlaylist.size)
|
||||
sourceType == MusicSourceType.STREAM_URL && streamPlaylist.size == 1 ->
|
||||
stringResource(R.string.music_summary_stream_one)
|
||||
sourceType == MusicSourceType.STREAM_URL && streamPlaylist.isNotEmpty() ->
|
||||
stringResource(R.string.music_summary_stream, streamPlaylist.size)
|
||||
else -> stringResource(R.string.music_summary_empty)
|
||||
}
|
||||
MusicStatusCard(
|
||||
tourName = tourName,
|
||||
enabled = enabled,
|
||||
summary = summaryText,
|
||||
onToggle = { enabled = it }
|
||||
)
|
||||
|
||||
// ─── Aktivieren/Deaktivieren ─────────────────────────────────────
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.music_enable),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(checked = enabled, onCheckedChange = { enabled = it })
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
if (enabled) {
|
||||
// ─── Quelltyp ─────────────────────────────────────────────────
|
||||
Text(
|
||||
stringResource(R.string.music_source_label),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
// ─── Quelle ────────────────────────────────────────────────
|
||||
SectionLabel(stringResource(R.string.music_section_source))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -190,39 +186,33 @@ fun BegleitmusikDialog(
|
||||
selected = sourceType == MusicSourceType.LOCAL_PLAYLIST,
|
||||
onClick = { sourceType = MusicSourceType.LOCAL_PLAYLIST },
|
||||
label = { Text(stringResource(R.string.music_source_local)) },
|
||||
leadingIcon = if (sourceType == MusicSourceType.LOCAL_PLAYLIST) {
|
||||
{ Icon(Icons.Filled.Check, null, Modifier.size(16.dp)) }
|
||||
} else null
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
if (sourceType == MusicSourceType.LOCAL_PLAYLIST)
|
||||
Icons.Filled.Check else Icons.Filled.LibraryMusic,
|
||||
null, Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
FilterChip(
|
||||
selected = sourceType == MusicSourceType.STREAM_URL,
|
||||
onClick = { sourceType = MusicSourceType.STREAM_URL },
|
||||
label = { Text(stringResource(R.string.music_source_stream)) },
|
||||
leadingIcon = if (sourceType == MusicSourceType.STREAM_URL) {
|
||||
{ Icon(Icons.Filled.Check, null, Modifier.size(16.dp)) }
|
||||
} else null
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
if (sourceType == MusicSourceType.STREAM_URL)
|
||||
Icons.Filled.Check else Icons.Filled.Radio,
|
||||
null, Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Lokale Playlist ──────────────────────────────────────────
|
||||
if (sourceType == MusicSourceType.LOCAL_PLAYLIST) {
|
||||
Text(
|
||||
stringResource(R.string.music_playlist_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (localPlaylist.isEmpty()) {
|
||||
// Hinweis je nach Verfügbarkeit der Audio-Bibliothek anpassen,
|
||||
// damit die User wissen, dass sie auch bereits hinzugefügte
|
||||
// Dateien erneut auswählen können.
|
||||
Text(
|
||||
if (audioLibrary.isEmpty())
|
||||
stringResource(R.string.music_playlist_empty)
|
||||
else
|
||||
stringResource(R.string.music_playlist_empty_with_library),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
// Einladender Empty-State statt technischem Hinweis.
|
||||
MusicEmptyState()
|
||||
} else {
|
||||
localPlaylist.forEachIndexed { idx, item ->
|
||||
Row(
|
||||
@@ -265,7 +255,8 @@ fun BegleitmusikDialog(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedButton(
|
||||
// Primäre Aktion „Musik auswählen" (gefüllter Button).
|
||||
Button(
|
||||
onClick = {
|
||||
multiPickLauncher.launch(
|
||||
arrayOf("audio/*", "audio/mpeg", "audio/ogg", "audio/flac", "*/*")
|
||||
@@ -325,11 +316,7 @@ fun BegleitmusikDialog(
|
||||
)
|
||||
|
||||
if (streamPlaylist.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.music_stream_playlist_empty),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
MusicEmptyState()
|
||||
} else {
|
||||
streamPlaylist.forEachIndexed { idx, entry ->
|
||||
val displayLabel = entry.name.ifBlank { entry.url }
|
||||
@@ -597,34 +584,31 @@ fun BegleitmusikDialog(
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ─── Verhalten beim Wegpunkt ──────────────────────────────────
|
||||
Text(
|
||||
stringResource(R.string.music_behavior_label),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
// ─── Verhalten & Optionen (v2.7.33: zusammengefasster Bereich) ─
|
||||
SectionLabel(stringResource(R.string.music_section_advanced))
|
||||
|
||||
// Klartext-Titel, gleiche Funktionen wie zuvor.
|
||||
BehaviorOption(
|
||||
selected = behavior == WaypointMusicBehavior.PAUSE_RESUME,
|
||||
title = stringResource(R.string.music_behavior_pause_resume),
|
||||
title = stringResource(R.string.music_behavior_pause_resume_plain),
|
||||
description = stringResource(R.string.music_behavior_pause_resume_desc),
|
||||
onClick = { behavior = WaypointMusicBehavior.PAUSE_RESUME }
|
||||
)
|
||||
BehaviorOption(
|
||||
selected = behavior == WaypointMusicBehavior.FADE_OUT_IN,
|
||||
title = stringResource(R.string.music_behavior_fade),
|
||||
title = stringResource(R.string.music_behavior_fade_plain),
|
||||
description = stringResource(R.string.music_behavior_fade_desc),
|
||||
onClick = { behavior = WaypointMusicBehavior.FADE_OUT_IN }
|
||||
)
|
||||
BehaviorOption(
|
||||
selected = behavior == WaypointMusicBehavior.DUCK_UNDERLAY,
|
||||
title = stringResource(R.string.music_behavior_duck),
|
||||
title = stringResource(R.string.music_behavior_duck_plain),
|
||||
description = stringResource(R.string.music_behavior_duck_desc),
|
||||
onClick = { behavior = WaypointMusicBehavior.DUCK_UNDERLAY }
|
||||
)
|
||||
BehaviorOption(
|
||||
selected = behavior == WaypointMusicBehavior.CONTINUE_UNDERLAY,
|
||||
title = stringResource(R.string.music_behavior_continue),
|
||||
title = stringResource(R.string.music_behavior_continue_plain),
|
||||
description = stringResource(R.string.music_behavior_continue_desc),
|
||||
onClick = { behavior = WaypointMusicBehavior.CONTINUE_UNDERLAY }
|
||||
)
|
||||
@@ -691,11 +675,7 @@ fun BegleitmusikDialog(
|
||||
HorizontalDivider()
|
||||
|
||||
// ─── Test-Steuerung ───────────────────────────────────────────
|
||||
Text(
|
||||
stringResource(R.string.music_test_label),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
SectionLabel(stringResource(R.string.music_test_label))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FilledIconButton(
|
||||
onClick = { viewModel.toggleMusicPlayback() },
|
||||
@@ -891,3 +871,132 @@ private fun BehaviorOption(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.33 — Integrierte Status-/Musik-Karte (rein gestalterisch).
|
||||
* Fasst Tour-Kontext, Aktiv-Status, Kurz-Zusammenfassung und den
|
||||
* Aktivieren-Schalter in einer Karte im GPS2Audio-Teal/Dark-Stil zusammen.
|
||||
*/
|
||||
@Composable
|
||||
private fun MusicStatusCard(
|
||||
tourName: String,
|
||||
enabled: Boolean,
|
||||
summary: String,
|
||||
onToggle: (Boolean) -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(14.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (enabled)
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (enabled) Icons.Filled.LibraryMusic else Icons.Filled.MusicNote,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = if (enabled)
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.music_card_heading),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (enabled)
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.music_for_tour, tourName),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (enabled)
|
||||
MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f)
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = if (enabled) summary else stringResource(R.string.music_status_off),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (enabled)
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Switch(checked = enabled, onCheckedChange = onToggle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.33 — Einladender Empty-State, wenn noch keine Musik gewählt wurde.
|
||||
* Ersetzt den technischen „Noch keine Audiodateien"-Hinweis.
|
||||
*/
|
||||
@Composable
|
||||
private fun MusicEmptyState() {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(14.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MusicNote,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
stringResource(R.string.music_empty_state_title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
stringResource(R.string.music_empty_state_body),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** v2.7.33 — Einheitliche Abschnittsüberschrift im Teal-Akzent. */
|
||||
@Composable
|
||||
private fun SectionLabel(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -192,15 +192,19 @@ fun ExplorerScreen(
|
||||
val selectedProvider by providerStore.provider.collectAsState(initial = MapProvider.DEFAULT)
|
||||
|
||||
val settingsStore = remember { ExplorerSettingsStore(context.applicationContext) }
|
||||
val persistedSettings by settingsStore.settings.collectAsState(
|
||||
initial = de.waypointaudio.data.ExplorerSettings()
|
||||
// v2.7.27 — Fix: null initial damit followLocation Restore erst nach echtem DataStore-Emit.
|
||||
val persistedSettingsOrNull by settingsStore.settings.collectAsState(
|
||||
initial = null
|
||||
)
|
||||
val persistedSettings = persistedSettingsOrNull ?: de.waypointaudio.data.ExplorerSettings()
|
||||
|
||||
// v2.3.0 — MapLibre/Overlays: persistente Karten-Stil-Einstellungen.
|
||||
val mapStyleStore = remember { MapStyleStore(context.applicationContext) }
|
||||
val mapStyleSettings by mapStyleStore.settings.collectAsState(
|
||||
initial = MapStyleSettings()
|
||||
// v2.7.27 — Fix: null als Initial-Wert, damit Restore erst nach echtem DataStore-Emit feuert.
|
||||
val mapStyleSettingsOrNull by mapStyleStore.settings.collectAsState(
|
||||
initial = null
|
||||
)
|
||||
val mapStyleSettings = mapStyleSettingsOrNull ?: MapStyleSettings()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Audio-Dauer-Cache je Wegpunkt (URI → Millisekunden).
|
||||
@@ -239,13 +243,13 @@ fun ExplorerScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Persistente Nachführungs-Einstellung. Erstinitialisierung aus dem DataStore,
|
||||
// danach: manuelle Map-Bewegung darf das Flag NICHT automatisch zurücksetzen.
|
||||
var followLocation by remember { mutableStateOf(persistedSettings.followLocation) }
|
||||
// v2.7.27 — followLocation startet mit true (default); echter Wert aus DataStore via null-safe Emit.
|
||||
var followLocation by remember { mutableStateOf(true) }
|
||||
var followInitialized by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(persistedSettings.followLocation) {
|
||||
LaunchedEffect(persistedSettingsOrNull) {
|
||||
val loaded = persistedSettingsOrNull ?: return@LaunchedEffect
|
||||
if (!followInitialized) {
|
||||
followLocation = persistedSettings.followLocation
|
||||
followLocation = loaded.followLocation
|
||||
followInitialized = true
|
||||
}
|
||||
}
|
||||
@@ -265,14 +269,18 @@ fun ExplorerScreen(
|
||||
var layerState by remember {
|
||||
mutableStateOf(MapLayerState(followLocation = persistedSettings.followLocation))
|
||||
}
|
||||
// Initial-Sync der Stil-Einstellungen (einmalig, danach trägt onStateChange).
|
||||
// v2.7.27 — styleHydrated: true sobald echter DataStore-Wert (nicht null) verarbeitet wurde.
|
||||
var styleHydrated by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(mapStyleSettings) {
|
||||
LaunchedEffect(mapStyleSettingsOrNull) {
|
||||
val loaded = mapStyleSettingsOrNull ?: return@LaunchedEffect
|
||||
if (!styleHydrated) {
|
||||
layerState = layerState.copy(
|
||||
baseStyle = mapStyleSettings.baseStyle,
|
||||
showAudioLength = mapStyleSettings.showAudioLength,
|
||||
showAudioLines = mapStyleSettings.showAudioLines,
|
||||
baseStyle = loaded.baseStyle,
|
||||
showAudioLength = loaded.showAudioLength,
|
||||
showAudioLines = loaded.showAudioLines,
|
||||
showMarkers = loaded.overlayLayers.showMarkers,
|
||||
showRadius = loaded.overlayLayers.showRadius,
|
||||
showTrack = loaded.overlayLayers.showTrack,
|
||||
)
|
||||
styleHydrated = true
|
||||
}
|
||||
@@ -304,6 +312,25 @@ fun ExplorerScreen(
|
||||
runCatching { mapStyleStore.setShowAudioLines(layerState.showAudioLines) }
|
||||
}
|
||||
}
|
||||
// v2.7.26 — Layer-Overlay-Toggles persistent speichern (showMarkers, showRadius, showTrack).
|
||||
LaunchedEffect(layerState.showMarkers, layerState.showRadius, layerState.showTrack) {
|
||||
if (!styleHydrated) return@LaunchedEffect
|
||||
val current = mapStyleSettings.overlayLayers
|
||||
if (layerState.showMarkers != current.showMarkers ||
|
||||
layerState.showRadius != current.showRadius ||
|
||||
layerState.showTrack != current.showTrack) {
|
||||
runCatching {
|
||||
mapStyleStore.setOverlayLayers(
|
||||
de.waypointaudio.data.MapLayerOverlaySettings(
|
||||
showMarkers = layerState.showMarkers,
|
||||
showRadius = layerState.showRadius,
|
||||
showTrack = layerState.showTrack,
|
||||
autoPoiLoadEnabled = true, // osmdroid-Fallback hat kein autoPoiLoad
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showSearchDialog by remember { mutableStateOf(false) }
|
||||
var searchPin by remember { mutableStateOf<GeoPoint?>(null) }
|
||||
@@ -357,6 +384,14 @@ fun ExplorerScreen(
|
||||
// Manager-Track für die neutrale Standardtour nicht als Phantom erscheint.
|
||||
val isNeutralStandard: Boolean =
|
||||
selectedTour == de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME
|
||||
|
||||
// v2.7.6 — UI-Entkopplung: DEFAULT_TOUR_NAME ("Standard") wird dem Nutzer
|
||||
// nicht als interner Konstantenname angezeigt. Im neutralen Explorer-Kontext
|
||||
// erscheint stattdessen "Spontane Tour". Echte Tournamen bleiben unverändert.
|
||||
val displayTourLabel: String =
|
||||
if (isNeutralStandard) stringResource(R.string.explorer_spontane_tour)
|
||||
else selectedTour
|
||||
|
||||
val visibleTrack: List<GpsTrackPoint> = when {
|
||||
isRecording -> currentTrack
|
||||
activeDraft != null -> activeDraft.points
|
||||
@@ -638,7 +673,13 @@ fun ExplorerScreen(
|
||||
prefillTourName = selectedTour,
|
||||
tourDefaults = tourDefaults,
|
||||
onConfirm = { newWaypoint ->
|
||||
viewModel.upsert(newWaypoint)
|
||||
// v2.7.7 — Spontane-Tour-Autocreate (auch im OSM-Explorer-Pfad):
|
||||
// Neuer Wegpunkt im neutralen Kontext → automatisch echte Tour erzeugen.
|
||||
if (editDialogWaypoint == null) {
|
||||
viewModel.upsertFromNeutralExplorer(newWaypoint)
|
||||
} else {
|
||||
viewModel.upsert(newWaypoint)
|
||||
}
|
||||
showEditDialog = false
|
||||
editDialogWaypoint = null
|
||||
editDialogPrefill = null
|
||||
@@ -923,7 +964,7 @@ fun ExplorerScreen(
|
||||
MapModeSegmentedHeader(
|
||||
current = currentMode,
|
||||
onChange = onModeChange,
|
||||
subtitle = selectedTour
|
||||
subtitle = displayTourLabel
|
||||
)
|
||||
} else {
|
||||
Column {
|
||||
@@ -937,7 +978,7 @@ fun ExplorerScreen(
|
||||
Text(stringResource(R.string.explorer_title))
|
||||
}
|
||||
Text(
|
||||
text = selectedTour,
|
||||
text = displayTourLabel,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||
)
|
||||
@@ -1616,11 +1657,28 @@ internal fun StoryboardPanel(
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
stringResource(R.string.explorer_storyboard_no_routing_hint),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
// v2.6.0 — Erklärtext: Unterschied Tourverlaufspunkte vs. Audio-Wegpunkte
|
||||
androidx.compose.material3.Surface(
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
androidx.compose.foundation.layout.Column(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.explorer_storyboard_explainer),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
stringResource(R.string.explorer_storyboard_no_routing_hint),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,7 +74,9 @@ fun HomeScreen(
|
||||
val store = remember(context) { HomeModulesSettingsStore(context.applicationContext) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val tourList by viewModel.tourList.collectAsState()
|
||||
// v2.7.5 — visibleTourList blendet Standard-Tour (Blueprint) aus, sobald echte
|
||||
// Touren vorhanden sind. Intern bleibt Standard-Tour als Fallback erhalten.
|
||||
val tourList by viewModel.visibleTourList.collectAsState()
|
||||
val waypoints by viewModel.waypoints.collectAsState()
|
||||
val tourRoutes by viewModel.tourRoutes.collectAsState()
|
||||
val tourCovers by viewModel.tourCovers.collectAsState()
|
||||
@@ -240,16 +242,24 @@ fun HomeScreen(
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Touren",
|
||||
text = "Meine Touren",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "${tourList.size}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f))
|
||||
.padding(horizontal = 10.dp, vertical = 3.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "${tourList.size}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─ Cover-Grid ─
|
||||
@@ -522,8 +532,14 @@ private fun TourCoverCard(
|
||||
}
|
||||
}
|
||||
|
||||
// Text-Block
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
// Text-Block — feste Struktur (Titel, eine Metazeile, Chips-Zeile),
|
||||
// damit alle Karten gleich hoch wirken und nichts abgeschnitten wird.
|
||||
val isEmptyTour = waypointCount == 0
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.heightIn(min = 86.dp)
|
||||
) {
|
||||
Text(
|
||||
text = tourName,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
@@ -536,26 +552,34 @@ private fun TourCoverCard(
|
||||
Text(
|
||||
text = subtitleFor(waypointCount, hasRoute),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
color = if (isEmptyTour) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Spacer(Modifier.weight(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Badge(
|
||||
icon = Icons.Filled.PinDrop,
|
||||
text = "$waypointCount"
|
||||
)
|
||||
if (hasRoute) {
|
||||
if (isEmptyTour) {
|
||||
Badge(
|
||||
icon = Icons.Filled.Route,
|
||||
text = "Route",
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
icon = Icons.Filled.Add,
|
||||
text = "Noch leer",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
Badge(
|
||||
icon = Icons.Filled.PinDrop,
|
||||
text = "$waypointCount"
|
||||
)
|
||||
}
|
||||
Badge(
|
||||
icon = Icons.Filled.Route,
|
||||
text = if (hasRoute) "Route" else "Keine Route",
|
||||
tint = if (hasRoute) MaterialTheme.colorScheme.tertiary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,10 +734,13 @@ private fun PttModuleCard(
|
||||
// Wiederverwendung der bestehenden LivePttCard (volle Funktion: Mic-Permission,
|
||||
// Aufnahmen, Geräte). Die Card wird hier nur in einen Wrapper gehüllt, der
|
||||
// Abstände/Form an die übrige Bibliothek-Oberfläche anpasst.
|
||||
// v2.7.35 — Live/PTT optisch zurückgenommen: an den Bibliothek-Rand
|
||||
// eingerückt und vertikal kompakter, damit es die Touren nicht dominiert.
|
||||
// KEINE Verhaltensänderung an der LivePttCard selbst.
|
||||
LivePttCard(
|
||||
pttManager = pttManager,
|
||||
onOpenRecordings = onOpenRecordings,
|
||||
modifier = Modifier.padding(horizontal = 0.dp, vertical = 6.dp)
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -869,11 +896,11 @@ private fun ToggleRow(
|
||||
|
||||
/* ───────────────────────────── Helpers ─────────────────────────────────── */
|
||||
|
||||
private fun subtitleFor(waypointCount: Int, hasRoute: Boolean): String {
|
||||
val parts = mutableListOf<String>()
|
||||
parts.add("$waypointCount Wegpunkte")
|
||||
if (hasRoute) parts.add("Route gespeichert")
|
||||
return parts.joinToString(" · ")
|
||||
private fun subtitleFor(waypointCount: Int, hasRoute: Boolean): String = when {
|
||||
waypointCount == 0 -> "Noch leer · füge Wegpunkte hinzu"
|
||||
waypointCount == 1 -> if (hasRoute) "1 Wegpunkt · Route geplant" else "1 Wegpunkt"
|
||||
else -> if (hasRoute) "$waypointCount Wegpunkte · Route geplant"
|
||||
else "$waypointCount Wegpunkte"
|
||||
}
|
||||
|
||||
private fun atmoSourceLabel(type: MusicSourceType): String = when (type) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AltRoute
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Explore
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.GpsFixed
|
||||
@@ -106,6 +107,8 @@ fun MapLayerFab(
|
||||
onPerspectiveTiltChange: (Int) -> Unit = {},
|
||||
onShowRoutingInfo: () -> Unit = {},
|
||||
onShowRoutePlanner: () -> Unit = {},
|
||||
// v2.7.15 — Unified Tourverlauf-Editor-Einstieg (zusammengeführt aus RoutePlanner + Editor)
|
||||
onShowTourverlaufEditor: () -> Unit = {},
|
||||
// v2.5.45 — Auto-POI-Load-Toggle
|
||||
poiAutoLoadEnabled: Boolean = true,
|
||||
onPoiAutoLoadChange: (Boolean) -> Unit = {},
|
||||
@@ -546,10 +549,11 @@ fun MapLayerFab(
|
||||
}
|
||||
}
|
||||
|
||||
// v2.4.2 — Routenplaner-Eintrag im Layer-Panel.
|
||||
// v2.7.16 — Route planen (wiederhergestellt): direkter Einstieg in RoutePlannerSheet.
|
||||
// Start/Ziel/Via, Planlinie, Online-Routing. War in v2.7.15 fälschlich entfernt.
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.65f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
@@ -560,23 +564,63 @@ fun MapLayerFab(
|
||||
Icons.Filled.AltRoute,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.route_planner_menu_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.route_planner_menu_subtitle),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.78f)
|
||||
)
|
||||
}
|
||||
TextButton(onClick = {
|
||||
showSheet = false
|
||||
onShowRoutePlanner()
|
||||
}) {
|
||||
Text(stringResource(R.string.route_planner_menu_open))
|
||||
}
|
||||
}
|
||||
}
|
||||
// v2.7.15 / v2.7.16 — Tourverlauf bearbeiten: manuelles Zeichnen/Setzen von Verlaufspunkten.
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.route_planner_open_short),
|
||||
stringResource(R.string.tourverlauf_menu_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.route_planner_subtitle),
|
||||
stringResource(R.string.tourverlauf_menu_subtitle),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f)
|
||||
)
|
||||
}
|
||||
TextButton(onClick = onShowRoutePlanner) {
|
||||
Text(stringResource(R.string.route_planner_open))
|
||||
TextButton(onClick = {
|
||||
showSheet = false
|
||||
onShowTourverlaufEditor()
|
||||
}) {
|
||||
Text(stringResource(R.string.tourverlauf_menu_open))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,8 @@ fun MapScreen(
|
||||
) {
|
||||
val allWaypoints by viewModel.waypoints.collectAsState()
|
||||
val selectedTour by viewModel.selectedTour.collectAsState()
|
||||
val tourList by viewModel.tourList.collectAsState()
|
||||
// v2.7.5 — visibleTourList: Standard-Tour in Kartenansicht-Kontext ausblenden
|
||||
val tourList by viewModel.visibleTourList.collectAsState()
|
||||
val tourDefaults by viewModel.tourDefaults.collectAsState()
|
||||
val currentTrack by viewModel.currentTrack.collectAsState()
|
||||
val isRecording by viewModel.isRecording.collectAsState()
|
||||
|
||||
@@ -455,7 +455,8 @@ fun WaypointChooserDialog(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val tourList by viewModel.tourList.collectAsState()
|
||||
// v2.7.5 — visibleTourList: Standard-Tour wird aus PTT-Ziel-Dropdown ausgeblendet
|
||||
val tourList by viewModel.visibleTourList.collectAsState()
|
||||
val selectedTour by viewModel.selectedTour.collectAsState()
|
||||
val allWaypoints by viewModel.waypoints.collectAsState()
|
||||
val currentLocation by viewModel.currentLocation.collectAsState()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,456 @@
|
||||
package de.waypointaudio.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddLocation
|
||||
import androidx.compose.material.icons.filled.AltRoute
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.HeadsetMic
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.PinDrop
|
||||
import androidx.compose.material.icons.filled.TouchApp
|
||||
import androidx.compose.material.icons.filled.Undo
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.waypointaudio.R
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
|
||||
/**
|
||||
* v2.7.0 — Tourverlauf-Editor-BottomSheet.
|
||||
* v2.7.17 — Route-first: Sprache: Routenpunkte statt Verlaufspunkte,
|
||||
* "Audio hier hinzufügen" statt technischem "Als Audio-Wegpunkt erstellen".
|
||||
* Keine Logik-Änderungen.
|
||||
*
|
||||
* Klarer Bearbeitungsmodus für den Tourverlauf:
|
||||
* - Verlaufspunkte direkt auf der Karte setzen (Tipp-Modus oder Kartenmitte)
|
||||
* - Punkte per Dialog entfernen/verschieben (bestehende Logik)
|
||||
* - "Als Audio-Wegpunkt erstellen" für einen Verlaufspunkt
|
||||
* - Letzten Punkt entfernen / Verlauf komplett löschen
|
||||
* - Einklappbarer "Optional: Online-Route berechnen"-Abschnitt (→ öffnet RoutePlannerSheet)
|
||||
*
|
||||
* Wiederverwendete Bausteine:
|
||||
* - manualRoutePoints / TourRoute / RouteSourceKind.MANUAL (Persistenz, Auto-Save)
|
||||
* - WaypointEditDialog (Audio-Wegpunkt erstellen)
|
||||
* - RoutePlannerSheet (Online-Routing, optional/einklappbar)
|
||||
*
|
||||
* Keine Regressionen: Logik, Playback, Backup, Lizenz, GPS-Aufzeichnung unverändert.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TourverlaufEditorSheet(
|
||||
/** Aktuelle Verlaufspunkte (manualRoutePoints). */
|
||||
routePoints: List<LatLng>,
|
||||
/** True wenn Tipp-Modus für neuen Punkt aktiv. */
|
||||
isDrawing: Boolean,
|
||||
/** Name der ausgewählten Tour (für Kontext-Labels). */
|
||||
tourName: String,
|
||||
/** Letzter bekannter Verlaufspunkt, aus dem ein Audio-Wegpunkt erstellt werden soll. */
|
||||
lastSelectedPointIndex: Int?,
|
||||
|
||||
// Callbacks
|
||||
/** Punkt an der Kartenmitte hinzufügen. */
|
||||
onAddCenterPoint: () -> Unit,
|
||||
/** Tipp-Modus zum Setzen per Kartentap umschalten. */
|
||||
onToggleDrawMode: () -> Unit,
|
||||
/** Letzten Punkt entfernen. */
|
||||
onRemoveLast: () -> Unit,
|
||||
/** Gesamten Verlauf löschen. */
|
||||
onClear: () -> Unit,
|
||||
/** Für einen Verlaufspunkt einen Audio-Wegpunkt erstellen. */
|
||||
onCreateWaypointFromPoint: (LatLng) -> Unit,
|
||||
/** Online-Routing öffnen (RoutePlannerSheet). */
|
||||
onOpenOnlineRouting: () -> Unit,
|
||||
/** Sheet schließen. */
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
var showClearConfirm by remember { mutableStateOf(false) }
|
||||
var onlineRoutingExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val pointCount = routePoints.size
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// ─── Header ───────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
stringResource(R.string.tourverlauf_editor_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
// v2.7.15 — Untertitel: Beschreibung des konsolidierten Einstiegs
|
||||
Text(
|
||||
stringResource(R.string.tourverlauf_menu_subtitle),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
if (tourName.isNotBlank()) {
|
||||
Text(
|
||||
tourName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.tourverlauf_editor_close))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Erklärtext ───────────────────────────────────────────────
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.7f)
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp).padding(top = 2.dp),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.tourverlauf_editor_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Punkt-Zähler ─────────────────────────────────────────────
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (pointCount > 0)
|
||||
MaterialTheme.colorScheme.tertiaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
if (pointCount > 0) Icons.Filled.CheckCircle else Icons.Filled.PinDrop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (pointCount > 0)
|
||||
MaterialTheme.colorScheme.tertiary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
Text(
|
||||
if (pointCount > 0)
|
||||
stringResource(R.string.tourverlauf_editor_point_count, pointCount)
|
||||
else
|
||||
stringResource(R.string.tourverlauf_editor_no_points),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (pointCount > 0)
|
||||
MaterialTheme.colorScheme.onTertiaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Punkt setzen ─────────────────────────────────────────────
|
||||
Text(
|
||||
"Routenpunkt hinzufügen",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Kartenmitte-Button
|
||||
Button(
|
||||
onClick = {
|
||||
onAddCenterPoint()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Filled.PinDrop, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.tourverlauf_editor_add_center))
|
||||
}
|
||||
|
||||
// Tipp-Modus-Toggle
|
||||
Button(
|
||||
onClick = onToggleDrawMode,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isDrawing)
|
||||
MaterialTheme.colorScheme.tertiary
|
||||
else
|
||||
MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = if (isDrawing)
|
||||
MaterialTheme.colorScheme.onTertiary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Filled.TouchApp, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
if (isDrawing) "Tipp-Modus aktiv" else stringResource(R.string.tourverlauf_editor_add_tap)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Aktionen ─────────────────────────────────────────────────
|
||||
if (pointCount > 0) {
|
||||
Text(
|
||||
"Tourpunkt-Aktionen",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
|
||||
// Als Audio-Wegpunkt erstellen
|
||||
val targetPoint = lastSelectedPointIndex?.let { routePoints.getOrNull(it) }
|
||||
?: routePoints.lastOrNull()
|
||||
if (targetPoint != null) {
|
||||
OutlinedButton(
|
||||
onClick = { onCreateWaypointFromPoint(targetPoint) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.HeadsetMic,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.tourverlauf_editor_as_waypoint))
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Letzten Punkt entfernen
|
||||
OutlinedButton(
|
||||
onClick = onRemoveLast,
|
||||
enabled = pointCount > 0,
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.secondary
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Filled.Undo, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.tourverlauf_editor_remove_last))
|
||||
}
|
||||
|
||||
// Verlauf löschen
|
||||
OutlinedButton(
|
||||
onClick = { showClearConfirm = true },
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.tourverlauf_editor_clear))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Optional: Online-Routing ──────────────────────────────────
|
||||
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
// Header — einklappbar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.AltRoute,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.tourverlauf_online_routing_section),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onlineRoutingExpanded = !onlineRoutingExpanded },
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
if (onlineRoutingExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||
contentDescription = if (onlineRoutingExpanded) "Schließen" else "Aufklappen",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = onlineRoutingExpanded,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.tourverlauf_online_routing_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onlineRoutingExpanded = false
|
||||
onOpenOnlineRouting()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.AltRoute,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.route_planner_open))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Lösch-Bestätigungsdialog
|
||||
if (showClearConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showClearConfirm = false },
|
||||
title = { Text(stringResource(R.string.tourverlauf_editor_clear_confirm)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onClear()
|
||||
showClearConfirm = false
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.delete))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showClearConfirm = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fun VersionNoticeDialogHost() {
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.version_notice_body_v2538),
|
||||
text = stringResource(R.string.version_notice_body_v2720),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Row(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,7 +90,9 @@ fun WaypointListScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val waypoints by viewModel.waypoints.collectAsState()
|
||||
val tourList by viewModel.tourList.collectAsState()
|
||||
// v2.7.5 — visibleTourList blendet Standard-Tour aus der UI aus (Tabs, Dialoge),
|
||||
// sobald echte Touren vorhanden sind. Intern bleibt sie als Fallback erhalten.
|
||||
val tourList by viewModel.visibleTourList.collectAsState()
|
||||
val selectedTour by viewModel.selectedTour.collectAsState()
|
||||
val serviceRunning by viewModel.serviceRunning.collectAsState()
|
||||
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||
@@ -160,6 +162,33 @@ fun WaypointListScreen(
|
||||
// statt als Bottom-Sheet angezeigt. Minimize stellt den Sheet-Modus
|
||||
// wieder her, ohne den Player-Zustand zu verlieren.
|
||||
var clipDetailFullscreen by remember { mutableStateOf(false) }
|
||||
|
||||
// v2.7.32 — Player-Sichtbarkeits-Regression behoben.
|
||||
// Wird ein Track/Clip manuell gestartet (Karten-Play-Button oder
|
||||
// ManualPlayerBar), soll der etablierte GPS2Audio-Player (ClipDetailSheet /
|
||||
// -Fullscreen) wieder automatisch erscheinen. Zuvor öffnete sich die
|
||||
// Detailansicht nur durch Tippen auf die Karte; bei manuellem Start blieb
|
||||
// der Player unsichtbar, obwohl Audio lief.
|
||||
// Der Effekt folgt dem aktiv abgespielten Wegpunkt und öffnet die Ansicht,
|
||||
// sobald Wiedergabe beginnt. Die bestehende Auto-Follow-Logik (Clip-Wechsel
|
||||
// innerhalb geöffneter Ansicht) und manuelles Schließen bleiben unberührt:
|
||||
// Schließt der Nutzer die Ansicht, wird sie erst beim nächsten Start eines
|
||||
// anderen Wegpunkts wieder geöffnet.
|
||||
val activePlaybackWaypointId: String? = when {
|
||||
singlePlayingWaypointId != null -> singlePlayingWaypointId
|
||||
manualPlaying -> currentPlayable?.waypointId
|
||||
else -> null
|
||||
}
|
||||
LaunchedEffect(activePlaybackWaypointId) {
|
||||
val activeId = activePlaybackWaypointId
|
||||
if (activeId != null &&
|
||||
clipDetailWaypointId != activeId &&
|
||||
filteredWaypoints.any { it.id == activeId }
|
||||
) {
|
||||
clipDetailWaypointId = activeId
|
||||
}
|
||||
}
|
||||
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
var showAboutDialog by remember { mutableStateOf(false) }
|
||||
var showWakeLockDialog by remember { mutableStateOf(false) }
|
||||
@@ -459,22 +488,65 @@ fun WaypointListScreen(
|
||||
showNewTourDialog = true
|
||||
}
|
||||
)
|
||||
// v2.7.5 — Umbenennen/Duplizieren für Standard-Tour (Blueprint) sperren
|
||||
val isSystemTour = selectedTour == Waypoint.DEFAULT_TOUR_NAME
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.tour_rename)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Edit, null) },
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.tour_rename),
|
||||
color = if (isSystemTour)
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
else
|
||||
LocalContentColor.current
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Filled.Edit,
|
||||
null,
|
||||
tint = if (isSystemTour)
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
else
|
||||
LocalContentColor.current
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showOverflowMenu = false
|
||||
showRenameTourDialog = true
|
||||
}
|
||||
if (!isSystemTour) {
|
||||
showOverflowMenu = false
|
||||
showRenameTourDialog = true
|
||||
}
|
||||
},
|
||||
enabled = !isSystemTour
|
||||
)
|
||||
// v2.5.15 — Tour als Variante duplizieren
|
||||
// v2.7.5 — Duplizieren der Standard-Tour gesperrt
|
||||
DropdownMenuItem(
|
||||
text = { Text("Tour duplizieren…") },
|
||||
leadingIcon = { Icon(Icons.Filled.ContentCopy, null) },
|
||||
text = {
|
||||
Text(
|
||||
"Tour duplizieren…",
|
||||
color = if (isSystemTour)
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
else
|
||||
LocalContentColor.current
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Filled.ContentCopy,
|
||||
null,
|
||||
tint = if (isSystemTour)
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
else
|
||||
LocalContentColor.current
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showOverflowMenu = false
|
||||
showDuplicateTourDialog = true
|
||||
}
|
||||
if (!isSystemTour) {
|
||||
showOverflowMenu = false
|
||||
showDuplicateTourDialog = true
|
||||
}
|
||||
},
|
||||
enabled = !isSystemTour
|
||||
)
|
||||
if (selectedTour == Waypoint.DEFAULT_TOUR_NAME) {
|
||||
// Standard-Tour kann nicht gelöscht werden – Eintrag deaktiviert
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.Locale
|
||||
import java.util.UUID
|
||||
import de.waypointaudio.io.BackupImportManager
|
||||
import de.waypointaudio.io.ImportExportManager
|
||||
import de.waypointaudio.ui.RoutePlan
|
||||
import de.waypointaudio.repository.WaypointRepository
|
||||
import de.waypointaudio.service.BackgroundMusicManager
|
||||
import de.waypointaudio.service.LivePttManager
|
||||
@@ -46,10 +47,13 @@ import de.waypointaudio.service.TrackRecordingManager
|
||||
import de.waypointaudio.service.WaypointLocationService
|
||||
import de.waypointaudio.service.MusicPlaybackState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withTimeout
|
||||
@@ -121,12 +125,42 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _tourList = MutableStateFlow<List<String>>(listOf(Waypoint.DEFAULT_TOUR_NAME))
|
||||
val tourList: StateFlow<List<String>> = _tourList.asStateFlow()
|
||||
|
||||
/**
|
||||
* v2.7.5 — Öffentlich sichtbare Tourliste für die UI (Audio-Bibliothek, Tabs).
|
||||
* Die Standard-Tour (DEFAULT_TOUR_NAME) wird ausgeblendet, sobald mindestens
|
||||
* eine echte (benutzerdefinierte) Tour existiert. Intern bleibt DEFAULT_TOUR_NAME
|
||||
* stets als Fallback erhalten — Daten werden nicht gelöscht.
|
||||
* Wenn keine echten Touren vorhanden sind, wird DEFAULT_TOUR_NAME als
|
||||
* einziger Eintrag angezeigt (leerer Zustand für neue Nutzer).
|
||||
*/
|
||||
val visibleTourList: StateFlow<List<String>> = _tourList
|
||||
.map { list ->
|
||||
val realTours = list.filter { it != Waypoint.DEFAULT_TOUR_NAME }
|
||||
if (realTours.isEmpty()) listOf(Waypoint.DEFAULT_TOUR_NAME) else realTours
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = listOf(Waypoint.DEFAULT_TOUR_NAME)
|
||||
)
|
||||
|
||||
/**
|
||||
* Set of tour names that have been deleted optimistically.
|
||||
* The combine collector must not re-add these from waypoint-derived tours.
|
||||
*/
|
||||
private val _deletedTours = mutableSetOf<String>()
|
||||
|
||||
/**
|
||||
* v2.7.14 — Neutral-Explorer-Modus-Flag.
|
||||
* Wenn true, unterdrückt der combine-Kollektor den automatischen Upgrade
|
||||
* von DEFAULT_TOUR_NAME zur ersten echten Tour. Das ermöglicht einen
|
||||
* bewusst leeren Explorer-Einstieg aus der Übersicht/Audio-Bibliothek.
|
||||
* Wird durch selectNeutralTourForNewExplorer() aktiviert und durch
|
||||
* selectTour(echte Tour) oder upsertFromNeutralExplorer() wieder deaktiviert.
|
||||
*/
|
||||
@Volatile
|
||||
private var _neutralExplorerMode: Boolean = false
|
||||
|
||||
/**
|
||||
* Aktuell ausgewählte Tour (für Tabs und Filter).
|
||||
*/
|
||||
@@ -163,6 +197,15 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _manualPlaying = MutableStateFlow(false)
|
||||
val manualPlaying: StateFlow<Boolean> = _manualPlaying.asStateFlow()
|
||||
|
||||
// v2.7.39 — Die in v2.7.37 eingeführte feste Stille-Pause zwischen
|
||||
// manuellen Clips/Wegpunkten ist aus Stabilitätsgründen wieder entfernt.
|
||||
// Auf einigen Samsung-Geräten führte der verzögerte Auto-Advance zu
|
||||
// uneindeutigem Player-State und konnte fälschlich "Audio konnte nicht
|
||||
// abgespielt werden" auslösen. Die Methode bleibt als No-op bestehen,
|
||||
// damit bestehende Stop-/Skip-/Tourwechsel-Pfade ohne strukturelles Risiko
|
||||
// weiter denselben Aufräumaufruf verwenden können.
|
||||
private fun cancelManualGap() = Unit
|
||||
|
||||
/** Name of the waypoint currently loaded in the manual player (null = nothing loaded). */
|
||||
private val _manualCurrentName = MutableStateFlow<String?>(null)
|
||||
val manualCurrentName: StateFlow<String?> = _manualCurrentName.asStateFlow()
|
||||
@@ -265,6 +308,66 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
get() = _activeTourRoute
|
||||
private val _activeTourRoute = MutableStateFlow<de.waypointaudio.data.TourRoute?>(null)
|
||||
|
||||
// ─── Session-Route (v2.6.2) ───────────────────────────────────────────────
|
||||
// Hält die zuletzt im Explorer berechnete/geplante Route (OSRM oder Planlinie)
|
||||
// für die aktuelle Tour. Überlebt Navigation/Workspace-Wechsel, da im ViewModel
|
||||
// gespeichert. Wird beim Tourwechsel oder explizitem Löschen geleert.
|
||||
// Kein Datenbankschreiben — dient ausschließlich der Session-Wiederherstellung.
|
||||
// Enthält: tourName (zur Validierung) + TourRoute-Objekt.
|
||||
private val _currentSessionRoute =
|
||||
MutableStateFlow<Pair<String, de.waypointaudio.data.TourRoute>?>(null)
|
||||
val currentSessionRoute: StateFlow<Pair<String, de.waypointaudio.data.TourRoute>?> =
|
||||
_currentSessionRoute.asStateFlow()
|
||||
|
||||
/**
|
||||
* v2.6.2 — Speichert die aktuell berechnete/geplante Route für die laufende
|
||||
* Explorer-Session. Wird nicht in die Datenbank geschrieben, sondern dient
|
||||
* der Wiederherstellung der Kartenvisualisierung nach einem Workspace-Wechsel.
|
||||
* Zusätzlich wird die Route dauerhaft als Tourroute persistiert, damit sie
|
||||
* auch nach App-Neustart sichtbar ist.
|
||||
*/
|
||||
fun setCurrentSessionRoute(
|
||||
tourName: String,
|
||||
route: de.waypointaudio.data.TourRoute
|
||||
) {
|
||||
_currentSessionRoute.value = tourName to route
|
||||
// Gleichzeitig dauerhaft persistieren, damit activeTourRoute beim Zurückkehren
|
||||
// die Route enthält und savedRouteLine sie als Backup zeichnet.
|
||||
saveTourRouteFor(tourName, route)
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.6.2 — Löscht die Session-Route (z. B. beim Tourwechsel oder explizitem
|
||||
* Löschen durch den Nutzer). Berührt nicht die persistente Tourroute.
|
||||
*/
|
||||
fun clearCurrentSessionRoute() {
|
||||
_currentSessionRoute.value = null
|
||||
}
|
||||
|
||||
// ─── Session-Plan (v2.7.2) ───────────────────────────────────────────────
|
||||
// Hält den zuletzt genutzten RoutePlan (Start/Via/Ziel) pro Tour.
|
||||
// Überlebt Navigation/Workspace-Wechsel, da im ViewModel gespeichert.
|
||||
// Wird beim echten Tourwechsel geleert. Kein DB-Schreiben.
|
||||
private val _currentSessionPlan =
|
||||
MutableStateFlow<Pair<String, RoutePlan>?>(null)
|
||||
val currentSessionPlan: StateFlow<Pair<String, RoutePlan>?> =
|
||||
_currentSessionPlan.asStateFlow()
|
||||
|
||||
/**
|
||||
* v2.7.2 — Speichert den aktuellen RoutePlan für die laufende Session.
|
||||
* Wird bei jedem Plan-Update aufgerufen (Start/Via/Ziel-Änderung).
|
||||
*/
|
||||
fun setCurrentSessionPlan(tourName: String, plan: RoutePlan) {
|
||||
_currentSessionPlan.value = tourName to plan
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.2 — Löscht den Session-Plan (beim Tourwechsel oder explizitem Leeren).
|
||||
*/
|
||||
fun clearCurrentSessionPlan() {
|
||||
_currentSessionPlan.value = null
|
||||
}
|
||||
|
||||
// ─── Tour-Cover (v2.5.5) ──────────────────────────────────────────────────
|
||||
// Persistente Map tourName → TourCover. Wenn keine Auswahl existiert,
|
||||
// behält die UI ihre deterministische Standard-Optik (siehe TourHeroSection).
|
||||
@@ -301,6 +404,16 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
if (current.isNotBlank() && current !in combined) {
|
||||
_selectedTour.value = combined.firstOrNull() ?: Waypoint.DEFAULT_TOUR_NAME
|
||||
}
|
||||
// v2.7.5 — Wenn Standard-Tour ausgewählt ist, aber echte Touren existieren,
|
||||
// automatisch zur ersten echten Tour wechseln. Standard bleibt interner Fallback.
|
||||
// v2.7.14 — Wenn _neutralExplorerMode aktiv ist, wird dieser Upgrade NICHT
|
||||
// durchgeführt: Explorer soll leer/neutral bleiben (Einstieg aus Übersicht).
|
||||
if (_selectedTour.value == Waypoint.DEFAULT_TOUR_NAME && !_neutralExplorerMode) {
|
||||
val firstReal = combined.firstOrNull { it != Waypoint.DEFAULT_TOUR_NAME }
|
||||
if (firstReal != null) {
|
||||
_selectedTour.value = firstReal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Begleitmusik-Einstellungen für die initiale Tour laden
|
||||
@@ -329,10 +442,15 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
_activeTourRoute.value = map[_selectedTour.value]
|
||||
}
|
||||
}
|
||||
// Beim Tourwechsel die aktive Tourroute aktualisieren.
|
||||
// Beim Tourwechsel die aktive Tourroute und Session-Route aktualisieren.
|
||||
viewModelScope.launch {
|
||||
_selectedTour.collect { tour ->
|
||||
_activeTourRoute.value = _tourRoutes.value[tour]
|
||||
// v2.6.2 — Session-Route leeren, wenn der Nutzer die Tour wechselt.
|
||||
// So zeigt der Explorer nach einem Tourwechsel keine fremde Route.
|
||||
_currentSessionRoute.value = null
|
||||
// v2.7.2 — Session-Plan leeren beim Tourwechsel.
|
||||
_currentSessionPlan.value = null
|
||||
}
|
||||
}
|
||||
// v2.5.5 — Tour-Cover beobachten.
|
||||
@@ -415,13 +533,59 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
|
||||
/**
|
||||
* v2.5.34 — Explorer-Kontext-Fix:
|
||||
* Setzt die ausgewählte Tour auf den neutralen Standard zurück.
|
||||
* Wird aufgerufen, wenn der Explorer aus der Audio-Bibliothek/Home
|
||||
* geöffnet wird, damit kein zuletzt aktiver Tour-Kontext übernommen wird.
|
||||
* Setzt die ausgewählte Tour auf den neutralen Standard zurück,
|
||||
* AUSSER wenn bereits echte (nicht-Standard) Touren existieren –
|
||||
* dann wird direkt die erste echte Tour gewählt.
|
||||
*
|
||||
* v2.7.10 — Waypoint-Persistence-Fix:
|
||||
* Wenn eine spontane "Unbenannte Tour" im Explorer erzeugt wurde und der
|
||||
* Nutzer den Explorer verlässt und erneut öffnet, darf selectDefaultTour()
|
||||
* nicht blind auf DEFAULT zurückspringen und die frisch erstellten Wegpunkte
|
||||
* hinter einem Tour-Filter verstecken. Stattdessen: Wenn echte Touren
|
||||
* vorhanden sind, sofort zur ersten echten Tour wechseln (gleiche Logik wie
|
||||
* im combine-Kollektor, aber synchron, um Race Conditions zu vermeiden).
|
||||
*
|
||||
* Der GPS-Dienst und die Wiedergabe werden dabei NICHT unterbrochen.
|
||||
*/
|
||||
fun selectDefaultTour() {
|
||||
selectTour(de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME)
|
||||
val knownTours = _tourList.value
|
||||
val firstReal = knownTours.firstOrNull { it != de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME }
|
||||
if (firstReal != null) {
|
||||
// Echte Touren existieren – direkt zur ersten echten Tour wechseln.
|
||||
// Kein Rücksprung auf DEFAULT_TOUR_NAME, damit Wegpunkte sichtbar bleiben.
|
||||
selectTour(firstReal)
|
||||
} else {
|
||||
// Keine echten Touren – Standard-Tour ist der einzig sinnvolle Zustand.
|
||||
selectTour(de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.14 — Neutraler Explorer-Start aus der Übersicht/Audio-Bibliothek.
|
||||
*
|
||||
* Setzt selectedTour explizit auf DEFAULT_TOUR_NAME ("Unbenannte Tour") und
|
||||
* aktiviert den Neutral-Explorer-Modus-Flag, der den automatischen Upgrade
|
||||
* durch den combine-Kollektor (v2.7.5-Logik) verhindert.
|
||||
*
|
||||
* Konzept:
|
||||
* - Explorer aus Audio-Bibliothek/Übersicht → diese Funktion → leerer Kontext
|
||||
* - Explorer aus Touransicht/WaypointListScreen → selectTour(tourName) bleibt unverändert
|
||||
*
|
||||
* Der Flag wird zurückgesetzt, sobald:
|
||||
* a) der Nutzer einen Wegpunkt im neutralen Kontext setzt (upsertFromNeutralExplorer)
|
||||
* b) der Nutzer explizit eine echte Tour über selectTour() auswählt
|
||||
*/
|
||||
fun selectNeutralTourForNewExplorer() {
|
||||
_neutralExplorerMode = true
|
||||
// Direkt auf DEFAULT setzen; selectTour() würde den Guard auslösen (DEFAULT nicht in
|
||||
// knownTours wenn nur echte Touren existieren), daher direktes Assignment.
|
||||
_selectedTour.value = de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME
|
||||
musicManager.loadTour(de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME)
|
||||
viewModelScope.launch {
|
||||
loadMusicSettingsForCurrentTour()
|
||||
loadTourDefaultsForCurrentTour()
|
||||
loadPersistedTrackForCurrentTour()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTour(tourName: String) {
|
||||
@@ -429,6 +593,16 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
val knownTours = _tourList.value
|
||||
val safeTarget = if (tourName in knownTours || knownTours.isEmpty()) tourName
|
||||
else knownTours.first()
|
||||
// v2.7.14 — Wechsel zu einer echten Tour deaktiviert den Neutral-Explorer-Modus.
|
||||
if (safeTarget != de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME) {
|
||||
_neutralExplorerMode = false
|
||||
}
|
||||
// v2.7.39 — Legacy-Aufräumaufruf bleibt erhalten, ist nach Entfernen
|
||||
// der festen manuellen Pause aber ein No-op. Aktiv laufende Wiedergabe
|
||||
// bleibt vom bloßen Tour-Wechsel weiterhin unberührt (Persistenz, s. u.).
|
||||
if (safeTarget != _selectedTour.value) {
|
||||
cancelManualGap()
|
||||
}
|
||||
_selectedTour.value = safeTarget
|
||||
// v2.5.30 — Playback-Persistenz beim Tour-Wechsel:
|
||||
// Laufende Wiedergabe (manuell oder GPS-gesteuert) wird beim blossen
|
||||
@@ -467,7 +641,12 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
// Use direct assignment instead of selectTour() to avoid triggering
|
||||
// music-loading coroutine before the tour list is fully stable.
|
||||
_selectedTour.value = trimmed
|
||||
// v2.7.14 — Neue echte Tour erstellt: Neutral-Explorer-Modus deaktivieren.
|
||||
if (trimmed != de.waypointaudio.data.Waypoint.DEFAULT_TOUR_NAME) {
|
||||
_neutralExplorerMode = false
|
||||
}
|
||||
// Reset manual player state
|
||||
cancelManualGap()
|
||||
manualPlayer.stop()
|
||||
_manualPlaying.value = false
|
||||
_manualCurrentName.value = null
|
||||
@@ -493,20 +672,78 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.7 — Spontane-Tour-Autocreate (v2.7.8: "Unbenannte Tour"):
|
||||
* Erzeugt eine neue echte Tour mit eindeutigem Namen ("Unbenannte Tour",
|
||||
* "Unbenannte Tour 2", usw.) und wählt sie sofort als selectedTour.
|
||||
* Wird aufgerufen, wenn im neutralen Explorer-Kontext (selectedTour ==
|
||||
* DEFAULT_TOUR_NAME) der erste Wegpunkt gesetzt wird.
|
||||
* Rückgabe: der Name der neu erzeugten Tour.
|
||||
* Interner Funktionsname bleibt createSpontaneousTour() für minimales Risiko.
|
||||
*/
|
||||
fun createSpontaneousTour(): String {
|
||||
val base = "Unbenannte Tour"
|
||||
val existing = _tourList.value
|
||||
var candidate = base
|
||||
var counter = 2
|
||||
while (candidate in existing) {
|
||||
candidate = "$base $counter"
|
||||
counter++
|
||||
}
|
||||
addTour(candidate) // setzt auch _selectedTour auf candidate
|
||||
return candidate
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.7.7 — Upsert-Wrapper für den neutralen Explorer-Kontext.
|
||||
* Wenn [waypoint].tourName == DEFAULT_TOUR_NAME (neutral), wird automatisch
|
||||
* eine neue Spontane Tour erzeugt, [waypoint] auf diese Tour umgeschrieben
|
||||
* und erst dann gespeichert. Danach ist selectedTour = neue Tour.
|
||||
* In allen anderen Kontexten verhält sich diese Funktion identisch zu [upsert].
|
||||
*/
|
||||
fun upsertFromNeutralExplorer(waypoint: Waypoint) {
|
||||
val isNeutral = waypoint.tourName.ifBlank { Waypoint.DEFAULT_TOUR_NAME } == Waypoint.DEFAULT_TOUR_NAME
|
||||
if (isNeutral) {
|
||||
val newTourName = createSpontaneousTour()
|
||||
val adjusted = waypoint.copy(tourName = newTourName)
|
||||
upsert(adjusted)
|
||||
} else {
|
||||
upsert(waypoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschiebt eine Tour in der Tab-Reihenfolge.
|
||||
* Index-Verschiebung: from → to. Ungültige Indizes werden ignoriert.
|
||||
* Persistiert die neue Reihenfolge sofort.
|
||||
*/
|
||||
fun moveTour(fromIndex: Int, toIndex: Int) {
|
||||
val current = _tourList.value.toMutableList()
|
||||
if (fromIndex !in current.indices || toIndex !in current.indices) return
|
||||
// v2.7.32 — Bugfix: Die Reorder-UI (TourReorderDialog) arbeitet auf
|
||||
// visibleTourList (Standard-Tour ausgeblendet), moveTour operierte aber
|
||||
// direkt auf _tourList (inkl. DEFAULT_TOUR_NAME). Lag die Standard-Tour
|
||||
// an Index 0, ergab sich ein Off-by-one, wodurch die erste und letzte
|
||||
// SICHTBARE Tour faktisch nicht mehr verschoben werden konnten.
|
||||
// Fix: Indizes beziehen sich auf die sichtbare Liste; die ausgeblendete
|
||||
// Standard-Tour behält ihre relative Position in _tourList.
|
||||
val full = _tourList.value
|
||||
val visible = full.filter { it != Waypoint.DEFAULT_TOUR_NAME }
|
||||
if (fromIndex !in visible.indices || toIndex !in visible.indices) return
|
||||
if (fromIndex == toIndex) return
|
||||
val item = current.removeAt(fromIndex)
|
||||
current.add(toIndex, item)
|
||||
_tourList.value = current
|
||||
|
||||
val reorderedVisible = visible.toMutableList()
|
||||
val item = reorderedVisible.removeAt(fromIndex)
|
||||
reorderedVisible.add(toIndex, item)
|
||||
|
||||
// Merge zurück nach _tourList: ausgeblendete Standard-Tour(en) an ihrer
|
||||
// ursprünglichen relativen Position erhalten, sichtbare Touren in neuer
|
||||
// Reihenfolge einsetzen.
|
||||
val iter = reorderedVisible.iterator()
|
||||
val merged = full.map { name ->
|
||||
if (name == Waypoint.DEFAULT_TOUR_NAME) name else iter.next()
|
||||
}
|
||||
_tourList.value = merged
|
||||
viewModelScope.launch {
|
||||
runCatching { repository.saveTours(current) }
|
||||
runCatching { repository.saveTours(merged) }
|
||||
.onFailure { e ->
|
||||
android.util.Log.e("WaypointViewModel", "Fehler beim Speichern der Tour-Reihenfolge", e)
|
||||
}
|
||||
@@ -558,10 +795,13 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
viewModelScope.launch {
|
||||
val updated = current.map { if (it == oldName) trimmed else it }
|
||||
repository.saveTours(updated)
|
||||
val updatedWps = _waypoints.value.map {
|
||||
// v2.7.21 — Race-Condition-Fix: Direkt aus DataStore lesen.
|
||||
val freshWps = runCatching { repository.waypoints.first() }
|
||||
.getOrDefault(_waypoints.value)
|
||||
val updatedWps = freshWps.map {
|
||||
if (it.tourName == oldName) it.copy(tourName = trimmed) else it
|
||||
}
|
||||
repository.saveAll(updatedWps)
|
||||
repository.saveAllForced(updatedWps)
|
||||
// v2.4.6 — Tourroute-Eintrag mitziehen, damit die Route nicht
|
||||
// verwaist unter dem alten Namen liegen bleibt.
|
||||
runCatching { repository.renameTourRouteKey(oldName, trimmed) }
|
||||
@@ -596,7 +836,14 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
|
||||
/** Speichert/ersetzt die Route für eine konkrete Tour (nicht zwingend die aktive). */
|
||||
/**
|
||||
* Speichert/ersetzt die Route für eine konkrete Tour (nicht zwingend die aktive).
|
||||
*
|
||||
* v2.7.21 — Waypoint-Preservation-Invariante:
|
||||
* Diese Funktion schreibt AUSSCHLIESSLICH in KEY_TOUR_ROUTES.
|
||||
* Audio-Wegpunkte (KEY_WAYPOINTS) werden durch diesen Aufruf NIEMALS verändert.
|
||||
* Der Routenplaner darf diese Funktion frei nutzen.
|
||||
*/
|
||||
fun saveTourRouteFor(tourName: String, route: de.waypointaudio.data.TourRoute) {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
@@ -726,6 +973,9 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
val nextTour = current.firstOrNull() ?: Waypoint.DEFAULT_TOUR_NAME
|
||||
if (_selectedTour.value == tourName) {
|
||||
// Reset manual player state
|
||||
// v2.7.39 — Legacy-Aufräumaufruf; nach Entfernen der festen
|
||||
// manuellen Pause gibt es keinen verzögerten Auto-Advance-Job mehr.
|
||||
cancelManualGap()
|
||||
manualPlayer.stop()
|
||||
_manualPlaying.value = false
|
||||
_manualCurrentName.value = null
|
||||
@@ -739,10 +989,17 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
repository.saveTours(current)
|
||||
val updatedWps = _waypoints.value.map {
|
||||
// v2.7.21 — Race-Condition-Fix: Direkt aus DataStore lesen statt
|
||||
// _waypoints.value, um sicherzustellen, dass keine Wegpunkte verloren
|
||||
// gehen, falls _waypoints.value noch nicht vollständig geladen ist.
|
||||
val freshWps = runCatching { repository.waypoints.first() }
|
||||
.getOrDefault(_waypoints.value)
|
||||
val updatedWps = freshWps.map {
|
||||
if (it.tourName == tourName) it.copy(tourName = Waypoint.DEFAULT_TOUR_NAME) else it
|
||||
}
|
||||
repository.saveAll(updatedWps)
|
||||
// Forciert schreiben, da dies eine bewusste Mutations-Operation ist
|
||||
// (Tour-Löschung mit Reassignment auf DEFAULT_TOUR_NAME).
|
||||
repository.saveAllForced(updatedWps)
|
||||
// Clear GPS track for the deleted tour
|
||||
repository.clearGpsTrack(tourName)
|
||||
// v2.4.6 — Gespeicherte Tourroute beim Löschen entfernen
|
||||
@@ -885,7 +1142,13 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
else base.withClips(newClips)
|
||||
}
|
||||
if (newWps.isNotEmpty()) {
|
||||
repository.saveAll(_waypoints.value + newWps)
|
||||
// v2.7.21 — Race-Condition-Fix: Statt _waypoints.value (könnte noch
|
||||
// leer sein, wenn der DataStore-Lade-Coroutine noch nicht feuerte)
|
||||
// direkt aus dem DataStore lesen. Das garantiert die vollständige
|
||||
// Wegpunktliste und verhindert versehentlichen Datenverlust.
|
||||
val currentWps = runCatching { repository.waypoints.first() }
|
||||
.getOrDefault(_waypoints.value)
|
||||
repository.saveAll(currentWps + newWps)
|
||||
}
|
||||
// Tour-Cover, Tour-Route und Tour-Defaults referenziell mitkopieren.
|
||||
runCatching {
|
||||
@@ -1193,49 +1456,68 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val ctx = getApplication<Application>()
|
||||
val gpsTracks = runCatching { repository.gpsTracks.first() }.getOrDefault(emptyMap())
|
||||
val tourDefaultsMap = runCatching {
|
||||
repository.tourDefaults.first()
|
||||
}.getOrDefault(emptyMap())
|
||||
val musicSettingsMap = runCatching {
|
||||
musicStore.settingsFlow.first()
|
||||
}.getOrDefault(emptyMap())
|
||||
// v2.7.36 FIX: Das gesamte Backup (Flow-Reads, ZIP-Aufbau und das
|
||||
// chunk-weise Kopieren aller Mediendateien) lief bisher direkt im
|
||||
// viewModelScope auf dem Main-Dispatcher. Bei Backups mit Medien
|
||||
// blockierte das den UI-Thread für Sekunden → ANR ("App reagiert
|
||||
// nicht / Absturz"), obwohl die Datei erfolgreich geschrieben wurde.
|
||||
// Wie der Import-Pfad läuft die schwere IO jetzt auf Dispatchers.IO;
|
||||
// die State-Updates für Erfolg/Fehler bleiben danach auf Main.
|
||||
val result = runCatching {
|
||||
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
val gpsTracks = runCatching { repository.gpsTracks.first() }.getOrDefault(emptyMap())
|
||||
val tourDefaultsMap = runCatching {
|
||||
repository.tourDefaults.first()
|
||||
}.getOrDefault(emptyMap())
|
||||
val musicSettingsMap = runCatching {
|
||||
musicStore.settingsFlow.first()
|
||||
}.getOrDefault(emptyMap())
|
||||
|
||||
val pkgInfo = runCatching {
|
||||
ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
||||
}.getOrNull()
|
||||
@Suppress("DEPRECATION")
|
||||
val versionCode = pkgInfo?.let {
|
||||
runCatching { it.longVersionCode.toInt() }
|
||||
.getOrDefault(it.versionCode)
|
||||
} ?: 0
|
||||
val pkgInfo = runCatching {
|
||||
ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
||||
}.getOrNull()
|
||||
@Suppress("DEPRECATION")
|
||||
val versionCode = pkgInfo?.let {
|
||||
runCatching { it.longVersionCode.toInt() }
|
||||
.getOrDefault(it.versionCode)
|
||||
} ?: 0
|
||||
|
||||
val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap())
|
||||
val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap())
|
||||
// v2.5.48 — Echte TrackDraft-Objekte für Backup lesen (nicht gpsTracks).
|
||||
val trackDraftsSnapshot = runCatching { repository.trackDrafts.first() }.getOrDefault(emptyList())
|
||||
val tourRoutesMap = runCatching { repository.tourRoutes.first() }.getOrDefault(emptyMap())
|
||||
val tourCoversMap = runCatching { repository.tourCovers.first() }.getOrDefault(emptyMap())
|
||||
// v2.5.48 — Echte TrackDraft-Objekte für Backup lesen (nicht gpsTracks).
|
||||
val trackDraftsSnapshot = runCatching { repository.trackDrafts.first() }.getOrDefault(emptyList())
|
||||
|
||||
val input = de.waypointaudio.io.BackupExportManager.BackupInput(
|
||||
waypoints = _waypoints.value,
|
||||
tours = _tourList.value,
|
||||
tourDefaults = tourDefaultsMap,
|
||||
gpsTracks = gpsTracks,
|
||||
musicSettings = musicSettingsMap,
|
||||
tourRoutes = tourRoutesMap,
|
||||
tourCovers = tourCoversMap,
|
||||
trackDrafts = trackDraftsSnapshot,
|
||||
appVersionName = pkgInfo?.versionName ?: "",
|
||||
appVersionCode = versionCode
|
||||
)
|
||||
val selection = de.waypointaudio.io.BackupExportManager.BackupSelection(
|
||||
toursAndWaypoints = toursAndWaypoints,
|
||||
trackDrafts = trackDrafts,
|
||||
mediaAndAtmo = mediaAndAtmo,
|
||||
settings = settings
|
||||
)
|
||||
de.waypointaudio.io.BackupExportManager.writeBackup(
|
||||
ctx, uri, selection, input
|
||||
)
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
// Falls trotz interner Absicherung etwas Unerwartetes durchschlägt
|
||||
// (z. B. SecurityException auf der Ziel-URI): nicht abstürzen,
|
||||
// sondern als nicht-fataler Fehler melden.
|
||||
de.waypointaudio.io.BackupExportManager.BackupResult(
|
||||
errorMessage = "Backup-Fehler: ${e.localizedMessage ?: e.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
|
||||
val input = de.waypointaudio.io.BackupExportManager.BackupInput(
|
||||
waypoints = _waypoints.value,
|
||||
tours = _tourList.value,
|
||||
tourDefaults = tourDefaultsMap,
|
||||
gpsTracks = gpsTracks,
|
||||
musicSettings = musicSettingsMap,
|
||||
tourRoutes = tourRoutesMap,
|
||||
tourCovers = tourCoversMap,
|
||||
trackDrafts = trackDraftsSnapshot,
|
||||
appVersionName = pkgInfo?.versionName ?: "",
|
||||
appVersionCode = versionCode
|
||||
)
|
||||
val selection = de.waypointaudio.io.BackupExportManager.BackupSelection(
|
||||
toursAndWaypoints = toursAndWaypoints,
|
||||
trackDrafts = trackDrafts,
|
||||
mediaAndAtmo = mediaAndAtmo,
|
||||
settings = settings
|
||||
)
|
||||
val result = de.waypointaudio.io.BackupExportManager.writeBackup(
|
||||
ctx, uri, selection, input
|
||||
)
|
||||
if (result.errorMessage != null) {
|
||||
_errorMessage.value = result.errorMessage
|
||||
} else {
|
||||
@@ -1352,7 +1634,9 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
// ---- Persistieren ----
|
||||
runCatching {
|
||||
if (toursAndWaypoints) {
|
||||
repository.saveAll(payload.mergedWaypoints)
|
||||
// v2.7.21 — Backup-Restore ist ein forcierter Vollersatz; Preservation-Guard
|
||||
// nicht anwenden (mergedWaypoints kann leer sein bei leeren Backups).
|
||||
repository.saveAllForced(payload.mergedWaypoints)
|
||||
if (payload.mergedTours != existingTours) {
|
||||
repository.saveTours(payload.mergedTours)
|
||||
}
|
||||
@@ -1655,6 +1939,10 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
*/
|
||||
private fun startPlayableAt(sequence: List<PlayableItem>, seqIndex: Int) {
|
||||
if (sequence.isEmpty()) return
|
||||
// v2.7.37 — Jede neue Playback-Entscheidung (Skip/Play/Resume) bricht
|
||||
// eine evtl. laufende Auto-Advance-Stille ab, damit kein verspäteter
|
||||
// Start den frisch gewählten Clip überschreibt.
|
||||
cancelManualGap()
|
||||
val safeIdx = seqIndex.coerceIn(0, sequence.size - 1)
|
||||
val item = sequence[safeIdx]
|
||||
_manualIndex.value = item.waypointIndex - 1
|
||||
@@ -1684,7 +1972,18 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
title = title,
|
||||
tour = _selectedTour.value
|
||||
)
|
||||
manualPlayer.play(ctx, item.uri) {
|
||||
manualPlayer.play(ctx, item.uri, onError = {
|
||||
// v2.7.38 — Wiedergabe konnte nicht starten (z. B. Samsung-SAF-/
|
||||
// Codec-Fehler). Zustand sauber zurücksetzen statt "spielt" ohne
|
||||
// Ton anzuzeigen, und den Nutzer informieren.
|
||||
cancelManualGap()
|
||||
_manualPlaying.value = false
|
||||
_manualCurrentName.value = null
|
||||
_currentPlayable.value = null
|
||||
stopProgressTicker(keepSnapshot = false)
|
||||
musicManager.afterWaypointAudio()
|
||||
_errorMessage.value = "Audio konnte nicht abgespielt werden. Bitte erneut versuchen."
|
||||
}) {
|
||||
if (!autoAdvance) {
|
||||
_manualPlaying.value = false
|
||||
_manualCurrentName.value = null
|
||||
@@ -1712,8 +2011,26 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
return@play
|
||||
}
|
||||
val nextItem = nextSeq[nextIdx]
|
||||
_currentPlayable.value = nextItem
|
||||
_manualIndex.value = nextItem.waypointIndex - 1
|
||||
// v2.7.39 — Keine feste Stille-Pause mehr: Der nächste Eintrag wird
|
||||
// direkt gestartet. Dadurch gibt es keinen verzögerten Job mehr, der
|
||||
// auf Samsung-Geräten oder nach Tour-/GPS-State-Wechseln in einen
|
||||
// veralteten Player-Zustand feuern könnte.
|
||||
if (!_manualPlaying.value || manualPlayer.isSingleMode) {
|
||||
return@play
|
||||
}
|
||||
val seqNow = playableSequence()
|
||||
val resolvedIdx = seqNow.indexOfFirst { it.compoundId == nextItem.compoundId }
|
||||
if (resolvedIdx < 0) {
|
||||
_manualPlaying.value = false
|
||||
_manualCurrentName.value = null
|
||||
_currentPlayable.value = null
|
||||
stopProgressTicker(keepSnapshot = false)
|
||||
musicManager.afterWaypointAudio()
|
||||
return@play
|
||||
}
|
||||
val resolved = seqNow[resolvedIdx]
|
||||
_currentPlayable.value = resolved
|
||||
_manualIndex.value = resolved.waypointIndex - 1
|
||||
playFromCurrentPlayable(autoAdvance = true)
|
||||
}
|
||||
startProgressTicker()
|
||||
@@ -1737,6 +2054,7 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
|
||||
/** Stop manual playback completely (both playlist and single mode). */
|
||||
fun manualStop() {
|
||||
cancelManualGap()
|
||||
manualPlayer.stop()
|
||||
_manualPlaying.value = false
|
||||
_singlePlayingWaypointId.value = null
|
||||
@@ -1816,6 +2134,11 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
val wpName = waypoint.name.ifBlank { "Wegpunkt" }
|
||||
val displayName = if (clipTitle.isBlank()) wpName else "$wpName · $clipTitle"
|
||||
|
||||
// v2.7.37 — Einzelclip-Vorschau übernimmt: eine evtl. laufende
|
||||
// Auto-Advance-Stille der Tour-Sequenz abbrechen, sonst würde der
|
||||
// verzögerte Start die Vorschau überschreiben.
|
||||
cancelManualGap()
|
||||
|
||||
// Tour-Play beenden, falls aktiv.
|
||||
if (_manualPlaying.value && !manualPlayer.isSingleMode) {
|
||||
manualPlayer.stop()
|
||||
@@ -2362,5 +2685,6 @@ class WaypointViewModel(application: Application) : AndroidViewModel(application
|
||||
* Akku spürbar gegenüber häufigeren Ticks.
|
||||
*/
|
||||
private const val PROGRESS_TICK_MS = 500L
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,10 @@ Viele Grüße</string>
|
||||
<string name="version_notice_body_v2523">Neu in 2.5.23: Lizenzimport-Regression-Fix. Erstimport nach Neuinstallation funktioniert wieder zuverlässig. Dateiauswahl nutzt wieder OpenDocument() (echter Datei-Browser) statt GetContent() (das auf manchen Geräten nur Medien anzeigt). Reimport derselben Datei nach „Lizenz entfernen“ funktioniert via importNonce-Mechanismus. Fehlermeldungen (aus v2.5.22) bleiben differenziert. Geräte-ID und alle anderen Funktionen aus v2.5.21/v2.5.22 bleiben unverändert.</string>
|
||||
<string name="version_notice_body_v2521">Neu in 2.5.21: Lizenz-Re-Import-Bugfix und Geräte-ID. Der Lizenzimport funktioniert jetzt zuverlässig auch nach Entfernen und erneutem Importieren einer Lizenzdatei. Außerdem zeigt der Lizenz-Bereich jetzt eine Geräte-ID an (SHA-256-Hash) — für spätere optionale gerätegebundene Lizenzen. In dieser Version ist keine harte Gerätebindung aktiv. Alle bisherigen Funktionen aus v2.5.20 bleiben unverändert.</string>
|
||||
<string name="version_notice_body_v2519">Neu in 2.5.19: Menü- und UX-Politur. Der Eintrag „Track Editor" wurde aus dem Hauptmenü entfernt – die Funktion ist über den Explorer weiterhin vollständig erreichbar. „Klassische Ansicht" heißt jetzt „Bearbeitungsmodus" (klarere Begriffswahl). „Touren ordnen" liegt jetzt im Menü der Audio-Bibliothek (Hauptseite) und nicht mehr in der einzelnen Touransicht. Tour-Cover (Bild, Icon, Palette) werden weiterhin als Teil des ZIP-Backups exportiert und beim Import wiederhergestellt. Alle bisherigen Funktionen – additiver GPX-Import, Lizenz/Pro/About, Multi-Clip, Sequenzplayer, Countdown, maximierbarer Player, Tour duplizieren, Waypoint kopieren, Explorer/MapLibre/Routing, PTT/Atmo, Hintergrundwiedergabe – bleiben unverändert.</string>
|
||||
<!-- v2.6.0 Version-Notice -->
|
||||
<string name="version_notice_body_v2716">Neu in 2.7.16 (RoutePlanner Restore): Der direkte Routen-Planer (Start/Ziel/Via, Planlinie, Online-Routing) war in v2.7.15 faelschlicherweise entfernt worden. Er ist jetzt wiederhergestellt. Es gibt nun zwei klar benannte Einstiege: (1) "Route planen" (AltRoute-Icon, blau) = RoutePlannerSheet mit Start/Ziel/Via, Planlinie und optionalem Online-Routing via OSRM; (2) "Tourverlauf bearbeiten" (Stift-Icon) = TourverlaufEditorSheet fuer manuelles Zeichnen/Setzen von Verlaufspunkten. Beide sind als FAB auf der Karte und im Layer-Panel zugaenglich. Karten-Rendering, Waypoints, Planlinie, Explorer-Kontext-Isolation: keine Aenderung.</string>
|
||||
<string name="version_notice_body_v2715">Neu in 2.7.15 (Tourverlauf-Menü-Merge): Die beiden Menüpunkte „Tourverlauf Anpassung“ (Routenplaner) und „Tourverlauf bearbeiten“ (Editor) sind zu einem einzigen Einstieg zusammengeführt. Es gibt jetzt nur noch einen FAB-Knopf „Tourverlauf“ im Explorer — der öffnet den Tourverlauf-Editor, der Zeichnen, Punkte setzen und (optional) Online-Routing in einem Sheet bietet. Das Layer-Panel zeigt ebenfalls nur noch den einheitlichen Tourverlauf-Einstieg. Alle internen Funktionen (Planlinie, Online-Routing via OSRM, Speichern/Löschen des Tourverlaufs) bleiben vollständig erhalten. Karten-Rendering, Waypoints, Planlinie, Explorer-Kontext-Isolation: keine Änderung.</string>
|
||||
<string name="version_notice_body_v260">Neu in 2.6.0 (Tourverlauf/Storyboard): Der bisherige Routenplaner heißt jetzt „Tourverlauf“ — klar als manueller Verlauf ohne Auto-Navi-Erwartung. Verlaufspunkte (stille Orientierungspunkte) und Audio-Wegpunkte werden in Labels und Erklärtexten klar unterschieden. Online-Routing (OSRM) bleibt erhalten, wird aber als optional/experimentell eingestuft — die Planlinie genügt für den Tourverlauf. Das Storyboard-Panel erklärt den Unterschied: Tourverlaufspunkte = Linie, Wegpunkte = Audio. Keine Änderung an Logik, Backup, Lizenz, radiusbasiertem Playback oder GPS-Aufzeichnung. Alle v2.5.48-Funktionen bleiben vollständig erhalten.</string>
|
||||
<string name="version_notice_body_v242">Neu in 2.5.1: Design-Harmonisierung der sekundären Bereiche. Die Tour-/Waypoint-Ansicht, klassische Hauptansicht, PTT-Bereich, Atmo/Stream-Playlist, Track-Entwürfe, Backup/Import sowie About-Dialog nutzen jetzt die gleiche GPS2Audio-Farbwelt wie die neue Startseite: dunkle Surface-Karten, türkise Primäraktionen, magenta/destruktiv, grün/aktiv, weichere Cards und kompaktere Listen. Tabs, Top-App-Bars und Dialoge wirken ruhiger und moderner, ohne dass Funktionen entfallen: Explorer, MapLibre, Routenplaner, POI, PTT, Atmo/Stream-Playlist, Recorder, Backup/Import, About/PayPal, Drag-and-drop/Sortierung, TourRoute-Persistenz und Classic-Fallback bleiben unverändert erreichbar. Wegpunkte lassen sich weiterhin direkt per Drag-and-drop verschieben.</string>
|
||||
<string name="version_notice_body">Neu in 2.4.0: MapLibre Engine Cutover — der Explorer rendert jetzt aktiv mit MapLibre Native (org.maplibre.gl 11.x), nicht mehr mit osmdroid. Wegpunkte, Standort/Follow, Trigger-Radius/Audiozone, GPS-Track, Drafts, Storyboard, Routing/POI-MVP, Suche und Tap-Editiermodus sind 1:1 portiert. Echter Pitch/Tilt für den Perspektiv-Modus über MapLibre-Kamera. Clip-Längenlinien werden ab jetzt nur noch entlang einer bekannten Geometrie (aufgezeichneter Track/Draft oder berechnete OSRM-Route) gezeichnet — keine freien Projektionen mehr. Bestehende Funktionen (Backup, PTT, Atmo, Audio-Bibliothek, Touren ordnen, Drag-and-drop, About/PayPal) unverändert. Der osmdroid-Renderer bleibt im Quelltext als dokumentierter Fallback erhalten, ist aber nicht mehr sichtbar verdrahtet.</string>
|
||||
|
||||
@@ -310,17 +314,19 @@ Viele Grüße</string>
|
||||
<string name="map_layers_satellite_info_button">Info / Quelle</string>
|
||||
<string name="map_layers_satellite_info">Satellit nutzt standardmäßig ESRI World Imagery (kostenfrei für persönliche/nicht-kommerzielle Nutzung, Attribution „Tiles © Esri\"). Eigene Tile-URL ist möglich, z. B. self-hosted oder ein anderer Anbieter. Es werden keine kostenpflichtigen APIs erzwungen.</string>
|
||||
<string name="map_layers_3d_preview_note">Perspektive aktiv: geneigte Kameraansicht als Vorschau. Echtes 3D-Terrain (DEM) folgt mit dem MapLibre-Native-Renderer.</string>
|
||||
<string name="map_layers_routing_label">Routenplaner</string>
|
||||
<string name="map_layers_routing_prepared">Folgt — kein externer Routingdienst aktiv. Self-hosted OSRM/Valhalla/GraphHopper geplant.</string>
|
||||
<string name="map_layers_routing_label">Tourverlauf-Planer</string>
|
||||
<string name="map_layers_routing_prepared">Kein externer Routingdienst aktiv. Online-Routing optional via Self-hosted OSRM/Valhalla/GraphHopper.</string>
|
||||
<string name="map_provider_maplibre_demo">MapLibre Demotiles (frei)</string>
|
||||
<string name="routing_dialog_title">Routenplaner folgt</string>
|
||||
<string name="routing_dialog_body">GPS2Audio v2.3.0 enthält bewusst keine Routing-Engine. Ein vollwertiger Routenplaner würde entweder einen externen, kostenpflichtigen Dienst benötigen (vermieden) oder eine self-hosted Engine (z. B. OSRM, Valhalla, GraphHopper) bzw. einen Offline-Routing-Ansatz. Die nächste Version bindet eine der genannten Engines an – das vorhandene Storyboard bleibt Eingabe.</string>
|
||||
<string name="routing_dialog_title">Tourverlauf-Planer</string>
|
||||
<string name="routing_dialog_body">GPS2Audio verwendet keinen Navigations-Routenplaner. Der Tourverlauf wird manuell gesetzt — keine Online-Routing-Pflicht, keine Kosten. Optionales Online-Routing via Self-hosted OSRM/Valhalla/GraphHopper kann konfiguriert werden.</string>
|
||||
<string name="map_attribution_satellite_esri">Tiles © Esri</string>
|
||||
<string name="map_attribution_satellite_custom">Satellit: eigene Tile-Quelle</string>
|
||||
<string name="explorer_waypoint_audio_duration">Audio-Dauer: %1$s</string>
|
||||
|
||||
<!-- Explorer (v2.2.0) -->
|
||||
<string name="explorer_title">Explorer</string>
|
||||
<!-- v2.7.6 — Spontane Tour → v2.7.8 umbenannt: UI-Label wenn kein echter Tourname aktiv ist (DEFAULT_TOUR_NAME intern) -->
|
||||
<string name="explorer_spontane_tour">Unbenannte Tour</string>
|
||||
<string name="explorer_open">Explorer öffnen</string>
|
||||
<string name="explorer_subtitle">Karte · Standort · Wegpunkte (read-only)</string>
|
||||
<string name="explorer_hint_no_location">Standort nicht verfügbar — Berechtigung oder GPS prüfen.</string>
|
||||
@@ -347,22 +353,25 @@ Viele Grüße</string>
|
||||
<string name="explorer_cancel_tap">Tap-Modus abbrechen</string>
|
||||
<string name="menu_legacy_map_editor">Karteneditor (Track / Drafts)</string>
|
||||
|
||||
<!-- v2.2.3 — Explorer als zentraler Arbeitsbereich: GPS-Recording, Drafts, Storyboard -->
|
||||
<string name="explorer_subtitle_v223">Karte · Standort · Wegpunkte · GPS-Aufzeichnung · Drafts · Storyboard</string>
|
||||
<!-- v2.2.3 — Explorer als zentraler Arbeitsbereich: GPS-Recording, Drafts, Tourverlauf -->
|
||||
<string name="explorer_subtitle_v223">Karte · Standort · Wegpunkte · GPS-Aufzeichnung · Drafts · Tourverlauf</string>
|
||||
<string name="explorer_subtitle_recording">Aufzeichnung läuft · %1$d Punkte</string>
|
||||
<string name="explorer_subtitle_draft_active">Draft aktiv: %1$s · %2$d Punkte · %3$d markiert</string>
|
||||
<string name="explorer_drafts_open">Aufgenommene Tracks (Drafts)</string>
|
||||
<string name="explorer_drafts_title">Aufgenommene Tracks</string>
|
||||
<string name="explorer_drafts_hint">Wähle einen Draft, um seine Punkte direkt im Explorer zu sehen und zu bearbeiten. Detaillierte Bearbeitung im Vollbild-Editor weiterhin verfügbar.</string>
|
||||
<string name="explorer_storyboard_open">Storyboard / Routenplanung</string>
|
||||
<string name="explorer_storyboard_title">Storyboard / Routenplanung</string>
|
||||
<string name="explorer_storyboard_hint">Bring die Wegpunkte der aktuellen Tour in eine gewünschte Reihenfolge und speichere sie als neue (oder bestehende) Tour. Kein Straßen-Routing — eine konzeptionelle Tour-Zusammenstellung.</string>
|
||||
<!-- v2.6.0 — Tourverlauf/Storyboard (umbenannt von Storyboard / Routenplanung) -->
|
||||
<string name="explorer_storyboard_open">Tourverlauf (Storyboard)</string>
|
||||
<string name="explorer_storyboard_title">Tourverlauf</string>
|
||||
<string name="explorer_storyboard_hint">Ordne die Audio-Wegpunkte entlang der Route in die gewünschte Reihenfolge. Tourpunkte ohne Audio formen die Linie, Wegpunkte mit Audio lösen den Ton aus.</string>
|
||||
<string name="explorer_storyboard_target_tour">Zieltour (Name)</string>
|
||||
<string name="explorer_storyboard_empty">Keine Wegpunkte in der aktuellen Tour. Lege erst welche im Explorer an.</string>
|
||||
<string name="explorer_storyboard_empty">Keine Wegpunkte in der aktuellen Tour. Lege erst Wegpunkte im Explorer an.</string>
|
||||
<string name="explorer_storyboard_up">Nach oben</string>
|
||||
<string name="explorer_storyboard_down">Nach unten</string>
|
||||
<string name="explorer_storyboard_save">Reihenfolge übernehmen</string>
|
||||
<string name="explorer_storyboard_no_routing_hint">Hinweis: Es wird keine externe Routing-Engine verwendet (keine API, keine Kosten). Das Storyboard ist eine geordnete Wegpunkt-Liste — Entwurfsstatus.</string>
|
||||
<string name="explorer_storyboard_no_routing_hint">Tourpunkte ohne Audio formen die Routenlinie. Wegpunkte mit Audio lösen den Ton aus. Audio kann jederzeit nachträglich hinzugefügt werden.</string>
|
||||
<!-- v2.6.0 — neuer Erklärtext für den Tourverlauf-Bereich -->
|
||||
<string name="explorer_storyboard_explainer">Route zuerst: Tourpunkte formen die Linie. Audio wird bewusst an einzelne Punkte angehängt.</string>
|
||||
<string name="explorer_track_point_title">Trackpunkt %1$d</string>
|
||||
<string name="explorer_track_point_no_draft_hint">Aktiviere zuerst einen Draft im Drafts-Sheet, um Trackpunkte zu bearbeiten.</string>
|
||||
<string name="explorer_tap_move_track_active">Tippe die neue Position an — der gewählte Trackpunkt wird dorthin verschoben.</string>
|
||||
@@ -454,6 +463,25 @@ Viele Grüße</string>
|
||||
<string name="music_for_tour">Tour: %1$s</string>
|
||||
<string name="music_enable">Begleitmusik aktivieren</string>
|
||||
|
||||
<!-- v2.7.33 — Musik-Karte (UI-Politur, rein gestalterisch) -->
|
||||
<string name="music_card_heading">Musik zur Tour</string>
|
||||
<string name="music_status_on">Aktiv für diese Tour</string>
|
||||
<string name="music_status_off">Aus – tippe zum Aktivieren</string>
|
||||
<string name="music_summary_local">%1$d Titel in der Playlist</string>
|
||||
<string name="music_summary_local_one">1 Titel in der Playlist</string>
|
||||
<string name="music_summary_stream">%1$d Sender / Streams</string>
|
||||
<string name="music_summary_stream_one">1 Sender / Stream</string>
|
||||
<string name="music_summary_empty">Noch keine Musik ausgewählt</string>
|
||||
<string name="music_choose_primary">Musik auswählen</string>
|
||||
<string name="music_empty_state_title">Diese Tour hat noch keine Musik</string>
|
||||
<string name="music_empty_state_body">Lege Titel aus deiner Bibliothek oder vom Gerät an – oder füge einen Radio-Stream hinzu. Die Musik begleitet die Tour und tritt bei Ansagen automatisch in den Hintergrund.</string>
|
||||
<string name="music_section_source">Quelle</string>
|
||||
<string name="music_source_library_chip">Bibliothek</string>
|
||||
<string name="music_source_files_chip">Dateien</string>
|
||||
<string name="music_source_stream_chip">Stream</string>
|
||||
<string name="music_section_advanced">Verhalten & Optionen</string>
|
||||
<string name="music_behavior_short_label">Bei Ansagen</string>
|
||||
|
||||
<!-- Quelle -->
|
||||
<string name="music_source_label">Musikquelle</string>
|
||||
<string name="music_source_local">Lokale Playlist</string>
|
||||
@@ -509,6 +537,12 @@ Viele Grüße</string>
|
||||
<string name="music_behavior_continue">Normal unterlegen</string>
|
||||
<string name="music_behavior_continue_desc">Musik läuft auf normaler Lautstärke parallel zum Wegpunkt-Ton.</string>
|
||||
|
||||
<!-- v2.7.33 — Klartext-Titel für die Verhaltensoptionen (gleiche Funktionen) -->
|
||||
<string name="music_behavior_pause_resume_plain">Bei Ansagen pausieren</string>
|
||||
<string name="music_behavior_fade_plain">Sanft aus- und einblenden</string>
|
||||
<string name="music_behavior_duck_plain">Leiser im Hintergrund</string>
|
||||
<string name="music_behavior_continue_plain">Weiterlaufen lassen</string>
|
||||
|
||||
<!-- Fade und Duck Parameter -->
|
||||
<string name="music_fade_duration_label">Fade-Dauer: %1$d ms</string>
|
||||
<string name="music_duck_volume_label">Lautstärke während Wegpunkt: %1$d %%</string>
|
||||
@@ -704,8 +738,8 @@ Viele Grüße</string>
|
||||
<string name="explorer_renderer_maplibre_badge">MapLibre · Engine v2.4</string>
|
||||
|
||||
<!-- v2.3.1 — Routing/POI MVP -->
|
||||
<string name="routing_poi_open">Routing & POI</string>
|
||||
<string name="routing_poi_title">Routing & Online-POI (MVP)</string>
|
||||
<string name="routing_poi_open">Tourverlauf & POI</string>
|
||||
<string name="routing_poi_title">Tourverlauf & Online-POI</string>
|
||||
<string name="routing_poi_search_label">POI-Suche (online)</string>
|
||||
<string name="routing_poi_search_hint">z. B. „bäckerei", „museum", „bahnhof"</string>
|
||||
<string name="routing_poi_search_button">Suchen</string>
|
||||
@@ -713,64 +747,76 @@ Viele Grüße</string>
|
||||
<string name="routing_poi_search_no_results">Keine POIs gefunden.</string>
|
||||
<string name="routing_poi_search_error">Suche fehlgeschlagen — bitte Verbindung/Server prüfen.</string>
|
||||
<string name="routing_poi_search_attribution">POI-Quelle: OpenStreetMap · Overpass/Nominatim · Bitte sparsam abfragen.</string>
|
||||
<string name="routing_poi_add_waypoint">Als Wegpunkt</string>
|
||||
<string name="routing_poi_add_storyboard">Ins Storyboard</string>
|
||||
<string name="routing_poi_add_waypoint">Als Audio-Wegpunkt</string>
|
||||
<string name="routing_poi_add_storyboard">In Tourverlauf</string>
|
||||
<string name="routing_poi_distance">%1$s entfernt</string>
|
||||
<string name="routing_endpoint_label">Routing-Endpunkt (OSRM-kompatibel)</string>
|
||||
<string name="routing_endpoint_label">Optionaler Routing-Endpunkt (OSRM-kompatibel)</string>
|
||||
<string name="routing_endpoint_hint">URL, z. B. https://router.project-osrm.org (Demo, Limits) oder eigene self-hosted Instanz.</string>
|
||||
<string name="routing_endpoint_save">Speichern</string>
|
||||
<string name="routing_endpoint_not_configured">Routing-Endpunkt nicht konfiguriert — Storyboard-Linie wird als Luftlinie/Planlinie angezeigt.</string>
|
||||
<string name="routing_endpoint_not_configured">Kein Routing-Endpunkt — Tourverlauf wird als Planlinie (Luftlinie) angezeigt. Online-Routing optional.</string>
|
||||
<string name="routing_endpoint_demo_warning">Demo-OSRM-Server (router.project-osrm.org) hat Rate-Limits und ist nicht für Produktion gedacht.</string>
|
||||
<string name="routing_compute_button">Route berechnen</string>
|
||||
<string name="routing_compute_button">Route berechnen (optional)</string>
|
||||
<string name="routing_compute_loading">Route wird berechnet …</string>
|
||||
<string name="routing_compute_error">Routenberechnung fehlgeschlagen — Endpunkt prüfen.</string>
|
||||
<string name="routing_compute_no_points">Mindestens zwei Wegpunkte im Storyboard nötig.</string>
|
||||
<string name="routing_compute_success">Route geladen (%1$s · %2$s).</string>
|
||||
<string name="routing_planline_hint">Planlinie (Luftlinie). Für echte Straßenrouten Endpunkt konfigurieren.</string>
|
||||
|
||||
<!-- v2.4.2 — Explorer-Routenplaner -->
|
||||
<string name="route_planner_title">Routenplanung</string>
|
||||
<string name="route_planner_subtitle">Start, Via-Punkte und Ziel wählen, Route berechnen oder Planlinie nutzen.</string>
|
||||
<string name="route_planner_open">Routenplanung öffnen</string>
|
||||
<string name="route_planner_open_short">Routenplaner</string>
|
||||
<string name="route_planner_section_start">Start</string>
|
||||
<string name="route_planner_section_via">Via-Stopps</string>
|
||||
<string name="route_planner_section_destination">Ziel</string>
|
||||
<string name="route_planner_empty_start">Noch kein Start gewählt</string>
|
||||
<string name="route_planner_empty_destination">Noch kein Ziel gewählt</string>
|
||||
<string name="route_planner_empty_via">Keine Via-Stopps. Beliebige Zwischenstopps lassen sich oben hinzufügen.</string>
|
||||
<string name="route_planner_choose_start">Start wählen</string>
|
||||
<string name="route_planner_choose_destination">Ziel wählen</string>
|
||||
<string name="route_planner_add_via">Via-Stopp hinzufügen</string>
|
||||
<string name="route_planner_clear">Plan zurücksetzen</string>
|
||||
<string name="route_planner_compute">Route berechnen</string>
|
||||
<string name="route_planner_planline">Nur Planlinie zeigen</string>
|
||||
<string name="route_planner_planline_active">Planlinie wird angezeigt.</string>
|
||||
<string name="route_planner_planline_hint">Planlinie ist eine Luftlinie zwischen den Stops — sie folgt keinen Straßen.</string>
|
||||
<string name="route_planner_route_summary">Route: %1$s · %2$s</string>
|
||||
<string name="route_planner_planline_summary">Planlinie: %1$s (Luftlinie)</string>
|
||||
<string name="route_planner_save_as_tour">Als Tour-Entwurf speichern</string>
|
||||
<string name="route_planner_save_as_tour_done">Tour-Entwurf %1$s gespeichert.</string>
|
||||
<string name="route_planner_save_as_tour_error">Tour-Entwurf konnte nicht gespeichert werden.</string>
|
||||
<string name="route_planner_save_as_tour_dialog_title">Tour-Entwurf speichern</string>
|
||||
<!-- v2.4.2 — Explorer-Routenplaner / v2.6.0 — umbenannt zu Tourverlauf / v2.7.24 — UX Polish: Freie Linie primär -->
|
||||
<string name="route_planner_title">Tourverlauf erstellen</string>
|
||||
<string name="route_planner_subtitle">Zeichne zuerst den Verlauf deiner Tour. Audio-Punkte setzt du danach.</string>
|
||||
<string name="route_planner_open">Tourverlauf öffnen</string>
|
||||
<string name="route_planner_open_short">Route</string>
|
||||
<string name="route_planner_section_start">Start der Route</string>
|
||||
<string name="route_planner_section_via">Route über Punkte formen</string>
|
||||
<string name="route_planner_section_destination">Ziel der Route</string>
|
||||
<string name="route_planner_empty_start">Wo beginnt die Tour?</string>
|
||||
<string name="route_planner_empty_destination">Wo endet die Tour?</string>
|
||||
<string name="route_planner_empty_via">Noch keine Punkte gesetzt. Tippe Punkte auf die Karte, um den Verlauf zu lenken. Keine Audio-Punkte.</string>
|
||||
<string name="route_planner_choose_start">Start der Route wählen</string>
|
||||
<string name="route_planner_choose_destination">Ziel der Route wählen</string>
|
||||
<string name="route_planner_add_via">Punkt hinzufügen</string>
|
||||
<string name="route_planner_clear">Verlauf zurücksetzen</string>
|
||||
<string name="route_planner_compute">Online-Route berechnen</string>
|
||||
<string name="route_planner_planline">Verlaufslinie anzeigen</string>
|
||||
<string name="route_planner_planline_active">Routenlinie wird angezeigt (Luftlinie zwischen den Tourpunkten).</string>
|
||||
<string name="route_planner_planline_hint">Routenlinie = direkte Linie zwischen den Tourpunkten. Für Straßen-Routing: optionalen OSRM-Endpunkt konfigurieren.</string>
|
||||
<string name="route_planner_route_summary">Berechnete Route: %1$s · %2$s</string>
|
||||
<string name="route_planner_planline_summary">Tourlinie: %1$s (Luftlinie)</string>
|
||||
<string name="route_planner_save_as_tour">Als Tourentwurf speichern</string>
|
||||
<string name="route_planner_save_as_tour_done">Tourentwurf %1$s gespeichert.</string>
|
||||
<string name="route_planner_save_as_tour_error">Tourentwurf konnte nicht gespeichert werden.</string>
|
||||
<string name="route_planner_save_as_tour_dialog_title">Tourentwurf speichern</string>
|
||||
<string name="route_planner_save_as_tour_dialog_hint">Name der neuen Tour</string>
|
||||
<string name="route_planner_add_stops_as_waypoints">Punkte als Wegpunkte übernehmen</string>
|
||||
<string name="route_planner_add_stops_as_waypoints">Tourpunkte mit Audio versehen</string>
|
||||
<!-- v2.7.19 — Manuelles Zeichnen im Tourplaner / v2.7.24 — Primäres Werkzeug, kein Fallback-Label -->
|
||||
<string name="route_planner_draw_section_title">Tourverlauf frei zeichnen</string>
|
||||
<string name="route_planner_draw_section_hint">Das ursprüngliche Werkzeug: Linie direkt auf der Karte zeichnen — kein Netz, kein Server nötig. Ideal für freie Routen abseits der Straße. (Intern auch als Fallback verfügbar, wenn kein Routing möglich ist.)</string>
|
||||
<string name="route_planner_draw_open_editor">Tourverlauf frei zeichnen</string>
|
||||
<!-- v2.7.20 — Route formen: Zwischenpunkt-Karten-Tippen -->
|
||||
<string name="route_planner_shaping_section_title">Route über Punkte formen</string>
|
||||
<string name="route_planner_shaping_section_hint">Tippe Punkte auf die Karte, um den Verlauf zu lenken. Das erzeugt keine Audio-Punkte. Start/Ziel müssen zuerst gesetzt sein.</string>
|
||||
<string name="route_planner_shaping_activate">Punkt auf Karte tippen</string>
|
||||
<string name="route_planner_shaping_activate_hint">Modus aktiv — nächster Tipp auf die Karte setzt einen Formungspunkt.</string>
|
||||
<string name="route_planner_shaping_no_start_dest">Zuerst Start und Ziel setzen, dann Punkte ergänzen.</string>
|
||||
<string name="explorer_tap_add_via_active">Karte antippen → Zwischenpunkt setzen (kein Audio-Waypoint). Abbrechen: Cancel.</string>
|
||||
<string name="route_planner_add_stops_done">%1$d Wegpunkte hinzugefügt.</string>
|
||||
<string name="route_planner_no_route">Noch keine berechnete Route. Endpunkt konfigurieren und berechnen, oder Planlinie nutzen.</string>
|
||||
<string name="route_planner_endpoint_not_configured">Routing-Endpunkt nicht konfiguriert — nur Planlinie verfügbar.</string>
|
||||
<string name="route_planner_endpoint_configure">Endpunkt einrichten</string>
|
||||
<string name="route_planner_status_ready">Bereit. Stops wählen und berechnen.</string>
|
||||
<string name="route_planner_no_route">Noch kein Verlauf sichtbar. Freie Linie zeichnen oder Punkte setzen — Online-Routing ist optional.</string>
|
||||
<string name="route_planner_endpoint_not_configured">Kein Routing-Endpunkt gesetzt. Online-Routing ist optional.</string>
|
||||
<string name="route_planner_endpoint_configure">Routing-Endpunkt einrichten</string>
|
||||
<string name="route_planner_status_ready">Tourpunkte setzen, dann Routenlinie anzeigen. Online-Routing ist optional.</string>
|
||||
<string name="route_planner_role_start">Start</string>
|
||||
<string name="route_planner_role_via">Via %1$d</string>
|
||||
<string name="route_planner_role_via">Formungspunkt %1$d</string>
|
||||
<string name="route_planner_section_via_points">Formungspunkte</string>
|
||||
<string name="route_planner_role_destination">Ziel</string>
|
||||
<string name="route_planner_picker_title">Quelle wählen</string>
|
||||
<string name="route_planner_picker_title">Punkt-Quelle wählen</string>
|
||||
<string name="route_planner_picker_current_location">Aktueller Standort</string>
|
||||
<string name="route_planner_picker_current_location_unavailable">Aktueller Standort: noch keine Position</string>
|
||||
<string name="route_planner_picker_map_center">Kartenmitte</string>
|
||||
<string name="route_planner_picker_waypoint">Vorhandenen Wegpunkt wählen</string>
|
||||
<string name="route_planner_picker_waypoint">Audio-Wegpunkt als Tourpunkt</string>
|
||||
<string name="route_planner_picker_waypoint_empty">Keine Wegpunkte in dieser Tour</string>
|
||||
<string name="route_planner_picker_poi">Online-POI suchen</string>
|
||||
<string name="route_planner_picker_manual">Eigene Koordinate eingeben</string>
|
||||
<string name="route_planner_picker_manual">Koordinate manuell eingeben</string>
|
||||
<string name="route_planner_picker_search">Ortssuche (Nominatim)</string>
|
||||
<string name="route_planner_manual_title">Koordinate eingeben</string>
|
||||
<string name="route_planner_manual_lat">Breitengrad (Lat)</string>
|
||||
@@ -783,65 +829,102 @@ Viele Grüße</string>
|
||||
<string name="route_planner_label_current_location">Mein Standort</string>
|
||||
<string name="route_planner_label_map_center">Kartenmitte</string>
|
||||
<string name="route_planner_label_manual">Eigene Koordinate</string>
|
||||
<string name="route_planner_status_error">Routing fehlgeschlagen — Endpunkt prüfen.</string>
|
||||
<string name="route_planner_status_error_with_detail">Routing fehlgeschlagen: %1$s — Endpunkt oder Netzwerk prüfen.</string>
|
||||
<string name="route_planner_status_endpoint_missing">Kein Routing-Endpunkt gesetzt. Mit „OSRM-Demo nutzen\" startest du sofort einen Testlauf, oder richte unter „Endpunkt einrichten\" einen eigenen Server ein.</string>
|
||||
<string name="route_planner_use_demo_endpoint">OSRM-Demo nutzen</string>
|
||||
<string name="route_planner_use_demo_endpoint_hint">Setzt einen öffentlichen OSRM-Testserver (router.project-osrm.org). Nur für Tests/Demo, Rate-Limits, keine Garantie. Bitte sparsam verwenden.</string>
|
||||
<string name="route_planner_demo_endpoint_set">Demo-Endpunkt aktiv (router.project-osrm.org). Jetzt „Route berechnen\" drücken.</string>
|
||||
<string name="route_planner_status_error">Online-Routing fehlgeschlagen — Endpunkt prüfen. Planlinie weiterhin verfügbar.</string>
|
||||
<string name="route_planner_status_error_with_detail">Online-Routing fehlgeschlagen: %1$s — Endpunkt oder Netzwerk prüfen. Planlinie weiterhin verfügbar.</string>
|
||||
<string name="route_planner_status_endpoint_missing">Kein Routing-Endpunkt gesetzt — Planlinie (Luftlinie) ist verfügbar. Optional: Mit „OSRM-Demo“ starten oder eigenen Server einrichten.</string>
|
||||
<string name="route_planner_use_demo_endpoint">OSRM-Demo (optional, experimentell)</string>
|
||||
<string name="route_planner_use_demo_endpoint_hint">Setzt einen öffentlichen OSRM-Testserver (router.project-osrm.org). Nur für Tests/Demo, Rate-Limits, keine Garantie. Online-Routing ist nicht notwendig — die Planlinie genügt für den Tourverlauf.</string>
|
||||
<string name="route_planner_demo_endpoint_set">Demo-Endpunkt aktiv (router.project-osrm.org). Jetzt „Route berechnen“ drücken oder Planlinie nutzen.</string>
|
||||
<string name="routing_endpoint_use_demo">OSRM-Demo-URL einsetzen</string>
|
||||
<string name="routing_endpoint_accepted_formats">Erlaubt: Basis-URL (https://router.project-osrm.org) oder vollständig mit Pfad /route/v1/driving. Wird intern normalisiert.</string>
|
||||
<string name="route_planner_layer_section">Routen-Darstellung</string>
|
||||
<string name="route_planner_layer_show">Route/Planlinie anzeigen</string>
|
||||
<string name="route_planner_layer_hint">Wenn aktiviert, werden berechnete Route, Planlinie und Stop-Marker auf der Karte gezeichnet.</string>
|
||||
<string name="route_planner_legend_route">Berechnete Route</string>
|
||||
<string name="route_planner_layer_section">Routenlinie auf Karte</string>
|
||||
<string name="route_planner_layer_show">Tourlinie / Planlinie anzeigen</string>
|
||||
<string name="route_planner_layer_hint">Wenn aktiviert, werden Routenlinie und Tourpunkt-Marker auf der Karte gezeichnet.</string>
|
||||
<string name="route_planner_legend_route">Berechnete Route (optional)</string>
|
||||
<string name="route_planner_legend_planline">Planlinie (Luftlinie)</string>
|
||||
<string name="route_planner_set_as_start">Als Start</string>
|
||||
<string name="route_planner_set_as_via">Als Via</string>
|
||||
<string name="route_planner_set_as_destination">Als Ziel</string>
|
||||
<string name="route_planner_set_as_start">Als Startpunkt</string>
|
||||
<string name="route_planner_set_as_via">Als Tourpunkt</string>
|
||||
<string name="route_planner_set_as_destination">Als Endpunkt</string>
|
||||
<string name="route_planner_poi_use">Punkt verwenden</string>
|
||||
<string name="route_planner_poi_explanation">Treffer wird als Stopp dem Routenplaner hinzugefügt.</string>
|
||||
<string name="route_planner_poi_explanation">Treffer wird als Tourpunkt der Route hinzugefügt.</string>
|
||||
<string name="route_planner_use_searched">Gesuchten Ort übernehmen</string>
|
||||
<string name="route_planner_attribution">Routing optional via OSRM-kompatiblem Endpunkt — sparsam abfragen.</string>
|
||||
<!-- v2.5.2 — Routingprofile (Auto/Fahrrad/Fußgänger) -->
|
||||
<string name="route_planner_attribution">Online-Routing optional — Planlinie braucht keinen Server. Sparsam abfragen.</string>
|
||||
<!-- v2.5.2 — Routingprofile (Auto/Fahrrad/Fußgänger) / v2.6.0 als sekundär markiert / v2.7.24 im Experimentell-Bereich -->
|
||||
<string name="route_planner_profile_section">Fortbewegungsmittel</string>
|
||||
<string name="route_planner_profile_driving">Auto</string>
|
||||
<string name="route_planner_profile_cycling">Fahrrad</string>
|
||||
<string name="route_planner_profile_walking">Fußgänger</string>
|
||||
<string name="route_planner_profile_hint">Wird für die Routing-Anfrage verwendet (driving/cycling/walking) und mit der gespeicherten Tourroute übernommen.</string>
|
||||
<string name="route_planner_status_error_with_profile">Routing für „%1$s\" fehlgeschlagen: %2$s — Endpunkt oder Netzwerk prüfen.</string>
|
||||
<string name="route_planner_status_endpoint_missing_with_profile">Kein Routing-Endpunkt gesetzt (Profil: %1$s). Mit „OSRM-Demo nutzen\" startest du sofort einen Testlauf, oder richte unter „Endpunkt einrichten\" einen eigenen Server ein.</string>
|
||||
<string name="route_planner_profile_hint">Nur relevant wenn Online-Routing (OSRM) genutzt wird. Planlinie benötigt kein Profil.</string>
|
||||
<string name="route_planner_status_error_with_profile">Online-Routing für „%1$s“ fehlgeschlagen: %2$s — Endpunkt oder Netzwerk prüfen.</string>
|
||||
<string name="route_planner_status_endpoint_missing_with_profile">Kein Routing-Endpunkt gesetzt (Profil: %1$s) — Planlinie verfügbar. Optional: OSRM-Demo oder eigener Server.</string>
|
||||
<string name="tour_route_profile_label">Profil: %1$s</string>
|
||||
|
||||
<!-- v2.4.6 — Tourrouten (persistente Route pro Tour) -->
|
||||
<string name="tour_route_status_title">Tourroute „%1$s"</string>
|
||||
<string name="tour_route_status_none">Keine Tourroute gespeichert.</string>
|
||||
<!-- v2.4.6 — Tourrouten (persistente Route pro Tour) / v2.6.0 Begriffe klargestellt -->
|
||||
<string name="tour_route_status_title">Gespeicherter Tourverlauf „%1$s“</string>
|
||||
<string name="tour_route_status_none">Kein Tourverlauf gespeichert.</string>
|
||||
<string name="tour_route_updated_at">aktualisiert %1$s</string>
|
||||
<string name="tour_route_source">Quelle: %1$s</string>
|
||||
<string name="tour_route_save">Tourroute speichern</string>
|
||||
<string name="tour_route_replace">Tourroute ersetzen</string>
|
||||
<string name="tour_route_delete">Tourroute löschen</string>
|
||||
<string name="tour_route_hint_compute_first">Erst Route berechnen oder Planlinie zeigen — dann lässt sich die Tourroute dauerhaft speichern.</string>
|
||||
<string name="tour_route_saved_msg">Tourroute für „%1$s" gespeichert.</string>
|
||||
<string name="tour_route_replaced_msg">Tourroute für „%1$s" ersetzt.</string>
|
||||
<string name="tour_route_deleted_msg">Tourroute für „%1$s" gelöscht.</string>
|
||||
<string name="tour_route_loaded_status">Gespeicherte Tourroute geladen (%1$s).</string>
|
||||
<string name="tour_route_save">Tourverlauf speichern</string>
|
||||
<string name="tour_route_replace">Tourverlauf ersetzen</string>
|
||||
<string name="tour_route_delete">Tourverlauf löschen</string>
|
||||
<string name="tour_route_hint_compute_first">Erst Planlinie zeigen oder Route berechnen — dann kann der Tourverlauf dauerhaft gespeichert werden.</string>
|
||||
<!-- v2.7.24 — Online-Routing Experimentell-Bereich -->
|
||||
<string name="route_planner_online_routing_section">Experimentelles Online-Routing</string>
|
||||
<string name="route_planner_online_routing_hint">Abhängig von externem OSRM-Dienst. Offline nicht nötig — freie Linie und Punkte formen funktionieren ohne Netz.</string>
|
||||
<!-- v2.7.23 — Route Planner Flow Fix: Planlinie als Tourverlauf übernehmen -->
|
||||
<string name="route_planner_adopt_planline">Als Tourverlauf übernehmen</string>
|
||||
<string name="route_planner_adopt_planline_hint">Speichert die aktuelle Planlinie (oder berechnete Route) dauerhaft als Tourverlauf der aktiven Tour.</string>
|
||||
<string name="route_planner_adopted_planline">Tourverlauf übernommen (Planlinie).</string>
|
||||
<string name="route_planner_adopted_route">Tourverlauf übernommen (berechnete Route).</string>
|
||||
<string name="route_planner_adopt_none">Zuerst Start und Ziel setzen, dann Tourverlauf übernehmen.</string>
|
||||
<string name="tour_route_saved_msg">Tourverlauf für „%1$s“ gespeichert.</string>
|
||||
<string name="tour_route_replaced_msg">Tourverlauf für „%1$s“ ersetzt.</string>
|
||||
<string name="tour_route_deleted_msg">Tourverlauf für „%1$s“ gelöscht.</string>
|
||||
<string name="tour_route_loaded_status">Gespeicherter Tourverlauf geladen (%1$s).</string>
|
||||
<string name="tour_route_source_planline">Planlinie</string>
|
||||
<string name="tour_route_source_track">GPS-Track</string>
|
||||
|
||||
<!-- v2.5.3 — Manueller Routemodus im Explorer -->
|
||||
<string name="explorer_mode_station">Station hinzufügen</string>
|
||||
<string name="explorer_mode_draw_route">Route zeichnen</string>
|
||||
<string name="explorer_mode_draw_route_active">Route zeichnen: Tippe auf die Karte, um den Wegverlauf zu setzen. Nur Stationen spielen Audio ab.</string>
|
||||
<string name="explorer_mode_draw_route_hint">Route zeichnen: Wegverlauf setzen. Nur Stationen spielen Audio ab.</string>
|
||||
<string name="explorer_route_point_dialog_title">Routenpunkt</string>
|
||||
<!-- v2.5.3 — Manueller Routemodus im Explorer / v2.6.0 — Begriffe für Tourverlauf -->
|
||||
<string name="explorer_mode_station">Audio-Wegpunkt hinzufügen</string>
|
||||
<string name="explorer_mode_draw_route">Tourverlauf zeichnen</string>
|
||||
<string name="explorer_mode_draw_route_active">Tourverlauf zeichnen: Tippe auf die Karte, um Verlaufspunkte zu setzen. Nur Audio-Wegpunkte lösen Klänge aus.</string>
|
||||
<string name="explorer_mode_draw_route_hint">Tourverlauf: Verlaufslinie setzen. Nur Audio-Wegpunkte lösen Klänge aus.</string>
|
||||
<string name="explorer_route_point_dialog_title">Tourverlaufspunkt</string>
|
||||
<string name="explorer_route_point_move">Verschieben</string>
|
||||
<string name="explorer_route_point_delete">Löschen</string>
|
||||
<string name="explorer_route_point_move_active">Tippe die neue Position an — der Routenpunkt wird dorthin verschoben.</string>
|
||||
<string name="explorer_manual_route_badge">Manuell gezeichnete Route</string>
|
||||
<string name="explorer_manual_route_cleared">Manuelle Route gelöscht.</string>
|
||||
<string name="explorer_manual_route_saved">Manuelle Route gespeichert (%1$d Punkte).</string>
|
||||
<string name="explorer_manual_route_clear">Manuelle Route löschen</string>
|
||||
<string name="explorer_route_point_move_active">Tippe die neue Position an — der Verlaufspunkt wird dorthin verschoben.</string>
|
||||
|
||||
<!-- v2.7.0 — Tourverlauf-Editor -->
|
||||
<!-- v2.7.15 — Tourverlauf-Menü-Merge: einheitlicher Einstieg -->
|
||||
<string name="tourverlauf_menu_title">Route bearbeiten</string>
|
||||
<string name="tourverlauf_menu_subtitle">Routenpunkte setzen · Audio später hinzufügen</string>
|
||||
<string name="tourverlauf_menu_open">Bearbeiten</string>
|
||||
<string name="tourverlauf_editor_title">Routenpunkte</string>
|
||||
<string name="tourverlauf_editor_hint">Tourpunkte formen die Routenlinie. Sie lösen kein Audio aus. Audio wird über "Als Audio-Wegpunkt erstellen" bewusst hinzugefügt.</string>
|
||||
<string name="tourverlauf_editor_point_count">%1$d Routenpunkt(e) gesetzt</string>
|
||||
<string name="tourverlauf_editor_no_points">Noch keine Routenpunkte. Tippe auf die Karte oder nutze "Hier setzen".</string>
|
||||
<string name="tourverlauf_editor_add_center">Punkt hier setzen</string>
|
||||
<string name="tourverlauf_editor_add_tap">Karte antippen (Punkte setzen)</string>
|
||||
<string name="tourverlauf_editor_remove_last">Letzten Punkt entfernen</string>
|
||||
<string name="tourverlauf_editor_clear">Verlauf löschen</string>
|
||||
<string name="tourverlauf_editor_clear_confirm">Gesamten Tourverlauf löschen?</string>
|
||||
<string name="tourverlauf_editor_as_waypoint">Audio hier hinzufügen</string>
|
||||
<string name="tourverlauf_editor_as_waypoint_hint">Erstellt einen Audio-Wegpunkt an diesem Tourpunkt.</string>
|
||||
<string name="tourverlauf_editor_close">Editor schließen</string>
|
||||
<string name="tourverlauf_editor_open">Route bearbeiten</string>
|
||||
<string name="tourverlauf_online_routing_section">Optional: Online-Route berechnen</string>
|
||||
<string name="tourverlauf_online_routing_hint">OSRM/Straßen-Routing ist optional. Der manuelle Tourverlauf ist die Hauptmethode.</string>
|
||||
<string name="tourverlauf_editor_fab_hint">Route bearbeiten</string>
|
||||
<!-- v2.7.17 — Route-first Explorer -->
|
||||
<string name="route_planner_fab_hint">Route planen</string>
|
||||
<string name="route_planner_menu_title">Route planen</string>
|
||||
<string name="route_planner_menu_subtitle">Start/Ziel/Via, Planlinie, Online-Routing (OSRM)</string>
|
||||
<string name="route_planner_menu_open">Planer öffnen</string>
|
||||
<string name="tourverlauf_editor_saved">Tourverlauf gespeichert (%1$d Punkte).</string>
|
||||
<string name="explorer_manual_route_badge">Manuell gezeichneter Tourverlauf</string>
|
||||
<string name="explorer_manual_route_cleared">Tourverlauf gelöscht.</string>
|
||||
<string name="explorer_manual_route_saved">Tourverlauf gespeichert (%1$d Punkte).</string>
|
||||
<string name="explorer_manual_route_clear">Tourverlauf löschen</string>
|
||||
<string name="tour_route_source_manual">Manuell gezeichnet</string>
|
||||
|
||||
<string name="version_notice_body_v231">Neu in 2.3.1: Wegpunkt-Dialog spielt Clips jetzt direkt ab statt „auf Kartenmitte verschieben". Karten-UI deutlich entschlackt: nur noch Icon-FABs und kompakte Status-Chips. Clip-Längenlinien projizieren die Audiodauer als farbige Segmente auf die Karte (Layer schaltbar). Neue Routing & Online-POI-Sektion: konfigurierbarer OSRM-Endpunkt + Overpass-POI-Suche (frei, sparsam). MapLibre-Pipeline weiter vorbereitet; aktives Rendering bleibt osmdroid.</string>
|
||||
@@ -951,4 +1034,17 @@ Viele Grüße</string>
|
||||
<string name="explorer_poi_added_snack">als Wegpunkt angelegt.</string>
|
||||
<string name="version_notice_body_v2540">Neu in 2.5.40: Echter Explorer-POI-Layer über Overpass-API — Sehenswürdigkeiten, Gastronomie, Mobilität, Infrastruktur direkt aus OpenStreetMap laden (kostenlos, kein API-Key, rate-limit-freundlich, nur auf Nutzer-Aktion). Antippen eines POI-Markers öffnet Detail-Sheet mit Name, Kategorie, Entfernung, Tags und Option „Als Wegpunkt“. Kompass-Button in der rechten Werkzeugleiste: setzt Karten-Ausrichtung auf Norden (Bearing 0) mit Animation. Rechte Werkzeugleiste neu geordnet — kein Überlappen der Buttons mehr. Alle v2.5.39-Fixes (Scroll, Attribution, Kartenstil-Persistenz) bleiben erhalten.</string>
|
||||
|
||||
<!-- v2.7.17 — Route-first Explorer: Tourpunkt-Konzept -->
|
||||
<string name="tourpunkt_has_audio">Audio vorhanden</string>
|
||||
<string name="tourpunkt_no_audio">Kein Audio (nur Routenpunkt)</string>
|
||||
<string name="tourpunkt_add_audio">Audio hinzufügen</string>
|
||||
<string name="tourpunkt_edit_audio">Audio bearbeiten</string>
|
||||
<!-- v2.7.17 — Route-first Explorer: UI/UX-Umbau -->
|
||||
<string name="version_notice_body_v2717">Neu in 2.7.17 (Route-first Explorer): Die Planung folgt jetzt dem mentalen Modell „Route zuerst, Audio danach". Der Routen-Planer heißt jetzt „Route planen"; Verlaufspunkte werden als „Tourpunkte" bezeichnet. Der „Tourverlauf bearbeiten"-Editor wird als „Route bearbeiten" geführt. Audio wird bewusst nachträglich per „Audio hier hinzufügen" einem Tourpunkt zugewiesen. Bestehende Audio-Wegpunkte bleiben vollständig erhalten und abwärtskompatibel. Keine Änderung an Logik, Backup, Lizenz, MapLibre-Rendering, Waypoint-Playback oder GPS-Aufzeichnung.</string>
|
||||
|
||||
<!-- v2.7.18 — Unified Tour Layers -->
|
||||
<string name="version_notice_body_v2718">Neu in 2.7.18 (Unified Tour Layers): Routing, manuelles Zeichnen und Audio-Wegpunkte wirken jetzt als ein gemeinsames Tour-System. Die manuelle Tourlinie wird nicht mehr doppelt gezeichnet — im gespeicherten Zustand zeigt die Karte eine einheitliche grüne Tourlinie (egal ob manuell gezeichnet oder per OSRM berechnet). Die violette Bearbeitungs-Vorschau erscheint nur noch im aktiven Zeichenmodus. Audio-Trigger-Zonen bleiben als Radius erhalten. Keine Änderung an Logik, Backup, Lizenz, MapLibre-Rendering, Routing, Waypoint-Playback oder GPS-Aufzeichnung.</string>
|
||||
<string name="version_notice_body_v2720">Neu in 2.7.20 (Flexible Routenformung): Der Tourplaner ist jetzt klar auf “Route formen” ausgerichtet. Zwischenpunkte lenken die Route – sie sind keine Audio-Punkte. Neu: Zwischenpunkt direkt auf der Karte setzen (Schaltfläche “Zwischenpunkt auf Karte tippen”). Die freie Linie ist klar als Fallback gekennzeichnet. Start/Ziel/Via-Persistenz aus v2.7.19 bleibt vollständig erhalten. Keine Änderung an Audio-Waypoints, Multiclip, PTT, Atmo, Backup, Lizenz oder MapLibre-Rendering.</string>
|
||||
<string name="version_notice_body_v2719">Neu in 2.7.19 (Tourplaner Integration Fix): Start, Ziel und Via-Ziele bleiben im Tourplaner dauerhaft gespeichert — auch nach App-Neustart. Der Tourplaner schreibt den Plan jetzt sofort in die Datenbank, sobald Start und Ziel gesetzt sind. Außerdem: Manuelles Zeichnen ist direkt im Tourplaner zugänglich (Schaltfläche “Zeichenmodus öffnen”). Routing per OSRM und manuelles Zeichnen sind gleichwertige Bearbeitungsmethoden im selben Planer. Die violette Vorschaulinie erscheint weiterhin nur im aktiven Zeichenmodus (kein Rückschritt in 2.7.18).</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# GPS2Audio v2.7.38 — Handoff (Audio Regression Fix)
|
||||
|
||||
**Date:** 2026-06-22
|
||||
**Base:** v2.7.37 (versionCode 143) → **v2.7.38 (versionCode 144)**
|
||||
**Source:** `/home/user/workspace/GPS2Audio_v2737_from_gitea/src`
|
||||
**Priority honored:** Audio reliability over preserving the silent gap. The gap is kept, but made strictly safe.
|
||||
|
||||
---
|
||||
|
||||
## Suspected root cause
|
||||
|
||||
The only functional change in v2.7.37 was a hidden ~1.7 s silent gap between manual sequence items, implemented as a delayed coroutine (`manualGapJob`) launched inside the `manualPlayer.play(...)` natural-completion callback. The gap itself is correctly scoped to post-completion auto-advance. The regression — "audio broken, especially on Samsung tablets" — comes from **two unsafe state interactions introduced/left open by that delayed job, plus a pre-existing silent-failure path that the gap made far more visible**:
|
||||
|
||||
1. **Stale auto-advance across tour switch / tour delete.**
|
||||
`selectTour()` deliberately preserves running playback (v2.5.30) and did **not** cancel a pending gap job. `deleteTour()` stopped the player but also did **not** cancel the gap job. So a gap job scheduled in tour A could fire ~1.7 s later after the user switched to / deleted tour A, calling `playFromCurrentPlayable(autoAdvance=true)` and setting `_manualPlaying=true` against stale `_currentPlayable` — leaving the UI in a "playing" state with the wrong audio or no audio.
|
||||
|
||||
2. **Gap job did not re-validate user intent before firing.**
|
||||
The delayed job only re-checked PTT and whether the next item still exists. It did **not** check whether the user had paused/stopped or switched to single-clip mode during the silence, so it could resurrect playback the user had just stopped.
|
||||
|
||||
3. **Silent load/start failure left state as "playing".**
|
||||
`ManualAudioPlayer.loadAndPlay()` uses raw `MediaPlayer` with synchronous `prepare()`/`start()` and **no error feedback to the caller**. On some devices (notably Samsung tablets) `prepare()`/`start()` on `content://` SAF URIs, or an async `MediaPlayer` error, fails — the player released silently while the ViewModel had already set `_manualPlaying=true`. Result: UI shows "playing", no sound, no error. The v2.7.37 gap (which chains auto-advance starts) made this dead-end far more reachable across a sequence.
|
||||
|
||||
---
|
||||
|
||||
## Exact fixes
|
||||
|
||||
### `app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt`
|
||||
- **`playFromCurrentPlayable(autoAdvance)` — gap job hardened.** Capture `gapTour = _selectedTour.value` when scheduling. After the `delay`, before advancing, the job now aborts if:
|
||||
- the selected tour changed during the silence (`_selectedTour.value != gapTour`) — no start in the wrong tour;
|
||||
- the user is no longer playing or has entered single mode (`!_manualPlaying.value || manualPlayer.isSingleMode`) — no resurrection after pause/stop/preview.
|
||||
PTT and "next item removed" checks unchanged. This makes the gap **strictly post-completion auto-advance within the same tour**.
|
||||
- **`playFromCurrentPlayable` — `onError` wired.** `manualPlayer.play(...)` is now called with an `onError` callback that cancels any gap, resets `_manualPlaying`/`_manualCurrentName`/`_currentPlayable`, stops the ticker, calls `musicManager.afterWaypointAudio()`, and surfaces `_errorMessage` ("Audio konnte nicht abgespielt werden. Bitte erneut versuchen."). No more silent "playing-with-no-audio".
|
||||
- **`selectTour()`** — cancels a pending gap job on an actual tour change (`if (safeTarget != _selectedTour.value) cancelManualGap()`), without stopping active playback (persistence preserved).
|
||||
- **`deleteTour()`** — calls `cancelManualGap()` alongside the existing `manualPlayer.stop()` reset when the deleted tour is selected.
|
||||
|
||||
### `app/src/main/kotlin/de/waypointaudio/service/ManualAudioPlayer.kt`
|
||||
- **`play(...)`** gains an optional `onError: () -> Unit = {}` (placed before `onCompletion` so the trailing-lambda completion call sites stay valid). Fires `onError()` on blank URI.
|
||||
- **`loadAndPlay(...)`** gains optional `onError`; fires it both on the synchronous `runCatching{}.onFailure` (prepare/start failure) and inside `setOnErrorListener` (async MediaPlayer error), after releasing resources.
|
||||
- **`startSingleLoad(...)`** updated to named `onCompletion =` argument (behavior identical; single-clip preview/GPS unaffected since `onError` defaults to no-op).
|
||||
|
||||
### `app/build.gradle.kts`
|
||||
- `versionCode 143 → 144`, `versionName "2.7.37" → "2.7.38"`, with a v2.7.38 changelog comment block.
|
||||
|
||||
**First manual playback is unchanged and still immediate** — the gap only exists in the auto-advance callback after a clip ends, never on the initial `startPlayableAt` → `playFromCurrentPlayable` path. `cancelManualGap()` is still called at the top of `startPlayableAt`, so Play/Resume/Next/Prev/Skip cannot be suppressed by a pending gap.
|
||||
|
||||
---
|
||||
|
||||
## Playback paths reviewed
|
||||
|
||||
| Path | Verdict |
|
||||
|---|---|
|
||||
| Manual sequence play/pause/resume/next/prev/stop | **Fixed/hardened** (above). `cancelManualGap()` confirmed on every control path. |
|
||||
| Auto-advance silent gap (v2.7.37) | **Kept but made strictly safe** (same-tour, still-playing, post-completion only). |
|
||||
| Single clip preview (`manualPlaySingleClip` / `playSingle` / `startSingleLoad`) | Reviewed — unchanged behavior; `onError` defaults to no-op. No change needed. |
|
||||
| GPS-triggered playback (`service/AudioPlayer.kt`) | Separate class, not touched by these edits. No change needed. |
|
||||
| PTT suppression / interruption coordinator | Reviewed — gap job and play paths still honor `pttManager.pttActive`; coordinator pause/resume unchanged. |
|
||||
| Atmo / background music (`BackgroundMusicPlayer`, `musicManager`) | Not touched. `afterWaypointAudio()`/`beforeWaypointAudio()` calls preserved. |
|
||||
| Background manual playback (`ManualPlaybackService`) | Not touched; start/stop/meta lifecycle unchanged. |
|
||||
| Tour switch / delete playback persistence (v2.5.30) | Preserved — active playback still survives a plain tour switch; only the stale *pending gap* is cancelled. |
|
||||
| Player auto-open (v2.7.32) | Not touched. |
|
||||
|
||||
---
|
||||
|
||||
## Build / test results
|
||||
|
||||
- **Signed APK: NOT BUILT — toolchain unavailable in this environment.**
|
||||
- No Android SDK (`/home/user/tools/android-sdk` missing).
|
||||
- No Gradle (`gradlew` execs `/home/user/tools/gradle/bin/gradle`, which does not exist; `/home/user/tools/` is absent).
|
||||
- No keystore (`keystore.properties` points to `/home/user/workspace/GPS2Audio_release_keystore_v2.jks`, which is missing).
|
||||
- The Gradle release signing config itself is intact in `app/build.gradle.kts` (reads `keystore.properties` or `GPS2AUDIO_RELEASE_*` env vars), so a signed universal release APK **will** build on a properly provisioned machine: `./gradlew :app:assembleRelease` with the keystore + SDK present.
|
||||
- **Compilation:** could not run (no SDK/Gradle). Changes were made minimal and verified by manual review for Kotlin correctness:
|
||||
- `play(...)` parameter order changed so the existing trailing-lambda completion call site still binds to `onCompletion`; the new ViewModel call passes `onError = { ... }` (named) + trailing `onCompletion` lambda.
|
||||
- All three `loadAndPlay(...)` call sites match the updated 4-arg signature.
|
||||
- **Recommended manual QA on a Samsung tablet:** (1) play a tour clip and let it auto-advance — verify ~1.7 s gap then next clip; (2) during the gap, switch tours — verify no audio starts in the new tour and no stuck "playing"; (3) during the gap, delete the tour — verify clean stop; (4) during the gap, tap pause/stop — verify it stays stopped; (5) force a bad/unreadable clip — verify error toast appears and state resets instead of silent "playing"; (6) first manual play — verify it starts immediately with no delay.
|
||||
|
||||
---
|
||||
|
||||
## Artifacts
|
||||
|
||||
- **Clean source ZIP:** `/home/user/workspace/GPS2Audio_v2738_artifacts/GPS2Audio_v2.7.38_src.zip`
|
||||
- 136 files. Excludes `.gradle`, `.idea`, `.kotlin`, all `build/` dirs, APK/AAB/JKS/keystore binaries, `.class`/`.dex`, and `.git`.
|
||||
- **Also excludes `keystore.properties` and `local.properties`** — `keystore.properties` contained plaintext signing passwords (do not redistribute); `local.properties` is machine-specific. Recreate both from the build machine's secrets when building.
|
||||
- **This handoff:** `/home/user/workspace/GPS2Audio_v2738_artifacts/HANDOFF_v2.7.38.md`
|
||||
|
||||
## Constraints honored
|
||||
Preserved v2.7.32 (tour ordering + player auto-open), v2.7.33 (Begleitmusik UI), v2.7.34 (waypoint edit UI), v2.7.35 (home UI), v2.7.36 (backup export IO fix), v2.7.37 gap (kept, made safe), and Explorer/MapLibre/routing/POI/settings/license/PTT/Atmo/multiclip/background playback. No refactors, no data-model changes.
|
||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
||||
# GPS2Audio v2.7.39 — Handoff
|
||||
|
||||
## Ziel
|
||||
|
||||
Stabilitätsfix für Samsung-Geräte: Die in v2.7.37 eingeführte feste Stille-Pause zwischen manuellen Clips/Wegpunkten wurde entfernt. Der Nutzen der Pause war rein kosmetisch, während sie auf einigen Geräten offenbar zu uneindeutigen Player-Zuständen und der sichtbaren Fehlermeldung „Audio konnte nicht abgespielt werden“ beitragen konnte.
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
- `app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt`
|
||||
- `manualGapJob` und `MANUAL_GAP_MS` wurden entfernt.
|
||||
- Der Sonderfall „Play/Pause während der Stille-Pause“ wurde entfernt.
|
||||
- Der manuelle Auto-Advance startet den nächsten aktiven Clip/Wegpunkt jetzt wieder direkt.
|
||||
- `cancelManualGap()` bleibt als No-op bestehen, damit bestehende Stop-, Skip- und Tourwechsel-Pfade ohne strukturelles Risiko gleich bleiben.
|
||||
|
||||
- `app/build.gradle.kts`
|
||||
- `versionCode` von `144` auf `145` erhöht.
|
||||
- `versionName` von `2.7.38` auf `2.7.39` erhöht.
|
||||
- Changelog-Kommentar auf v2.7.39 aktualisiert.
|
||||
|
||||
- `RELEASES.md`
|
||||
- v2.7.39 als Audio-Stabilitätsfix dokumentiert.
|
||||
|
||||
## Entscheidungslogik
|
||||
|
||||
Die Pause wurde nicht als Benutzeroption beibehalten, sondern vollständig aus dem aktiven Ablauf entfernt. Dadurch gibt es keinen verzögerten Coroutine-Job mehr, der nach einem Tourwechsel, Stop/Pause oder auf Samsung-Geräten in einen veralteten Player-Zustand laufen kann. Der manuelle Modus spielt weiterhin alle aktivierten Clips und Waypoints der Reihe nach, nur ohne künstliche Pause.
|
||||
|
||||
Die GPS-Wiedergabe nutzt weiterhin ihren eigenen Pfad und wurde in diesem Fix bewusst nicht verändert.
|
||||
|
||||
## Build-Status in dieser Sandbox
|
||||
|
||||
Der Quellcode-Fix ist vorbereitet. In der aktuellen Sandbox fehlen jedoch Android-SDK/Gradle-Toolchain und der Release-Keystore, daher konnte hier keine signierte updatefähige APK gebaut werden. Für den finalen Release-Build muss die bekannte GPS2Audio-Release-Buildumgebung beziehungsweise der Keystore erneut verfügbar sein.
|
||||
|
||||
## Empfohlene Tests
|
||||
|
||||
- Manueller Tour-Play mit mehreren Waypoints und mehreren Clips pro Waypoint.
|
||||
- Manuelles Weiter/Zurück zwischen Clips und Waypoints.
|
||||
- GPS-Trigger bei aktiver GPS-Wiedergabe auf einem Samsung-Tablet.
|
||||
- Ungültige oder gelöschte Audioquelle: Fehlermeldung darf weiterhin erscheinen.
|
||||
- PTT während laufender Waypoint-Wiedergabe.
|
||||
- Atmo-Pause/Resume während Waypoint-Audio.
|
||||
Binary file not shown.
Reference in New Issue
Block a user