Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a018f342a3 | ||
|
|
9ce05da6af | ||
|
|
d7a1b9423f | ||
|
|
f15e8bb488 | ||
|
|
e5ba4d2ba0 | ||
|
|
581b699234 | ||
|
|
0b21192d02 | ||
|
|
cedc509549 | ||
|
|
b0a82f4066 | ||
|
|
65e2cfd903 | ||
|
|
568dfd179d | ||
|
|
a80854f0c0 | ||
|
|
97cc76d6de | ||
|
|
d4b0c82a81 | ||
|
|
4a471bba96 | ||
|
|
711d20b492 | ||
|
|
0f5fb54de2 | ||
|
|
c05c200953 | ||
|
|
f4a105cc2d | ||
|
|
a6c7c653ff | ||
|
|
60b560f35f | ||
|
|
af59ab4a9d | ||
|
|
a28201bdf6 |
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
# Python bytecode
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
@@ -6,20 +6,21 @@ clipboard when you click it. Built to be dropped onto an nginx server as plain
|
|||||||
static files.
|
static files.
|
||||||
|
|
||||||
```
|
```
|
||||||
index.html the page
|
index.html the page
|
||||||
assets/styles.css styling (auto light/dark)
|
assets/styles.css styling (auto light/dark)
|
||||||
assets/app.js search, rendering, clipboard logic
|
assets/app.js search, rendering, clipboard logic
|
||||||
scripts/generate-manifest.mjs scans img/ -> images.json
|
generate-manifest.py scans img/ -> images.json, then commits
|
||||||
images.json generated manifest the page fetches
|
pushAll regenerate + commit + push, in one command
|
||||||
img/ put your images here
|
images.json generated manifest the page fetches
|
||||||
|
img/ put your images here
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why a manifest?
|
## Why a manifest?
|
||||||
|
|
||||||
A browser cannot list a directory on its own, and "no backend" rules out an API
|
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:
|
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
|
`generate-manifest.py` walks `img/` and writes `images.json`. The page fetches
|
||||||
fetches that file at runtime. Everything the server does is serve static files.
|
that file at runtime. Everything the server does is serve static files.
|
||||||
|
|
||||||
## Quick start
|
## 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)
|
# 1. Add images (subfolders are fine)
|
||||||
cp ~/pictures/*.png img/
|
cp ~/pictures/*.png img/
|
||||||
|
|
||||||
# 2. Build the manifest
|
# 2. Build the manifest (and commit it)
|
||||||
node scripts/generate-manifest.mjs
|
./generate-manifest.py
|
||||||
|
|
||||||
# 3. Preview locally
|
# 3. Preview locally
|
||||||
python3 -m http.server 8080
|
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.
|
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
|
## Features
|
||||||
|
|
||||||
- **Search** — case-insensitive, multi-word with AND semantics. Every word must
|
- **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
|
```sh
|
||||||
# build the manifest locally, then upload everything
|
# 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/
|
rsync -av --delete ./ user@server:/var/www/image-library/
|
||||||
```
|
```
|
||||||
|
|
||||||
If you'd rather generate the manifest on the server, install Node there and run
|
If you'd rather generate the manifest on the server, install Python 3 there and
|
||||||
the script after uploading images. Node is only needed for that step — nginx
|
run the script after uploading images. Python is only needed for that step —
|
||||||
never runs it.
|
nginx never runs it.
|
||||||
|
|
||||||
### Clipboard over plain HTTP
|
### Clipboard over plain HTTP
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Image Library — single-page app, no backend.
|
/* 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.
|
* searchable grid, and copies an image's absolute URL to the clipboard on click.
|
||||||
*/
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
els.manifestInfo.textContent = "";
|
els.manifestInfo.textContent = "";
|
||||||
showEmpty(
|
showEmpty(
|
||||||
"Could not load images.json",
|
"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;
|
els.loading.hidden = true;
|
||||||
return;
|
return;
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
showEmpty(
|
showEmpty(
|
||||||
"No images yet",
|
"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);
|
updateCount(0, 0);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -67,15 +67,31 @@ def find_images() -> list[dict]:
|
|||||||
return images
|
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 = {
|
manifest = {
|
||||||
"generatedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
"generatedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||||
"count": len(images),
|
"count": len(images),
|
||||||
"images": images,
|
"images": images,
|
||||||
}
|
}
|
||||||
OUT_FILE.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
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}")
|
print(f"✓ Wrote {len(images)} image{plural} to {OUT_FILE.name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def git(*args: str, check: bool = True) -> subprocess.CompletedProcess:
|
def git(*args: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
@@ -117,7 +133,8 @@ def main() -> None:
|
|||||||
if args.no_commit:
|
if args.no_commit:
|
||||||
return
|
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)
|
commit(message)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,210 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-09-27T14:23:41.878271Z",
|
"generatedAt": "2026-11-07T18:23:07.545629Z",
|
||||||
"count": 1,
|
"count": 34,
|
||||||
"images": [
|
"images": [
|
||||||
|
{
|
||||||
|
"name": "20_4server.png",
|
||||||
|
"path": "img/20_4server.png",
|
||||||
|
"size": 75795,
|
||||||
|
"mtime": "2026-09-27T14:53:46.302842Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "20_Widgets.png",
|
||||||
|
"path": "img/20_Widgets.png",
|
||||||
|
"size": 544100,
|
||||||
|
"mtime": "2026-09-28T08:22:39.268966Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "agape.png",
|
||||||
|
"path": "img/agape.png",
|
||||||
|
"size": 547758,
|
||||||
|
"mtime": "2026-09-27T14:57:25.924802Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "freelance_platform.png",
|
||||||
|
"path": "img/freelance_platform.png",
|
||||||
|
"size": 132494,
|
||||||
|
"mtime": "2026-11-03T23:30:26.879252Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "freelancer_answer.png",
|
||||||
|
"path": "img/freelancer_answer.png",
|
||||||
|
"size": 50845,
|
||||||
|
"mtime": "2026-11-05T17:17:54.462872Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mail_tracker.png",
|
||||||
|
"path": "img/mail_tracker.png",
|
||||||
|
"size": 16086,
|
||||||
|
"mtime": "2026-10-24T10:41:35.355484Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "much.png",
|
||||||
|
"path": "img/much.png",
|
||||||
|
"size": 97957,
|
||||||
|
"mtime": "2026-10-23T07:40:00.171519Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oc-accounting.png",
|
||||||
|
"path": "img/oc-accounting.png",
|
||||||
|
"size": 47738,
|
||||||
|
"mtime": "2026-10-26T20:44:56.621428Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oc-guy-homepage.png",
|
||||||
|
"path": "img/oc-guy-homepage.png",
|
||||||
|
"size": 612313,
|
||||||
|
"mtime": "2026-10-26T20:32:04.193970Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OCA.png",
|
||||||
|
"path": "img/OCA.png",
|
||||||
|
"size": 32945,
|
||||||
|
"mtime": "2026-10-26T21:19:06.384417Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "oca_module_status.png",
|
"name": "oca_module_status.png",
|
||||||
"path": "img/oca_module_status.png",
|
"path": "img/oca_module_status.png",
|
||||||
"size": 34048,
|
"size": 34048,
|
||||||
"mtime": "2026-09-27T14:20:34.636407Z"
|
"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": "openerp_ant.png",
|
||||||
|
"path": "img/openerp_ant.png",
|
||||||
|
"size": 16751,
|
||||||
|
"mtime": "2026-11-03T18:05:37.663468Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "project_planner.png",
|
||||||
|
"path": "img/project_planner.png",
|
||||||
|
"size": 217244,
|
||||||
|
"mtime": "2026-09-27T14:51:25.364793Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PY-modules.png",
|
||||||
|
"path": "img/PY-modules.png",
|
||||||
|
"size": 51699,
|
||||||
|
"mtime": "2026-11-07T12:06:03.842747Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "py.png",
|
||||||
|
"path": "img/py.png",
|
||||||
|
"size": 107159,
|
||||||
|
"mtime": "2026-10-24T09:57:37.876016Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "py_activar.png",
|
||||||
|
"path": "img/py_activar.png",
|
||||||
|
"size": 6803,
|
||||||
|
"mtime": "2026-11-07T12:24:31.738350Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "py_chart_accounts.png",
|
||||||
|
"path": "img/py_chart_accounts.png",
|
||||||
|
"size": 89056,
|
||||||
|
"mtime": "2026-11-07T12:17:24.931823Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "py_first_commit.png",
|
||||||
|
"path": "img/py_first_commit.png",
|
||||||
|
"size": 22299,
|
||||||
|
"mtime": "2026-11-07T12:48:04.004624Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "py_first_day.png",
|
||||||
|
"path": "img/py_first_day.png",
|
||||||
|
"size": 77792,
|
||||||
|
"mtime": "2026-11-07T16:41:08.943775Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "py_hackathon.png",
|
||||||
|
"path": "img/py_hackathon.png",
|
||||||
|
"size": 457773,
|
||||||
|
"mtime": "2026-09-27T14:58:11.247175Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PY_local_Fiscalization.png",
|
||||||
|
"path": "img/PY_local_Fiscalization.png",
|
||||||
|
"size": 15523,
|
||||||
|
"mtime": "2026-11-07T11:54:08.366274Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "screen_currency.png",
|
||||||
|
"path": "img/screen_currency.png",
|
||||||
|
"size": 69444,
|
||||||
|
"mtime": "2026-10-26T21:12:01.506867Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "screen_reconcile.png",
|
||||||
|
"path": "img/screen_reconcile.png",
|
||||||
|
"size": 59135,
|
||||||
|
"mtime": "2026-10-26T21:24:29.530384Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "screencast.png",
|
||||||
|
"path": "img/screencast.png",
|
||||||
|
"size": 871202,
|
||||||
|
"mtime": "2026-11-02T10:34:54.105778Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "screenshot_git.png",
|
||||||
|
"path": "img/screenshot_git.png",
|
||||||
|
"size": 123951,
|
||||||
|
"mtime": "2026-10-28T09:52:19.768785Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "screenshot_quick_git.png",
|
||||||
|
"path": "img/screenshot_quick_git.png",
|
||||||
|
"size": 117697,
|
||||||
|
"mtime": "2026-10-28T09:51:15.338913Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skill_import_bank_statements.png",
|
||||||
|
"path": "img/skill_import_bank_statements.png",
|
||||||
|
"size": 32643,
|
||||||
|
"mtime": "2026-11-07T18:11:36.030758Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skills_import_bank_statement.png",
|
||||||
|
"path": "img/skills_import_bank_statement.png",
|
||||||
|
"size": 67178,
|
||||||
|
"mtime": "2026-11-07T18:22:42.364092Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "website_innter_content.png",
|
||||||
|
"path": "img/website_innter_content.png",
|
||||||
|
"size": 14830,
|
||||||
|
"mtime": "2026-09-28T08:23:45.229053Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 851 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
#
|
|
||||||
# One command to publish everything:
|
|
||||||
# 1. regenerate images.json from ./img
|
|
||||||
# 2. stage + commit the changes
|
|
||||||
# 3. push to the remote
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./pushAll
|
|
||||||
#
|
|
||||||
# Tip: make it a real command by adding this folder to your PATH, or create a
|
|
||||||
# git alias once with: git config alias.pushAll '!./pushAll'
|
|
||||||
# then run it as: git pushAll
|
|
||||||
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# 1 + 2: rebuild the manifest and commit (handled by the Python script).
|
|
||||||
./generate-manifest.py
|
|
||||||
|
|
||||||
# 3: push, setting the upstream on first push.
|
|
||||||
if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
|
||||||
git push
|
|
||||||
else
|
|
||||||
git push -u origin HEAD
|
|
||||||
fi
|
|
||||||