Moments: per-user saved pages and regions with one category (BL-0055)
Reader: bookmark button and M save the page (Flow: page at the reading line, Book: the spread) with an optional dragged region, exactly one category (Twist, Hot, Awesome, Cute, Funny, Feels) and a note. I now toggles the bars; ? lists all keys. Keys dispatch through public/keys.js, which also feeds the new Settings -> Keyboard shortcuts section. Manga page: "Your moments" strip of crops (story or recent order, category filters), viewer with step, full page, edit, delete and "Open in reader" (read.html ...&p=). Moments point at pages by file name and show "page moved" when the file is gone. Private per user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
41e7ed063e
commit
74842c59e0
15 files changed
+734
-18
No files matched your search
@@ -164,6 +164,7 @@
|
||||
main.append(h('details', { class: 'metadata card', open: metadataOpen },
|
||||
h('summary', { class: 'metadata-summary' }, h('h2', {}, 'Metadata')),
|
||||
metadata, rawMeta));
|
||||
main.append(momentsGallery());
|
||||
|
||||
const volumeButtons = new Map();
|
||||
const volumeBar = h('div', { class: 'volume-filter', 'aria-label': 'Filter chapters by volume' });
|
||||
@@ -316,6 +317,54 @@
|
||||
return { text: `◷ ${readingDuration(active, true)}`, title: `${readingDuration(active)} active; current pass in progress` };
|
||||
}
|
||||
|
||||
// Your moments (BL-0055): hidden until there is at least one. Only the user's own are ever returned.
|
||||
function momentsGallery() {
|
||||
const sortKey = `moments-sort.${me.name}`;
|
||||
let list = [], sort = store.get(sortKey) === 'recent' ? 'recent' : 'story', filter = null;
|
||||
const section = h('section', { class: 'card moments-card', hidden: true });
|
||||
const count = h('span', { class: 'muted small' });
|
||||
const sortButtons = ['story', 'recent'].map(key => h('button', { type: 'button', 'data-sort': key,
|
||||
onclick: () => { sort = key; store.set(sortKey, key); draw(); } }, key === 'story' ? 'Story' : 'Recent'));
|
||||
const filters = h('div', { class: 'moment-filters', role: 'group', 'aria-label': 'Filter moments by category' });
|
||||
const strip = h('div', { class: 'moment-strip' });
|
||||
section.append(h('div', { class: 'moments-head' }, h('h2', {}, 'Your moments'), count, h('div', { class: 'grow' }),
|
||||
h('div', { class: 'seg', role: 'group', 'aria-label': 'Sort moments' }, sortButtons)), filters, strip);
|
||||
const shown = () => {
|
||||
const items = list.filter(m => !filter || m.cat === filter);
|
||||
return sort === 'recent' ? items.sort((a, b) => b.t - a.t) : items.sort((a, b) => {
|
||||
for (let k = 0; k < 3; k++) if (a.order[k] !== b.order[k]) return a.order[k] - b.order[k];
|
||||
return a.t - b.t;
|
||||
});
|
||||
};
|
||||
function draw() {
|
||||
section.hidden = !list.length;
|
||||
count.textContent = `${list.length} saved`;
|
||||
for (const b of sortButtons) b.classList.toggle('on', b.dataset.sort === sort);
|
||||
// Category filters only help once there are a few moments in more than one category.
|
||||
const cats = MOMENT_CATS.filter(c => list.some(m => m.cat === c.id));
|
||||
if (filter && !cats.some(c => c.id === filter)) filter = null;
|
||||
filters.hidden = list.length < 5 || cats.length < 2;
|
||||
filters.replaceChildren(...cats.map(c => h('button', { type: 'button', class: 'filter-chip moment-filter' + (filter === c.id ? ' on' : ''),
|
||||
'aria-pressed': String(filter === c.id), onclick: () => { filter = filter === c.id ? null : c.id; draw(); } },
|
||||
momentIcon(c.id, 14), `${c.label} ${list.filter(m => m.cat === c.id).length}`)));
|
||||
const items = shown();
|
||||
strip.replaceChildren(...items.map((m, k) => h('button', { type: 'button', class: 'moment-thumb', title: `${momentCat(m.cat).label} · ${m.chapter.label} · ${m.missing ? 'page moved' : momentPageLabel(m.pages)}${m.note ? '\n' + m.note : ''}`,
|
||||
onclick: () => momentViewer({ mangaId: M.id, mangaTitle: M.title, list: items, index: k, onChange: changed }) },
|
||||
momentCrop(m.pages, m.region, 200, 120),
|
||||
h('span', { class: 'moment-thumb-cat' }, momentIcon(m.cat, 14)),
|
||||
h('span', { class: 'moment-thumb-label' }, `${m.chapter.label} · ${m.missing ? 'moved' : momentPageLabel(m.pages)}`))));
|
||||
}
|
||||
// The viewer edits its (sorted, filtered) copy; fold the changes back into the full list.
|
||||
function changed(items) {
|
||||
const byId = new Map(items.map(m => [m.id, m]));
|
||||
const visible = new Set(shown().map(m => m.id));
|
||||
list = list.filter(m => !visible.has(m.id) || byId.has(m.id)).map(m => byId.get(m.id) || m);
|
||||
draw();
|
||||
}
|
||||
api('api/moments?' + qsa({ m: M.id })).then(data => { list = data.moments; draw(); }).catch(() => {});
|
||||
return section;
|
||||
}
|
||||
|
||||
drawVolumeFilter();
|
||||
drawChapters();
|
||||
})();
|
||||
Reference in new issue
Block a user