cull.papp
All 47 lines. No libraries, nothing hidden. Read it before you run it.
<!doctype html>
<meta charset="utf-8">
<meta name="papp-write" content=".">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect x='14' y='26' width='46' height='34' rx='5' fill='%23c4c7d4'/><rect x='4' y='16' width='46' height='36' rx='5' fill='%23111114'/><path d='M9 47l12-14 8 9 6-6 10 11z' fill='%23fff'/><circle cx='36' cy='26' r='4.5' fill='%23ff4f00'/></svg>">
<title>Cull</title>
<style>
html, body { height: 100%; margin: 0; background: #0f0f11; color: #f2f2f3; font: 13px/1.4 system-ui, sans-serif; }
img { display: block; width: 100%; height: 100%; object-fit: contain; }
#bar { position: fixed; inset: 0 auto auto 0; height: 3px; background: #ff4f00; transition: width 0.2s; }
p { position: fixed; bottom: 16px; margin: 0; padding: 7px 12px; border-radius: 99px; background: rgb(15 15 17 / 0.7); backdrop-filter: blur(12px); }
#progress { left: 16px; font-variant-numeric: tabular-nums; }
#keys { right: 16px; color: #9b9ba4; }
kbd { padding: 0 5px; border: 1px solid #3a3a42; border-radius: 4px; color: #f2f2f3; font: 12px ui-monospace, monospace; }
</style>
<img id="photo" alt="">
<div id="bar"></div>
<p id="progress"></p>
<p id="keys"><kbd>→</kbd> keep · <kbd>←</kbd> reject · <kbd>Z</kbd> undo</p>
<script type="module">
// → keeps the photo, ← moves it to ./rejects, Z takes back the last choice.
const photos = (await papp.readdir('.')).filter(name => /\.(jpe?g|png|gif|webp)$/i.test(name)).sort();
const choices = [];
await papp.mkdir('rejects');
show();
addEventListener('keydown', async ({ key }) => {
const name = photos[choices.length];
if (key === 'ArrowRight' && name) {
choices.push({ name, kept: true });
} else if (key === 'ArrowLeft' && name) {
choices.push({ name, kept: false });
await papp.rename(name, `rejects/${name}`);
} else if (key === 'z' && choices.length) {
const last = choices.pop();
if (!last.kept) await papp.rename(`rejects/${last.name}`, last.name);
}
show();
});
function show() {
const name = photos[choices.length];
const kept = choices.filter(choice => choice.kept).length;
photo.src = name ? encodeURIComponent(name) : '';
bar.style.width = `${(choices.length / photos.length) * 100}%`;
progress.textContent = name ? `${choices.length + 1} / ${photos.length}, kept ${kept} · ${name}` : `Done. Kept ${kept} of ${photos.length}.`;
}
</script>