Skip to content

Custom exercise photo/GIF, and per-side on timed holds - #295

Closed
horusglez wants to merge 1 commit into
DuarteSantos8:mainfrom
horusglez:feat/custom-exercise-media
Closed

horusglez wants to merge 1 commit into
DuarteSantos8:mainfrom
horusglez:feat/custom-exercise-media

Conversation

@horusglez

Copy link
Copy Markdown

What

Two small, additive things I was missing running my own instance:

1. A photo or GIF on a custom exercise. Issue #11's custom exercises have no
animation by design — the shipped dataset's media isn't ours to redistribute
(NOTICE.md) — but a picture you supply is a different thing. It's stored as a
data: URL right on the exercise, riding along inside the same PUT /api/data
blob everything else in the store already goes through: no upload endpoint, no
server-side storage, no new dependency. imgSrc/gifSrc pass a data: URL
through untouched instead of prefixing it with the dataset's base path. A GIF
is used as both the still and the animation (no first frame to pull out of it
without a library); a plain photo is downscaled on a canvas so a phone photo
doesn't blow the 5 MB body cap (api/server.js MAX_BODY) on its own.

2. "Per side" on a timed hold. Issues #31/#32/#33 added per-side reps, but
side was dropped when switching to Time — a hold has no rep count to split.
"Per side" doesn't have to mean splitting, though: for a hold it means doing
the whole thing once on each side. The flag now survives the switch to Time
and buildWorkSets doubles the planned sets instead of dividing the duration —
2 sets of 30s becomes 2 left + 2 right, each still 30s. These are plain rows
tagged side: 'L'|'R', not the L/R sub-row pair reps mode uses (isSideSet) —
a hold has one duration to log per row, not two independent values to track
side by side. exLine reads 3 × 0:45 · per side rather than trying to spell
out a split that was never how a hold worked.

Why bundled together

Both are small, both touch the same exercise-config sheet, and both are purely
additive (existing configs / existing per-side reps behavior are unchanged) —
happy to split into two PRs if you'd rather review them separately.

Testing

  • npm run build — clean.
  • npm test — 1589 pass (1584 on main + 5 new: buildSets doubling and
    history-carry for a timed per-side hold, exLine's new "per side" wording,
    imgSrc/gifSrc's data: URL passthrough).
  • I could not click through the actual screens in a browser for this PR (no
    browser available in the environment I built this in) — logic is covered by
    the tests above, but the UI itself (the photo picker, the doubled rows
    during a workout) has not been eyeballed. Flagging that explicitly per
    CONTRIBUTING.md's "test the flow you touched" — happy to do a follow-up pass
    once I can.

Open questions for you

  • Any size/dimension limits you'd rather enforce on the custom photo/GIF
    differently than what I picked (4 MB raw, ~1.5 MB encoded, 640px max
    dimension for a downscaled photo)?
  • Is doubling the planned sets the right call for timed per-side, or would you
    rather it stayed a single row with an L/R label and no duration change?
    I went with doubling because that's the actual training protocol (hold
    it once per side), but I know it's a bigger behavioral surface than the
    photo/GIF change.

🤖 Generated with Claude Code

Custom exercises (issue DuarteSantos8#11) had no way to carry a photo or GIF — by
design, since the shipped dataset's media is not ours to redistribute
(NOTICE.md). A picture you supply yourself is a different thing: it is
stored as a data: URL right on the exercise, riding along inside the
same PUT /api/data blob everything else in the store already goes
through — no upload endpoint, no server-side storage, no new
dependency. imgSrc/gifSrc pass a data: URL through untouched instead of
prefixing it with the dataset's base path. A GIF is used as both the
still and the animation (no first frame to pull out of it without a
library); a plain photo is downscaled on a canvas so a phone photo does
not blow the 5 MB PUT /api/data body cap on its own.

Per side (issues DuarteSantos8#31/DuarteSantos8#32/DuarteSantos8#33) was dropped when switching an exercise to
Time, because it counts reps and a timed hold has none to split. But
"per side" does not have to mean splitting: for a hold it means doing
the whole thing once per side, so the flag now survives the switch and
buildWorkSets doubles the planned sets instead of dividing the
duration — 2 sets of 30s becomes 2 left + 2 right, each still 30s. The
rows are plain, tagged with `side: 'L'|'R'`, not the L/R sub-row pair
reps mode uses (isSideSet) — a hold has one duration to log per row,
not two independent values. exLine reads "3 × 0:45 · per side" rather
than trying to spell out a split that was never how a hold works.

1589 tests pass (1584 before + 5 new: buildSets doubling/history-carry
for a timed per-side hold, exLine's new "per side" wording, imgSrc/
gifSrc's data: URL passthrough).
DuarteSantos8 added a commit that referenced this pull request Sep 28, 2026
…video or guide — in the editor, and wherever an exercise's picture shows

- The editor gets a "Photo, GIF or video" row and a link field. A picked file is re-encoded on the device (photos: WebP where the browser really writes it, else JPEG, at most 1600 px, plus a 480 px poster — EXIF and GPS gone by construction; GIFs lose their comment and metadata blocks; MP4/MOV have their metadata boxes and non-picture, non-sound tracks zeroed in place), hashed, and kept in the local store before the form is saved. The name of the file is never kept. Refusals say why: type, size, length, a photo too large to decode, a file this browser cannot read; a video that may not play everywhere says so
- Media.jsx sends every custom exercise to CustomMedia.jsx, so the detail sheet, the config sheet, the workout card, the picker, the library, a past workout, the routine editor and the muscle explorer all show it with no change of their own. Lists load posters only. Videos up to 15 s loop muted like the catalogue's GIFs (not in the list layout, not under reduced motion); longer ones play with sound and controls on a tap. A file that is not here shows the tile, and a tap asks again
- A link is a card that opens it in a new context without opener or referrer; nothing fetches it, no thumbnail, no embed. It is cleaned on save and again on every open
- "Create your own exercise" no longer says "no animation"

Based on PR #295 by horusglez (the picture row of the editor: the draft's thumb, Add/Change and Remove).
Based on PR #246 by Vaibhav159 (the link field, cleanUrl and the link card).
DuarteSantos8 added a commit that referenced this pull request Sep 28, 2026
…n the exercises you create, kept on your server and on the device, never in the synced data (#126, #170, #295, #246)
@DuarteSantos8

Copy link
Copy Markdown
Owner

Thanks @horusglez. Custom exercises can carry a photo, GIF or short video in v1.3.9, and that work is credited "Based on PR #295 by horusglez / #246 by Vaibhav159".

I built it differently from your version: a data: URL inside the synced state would have pushed PUT /api/data over its 5 MB cap after a few pictures, and then the whole profile stops syncing. So the files are kept on your server (owner-only) and on the device, never in the synced data. EXIF/GPS is stripped, they work offline once viewed, and there's a per-profile quota.

I didn't take per side on timed holds. It's a good idea, but doubling the rows to 2n needs a progression fix too: readSession only grades the first planned rows, so the right side would never count. Could you open it as its own PR, rebased on v1.3.9, with that fixed? Closing this one for now.

Released in v1.3.9: https://github.com/DuarteSantos8/openGym/releases/tag/v1.3.9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants