Stage 1: project setup, Exercise Library, both Mode designs and the Mode switch
- Flutter Android app on CookIT's stack (provider, shared_preferences, gen-l10n) - tools/exercise-import turns free-exercise-db into 876 bundled Exercises with 1,746 WebP images (20 MB); stretching -> Stretching, rest -> Workout - ZenStyle tokens per Mode: calm rounded sage/teal with breath ripples vs. dark ember-to-red with floor-tape slashes; the theme morphs at each Mode's own pace - Remembered global Mode switch, Plans / Library / History tabs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5dfa995d55
commit
1289fadb98
1822 files changed
+2819
No files matched your search
@@ -0,0 +1,69 @@
|
||||
"""Turns a checkout of free-exercise-db into ZenXercise's bundled Exercise Library.
|
||||
|
||||
Writes `exercises.json` (slimmed, with each Exercise's Mode) and one WebP per
|
||||
image into the output folder, flattened to `images/<id>_<n>.webp` because
|
||||
Flutter assets are declared per folder.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
STRETCHING_CATEGORY = "stretching"
|
||||
MAX_EDGE = 480
|
||||
WEBP_QUALITY = 72
|
||||
|
||||
|
||||
def mode_of(category: str) -> str:
|
||||
"""Category `stretching` belongs to Stretching Mode, everything else to Workout Mode."""
|
||||
return "stretching" if category == STRETCHING_CATEGORY else "workout"
|
||||
|
||||
|
||||
def slim(exercise: dict) -> dict:
|
||||
"""The fields the app needs, with images reduced to a count."""
|
||||
return {
|
||||
"id": exercise["id"],
|
||||
"name": exercise["name"],
|
||||
"mode": mode_of(exercise["category"]),
|
||||
"category": exercise["category"],
|
||||
"level": exercise["level"],
|
||||
"equipment": exercise.get("equipment"),
|
||||
"force": exercise.get("force"),
|
||||
"mechanic": exercise.get("mechanic"),
|
||||
"primaryMuscles": exercise["primaryMuscles"],
|
||||
"secondaryMuscles": exercise["secondaryMuscles"],
|
||||
"instructions": [line.strip() for line in exercise["instructions"] if line.strip()],
|
||||
"imageCount": len(exercise["images"]),
|
||||
}
|
||||
|
||||
|
||||
def image_name(exercise_id: str, index: int) -> str:
|
||||
return f"{exercise_id}_{index}.webp"
|
||||
|
||||
|
||||
def convert_image(source: Path, target: Path) -> None:
|
||||
with Image.open(source) as image:
|
||||
image = image.convert("RGB")
|
||||
image.thumbnail((MAX_EDGE, MAX_EDGE))
|
||||
image.save(target, "WEBP", quality=WEBP_QUALITY, method=6)
|
||||
|
||||
|
||||
def run(source_root: Path, output_root: Path) -> list[dict]:
|
||||
exercises = json.loads((source_root / "dist" / "exercises.json").read_text(encoding="utf-8"))
|
||||
images = output_root / "images"
|
||||
images.mkdir(parents=True, exist_ok=True)
|
||||
for stale in images.glob("*.webp"):
|
||||
stale.unlink()
|
||||
|
||||
slimmed = sorted((slim(e) for e in exercises), key=lambda e: e["name"].lower())
|
||||
for exercise in exercises:
|
||||
for index, relative in enumerate(exercise["images"]):
|
||||
convert_image(source_root / "exercises" / relative, images / image_name(exercise["id"], index))
|
||||
|
||||
(output_root / "exercises.json").write_text(
|
||||
json.dumps(slimmed, ensure_ascii=False, separators=(",", ":")), encoding="utf-8"
|
||||
)
|
||||
return slimmed
|
||||
Reference in new issue
Block a user