diff --git a/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt b/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt index 5b09e4e..97463fc 100644 --- a/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt +++ b/src-tauri/gen/android/app/src/main/java/org/bocken/app/AndroidBridge.kt @@ -19,7 +19,7 @@ class AndroidBridge(private val context: Context) { private var ttsForVoices: TextToSpeech? = null @JavascriptInterface - fun startLocationService(ttsConfigJson: String) { + fun startLocationService(ttsConfigJson: String, startPaused: Boolean) { if (context is Activity) { // Request notification permission on Android 13+ (required for foreground service notification) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -50,6 +50,7 @@ class AndroidBridge(private val context: Context) { val intent = Intent(context, LocationForegroundService::class.java).apply { putExtra("ttsConfig", ttsConfigJson) + putExtra("startPaused", startPaused) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) @@ -58,10 +59,16 @@ class AndroidBridge(private val context: Context) { } } - /** Backwards-compatible overload for calls without TTS config */ + /** Overload: TTS config only (not paused) */ + @JavascriptInterface + fun startLocationService(ttsConfigJson: String) { + startLocationService(ttsConfigJson, false) + } + + /** Overload: no args (not paused, no TTS) */ @JavascriptInterface fun startLocationService() { - startLocationService("{}") + startLocationService("{}", false) } @JavascriptInterface diff --git a/src-tauri/gen/android/app/src/main/java/org/bocken/app/LocationForegroundService.kt b/src-tauri/gen/android/app/src/main/java/org/bocken/app/LocationForegroundService.kt index 807d2d7..b965371 100644 --- a/src-tauri/gen/android/app/src/main/java/org/bocken/app/LocationForegroundService.kt +++ b/src-tauri/gen/android/app/src/main/java/org/bocken/app/LocationForegroundService.kt @@ -127,10 +127,11 @@ class LocationForegroundService : Service(), TextToSpeech.OnInitListener { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val startPaused = intent?.getBooleanExtra("startPaused", false) ?: false startTimeMs = System.currentTimeMillis() pausedAccumulatedMs = 0L - pausedSinceMs = 0L - paused = false + pausedSinceMs = if (startPaused) startTimeMs else 0L + paused = startPaused totalDistanceKm = 0.0 lastLat = Double.NaN lastLng = Double.NaN @@ -151,7 +152,11 @@ class LocationForegroundService : Service(), TextToSpeech.OnInitListener { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - val notification = buildNotification("0:00", "0.00 km", "") + val notification = if (startPaused) { + buildNotification("Waiting to start...", "", "") + } else { + buildNotification("0:00", "0.00 km", "") + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground(NOTIFICATION_ID, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION) @@ -488,7 +493,7 @@ class LocationForegroundService : Service(), TextToSpeech.OnInitListener { val channel = NotificationChannel( CHANNEL_ID, "GPS Tracking", - NotificationManager.IMPORTANCE_LOW + NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "Shows while GPS is recording your workout" } diff --git a/src/lib/js/gps.svelte.ts b/src/lib/js/gps.svelte.ts index 8d47f2a..4745c2b 100644 --- a/src/lib/js/gps.svelte.ts +++ b/src/lib/js/gps.svelte.ts @@ -23,7 +23,7 @@ export interface VoiceGuidanceConfig { } interface AndroidBridge { - startLocationService(ttsConfigJson: string): void; + startLocationService(ttsConfigJson: string, startPaused: boolean): void; stopLocationService(): void; getPoints(): string; isTracking(): boolean; @@ -112,7 +112,7 @@ export function createGpsTracker() { } } - async function start(voiceGuidance?: VoiceGuidanceConfig) { + async function start(voiceGuidance?: VoiceGuidanceConfig, startPaused = false) { _debugMsg = 'starting...'; if (!checkTauri() || isTracking) { _debugMsg = `bail: tauri=${checkTauri()} tracking=${isTracking}`; @@ -145,7 +145,7 @@ export function createGpsTracker() { if (bridge) { _debugMsg = 'starting native GPS service...'; const ttsConfig = JSON.stringify(voiceGuidance ?? {}); - bridge.startLocationService(ttsConfig); + bridge.startLocationService(ttsConfig, startPaused); // Poll the native side for collected points _pollTimer = setInterval(pollPoints, POLL_INTERVAL_MS); _debugMsg = 'native GPS service started, polling...'; diff --git a/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte b/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte index 678c603..3109b49 100644 --- a/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte +++ b/src/routes/fitness/[workout=fitnessWorkout]/[active=fitnessActive]/+page.svelte @@ -18,6 +18,7 @@ import SetTable from '$lib/components/fitness/SetTable.svelte'; import ExercisePicker from '$lib/components/fitness/ExercisePicker.svelte'; import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte'; + import Toggle from '$lib/components/Toggle.svelte'; import { onMount } from 'svelte'; const workout = getWorkout(); @@ -41,13 +42,26 @@ let useGps = $state(gps.isTracking); - // Voice guidance config + // Voice guidance config (defaults, overridden from localStorage in onMount) let vgEnabled = $state(false); let vgTriggerType = $state('distance'); let vgTriggerValue = $state(1); let vgMetrics = $state(['totalTime', 'totalDistance', 'avgPace']); - let vgLanguage = $state('en'); + const vgLanguage = $derived(lang); let vgShowPanel = $state(false); + let vgLoaded = $state(false); + + // Persist voice guidance settings to localStorage + $effect(() => { + const settings = { + enabled: vgEnabled, + triggerType: vgTriggerType, + triggerValue: vgTriggerValue, + metrics: vgMetrics, + }; + if (!vgLoaded) return; + localStorage.setItem('vg_settings', JSON.stringify(settings)); + }); // GPS workout mode state — if we're restoring a GPS workout that was already tracking, it's started let gpsStarted = $state(gps.isTracking && workout.mode === 'gps' && !workout.paused); @@ -250,11 +264,24 @@ return; } + // Restore voice guidance settings from localStorage + try { + const saved = localStorage.getItem('vg_settings'); + if (saved) { + const s = JSON.parse(saved); + if (typeof s.enabled === 'boolean') vgEnabled = s.enabled; + if (s.triggerType === 'distance' || s.triggerType === 'time') vgTriggerType = s.triggerType; + if (typeof s.triggerValue === 'number' && s.triggerValue > 0) vgTriggerValue = s.triggerValue; + if (Array.isArray(s.metrics)) vgMetrics = s.metrics; + } + } catch {} + vgLoaded = true; + // For GPS workouts in pre-start: start GPS immediately so the map // shows the user's position while they configure activity/audio. if (workout.mode === 'gps' && !gpsStarted && !gps.isTracking) { _prestartGps = true; - gps.start(); + gps.start(undefined, true); } }); @@ -873,7 +900,7 @@
-
+
{#if gpsStarted}
@@ -950,10 +977,7 @@
{:else} - + {#if vgEnabled}
@@ -989,13 +1013,6 @@
-
- Language - -
{/if} {/if}
@@ -1059,10 +1076,7 @@
{:else} - + {#if vgEnabled}
@@ -1097,14 +1111,6 @@ {/each}
- -
- Language - -
{/if} {/if} @@ -1814,10 +1820,10 @@ color: var(--nord7); } .gps-overlay .vg-panel { - background: rgba(255,255,255,0.1); + background: rgba(46, 52, 64, 0.82); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - border: 1px solid rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; padding: 0.6rem; border-top: none; @@ -1885,10 +1891,45 @@ gap: 0.5rem; padding: 0.75rem; padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px)); - background: linear-gradient(to top, rgba(0,0,0,0.7) 60%, transparent); - color: #fff; + background: var(--nav-bg, rgba(46, 52, 64, 0.82)); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-top: 1px solid var(--nav-border, rgba(255,255,255,0.08)); + box-shadow: 0 -4px 24px var(--nav-shadow, rgba(0,0,0,0.25)); + color: var(--nav-text-active, #fff); pointer-events: none; } + @media (prefers-color-scheme: dark) { + .gps-overlay { + --nav-bg: rgba(20, 20, 20, 0.78); + --nav-border: rgba(255,255,255,0.06); + } + } + :global(:root[data-theme="dark"]) .gps-overlay { + --nav-bg: rgba(20, 20, 20, 0.78); + --nav-border: rgba(255,255,255,0.06); + } + :global(:root[data-theme="light"]) .gps-overlay { + --nav-bg: rgba(255, 255, 255, 0.82); + --nav-border: rgba(0,0,0,0.08); + --nav-shadow: rgba(0,0,0,0.1); + --nav-text-active: var(--nord0); + } + @media (prefers-color-scheme: light) { + :global(:root:not([data-theme])) .gps-overlay { + --nav-bg: rgba(255, 255, 255, 0.82); + --nav-border: rgba(0,0,0,0.08); + --nav-shadow: rgba(0,0,0,0.1); + --nav-text-active: var(--nord0); + } + } + .gps-overlay-prestart { + background: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + border-top: none !important; + box-shadow: none !important; + } .gps-overlay > * { pointer-events: auto; } @@ -1906,14 +1947,13 @@ font-size: 1.8rem; font-weight: 800; font-variant-numeric: tabular-nums; - color: #fff; - text-shadow: 0 1px 4px rgba(0,0,0,0.5); + color: inherit; } .gps-stat-unit { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; - color: rgba(255,255,255,0.75); + opacity: 0.65; } .gps-options-grid { display: grid; @@ -1926,10 +1966,10 @@ align-items: center; gap: 0.25rem; padding: 0.65rem 0.5rem; - background: rgba(255,255,255,0.12); + background: rgba(46, 52, 64, 0.82); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - border: 1px solid rgba(255,255,255,0.2); + border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; cursor: pointer; font: inherit; @@ -1962,10 +2002,10 @@ align-items: center; gap: 0.5rem; padding: 0.55rem 0.75rem; - background: rgba(255,255,255,0.1); + background: rgba(46, 52, 64, 0.82); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - border: 1px solid rgba(255,255,255,0.2); + border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; cursor: pointer; font: inherit; @@ -1976,8 +2016,9 @@ } .gps-activity-choice.active { border-color: var(--nord8); - background: rgba(136,192,208,0.25); + background: rgba(46, 52, 64, 0.9); color: var(--nord8); + box-shadow: inset 0 0 0 1px rgba(136,192,208,0.25); } .gps-activity-choice:hover:not(.active) { border-color: rgba(255,255,255,0.4); @@ -2010,13 +2051,15 @@ align-items: center; justify-content: center; gap: 0.3rem; - background: none; border: none; - color: rgba(255,255,255,0.5); + color: #fff; + text-shadow: 0 1px 4px rgba(0,0,0,0.6); font: inherit; font-size: 0.8rem; cursor: pointer; - padding: 0.25rem; + padding: 1rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom, 0px)); + margin: 0 -0.75rem calc(-0.75rem - env(safe-area-inset-bottom, 0px)); + background: linear-gradient(to top, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.3) 50%, transparent 100%); } .gps-cancel-link:hover { color: var(--nord11); @@ -2032,17 +2075,15 @@ justify-content: center; width: 3rem; height: 3rem; - background: rgba(255,255,255,0.15); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border: 1px solid rgba(255,255,255,0.25); + background: rgba(128,128,128,0.12); + border: 1px solid var(--nav-border, rgba(255,255,255,0.25)); border-radius: 50%; - color: #fff; + color: inherit; cursor: pointer; flex-shrink: 0; } .gps-overlay-pause:hover { - background: rgba(255,255,255,0.25); + background: rgba(128,128,128,0.2); } .gps-overlay-cancel { display: flex; @@ -2051,8 +2092,6 @@ width: 3rem; height: 3rem; background: rgba(191,97,106,0.25); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); border: 1px solid var(--nord11); border-radius: 50%; color: var(--nord11);