Files
MangaCircus/public/library.js
T
Maashkinoozhe 8dd776b7f4 MangaParty: self-hosted manga/comic/webtoon reader
Zero-dependency Node server + plain HTML/JS frontend.

- Library is a folder tree (Manga/Book/Chapter/pages), indexed in place;
  page roles (cover, double, extra, rotation) are file name tags
- Accounts with per-user progress, reading settings, follows and
  new-chapter notifications (in-app + optional webhook)
- Reader with Flow, Page and Book modes, pointer-anchored page zoom,
  tap zones, auto-hiding controls, chapter picker, page preloading
- Multiple versions per chapter with community votes
- Docker image and compose file
- Demo library generator (tools/make-dummy.js)
- Docs: architecture, library format, import, scraper guide
2026-10-01 09:35:05 +02:00

46 lines
2.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* Library overview: a grid of covers. */
(async function () {
let me, data;
try { [me, data] = await Promise.all([api('api/me'), api('api/library')]); } catch (e) { return showError(e); }
topNav(me);
const main = $('main');
main.textContent = '';
if (!data.mangas.length) {
main.append(h('div', { class: 'empty' },
h('h1', {}, data.scanning ? 'Scanning the library…' : 'The library is empty'),
h('p', { class: 'muted' }, data.scanning ? 'This page refreshes by itself.' : 'Put manga folders into the library folder (see README.md for the format)' + (me.admin ? ', then rescan it in the settings.' : '.'))));
if (data.scanning) setTimeout(() => location.reload(), 2000);
return;
}
const filter = h('input', { type: 'search', placeholder: 'Filter…', 'aria-label': 'Filter', value: store.get('filter') || '' });
const sort = h('select', { 'aria-label': 'Sort' },
h('option', { value: 'title' }, 'A–Z'), h('option', { value: 'updated' }, 'Recently updated'), h('option', { value: 'read' }, 'Recently read'));
sort.value = store.get('sort') || 'title';
const grid = h('div', { class: 'grid' });
main.append(h('div', { class: 'toolbar' }, filter, sort), grid);
function render() {
store.set('filter', filter.value); store.set('sort', sort.value);
const q = filter.value.trim().toLowerCase();
const list = data.mangas.filter(m => !q || m.title.toLowerCase().includes(q));
if (sort.value === 'updated') list.sort((a, b) => b.updated - a.updated);
if (sort.value === 'read') list.sort((a, b) => b.lastRead - a.lastRead);
grid.textContent = '';
for (const m of list) {
grid.append(h('a', { class: 'tile', href: mangaUrl(m.id) },
h('div', { class: 'cv' }, m.cover ? h('img', { loading: 'lazy', src: m.cover, alt: '' }) : null),
m.unread ? h('span', { class: 'badge' }, m.unread + ' new') : null,
h('div', { class: 't' }, m.title),
h('div', { class: 's' }, m.read ? `${m.read} / ${m.chapters} read` : `${m.chapters} chapter${m.chapters === 1 ? '' : 's'}`),
m.read ? h('div', { class: 'prog' }, h('i', { style: `width:${Math.round((100 * m.read) / m.chapters)}%` })) : null));
}
if (!list.length) grid.append(h('p', { class: 'muted' }, 'Nothing matches.'));
}
filter.addEventListener('input', render);
sort.addEventListener('change', render);
render();
})();