added reader
This commit is contained in:
@@ -164,14 +164,17 @@ function renderTable() {
|
|||||||
|
|
||||||
pageItems.forEach(ch => {
|
pageItems.forEach(ch => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${ch.number}</td>
|
<td>${ch.number}</td>
|
||||||
<td>${ch.title || `Chapter ${ch.number}`}</td>
|
<td>${ch.title || 'Chapter ' + ch.number}</td>
|
||||||
<td><span class="pill" style="font-size:0.75rem;">${ch.provider}</span></td>
|
<td><span class="pill" style="font-size:0.75rem;">${ch.provider}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="read-btn-small" onclick="openReader('${ch.id}')">Read</button>
|
<button class="read-btn-small" onclick="openReader('${bookId}', '${ch.number - 1}', '${ch.provider}')">
|
||||||
</td>
|
Read
|
||||||
`;
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,9 +205,10 @@ function updatePagination() {
|
|||||||
nextBtn.onclick = () => { currentPage++; renderTable(); };
|
nextBtn.onclick = () => { currentPage++; renderTable(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
function openReader(chapterId) {
|
function openReader(bookId, chapterId, provider) {
|
||||||
alert("Opening Reader for Chapter ID: " + chapterId);
|
const c = encodeURIComponent(chapterId);
|
||||||
// window.location.href = `/read/${bookId}/${chapterId}`;
|
const p = encodeURIComponent(provider);
|
||||||
|
window.location.href = `/read/${bookId}/${c}/${p}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
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 {
|
try {
|
||||||
delete require.cache[require.resolve(filePath)];
|
delete require.cache[require.resolve(filePath)];
|
||||||
const ExtensionClass = require(filePath);
|
const ExtensionClass = require(filePath);
|
||||||
const instance = typeof ExtensionClass === 'function'
|
const instance = typeof ExtensionClass === 'function'
|
||||||
? new ExtensionClass()
|
? new ExtensionClass()
|
||||||
: (ExtensionClass.default ? new ExtensionClass.default() : null);
|
: (ExtensionClass.default ? new ExtensionClass.default() : null);
|
||||||
|
|
||||||
if (instance && (instance.type === "anime-board" || instance.type === "book-board")) {
|
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]) => {
|
.map(async ([name, ext]) => {
|
||||||
try {
|
try {
|
||||||
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
||||||
|
|
||||||
// Pass strict search options
|
// Pass strict search options
|
||||||
const matches = await ext.search({
|
const matches = await ext.search({
|
||||||
query: searchTitle,
|
query: searchTitle,
|
||||||
@@ -366,7 +366,7 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => {
|
|||||||
// Use the first match to find chapters
|
// Use the first match to find chapters
|
||||||
const best = matches[0];
|
const best = matches[0];
|
||||||
const chaps = await ext.findChapters(best.id);
|
const chaps = await ext.findChapters(best.id);
|
||||||
|
|
||||||
if (chaps && chaps.length > 0) {
|
if (chaps && chaps.length > 0) {
|
||||||
console.log(`[${name}] Found ${chaps.length} chapters.`);
|
console.log(`[${name}] Found ${chaps.length} chapters.`);
|
||||||
chaps.forEach(ch => {
|
chaps.forEach(ch => {
|
||||||
@@ -398,6 +398,73 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => {
|
|||||||
return { chapters: sortedChapters };
|
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) => {
|
fastify.get('/api/book/:id', async (req, reply) => {
|
||||||
const id = req.params.id;
|
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)) : [] })));
|
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 () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
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