still-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 0fa0e84d537b
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 a8c677472ab5691741798a3d
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/Player.tsx
// The full-screen player every session runs in, the pausable clock it keeps time with, and the
// screen shown when a session ends.
import { useEffect, useRef, useState, type ReactNode } from "react";
import { record, setAfterMood, MIN_SECONDS, type Entry, type Kind } from "./lib/progress";
import { Icon, Mood, fmtMinutes } from "./ui";

/** Elapsed seconds that stop while paused. */
export class Clock {
  private base = 0;
  private since: number | null = null;
  start() { if (this.since == null) this.since = performance.now(); }
  pause() { if (this.since != null) { this.base += performance.now() - this.since; this.since = null; } }
  get running() { return this.since != null; }
  get elapsed() { return (this.base + (this.since == null ? 0 : performance.now() - this.since)) / 1000; }
}

export function PlayerShell({ title, onClose, children, phase }: { title: string; onClose: () => void; children: ReactNode; phase?: string }) {
  const close = useRef<HTMLButtonElement>(null);
  useEffect(() => { close.current?.focus(); }, []);
  useEffect(() => {
    document.body.classList.add("playing");
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => {
      document.body.classList.remove("playing");
      document.removeEventListener("keydown", onKey);
    };
  }, [onClose]);
  return (
    <div className="player" role="dialog" aria-modal="true" aria-label={title} data-phase={phase}>
      <div className="player-top">
        <button type="button" className="round-btn" onClick={onClose} aria-label="Close" ref={close}>
          <Icon name="close" />
        </button>
        <p className="player-title">{title}</p>
        <span className="round-spacer" />
      </div>
      {children}
    </div>
  );
}

/** Saves a finished session: anything finished, or a minute or more when stopped early. */
export function finishSession(kind: Kind, title: string, sec: number, completed: boolean, before?: number): Entry | null {
  if (!completed && sec < MIN_SECONDS) return null;
  return record({ kind, title, sec, before });
}

export function Done({ entry, seconds, what, onAgain, onClose, note }: { entry: Entry | null; seconds: number; what: string; onAgain?: () => void; onClose: () => void; note?: ReactNode }) {
  const [after, setAfter] = useState<number | undefined>();
  return (
    <div className="done">
      <p className="eyebrow">Finished</p>
      <h2 className="done-title">Well done.</h2>
      <p className="done-sub">{entry ? `${fmtMinutes(seconds)} of ${what}.` : "Sessions under a minute are not counted, but every breath helps."}</p>
      {note}
      {entry && (
        <Mood label="How do you feel now?" value={after} onChange={(v) => { setAfter(v); setAfterMood(entry.at, v); }} />
      )}
      <div className="done-actions">
        <button type="button" className="btn btn-lg" onClick={onClose}>Done</button>
        {onAgain && <button type="button" className="btn btn-lg btn-line" onClick={onAgain}>Again</button>}
      </div>
    </div>
  );
}

/** Play and pause, plus optional step buttons on either side. */
export function Controls({ running, onToggle, onPrev, onNext, label }: { running: boolean; onToggle: () => void; onPrev?: () => void; onNext?: () => void; label: string }) {
  return (
    <div className="controls">
      {onPrev ? <button type="button" className="round-btn" onClick={onPrev} aria-label="Previous part"><Icon name="prev" /></button> : <span className="round-spacer" />}
      <button type="button" className="play-btn" onClick={onToggle} aria-label={running ? `Pause ${label}` : `Resume ${label}`}>
        <Icon name={running ? "pause" : "play"} size={28} />
      </button>
      {onNext ? <button type="button" className="round-btn" onClick={onNext} aria-label="Next part"><Icon name="next" /></button> : <span className="round-spacer" />}
    </div>
  );
}

Change history

Link to OpenApps pages and add a contribution strip
0fa0e84d53 · 19 hours ago
Bind the agent address in the policy
6f3baa1c72 · 1 day ago
Add favicon.ico so browsers that ask for it stop getting a 404
1a004fe0e3 · 1 day ago
Bounded source sync
afe642fe50 · 2 days ago
Bounded source sync
2c581f55ea · 2 days ago
Bounded source sync
375934c28b · 2 days ago
Bounded source sync
74ff37d51a · 2 days ago