added reader
This commit is contained in:
@@ -164,14 +164,17 @@ function renderTable() {
|
||||
|
||||
pageItems.forEach(ch => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${ch.number}</td>
|
||||
<td>${ch.title || `Chapter ${ch.number}`}</td>
|
||||
<td><span class="pill" style="font-size:0.75rem;">${ch.provider}</span></td>
|
||||
<td>
|
||||
<button class="read-btn-small" onclick="openReader('${ch.id}')">Read</button>
|
||||
</td>
|
||||
`;
|
||||
<td>${ch.number}</td>
|
||||
<td>${ch.title || 'Chapter ' + ch.number}</td>
|
||||
<td><span class="pill" style="font-size:0.75rem;">${ch.provider}</span></td>
|
||||
<td>
|
||||
<button class="read-btn-small" onclick="openReader('${bookId}', '${ch.number - 1}', '${ch.provider}')">
|
||||
Read
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
@@ -202,9 +205,10 @@ function updatePagination() {
|
||||
nextBtn.onclick = () => { currentPage++; renderTable(); };
|
||||
}
|
||||
|
||||
function openReader(chapterId) {
|
||||
alert("Opening Reader for Chapter ID: " + chapterId);
|
||||
// window.location.href = `/read/${bookId}/${chapterId}`;
|
||||
function openReader(bookId, chapterId, provider) {
|
||||
const c = encodeURIComponent(chapterId);
|
||||
const p = encodeURIComponent(provider);
|
||||
window.location.href = `/read/${bookId}/${c}/${p}`;
|
||||
}
|
||||
|
||||
init();
|
||||
555
public/reader.css
Normal file
555
public/reader.css
Normal file
@@ -0,0 +1,555 @@
|
||||
:root {
|
||||
--bg-base: #0a0a0f;
|
||||
--bg-surface: #14141b;
|
||||
--bg-elevated: #1c1c26;
|
||||
--bg-hover: #252530;
|
||||
--accent: #8b5cf6;
|
||||
--accent-hover: #7c3aed;
|
||||
--accent-light: rgba(139, 92, 246, 0.15);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--border-focus: rgba(139, 92, 246, 0.4);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.5);
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 24px;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-base);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
overflow-x: hidden;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* ===== TOP BAR ===== */
|
||||
.top-bar {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 64px;
|
||||
background: rgba(10, 10, 15, 0.85);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1.5rem;
|
||||
z-index: 1000;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.glass-btn {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.glass-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.glass-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-arrow:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.nav-arrow:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== READER CONTAINER ===== */
|
||||
#reader {
|
||||
margin-top: 64px;
|
||||
padding: 2rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 64px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== MANGA STYLES ===== */
|
||||
.manga-container {
|
||||
width: 100%;
|
||||
max-width: var(--manga-max-width, 1200px);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--page-spacing, 16px);
|
||||
}
|
||||
|
||||
.page-img {
|
||||
width: 100%;
|
||||
max-width: var(--page-max-width, 900px);
|
||||
height: auto;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: transform 0.3s ease;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.page-img:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.page-img.zoomed {
|
||||
cursor: zoom-out;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.double-container {
|
||||
display: flex;
|
||||
gap: var(--page-spacing, 16px);
|
||||
width: 100%;
|
||||
max-width: var(--manga-max-width, 1400px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.double-container img {
|
||||
width: 48%;
|
||||
max-width: 700px;
|
||||
height: auto;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
object-fit: contain;
|
||||
cursor: zoom-in;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.double-container img:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* ===== LIGHT NOVEL STYLES ===== */
|
||||
.ln-content {
|
||||
max-width: var(--ln-max-width, 750px);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2.5rem;
|
||||
line-height: var(--ln-line-height, 1.8);
|
||||
font-size: var(--ln-font-size, 18px);
|
||||
font-family: var(--ln-font-family, 'Georgia', serif);
|
||||
color: var(--ln-text-color, #e5e7eb);
|
||||
background: var(--ln-bg, #14141b);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
text-align: var(--ln-text-align, left);
|
||||
}
|
||||
|
||||
.ln-content p {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.ln-content h1, .ln-content h2, .ln-content h3 {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===== SETTINGS PANEL ===== */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 64px;
|
||||
bottom: 0;
|
||||
width: 400px;
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border);
|
||||
padding: 0;
|
||||
z-index: 1001;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-panel.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg-elevated);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-group h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--accent);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.control {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.control label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.control label span {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Range Inputs */
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius-full);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.6);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* Select & Color Inputs */
|
||||
select, input[type="color"], input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select:hover, input[type="color"]:hover, input[type="number"]:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
select:focus, input[type="color"]:focus, input[type="number"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-light);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
height: 44px;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Presets */
|
||||
.presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.presets button {
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.presets button:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Toggle Switches */
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--bg-elevated);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
gap: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.settings-panel::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.settings-panel::-webkit-scrollbar-track {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.settings-panel::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.settings-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.settings-panel {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.glass-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
font-size: 0.875rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.double-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.double-container img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ln-content {
|
||||
padding: 2rem 1.5rem;
|
||||
font-size: var(--ln-font-size, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Image Fit Modes */
|
||||
.fit-width {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.fit-height {
|
||||
height: var(--viewport-height, 85vh) !important;
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.fit-screen {
|
||||
max-height: var(--viewport-height, 85vh) !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
object-fit: contain !important;
|
||||
}
|
||||
515
public/reader.js
Normal file
515
public/reader.js
Normal file
@@ -0,0 +1,515 @@
|
||||
const reader = document.getElementById('reader');
|
||||
const panel = document.getElementById('settings-panel');
|
||||
const overlay = document.getElementById('overlay');
|
||||
const settingsBtn = document.getElementById('settings-btn');
|
||||
const closePanel = document.getElementById('close-panel');
|
||||
const chapterLabel = document.getElementById('chapter-label');
|
||||
const prevBtn = document.getElementById('prev-chapter');
|
||||
const nextBtn = document.getElementById('next-chapter');
|
||||
|
||||
const lnSettings = document.getElementById('ln-settings');
|
||||
const mangaSettings = document.getElementById('manga-settings');
|
||||
|
||||
const config = {
|
||||
ln: {
|
||||
fontSize: 18,
|
||||
lineHeight: 1.8,
|
||||
maxWidth: 750,
|
||||
fontFamily: '"Georgia", serif',
|
||||
textColor: '#e5e7eb',
|
||||
bg: '#14141b',
|
||||
textAlign: 'justify'
|
||||
},
|
||||
manga: {
|
||||
direction: 'rtl',
|
||||
mode: 'auto',
|
||||
spacing: 16,
|
||||
imageFit: 'screen',
|
||||
maxWidth: 900,
|
||||
quality: 'high',
|
||||
preloadCount: 3
|
||||
}
|
||||
};
|
||||
|
||||
let currentType = null;
|
||||
let currentPages = [];
|
||||
let observer = null;
|
||||
|
||||
const parts = window.location.pathname.split('/');
|
||||
|
||||
const bookId = parts[2];
|
||||
let chapter = parts[3];
|
||||
let provider = parts[4];
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const saved = localStorage.getItem('readerConfig');
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
Object.assign(config.ln, parsed.ln || {});
|
||||
Object.assign(config.manga, parsed.manga || {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading config:', e);
|
||||
}
|
||||
updateUIFromConfig();
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
try {
|
||||
localStorage.setItem('readerConfig', JSON.stringify(config));
|
||||
} catch (e) {
|
||||
console.error('Error saving config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUIFromConfig() {
|
||||
// Light Novel
|
||||
document.getElementById('font-size').value = config.ln.fontSize;
|
||||
document.getElementById('font-size-value').textContent = config.ln.fontSize + 'px';
|
||||
|
||||
document.getElementById('line-height').value = config.ln.lineHeight;
|
||||
document.getElementById('line-height-value').textContent = config.ln.lineHeight;
|
||||
|
||||
document.getElementById('max-width').value = config.ln.maxWidth;
|
||||
document.getElementById('max-width-value').textContent = config.ln.maxWidth + 'px';
|
||||
|
||||
document.getElementById('font-family').value = config.ln.fontFamily;
|
||||
document.getElementById('text-color').value = config.ln.textColor;
|
||||
document.getElementById('bg-color').value = config.ln.bg;
|
||||
|
||||
// Text alignment buttons
|
||||
document.querySelectorAll('[data-align]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.align === config.ln.textAlign);
|
||||
});
|
||||
|
||||
// Manga
|
||||
document.getElementById('display-mode').value = config.manga.mode;
|
||||
document.getElementById('image-fit').value = config.manga.imageFit;
|
||||
document.getElementById('manga-max-width').value = config.manga.maxWidth;
|
||||
document.getElementById('manga-max-width-value').textContent = config.manga.maxWidth + 'px';
|
||||
document.getElementById('page-spacing').value = config.manga.spacing;
|
||||
document.getElementById('page-spacing-value').textContent = config.manga.spacing + 'px';
|
||||
document.getElementById('preload-count').value = config.manga.preloadCount;
|
||||
|
||||
// Direction buttons
|
||||
document.querySelectorAll('[data-direction]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.direction === config.manga.direction);
|
||||
});
|
||||
|
||||
// Quality buttons
|
||||
document.querySelectorAll('[data-quality]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.quality === config.manga.quality);
|
||||
});
|
||||
}
|
||||
|
||||
function applyStyles() {
|
||||
if (currentType === 'ln') {
|
||||
document.documentElement.style.setProperty('--ln-font-size', config.ln.fontSize + 'px');
|
||||
document.documentElement.style.setProperty('--ln-line-height', config.ln.lineHeight);
|
||||
document.documentElement.style.setProperty('--ln-max-width', config.ln.maxWidth + 'px');
|
||||
document.documentElement.style.setProperty('--ln-font-family', config.ln.fontFamily);
|
||||
document.documentElement.style.setProperty('--ln-text-color', config.ln.textColor);
|
||||
document.documentElement.style.setProperty('--ln-bg', config.ln.bg);
|
||||
document.documentElement.style.setProperty('--ln-text-align', config.ln.textAlign);
|
||||
}
|
||||
|
||||
if (currentType === 'manga') {
|
||||
document.documentElement.style.setProperty('--page-spacing', config.manga.spacing + 'px');
|
||||
document.documentElement.style.setProperty('--page-max-width', config.manga.maxWidth + 'px');
|
||||
document.documentElement.style.setProperty('--manga-max-width', config.manga.maxWidth + 'px');
|
||||
|
||||
const viewportHeight = window.innerHeight - 64 - 32; // header + padding
|
||||
document.documentElement.style.setProperty('--viewport-height', viewportHeight + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSettingsVisibility() {
|
||||
lnSettings.classList.toggle('hidden', currentType !== 'ln');
|
||||
mangaSettings.classList.toggle('hidden', currentType !== 'manga');
|
||||
}
|
||||
|
||||
async function loadChapter() {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>Loading chapter...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/book/${bookId}/${chapter}/${provider}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.title) {
|
||||
chapterLabel.textContent = data.title;
|
||||
document.title = data.title;
|
||||
} else {
|
||||
chapterLabel.textContent = `Chapter ${chapter}`;
|
||||
document.title = `Chapter ${chapter}`;
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<span style="color: #ef4444;">Error: ${data.error}</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentType = data.type;
|
||||
updateSettingsVisibility();
|
||||
applyStyles();
|
||||
reader.innerHTML = '';
|
||||
|
||||
if (data.type === 'manga') {
|
||||
currentPages = data.pages || [];
|
||||
loadManga(currentPages);
|
||||
} else if (data.type === 'ln') {
|
||||
loadLN(data.content);
|
||||
}
|
||||
} catch (error) {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<span style="color: #ef4444;">❌ Error loading chapter: ${error.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadManga(pages) {
|
||||
if (!pages || pages.length === 0) {
|
||||
reader.innerHTML = '<div class="loading-container"><span>No pages found</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'manga-container';
|
||||
|
||||
const isLongStrip = config.manga.mode === 'longstrip' ||
|
||||
(config.manga.mode === 'auto' && detectLongStrip(pages));
|
||||
|
||||
const useDouble = config.manga.mode === 'double' ||
|
||||
(config.manga.mode === 'auto' && !isLongStrip && pages.length > 5);
|
||||
|
||||
if (useDouble) {
|
||||
loadDoublePage(container, pages);
|
||||
} else {
|
||||
loadSinglePage(container, pages);
|
||||
}
|
||||
|
||||
reader.appendChild(container);
|
||||
setupLazyLoading();
|
||||
}
|
||||
|
||||
function loadSinglePage(container, pages) {
|
||||
pages.forEach((page, index) => {
|
||||
const img = createImageElement(page.url, index);
|
||||
container.appendChild(img);
|
||||
});
|
||||
}
|
||||
|
||||
function loadDoublePage(container, pages) {
|
||||
for (let i = 0; i < pages.length; i += 2) {
|
||||
const doubleContainer = document.createElement('div');
|
||||
doubleContainer.className = 'double-container';
|
||||
|
||||
const leftPage = createImageElement(pages[i].url, i);
|
||||
|
||||
if (pages[i + 1]) {
|
||||
const rightPage = createImageElement(pages[i + 1].url, i + 1);
|
||||
|
||||
if (config.manga.direction === 'rtl') {
|
||||
doubleContainer.appendChild(rightPage);
|
||||
doubleContainer.appendChild(leftPage);
|
||||
} else {
|
||||
doubleContainer.appendChild(leftPage);
|
||||
doubleContainer.appendChild(rightPage);
|
||||
}
|
||||
} else {
|
||||
doubleContainer.appendChild(leftPage);
|
||||
}
|
||||
|
||||
container.appendChild(doubleContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function createImageElement(url, index) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'page-img';
|
||||
img.dataset.index = index;
|
||||
|
||||
if (config.manga.imageFit === 'width') {
|
||||
img.classList.add('fit-width');
|
||||
} else if (config.manga.imageFit === 'height') {
|
||||
img.classList.add('fit-height');
|
||||
} else if (config.manga.imageFit === 'screen') {
|
||||
img.classList.add('fit-screen');
|
||||
}
|
||||
|
||||
// Preload o lazy load
|
||||
if (index < config.manga.preloadCount) {
|
||||
img.src = buildProxyUrl(url);
|
||||
} else {
|
||||
img.dataset.src = buildProxyUrl(url);
|
||||
img.loading = 'lazy';
|
||||
}
|
||||
|
||||
img.alt = `Page ${index + 1}`;
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
function buildProxyUrl(url) {
|
||||
return `/api/proxy?url=${encodeURIComponent(url)}&referer=https%3A%2F%2Fmangapark.net`;
|
||||
}
|
||||
|
||||
function detectLongStrip(pages) {
|
||||
if (!pages || pages.length === 0) return false;
|
||||
const tallPages = pages.filter(p => {
|
||||
if (!p.height || !p.width) return false;
|
||||
return (p.height / p.width) > 2.5;
|
||||
});
|
||||
return tallPages.length >= Math.min(4, pages.length * 0.5);
|
||||
}
|
||||
|
||||
function setupLazyLoading() {
|
||||
if (observer) observer.disconnect();
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
delete img.dataset.src;
|
||||
observer.unobserve(img);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '200px'
|
||||
});
|
||||
|
||||
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
|
||||
}
|
||||
|
||||
function loadLN(html) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'ln-content';
|
||||
div.innerHTML = html;
|
||||
reader.appendChild(div);
|
||||
}
|
||||
|
||||
document.getElementById('font-size').addEventListener('input', (e) => {
|
||||
config.ln.fontSize = parseInt(e.target.value);
|
||||
document.getElementById('font-size-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('line-height').addEventListener('input', (e) => {
|
||||
config.ln.lineHeight = parseFloat(e.target.value);
|
||||
document.getElementById('line-height-value').textContent = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('max-width').addEventListener('input', (e) => {
|
||||
config.ln.maxWidth = parseInt(e.target.value);
|
||||
document.getElementById('max-width-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('font-family').addEventListener('change', (e) => {
|
||||
config.ln.fontFamily = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('text-color').addEventListener('change', (e) => {
|
||||
config.ln.textColor = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('bg-color').addEventListener('change', (e) => {
|
||||
config.ln.bg = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
// Text alignment
|
||||
document.querySelectorAll('[data-align]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-align]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
config.ln.textAlign = btn.dataset.align;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
});
|
||||
|
||||
// Presets
|
||||
document.querySelectorAll('[data-preset]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const preset = btn.dataset.preset;
|
||||
|
||||
const presets = {
|
||||
dark: { bg: '#14141b', textColor: '#e5e7eb' },
|
||||
sepia: { bg: '#f4ecd8', textColor: '#5c472d' },
|
||||
light: { bg: '#fafafa', textColor: '#1f2937' },
|
||||
amoled: { bg: '#000000', textColor: '#ffffff' }
|
||||
};
|
||||
|
||||
if (presets[preset]) {
|
||||
Object.assign(config.ln, presets[preset]);
|
||||
document.getElementById('bg-color').value = config.ln.bg;
|
||||
document.getElementById('text-color').value = config.ln.textColor;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('display-mode').addEventListener('change', (e) => {
|
||||
config.manga.mode = e.target.value;
|
||||
saveConfig();
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
document.getElementById('image-fit').addEventListener('change', (e) => {
|
||||
config.manga.imageFit = e.target.value;
|
||||
saveConfig();
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
document.getElementById('manga-max-width').addEventListener('input', (e) => {
|
||||
config.manga.maxWidth = parseInt(e.target.value);
|
||||
document.getElementById('manga-max-width-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('page-spacing').addEventListener('input', (e) => {
|
||||
config.manga.spacing = parseInt(e.target.value);
|
||||
document.getElementById('page-spacing-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('preload-count').addEventListener('change', (e) => {
|
||||
config.manga.preloadCount = parseInt(e.target.value);
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
// Direction
|
||||
document.querySelectorAll('[data-direction]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-direction]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
config.manga.direction = btn.dataset.direction;
|
||||
saveConfig();
|
||||
loadChapter();
|
||||
});
|
||||
});
|
||||
|
||||
// Quality
|
||||
document.querySelectorAll('[data-quality]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-quality]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
config.manga.quality = btn.dataset.quality;
|
||||
saveConfig();
|
||||
});
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', () => {
|
||||
const newChapter = String(parseInt(chapter) - 1);
|
||||
updateURL(newChapter);
|
||||
window.scrollTo(0, 0);
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', () => {
|
||||
const newChapter = String(parseInt(chapter) + 1);
|
||||
updateURL(newChapter);
|
||||
window.scrollTo(0, 0);
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
function updateURL(newChapter) {
|
||||
chapter = newChapter;
|
||||
const newUrl = `/reader/${bookId}/${chapter}/${provider}`;
|
||||
window.history.pushState({}, '', newUrl);
|
||||
}
|
||||
|
||||
document.getElementById('back-btn').addEventListener('click', () => {
|
||||
history.back();
|
||||
});
|
||||
|
||||
settingsBtn.addEventListener('click', () => {
|
||||
panel.classList.add('open');
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
|
||||
closePanel.addEventListener('click', closeSettings);
|
||||
overlay.addEventListener('click', closeSettings);
|
||||
|
||||
function closeSettings() {
|
||||
panel.classList.remove('open');
|
||||
overlay.classList.remove('active');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && panel.classList.contains('open')) {
|
||||
closeSettings();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowLeft':
|
||||
if (config.manga.direction === 'rtl') {
|
||||
nextBtn.click();
|
||||
} else {
|
||||
prevBtn.click();
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (config.manga.direction === 'rtl') {
|
||||
prevBtn.click();
|
||||
} else {
|
||||
nextBtn.click();
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
settingsBtn.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
applyStyles();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
if (!bookId || !chapter || !provider) {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<span style="color: #ef4444;"> Missing required parameters (bookId, chapter, provider)</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
loadConfig();
|
||||
loadChapter();
|
||||
}
|
||||
80
server.js
80
server.js
@@ -31,8 +31,8 @@ async function loadExtensions() {
|
||||
try {
|
||||
delete require.cache[require.resolve(filePath)];
|
||||
const ExtensionClass = require(filePath);
|
||||
const instance = typeof ExtensionClass === 'function'
|
||||
? new ExtensionClass()
|
||||
const instance = typeof ExtensionClass === 'function'
|
||||
? new ExtensionClass()
|
||||
: (ExtensionClass.default ? new ExtensionClass.default() : null);
|
||||
|
||||
if (instance && (instance.type === "anime-board" || instance.type === "book-board")) {
|
||||
@@ -351,7 +351,7 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => {
|
||||
.map(async ([name, ext]) => {
|
||||
try {
|
||||
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
||||
|
||||
|
||||
// Pass strict search options
|
||||
const matches = await ext.search({
|
||||
query: searchTitle,
|
||||
@@ -366,7 +366,7 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => {
|
||||
// Use the first match to find chapters
|
||||
const best = matches[0];
|
||||
const chaps = await ext.findChapters(best.id);
|
||||
|
||||
|
||||
if (chaps && chaps.length > 0) {
|
||||
console.log(`[${name}] Found ${chaps.length} chapters.`);
|
||||
chaps.forEach(ch => {
|
||||
@@ -398,6 +398,73 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => {
|
||||
return { chapters: sortedChapters };
|
||||
});
|
||||
|
||||
fastify.get('/api/book/:bookId/:chapter/:provider', async (req, reply) => {
|
||||
const { bookId, chapter, provider } = req.params;
|
||||
|
||||
const ext = extensions.get(provider);
|
||||
if (!ext)
|
||||
return reply.code(404).send({ error: "Provider not found" });
|
||||
|
||||
let chapterId = decodeURIComponent(chapter);
|
||||
let chapterTitle = null;
|
||||
let chapterNumber = null;
|
||||
|
||||
const index = parseInt(chapter);
|
||||
const chapterList = await fetch(
|
||||
`http://localhost:3000/api/book/${bookId}/chapters`
|
||||
).then(r => r.json());
|
||||
|
||||
if (!chapterList?.chapters)
|
||||
return reply.code(404).send({ error: "Chapters not found" });
|
||||
|
||||
const providerChapters = chapterList.chapters.filter(
|
||||
c => c.provider === provider
|
||||
);
|
||||
|
||||
if (!providerChapters[index])
|
||||
return reply.code(404).send({ error: "Chapter index out of range" });
|
||||
|
||||
const selected = providerChapters[index];
|
||||
|
||||
chapterId = selected.id;
|
||||
chapterTitle = selected.title || null;
|
||||
chapterNumber = selected.number || index;
|
||||
|
||||
|
||||
try {
|
||||
if (ext.mediaType === "manga") {
|
||||
const pages = await ext.findChapterPages(chapterId);
|
||||
return reply.send({
|
||||
type: "manga",
|
||||
chapterId,
|
||||
title: chapterTitle,
|
||||
number: chapterNumber,
|
||||
provider,
|
||||
pages
|
||||
});
|
||||
}
|
||||
|
||||
if (ext.mediaType === "ln") {
|
||||
const content = await ext.findChapterPages(chapterId);
|
||||
return reply.send({
|
||||
type: "ln",
|
||||
chapterId,
|
||||
title: chapterTitle,
|
||||
number: chapterNumber,
|
||||
provider,
|
||||
content
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(400).send({ error: "Unknown mediaType" });
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return reply.code(500).send({ error: "Error loading chapter" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
fastify.get('/api/book/:id', async (req, reply) => {
|
||||
const id = req.params.id;
|
||||
|
||||
@@ -493,6 +560,11 @@ fastify.get('/api/top-airing', (req, reply) => {
|
||||
return new Promise((resolve) => db.all("SELECT full_data FROM top_airing ORDER BY rank ASC LIMIT 10", [], (err, rows) => resolve({ results: rows ? rows.map(r => JSON.parse(r.full_data)) : [] })));
|
||||
});
|
||||
|
||||
fastify.get('/read/:id/:chapter/:provider', (req, reply) => {
|
||||
const stream = fs.createReadStream(path.join(__dirname, 'views', 'reader.html'));
|
||||
reply.type('text/html').send(stream);
|
||||
});
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
||||
|
||||
190
views/reader.html
Normal file
190
views/reader.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reader</title>
|
||||
<link rel="stylesheet" href="/public/reader.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Top Bar -->
|
||||
<header class="top-bar">
|
||||
<button id="back-btn" class="glass-btn">
|
||||
X
|
||||
</button>
|
||||
|
||||
<div class="chapter-info">
|
||||
<button id="prev-chapter" class="nav-arrow">‹</button>
|
||||
<span id="chapter-label">Loading...</span>
|
||||
<button id="next-chapter" class="nav-arrow">›</button>
|
||||
</div>
|
||||
|
||||
<button id="settings-btn" class="glass-btn">
|
||||
⚙ Settings
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<aside id="settings-panel" class="settings-panel">
|
||||
<div class="panel-header">
|
||||
<h3>Reader Settings</h3>
|
||||
<button id="close-panel" class="close-btn">×</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<!-- Light Novel Settings -->
|
||||
<div id="ln-settings" class="settings-group hidden">
|
||||
<h4>Text Settings</h4>
|
||||
|
||||
<div class="control">
|
||||
<label>
|
||||
Font Size
|
||||
<span id="font-size-value">18px</span>
|
||||
</label>
|
||||
<input type="range" id="font-size" min="12" max="40" value="18" step="1">
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>
|
||||
Line Height
|
||||
<span id="line-height-value">1.8</span>
|
||||
</label>
|
||||
<input type="range" id="line-height" min="1.2" max="3" step="0.1" value="1.8">
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>
|
||||
Content Width
|
||||
<span id="max-width-value">750px</span>
|
||||
</label>
|
||||
<input type="range" id="max-width" min="500" max="1200" value="750" step="50">
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>Font Family</label>
|
||||
<select id="font-family">
|
||||
<option value='"Georgia", serif'>Georgia (Serif)</option>
|
||||
<option value='"Charter", serif'>Charter (Serif)</option>
|
||||
<option value='"Merriweather", serif'>Merriweather</option>
|
||||
<option value='"Inter", system-ui, sans-serif'>Inter (Sans)</option>
|
||||
<option value='system-ui, sans-serif'>System UI</option>
|
||||
<option value='"JetBrains Mono", monospace'>JetBrains Mono</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>Text Alignment</label>
|
||||
<div class="toggle-group">
|
||||
<button class="toggle-btn" data-align="left">Left</button>
|
||||
<button class="toggle-btn active" data-align="justify">Justify</button>
|
||||
<button class="toggle-btn" data-align="center">Center</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<h4>🎨 Color Theme</h4>
|
||||
|
||||
<div class="control">
|
||||
<label>Text Color</label>
|
||||
<input type="color" id="text-color" value="#e5e7eb">
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>Background Color</label>
|
||||
<input type="color" id="bg-color" value="#14141b">
|
||||
</div>
|
||||
|
||||
<div class="presets">
|
||||
<button data-preset="dark">Dark</button>
|
||||
<button data-preset="sepia">Sepia</button>
|
||||
<button data-preset="light">Light</button>
|
||||
<button data-preset="amoled">AMOLED</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manga Settings -->
|
||||
<div id="manga-settings" class="settings-group hidden">
|
||||
<h4>Display Mode</h4>
|
||||
|
||||
<div class="control">
|
||||
<label>Layout Mode</label>
|
||||
<select id="display-mode">
|
||||
<option value="auto">Auto Detect</option>
|
||||
<option value="single">Single Page</option>
|
||||
<option value="double">Double Page</option>
|
||||
<option value="longstrip">Long Strip</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>Reading Direction</label>
|
||||
<div class="toggle-group">
|
||||
<button class="toggle-btn" data-direction="ltr">← LTR</button>
|
||||
<button class="toggle-btn active" data-direction="rtl">RTL →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<h4>Image Settings</h4>
|
||||
|
||||
<div class="control">
|
||||
<label>Image Fit</label>
|
||||
<select id="image-fit">
|
||||
<option value="width">Fit Width</option>
|
||||
<option value="height">Fit Height</option>
|
||||
<option value="screen" selected>Fit Screen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>
|
||||
Max Image Width
|
||||
<span id="manga-max-width-value">900px</span>
|
||||
</label>
|
||||
<input type="range" id="manga-max-width" min="600" max="1600" value="900" step="50">
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>
|
||||
Page Spacing
|
||||
<span id="page-spacing-value">16px</span>
|
||||
</label>
|
||||
<input type="range" id="page-spacing" min="0" max="60" value="16" step="4">
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<h4>⚡ Performance</h4>
|
||||
|
||||
<div class="control">
|
||||
<label>Image Quality</label>
|
||||
<div class="toggle-group">
|
||||
<button class="toggle-btn" data-quality="low">Low</button>
|
||||
<button class="toggle-btn active" data-quality="high">High</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<label>Preload Pages</label>
|
||||
<input type="number" id="preload-count" min="0" max="10" value="3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="overlay" class="overlay"></div>
|
||||
|
||||
<main id="reader">
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>Loading chapter...</span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/public/reader.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user