My Store: select books and add them to chosen people's shelves
build-container / build (push) Successful in 3m53s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-28 09:50:30 -04:00
1 parent 3376499805
commit 4e26bea455
5 files changed
+52 -2

No files matched your search

+24 -2
View File
@@ -29,19 +29,26 @@ async function viewStore(args = []) {
}
// ---------------------------------------------------------------- My Store
let stSelecting = false; const stPicked = new Set();
async function stLoad() { const r = await stApi('/items'); stItems = r.items; stInstalled = r.installed; stSeen = r.seen || {}; stAdmin = r.admin; return r; }
async function stMine(el) {
try { await stLoad(); } catch (e) { el.innerHTML = `<p class="err">${esc(e.message)}</p>`; return; }
const draw = () => {
if (!$('#stbody')) return;
const mine = stItems.filter(i => i.kind === 'extension' && stInstalled.includes(i.id)), shown = stItems.filter(i => stFilter === 'all' || i.kind === stFilter);
el.innerHTML = `<div class="bar" style="margin:0 0 8px;justify-content:flex-start"><p class="sub" style="margin:0;flex:1">Books, music, apps and extensions added just for this hub.</p>${stAdmin ? `<label class="btn ghost" style="cursor:pointer;margin:0 6px 0 0" title="Pick many EPUB, PDF, MOBI, AZW3 or DOCX files at once. Titles, authors and covers are read from the books.">${icon('plus')}Add many books<input type="file" id="stbulk" hidden multiple accept=".epub,.pdf,.mobi,.azw3,.azw,.docx"></label><button class="btn" id="stadd">${icon('plus')}Add</button>` : ''}</div>
el.innerHTML = `<div class="bar" style="margin:0 0 8px;justify-content:flex-start"><p class="sub" style="margin:0;flex:1">Books, music, apps and extensions added just for this hub.</p>${stAdmin ? `<label class="btn ghost" style="cursor:pointer;margin:0 6px 0 0" title="Pick many EPUB, PDF, MOBI, AZW3 or DOCX files at once. Titles, authors and covers are read from the books.">${icon('plus')}Add many books<input type="file" id="stbulk" hidden multiple accept=".epub,.pdf,.mobi,.azw3,.azw,.docx"></label><button class="btn ghost" id="stsel" style="margin:0 6px 0 0">${stSelecting ? 'Cancel' : 'Select'}</button><button class="btn" id="stadd">${icon('plus')}Add</button>` : ''}</div>
${stSelecting ? `<div class="bar" style="justify-content:flex-start;position:sticky;top:0;z-index:3;background:var(--bg,var(--panel));padding:8px 0;flex-wrap:wrap"><b style="margin-right:8px">${stPicked.size} selected</b><button class="btn ghost" id="stall">Select all shown</button><button class="btn" id="stgive"${stPicked.size ? '' : ' disabled'}>Add to people…</button></div>` : ''}
${mine.length ? `<h2>My extensions</h2><div class="vgrid">${mine.map(i => stCard(i, true)).join('')}</div>` : ''}
<div class="seg" id="stkinds"><a href="#" data-k="all" class="${stFilter === 'all' ? 'on' : ''}">All</a>${ST_KINDS.map(k => `<a href="#" data-k="${k[0]}" class="${stFilter === k[0] ? 'on' : ''}">${k[1]}</a>`).join('')}</div>
${shown.length ? `<div class="vgrid">${shown.map(i => stCard(i)).join('')}</div>` : `<p class="empty">${stItems.length ? 'Nothing of that kind yet.' : stAdmin ? 'Nothing here yet. Add your first book, album, app or extension.' : 'Nothing has been added yet.'}</p>`}`;
el.querySelectorAll('#stkinds a').forEach(a => a.onclick = e => { e.preventDefault(); stFilter = a.dataset.k; draw(); });
el.querySelectorAll('[data-it]').forEach(a => a.onclick = e => { e.preventDefault(); const it = stItems.find(x => x.id === a.dataset.it); if (a.dataset.run) location.hash = '#/store/ext/' + it.id; else stDetail(it, draw); });
el.querySelectorAll('[data-it]').forEach(a => a.onclick = e => { e.preventDefault(); const it = stItems.find(x => x.id === a.dataset.it);
if (stSelecting) { if (it.kind !== 'book' || !it.files.length) return toast('Only books can be given to people'); stPicked.has(it.id) ? stPicked.delete(it.id) : stPicked.add(it.id); return draw(); } if (a.dataset.run) location.hash = '#/store/ext/' + it.id; else stDetail(it, draw); });
if ($('#stadd')) $('#stadd').onclick = () => stEdit(null, draw);
if ($('#stsel')) $('#stsel').onclick = () => { stSelecting = !stSelecting; stPicked.clear(); if (stSelecting) stFilter = 'book'; draw(); };
el.querySelectorAll('[data-it]').forEach(a => a.classList.toggle('picked', stPicked.has(a.dataset.it)));
if ($('#stgive')) $('#stgive').onclick = () => stGive([...stPicked], () => { stSelecting = false; stPicked.clear(); draw(); });
if ($('#stall')) $('#stall').onclick = () => { const bs = shown.filter(i => i.kind === 'book' && i.files.length); bs.every(i => stPicked.has(i.id)) ? stPicked.clear() : bs.forEach(i => stPicked.add(i.id)); draw(); };
if ($('#stbulk')) $('#stbulk').onchange = e => { const fs = [...e.target.files]; e.target.value = ''; if (fs.length) stBulk(fs, draw); };
};
draw();
@@ -268,3 +275,18 @@ async function stBulk(files, redraw) {
q('#sbx').textContent = 'Done'; done = files.length;
try { await stLoad(); } catch {} redraw();
}
// Give the chosen books to people: they land on each person's shelf (a person without the Books tab is skipped, and a book they already have is left alone).
async function stGive(ids, after) {
let people; try { people = await stApi('/people'); } catch (e) { return toast(e.message); }
const el = openSheet('Add to people', ids.length + (ids.length === 1 ? ' book' : ' books'), `<div class="list" style="max-height:50dvh;overflow:auto">${people.map(p => `<label class="row" style="cursor:pointer"><input type="checkbox" data-u="${esc(p.id)}"${p.books ? '' : ' disabled'}><span class="grow"><b>${esc(p.name)}</b>${p.books ? '' : '<small>No Books tab</small>'}</span></label>`).join('')}</div>
<div class="err" id="sterr"></div><div class="bar"><button class="btn ghost" id="sgx" style="flex:1;justify-content:center">Cancel</button><button class="btn" id="sgok" style="flex:1;justify-content:center">Add</button></div>`);
el.querySelector('#sgx').onclick = closeSheet;
el.querySelector('#sgok').onclick = async e => {
const us = [...el.querySelectorAll('[data-u]:checked')].map(c => c.dataset.u), err = m => { el.querySelector('#sterr').textContent = m; }; if (!us.length) return err('Choose at least one person');
e.currentTarget.disabled = true; e.currentTarget.textContent = 'Adding…';
try { const r = await stApi('/assign', stOpt('POST', { items: ids, users: us })); closeSheet(); const n = r.people.reduce((a, p) => a + p.added, 0), had = r.people.reduce((a, p) => a + p.had, 0), bad = r.people.reduce((a, p) => a + p.failed, 0);
toast(`Added ${n} ${n === 1 ? 'book' : 'books'} to ${r.people.filter(p => p.added).length} ${r.people.filter(p => p.added).length === 1 ? 'person' : 'people'}` + (had ? `, ${had} already had it` : '') + (bad ? `, ${bad} failed` : '')); after();
} catch (x) { err(x.message); e.currentTarget.disabled = false; e.currentTarget.textContent = 'Add'; }
};
}