Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 23e397d8a5d5bf7756362640
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/lib/store.ts
// Everything Lift keeps. Two stores on the device:
// - the log (settings, your exercises, body weight, routines, finished workouts), synced with the
// person's Bounded account by src/lib/sync.ts;
// - the workout in progress and its rest timer, kept only on this device so typing a set stays fast
// and a reload or a dropped connection never loses it.
import { useSyncExternalStore } from "react";
import { LIBRARY, type Exercise } from "./library";
import { findExercise, history, randomId, recordsByWorkout, workoutFromRoutine, type Custom, type Routine, type Unit, type Workout } from "./model";
export interface Settings { unit: Unit; rest: number; sound: boolean; vibrate: boolean; rpe: boolean; awake: boolean; at: number }
/** One body weight entry; kg is left out when the entry was deleted (kept so other devices delete it too). */
export interface BodyWeight { kg?: number; t: number }
export interface State {
v: 1;
owner: string | null; // the account this device copy belongs to
settings: Settings;
custom: Record<string, Custom>;
bw: Record<string, BodyWeight>; // YYYY-MM-DD -> entry
routines: Record<string, Routine>;
workouts: Record<string, Workout>; // finished workouts; deleted ones stay as small markers
/** What the account is known to hold, so a sync reads only what changed and writes only what changed:
* since is the newest server time seen (seconds), at maps each routine and workout doc to the version written or read. */
sync?: { uid: string; since: number; at: Record<string, number> };
}
export interface Rest { end: number; total: number }
export interface Active { workout: Workout | null; rest: Rest | null }
const pounds = () => typeof navigator !== "undefined" && /^en-US|^en-LR|^my/.test(navigator.language || "");
export function fresh(): State {
return {
v: 1, owner: null,
settings: { unit: pounds() ? "lb" : "kg", rest: 90, sound: true, vibrate: true, rpe: false, awake: true, at: 0 },
custom: {}, bw: {}, routines: {}, workouts: {},
};
}
const KEY = "lift:v1";
const ACTIVE = "lift:active";
function read<T>(key: string, base: T): T {
try {
const raw = localStorage.getItem(key);
if (raw) return { ...base, ...JSON.parse(raw) };
} catch { /* storage blocked or corrupt: start fresh */ }
return base;
}
let state: State = read(KEY, fresh());
state.settings = { ...fresh().settings, ...state.settings };
let active: Active = read<Active>(ACTIVE, { workout: null, rest: null });
const listeners = new Set<() => void>();
let afterUpdate: (() => void) | null = null;
export const getState = () => state;
export const getActive = () => active;
export function onLocalChange(fn: () => void) { afterUpdate = fn; }
const subscribe = (cb: () => void) => { listeners.add(cb); return () => { listeners.delete(cb); }; };
const emit = () => { for (const l of listeners) l(); };
export const useStore = (): State => useSyncExternalStore(subscribe, () => state);
export const useActive = (): Active => useSyncExternalStore(subscribe, () => active);
/** Replaces the log (after merging with the account's copy). Does not trigger a sync. */
export function replace(next: State) {
state = next;
persist();
}
/** Records what the account holds. Saved with the log in one write, so the two never disagree. */
export function saveSync(sync: NonNullable<State["sync"]>) {
state = { ...state, sync };
persist();
}
/** Changes the log, saves it on the device and schedules a sync. */
export function update(fn: (s: State) => void) {
const next: State = structuredClone(state);
fn(next);
state = next;
persist();
afterUpdate?.();
}
function persist() {
try { localStorage.setItem(KEY, JSON.stringify(state)); } catch { /* full or blocked: the account copy still has it */ }
emit();
}
/** Changes the workout in progress or the rest timer. Device only. */
export function updateActive(fn: (a: Active) => void) {
const next: Active = structuredClone(active);
fn(next);
if (next.workout) next.workout.at = Date.now();
active = next;
try { localStorage.setItem(ACTIVE, JSON.stringify(active)); } catch { /* blocked: it stays in memory */ }
emit();
}
// Another tab of Lift changed something: follow it.
if (typeof window !== "undefined") {
window.addEventListener("storage", (e) => {
if (e.key === ACTIVE) active = read<Active>(ACTIVE, { workout: null, rest: null });
else if (e.key === KEY) state = read(KEY, fresh());
else return;
emit();
});
}
// Shortcuts used across pages ---------------------------------------------------------------------
export const exerciseOf = (id: string, s: State = state): Exercise => findExercise(id, s.custom);
export function allExercises(s: State): Exercise[] {
const mine = Object.values(s.custom).filter((c) => !c.deleted);
return [...LIBRARY, ...mine].sort((a, b) => a.name.localeCompare(b.name));
}
let pastCache: { of: State["workouts"]; list: Workout[] } | null = null;
/** Finished workouts, newest first (cached until the log changes). */
export function pastWorkouts(s: State = state): Workout[] {
if (pastCache?.of !== s.workouts) pastCache = { of: s.workouts, list: history(s.workouts) };
return pastCache.list;
}
export function startWorkout(w: Workout) {
updateActive((a) => { a.workout = w; a.rest = null; });
}
export function startEmpty(name: string) {
startWorkout(workoutFromRoutine({ name, items: [] }));
}
export function saveRoutine(r: Omit<Routine, "id" | "at">, created: Custom[] = []): string {
const id = randomId();
update((s) => {
for (const c of created) s.custom[c.id] = c;
s.routines[id] = { ...r, id, at: Date.now() };
});
return id;
}
export function bodyWeights(s: State): { day: string; kg: number }[] {
return Object.entries(s.bw).filter(([, e]) => e.kg).map(([day, e]) => ({ day, kg: e.kg! })).sort((a, b) => a.day.localeCompare(b.day));
}
let recCache: { of: State["workouts"]; map: ReturnType<typeof recordsByWorkout> } | null = null;
/** The records each finished workout set (cached until the log changes). */
export function workoutRecords(s: State = state) {
if (recCache?.of !== s.workouts) recCache = { of: s.workouts, map: recordsByWorkout(pastWorkouts(s), s.custom) };
return recCache.map;
}
Change history
Point OpenApps store links to openapps.free
Add favicon.ico, local share image and openapi.json for links and guides
Bind agent address in policy constants
Add OpenApp contribution strip and OpenApps source links
Bounded source sync
Bounded source sync
Bounded source sync