fitness: GPS workout templates with interval pre-selection
Enable creating templates for GPS-tracked workouts with activity type and optional interval training. GPS templates show activity/interval info instead of exercise lists in cards, modals, and schedule. Starting a GPS template pre-selects the interval and jumps to the map screen.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { getExerciseById } from '$lib/data/exercises';
|
||||
import { EllipsisVertical } from 'lucide-svelte';
|
||||
import { EllipsisVertical, MapPin } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* template: { _id: string, name: string, exercises: Array<{ exerciseId: string, sets: any[] }> },
|
||||
* template: { _id: string, name: string, mode?: string, activityType?: string, exercises: Array<{ exerciseId: string, sets: any[] }> },
|
||||
* lastUsed?: string | null,
|
||||
* onStart?: (() => void) | null,
|
||||
* onMenu?: ((e: MouseEvent) => void) | null
|
||||
@@ -42,15 +42,19 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<ul class="exercise-preview">
|
||||
{#each template.exercises.slice(0, 4) as ex}
|
||||
{@const exercise = getExerciseById(ex.exerciseId, lang)}
|
||||
<li>{ex.sets.length} × {exercise?.localName ?? ex.exerciseId}</li>
|
||||
{/each}
|
||||
{#if template.exercises.length > 4}
|
||||
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{#if template.mode === 'gps'}
|
||||
<div class="gps-badge"><MapPin size={12} /> {template.activityType?.[0]?.toUpperCase()}{template.activityType?.slice(1) ?? 'Running'}</div>
|
||||
{:else}
|
||||
<ul class="exercise-preview">
|
||||
{#each template.exercises.slice(0, 4) as ex}
|
||||
{@const exercise = getExerciseById(ex.exerciseId, lang)}
|
||||
<li>{ex.sets.length} × {exercise?.localName ?? ex.exerciseId}</li>
|
||||
{/each}
|
||||
{#if template.exercises.length > 4}
|
||||
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if lastUsed}
|
||||
<p class="last-used">{t('last_performed', lang)} {formatDate(lastUsed)}</p>
|
||||
{/if}
|
||||
@@ -115,6 +119,14 @@
|
||||
color: var(--color-primary);
|
||||
font-style: italic;
|
||||
}
|
||||
.gps-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--nord10);
|
||||
font-weight: 600;
|
||||
}
|
||||
.last-used {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface StoredState {
|
||||
activityType: GpsActivityType | null;
|
||||
name: string;
|
||||
templateId: string | null;
|
||||
intervalTemplateId: string | null;
|
||||
exercises: WorkoutExercise[];
|
||||
elapsed: number; // total elapsed seconds at time of save
|
||||
savedAt: number; // Date.now() at time of save
|
||||
@@ -57,6 +58,7 @@ export interface RemoteState {
|
||||
mode: WorkoutMode;
|
||||
activityType: GpsActivityType | null;
|
||||
templateId: string | null;
|
||||
intervalTemplateId: string | null;
|
||||
exercises: WorkoutExercise[];
|
||||
paused: boolean;
|
||||
elapsed: number;
|
||||
@@ -100,6 +102,7 @@ export function createWorkout() {
|
||||
let activityType = $state<GpsActivityType | null>(null);
|
||||
let name = $state('');
|
||||
let templateId: string | null = $state(null);
|
||||
let intervalTemplateId: string | null = $state(null);
|
||||
let exercises = $state<WorkoutExercise[]>([]);
|
||||
let startTime: Date | null = $state(null);
|
||||
let _pausedElapsed = $state(0); // seconds accumulated before current run
|
||||
@@ -128,6 +131,7 @@ export function createWorkout() {
|
||||
activityType,
|
||||
name,
|
||||
templateId,
|
||||
intervalTemplateId,
|
||||
exercises: JSON.parse(JSON.stringify(exercises)),
|
||||
elapsed: _elapsed,
|
||||
savedAt: Date.now(),
|
||||
@@ -197,6 +201,7 @@ export function createWorkout() {
|
||||
activityType = stored.activityType ?? null;
|
||||
name = stored.name;
|
||||
templateId = stored.templateId;
|
||||
intervalTemplateId = stored.intervalTemplateId ?? null;
|
||||
exercises = stored.exercises;
|
||||
|
||||
if (stored.paused) {
|
||||
@@ -233,6 +238,7 @@ export function createWorkout() {
|
||||
function startFromTemplate(template: TemplateData) {
|
||||
name = template.name;
|
||||
templateId = template._id;
|
||||
intervalTemplateId = null;
|
||||
mode = 'manual';
|
||||
exercises = template.exercises.map((e) => ({
|
||||
exerciseId: e.exerciseId,
|
||||
@@ -260,6 +266,7 @@ export function createWorkout() {
|
||||
function startEmpty() {
|
||||
name = 'Quick Workout';
|
||||
templateId = null;
|
||||
intervalTemplateId = null;
|
||||
mode = 'manual';
|
||||
exercises = [];
|
||||
startTime = new Date();
|
||||
@@ -280,6 +287,7 @@ export function createWorkout() {
|
||||
};
|
||||
name = labels[activity];
|
||||
templateId = null;
|
||||
intervalTemplateId = null;
|
||||
mode = 'gps';
|
||||
activityType = activity;
|
||||
exercises = [];
|
||||
@@ -291,6 +299,21 @@ export function createWorkout() {
|
||||
_persist();
|
||||
}
|
||||
|
||||
function startFromGpsTemplate(template: { _id: string; name: string; activityType?: string; intervalTemplateId?: string }) {
|
||||
name = template.name;
|
||||
templateId = template._id;
|
||||
intervalTemplateId = template.intervalTemplateId ?? null;
|
||||
mode = 'gps';
|
||||
activityType = (template.activityType as GpsActivityType) ?? 'running';
|
||||
exercises = [];
|
||||
startTime = null;
|
||||
_pausedElapsed = 0;
|
||||
_elapsed = 0;
|
||||
paused = true;
|
||||
active = true;
|
||||
_persist();
|
||||
}
|
||||
|
||||
function pauseTimer() {
|
||||
if (!active || paused) return;
|
||||
_computeElapsed();
|
||||
@@ -450,6 +473,7 @@ export function createWorkout() {
|
||||
activityType = null;
|
||||
name = '';
|
||||
templateId = null;
|
||||
intervalTemplateId = null;
|
||||
exercises = [];
|
||||
startTime = null;
|
||||
_pausedElapsed = 0;
|
||||
@@ -469,6 +493,7 @@ export function createWorkout() {
|
||||
mode = remote.mode ?? 'manual';
|
||||
activityType = remote.activityType ?? null;
|
||||
templateId = remote.templateId;
|
||||
intervalTemplateId = remote.intervalTemplateId ?? null;
|
||||
exercises = remote.exercises;
|
||||
|
||||
if (remote.paused) {
|
||||
@@ -515,6 +540,7 @@ export function createWorkout() {
|
||||
activityType,
|
||||
name,
|
||||
templateId,
|
||||
intervalTemplateId,
|
||||
exercises: JSON.parse(JSON.stringify(exercises)),
|
||||
elapsed: _elapsed,
|
||||
savedAt: Date.now(),
|
||||
@@ -544,6 +570,7 @@ export function createWorkout() {
|
||||
get name() { return name; },
|
||||
set name(v: string) { name = v; _persist(); },
|
||||
get templateId() { return templateId; },
|
||||
get intervalTemplateId() { return intervalTemplateId; },
|
||||
get exercises() { return exercises; },
|
||||
get startTime() { return startTime; },
|
||||
get elapsedSeconds() { return _elapsed; },
|
||||
@@ -557,6 +584,7 @@ export function createWorkout() {
|
||||
startFromTemplate,
|
||||
startEmpty,
|
||||
startGpsWorkout,
|
||||
startFromGpsTemplate,
|
||||
pauseTimer,
|
||||
resumeTimer,
|
||||
addExercise,
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ServerWorkout {
|
||||
mode: WorkoutMode;
|
||||
activityType: GpsActivityType | null;
|
||||
templateId: string | null;
|
||||
intervalTemplateId: string | null;
|
||||
exercises: WorkoutExercise[];
|
||||
paused: boolean;
|
||||
elapsed: number;
|
||||
@@ -47,6 +48,7 @@ export function createWorkoutSync() {
|
||||
mode: workout.mode,
|
||||
activityType: workout.activityType,
|
||||
templateId: workout.templateId,
|
||||
intervalTemplateId: workout.intervalTemplateId,
|
||||
exercises: JSON.parse(JSON.stringify(workout.exercises)),
|
||||
paused: workout.paused,
|
||||
elapsed,
|
||||
@@ -114,6 +116,7 @@ export function createWorkoutSync() {
|
||||
mode: doc.mode ?? 'manual',
|
||||
activityType: doc.activityType ?? null,
|
||||
templateId: doc.templateId,
|
||||
intervalTemplateId: doc.intervalTemplateId ?? null,
|
||||
exercises: doc.exercises,
|
||||
paused: doc.paused,
|
||||
elapsed: doc.elapsed,
|
||||
@@ -234,6 +237,7 @@ export function createWorkoutSync() {
|
||||
mode: serverDoc.mode ?? 'manual',
|
||||
activityType: serverDoc.activityType ?? null,
|
||||
templateId: serverDoc.templateId,
|
||||
intervalTemplateId: serverDoc.intervalTemplateId ?? null,
|
||||
exercises: serverDoc.exercises,
|
||||
paused: serverDoc.paused,
|
||||
elapsed: serverDoc.elapsed,
|
||||
|
||||
Reference in New Issue
Block a user