feat: redesign GPS workout UI with Runkeeper-style map overlay
- Full-screen fixed map with controls overlaid at the bottom - Activity type selector (running/walking/cycling/hiking) with proper exercise mapping for history display - GPS starts immediately on entering workout screen for faster lock - GPS track attached to cardio exercise (like GPX upload) so history shows distance, pace, splits, and map - Add activityType field to workout state, session model, and sync - Cancel button appears when workout is paused - GPS Workout button only shown in Tauri app
This commit is contained in:
@@ -221,6 +221,22 @@ export function createGpsTracker() {
|
||||
bridge?.resumeTracking();
|
||||
}
|
||||
|
||||
/** Request location permissions without starting the tracking service.
|
||||
* Returns true if permissions were granted. */
|
||||
async function ensurePermissions(): Promise<boolean> {
|
||||
if (!checkTauri()) return false;
|
||||
try {
|
||||
const geo = await import('@tauri-apps/plugin-geolocation');
|
||||
let perms = await geo.checkPermissions();
|
||||
if (perms.location !== 'granted') {
|
||||
perms = await geo.requestPermissions(['location']);
|
||||
}
|
||||
return perms.location === 'granted';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get track() { return track; },
|
||||
get isTracking() { return isTracking; },
|
||||
@@ -237,7 +253,8 @@ export function createGpsTracker() {
|
||||
hasTtsEngine,
|
||||
installTtsEngine,
|
||||
pauseTracking,
|
||||
resumeTracking
|
||||
resumeTracking,
|
||||
ensurePermissions
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,14 @@ export interface TemplateData {
|
||||
|
||||
const STORAGE_KEY = 'fitness-active-workout';
|
||||
|
||||
export type WorkoutMode = 'manual' | 'gps';
|
||||
export type GpsActivityType = 'running' | 'walking' | 'cycling' | 'hiking';
|
||||
|
||||
export interface StoredState {
|
||||
active: boolean;
|
||||
paused: boolean;
|
||||
mode: WorkoutMode;
|
||||
activityType: GpsActivityType | null;
|
||||
name: string;
|
||||
templateId: string | null;
|
||||
exercises: WorkoutExercise[];
|
||||
@@ -49,6 +54,8 @@ export interface StoredState {
|
||||
|
||||
export interface RemoteState {
|
||||
name: string;
|
||||
mode: WorkoutMode;
|
||||
activityType: GpsActivityType | null;
|
||||
templateId: string | null;
|
||||
exercises: WorkoutExercise[];
|
||||
paused: boolean;
|
||||
@@ -89,6 +96,8 @@ function clearStorage() {
|
||||
export function createWorkout() {
|
||||
let active = $state(false);
|
||||
let paused = $state(false);
|
||||
let mode = $state<WorkoutMode>('manual');
|
||||
let activityType = $state<GpsActivityType | null>(null);
|
||||
let name = $state('');
|
||||
let templateId: string | null = $state(null);
|
||||
let exercises = $state<WorkoutExercise[]>([]);
|
||||
@@ -115,6 +124,8 @@ export function createWorkout() {
|
||||
saveToStorage({
|
||||
active,
|
||||
paused,
|
||||
mode,
|
||||
activityType,
|
||||
name,
|
||||
templateId,
|
||||
exercises: JSON.parse(JSON.stringify(exercises)),
|
||||
@@ -182,6 +193,8 @@ export function createWorkout() {
|
||||
|
||||
active = true;
|
||||
paused = stored.paused;
|
||||
mode = stored.mode ?? 'manual';
|
||||
activityType = stored.activityType ?? null;
|
||||
name = stored.name;
|
||||
templateId = stored.templateId;
|
||||
exercises = stored.exercises;
|
||||
@@ -220,6 +233,7 @@ export function createWorkout() {
|
||||
function startFromTemplate(template: TemplateData) {
|
||||
name = template.name;
|
||||
templateId = template._id;
|
||||
mode = 'manual';
|
||||
exercises = template.exercises.map((e) => ({
|
||||
exerciseId: e.exerciseId,
|
||||
sets: e.sets.length > 0
|
||||
@@ -246,6 +260,7 @@ export function createWorkout() {
|
||||
function startEmpty() {
|
||||
name = 'Quick Workout';
|
||||
templateId = null;
|
||||
mode = 'manual';
|
||||
exercises = [];
|
||||
startTime = new Date();
|
||||
_pausedElapsed = 0;
|
||||
@@ -256,6 +271,26 @@ export function createWorkout() {
|
||||
_persist();
|
||||
}
|
||||
|
||||
function startGpsWorkout(activity: GpsActivityType = 'running') {
|
||||
const labels: Record<GpsActivityType, string> = {
|
||||
running: 'Running',
|
||||
walking: 'Walking',
|
||||
cycling: 'Cycling',
|
||||
hiking: 'Hiking'
|
||||
};
|
||||
name = labels[activity];
|
||||
templateId = null;
|
||||
mode = 'gps';
|
||||
activityType = activity;
|
||||
exercises = [];
|
||||
startTime = null;
|
||||
_pausedElapsed = 0;
|
||||
_elapsed = 0;
|
||||
paused = true;
|
||||
active = true;
|
||||
_persist();
|
||||
}
|
||||
|
||||
function pauseTimer() {
|
||||
if (!active || paused) return;
|
||||
_computeElapsed();
|
||||
@@ -374,6 +409,8 @@ export function createWorkout() {
|
||||
templateId,
|
||||
templateName: templateId ? name : undefined,
|
||||
name,
|
||||
mode,
|
||||
activityType,
|
||||
exercises: exercises
|
||||
.filter((e) => e.sets.some((s) => s.completed))
|
||||
.map((e) => ({
|
||||
@@ -409,6 +446,8 @@ export function createWorkout() {
|
||||
function _reset() {
|
||||
active = false;
|
||||
paused = false;
|
||||
mode = 'manual';
|
||||
activityType = null;
|
||||
name = '';
|
||||
templateId = null;
|
||||
exercises = [];
|
||||
@@ -427,6 +466,8 @@ export function createWorkout() {
|
||||
/** Apply state from another device (merge strategy: incoming wins) */
|
||||
function applyRemoteState(remote: RemoteState) {
|
||||
name = remote.name;
|
||||
mode = remote.mode ?? 'manual';
|
||||
activityType = remote.activityType ?? null;
|
||||
templateId = remote.templateId;
|
||||
exercises = remote.exercises;
|
||||
|
||||
@@ -470,6 +511,8 @@ export function createWorkout() {
|
||||
saveToStorage({
|
||||
active: true,
|
||||
paused,
|
||||
mode,
|
||||
activityType,
|
||||
name,
|
||||
templateId,
|
||||
exercises: JSON.parse(JSON.stringify(exercises)),
|
||||
@@ -496,6 +539,8 @@ export function createWorkout() {
|
||||
return {
|
||||
get active() { return active; },
|
||||
get paused() { return paused; },
|
||||
get mode() { return mode; },
|
||||
get activityType() { return activityType; },
|
||||
get name() { return name; },
|
||||
set name(v: string) { name = v; _persist(); },
|
||||
get templateId() { return templateId; },
|
||||
@@ -511,6 +556,7 @@ export function createWorkout() {
|
||||
restore,
|
||||
startFromTemplate,
|
||||
startEmpty,
|
||||
startGpsWorkout,
|
||||
pauseTimer,
|
||||
resumeTimer,
|
||||
addExercise,
|
||||
|
||||
@@ -7,13 +7,15 @@
|
||||
*/
|
||||
|
||||
import { getWorkout } from '$lib/js/workout.svelte';
|
||||
import type { WorkoutExercise } from '$lib/js/workout.svelte';
|
||||
import type { WorkoutExercise, WorkoutMode, GpsActivityType } from '$lib/js/workout.svelte';
|
||||
|
||||
type SyncStatus = 'idle' | 'synced' | 'syncing' | 'offline' | 'conflict';
|
||||
|
||||
interface ServerWorkout {
|
||||
version: number;
|
||||
name: string;
|
||||
mode: WorkoutMode;
|
||||
activityType: GpsActivityType | null;
|
||||
templateId: string | null;
|
||||
exercises: WorkoutExercise[];
|
||||
paused: boolean;
|
||||
@@ -42,6 +44,8 @@ export function createWorkoutSync() {
|
||||
return {
|
||||
version: serverVersion,
|
||||
name: workout.name,
|
||||
mode: workout.mode,
|
||||
activityType: workout.activityType,
|
||||
templateId: workout.templateId,
|
||||
exercises: JSON.parse(JSON.stringify(workout.exercises)),
|
||||
paused: workout.paused,
|
||||
@@ -107,6 +111,8 @@ export function createWorkoutSync() {
|
||||
// but we keep the higher value for completed sets
|
||||
workout.applyRemoteState({
|
||||
name: doc.name,
|
||||
mode: doc.mode ?? 'manual',
|
||||
activityType: doc.activityType ?? null,
|
||||
templateId: doc.templateId,
|
||||
exercises: doc.exercises,
|
||||
paused: doc.paused,
|
||||
@@ -225,6 +231,8 @@ export function createWorkoutSync() {
|
||||
serverVersion = serverDoc.version;
|
||||
workout.restoreFromRemote({
|
||||
name: serverDoc.name,
|
||||
mode: serverDoc.mode ?? 'manual',
|
||||
activityType: serverDoc.activityType ?? null,
|
||||
templateId: serverDoc.templateId,
|
||||
exercises: serverDoc.exercises,
|
||||
paused: serverDoc.paused,
|
||||
|
||||
Reference in New Issue
Block a user