Compare commits

...
4 Commits
Author SHA1 Message Date
oliver a6c7c653ff Update image manifest (10 images) 2026-09-27 11:58:40 -03:00
oliver 60b560f35f Update image manifest (1 image) 2026-09-27 11:32:58 -03:00
oliver af59ab4a9d Update image manifest (1 image) 2026-09-27 11:25:35 -03:00
oliver a28201bdf6 Update image manifest (1 images) 2026-09-27 11:25:20 -03:00
14 changed files with 118 additions and 17 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Python bytecode
__pycache__/
*.py[cod]
+36 -9
View File
@@ -9,7 +9,8 @@ static files.
index.html the page
assets/styles.css styling (auto light/dark)
assets/app.js search, rendering, clipboard logic
scripts/generate-manifest.mjs scans img/ -> images.json
generate-manifest.py scans img/ -> images.json, then commits
pushAll regenerate + commit + push, in one command
images.json generated manifest the page fetches
img/ put your images here
```
@@ -18,8 +19,8 @@ img/ put your images here
A browser cannot list a directory on its own, and "no backend" rules out an API
that would do it. So the file list is produced by a tiny build step:
`scripts/generate-manifest.mjs` walks `img/` and writes `images.json`. The page
fetches that file at runtime. Everything the server does is serve static files.
`generate-manifest.py` walks `img/` and writes `images.json`. The page fetches
that file at runtime. Everything the server does is serve static files.
## Quick start
@@ -27,8 +28,8 @@ fetches that file at runtime. Everything the server does is serve static files.
# 1. Add images (subfolders are fine)
cp ~/pictures/*.png img/
# 2. Build the manifest
node scripts/generate-manifest.mjs
# 2. Build the manifest (and commit it)
./generate-manifest.py
# 3. Preview locally
python3 -m http.server 8080
@@ -37,6 +38,32 @@ python3 -m http.server 8080
Re-run step 2 whenever you add, rename, or delete images.
## Publishing with `pushAll`
`pushAll` does the whole publish flow in one command: regenerate the manifest,
commit the changes, and push to the remote.
```sh
./pushAll
```
To run it as `pushAll` from anywhere, either add this folder to your `PATH`, or
create a git alias once:
```sh
git config alias.pushAll '!./pushAll'
# then:
git pushAll
```
### `generate-manifest.py` options
```sh
./generate-manifest.py # generate + commit
./generate-manifest.py --no-commit # generate only
./generate-manifest.py -m "Add logos" # custom commit message
```
## Features
- **Search** — case-insensitive, multi-word with AND semantics. Every word must
@@ -54,13 +81,13 @@ and point a server block at it. See [`nginx.conf.example`](nginx.conf.example).
```sh
# build the manifest locally, then upload everything
node scripts/generate-manifest.mjs
./generate-manifest.py --no-commit
rsync -av --delete ./ user@server:/var/www/image-library/
```
If you'd rather generate the manifest on the server, install Node there and run
the script after uploading images. Node is only needed for that step — nginx
never runs it.
If you'd rather generate the manifest on the server, install Python 3 there and
run the script after uploading images. Python is only needed for that step —
nginx never runs it.
### Clipboard over plain HTTP
+3 -3
View File
@@ -1,6 +1,6 @@
/* Image Library — single-page app, no backend.
*
* Reads `images.json` (generated by scripts/generate-manifest.mjs), renders a
* Reads `images.json` (generated by ./generate-manifest.py), renders a
* searchable grid, and copies an image's absolute URL to the clipboard on click.
*/
(() => {
@@ -42,7 +42,7 @@
els.manifestInfo.textContent = "";
showEmpty(
"Could not load images.json",
`Run <code>node scripts/generate-manifest.mjs</code> to build the manifest, then reload.`
`Run <code>./generate-manifest.py</code> to build the manifest, then reload.`
);
els.loading.hidden = true;
return;
@@ -96,7 +96,7 @@
if (images.length === 0) {
showEmpty(
"No images yet",
`Drop files into <code>img/</code>, then run <code>node scripts/generate-manifest.mjs</code>.`
`Drop files into <code>img/</code>, then run <code>./generate-manifest.py</code>.`
);
updateCount(0, 0);
return;
+20 -3
View File
@@ -67,15 +67,31 @@ def find_images() -> list[dict]:
return images
def write_manifest(images: list[dict]) -> None:
def write_manifest(images: list[dict]) -> bool:
"""Write images.json. Returns True if the file changed.
If the image list is identical to what's already on disk, the file is left
untouched (keeping its timestamp) so a no-op run produces no commit.
"""
plural = "" if len(images) == 1 else "s"
if OUT_FILE.exists():
try:
existing = json.loads(OUT_FILE.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
existing = None
if isinstance(existing, dict) and existing.get("images") == images:
print(f"• Manifest unchanged ({len(images)} image{plural}).")
return False
manifest = {
"generatedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"count": len(images),
"images": images,
}
OUT_FILE.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
plural = "" if len(images) == 1 else "s"
print(f"✓ Wrote {len(images)} image{plural} to {OUT_FILE.name}")
return True
def git(*args: str, check: bool = True) -> subprocess.CompletedProcess:
@@ -117,7 +133,8 @@ def main() -> None:
if args.no_commit:
return
message = args.message or f"Update image manifest ({len(images)} images)"
plural = "" if len(images) == 1 else "s"
message = args.message or f"Update image manifest ({len(images)} image{plural})"
commit(message)
+56 -2
View File
@@ -1,12 +1,66 @@
{
"generatedAt": "2026-09-27T14:23:41.878271Z",
"count": 1,
"generatedAt": "2026-09-27T14:58:40.581641Z",
"count": 10,
"images": [
{
"name": "20_4server.png",
"path": "img/20_4server.png",
"size": 75795,
"mtime": "2026-09-27T14:53:46.302842Z"
},
{
"name": "agape.png",
"path": "img/agape.png",
"size": 547758,
"mtime": "2026-09-27T14:57:25.924802Z"
},
{
"name": "module_bi_API.png",
"path": "img/module_bi_API.png",
"size": 61897,
"mtime": "2026-09-27T14:55:22.255514Z"
},
{
"name": "module_bi_sql.png",
"path": "img/module_bi_sql.png",
"size": 30417,
"mtime": "2026-09-27T14:54:54.820894Z"
},
{
"name": "module_mcp.png",
"path": "img/module_mcp.png",
"size": 54085,
"mtime": "2026-09-27T14:56:52.260268Z"
},
{
"name": "module_stripe_basket.png",
"path": "img/module_stripe_basket.png",
"size": 41195,
"mtime": "2026-09-27T14:56:18.464736Z"
},
{
"name": "oca_module_status.png",
"path": "img/oca_module_status.png",
"size": 34048,
"mtime": "2026-09-27T14:20:34.636407Z"
},
{
"name": "ODOO_20_chatter.png",
"path": "img/ODOO_20_chatter.png",
"size": 43289,
"mtime": "2026-09-27T14:53:08.772361Z"
},
{
"name": "project_planner.png",
"path": "img/project_planner.png",
"size": 217244,
"mtime": "2026-09-27T14:51:25.364793Z"
},
{
"name": "py_hackathon.png",
"path": "img/py_hackathon.png",
"size": 457773,
"mtime": "2026-09-27T14:58:11.247175Z"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB