lift-free.openapps.xyz

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.
Owned app · rev f9f4e37d6945
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.
← All files
src/lib/timer.ts
// The rest timer's sound and vibration, and keeping the screen on during a workout.
// The end-of-rest beeps are scheduled on the audio clock as soon as rest starts, so they still play
// when the browser slows down timers in the background (on iPhone, a locked screen stops sound).
import { getActive, getState, updateActive } from "./store";

let ctx: AudioContext | null = null;
let beeps: OscillatorNode[] = [];
let buzzTimer: number | undefined;

/** Creates or wakes the audio engine. Browsers only allow this during a tap, so taps call it. */
export function unlockAudio() {
  if (!ctx) {
    const AC = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
    if (!AC) return;
    // Short sounds that mix with the music a person is listening to.
    const session = (navigator as Navigator & { audioSession?: { type: string } }).audioSession;
    if (session) { try { session.type = "transient"; } catch { /* older Safari */ } }
    ctx = new AC();
    ctx.addEventListener("statechange", arm);
  }
  if (ctx.state !== "running") void ctx.resume().catch(() => null);
}

function beep(c: AudioContext, at: number, freq: number, length: number) {
  const o = c.createOscillator();
  const g = c.createGain();
  o.type = "sine";
  o.frequency.value = freq;
  g.gain.setValueAtTime(0, at);
  g.gain.linearRampToValueAtTime(0.32, at + 0.01);
  g.gain.setValueAtTime(0.32, at + length - 0.04);
  g.gain.linearRampToValueAtTime(0, at + length);
  o.connect(g).connect(c.destination);
  o.start(at);
  o.stop(at + length + 0.02);
  beeps.push(o);
}

function cancel() {
  for (const o of beeps) { try { o.stop(); } catch { /* already done */ } }
  beeps = [];
  window.clearTimeout(buzzTimer);
}

/** Schedules (again) the sound and vibration for the rest timer as it stands. */
export function arm() {
  cancel();
  const rest = getActive().rest;
  if (!rest) return;
  const left = (rest.end - Date.now()) / 1000;
  if (left <= 0) return;
  const { sound, vibrate } = getState().settings;
  if (sound && ctx?.state === "running") {
    const t = ctx.currentTime + left;
    if (left > 2.2) beep(ctx, t - 2, 660, 0.12);
    if (left > 1.2) beep(ctx, t - 1, 660, 0.12);
    beep(ctx, t, 990, 0.42);
  }
  if (vibrate && canVibrate) buzzTimer = window.setTimeout(() => buzz([300, 120, 300]), left * 1000);
}

/** Starts the rest timer. */
export function startRest(seconds: number) {
  if (seconds <= 0) return;
  unlockAudio();
  updateActive((a) => { a.rest = { end: Date.now() + seconds * 1000, total: seconds }; });
  arm();
}

export function adjustRest(delta: number) {
  updateActive((a) => {
    if (!a.rest) return;
    a.rest.end += delta * 1000;
    a.rest.total = Math.max(1, a.rest.total + delta);
    if (a.rest.end <= Date.now()) a.rest = null;
  });
  arm();
}

export function stopRest() {
  updateActive((a) => { a.rest = null; });
  cancel();
}

/** A short test of the end-of-rest sound, from settings. */
export function testSound() {
  unlockAudio();
  if (!ctx) return;
  const c = ctx;
  const go = () => { const t = c.currentTime + 0.05; beep(c, t, 660, 0.12); beep(c, t + 0.4, 990, 0.3); };
  if (c.state === "running") go(); else void c.resume().then(go).catch(() => null);
}

/** Phone vibration (Android; iPhones do not let web pages vibrate). */
export const canVibrate = typeof navigator !== "undefined" && "vibrate" in navigator;
export function buzz(pattern: number | number[]) {
  if (canVibrate && document.visibilityState === "visible") { try { navigator.vibrate(pattern); } catch { /* not allowed */ } }
}

// Keeps the screen on during a workout where the browser allows it. Browsers drop the lock when the
// page is hidden, so it is asked for again when the page comes back.
type Sentinel = { release(): Promise<void>; addEventListener(t: "release", f: () => void): void };
let lock: Sentinel | null = null;
let wanted = false;
async function acquire() {
  const wl = (navigator as Navigator & { wakeLock?: { request(t: "screen"): Promise<Sentinel> } }).wakeLock;
  if (!wl || lock || !wanted || document.visibilityState !== "visible") return;
  lock = await wl.request("screen").catch(() => null);
  lock?.addEventListener("release", () => { lock = null; });
}
export function keepAwake(on: boolean) {
  wanted = on;
  if (on) void acquire();
  else if (lock) { void lock.release().catch(() => null); lock = null; }
}
if (typeof document !== "undefined") {
  document.addEventListener("visibilitychange", () => { void acquire(); if (document.visibilityState === "visible") arm(); });
}

Change history

Point OpenApps store links to openapps.free
f9f4e37d69 · 17 hours ago
Add favicon.ico, local share image and openapi.json for links and guides
2796c0063c · 17 hours ago
Bind agent address in policy constants
cf80a3ca66 · 19 hours ago
Add OpenApp contribution strip and OpenApps source links
5e4cc2b9f3 · 19 hours ago
Bounded source sync
ca4a3d3631 · 1 day ago
Bounded source sync
7351063f4d · 1 day ago
Bounded source sync
0ce3238c46 · 1 day ago