'use strict'; /* * Moments: a page, or a region across up to four neighbouring pages, that a reader saved with exactly one category. * `layout` says how those pages sit together: side by side ("row", Page and Book mode) or stacked ("column", Flow). * Stored per user in db.moments[user] as a list. A moment points at its pages by version folder and file * name (plus the file's SHA-256 for a later relocation), so a re-scan that shifts page numbers never makes it * point at the wrong picture: a file that is gone shows as "page moved" instead. */ const CATEGORIES = ['twist', 'hot', 'awesome', 'cute', 'funny', 'feels']; const NOTE_MAX = 200, MAX_PER_USER = 5000, MAX_PAGES = 4, LAYOUTS = ['row', 'column']; class MomentError extends Error { constructor(msg) { super(msg); this.code = 400; } } const fail = msg => { throw new MomentError(msg); }; const clamp01 = x => Math.min(1, Math.max(0, x)); // A region is a rectangle in fractions (0–1) of the displayed page, or of all its pages put together in `layout`. // Nothing or a rectangle covering (almost) everything means the whole page. function cleanRegion(r) { if (r == null) return null; const n = ['x', 'y', 'w', 'h'].map(k => Number(r[k])); if (n.some(v => !Number.isFinite(v))) fail('Invalid region.'); const x = clamp01(n[0]), y = clamp01(n[1]), w = clamp01(Math.min(n[2], 1 - x)), h = clamp01(Math.min(n[3], 1 - y)); if (w < 0.01 || h < 0.01) fail('The selected region is too small.'); if (w > 0.995 && h > 0.995) return null; const round = v => Math.round(v * 1e4) / 1e4; return { x: round(x), y: round(y), w: round(w), h: round(h) }; } function cleanCategory(cat) { return CATEGORIES.includes(cat) ? cat : fail('Pick one category: ' + CATEGORIES.join(', ') + '.'); } const cleanNote = note => String(note == null ? '' : note).replace(/\s+/g, ' ').trim().slice(0, NOTE_MAX); // Page indexes (into the version's full page list) in displayed order: left to right, or top to bottom. function cleanPages(pages, version) { if (!Array.isArray(pages) || pages.length < 1 || pages.length > MAX_PAGES) fail(`A moment covers 1 to ${MAX_PAGES} pages.`); const out = pages.map(Number); if (out.some(i => !Number.isInteger(i) || i < 0 || i >= version.pages.length)) fail('Page not found.'); if (new Set(out).size !== out.length) fail('A page appears twice.'); return out; } const cleanLayout = layout => (layout == null ? 'row' : LAYOUTS.includes(layout) ? layout : fail('Invalid page layout.')); // Where a stored moment's pages are now. Returns null when the chapter, the version or a page file is gone. function locate(moment, manga) { const chapter = manga && manga.chapterById.get(moment.c), version = chapter && chapter.versions.find(v => v.id === moment.v); if (!version) return null; const pages = moment.f.map((file, k) => { const at = moment.i[k]; return version.pages[at] && version.pages[at].f === file ? at : version.pages.findIndex(p => p.f === file); }); return pages.every(i => i >= 0) ? { chapter, version, pages } : null; } // The browser's view of a moment. `url` turns a library-relative path into an image URL. function view(moment, manga, url) { const at = locate(moment, manga); const chapter = at ? at.chapter : manga && manga.chapterById.get(moment.c); return { id: moment.id, c: moment.c, v: moment.v, cat: moment.cat, note: moment.note || '', t: moment.t, region: moment.region || null, layout: moment.layout || 'row', chapter: chapter ? { label: chapter.label, title: chapter.title, book: chapter.book } : { label: moment.c, title: '', book: '' }, // Story order: chapter position in the manga, then page, then how far down the page the region starts. order: [chapter ? manga.chapters.indexOf(chapter) : 1e9, at ? Math.min(...at.pages) : moment.i[0], moment.region ? moment.region.y : 0], missing: !at, pages: at ? at.pages.map(i => { const p = at.version.pages[i]; return { i, u: url(at.version.rel + '/' + p.f), w: p.w, h: p.h, r: p.r || 0 }; }) : [], }; } function storyCompare(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; } module.exports = { CATEGORIES, NOTE_MAX, MAX_PER_USER, MAX_PAGES, MomentError, cleanRegion, cleanCategory, cleanNote, cleanPages, cleanLayout, locate, view, storyCompare };