This commit is contained in:
2026-03-29 20:52:57 -05:00
parent a97c3a5b57
commit cf155183f2
102 changed files with 55674 additions and 0 deletions

503
animex/pdf.html Normal file
View File

@@ -0,0 +1,503 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animex PDF Reader</title>
<!-- PDF.js Library from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<!-- Font Awesome for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
<style>
:root {
--primary-bg: #121212;
--secondary-bg: rgba(30, 30, 30, 0.9);
--text-color: #e0e0e0;
--accent-color: #FF9500;
--border-color: #333333;
--shadow-color: rgba(0, 0, 0, 0.5);
--icon-fill: #e0e0e0;
--header-height: 55px;
--footer-height: 60px;
}
html, body {
margin: 0; padding: 0; width: 100%; height: 100%;
background-color: var(--primary-bg); color: var(--text-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
overflow: hidden;
}
#app-container { display: flex; flex-direction: column; height: 100vh; width: 100vw; position: relative; }
/* --- Welcome/Message Screen --- */
#message-container {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
display: flex; justify-content: center; align-items: center;
background-color: var(--primary-bg); z-index: 100;
flex-direction: column; padding: 20px; box-sizing: border-box; text-align: center;
}
.loader {
border: 5px solid var(--border-color); border-top: 5px solid var(--accent-color);
border-radius: 50%; width: 50px; height: 50px;
animation: spin 1s linear infinite; margin-bottom: 20px;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
#file-input-button {
background-color: var(--accent-color); color: #121212; border: none; padding: 12px 24px;
border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; display: none; margin-top: 15px;
transition: transform 0.2s ease, background-color 0.2s ease;
}
#file-input-button:hover { background-color: #ffae42; transform: scale(1.05); }
#file-input { display: none; }
/* --- Viewer --- */
#viewer-container {
flex-grow: 1; position: relative; overflow: auto;
display: flex; justify-content: center;
padding-top: calc(var(--header-height) + 20px);
padding-bottom: calc(var(--footer-height) + 20px);
box-sizing: border-box;
}
#pdf-container canvas {
display: block; margin: 0 auto;
max-width: 100%; height: auto;
transition: box-shadow 0.3s ease;
}
#pdf-container.paged-view canvas {
margin-bottom: 30px;
box-shadow: 0 8px 25px var(--shadow-color);
}
#pdf-container.webtoon-view canvas {
margin-bottom: 0px;
}
/* --- Controls --- */
.controls-bar {
position: fixed; left: 0; width: 100%;
background-color: var(--secondary-bg);
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
box-shadow: 0 0 15px var(--shadow-color);
display: flex; justify-content: space-between; align-items: center;
padding: 0 20px; box-sizing: border-box; z-index: 20;
transition: transform 0.3s ease-in-out;
-webkit-tap-highlight-color: transparent;
}
#header { top: 0; height: var(--header-height); border-bottom: 1px solid var(--border-color);}
#footer { bottom: 0; height: var(--footer-height); border-top: 1px solid var(--border-color);}
.controls-hidden #header { transform: translateY(-100%); }
.controls-hidden #footer { transform: translateY(100%); }
.control-group { display: flex; align-items: center; gap: 8px; }
.control-button {
background: none; border: none; color: var(--icon-fill);
cursor: pointer; padding: 10px; border-radius: 50%;
width: 44px; height: 44px; display: flex; justify-content: center; align-items: center;
font-size: 18px; transition: background-color 0.2s ease, color 0.2s ease;
}
.control-button:hover:not(:disabled) { background-color: rgba(255, 255, 255, 0.1); }
.control-button:disabled { color: #666; cursor: not-allowed; }
.control-button.active { color: var(--accent-color); background-color: rgba(255, 149, 0, 0.15); }
#file-name {
font-size: 16px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; max-width: calc(100vw - 400px);
}
#series-title, #chapter-title {
max-width: calc(100vw - 400px);
}
#page-info { font-size: 16px; min-width: 90px; text-align: center; cursor: pointer; padding: 8px 12px; border-radius: 20px; transition: background-color 0.2s ease; user-select: none; }
#page-info:hover { background-color: rgba(255, 255, 255, 0.1); }
#page-input { width: 50px; background-color: var(--primary-bg); color: var(--text-color); border: 1px solid var(--accent-color); text-align: center; font-size: 16px; border-radius: 4px; }
.divider { width: 1px; height: 25px; background-color: var(--border-color); margin: 0 10px; }
@media (max-width: 600px) {
#series-title, #chapter-title { max-width: calc(100vw - 250px); }
#file-name { display: none; }
.divider:not(.mobile-visible) { display: none; }
.control-group { gap: 5px; }
.controls-bar { padding: 0 10px; }
}
</style>
</head>
<body>
<div id="app-container">
<!-- Initial Message/Loader -->
<div id="message-container">
<div id="loader" class="loader"></div>
<p id="message-text">Loading...</p>
<input type="file" id="file-input" accept="application/pdf" />
<button id="file-input-button">Choose a Local PDF</button>
</div>
<!-- PDF Viewer Area -->
<div id="viewer-container">
<div id="pdf-container"></div>
</div>
<!-- Top Controls -->
<header id="header" class="controls-bar">
<div class="control-group">
<!-- <button id="back-button" class="control-button" title="Back" style="display: none;"><i class="fa-solid fa-arrow-left"></i></button> -->
<div style="display: flex; flex-direction: column; align-items: flex-start; margin-left: 10px;">
<span id="series-title" style="font-size: 1em; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></span>
<span id="chapter-title" style="font-size: 0.8em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.8;"></span>
</div>
</div>
<div class="control-group">
<button id="view-webtoon-btn" class="control-button" title="Webtoon View"><i class="fa-solid fa-arrows-up-down"></i></button>
<button id="view-paged-btn" class="control-button" title="Paged View"><i class="fa-solid fa-file-lines"></i></button>
<div class="divider"></div>
<button id="rotate-button" class="control-button" title="Rotate Clockwise"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</header>
<!-- Bottom Controls -->
<footer id="footer" class="controls-bar">
<div class="control-group">
<button id="zoom-out-button" class="control-button" title="Zoom Out"><i class="fa-solid fa-magnifying-glass-minus"></i></button>
<button id="zoom-fit-width-button" class="control-button" title="Fit to Width"><i class="fa-solid fa-arrows-left-right-to-line"></i></button>
<button id="zoom-fit-page-button" class="control-button" title="Fit to Page"><i class="fa-solid fa-up-right-and-down-left-from-center"></i></button>
<button id="zoom-in-button" class="control-button" title="Zoom In"><i class="fa-solid fa-magnifying-glass-plus"></i></button>
</div>
<div id="page-controls" class="control-group">
<span id="page-info">0 / 0</span>
</div>
<div id="progress-bar-container" style="display: none; width: 200px; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden;">
<div id="progress-bar" style="width: 0%; height: 100%; background-color: var(--accent-color);"></div>
</div>
<div class="control-group" style="min-width: 176px; justify-content: flex-end;">
<button id="next-chapter-button" class="control-button" title="Next Chapter" style="display: none;"><i class="fa-solid fa-step-forward"></i></button>
</div>
</footer>
</div>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js`;
// State variables
let pdfDoc = null, currentPageNum = 1, totalPages = 0;
let currentScale = 'auto', currentRotation = 0, currentMode = 'paged';
let isRendering = false, pdfUrl = null, autoHideTimeout = null;
let touchStartX = 0, touchMoveX = 0;
// DOM elements
const appContainer = document.getElementById('app-container');
const viewerContainer = document.getElementById('viewer-container');
const pdfContainer = document.getElementById('pdf-container');
const pageInfo = document.getElementById('page-info');
const messageContainer = document.getElementById('message-container');
const loader = document.getElementById('loader');
const messageText = document.getElementById('message-text');
const fileInput = document.getElementById('file-input');
const fileInputButton = document.getElementById('file-input-button');
const seriesTitleEl = document.getElementById('series-title');
const chapterTitleEl = document.getElementById('chapter-title');
// --- PDF Loading and Rendering ---
async function loadAndRenderPdf(source, seriesTitle, chapterTitle) {
try {
showLoadingState(`Loading PDF...`);
pdfUrl = source;
const loadingTask = pdfjsLib.getDocument(source);
pdfDoc = await loadingTask.promise;
totalPages = pdfDoc.numPages;
currentPageNum = 1;
currentRotation = 0;
seriesTitleEl.textContent = seriesTitle || 'Document';
chapterTitleEl.textContent = chapterTitle || '';
document.title = seriesTitle ? `${seriesTitle} - ${chapterTitle} - Reader` : 'PDF Reader';
messageContainer.style.display = 'none';
appContainer.classList.remove('controls-hidden');
await detectAndSetLayout(); // Smart layout detection
setupEventListeners();
showControls();
} catch (error) {
console.error("Error loading PDF:", error);
showErrorState(`Error: Could not load PDF. Check file/URL.`);
}
}
// Smart feature from Animex
async function detectAndSetLayout() {
const page = await pdfDoc.getPage(1);
const viewport = page.getViewport({ scale: 1 });
const isWebtoon = viewport.height > viewport.width * 2;
setViewMode(isWebtoon ? 'webtoon' : 'paged', true); // Set mode without re-rendering yet
await updateView(); // Now render with the correct mode
}
async function renderPage(num) {
if (isRendering) return;
isRendering = true;
try {
const page = await pdfDoc.getPage(num);
const scale = await calculateScale();
const viewport = page.getViewport({ scale, rotation: currentRotation });
const canvasId = `page-${num}`;
let canvas = document.getElementById(canvasId);
if (!canvas) { return; }
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
canvas.dataset.rendered = "true";
} finally {
isRendering = false;
}
}
async function updateView() {
if (!pdfDoc) return;
pdfContainer.innerHTML = '';
viewerContainer.scrollTop = 0;
if (currentMode === 'paged') {
viewerContainer.style.overflow = 'hidden';
const canvas = document.createElement('canvas');
canvas.id = `page-${currentPageNum}`;
pdfContainer.appendChild(canvas);
await renderPage(currentPageNum);
} else { // webtoon mode
viewerContainer.style.overflow = 'auto';
for (let i = 1; i <= totalPages; i++) {
const canvas = document.createElement('canvas');
canvas.id = `page-${i}`;
pdfContainer.appendChild(canvas);
}
lazyLoadVisiblePages();
}
updateControls();
}
async function calculateScale() {
const page = await pdfDoc.getPage(1);
const viewport = page.getViewport({ scale: 1.0, rotation: currentRotation });
const containerWidth = viewerContainer.clientWidth - 30;
const containerHeight = viewerContainer.clientHeight - 30;
if (currentScale === 'auto') {
return Math.min(containerWidth / viewport.width, containerHeight / viewport.height);
} else if (currentScale === 'width') {
return containerWidth / viewport.width;
}
return currentScale; // It's a number
}
// --- UI and Controls ---
function updateControls() {
pageInfo.textContent = `PG ${currentPageNum} / ${totalPages}`;
document.getElementById('view-paged-btn').classList.toggle('active', currentMode === 'paged');
document.getElementById('view-webtoon-btn').classList.toggle('active', currentMode === 'webtoon');
['zoom-fit-page-button', 'zoom-fit-width-button'].forEach(id => document.getElementById(id).classList.remove('active'));
if(currentScale === 'auto') document.getElementById('zoom-fit-page-button').classList.add('active');
if(currentScale === 'width') document.getElementById('zoom-fit-width-button').classList.add('active');
}
function showLoadingState(message) {
messageContainer.style.display = 'flex';
loader.style.display = 'block';
fileInputButton.style.display = 'none';
messageText.textContent = message;
}
function showErrorState(message) {
loader.style.display = 'none';
fileInputButton.style.display = 'block';
messageText.innerHTML = message;
}
function showControls() {
clearTimeout(autoHideTimeout);
appContainer.classList.remove('controls-hidden');
autoHideTimeout = setTimeout(() => {
if(!document.querySelector('#page-input')) appContainer.classList.add('controls-hidden');
}, 3000);
}
// --- Event Handlers ---
function goToPrevPage() { if (currentPageNum > 1) { currentPageNum--; updateView(); } }
function goToNextPage() { if (currentPageNum < totalPages) { currentPageNum++; updateView(); } }
function goToPage(num) {
const pageNumber = parseInt(num);
if (pageNumber > 0 && pageNumber <= totalPages) {
currentPageNum = pageNumber;
if (currentMode === 'paged') {
updateView();
} else {
document.getElementById(`page-${currentPageNum}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}
function setZoom(type) {
if (type === 'in') currentScale = (typeof currentScale !== 'number' ? 1.0 : currentScale) + 0.2;
else if (type === 'out') currentScale = Math.max(0.2, (typeof currentScale !== 'number' ? 1.0 : currentScale) - 0.2);
else currentScale = type;
updateView();
}
function rotate() { currentRotation = (currentRotation + 90) % 360; updateView(); }
function setViewMode(mode, isInitial = false) {
if (currentMode === mode && !isInitial) return;
currentMode = mode;
pdfContainer.className = `${mode}-view`;
const pageControls = document.getElementById('page-controls');
const progressBar = document.getElementById('progress-bar-container');
if (mode === 'webtoon') {
pageControls.style.display = 'none';
progressBar.style.display = 'block';
} else {
pageControls.style.display = 'flex';
progressBar.style.display = 'none';
}
if (!isInitial) updateView(); // Only re-render if it's a manual switch
}
// --- Lazy Loading for Webtoon Mode ---
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const canvas = entry.target;
const pageNum = parseInt(canvas.id.split('-')[1]);
if (entry.intersectionRatio > 0.5) {
if (currentPageNum !== pageNum) {
currentPageNum = pageNum;
updateControls();
}
}
if (canvas.dataset.rendered !== "true") renderPage(pageNum);
}
});
}, { root: viewerContainer, threshold: [0.1, 0.5, 0.9] });
function lazyLoadVisiblePages() {
observer.disconnect();
pdfContainer.querySelectorAll('canvas').forEach(canvas => observer.observe(canvas));
}
// --- Setup and Initialization ---
function setupEventListeners() {
if (setupEventListeners.bound) return;
setupEventListeners.bound = true;
// Header/Footer Controls
document.getElementById('view-webtoon-btn').addEventListener('click', () => setViewMode('webtoon'));
document.getElementById('view-paged-btn').addEventListener('click', () => setViewMode('paged'));
document.getElementById('rotate-button').addEventListener('click', rotate);
document.getElementById('zoom-in-button').addEventListener('click', () => setZoom('in'));
document.getElementById('zoom-out-button').addEventListener('click', () => setZoom('out'));
document.getElementById('zoom-fit-page-button').addEventListener('click', () => setZoom('auto'));
document.getElementById('zoom-fit-width-button').addEventListener('click', () => setZoom('width'));
pageInfo.addEventListener('click', () => {
pageInfo.innerHTML = `<input id="page-input" type="number" min="1" max="${totalPages}" value="${currentPageNum}" />`;
const pageInput = document.getElementById('page-input');
pageInput.focus(); pageInput.select();
const revert = () => { if (document.getElementById('page-input')) updateControls(); };
pageInput.addEventListener('blur', revert);
pageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { goToPage(pageInput.value); pageInput.blur(); }
if (e.key === 'Escape') pageInput.blur();
});
});
window.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
if (e.key === 'ArrowLeft') goToPrevPage();
else if (e.key === 'ArrowRight') goToNextPage();
});
['mousemove', 'mousedown', 'touchstart'].forEach(evt => window.addEventListener(evt, showControls));
viewerContainer.addEventListener('touchstart', (e) => { if(currentMode === 'paged') { touchStartX = e.changedTouches[0].screenX; touchMoveX = touchStartX; } }, { passive: true });
viewerContainer.addEventListener('touchmove', (e) => { if(currentMode === 'paged') { touchMoveX = e.changedTouches[0].screenX; } }, { passive: true });
viewerContainer.addEventListener('touchend', () => { if(currentMode === 'paged') { if (touchStartX - touchMoveX > 50) goToNextPage(); else if (touchMoveX - touchStartX > 50) goToPrevPage(); } });
viewerContainer.addEventListener('scroll', () => {
if (currentMode === 'webtoon') {
const { scrollTop, scrollHeight, clientHeight } = viewerContainer;
const scrollPercent = (scrollTop / (scrollHeight - clientHeight)) * 100;
document.getElementById('progress-bar').style.width = `${scrollPercent}%`;
}
});
}
window.addEventListener('load', () => {
const params = new URLSearchParams(window.location.search);
const fileUrlParam = params.get('file');
const seriesTitleParam = params.get('seriesTitle');
const chapterTitleParam = params.get('chapterTitle');
const isEmbedded = params.get('embedded');
const seriesIdParam = params.get('seriesId');
const typeParam = params.get('type');
const nextItemNumParam = params.get('nextItemNum');
if (seriesIdParam && typeParam && nextItemNumParam) {
const nextChapterButton = document.getElementById('next-chapter-button');
nextChapterButton.style.display = 'flex';
nextChapterButton.addEventListener('click', () => {
window.parent.postMessage({
type: 'pdf-reader-next',
seriesId: seriesIdParam,
type: typeParam,
itemNum: nextItemNumParam
}, '*');
});
}
if (isEmbedded) {
const backButton = document.getElementById('back-button');
backButton.style.display = 'flex';
backButton.addEventListener('click', () => {
window.parent.postMessage({ type: 'pdf-reader-back' }, '*');
});
// Listen for the PDF data from the parent
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'load-pdf') {
const { fileData, seriesTitle, chapterTitle } = event.data;
// Create a URL that is valid in *this* document's context
const localPdfUrl = URL.createObjectURL(fileData);
loadAndRenderPdf(localPdfUrl, seriesTitle, chapterTitle);
}
});
// Tell the parent that this iframe is ready to receive the file
window.parent.postMessage({ type: 'pdf-reader-ready' }, '*');
} else if (fileUrlParam) {
loadAndRenderPdf(fileUrlParam, decodeURIComponent(seriesTitleParam || ''), decodeURIComponent(chapterTitleParam || ''));
} else {
showErrorState(`Provide a PDF via the <code>?file=...</code> URL parameter, or choose one locally.`);
}
fileInputButton.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file?.type === 'application/pdf') {
loadAndRenderPdf(URL.createObjectURL(file), file.name, '');
}
});
});
</script>
</body>
</html>