added local manga, todo: novels
This commit is contained in:
@@ -7,7 +7,7 @@ let allChapters = [];
|
||||
let filteredChapters = [];
|
||||
|
||||
let availableExtensions = [];
|
||||
|
||||
let isLocal = false;
|
||||
const chapterPagination = Object.create(PaginationManager);
|
||||
chapterPagination.init(12, () => renderChapterTable());
|
||||
|
||||
@@ -16,6 +16,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setupModalClickOutside();
|
||||
});
|
||||
|
||||
async function checkLocalLibraryEntry() {
|
||||
try {
|
||||
const res = await fetch(`/api/library/manga/${bookId}`);
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
if (data.matched) {
|
||||
isLocal = true;
|
||||
const pill = document.getElementById('local-pill');
|
||||
if (pill) {
|
||||
pill.textContent = 'Local';
|
||||
pill.style.display = 'inline-flex';
|
||||
pill.style.background = 'rgba(34, 197, 94, 0.2)';
|
||||
pill.style.color = '#22c55e';
|
||||
pill.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error checking local status:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function markAsLocal() {
|
||||
isLocal = true;
|
||||
const pill = document.getElementById('local-pill');
|
||||
if (pill) {
|
||||
pill.textContent = 'Local';
|
||||
pill.style.display = 'inline-flex';
|
||||
pill.style.background = 'rgba(34, 197, 94, 0.2)';
|
||||
pill.style.color = '#22c55e';
|
||||
pill.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const urlData = URLUtils.parseEntityPath('book');
|
||||
@@ -27,7 +61,7 @@ async function init() {
|
||||
extensionName = urlData.extensionName;
|
||||
bookId = urlData.entityId;
|
||||
bookSlug = urlData.slug;
|
||||
|
||||
await checkLocalLibraryEntry();
|
||||
await loadBookMetadata();
|
||||
|
||||
await loadAvailableExtensions();
|
||||
@@ -71,7 +105,6 @@ async function loadBookMetadata() {
|
||||
const metadata = MediaMetadataUtils.formatBookData(raw, !!extensionName);
|
||||
bookData.entry_type =
|
||||
metadata.format === 'MANGA' ? 'MANGA' : 'NOVEL';
|
||||
|
||||
updatePageTitle(metadata.title);
|
||||
updateMetadata(metadata);
|
||||
updateExtensionPill();
|
||||
@@ -174,32 +207,46 @@ async function loadChapters(targetProvider = null) {
|
||||
const tbody = document.getElementById('chapters-body');
|
||||
if (!tbody) return;
|
||||
|
||||
// Si no se pasa provider, intentamos pillar el del select o el primero disponible
|
||||
if (!targetProvider) {
|
||||
const select = document.getElementById('provider-filter');
|
||||
targetProvider = select ? select.value : (availableExtensions[0] || 'all');
|
||||
}
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extension for chapters...</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Loading chapters...</td></tr>';
|
||||
|
||||
try {
|
||||
const source = extensionName || 'anilist';
|
||||
// Añadimos el query param 'provider' para que el backend filtre
|
||||
let fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
||||
if (targetProvider !== 'all') {
|
||||
fetchUrl += `&provider=${targetProvider}`;
|
||||
let fetchUrl;
|
||||
let isLocalRequest = targetProvider === 'local';
|
||||
|
||||
if (isLocalRequest) {
|
||||
// Nuevo endpoint para archivos locales
|
||||
fetchUrl = `/api/library/manga/${bookId}/units`;
|
||||
} else {
|
||||
const source = extensionName || 'anilist';
|
||||
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
||||
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
|
||||
}
|
||||
|
||||
const res = await fetch(fetchUrl);
|
||||
const data = await res.json();
|
||||
|
||||
allChapters = data.chapters || [];
|
||||
filteredChapters = [...allChapters];
|
||||
// Mapeo de datos: Si es local usamos 'units', si no, usamos 'chapters'
|
||||
if (isLocalRequest) {
|
||||
allChapters = (data.units || []).map((unit, idx) => ({
|
||||
number: unit.number,
|
||||
title: unit.name,
|
||||
provider: 'local',
|
||||
index: idx, // ✅ índice (0,1,2…)
|
||||
format: unit.format
|
||||
}));
|
||||
} else {
|
||||
allChapters = data.chapters || [];
|
||||
}
|
||||
|
||||
filteredChapters = [...allChapters];
|
||||
applyChapterFilter();
|
||||
|
||||
const totalEl = document.getElementById('total-chapters');
|
||||
|
||||
if (allChapters.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found.</td></tr>';
|
||||
if (totalEl) totalEl.innerText = "0 Found";
|
||||
@@ -209,7 +256,6 @@ async function loadChapters(targetProvider = null) {
|
||||
if (totalEl) totalEl.innerText = `${allChapters.length} Found`;
|
||||
|
||||
setupReadButton();
|
||||
|
||||
chapterPagination.setTotalItems(filteredChapters.length);
|
||||
chapterPagination.reset();
|
||||
renderChapterTable();
|
||||
@@ -236,16 +282,26 @@ function applyChapterFilter() {
|
||||
|
||||
function setupProviderFilter() {
|
||||
const select = document.getElementById('provider-filter');
|
||||
if (!select || availableExtensions.length === 0) return;
|
||||
if (!select) return;
|
||||
|
||||
select.style.display = 'inline-block';
|
||||
select.innerHTML = '';
|
||||
|
||||
// Opción para cargar todo
|
||||
const allOpt = document.createElement('option');
|
||||
allOpt.value = 'all';
|
||||
allOpt.innerText = 'Load All (Slower)';
|
||||
select.appendChild(allOpt);
|
||||
|
||||
// NUEVO: Si es local, añadimos la opción 'local' al principio
|
||||
if (isLocal) {
|
||||
const localOpt = document.createElement('option');
|
||||
localOpt.value = 'local';
|
||||
localOpt.innerText = 'Local';
|
||||
select.appendChild(localOpt);
|
||||
}
|
||||
|
||||
// Añadir extensiones normales
|
||||
availableExtensions.forEach(ext => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ext;
|
||||
@@ -253,7 +309,10 @@ function setupProviderFilter() {
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
if (extensionName && availableExtensions.includes(extensionName)) {
|
||||
// Lógica de selección automática
|
||||
if (isLocal) {
|
||||
select.value = 'local'; // Prioridad si es local
|
||||
} else if (extensionName && availableExtensions.includes(extensionName)) {
|
||||
select.value = extensionName;
|
||||
} else if (availableExtensions.length > 0) {
|
||||
select.value = availableExtensions[0];
|
||||
@@ -315,7 +374,14 @@ function renderChapterTable() {
|
||||
}
|
||||
|
||||
function openReader(chapterId, provider) {
|
||||
window.location.href = URLUtils.buildReadUrl(bookId, chapterId, provider, extensionName);
|
||||
const effectiveExtension = extensionName || 'anilist';
|
||||
|
||||
window.location.href = URLUtils.buildReadUrl(
|
||||
bookId, // SIEMPRE anilist
|
||||
chapterId, // número normal
|
||||
provider, // 'local' o extensión
|
||||
extensionName || 'anilist'
|
||||
);
|
||||
}
|
||||
|
||||
function setupModalClickOutside() {
|
||||
|
||||
@@ -129,11 +129,44 @@ async function loadChapter() {
|
||||
if (!source) {
|
||||
source = 'anilist';
|
||||
}
|
||||
const newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`;
|
||||
let newEndpoint;
|
||||
|
||||
if (provider === 'local') {
|
||||
newEndpoint = `/api/library/manga/${bookId}/units`;
|
||||
} else {
|
||||
newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(newEndpoint);
|
||||
const data = await res.json();
|
||||
if (provider === 'local') {
|
||||
const unit = data.units[Number(chapter)];
|
||||
|
||||
if (!unit) {
|
||||
reader.innerHTML = '<div class="loading-container"><span>Chapter not found</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (unit.format === 'cbz') {
|
||||
chapterLabel.textContent = unit.name; // ✅
|
||||
document.title = unit.name;
|
||||
const pagesRes = await fetch(
|
||||
`/api/library/manga/cbz/${unit.id}/pages`
|
||||
);
|
||||
const pagesData = await pagesRes.json();
|
||||
|
||||
currentType = 'manga';
|
||||
updateSettingsVisibility();
|
||||
applyStyles();
|
||||
|
||||
currentPages = pagesData.pages.map(url => ({ url }));
|
||||
reader.innerHTML = '';
|
||||
loadManga(currentPages);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (data.title) {
|
||||
chapterLabel.textContent = data.title;
|
||||
@@ -160,8 +193,13 @@ async function loadChapter() {
|
||||
reader.innerHTML = '';
|
||||
|
||||
if (data.type === 'manga') {
|
||||
currentPages = data.pages || [];
|
||||
loadManga(currentPages);
|
||||
if (provider === 'local' && data.format === 'cbz') {
|
||||
currentPages = data.pages.map(url => ({ url }));
|
||||
loadManga(currentPages);
|
||||
} else {
|
||||
currentPages = data.pages || [];
|
||||
loadManga(currentPages);
|
||||
}
|
||||
} else if (data.type === 'ln') {
|
||||
loadLN(data.content);
|
||||
}
|
||||
@@ -281,7 +319,9 @@ function createImageElement(page, index) {
|
||||
img.className = 'page-img';
|
||||
img.dataset.index = index;
|
||||
|
||||
const url = buildProxyUrl(page.url, page.headers);
|
||||
const url = provider === 'local'
|
||||
? page.url
|
||||
: buildProxyUrl(page.url, page.headers);
|
||||
const placeholder = "/public/assets/placeholder.svg";
|
||||
|
||||
img.onerror = () => {
|
||||
|
||||
89
docker/src/scripts/local-library-books.js
Normal file
89
docker/src/scripts/local-library-books.js
Normal file
@@ -0,0 +1,89 @@
|
||||
let activeFilter = 'all';
|
||||
let activeSort = 'az';
|
||||
let isLocalMode = false;
|
||||
let localEntries = [];
|
||||
|
||||
function toggleLibraryMode() {
|
||||
isLocalMode = !isLocalMode;
|
||||
const btn = document.getElementById('library-mode-btn');
|
||||
const onlineContent = document.getElementById('online-content');
|
||||
const localContent = document.getElementById('local-content');
|
||||
|
||||
if (isLocalMode) {
|
||||
btn.classList.add('active');
|
||||
onlineContent.classList.add('hidden');
|
||||
localContent.classList.remove('hidden');
|
||||
loadLocalEntries();
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
onlineContent.classList.remove('hidden');
|
||||
localContent.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocalEntries() {
|
||||
const grid = document.getElementById('local-entries-grid');
|
||||
grid.innerHTML = '<div class="skeleton-card"></div>'.repeat(6);
|
||||
|
||||
try {
|
||||
// Cambiado a endpoint de libros
|
||||
const response = await fetch('/api/library/manga');
|
||||
const entries = await response.json();
|
||||
localEntries = entries;
|
||||
|
||||
if (entries.length === 0) {
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-text-secondary); padding: 3rem;">No books found in your local library.</p>';
|
||||
return;
|
||||
}
|
||||
renderLocalEntries(entries);
|
||||
} catch (err) {
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-danger); padding: 3rem;">Error loading local books.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderLocalEntries(entries) {
|
||||
const grid = document.getElementById('local-entries-grid');
|
||||
grid.innerHTML = entries.map(entry => {
|
||||
const title = entry.metadata?.title?.romaji || entry.metadata?.title?.english || entry.id;
|
||||
const cover = entry.metadata?.coverImage?.extraLarge || '/public/assets/placeholder.jpg';
|
||||
const chapters = entry.metadata?.chapters || '??';
|
||||
|
||||
return `
|
||||
<div class="local-card" onclick="viewLocalEntry(${entry.metadata?.id || 'null'})">
|
||||
<div class="card-img-wrap">
|
||||
<img src="${cover}" alt="${title}" loading="lazy">
|
||||
</div>
|
||||
<div class="local-card-info">
|
||||
<div class="local-card-title">${title}</div>
|
||||
<p style="font-size: 0.85rem; color: var(--color-text-secondary); margin: 0;">
|
||||
${chapters} Chapters
|
||||
</p>
|
||||
<div class="match-status ${entry.matched ? 'status-linked' : 'status-unlinked'}">
|
||||
${entry.matched ? '● Linked' : '○ Unlinked'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function scanLocalLibrary() {
|
||||
const btnText = document.getElementById('scan-text');
|
||||
btnText.innerText = "Scanning...";
|
||||
try {
|
||||
// Asumiendo que el scan de libros usa este query param
|
||||
const response = await fetch('/api/library/scan?mode=incremental', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
await loadLocalEntries();
|
||||
if (window.NotificationUtils) NotificationUtils.show('Library scanned!', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
if (window.NotificationUtils) NotificationUtils.show('Scan failed', 'error');
|
||||
} finally {
|
||||
btnText.innerText = "Scan Library";
|
||||
}
|
||||
}
|
||||
|
||||
function viewLocalEntry(id) {
|
||||
if (id) window.location.href = `/book/${id}`;
|
||||
}
|
||||
Reference in New Issue
Block a user