Files
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

115 lines
6.0 KiB
JavaScript

'use strict';
// Minimal RGB canvas + PNG encoder + 5x7 bitmap font. Only used to generate the demo library.
const zlib = require('zlib');
// prettier-ignore
const FONT = {
A: '01110 10001 10001 11111 10001 10001 10001', B: '11110 10001 10001 11110 10001 10001 11110',
C: '01110 10001 10000 10000 10000 10001 01110', D: '11110 10001 10001 10001 10001 10001 11110',
E: '11111 10000 10000 11110 10000 10000 11111', F: '11111 10000 10000 11110 10000 10000 10000',
G: '01110 10001 10000 10111 10001 10001 01111', H: '10001 10001 10001 11111 10001 10001 10001',
I: '01110 00100 00100 00100 00100 00100 01110', J: '00111 00010 00010 00010 00010 10010 01100',
K: '10001 10010 10100 11000 10100 10010 10001', L: '10000 10000 10000 10000 10000 10000 11111',
M: '10001 11011 10101 10101 10001 10001 10001', N: '10001 10001 11001 10101 10011 10001 10001',
O: '01110 10001 10001 10001 10001 10001 01110', P: '11110 10001 10001 11110 10000 10000 10000',
Q: '01110 10001 10001 10001 10101 10010 01101', R: '11110 10001 10001 11110 10100 10010 10001',
S: '01111 10000 10000 01110 00001 00001 11110', T: '11111 00100 00100 00100 00100 00100 00100',
U: '10001 10001 10001 10001 10001 10001 01110', V: '10001 10001 10001 10001 10001 01010 00100',
W: '10001 10001 10001 10101 10101 10101 01010', X: '10001 10001 01010 00100 01010 10001 10001',
Y: '10001 10001 01010 00100 00100 00100 00100', Z: '11111 00001 00010 00100 01000 10000 11111',
0: '01110 10001 10011 10101 11001 10001 01110', 1: '00100 01100 00100 00100 00100 00100 01110',
2: '01110 10001 00001 00010 00100 01000 11111', 3: '11111 00010 00100 00010 00001 10001 01110',
4: '00010 00110 01010 10010 11111 00010 00010', 5: '11111 10000 11110 00001 00001 10001 01110',
6: '00110 01000 10000 11110 10001 10001 01110', 7: '11111 00001 00010 00100 01000 01000 01000',
8: '01110 10001 10001 01110 10001 10001 01110', 9: '01110 10001 10001 01111 00001 00010 01100',
'.': '00000 00000 00000 00000 00000 01100 01100', '-': '00000 00000 00000 11111 00000 00000 00000',
':': '00000 01100 01100 00000 01100 01100 00000', '/': '00000 00001 00010 00100 01000 10000 00000',
'#': '01010 01010 11111 01010 11111 01010 01010', '<': '00010 00100 01000 10000 01000 00100 00010',
'>': '01000 00100 00010 00001 00010 00100 01000', '(': '00010 00100 01000 01000 01000 00100 00010',
')': '01000 00100 00010 00010 00010 00100 01000', '!': '00100 00100 00100 00100 00100 00000 00100',
'?': '01110 10001 00001 00010 00100 00000 00100', ',': '00000 00000 00000 00000 01100 00100 01000',
"'": '00100 00100 01000 00000 00000 00000 00000', '&': '01100 10010 10100 01000 10101 10010 01101',
'+': '00000 00100 00100 11111 00100 00100 00000', '=': '00000 00000 11111 00000 11111 00000 00000',
};
const CRC = new Int32Array(256).map((_, n) => {
for (let k = 0; k < 8; k++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1;
return n;
});
function crc32(buf) {
let c = -1;
for (let i = 0; i < buf.length; i++) c = CRC[(c ^ buf[i]) & 255] ^ (c >>> 8);
return (c ^ -1) >>> 0;
}
function chunk(type, data) {
const head = Buffer.alloc(4), crc = Buffer.alloc(4), body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
head.writeUInt32BE(data.length);
crc.writeUInt32BE(crc32(body));
return Buffer.concat([head, body, crc]);
}
function hsl(h, s, l) {
h = (((h % 360) + 360) % 360) / 30;
const a = s * Math.min(l, 1 - l);
const f = n => { const k = (n + h) % 12; return Math.round(255 * (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)))); };
return [f(0), f(8), f(4)];
}
class Canvas {
constructor(w, h, bg) {
this.w = w; this.h = h; this.px = Buffer.alloc(w * h * 3);
if (bg) this.rect(0, 0, w, h, bg);
}
rect(x, y, w, h, color) {
const x0 = Math.max(0, Math.round(x)), y0 = Math.max(0, Math.round(y));
const x1 = Math.min(this.w, Math.round(x + w)), y1 = Math.min(this.h, Math.round(y + h));
if (x1 <= x0 || y1 <= y0) return;
const pat = Buffer.from(color);
for (let r = y0; r < y1; r++) this.px.fill(pat, (r * this.w + x0) * 3, (r * this.w + x1) * 3);
}
frame(x, y, w, h, t, color) {
this.rect(x, y, w, t, color); this.rect(x, y + h - t, w, t, color);
this.rect(x, y, t, h, color); this.rect(x + w - t, y, t, h, color);
}
textWidth(s, size) { return String(s).length * 6 * size - size; }
text(s, x, y, size, color, align) {
s = String(s).toUpperCase();
if (align === 'center') x -= this.textWidth(s, size) / 2;
if (align === 'right') x -= this.textWidth(s, size);
for (const ch of s) {
const g = FONT[ch];
if (g) g.split(' ').forEach((row, ry) => {
for (let rx = 0; rx < 5; rx++) if (row[rx] === '1') this.rect(x + rx * size, y + ry * size, size, size, color);
});
x += 6 * size;
}
}
// Text on a padded box, handy for labels that must stay readable on any background.
label(s, x, y, size, color, box, align = 'center') {
const w = this.textWidth(s, size), pad = size * 2;
const left = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
this.rect(left - pad, y - pad, w + 2 * pad, 7 * size + 2 * pad, box);
this.text(s, left, y, size, color);
}
crop(y, h) {
const c = new Canvas(this.w, h);
this.px.copy(c.px, 0, y * this.w * 3, (y + h) * this.w * 3);
return c;
}
// Rotate 90° counter-clockwise (so a viewer must rotate it 90° clockwise to see it upright).
rotateCCW() {
const c = new Canvas(this.h, this.w);
for (let y = 0; y < this.h; y++)
for (let x = 0; x < this.w; x++) this.px.copy(c.px, ((this.w - 1 - x) * c.w + y) * 3, (y * this.w + x) * 3, (y * this.w + x) * 3 + 3);
return c;
}
png() {
const ihdr = Buffer.alloc(13), stride = this.w * 3, raw = Buffer.alloc((stride + 1) * this.h);
ihdr.writeUInt32BE(this.w, 0); ihdr.writeUInt32BE(this.h, 4); ihdr[8] = 8; ihdr[9] = 2; // 8 bit RGB
for (let y = 0; y < this.h; y++) this.px.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
}
}
module.exports = { Canvas, hsl };