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
+66
-11
@@ -149,6 +149,8 @@
|
||||
const k = list.findIndex(x => x.i >= p);
|
||||
cur = k < 0 ? list.length - 1 : k;
|
||||
}
|
||||
// A link to a page (e.g. "Open in reader" from a moment) wins over the saved position.
|
||||
if (P.get('p') != null) { const k = list.findIndex(x => x.i >= +P.get('p')); if (k >= 0) cur = k; }
|
||||
|
||||
// Book mode: pages pair up inside each run of single pages. Runs restart after every double page / cover,
|
||||
// so one missing pair never shifts the rest of the chapter. "Shift" makes a run start with a lone page.
|
||||
@@ -480,18 +482,67 @@
|
||||
else toggleBars();
|
||||
});
|
||||
|
||||
// Keys are looked up in READER_KEYS (keys.js), the same table Settings and "?" show.
|
||||
const keyActions = {
|
||||
turn: e => { e.preventDefault(); (e.key === (rtl ? 'ArrowLeft' : 'ArrowRight') ? next : prev)(); },
|
||||
forward: e => { if (mode === 'flow') return; e.preventDefault(); (e.shiftKey ? prev : next)(); },
|
||||
back: e => { if (mode === 'flow') return; e.preventDefault(); prev(); },
|
||||
zoomIn: () => stepZoom(1),
|
||||
zoomOut: () => stepZoom(-1),
|
||||
zoomFit: () => setZoom(1),
|
||||
moment: e => { e.preventDefault(); saveMoment(); },
|
||||
bars: () => toggleBars(),
|
||||
fullscreen: () => toggleFullscreen(),
|
||||
help: () => showKeys(),
|
||||
};
|
||||
addEventListener('keydown', e => {
|
||||
if ((e.target.closest && e.target.closest('input, select, textarea')) || e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
const k = e.key;
|
||||
if (k === 'ArrowLeft' || k === 'ArrowRight') { e.preventDefault(); (k === (rtl ? 'ArrowLeft' : 'ArrowRight') ? next : prev)(); }
|
||||
else if ((k === ' ' || k === 'PageDown') && mode !== 'flow') { e.preventDefault(); (e.shiftKey ? prev : next)(); }
|
||||
else if (k === 'PageUp' && mode !== 'flow') { e.preventDefault(); prev(); }
|
||||
else if (k === '+' || k === '=') stepZoom(1);
|
||||
else if (k === '-') stepZoom(-1);
|
||||
else if (k === '0') setZoom(1);
|
||||
else if (k === 'm' || k === 'Escape') toggleBars();
|
||||
else if (k === 'f') toggleFullscreen();
|
||||
if (document.querySelector('dialog[open]')) return; // an open dialog handles its own keys (Esc closes it)
|
||||
const action = keyAction(READER_KEYS, e.key);
|
||||
if (action) keyActions[action](e);
|
||||
});
|
||||
function showKeys() {
|
||||
const dialog = h('dialog', { class: 'moment-dialog key-dialog' },
|
||||
h('div', { class: 'moment-head' }, h('h2', {}, 'Keyboard shortcuts'),
|
||||
h('button', { class: 'btn icon', type: 'button', 'aria-label': 'Close', onclick: () => dialog.close() }, '×')),
|
||||
keyTable([{ title: 'Reader', keys: READER_KEYS }]),
|
||||
h('p', { class: 'muted small' }, 'Also listed in Settings → Keyboard shortcuts.'));
|
||||
dialog.addEventListener('close', () => { dialog.remove(); stage.focus({ preventScroll: true }); });
|
||||
document.body.append(dialog);
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
// ---------- moments ----------
|
||||
const momentPages = new Set((D.moments || []).flatMap(x => x.pages));
|
||||
// The page(s) on screen, left to right as displayed. In Flow the page at the reading line.
|
||||
function currentPages() {
|
||||
if (atEnd && mode !== 'flow') return [];
|
||||
const ks = mode === 'book' ? shown.slice() : [cur];
|
||||
if (mode === 'book' && rtl) ks.reverse();
|
||||
return ks.filter(k => list[k]).map(k => {
|
||||
const p = list[k], raw = D.pages[p.i];
|
||||
return { i: p.i, u: p.u, w: raw.w, h: raw.h, r: p.r };
|
||||
});
|
||||
}
|
||||
let momentOpen = false;
|
||||
async function saveMoment() {
|
||||
const pages = currentPages();
|
||||
if (!pages.length) return toast('No page here');
|
||||
if (momentOpen) return;
|
||||
momentOpen = true;
|
||||
const chapter = D.chapters.find(c => c.id === D.chapter.id), book = D.books.length > 1 && chapter && D.books.find(b => b.key === chapter.book);
|
||||
const saved = await momentSheet({
|
||||
heading: 'Save moment', pages,
|
||||
context: [D.manga.title, [book && book.label, D.chapter.label + (D.chapter.title ? ' · ' + D.chapter.title : ''), momentPageLabel(pages)].filter(Boolean).join(' · ')],
|
||||
save: async v => (await api('api/moments', { m: D.manga.id, c: D.chapter.id, v: D.version, pages: v.pages.map(p => p.i), region: v.region, cat: v.cat, note: v.note })).moment,
|
||||
});
|
||||
momentOpen = false;
|
||||
stage.focus({ preventScroll: true });
|
||||
if (!saved) return;
|
||||
for (const p of saved.pages) momentPages.add(p.i);
|
||||
updateUI();
|
||||
toast(`Saved · ${momentCat(saved.cat).label} moment`, 1500);
|
||||
}
|
||||
|
||||
let scrollRaf = 0;
|
||||
stage.addEventListener('scroll', () => {
|
||||
@@ -598,8 +649,8 @@
|
||||
const key = mode === 'flow' ? 'hint.flow' : 'hint.paged';
|
||||
if (store.get(key)) return;
|
||||
store.set(key, '1');
|
||||
toast(mode === 'flow' ? 'Controls hidden · tap the middle of the screen or beside the pages to show them'
|
||||
: 'Controls hidden · tap the middle of the page or beside it to show them', 3500);
|
||||
toast(mode === 'flow' ? 'Controls hidden · tap the middle of the screen or beside the pages (or press I) to show them'
|
||||
: 'Controls hidden · tap the middle of the page or beside it (or press I) to show them', 3500);
|
||||
}
|
||||
|
||||
let toastTimer = 0;
|
||||
@@ -625,6 +676,9 @@
|
||||
sh.hidden = mode !== 'book';
|
||||
sh.classList.toggle('on', shown.length > 0 && !solo(list[shown[0]]) && shift.has(list[runStart(shown[0])].i));
|
||||
$('#zoom').textContent = Math.round(zoom() * 100) + '%';
|
||||
const hasMoment = currentPages().some(p => momentPages.has(p.i));
|
||||
$('#moment').classList.toggle('on', hasMoment);
|
||||
$('#moment').title = (hasMoment ? 'This page has a moment · save another' : 'Save a moment of this page') + ' (M)';
|
||||
}
|
||||
|
||||
let saveTimer = 0;
|
||||
@@ -669,6 +723,7 @@
|
||||
ver.onchange = () => { saveProgress(true); location.href = readUrl(D.manga.id, D.chapter.id, ver.value); };
|
||||
for (const b of modeBtns) b.onclick = () => { if (b.dataset.mode !== mode) setMode(b.dataset.mode, true); };
|
||||
toneButton.onclick = cycleTone;
|
||||
$('#moment').onclick = saveMoment;
|
||||
$('#shift').onclick = toggleShift;
|
||||
$('#zoomout').onclick = () => stepZoom(-1);
|
||||
$('#zoom').onclick = () => setZoom(1);
|
||||
|
||||
Reference in new issue
Block a user