Consolidate MangaCircus app, scraper, and Compose baseline
This commit is contained in:
1 parent
cfb2d79103
commit
eef4ffa07c
103 files changed
+9316
-172
No files matched your search
+81
-13
@@ -16,7 +16,7 @@
|
||||
catch (e) { canvas.textContent = ''; canvas.append(h('p', { class: 'loading' }, e.message + ' · ', h('a', { href: './' }, 'Library'))); return; }
|
||||
|
||||
const S = me.settings, rtl = D.direction === 'rtl', ttb = D.direction === 'ttb';
|
||||
const GAP = ttb ? 0 : 8, END_H = 280;
|
||||
const GAP = ttb || S.flowPageGap === false ? 0 : 8, END_H = 280;
|
||||
document.title = `${D.chapter.label} · ${D.manga.title}`;
|
||||
|
||||
// ---------- pages ----------
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
let cur = 0, atEnd = false, done = !!(D.progress && D.progress.done), bars = true, barsAt = 0;
|
||||
let spreads = [], spreadIdx = 0, shown = [], els = [], boxes = [], endEl = null, lastW = 0, lastH = 0;
|
||||
let lockFlowX = false, readingClock = null;
|
||||
const shift = new Set(D.progress && D.progress.v === D.version ? D.progress.shift : []);
|
||||
const zkey = () => (mode === 'flow' && ttb ? 'strip' : mode);
|
||||
const zoom = () => S.zoom[zkey()] || 1;
|
||||
@@ -112,8 +113,9 @@
|
||||
Object.assign(p, { unknown: false, w, a: w / hh, wide: p.wide || w > hh });
|
||||
relayout();
|
||||
}
|
||||
if (readingClock) readingClock.viewChanged();
|
||||
};
|
||||
img.onerror = state;
|
||||
img.onerror = () => { state(); if (readingClock) readingClock.viewChanged(); };
|
||||
state();
|
||||
}
|
||||
function setSrc(el, url) { el.firstChild.src = url; watch(el); }
|
||||
@@ -162,6 +164,7 @@
|
||||
}))).then(() => { if (token === buildToken) preloadAround(); });
|
||||
}
|
||||
layout();
|
||||
if (readingClock) readingClock.viewChanged();
|
||||
}
|
||||
let buildToken = 0;
|
||||
|
||||
@@ -198,7 +201,11 @@
|
||||
canvas.style.width = cw + 'px';
|
||||
canvas.style.height = ch + 'px';
|
||||
els.forEach((el, j) => place(el, boxes[j]));
|
||||
stage.classList.toggle('pan', cw > vw + 1 || (mode === 'flow' ? z > 1.01 : ch > vh + 1));
|
||||
const overflowsX = cw > vw + 1;
|
||||
lockFlowX = mode === 'flow' && !overflowsX;
|
||||
stage.classList.toggle('pan', overflowsX || (mode !== 'flow' && ch > vh + 1));
|
||||
stage.classList.toggle('lock-x', lockFlowX);
|
||||
if (lockFlowX) stage.scrollLeft = 0;
|
||||
lastW = vw; lastH = vh;
|
||||
}
|
||||
|
||||
@@ -230,17 +237,18 @@
|
||||
function restore(a, cx, cy) {
|
||||
if (!a || !boxes.length) return;
|
||||
const b = a.k < 0 ? union() : boxes[a.k];
|
||||
stage.scrollLeft = b.x + a.fx * b.w - cx;
|
||||
stage.scrollLeft = lockFlowX ? 0 : b.x + a.fx * b.w - cx;
|
||||
stage.scrollTop = b.y + a.fy * b.h - cy;
|
||||
barsAt = stage.scrollTop;
|
||||
}
|
||||
function relayout() { const a = anchor(lastW / 2, lastH / 2); layout(); restore(a, lastW / 2, lastH / 2); }
|
||||
|
||||
// ---------- zoom ----------
|
||||
const MIN_ZOOM = 0.2, MAX_ZOOM = 6, ZOOM_STEP = 0.05;
|
||||
let zoomTimer = 0;
|
||||
function setZoom(z, cx = stage.clientWidth / 2, cy = stage.clientHeight / 2) {
|
||||
z = Math.min(6, Math.max(0.2, z));
|
||||
if (Math.abs(z - 1) < 0.03) z = 1; // easy to get back to "fit"
|
||||
z = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z));
|
||||
if (Math.abs(z - 1) < 0.01) z = 1; // easy to get back to "fit" without swallowing fine adjustments
|
||||
if (Math.abs(z - zoom()) < 0.001) return;
|
||||
const a = anchor(cx, cy);
|
||||
S.zoom[zkey()] = z;
|
||||
@@ -252,11 +260,39 @@
|
||||
zoomTimer = setTimeout(() => api('api/settings', { zoom: S.zoom }).catch(() => {}), 800);
|
||||
}
|
||||
|
||||
// Pages exposed enough to count as something the reader could actually inspect.
|
||||
// The clock handles decoding, duration and plausibility; this keeps layout knowledge here.
|
||||
function readingView() {
|
||||
const viewport = stage.getBoundingClientRect(), viewportArea = viewport.width * viewport.height, pages = [];
|
||||
for (let j = 0; j < els.length; j++) {
|
||||
const el = els[j], rect = el.getBoundingClientRect();
|
||||
const w = Math.max(0, Math.min(rect.right, viewport.right) - Math.max(rect.left, viewport.left));
|
||||
const hh = Math.max(0, Math.min(rect.bottom, viewport.bottom) - Math.max(rect.top, viewport.top));
|
||||
const visibleArea = w * hh, pageArea = Math.max(1, rect.width * rect.height);
|
||||
if (visibleArea < 0.10 * Math.min(pageArea, viewportArea)) continue;
|
||||
const img = el.firstChild, page = mode === 'flow' ? list[j] : list[shown[j]];
|
||||
pages.push({
|
||||
index: page.i,
|
||||
loaded: !!(img.complete && img.naturalWidth),
|
||||
failed: !!(img.complete && img.getAttribute('src') && !img.naturalWidth),
|
||||
});
|
||||
}
|
||||
const last = boxes[boxes.length - 1];
|
||||
const flowEnd = mode === 'flow' && last && !pages.length && stage.scrollTop + stage.clientHeight >= last.y + last.h + 32;
|
||||
return { pages, atEnd: mode === 'flow' ? !!flowEnd : atEnd };
|
||||
}
|
||||
|
||||
function stepZoom(direction) {
|
||||
const units = zoom() / ZOOM_STEP;
|
||||
const stepped = (direction > 0 ? Math.floor(units + 1e-6) + 1 : Math.ceil(units - 1e-6) - 1) * ZOOM_STEP;
|
||||
setZoom(Math.round(stepped * 100) / 100);
|
||||
}
|
||||
|
||||
// Ctrl + wheel (and touchpad pinch, which browsers report the same way) zooms the pages, not the page controls.
|
||||
stage.addEventListener('wheel', e => {
|
||||
if (!e.ctrlKey) return;
|
||||
e.preventDefault();
|
||||
setZoom(zoom() * Math.exp(-e.deltaY * (e.deltaMode === 1 ? 0.05 : 0.0025)), e.clientX, e.clientY);
|
||||
setZoom(zoom() * Math.exp(-e.deltaY * (e.deltaMode === 1 ? 0.015 : 0.0005)), e.clientX, e.clientY);
|
||||
}, { passive: false });
|
||||
|
||||
// Touch: two fingers zoom, a quick horizontal swipe turns the page (page/book mode, when not zoomed in).
|
||||
@@ -266,10 +302,18 @@
|
||||
stage.addEventListener('touchstart', e => {
|
||||
const t = e.touches;
|
||||
if (t.length === 2) { pinch = { d: dist(t), z: zoom() }; swipe = null; }
|
||||
else if (t.length === 1) swipe = { x: t[0].clientX, y: t[0].clientY, t: Date.now() };
|
||||
else if (t.length === 1) swipe = { x: t[0].clientX, y: t[0].clientY, top: stage.scrollTop, t: Date.now() };
|
||||
}, { passive: true });
|
||||
stage.addEventListener('touchmove', e => {
|
||||
if (!pinch || e.touches.length !== 2) return;
|
||||
if (!pinch || e.touches.length !== 2) {
|
||||
const t = e.touches[0], s = swipe;
|
||||
if (t && s && lockFlowX && Math.abs(t.clientX - s.x) > Math.abs(t.clientY - s.y)) {
|
||||
e.preventDefault();
|
||||
stage.scrollLeft = 0;
|
||||
stage.scrollTop = s.top - (t.clientY - s.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const t = e.touches;
|
||||
pinchNext = [pinch.z * dist(t) / pinch.d, (t[0].clientX + t[1].clientX) / 2, (t[0].clientY + t[1].clientY) / 2];
|
||||
@@ -293,7 +337,14 @@
|
||||
// Mouse: drag to move a zoomed page around; a click without dragging uses the tap zones.
|
||||
let drag = null, dragged = false;
|
||||
stage.addEventListener('pointerdown', e => {
|
||||
if (e.pointerType === 'mouse' && e.button === 0) drag = { x: e.clientX, y: e.clientY, l: stage.scrollLeft, t: stage.scrollTop, moved: false };
|
||||
if (e.pointerType === 'mouse' && e.button === 0) drag = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
l: stage.scrollLeft,
|
||||
t: stage.scrollTop,
|
||||
panX: !lockFlowX && stage.scrollWidth > stage.clientWidth + 1,
|
||||
moved: false
|
||||
};
|
||||
});
|
||||
addEventListener('pointermove', e => {
|
||||
if (!drag) return;
|
||||
@@ -301,7 +352,7 @@
|
||||
if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 6) return;
|
||||
drag.moved = true;
|
||||
stage.classList.add('drag');
|
||||
stage.scrollLeft = drag.l - dx;
|
||||
if (drag.panX) stage.scrollLeft = drag.l - dx;
|
||||
stage.scrollTop = drag.t - dy;
|
||||
});
|
||||
addEventListener('pointerup', () => {
|
||||
@@ -338,8 +389,8 @@
|
||||
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 === '=') setZoom(zoom() * 1.15);
|
||||
else if (k === '-') setZoom(zoom() / 1.15);
|
||||
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();
|
||||
@@ -347,6 +398,7 @@
|
||||
|
||||
let scrollRaf = 0;
|
||||
stage.addEventListener('scroll', () => {
|
||||
if (lockFlowX && stage.scrollLeft !== 0) stage.scrollLeft = 0;
|
||||
if (mode !== 'flow' || scrollRaf) return;
|
||||
scrollRaf = requestAnimationFrame(() => {
|
||||
scrollRaf = 0;
|
||||
@@ -485,6 +537,7 @@
|
||||
if (b && stage.scrollTop + stage.clientHeight >= b.y + b.h - 4) { done = true; cur = list.length - 1; }
|
||||
} else if (atEnd || (mode === 'page' ? cur === list.length - 1 : spreadIdx === spreads.length - 1)) done = true;
|
||||
updateUI();
|
||||
if (readingClock) readingClock.viewChanged();
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(saveProgress, 700);
|
||||
}
|
||||
@@ -519,7 +572,9 @@
|
||||
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); };
|
||||
$('#shift').onclick = toggleShift;
|
||||
$('#zoomout').onclick = () => stepZoom(-1);
|
||||
$('#zoom').onclick = () => setZoom(1);
|
||||
$('#zoomin').onclick = () => stepZoom(1);
|
||||
slider.dir = rtl ? 'rtl' : 'ltr';
|
||||
slider.addEventListener('input', () => goTo(+slider.value - 1));
|
||||
function toggleFullscreen() {
|
||||
@@ -533,6 +588,19 @@
|
||||
for (const bar of [$('#top'), $('#bot')]) bar.addEventListener('click', e => { if (e.target.closest('button')) stage.focus({ preventScroll: true }); });
|
||||
|
||||
setMode(mode, false);
|
||||
if (S.readingClock) readingClock = createReadingClock({
|
||||
button: $('#readclock'),
|
||||
panel: $('#clockpanel'),
|
||||
identity: { m: D.manga.id, c: D.chapter.id, v: D.version },
|
||||
initial: D.reading,
|
||||
eligibleCount: list.length,
|
||||
eligiblePages: list.map(page => page.i),
|
||||
surface: stage,
|
||||
view: readingView,
|
||||
onComplete: result => toast(result.completedPasses > 1
|
||||
? 'Reread time is now counted · 60% of pages viewed'
|
||||
: 'Reading time is now counted · 60% of pages viewed', 3500),
|
||||
});
|
||||
stage.focus({ preventScroll: true });
|
||||
setBars(true);
|
||||
})();
|
||||
Reference in new issue
Block a user