From 1e144c4bad86c4ad085fd6e5b33b85dfaceea8f3 Mon Sep 17 00:00:00 2001 From: lenafx Date: Wed, 31 Dec 2025 17:57:33 +0100 Subject: [PATCH] multi language support for books and book page redesigned --- desktop/src/api/books/books.controller.ts | 4 +- desktop/src/api/books/books.service.ts | 70 +-- desktop/src/api/types.ts | 1 + desktop/src/scripts/books/book.js | 720 ++++++++++++---------- desktop/src/scripts/books/reader.js | 367 +++++------ desktop/views/books/book.html | 161 ++--- desktop/views/css/books/book.css | 312 +++++----- docker/src/api/books/books.controller.ts | 4 +- docker/src/api/books/books.service.ts | 70 +-- docker/src/api/types.ts | 1 + docker/src/scripts/books/book.js | 720 ++++++++++++---------- docker/src/scripts/books/reader.js | 356 +++++------ docker/views/books/book.html | 159 ++--- docker/views/css/books/book.css | 659 +++++--------------- 14 files changed, 1687 insertions(+), 1917 deletions(-) diff --git a/desktop/src/api/books/books.controller.ts b/desktop/src/api/books/books.controller.ts index d8f7a8a..04be2f4 100644 --- a/desktop/src/api/books/books.controller.ts +++ b/desktop/src/api/books/books.controller.ts @@ -101,12 +101,14 @@ export async function getChapterContent(req: any, reply: FastifyReply) { try { const { bookId, chapter, provider } = req.params; const source = req.query.source || 'anilist'; + const lang = req.query.lang || 'none'; const content = await booksService.getChapterContent( bookId, chapter, provider, - source + source, + lang ); return reply.send(content); diff --git a/desktop/src/api/books/books.service.ts b/desktop/src/api/books/books.service.ts index 6b3e611..255b9aa 100644 --- a/desktop/src/api/books/books.service.ts +++ b/desktop/src/api/books/books.service.ts @@ -67,7 +67,19 @@ export async function getBookById(id: string | number): Promise { +export async function getChapterContent(bookId: string, chapterId: string, providerName: string, source: string, lang: string): Promise { const extensions = getAllExtensions(); const ext = extensions.get(providerName); @@ -471,14 +484,14 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr throw new Error("Provider not found"); } - const contentCacheKey = `content:${providerName}:${source}:${bookId}:${chapterIndex}`; + const contentCacheKey = `content:${providerName}:${source}:${lang}:${bookId}:${chapterId}`; const cachedContent = await getCache(contentCacheKey); if (cachedContent) { const isExpired = Date.now() - cachedContent.created_at > CACHE_TTL_MS; if (!isExpired) { - console.log(`[${providerName}] Content cache hit for Book ID ${bookId}, Index ${chapterIndex}`); + console.log(`[${providerName}] Content cache hit for Book ID ${bookId}, Index ${chapterId}`); try { return JSON.parse(cachedContent.result) as ChapterContent; } catch (e) { @@ -486,33 +499,14 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr } } else { - console.log(`[${providerName}] Content cache expired for Book ID ${bookId}, Index ${chapterIndex}`); + console.log(`[${providerName}] Content cache expired for Book ID ${bookId}, Index ${chapterId}`); } } - const isExternal = source !== 'anilist'; - const chapterList = await getChaptersForBook(bookId, isExternal, providerName); - - if (!chapterList?.chapters || chapterList.chapters.length === 0) { - throw new Error("Chapters not found"); - } - - const providerChapters = chapterList.chapters.filter(c => c.provider === providerName); - const index = parseInt(chapterIndex, 10); - - if (Number.isNaN(index)) { - throw new Error("Invalid chapter index"); - } - - if (!providerChapters[index]) { - throw new Error("Chapter index out of range"); - } - - const selectedChapter = providerChapters[index]; - - const chapterId = selectedChapter.id; - const chapterTitle = selectedChapter.title || null; - const chapterNumber = typeof selectedChapter.number === 'number' ? selectedChapter.number : index; + const selectedChapter: any = { + id: chapterId, + provider: providerName + }; try { if (!ext.findChapterPages) { @@ -522,12 +516,13 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr let contentResult: ChapterContent; if (ext.mediaType === "manga") { + // Usamos el ID directamente const pages = await ext.findChapterPages(chapterId); contentResult = { type: "manga", - chapterId, - title: chapterTitle, - number: chapterNumber, + chapterId: selectedChapter.id, + title: selectedChapter.title, + number: selectedChapter.number, provider: providerName, pages }; @@ -535,9 +530,9 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr const content = await ext.findChapterPages(chapterId); contentResult = { type: "ln", - chapterId, - title: chapterTitle, - number: chapterNumber, + chapterId: selectedChapter.id, + title: selectedChapter.title, + number: selectedChapter.number, provider: providerName, content }; @@ -546,7 +541,6 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr } await setCache(contentCacheKey, contentResult, CACHE_TTL_MS); - return contentResult; } catch (err) { diff --git a/desktop/src/api/types.ts b/desktop/src/api/types.ts index 57d804b..d369c19 100644 --- a/desktop/src/api/types.ts +++ b/desktop/src/api/types.ts @@ -79,6 +79,7 @@ export interface Episode { } export interface Chapter { + language?: string | null; index: number; id: string; number: string | number; diff --git a/desktop/src/scripts/books/book.js b/desktop/src/scripts/books/book.js index 73a9643..6ac1337 100644 --- a/desktop/src/scripts/books/book.js +++ b/desktop/src/scripts/books/book.js @@ -5,25 +5,367 @@ let bookSlug = null; let allChapters = []; let filteredChapters = []; - let availableExtensions = []; let isLocal = false; + +let currentLanguage = null; +let uniqueLanguages = []; +let isSortAscending = true; + const chapterPagination = Object.create(PaginationManager); -chapterPagination.init(12, () => renderChapterTable()); +chapterPagination.init(6, () => renderChapterList()); document.addEventListener('DOMContentLoaded', () => { init(); setupModalClickOutside(); + document.getElementById('sort-btn')?.addEventListener('click', toggleSortOrder); }); +async function init() { + try { + const urlData = URLUtils.parseEntityPath('book'); + if (!urlData) { showError("Book Not Found"); return; } + + extensionName = urlData.extensionName; + bookId = urlData.entityId; + bookSlug = urlData.slug; + + await loadBookMetadata(); + await checkLocalLibraryEntry(); + await loadAvailableExtensions(); + await loadChapters(); + await setupAddToListButton(); + + } catch (err) { + console.error("Init Error:", err); + showError("Error loading book"); + } +} + +async function loadBookMetadata() { + const source = extensionName || 'anilist'; + const fetchUrl = `/api/book/${bookId}?source=${source}`; + + try { + const res = await fetch(fetchUrl); + const data = await res.json(); + + if (data.error || !data) { showError("Book Not Found"); return; } + + const raw = Array.isArray(data) ? data[0] : data; + bookData = raw; + + const metadata = MediaMetadataUtils.formatBookData(raw, !!extensionName); + bookData.entry_type = metadata.format === 'MANGA' ? 'MANGA' : 'NOVEL'; + + updatePageTitle(metadata.title); + updateMetadata(metadata, raw); + updateExtensionPill(); + + } catch (e) { + console.error(e); + showError("Error loading metadata"); + } +} + +function updatePageTitle(title) { + document.title = `${title} | WaifuBoard Books`; + const titleEl = document.getElementById('title'); + if (titleEl) titleEl.innerText = title; +} + +function updateMetadata(metadata, rawData) { + // 1. Cabecera (Score, Año, Status, Formato, Caps) + const elements = { + 'description': metadata.description, + 'published-date': metadata.year, + 'status': metadata.status, + 'format': metadata.format, + 'chapters-count': metadata.chapters ? `${metadata.chapters} Ch` : '?? Ch', + 'genres': metadata.genres ? metadata.genres.replace(/,/g, ' • ') : '', + 'poster': metadata.poster, + 'hero-bg': metadata.banner + }; + + if(document.getElementById('description')) document.getElementById('description').innerHTML = metadata.description; + if(document.getElementById('poster')) document.getElementById('poster').src = metadata.poster; + if(document.getElementById('hero-bg')) document.getElementById('hero-bg').src = metadata.banner; + + ['published-date','status','format','chapters-count','genres'].forEach(id => { + const el = document.getElementById(id); + if(el) el.innerText = elements[id]; + }); + + const scoreEl = document.getElementById('score'); + if (scoreEl) scoreEl.innerText = extensionName ? `${metadata.score}` : `${metadata.score}% Score`; + + // 2. Sidebar: Sinónimos (Para llenar espacio vacío) + if (rawData.synonyms && rawData.synonyms.length > 0) { + const sidebarInfo = document.getElementById('sidebar-info'); + const list = document.getElementById('synonyms-list'); + if (sidebarInfo && list) { + sidebarInfo.style.display = 'block'; + list.innerHTML = ''; + // Mostrar máx 5 sinónimos para no alargar demasiado + rawData.synonyms.slice(0, 5).forEach(syn => { + const li = document.createElement('li'); + li.innerText = syn; + list.appendChild(li); + }); + } + } + + // 3. Renderizar Personajes + if (rawData.characters && rawData.characters.nodes && rawData.characters.nodes.length > 0) { + renderCharacters(rawData.characters.nodes); + } + + // 4. Renderizar Relaciones + if (rawData.relations && rawData.relations.edges && rawData.relations.edges.length > 0) { + renderRelations(rawData.relations.edges); + } +} + +function renderCharacters(nodes) { + const container = document.getElementById('characters-list'); + if(!container) return; + container.innerHTML = ''; + + nodes.forEach(char => { + const el = document.createElement('div'); + el.className = 'character-item'; + + const img = char.image?.large || char.image?.medium || '/public/assets/no-image.png'; + const name = char.name?.full || 'Unknown'; + const role = char.role || 'Supporting'; + + el.innerHTML = ` +
+
+
${name}
+
${role}
+
+ `; + container.appendChild(el); + }); +} + +function renderRelations(edges) { + const container = document.getElementById('relations-list'); + const section = document.getElementById('relations-section'); + if(!container || !section) return; + + if (!edges || edges.length === 0) { + section.style.display = 'none'; + return; + } + + section.style.display = 'block'; + container.innerHTML = ''; + + edges.forEach(edge => { + const node = edge.node; + if (!node) return; + + const el = document.createElement('div'); + el.className = 'relation-card-horizontal'; + + const img = node.coverImage?.large || node.coverImage?.medium || '/public/assets/no-image.png'; + const title = node.title?.romaji || node.title?.english || node.title?.native || 'Unknown'; + const type = edge.relationType ? edge.relationType.replace(/_/g, ' ') : 'Related'; + + el.innerHTML = ` + ${title} +
+ ${type} + ${title} +
+ `; + + el.onclick = () => { + const targetType = node.type === 'ANIME' ? 'anime' : 'book'; + window.location.href = `/${targetType}/${node.id}`; + }; + + container.appendChild(el); + }); +} + +function processChaptersData(chaptersData) { + allChapters = chaptersData; + const langSet = new Set(allChapters.map(ch => ch.language).filter(l => l)); + uniqueLanguages = Array.from(langSet); + setupLanguageSelector(); + filterAndRenderChapters(); + setupReadButton(); +} + +async function loadChapters(targetProvider = null) { + const listContainer = document.getElementById('chapters-list'); + const loadingMsg = document.getElementById('loading-msg'); + + if(listContainer) listContainer.innerHTML = ''; + if(loadingMsg) loadingMsg.style.display = 'block'; + + if (!targetProvider) { + const select = document.getElementById('provider-filter'); + targetProvider = select ? select.value : (availableExtensions[0] || 'all'); + } + + try { + let fetchUrl; + let isLocalRequest = targetProvider === 'local'; + + if (isLocalRequest) { + fetchUrl = `/api/library/${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(); + + if(loadingMsg) loadingMsg.style.display = 'none'; + + let rawData = []; + if (isLocalRequest) { + rawData = (data.units || []).map((unit, idx) => ({ + id: unit.id || idx, number: unit.number, title: unit.name, + provider: 'local', index: idx, format: unit.format, language: 'local' + })); + } else { + rawData = data.chapters || []; + } + processChaptersData(rawData); + + } catch (err) { + if(loadingMsg) loadingMsg.style.display = 'none'; + if(listContainer) listContainer.innerHTML = '
Error loading chapters.
'; + console.error(err); + } +} + +function setupLanguageSelector() { + const selectorContainer = document.getElementById('language-selector-container'); + const select = document.getElementById('language-select'); + if (!selectorContainer || !select) return; + + if (uniqueLanguages.length <= 1) { + selectorContainer.classList.add('hidden'); + currentLanguage = uniqueLanguages[0] || null; + return; + } + selectorContainer.classList.remove('hidden'); + select.innerHTML = ''; + + const langNames = { 'es': 'Español', 'es-419': 'Latino', 'en': 'English', 'pt-br': 'Português', 'ja': '日本語' }; + + uniqueLanguages.forEach(lang => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = langNames[lang] || lang.toUpperCase(); + select.appendChild(option); + }); + + if (uniqueLanguages.includes('es-419')) currentLanguage = 'es-419'; + else if (uniqueLanguages.includes('es')) currentLanguage = 'es'; + else currentLanguage = uniqueLanguages[0]; + select.value = currentLanguage; + + select.onchange = (e) => { + currentLanguage = e.target.value; + chapterPagination.currentPage = 1; + filterAndRenderChapters(); + }; +} + +function filterAndRenderChapters() { + let tempChapters = [...allChapters]; + if (currentLanguage && uniqueLanguages.length > 1) { + tempChapters = tempChapters.filter(ch => ch.language === currentLanguage); + } + const searchQuery = document.getElementById('chapter-search')?.value.toLowerCase(); + if(searchQuery){ + tempChapters = tempChapters.filter(ch => + (ch.title && ch.title.toLowerCase().includes(searchQuery)) || + (ch.number && ch.number.toString().includes(searchQuery)) + ); + } + tempChapters.sort((a, b) => { + const numA = parseFloat(a.number) || 0; + const numB = parseFloat(b.number) || 0; + return isSortAscending ? numA - numB : numB - numA; + }); + filteredChapters = tempChapters; + chapterPagination.setTotalItems(filteredChapters.length); + renderChapterList(); +} + +function renderChapterList() { + const container = document.getElementById('chapters-list'); + if(!container) return; + container.innerHTML = ''; + const itemsToShow = chapterPagination.getCurrentPageItems(filteredChapters); + + if (itemsToShow.length === 0) { + container.innerHTML = '
No chapters found.
'; + return; + } + + itemsToShow.forEach(chapter => { + const el = document.createElement('div'); + el.className = 'chapter-item'; + el.onclick = () => openReader(chapter.id, chapter.provider); + + const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : ''; + const providerLabel = chapter.provider !== 'local' ? chapter.provider : ''; + + el.innerHTML = ` +
+ Chapter ${chapter.number} + ${chapter.title || ''} +
+
+ ${providerLabel ? `${providerLabel}` : ''} + ${dateStr ? `${dateStr}` : ''} + +
+ `; + container.appendChild(el); + }); + chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page'); +} + +function toggleSortOrder() { + isSortAscending = !isSortAscending; + const btn = document.getElementById('sort-btn'); + if(btn) btn.style.transform = isSortAscending ? 'rotate(180deg)' : 'rotate(0deg)'; + filterAndRenderChapters(); +} + +function setupReadButton() { + const readBtn = document.getElementById('read-start-btn'); + if (!readBtn || allChapters.length === 0) return; + const firstChapter = [...allChapters].sort((a,b) => a.index - b.index)[0]; + if (firstChapter) readBtn.onclick = () => + openReader(firstChapter.index ?? firstChapter.id, firstChapter.provider); + +} + +function openReader(chapterIndexOrId, provider) { + const lang = currentLanguage ?? 'none'; + window.location.href = + URLUtils.buildReadUrl(bookId, chapterIndexOrId, provider, extensionName || 'anilist') + + `?lang=${lang}`; +} + async function checkLocalLibraryEntry() { try { - const libraryType = - bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; - + const libraryType = bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; const res = await fetch(`/api/library/${libraryType}/${bookId}`); if (!res.ok) return; - const data = await res.json(); if (data.matched) { isLocal = true; @@ -36,34 +378,7 @@ async function checkLocalLibraryEntry() { pill.style.borderColor = 'rgba(34, 197, 94, 0.3)'; } } - } catch (e) { - console.error("Error checking local status:", e); - } -} - -async function init() { - try { - const urlData = URLUtils.parseEntityPath('book'); - if (!urlData) { - showError("Book Not Found"); - return; - } - - extensionName = urlData.extensionName; - bookId = urlData.entityId; - bookSlug = urlData.slug; - await loadBookMetadata(); - await checkLocalLibraryEntry(); - - await loadAvailableExtensions(); - await loadChapters(); - - await setupAddToListButton(); - - } catch (err) { - console.error("Metadata Error:", err); - showError("Error loading book"); - } + } catch (e) { console.error("Error checking local:", e); } } async function loadAvailableExtensions() { @@ -71,220 +386,21 @@ async function loadAvailableExtensions() { const res = await fetch('/api/extensions/book'); const data = await res.json(); availableExtensions = data.extensions || []; - setupProviderFilter(); - } catch (err) { - console.error("Error fetching extensions:", err); - } -} - -async function loadBookMetadata() { - const source = extensionName || 'anilist'; - const fetchUrl = `/api/book/${bookId}?source=${source}`; - - const res = await fetch(fetchUrl); - const data = await res.json(); - - if (data.error || !data) { - showError("Book Not Found"); - return; - } - - const raw = Array.isArray(data) ? data[0] : data; - bookData = raw; - - const metadata = MediaMetadataUtils.formatBookData(raw, !!extensionName); - bookData.entry_type = - metadata.format === 'MANGA' ? 'MANGA' : 'NOVEL'; - updatePageTitle(metadata.title); - updateMetadata(metadata); - updateExtensionPill(); -} - -function updatePageTitle(title) { - document.title = `${title} | WaifuBoard Books`; - const titleEl = document.getElementById('title'); - if (titleEl) titleEl.innerText = title; -} - -function updateMetadata(metadata) { - - const descEl = document.getElementById('description'); - if (descEl) descEl.innerHTML = metadata.description; - - const scoreEl = document.getElementById('score'); - if (scoreEl) { - scoreEl.innerText = extensionName - ? `${metadata.score}` - : `${metadata.score}% Score`; - } - - const pubEl = document.getElementById('published-date'); - if (pubEl) pubEl.innerText = metadata.year; - - const statusEl = document.getElementById('status'); - if (statusEl) statusEl.innerText = metadata.status; - - const formatEl = document.getElementById('format'); - if (formatEl) formatEl.innerText = metadata.format; - - const chaptersEl = document.getElementById('chapters'); - if (chaptersEl) chaptersEl.innerText = metadata.chapters; - - const genresEl = document.getElementById('genres'); - if (genresEl) genresEl.innerText = metadata.genres; - - const posterEl = document.getElementById('poster'); - if (posterEl) posterEl.src = metadata.poster; - - const heroBgEl = document.getElementById('hero-bg'); - if (heroBgEl) heroBgEl.src = metadata.banner; -} - -function updateExtensionPill() { - const pill = document.getElementById('extension-pill'); - if (!pill) return; - - if (extensionName) { - pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase(); - pill.style.display = 'inline-flex'; - } else { - pill.style.display = 'none'; - } -} - -async function setupAddToListButton() { - const btn = document.getElementById('add-to-list-btn'); - if (!btn || !bookData) return; - - ListModalManager.currentData = bookData; - const entryType = ListModalManager.getEntryType(bookData); - const idForCheck = extensionName ? bookSlug : bookId; - - await ListModalManager.checkIfInList( - idForCheck, - extensionName || 'anilist', - entryType - ); - - updateCustomAddButton(); - - btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist'); -} - -function updateCustomAddButton() { - const btn = document.getElementById('add-to-list-btn'); - if (!btn) return; - - if (ListModalManager.isInList) { - btn.innerHTML = ` - - - - In Your Library - `; - btn.style.background = 'rgba(34, 197, 94, 0.2)'; - btn.style.color = '#22c55e'; - btn.style.borderColor = 'rgba(34, 197, 94, 0.3)'; - } else { - btn.innerHTML = '+ Add to Library'; - btn.style.background = null; - btn.style.color = null; - btn.style.borderColor = null; - } -} - -async function loadChapters(targetProvider = null) { - const tbody = document.getElementById('chapters-body'); - if (!tbody) return; - - if (!targetProvider) { - const select = document.getElementById('provider-filter'); - targetProvider = select ? select.value : (availableExtensions[0] || 'all'); - } - - tbody.innerHTML = 'Loading chapters...'; - - try { - let fetchUrl; - let isLocalRequest = targetProvider === 'local'; - - if (isLocalRequest) { - // Nuevo endpoint para archivos locales - fetchUrl = `/api/library/${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(); - - // 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 = 'No chapters found.'; - if (totalEl) totalEl.innerText = "0 Found"; - return; - } - - if (totalEl) totalEl.innerText = `${allChapters.length} Found`; - - setupReadButton(); - chapterPagination.setTotalItems(filteredChapters.length); - chapterPagination.reset(); - renderChapterTable(); - - } catch (err) { - tbody.innerHTML = 'Error loading chapters.'; - console.error(err); - } -} - -function applyChapterFilter() { - const chapterParam = URLUtils.getQueryParam('chapter'); - if (!chapterParam) return; - - const chapterNumber = parseFloat(chapterParam); - if (isNaN(chapterNumber)) return; - - filteredChapters = allChapters.filter( - ch => parseFloat(ch.number) === chapterNumber - ); - - chapterPagination.reset(); + } catch (err) { console.error(err); } } function setupProviderFilter() { const select = document.getElementById('provider-filter'); 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)'; + allOpt.innerText = 'All Providers'; 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'; @@ -292,7 +408,6 @@ function setupProviderFilter() { select.appendChild(localOpt); } - // Añadir extensiones normales availableExtensions.forEach(ext => { const opt = document.createElement('option'); opt.value = ext; @@ -300,90 +415,43 @@ function setupProviderFilter() { select.appendChild(opt); }); - // 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]; + if (isLocal) select.value = 'local'; + else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName; + else if (availableExtensions.length > 0) select.value = availableExtensions[0]; + + select.onchange = () => loadChapters(select.value); +} + +function updateExtensionPill() { + const pill = document.getElementById('extension-pill'); + if(pill && extensionName) { pill.innerText = extensionName; pill.style.display = 'inline-flex'; } +} + +async function setupAddToListButton() { + const btn = document.getElementById('add-to-list-btn'); + if (!btn || !bookData) return; + ListModalManager.currentData = bookData; + const entryType = ListModalManager.getEntryType(bookData); + const idForCheck = extensionName ? bookSlug : bookId; + await ListModalManager.checkIfInList(idForCheck, extensionName || 'anilist', entryType); + updateCustomAddButton(); + btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist'); +} + +function updateCustomAddButton() { + const btn = document.getElementById('add-to-list-btn'); + if(btn && ListModalManager.isInList) { + btn.innerHTML = '✓ In Your List'; btn.style.background = 'rgba(34, 197, 94, 0.2)'; btn.style.color = '#22c55e'; btn.style.borderColor = '#22c55e'; } - - select.onchange = () => { - loadChapters(select.value); - }; -} - -function setupReadButton() { - const readBtn = document.getElementById('read-start-btn'); - if (!readBtn || filteredChapters.length === 0) return; - - const firstChapter = filteredChapters[0]; - readBtn.onclick = () => openReader(0, firstChapter.provider); -} - -function renderChapterTable() { - const tbody = document.getElementById('chapters-body'); - if (!tbody) return; - - tbody.innerHTML = ''; - - if (filteredChapters.length === 0) { - tbody.innerHTML = 'No chapters match this filter.'; - chapterPagination.renderControls( - 'pagination', - 'page-info', - 'prev-page', - 'next-page' - ); - return; - } - - const pageItems = chapterPagination.getCurrentPageItems(filteredChapters); - - pageItems.forEach((ch) => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${ch.number} - ${ch.title || 'Chapter ' + ch.number} - ${ch.provider} - - - - `; - tbody.appendChild(row); - }); - - chapterPagination.renderControls( - 'pagination', - 'page-info', - 'prev-page', - 'next-page' - ); -} - -function openReader(chapterId, provider) { - 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() { const modal = document.getElementById('add-list-modal'); - if (!modal) return; - - modal.addEventListener('click', (e) => { - if (e.target.id === 'add-list-modal') { - ListModalManager.close(); - } - }); + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target.id === 'add-list-modal') ListModalManager.close(); + }); + } } function showError(message) { @@ -391,21 +459,15 @@ function showError(message) { if (titleEl) titleEl.innerText = message; } -function saveToList() { +// Exports +window.openReader = openReader; +window.saveToList = () => { const idToSave = extensionName ? bookSlug : bookId; ListModalManager.save(idToSave, extensionName || 'anilist'); -} - -function deleteFromList() { +}; +window.deleteFromList = () => { const idToDelete = extensionName ? bookSlug : bookId; ListModalManager.delete(idToDelete, extensionName || 'anilist'); -} - -function closeAddToListModal() { - ListModalManager.close(); -} - -window.openReader = openReader; -window.saveToList = saveToList; -window.deleteFromList = deleteFromList; -window.closeAddToListModal = closeAddToListModal; \ No newline at end of file +}; +window.closeAddToListModal = () => ListModalManager.close(); +window.openAddToListModal = () => ListModalManager.open(bookData, extensionName || 'anilist'); \ No newline at end of file diff --git a/desktop/src/scripts/books/reader.js b/desktop/src/scripts/books/reader.js index 02cb9a7..6d38d04 100644 --- a/desktop/src/scripts/books/reader.js +++ b/desktop/src/scripts/books/reader.js @@ -1,3 +1,7 @@ +// reader.js refactorizado + +const urlParams = new URLSearchParams(window.location.search); +const lang = urlParams.get('lang') ?? 'none'; const reader = document.getElementById('reader'); const panel = document.getElementById('settings-panel'); const overlay = document.getElementById('overlay'); @@ -33,11 +37,12 @@ let currentType = null; let currentPages = []; let observer = null; +// === CAMBIO: Parseo de URL para obtener ID === const parts = window.location.pathname.split('/'); - const bookId = parts[4]; -let chapter = parts[3]; +let currentChapterId = parts[3]; // Ahora es un ID (string) let provider = parts[2]; +let chaptersList = []; // Buffer para guardar el orden de capítulos function loadConfig() { try { @@ -116,6 +121,31 @@ function updateSettingsVisibility() { mangaSettings.classList.toggle('hidden', currentType !== 'manga'); } +// === CAMBIO: Nueva función para traer la lista de capítulos y saber el orden === +async function fetchChapterList() { + const urlParams = new URLSearchParams(window.location.search); + const source = urlParams.get('source') || 'anilist'; + + try { + // Reusamos el endpoint que lista capítulos + const res = await fetch(`/api/book/${bookId}/chapters?source=${source}&provider=${provider}`); + const data = await res.json(); + + // Ordenamos por número para asegurar navegación correcta + let list = data.chapters || []; + list.sort((a, b) => Number(a.number) - Number(b.number)); + + // Si hay filtro de idioma en la URL, filtramos la navegación también + if (lang !== 'none') { + list = list.filter(c => c.language === lang); + } + + chaptersList = list; + } catch (e) { + console.error("Error fetching chapter list:", e); + } +} + async function loadChapter() { reader.innerHTML = `
@@ -126,23 +156,35 @@ async function loadChapter() { const urlParams = new URLSearchParams(window.location.search); let source = urlParams.get('source'); - if (!source) { - source = 'anilist'; + if (!source) source = 'anilist'; + + // === CAMBIO: Si no tenemos la lista de capítulos (y no es local), la pedimos === + if (provider !== 'local' && chaptersList.length === 0) { + await fetchChapterList(); } + let newEndpoint; if (provider === 'local') { newEndpoint = `/api/library/${bookId}/units`; } else { - newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`; + // === CAMBIO: Usamos currentChapterId en la URL === + newEndpoint = `/api/book/${bookId}/${currentChapterId}/${provider}?source=${source}&lang=${lang}`; } try { const res = await fetch(newEndpoint); const data = await res.json(); + + // Lógica específica para contenido LOCAL if (provider === 'local') { - const unit = data.units[Number(chapter)]; - if (!unit) return; + const unitIndex = Number(currentChapterId); // En local el ID suele ser el índice + const unit = data.units[unitIndex]; + + if (!unit) { + reader.innerHTML = '
Chapter not found (Local)
'; + return; + } chapterLabel.textContent = unit.name; document.title = unit.name; @@ -152,42 +194,28 @@ async function loadChapter() { reader.innerHTML = ''; - // ===== MANGA ===== + // Setup navegación manual para local (simple index +/- 1) + setupLocalNavigation(unitIndex, data.units.length); + if (manifest.type === 'manga') { currentType = 'manga'; updateSettingsVisibility(); applyStyles(); - currentPages = manifest.pages; loadManga(currentPages); return; } - - // ===== LN ===== if (manifest.type === 'ln') { currentType = 'ln'; updateSettingsVisibility(); applyStyles(); - const contentRes = await fetch(manifest.url); const html = await contentRes.text(); - loadLN(html); return; } } - - if (data.title) { - chapterLabel.textContent = data.title; - document.title = data.title; - } else { - chapterLabel.textContent = `Chapter ${chapter}`; - document.title = `Chapter ${chapter}`; - } - - setupProgressTracking(data, source); - const res2 = await fetch(`/api/book/${bookId}?source=${source}`); const data2 = await res2.json(); @@ -200,15 +228,25 @@ async function loadChapter() { mode: "reading" }) }); + if (data.error) { - reader.innerHTML = ` -
- Error: ${data.error} -
- `; + reader.innerHTML = `
Error: ${data.error}
`; return; } + if (data.title) { + chapterLabel.textContent = data.title; + document.title = data.title; + } else { + chapterLabel.textContent = `Chapter ${data.number ?? currentChapterId}`; + document.title = `Chapter ${data.number ?? currentChapterId}`; + } + + setupProgressTracking(data, source); + + // === CAMBIO: Actualizar botones basado en IDs === + updateNavigationButtons(); + currentType = data.type; updateSettingsVisibility(); applyStyles(); @@ -221,6 +259,7 @@ async function loadChapter() { loadLN(data.content); } } catch (error) { + console.error(error); reader.innerHTML = `
Error loading chapter: ${error.message} @@ -229,6 +268,87 @@ async function loadChapter() { } } +// === CAMBIO: Lógica de navegación basada en IDs === +function updateNavigationButtons() { + if (provider === 'local') return; // Se maneja aparte + + // Buscamos el índice actual en la lista completa + const currentIndex = chaptersList.findIndex(c => String(c.id) === String(currentChapterId)); + + if (currentIndex === -1) { + console.warn("Current chapter not found in list, navigation disabled"); + prevBtn.disabled = true; + nextBtn.disabled = true; + prevBtn.style.opacity = 0.5; + nextBtn.style.opacity = 0.5; + return; + } + + // Configurar botón ANTERIOR + if (currentIndex > 0) { + const prevId = chaptersList[currentIndex - 1].id; + prevBtn.onclick = () => changeChapter(prevId); + prevBtn.disabled = false; + prevBtn.style.opacity = 1; + } else { + prevBtn.onclick = null; + prevBtn.disabled = true; + prevBtn.style.opacity = 0.5; + } + + // Configurar botón SIGUIENTE + if (currentIndex < chaptersList.length - 1) { + const nextId = chaptersList[currentIndex + 1].id; + nextBtn.onclick = () => changeChapter(nextId); + nextBtn.disabled = false; + nextBtn.style.opacity = 1; + } else { + nextBtn.onclick = null; + nextBtn.disabled = true; + nextBtn.style.opacity = 0.5; + } +} + +// Fallback para navegación local (basada en índices) +function setupLocalNavigation(currentIndex, totalUnits) { + if (currentIndex > 0) { + prevBtn.onclick = () => changeChapter(currentIndex - 1); + prevBtn.disabled = false; + prevBtn.style.opacity = 1; + } else { + prevBtn.disabled = true; + prevBtn.style.opacity = 0.5; + } + + if (currentIndex < totalUnits - 1) { + nextBtn.onclick = () => changeChapter(currentIndex + 1); + nextBtn.disabled = false; + nextBtn.style.opacity = 1; + } else { + nextBtn.disabled = true; + nextBtn.style.opacity = 0.5; + } +} + +// === CAMBIO: Función helper para cambiar de capítulo === +function changeChapter(newId) { + currentChapterId = newId; + updateURL(newId); + window.scrollTo(0, 0); + loadChapter(); +} + +function updateURL(newId) { + const urlParams = new URLSearchParams(window.location.search); + const source = urlParams.get('source') ?? 'anilist'; + + // La URL ahora contiene el ID en lugar del número/índice + const newUrl = `/read/${provider}/${newId}/${bookId}?source=${source}&lang=${lang}`; + window.history.pushState({}, '', newUrl); +} + +// --- Resto de funciones UI (Manga/LN loading) sin cambios lógicos mayores --- + function loadManga(pages) { if (!pages || pages.length === 0) { reader.innerHTML = '
No pages found
'; @@ -239,7 +359,6 @@ function loadManga(pages) { container.className = 'manga-container'; let isLongStrip = false; - if (config.manga.mode === 'longstrip') { isLongStrip = true; } else if (config.manga.mode === 'auto' && detectLongStrip(pages)) { @@ -262,15 +381,12 @@ function loadManga(pages) { function shouldUseDoublePage(pages) { if (pages.length <= 5) return false; - const widePages = pages.filter(p => { if (!p.height || !p.width) return false; const ratio = p.width / p.height; return ratio > 1.3; }); - if (widePages.length > pages.length * 0.3) return false; - return true; } @@ -310,7 +426,6 @@ function loadDoublePage(container, pages) { i++; } else { const rightPage = createImageElement(nextPage, i + 1); - if (config.manga.direction === 'rtl') { doubleContainer.appendChild(rightPage); doubleContainer.appendChild(leftPage); @@ -318,7 +433,6 @@ function loadDoublePage(container, pages) { doubleContainer.appendChild(leftPage); doubleContainer.appendChild(rightPage); } - container.appendChild(doubleContainer); i += 2; } @@ -361,30 +475,23 @@ function createImageElement(page, index) { img.dataset.src = url; img.loading = 'lazy'; } - img.alt = `Page ${index + 1}`; - return img; } function buildProxyUrl(url, headers = {}) { const params = new URLSearchParams({ url }); - if (headers.Referer || headers.referer) params.append("referer", headers.Referer || headers.referer); - if (headers["User-Agent"] || headers["user-agent"]) params.append("userAgent", headers["User-Agent"] || headers["user-agent"]); - if (headers.Origin || headers.origin) params.append("origin", headers.Origin || headers.origin); - return `/api/proxy?${params.toString()}`; } function detectLongStrip(pages) { if (!pages || pages.length === 0) return false; - const relevant = pages.slice(1); const tall = relevant.filter(p => p.height && p.width && (p.height / p.width) > 2); return tall.length >= 2 || (tall.length / relevant.length) > 0.3; @@ -392,7 +499,6 @@ function detectLongStrip(pages) { function setupLazyLoading() { if (observer) observer.disconnect(); - observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { @@ -404,10 +510,7 @@ function setupLazyLoading() { } } }); - }, { - rootMargin: '200px' - }); - + }, { rootMargin: '200px' }); document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img)); } @@ -418,161 +521,102 @@ function loadLN(html) { reader.appendChild(div); } +// Listeners de configuración 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(); + 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(); + 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(); + applyStyles(); saveConfig(); }); - document.getElementById('font-family').addEventListener('change', (e) => { config.ln.fontFamily = e.target.value; - applyStyles(); - saveConfig(); + applyStyles(); saveConfig(); }); - document.getElementById('text-color').addEventListener('change', (e) => { config.ln.textColor = e.target.value; - applyStyles(); - saveConfig(); + applyStyles(); saveConfig(); }); - document.getElementById('bg-color').addEventListener('change', (e) => { config.ln.bg = e.target.value; - applyStyles(); - saveConfig(); + applyStyles(); saveConfig(); }); - 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(); + applyStyles(); saveConfig(); }); }); - 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(); + applyStyles(); saveConfig(); } }); }); - document.getElementById('display-mode').addEventListener('change', (e) => { config.manga.mode = e.target.value; - saveConfig(); - loadChapter(); + saveConfig(); loadChapter(); }); - document.getElementById('image-fit').addEventListener('change', (e) => { config.manga.imageFit = e.target.value; - saveConfig(); - loadChapter(); + saveConfig(); loadChapter(); }); - 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(); + applyStyles(); saveConfig(); }); - document.getElementById('preload-count').addEventListener('change', (e) => { config.manga.preloadCount = parseInt(e.target.value); saveConfig(); }); - 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(); + saveConfig(); loadChapter(); }); }); -prevBtn.addEventListener('click', () => { - const current = parseInt(chapter); - if (current <= 0) return; - - const newChapter = String(current - 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 urlParams = new URLSearchParams(window.location.search); - let source = urlParams.get('source'); - - let src; - if (source === 'anilist') { - src= "?source=anilist" - } else { - src= `?source=${source}` - } - const newUrl = `/read/${provider}/${chapter}/${bookId}${src}`; - window.history.pushState({}, '', newUrl); -} - +// Botón "Atrás" document.getElementById('back-btn').addEventListener('click', () => { - const parts = window.location.pathname.split('/'); - const mangaId = parts[4]; - const urlParams = new URLSearchParams(window.location.search); - let source = urlParams.get('source'); + let source = urlParams.get('source')?.split('?')[0]; - if (source === 'anilist') { - window.location.href = `/book/${mangaId}`; + if (source === 'anilist' || !source) { + window.location.href = `/book/${bookId}`; } else { - window.location.href = `/book/${source}/${mangaId}`; + window.location.href = `/book/${source}/${bookId}`; } }); +// Panel de configuración settingsBtn.addEventListener('click', () => { panel.classList.add('open'); overlay.classList.add('active'); }); - closePanel.addEventListener('click', closeSettings); overlay.addEventListener('click', closeSettings); @@ -580,7 +624,6 @@ function closeSettings() { panel.classList.remove('open'); overlay.classList.remove('active'); } - document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && panel.classList.contains('open')) { closeSettings(); @@ -590,37 +633,24 @@ document.addEventListener('keydown', (e) => { function enableMangaPageNavigation() { if (currentType !== 'manga') return; const logicalPages = []; - document.querySelectorAll('.manga-container > *').forEach(el => { - if (el.classList.contains('double-container')) { - logicalPages.push(el); - } else if (el.tagName === 'IMG') { + if (el.classList.contains('double-container') || el.tagName === 'IMG') { logicalPages.push(el); } }); - if (logicalPages.length === 0) return; function scrollToLogical(index) { if (index < 0 || index >= logicalPages.length) return; - const topBar = document.querySelector('.top-bar'); const offset = topBar ? -topBar.offsetHeight : 0; - - const y = logicalPages[index].getBoundingClientRect().top - + window.pageYOffset - + offset; - - window.scrollTo({ - top: y, - behavior: 'smooth' - }); + const y = logicalPages[index].getBoundingClientRect().top + window.pageYOffset + offset; + window.scrollTo({ top: y, behavior: 'smooth' }); } function getCurrentLogicalIndex() { let closest = 0; let minDist = Infinity; - logicalPages.forEach((el, i) => { const rect = el.getBoundingClientRect(); const dist = Math.abs(rect.top); @@ -629,53 +659,36 @@ function enableMangaPageNavigation() { closest = i; } }); - return closest; } - const rtl = () => config.manga.direction === 'rtl'; document.addEventListener('keydown', (e) => { if (currentType !== 'manga') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; - const index = getCurrentLogicalIndex(); - - if (e.key === 'ArrowLeft') { - scrollToLogical(rtl() ? index + 1 : index - 1); - } - if (e.key === 'ArrowRight') { - scrollToLogical(rtl() ? index - 1 : index + 1); - } + if (e.key === 'ArrowLeft') scrollToLogical(rtl() ? index + 1 : index - 1); + if (e.key === 'ArrowRight') scrollToLogical(rtl() ? index - 1 : index + 1); }); reader.addEventListener('click', (e) => { if (currentType !== 'manga') return; - const bounds = reader.getBoundingClientRect(); const x = e.clientX - bounds.left; const half = bounds.width / 2; - const index = getCurrentLogicalIndex(); - - if (x < half) { - scrollToLogical(rtl() ? index + 1 : index - 1); - } else { - scrollToLogical(rtl() ? index - 1 : index + 1); - } + if (x < half) scrollToLogical(rtl() ? index + 1 : index - 1); + else scrollToLogical(rtl() ? index - 1 : index + 1); }); } let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); - resizeTimer = setTimeout(() => { - applyStyles(); - }, 250); + resizeTimer = setTimeout(() => { applyStyles(); }, 250); }); let progressSaved = false; - function setupProgressTracking(data, source) { progressSaved = false; @@ -688,25 +701,16 @@ function setupProgressTracking(data, source) { source: source, entry_type: data.type === 'manga' ? 'MANGA' : 'NOVEL', status: 'CURRENT', - progress: source === 'anilist' - ? Math.floor(chapterNumber) - - : chapterNumber - + progress: source === 'anilist' ? Math.floor(chapterNumber) : chapterNumber }; try { await fetch('/api/list/entry', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }); - } catch (err) { - console.error('Error updating progress:', err); - } + } catch (err) { console.error('Error updating progress:', err); } } function checkProgress() { @@ -716,25 +720,24 @@ function setupProgressTracking(data, source) { if (percent >= 0.8 && !progressSaved) { progressSaved = true; - + // Usamos el número real del capítulo, no el ID const chapterNumber = (typeof data.number !== 'undefined' && data.number !== null) ? data.number - : Number(chapter); + : 0; // Fallback si no hay numero sendProgress(chapterNumber); - window.removeEventListener('scroll', checkProgress); } } - window.removeEventListener('scroll', checkProgress); window.addEventListener('scroll', checkProgress); } -if (!bookId || !chapter || !provider) { +// Inicialización +if (!bookId || !currentChapterId || !provider) { reader.innerHTML = `
- Missing required parameters (bookId, chapter, provider) + Missing required parameters (bookId, chapterId, provider)
`; } else { diff --git a/desktop/views/books/book.html b/desktop/views/books/book.html index a784508..4a2d33c 100644 --- a/desktop/views/books/book.html +++ b/desktop/views/books/book.html @@ -24,13 +24,13 @@
- - - Back to Books + + + + Back -
@@ -40,107 +40,110 @@
+
+

Loading...

- +
+
-
-
-

Loading...

- -
- - -
--% Score
-
Action
+ -
-
-

Chapters

-
+
- +
+
+

Chapters

+ +
+ + + + + + + +
+
+ +
+ + +
-
- - - - - - - - - - - - - -
#TitleProviderAction
-
-
-
+
+ + + +
+

Characters

+
+
- - diff --git a/desktop/views/css/books/book.css b/desktop/views/css/books/book.css index 3e3c042..3c32f10 100644 --- a/desktop/views/css/books/book.css +++ b/desktop/views/css/books/book.css @@ -1,194 +1,190 @@ +:root { + --bg-card: rgba(255, 255, 255, 0.04); + --border-subtle: rgba(255, 255, 255, 0.1); + --color-primary: #8b5cf6; +} + +/* --- BASICS --- */ .back-btn { - position: fixed; - top: 2rem; left: 2rem; z-index: 100; - display: flex; align-items: center; gap: 0.5rem; - padding: 0.8rem 1.5rem; - background: var(--color-glass-bg); backdrop-filter: blur(12px); - border: var(--border-subtle); border-radius: var(--radius-full); - color: white; text-decoration: none; font-weight: 600; - transition: all 0.2s ease; + position: fixed; top: 2rem; left: 2rem; z-index: 100; display: flex; align-items: center; gap: 0.5rem; + padding: 0.8rem 1.5rem; background: var(--color-glass-bg); backdrop-filter: blur(12px); + border: var(--border-subtle); border-radius: 8px; /* Anime style */ + color: white; text-decoration: none; font-weight: 600; transition: all 0.2s ease; } .back-btn:hover { background: rgba(255, 255, 255, 0.15); transform: translateX(-5px); } -.hero-wrapper { - position: relative; width: 100%; height: 60vh; overflow: hidden; -} +.hero-wrapper { position: relative; width: 100%; height: 60vh; overflow: hidden; } .hero-background { position: absolute; inset: 0; z-index: 0; } .hero-background img { width: 100%; height: 100%; object-fit: cover; opacity: 0.4; filter: blur(8px); transform: scale(1.1); } -.hero-overlay { - position: absolute; inset: 0; z-index: 1; - background: linear-gradient(to bottom, transparent 0%, var(--color-bg-base) 100%); -} +.hero-overlay { position: absolute; inset: 0; z-index: 1; background: linear-gradient(to bottom, transparent 0%, var(--color-bg-base) 100%); } .content-container { - position: relative; z-index: 10; - max-width: 1600px; margin: -350px auto 0 auto; + position: relative; z-index: 10; max-width: 1400px; margin: -300px auto 0 auto; padding: 0 3rem 4rem 3rem; - display: grid; - grid-template-columns: 260px 1fr; - gap: 3rem; - align-items: flex-start; - animation: slideUp 0.8s ease; } -.hero-content { display: none; } - -.sidebar { - display: flex; - flex-direction: column; - gap: 1.5rem; - position: sticky; - top: calc(var(--nav-height) + 2rem); - align-self: flex-start; - z-index: 20; +/* --- HEADER SECTION --- */ +.book-header-section { margin-bottom: 4rem; max-width: 900px; } +.book-title { + font-size: clamp(2.5rem, 6vw, 4.5rem); font-weight: 900; + margin-bottom: 0.5rem; text-shadow: 0 4px 30px rgba(0,0,0,0.6); line-height: 1.1; } +.hero-meta-info { + display: flex; align-items: center; gap: 0.8rem; + color: rgba(255,255,255,0.7); font-weight: 600; margin-bottom: 1.2rem; + flex-wrap: wrap; +} +.meta-separator { color: rgba(255,255,255,0.3); font-size: 0.8rem; } + +.pill-local { + background: #22c55e; color: black; padding: 2px 8px; + border-radius: 4px; font-size: 0.75rem; font-weight: 900; +} + +.hero-description-mini { + font-size: 1.05rem; line-height: 1.5; color: rgba(255,255,255,0.8); + margin-bottom: 1.2rem; max-width: 700px; + display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; +} +.hero-tags { + display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2.5rem; + color: rgba(255,255,255,0.5); font-weight: 500; +} + +/* --- BUTTONS --- */ +.action-row { display: flex; align-items: center; gap: 1rem; } +.btn-read { + padding: 0.8rem 2.2rem; background: white; color: black; + border-radius: 8px; font-weight: 800; border: none; cursor: pointer; + display: flex; align-items: center; gap: 0.6rem; transition: 0.2s ease; +} +.btn-read:hover { transform: scale(1.03); filter: brightness(0.9); } + +.btn-add-list { + padding: 0.8rem 1.5rem; background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); color: white; + border-radius: 8px; font-weight: 700; cursor: pointer; transition: 0.2s; +} +.btn-add-list:hover { background: rgba(255,255,255,0.2); } + + +/* --- MAIN LAYOUT --- */ +.main-layout { display: grid; grid-template-columns: 280px 1fr; gap: 4rem; margin-top: 2rem; } + +/* Sidebar */ +.poster-section { display: flex; flex-direction: column; gap: 1.5rem; } .poster-card { - width: 100%; aspect-ratio: 2/3; border-radius: var(--radius-lg); - overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.8); - border: 1px solid rgba(255,255,255,0.1); - background: #1a1a1a; + border-radius: 12px; overflow: hidden; width: 100%; + box-shadow: 0 30px 60px rgba(0,0,0,0.5); border: 1px solid var(--border-subtle); } -.poster-card img { width: 100%; height: 100%; object-fit: cover; } +.poster-card img { width: 100%; height: auto; display: block; aspect-ratio: 2/3; object-fit: cover; } -.info-grid { - background: var(--color-bg-elevated); border: var(--border-subtle); - border-radius: var(--radius-md); padding: 1.25rem; - display: flex; flex-direction: column; gap: 1rem; +/* Sidebar Extra Info (Synonyms) */ +.sidebar-extra-info { + background: rgba(255,255,255,0.03); padding: 1.5rem; + border-radius: 16px; border: 1px solid rgba(255,255,255,0.08); } -.info-item h4 { margin: 0 0 0.25rem 0; font-size: 0.8rem; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.5px; } -.info-item span { font-weight: 600; font-size: 0.95rem; } +.sidebar-label { + font-size: 0.8rem; color: #888; text-transform: uppercase; margin: 0 0 1rem 0; letter-spacing: 0.5px; +} +.synonyms-list { + list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; +} +.synonyms-list li { + font-size: 0.9rem; color: #ddd; line-height: 1.4; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 0.5rem; +} +.synonyms-list li:last-child { border-bottom: none; } -.main-content { - display: flex; flex-direction: column; - padding-top: 4rem; - justify-content: flex-start; -} -.book-header { margin-bottom: 1.5rem; } -.book-title { font-size: 3.5rem; font-weight: 900; line-height: 1.1; margin: 0 0 1rem 0; text-shadow: 0 4px 30px rgba(0,0,0,0.8); } +/* --- CONTENT COLUMN --- */ +.content-section { margin-top: 4rem; } +h2, .subsection-title { font-size: 1.8rem; font-weight: 800; margin-bottom: 1.5rem; color: white; } -.meta-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; } -.pill { padding: 0.4rem 1rem; background: rgba(255,255,255,0.1); border-radius: 99px; font-size: 0.9rem; font-weight: 600; border: var(--border-subtle); backdrop-filter: blur(10px); } -.pill.score { background: rgba(34, 197, 94, 0.2); color: #4ade80; border-color: rgba(34, 197, 94, 0.2); } +/* Chapters Section */ +.chapters-section { border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem; } +.chapters-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; } -#description { display: none; } -#year { display: none; } +/* Chapter Controls */ +.chapter-controls { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; } +.glass-select, .glass-input { + appearance: none; -webkit-appearance: none; + background-color: rgba(0, 0, 0, 0.4); + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='rgba(255,255,255,0.7)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; background-position: right 0.8rem center; background-size: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.9); + padding: 0.4rem 2rem 0.4rem 0.8rem; border-radius: 6px; font-size: 0.8rem; font-weight: 500; height: 34px; + outline: none; backdrop-filter: blur(4px); transition: all 0.2s ease; cursor: pointer; min-width: 110px; +} +.glass-input { background-image: none; padding: 0.4rem 0.8rem; width: 180px; } +.glass-input:focus { border-color: var(--color-primary); width: 220px; } +.glass-btn-icon { width: 34px; height: 34px; border-radius: 6px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; } +.glass-btn-icon:hover { background: var(--color-primary); border-color: var(--color-primary); } -.action-row { display: flex; gap: 1rem; } -.btn-primary { - padding: 0.8rem 2rem; background: white; color: black; border: none; border-radius: 99px; - font-weight: 800; cursor: pointer; transition: transform 0.2s; +/* Chapters Grid */ +.chapters-grid { display: flex; flex-direction: column; gap: 0.5rem; } +.chapter-item { + display: flex; align-items: center; justify-content: space-between; + background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05); + padding: 0.8rem 1.2rem; border-radius: 8px; + transition: all 0.2s ease; cursor: pointer; position: relative; overflow: hidden; } -.btn-primary:hover { transform: scale(1.05); } +.chapter-item:hover { background: rgba(255, 255, 255, 0.08); transform: translateX(5px); border-color: rgba(255, 255, 255, 0.2); } +.chapter-item::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--color-primary); opacity: 0; transition: opacity 0.2s; } +.chapter-info { display: flex; flex-direction: column; gap: 0.2rem; } +.chapter-number { font-size: 0.85rem; font-weight: 600; color: var(--color-text-secondary); text-transform: uppercase; } +.chapter-title { font-size: 1rem; font-weight: 500; color: white; } +.chapter-meta { display: flex; align-items: center; gap: 1rem; font-size: 0.85rem; color: #888; } +.lang-tag { font-size: 0.7rem; padding: 2px 6px; border-radius: 4px; background: rgba(255,255,255,0.1); text-transform: uppercase; color: #ccc; } -.btn-secondary { - padding: 0.8rem 2rem; - background: rgba(255,255,255,0.1); - color: white; - border: 1px solid rgba(255,255,255,0.2); - border-radius: 99px; - font-weight: 700; - cursor: pointer; - transition: 0.2s; - backdrop-filter: blur(10px); -} -.btn-secondary:hover { - background: rgba(255,255,255,0.2); -} -.btn-blur { - padding: 0.8rem 2rem; - background: rgba(255,255,255,0.1); - color: white; - border: 1px solid rgba(255,255,255,0.2); - border-radius: 99px; - font-weight: 700; - cursor: pointer; - transition: 0.2s; - backdrop-filter: blur(10px); +/* --- RELATIONS (Horizontal Cards) --- */ +.relations-horizontal { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } +.relation-card-horizontal { + display: flex; background: var(--bg-card); border: 1px solid var(--border-subtle); + border-radius: 10px; overflow: hidden; transition: 0.2s; cursor: pointer; } -.btn-blur:hover { background: rgba(255,255,255,0.2); } +.relation-card-horizontal:hover { background: rgba(255,255,255,0.08); transform: translateX(5px); } +.rel-img { width: 85px; height: 110px; object-fit: cover; } +.rel-info { padding: 1rem; display: flex; flex-direction: column; justify-content: center; } +.rel-type { + font-size: 0.7rem; color: var(--color-primary); font-weight: 800; + margin-bottom: 4px; background: rgba(139, 92, 246, 0.1); width: fit-content; padding: 2px 6px; border-radius: 4px; +} +.rel-title { font-size: 0.95rem; font-weight: 700; color: #eee; } -.chapters-section { margin-top: 1rem; } -.section-title { display: flex; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 0.8rem; margin-bottom: 1.5rem; } -.section-title h2 { font-size: 1.5rem; margin: 0; border-left: 4px solid var(--color-primary); padding-left: 1rem; } -.chapters-table-wrapper { - background: var(--color-bg-elevated); border-radius: var(--radius-md); - border: 1px solid rgba(255,255,255,0.05); overflow: hidden; -} -.chapters-table { width: 100%; border-collapse: collapse; text-align: left; } -.chapters-table th { - padding: 0.8rem 1.2rem; background: rgba(255,255,255,0.03); - color: var(--color-text-secondary); font-weight: 600; font-size: 0.85rem; - text-transform: uppercase; letter-spacing: 0.5px; -} -.chapters-table td { - padding: 1rem 1.2rem; border-bottom: 1px solid rgba(255,255,255,0.05); - color: var(--color-text-primary); font-size: 0.95rem; -} -.chapters-table tr:last-child td { border-bottom: none; } -.chapters-table tr:hover { background: var(--color-bg-elevated-hover); } +/* --- CHARACTERS (Grid Style) --- */ +.characters-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1rem; } +.character-item { display: flex; align-items: center; gap: 1rem; } +.char-avatar { width: 60px; height: 60px; border-radius: 10px; overflow: hidden; flex-shrink: 0; } +.char-avatar img { width: 100%; height: 100%; object-fit: cover; } +.char-info { display: flex; flex-direction: column; gap: 2px; } +.char-name { font-size: 1rem; font-weight: 700; color: #fff; } +.char-role { font-size: 0.8rem; color: #888; font-weight: 500; } -.filter-select { - appearance: none; - -webkit-appearance: none; - background-color: var(--color-bg-elevated); - color: var(--color-text-primary); - border: 1px solid rgba(255,255,255,0.1); - padding: 0.5rem 2rem 0.5rem 1rem; - border-radius: 99px; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - outline: none; - background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 1rem center; -} - -.filter-select:hover { - border-color: var(--color-primary); - background-color: var(--color-bg-elevated-hover); -} - -.filter-select option { - background-color: var(--color-bg-elevated); - color: var(--color-text-primary); -} - -.read-btn-small { - background: var(--color-primary); color: white; border: none; - padding: 0.4rem 0.9rem; border-radius: 6px; font-weight: 600; cursor: pointer; - font-size: 0.8rem; transition: 0.2s; -} -.read-btn-small:hover { background: #7c3aed; } - -.pagination-controls { - display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; align-items: center; -} -.page-btn { - background: var(--color-bg-elevated); border: 1px solid rgba(255,255,255,0.1); - color: white; padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; font-size: 0.9rem; -} +/* --- PAGINATION --- */ +.pagination-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 2rem; align-items: center; } +.page-btn { background: var(--color-bg-elevated); border: 1px solid rgba(255, 255, 255, 0.1); color: white; padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; font-size: 0.9rem; } .page-btn:disabled { opacity: 0.5; cursor: not-allowed; } .page-btn:hover:not(:disabled) { border-color: var(--color-primary); } -@keyframes slideUp { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } } @media (max-width: 1024px) { + .content-container { margin-top: -150px; padding: 0 1.5rem; } + .main-layout { grid-template-columns: 1fr; gap: 2rem; } + .poster-section { display: flex; flex-direction: column; align-items: center; } + .poster-card { width: 220px; } + .metadata-sidebar { width: 100%; max-width: 400px; } +} + +@media (max-width: 768px) { .hero-wrapper { height: 40vh; } - .content-container { grid-template-columns: 1fr; margin-top: -80px; padding: 0 1.5rem 4rem 1.5rem; } - .poster-card { display: none; } - - .main-content { padding-top: 0; align-items: center; text-align: center; } - .book-title { font-size: 2.2rem; } - .meta-row { justify-content: center; } - .action-row { justify-content: center; width: 100%; } - .btn-primary, .btn-blur { flex: 1; justify-content: center; } - - .sidebar { display: none; } - .chapters-table th:nth-child(3), .chapters-table td:nth-child(3) { display: none; } - .chapters-table th:nth-child(4), .chapters-table td:nth-child(4) { display: none; } + .book-title { font-size: 2.5rem; } + .action-row { flex-direction: column; width: 100%; } + .btn-read, .btn-add-list { width: 100%; justify-content: center; } + .chapters-header { flex-direction: column; align-items: flex-start; } + .chapter-controls { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } + .search-box { grid-column: span 2; } + .glass-input { width: 100%; } } \ No newline at end of file diff --git a/docker/src/api/books/books.controller.ts b/docker/src/api/books/books.controller.ts index d8f7a8a..04be2f4 100644 --- a/docker/src/api/books/books.controller.ts +++ b/docker/src/api/books/books.controller.ts @@ -101,12 +101,14 @@ export async function getChapterContent(req: any, reply: FastifyReply) { try { const { bookId, chapter, provider } = req.params; const source = req.query.source || 'anilist'; + const lang = req.query.lang || 'none'; const content = await booksService.getChapterContent( bookId, chapter, provider, - source + source, + lang ); return reply.send(content); diff --git a/docker/src/api/books/books.service.ts b/docker/src/api/books/books.service.ts index 6b3e611..255b9aa 100644 --- a/docker/src/api/books/books.service.ts +++ b/docker/src/api/books/books.service.ts @@ -67,7 +67,19 @@ export async function getBookById(id: string | number): Promise { +export async function getChapterContent(bookId: string, chapterId: string, providerName: string, source: string, lang: string): Promise { const extensions = getAllExtensions(); const ext = extensions.get(providerName); @@ -471,14 +484,14 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr throw new Error("Provider not found"); } - const contentCacheKey = `content:${providerName}:${source}:${bookId}:${chapterIndex}`; + const contentCacheKey = `content:${providerName}:${source}:${lang}:${bookId}:${chapterId}`; const cachedContent = await getCache(contentCacheKey); if (cachedContent) { const isExpired = Date.now() - cachedContent.created_at > CACHE_TTL_MS; if (!isExpired) { - console.log(`[${providerName}] Content cache hit for Book ID ${bookId}, Index ${chapterIndex}`); + console.log(`[${providerName}] Content cache hit for Book ID ${bookId}, Index ${chapterId}`); try { return JSON.parse(cachedContent.result) as ChapterContent; } catch (e) { @@ -486,33 +499,14 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr } } else { - console.log(`[${providerName}] Content cache expired for Book ID ${bookId}, Index ${chapterIndex}`); + console.log(`[${providerName}] Content cache expired for Book ID ${bookId}, Index ${chapterId}`); } } - const isExternal = source !== 'anilist'; - const chapterList = await getChaptersForBook(bookId, isExternal, providerName); - - if (!chapterList?.chapters || chapterList.chapters.length === 0) { - throw new Error("Chapters not found"); - } - - const providerChapters = chapterList.chapters.filter(c => c.provider === providerName); - const index = parseInt(chapterIndex, 10); - - if (Number.isNaN(index)) { - throw new Error("Invalid chapter index"); - } - - if (!providerChapters[index]) { - throw new Error("Chapter index out of range"); - } - - const selectedChapter = providerChapters[index]; - - const chapterId = selectedChapter.id; - const chapterTitle = selectedChapter.title || null; - const chapterNumber = typeof selectedChapter.number === 'number' ? selectedChapter.number : index; + const selectedChapter: any = { + id: chapterId, + provider: providerName + }; try { if (!ext.findChapterPages) { @@ -522,12 +516,13 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr let contentResult: ChapterContent; if (ext.mediaType === "manga") { + // Usamos el ID directamente const pages = await ext.findChapterPages(chapterId); contentResult = { type: "manga", - chapterId, - title: chapterTitle, - number: chapterNumber, + chapterId: selectedChapter.id, + title: selectedChapter.title, + number: selectedChapter.number, provider: providerName, pages }; @@ -535,9 +530,9 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr const content = await ext.findChapterPages(chapterId); contentResult = { type: "ln", - chapterId, - title: chapterTitle, - number: chapterNumber, + chapterId: selectedChapter.id, + title: selectedChapter.title, + number: selectedChapter.number, provider: providerName, content }; @@ -546,7 +541,6 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr } await setCache(contentCacheKey, contentResult, CACHE_TTL_MS); - return contentResult; } catch (err) { diff --git a/docker/src/api/types.ts b/docker/src/api/types.ts index 57d804b..d369c19 100644 --- a/docker/src/api/types.ts +++ b/docker/src/api/types.ts @@ -79,6 +79,7 @@ export interface Episode { } export interface Chapter { + language?: string | null; index: number; id: string; number: string | number; diff --git a/docker/src/scripts/books/book.js b/docker/src/scripts/books/book.js index 73a9643..6ac1337 100644 --- a/docker/src/scripts/books/book.js +++ b/docker/src/scripts/books/book.js @@ -5,25 +5,367 @@ let bookSlug = null; let allChapters = []; let filteredChapters = []; - let availableExtensions = []; let isLocal = false; + +let currentLanguage = null; +let uniqueLanguages = []; +let isSortAscending = true; + const chapterPagination = Object.create(PaginationManager); -chapterPagination.init(12, () => renderChapterTable()); +chapterPagination.init(6, () => renderChapterList()); document.addEventListener('DOMContentLoaded', () => { init(); setupModalClickOutside(); + document.getElementById('sort-btn')?.addEventListener('click', toggleSortOrder); }); +async function init() { + try { + const urlData = URLUtils.parseEntityPath('book'); + if (!urlData) { showError("Book Not Found"); return; } + + extensionName = urlData.extensionName; + bookId = urlData.entityId; + bookSlug = urlData.slug; + + await loadBookMetadata(); + await checkLocalLibraryEntry(); + await loadAvailableExtensions(); + await loadChapters(); + await setupAddToListButton(); + + } catch (err) { + console.error("Init Error:", err); + showError("Error loading book"); + } +} + +async function loadBookMetadata() { + const source = extensionName || 'anilist'; + const fetchUrl = `/api/book/${bookId}?source=${source}`; + + try { + const res = await fetch(fetchUrl); + const data = await res.json(); + + if (data.error || !data) { showError("Book Not Found"); return; } + + const raw = Array.isArray(data) ? data[0] : data; + bookData = raw; + + const metadata = MediaMetadataUtils.formatBookData(raw, !!extensionName); + bookData.entry_type = metadata.format === 'MANGA' ? 'MANGA' : 'NOVEL'; + + updatePageTitle(metadata.title); + updateMetadata(metadata, raw); + updateExtensionPill(); + + } catch (e) { + console.error(e); + showError("Error loading metadata"); + } +} + +function updatePageTitle(title) { + document.title = `${title} | WaifuBoard Books`; + const titleEl = document.getElementById('title'); + if (titleEl) titleEl.innerText = title; +} + +function updateMetadata(metadata, rawData) { + // 1. Cabecera (Score, Año, Status, Formato, Caps) + const elements = { + 'description': metadata.description, + 'published-date': metadata.year, + 'status': metadata.status, + 'format': metadata.format, + 'chapters-count': metadata.chapters ? `${metadata.chapters} Ch` : '?? Ch', + 'genres': metadata.genres ? metadata.genres.replace(/,/g, ' • ') : '', + 'poster': metadata.poster, + 'hero-bg': metadata.banner + }; + + if(document.getElementById('description')) document.getElementById('description').innerHTML = metadata.description; + if(document.getElementById('poster')) document.getElementById('poster').src = metadata.poster; + if(document.getElementById('hero-bg')) document.getElementById('hero-bg').src = metadata.banner; + + ['published-date','status','format','chapters-count','genres'].forEach(id => { + const el = document.getElementById(id); + if(el) el.innerText = elements[id]; + }); + + const scoreEl = document.getElementById('score'); + if (scoreEl) scoreEl.innerText = extensionName ? `${metadata.score}` : `${metadata.score}% Score`; + + // 2. Sidebar: Sinónimos (Para llenar espacio vacío) + if (rawData.synonyms && rawData.synonyms.length > 0) { + const sidebarInfo = document.getElementById('sidebar-info'); + const list = document.getElementById('synonyms-list'); + if (sidebarInfo && list) { + sidebarInfo.style.display = 'block'; + list.innerHTML = ''; + // Mostrar máx 5 sinónimos para no alargar demasiado + rawData.synonyms.slice(0, 5).forEach(syn => { + const li = document.createElement('li'); + li.innerText = syn; + list.appendChild(li); + }); + } + } + + // 3. Renderizar Personajes + if (rawData.characters && rawData.characters.nodes && rawData.characters.nodes.length > 0) { + renderCharacters(rawData.characters.nodes); + } + + // 4. Renderizar Relaciones + if (rawData.relations && rawData.relations.edges && rawData.relations.edges.length > 0) { + renderRelations(rawData.relations.edges); + } +} + +function renderCharacters(nodes) { + const container = document.getElementById('characters-list'); + if(!container) return; + container.innerHTML = ''; + + nodes.forEach(char => { + const el = document.createElement('div'); + el.className = 'character-item'; + + const img = char.image?.large || char.image?.medium || '/public/assets/no-image.png'; + const name = char.name?.full || 'Unknown'; + const role = char.role || 'Supporting'; + + el.innerHTML = ` +
+
+
${name}
+
${role}
+
+ `; + container.appendChild(el); + }); +} + +function renderRelations(edges) { + const container = document.getElementById('relations-list'); + const section = document.getElementById('relations-section'); + if(!container || !section) return; + + if (!edges || edges.length === 0) { + section.style.display = 'none'; + return; + } + + section.style.display = 'block'; + container.innerHTML = ''; + + edges.forEach(edge => { + const node = edge.node; + if (!node) return; + + const el = document.createElement('div'); + el.className = 'relation-card-horizontal'; + + const img = node.coverImage?.large || node.coverImage?.medium || '/public/assets/no-image.png'; + const title = node.title?.romaji || node.title?.english || node.title?.native || 'Unknown'; + const type = edge.relationType ? edge.relationType.replace(/_/g, ' ') : 'Related'; + + el.innerHTML = ` + ${title} +
+ ${type} + ${title} +
+ `; + + el.onclick = () => { + const targetType = node.type === 'ANIME' ? 'anime' : 'book'; + window.location.href = `/${targetType}/${node.id}`; + }; + + container.appendChild(el); + }); +} + +function processChaptersData(chaptersData) { + allChapters = chaptersData; + const langSet = new Set(allChapters.map(ch => ch.language).filter(l => l)); + uniqueLanguages = Array.from(langSet); + setupLanguageSelector(); + filterAndRenderChapters(); + setupReadButton(); +} + +async function loadChapters(targetProvider = null) { + const listContainer = document.getElementById('chapters-list'); + const loadingMsg = document.getElementById('loading-msg'); + + if(listContainer) listContainer.innerHTML = ''; + if(loadingMsg) loadingMsg.style.display = 'block'; + + if (!targetProvider) { + const select = document.getElementById('provider-filter'); + targetProvider = select ? select.value : (availableExtensions[0] || 'all'); + } + + try { + let fetchUrl; + let isLocalRequest = targetProvider === 'local'; + + if (isLocalRequest) { + fetchUrl = `/api/library/${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(); + + if(loadingMsg) loadingMsg.style.display = 'none'; + + let rawData = []; + if (isLocalRequest) { + rawData = (data.units || []).map((unit, idx) => ({ + id: unit.id || idx, number: unit.number, title: unit.name, + provider: 'local', index: idx, format: unit.format, language: 'local' + })); + } else { + rawData = data.chapters || []; + } + processChaptersData(rawData); + + } catch (err) { + if(loadingMsg) loadingMsg.style.display = 'none'; + if(listContainer) listContainer.innerHTML = '
Error loading chapters.
'; + console.error(err); + } +} + +function setupLanguageSelector() { + const selectorContainer = document.getElementById('language-selector-container'); + const select = document.getElementById('language-select'); + if (!selectorContainer || !select) return; + + if (uniqueLanguages.length <= 1) { + selectorContainer.classList.add('hidden'); + currentLanguage = uniqueLanguages[0] || null; + return; + } + selectorContainer.classList.remove('hidden'); + select.innerHTML = ''; + + const langNames = { 'es': 'Español', 'es-419': 'Latino', 'en': 'English', 'pt-br': 'Português', 'ja': '日本語' }; + + uniqueLanguages.forEach(lang => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = langNames[lang] || lang.toUpperCase(); + select.appendChild(option); + }); + + if (uniqueLanguages.includes('es-419')) currentLanguage = 'es-419'; + else if (uniqueLanguages.includes('es')) currentLanguage = 'es'; + else currentLanguage = uniqueLanguages[0]; + select.value = currentLanguage; + + select.onchange = (e) => { + currentLanguage = e.target.value; + chapterPagination.currentPage = 1; + filterAndRenderChapters(); + }; +} + +function filterAndRenderChapters() { + let tempChapters = [...allChapters]; + if (currentLanguage && uniqueLanguages.length > 1) { + tempChapters = tempChapters.filter(ch => ch.language === currentLanguage); + } + const searchQuery = document.getElementById('chapter-search')?.value.toLowerCase(); + if(searchQuery){ + tempChapters = tempChapters.filter(ch => + (ch.title && ch.title.toLowerCase().includes(searchQuery)) || + (ch.number && ch.number.toString().includes(searchQuery)) + ); + } + tempChapters.sort((a, b) => { + const numA = parseFloat(a.number) || 0; + const numB = parseFloat(b.number) || 0; + return isSortAscending ? numA - numB : numB - numA; + }); + filteredChapters = tempChapters; + chapterPagination.setTotalItems(filteredChapters.length); + renderChapterList(); +} + +function renderChapterList() { + const container = document.getElementById('chapters-list'); + if(!container) return; + container.innerHTML = ''; + const itemsToShow = chapterPagination.getCurrentPageItems(filteredChapters); + + if (itemsToShow.length === 0) { + container.innerHTML = '
No chapters found.
'; + return; + } + + itemsToShow.forEach(chapter => { + const el = document.createElement('div'); + el.className = 'chapter-item'; + el.onclick = () => openReader(chapter.id, chapter.provider); + + const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : ''; + const providerLabel = chapter.provider !== 'local' ? chapter.provider : ''; + + el.innerHTML = ` +
+ Chapter ${chapter.number} + ${chapter.title || ''} +
+
+ ${providerLabel ? `${providerLabel}` : ''} + ${dateStr ? `${dateStr}` : ''} + +
+ `; + container.appendChild(el); + }); + chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page'); +} + +function toggleSortOrder() { + isSortAscending = !isSortAscending; + const btn = document.getElementById('sort-btn'); + if(btn) btn.style.transform = isSortAscending ? 'rotate(180deg)' : 'rotate(0deg)'; + filterAndRenderChapters(); +} + +function setupReadButton() { + const readBtn = document.getElementById('read-start-btn'); + if (!readBtn || allChapters.length === 0) return; + const firstChapter = [...allChapters].sort((a,b) => a.index - b.index)[0]; + if (firstChapter) readBtn.onclick = () => + openReader(firstChapter.index ?? firstChapter.id, firstChapter.provider); + +} + +function openReader(chapterIndexOrId, provider) { + const lang = currentLanguage ?? 'none'; + window.location.href = + URLUtils.buildReadUrl(bookId, chapterIndexOrId, provider, extensionName || 'anilist') + + `?lang=${lang}`; +} + async function checkLocalLibraryEntry() { try { - const libraryType = - bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; - + const libraryType = bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; const res = await fetch(`/api/library/${libraryType}/${bookId}`); if (!res.ok) return; - const data = await res.json(); if (data.matched) { isLocal = true; @@ -36,34 +378,7 @@ async function checkLocalLibraryEntry() { pill.style.borderColor = 'rgba(34, 197, 94, 0.3)'; } } - } catch (e) { - console.error("Error checking local status:", e); - } -} - -async function init() { - try { - const urlData = URLUtils.parseEntityPath('book'); - if (!urlData) { - showError("Book Not Found"); - return; - } - - extensionName = urlData.extensionName; - bookId = urlData.entityId; - bookSlug = urlData.slug; - await loadBookMetadata(); - await checkLocalLibraryEntry(); - - await loadAvailableExtensions(); - await loadChapters(); - - await setupAddToListButton(); - - } catch (err) { - console.error("Metadata Error:", err); - showError("Error loading book"); - } + } catch (e) { console.error("Error checking local:", e); } } async function loadAvailableExtensions() { @@ -71,220 +386,21 @@ async function loadAvailableExtensions() { const res = await fetch('/api/extensions/book'); const data = await res.json(); availableExtensions = data.extensions || []; - setupProviderFilter(); - } catch (err) { - console.error("Error fetching extensions:", err); - } -} - -async function loadBookMetadata() { - const source = extensionName || 'anilist'; - const fetchUrl = `/api/book/${bookId}?source=${source}`; - - const res = await fetch(fetchUrl); - const data = await res.json(); - - if (data.error || !data) { - showError("Book Not Found"); - return; - } - - const raw = Array.isArray(data) ? data[0] : data; - bookData = raw; - - const metadata = MediaMetadataUtils.formatBookData(raw, !!extensionName); - bookData.entry_type = - metadata.format === 'MANGA' ? 'MANGA' : 'NOVEL'; - updatePageTitle(metadata.title); - updateMetadata(metadata); - updateExtensionPill(); -} - -function updatePageTitle(title) { - document.title = `${title} | WaifuBoard Books`; - const titleEl = document.getElementById('title'); - if (titleEl) titleEl.innerText = title; -} - -function updateMetadata(metadata) { - - const descEl = document.getElementById('description'); - if (descEl) descEl.innerHTML = metadata.description; - - const scoreEl = document.getElementById('score'); - if (scoreEl) { - scoreEl.innerText = extensionName - ? `${metadata.score}` - : `${metadata.score}% Score`; - } - - const pubEl = document.getElementById('published-date'); - if (pubEl) pubEl.innerText = metadata.year; - - const statusEl = document.getElementById('status'); - if (statusEl) statusEl.innerText = metadata.status; - - const formatEl = document.getElementById('format'); - if (formatEl) formatEl.innerText = metadata.format; - - const chaptersEl = document.getElementById('chapters'); - if (chaptersEl) chaptersEl.innerText = metadata.chapters; - - const genresEl = document.getElementById('genres'); - if (genresEl) genresEl.innerText = metadata.genres; - - const posterEl = document.getElementById('poster'); - if (posterEl) posterEl.src = metadata.poster; - - const heroBgEl = document.getElementById('hero-bg'); - if (heroBgEl) heroBgEl.src = metadata.banner; -} - -function updateExtensionPill() { - const pill = document.getElementById('extension-pill'); - if (!pill) return; - - if (extensionName) { - pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase(); - pill.style.display = 'inline-flex'; - } else { - pill.style.display = 'none'; - } -} - -async function setupAddToListButton() { - const btn = document.getElementById('add-to-list-btn'); - if (!btn || !bookData) return; - - ListModalManager.currentData = bookData; - const entryType = ListModalManager.getEntryType(bookData); - const idForCheck = extensionName ? bookSlug : bookId; - - await ListModalManager.checkIfInList( - idForCheck, - extensionName || 'anilist', - entryType - ); - - updateCustomAddButton(); - - btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist'); -} - -function updateCustomAddButton() { - const btn = document.getElementById('add-to-list-btn'); - if (!btn) return; - - if (ListModalManager.isInList) { - btn.innerHTML = ` - - - - In Your Library - `; - btn.style.background = 'rgba(34, 197, 94, 0.2)'; - btn.style.color = '#22c55e'; - btn.style.borderColor = 'rgba(34, 197, 94, 0.3)'; - } else { - btn.innerHTML = '+ Add to Library'; - btn.style.background = null; - btn.style.color = null; - btn.style.borderColor = null; - } -} - -async function loadChapters(targetProvider = null) { - const tbody = document.getElementById('chapters-body'); - if (!tbody) return; - - if (!targetProvider) { - const select = document.getElementById('provider-filter'); - targetProvider = select ? select.value : (availableExtensions[0] || 'all'); - } - - tbody.innerHTML = 'Loading chapters...'; - - try { - let fetchUrl; - let isLocalRequest = targetProvider === 'local'; - - if (isLocalRequest) { - // Nuevo endpoint para archivos locales - fetchUrl = `/api/library/${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(); - - // 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 = 'No chapters found.'; - if (totalEl) totalEl.innerText = "0 Found"; - return; - } - - if (totalEl) totalEl.innerText = `${allChapters.length} Found`; - - setupReadButton(); - chapterPagination.setTotalItems(filteredChapters.length); - chapterPagination.reset(); - renderChapterTable(); - - } catch (err) { - tbody.innerHTML = 'Error loading chapters.'; - console.error(err); - } -} - -function applyChapterFilter() { - const chapterParam = URLUtils.getQueryParam('chapter'); - if (!chapterParam) return; - - const chapterNumber = parseFloat(chapterParam); - if (isNaN(chapterNumber)) return; - - filteredChapters = allChapters.filter( - ch => parseFloat(ch.number) === chapterNumber - ); - - chapterPagination.reset(); + } catch (err) { console.error(err); } } function setupProviderFilter() { const select = document.getElementById('provider-filter'); 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)'; + allOpt.innerText = 'All Providers'; 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'; @@ -292,7 +408,6 @@ function setupProviderFilter() { select.appendChild(localOpt); } - // Añadir extensiones normales availableExtensions.forEach(ext => { const opt = document.createElement('option'); opt.value = ext; @@ -300,90 +415,43 @@ function setupProviderFilter() { select.appendChild(opt); }); - // 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]; + if (isLocal) select.value = 'local'; + else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName; + else if (availableExtensions.length > 0) select.value = availableExtensions[0]; + + select.onchange = () => loadChapters(select.value); +} + +function updateExtensionPill() { + const pill = document.getElementById('extension-pill'); + if(pill && extensionName) { pill.innerText = extensionName; pill.style.display = 'inline-flex'; } +} + +async function setupAddToListButton() { + const btn = document.getElementById('add-to-list-btn'); + if (!btn || !bookData) return; + ListModalManager.currentData = bookData; + const entryType = ListModalManager.getEntryType(bookData); + const idForCheck = extensionName ? bookSlug : bookId; + await ListModalManager.checkIfInList(idForCheck, extensionName || 'anilist', entryType); + updateCustomAddButton(); + btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist'); +} + +function updateCustomAddButton() { + const btn = document.getElementById('add-to-list-btn'); + if(btn && ListModalManager.isInList) { + btn.innerHTML = '✓ In Your List'; btn.style.background = 'rgba(34, 197, 94, 0.2)'; btn.style.color = '#22c55e'; btn.style.borderColor = '#22c55e'; } - - select.onchange = () => { - loadChapters(select.value); - }; -} - -function setupReadButton() { - const readBtn = document.getElementById('read-start-btn'); - if (!readBtn || filteredChapters.length === 0) return; - - const firstChapter = filteredChapters[0]; - readBtn.onclick = () => openReader(0, firstChapter.provider); -} - -function renderChapterTable() { - const tbody = document.getElementById('chapters-body'); - if (!tbody) return; - - tbody.innerHTML = ''; - - if (filteredChapters.length === 0) { - tbody.innerHTML = 'No chapters match this filter.'; - chapterPagination.renderControls( - 'pagination', - 'page-info', - 'prev-page', - 'next-page' - ); - return; - } - - const pageItems = chapterPagination.getCurrentPageItems(filteredChapters); - - pageItems.forEach((ch) => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${ch.number} - ${ch.title || 'Chapter ' + ch.number} - ${ch.provider} - - - - `; - tbody.appendChild(row); - }); - - chapterPagination.renderControls( - 'pagination', - 'page-info', - 'prev-page', - 'next-page' - ); -} - -function openReader(chapterId, provider) { - 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() { const modal = document.getElementById('add-list-modal'); - if (!modal) return; - - modal.addEventListener('click', (e) => { - if (e.target.id === 'add-list-modal') { - ListModalManager.close(); - } - }); + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target.id === 'add-list-modal') ListModalManager.close(); + }); + } } function showError(message) { @@ -391,21 +459,15 @@ function showError(message) { if (titleEl) titleEl.innerText = message; } -function saveToList() { +// Exports +window.openReader = openReader; +window.saveToList = () => { const idToSave = extensionName ? bookSlug : bookId; ListModalManager.save(idToSave, extensionName || 'anilist'); -} - -function deleteFromList() { +}; +window.deleteFromList = () => { const idToDelete = extensionName ? bookSlug : bookId; ListModalManager.delete(idToDelete, extensionName || 'anilist'); -} - -function closeAddToListModal() { - ListModalManager.close(); -} - -window.openReader = openReader; -window.saveToList = saveToList; -window.deleteFromList = deleteFromList; -window.closeAddToListModal = closeAddToListModal; \ No newline at end of file +}; +window.closeAddToListModal = () => ListModalManager.close(); +window.openAddToListModal = () => ListModalManager.open(bookData, extensionName || 'anilist'); \ No newline at end of file diff --git a/docker/src/scripts/books/reader.js b/docker/src/scripts/books/reader.js index 77e5ae1..f7a5645 100644 --- a/docker/src/scripts/books/reader.js +++ b/docker/src/scripts/books/reader.js @@ -1,3 +1,7 @@ +// reader.js refactorizado + +const urlParams = new URLSearchParams(window.location.search); +const lang = urlParams.get('lang') ?? 'none'; const reader = document.getElementById('reader'); const panel = document.getElementById('settings-panel'); const overlay = document.getElementById('overlay'); @@ -33,11 +37,12 @@ let currentType = null; let currentPages = []; let observer = null; +// === CAMBIO: Parseo de URL para obtener ID === const parts = window.location.pathname.split('/'); - const bookId = parts[4]; -let chapter = parts[3]; +let currentChapterId = parts[3]; // Ahora es un ID (string) let provider = parts[2]; +let chaptersList = []; // Buffer para guardar el orden de capítulos function loadConfig() { try { @@ -116,6 +121,31 @@ function updateSettingsVisibility() { mangaSettings.classList.toggle('hidden', currentType !== 'manga'); } +// === CAMBIO: Nueva función para traer la lista de capítulos y saber el orden === +async function fetchChapterList() { + const urlParams = new URLSearchParams(window.location.search); + const source = urlParams.get('source') || 'anilist'; + + try { + // Reusamos el endpoint que lista capítulos + const res = await fetch(`/api/book/${bookId}/chapters?source=${source}&provider=${provider}`); + const data = await res.json(); + + // Ordenamos por número para asegurar navegación correcta + let list = data.chapters || []; + list.sort((a, b) => Number(a.number) - Number(b.number)); + + // Si hay filtro de idioma en la URL, filtramos la navegación también + if (lang !== 'none') { + list = list.filter(c => c.language === lang); + } + + chaptersList = list; + } catch (e) { + console.error("Error fetching chapter list:", e); + } +} + async function loadChapter() { reader.innerHTML = `
@@ -126,23 +156,35 @@ async function loadChapter() { const urlParams = new URLSearchParams(window.location.search); let source = urlParams.get('source'); - if (!source) { - source = 'anilist'; + if (!source) source = 'anilist'; + + // === CAMBIO: Si no tenemos la lista de capítulos (y no es local), la pedimos === + if (provider !== 'local' && chaptersList.length === 0) { + await fetchChapterList(); } + let newEndpoint; if (provider === 'local') { newEndpoint = `/api/library/${bookId}/units`; } else { - newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`; + // === CAMBIO: Usamos currentChapterId en la URL === + newEndpoint = `/api/book/${bookId}/${currentChapterId}/${provider}?source=${source}&lang=${lang}`; } try { const res = await fetch(newEndpoint); const data = await res.json(); + + // Lógica específica para contenido LOCAL if (provider === 'local') { - const unit = data.units[Number(chapter)]; - if (!unit) return; + const unitIndex = Number(currentChapterId); // En local el ID suele ser el índice + const unit = data.units[unitIndex]; + + if (!unit) { + reader.innerHTML = '
Chapter not found (Local)
'; + return; + } chapterLabel.textContent = unit.name; document.title = unit.name; @@ -152,50 +194,46 @@ async function loadChapter() { reader.innerHTML = ''; - // ===== MANGA ===== + // Setup navegación manual para local (simple index +/- 1) + setupLocalNavigation(unitIndex, data.units.length); + if (manifest.type === 'manga') { currentType = 'manga'; updateSettingsVisibility(); applyStyles(); - currentPages = manifest.pages; loadManga(currentPages); return; } - - // ===== LN ===== if (manifest.type === 'ln') { currentType = 'ln'; updateSettingsVisibility(); applyStyles(); - const contentRes = await fetch(manifest.url); const html = await contentRes.text(); - loadLN(html); return; } } + // Lógica para Extensiones / Anilist + if (data.error) { + reader.innerHTML = `
Error: ${data.error}
`; + return; + } if (data.title) { chapterLabel.textContent = data.title; document.title = data.title; } else { - chapterLabel.textContent = `Chapter ${chapter}`; - document.title = `Chapter ${chapter}`; + chapterLabel.textContent = `Chapter ${data.number ?? currentChapterId}`; + document.title = `Chapter ${data.number ?? currentChapterId}`; } setupProgressTracking(data, source); - if (data.error) { - reader.innerHTML = ` -
- Error: ${data.error} -
- `; - return; - } + // === CAMBIO: Actualizar botones basado en IDs === + updateNavigationButtons(); currentType = data.type; updateSettingsVisibility(); @@ -209,6 +247,7 @@ async function loadChapter() { loadLN(data.content); } } catch (error) { + console.error(error); reader.innerHTML = `
Error loading chapter: ${error.message} @@ -217,6 +256,87 @@ async function loadChapter() { } } +// === CAMBIO: Lógica de navegación basada en IDs === +function updateNavigationButtons() { + if (provider === 'local') return; // Se maneja aparte + + // Buscamos el índice actual en la lista completa + const currentIndex = chaptersList.findIndex(c => String(c.id) === String(currentChapterId)); + + if (currentIndex === -1) { + console.warn("Current chapter not found in list, navigation disabled"); + prevBtn.disabled = true; + nextBtn.disabled = true; + prevBtn.style.opacity = 0.5; + nextBtn.style.opacity = 0.5; + return; + } + + // Configurar botón ANTERIOR + if (currentIndex > 0) { + const prevId = chaptersList[currentIndex - 1].id; + prevBtn.onclick = () => changeChapter(prevId); + prevBtn.disabled = false; + prevBtn.style.opacity = 1; + } else { + prevBtn.onclick = null; + prevBtn.disabled = true; + prevBtn.style.opacity = 0.5; + } + + // Configurar botón SIGUIENTE + if (currentIndex < chaptersList.length - 1) { + const nextId = chaptersList[currentIndex + 1].id; + nextBtn.onclick = () => changeChapter(nextId); + nextBtn.disabled = false; + nextBtn.style.opacity = 1; + } else { + nextBtn.onclick = null; + nextBtn.disabled = true; + nextBtn.style.opacity = 0.5; + } +} + +// Fallback para navegación local (basada en índices) +function setupLocalNavigation(currentIndex, totalUnits) { + if (currentIndex > 0) { + prevBtn.onclick = () => changeChapter(currentIndex - 1); + prevBtn.disabled = false; + prevBtn.style.opacity = 1; + } else { + prevBtn.disabled = true; + prevBtn.style.opacity = 0.5; + } + + if (currentIndex < totalUnits - 1) { + nextBtn.onclick = () => changeChapter(currentIndex + 1); + nextBtn.disabled = false; + nextBtn.style.opacity = 1; + } else { + nextBtn.disabled = true; + nextBtn.style.opacity = 0.5; + } +} + +// === CAMBIO: Función helper para cambiar de capítulo === +function changeChapter(newId) { + currentChapterId = newId; + updateURL(newId); + window.scrollTo(0, 0); + loadChapter(); +} + +function updateURL(newId) { + const urlParams = new URLSearchParams(window.location.search); + const source = urlParams.get('source') ?? 'anilist'; + + // La URL ahora contiene el ID en lugar del número/índice + const newUrl = `/read/${provider}/${newId}/${bookId}?source=${source}&lang=${lang}`; + window.history.pushState({}, '', newUrl); +} + +// --- Resto de funciones UI (Manga/LN loading) sin cambios lógicos mayores --- + function loadManga(pages) { if (!pages || pages.length === 0) { reader.innerHTML = '
No pages found
'; @@ -227,7 +347,6 @@ function loadManga(pages) { container.className = 'manga-container'; let isLongStrip = false; - if (config.manga.mode === 'longstrip') { isLongStrip = true; } else if (config.manga.mode === 'auto' && detectLongStrip(pages)) { @@ -250,15 +369,12 @@ function loadManga(pages) { function shouldUseDoublePage(pages) { if (pages.length <= 5) return false; - const widePages = pages.filter(p => { if (!p.height || !p.width) return false; const ratio = p.width / p.height; return ratio > 1.3; }); - if (widePages.length > pages.length * 0.3) return false; - return true; } @@ -298,7 +414,6 @@ function loadDoublePage(container, pages) { i++; } else { const rightPage = createImageElement(nextPage, i + 1); - if (config.manga.direction === 'rtl') { doubleContainer.appendChild(rightPage); doubleContainer.appendChild(leftPage); @@ -306,7 +421,6 @@ function loadDoublePage(container, pages) { doubleContainer.appendChild(leftPage); doubleContainer.appendChild(rightPage); } - container.appendChild(doubleContainer); i += 2; } @@ -349,31 +463,23 @@ function createImageElement(page, index) { img.dataset.src = url; img.loading = 'lazy'; } - img.alt = `Page ${index + 1}`; - return img; } function buildProxyUrl(url, headers = {}) { const params = new URLSearchParams({ url }); - if (headers.Referer || headers.referer) params.append("referer", headers.Referer || headers.referer); - if (headers["User-Agent"] || headers["user-agent"]) params.append("userAgent", headers["User-Agent"] || headers["user-agent"]); - if (headers.Origin || headers.origin) params.append("origin", headers.Origin || headers.origin); - return `/api/proxy?${params.toString()}`; } - function detectLongStrip(pages) { if (!pages || pages.length === 0) return false; - const relevant = pages.slice(1); const tall = relevant.filter(p => p.height && p.width && (p.height / p.width) > 2); return tall.length >= 2 || (tall.length / relevant.length) > 0.3; @@ -381,7 +487,6 @@ function detectLongStrip(pages) { function setupLazyLoading() { if (observer) observer.disconnect(); - observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { @@ -393,10 +498,7 @@ function setupLazyLoading() { } } }); - }, { - rootMargin: '200px' - }); - + }, { rootMargin: '200px' }); document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img)); } @@ -407,161 +509,102 @@ function loadLN(html) { reader.appendChild(div); } +// Listeners de configuración 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(); + 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(); + 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(); + applyStyles(); saveConfig(); }); - document.getElementById('font-family').addEventListener('change', (e) => { config.ln.fontFamily = e.target.value; - applyStyles(); - saveConfig(); + applyStyles(); saveConfig(); }); - document.getElementById('text-color').addEventListener('change', (e) => { config.ln.textColor = e.target.value; - applyStyles(); - saveConfig(); + applyStyles(); saveConfig(); }); - document.getElementById('bg-color').addEventListener('change', (e) => { config.ln.bg = e.target.value; - applyStyles(); - saveConfig(); + applyStyles(); saveConfig(); }); - 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(); + applyStyles(); saveConfig(); }); }); - 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(); + applyStyles(); saveConfig(); } }); }); - document.getElementById('display-mode').addEventListener('change', (e) => { config.manga.mode = e.target.value; - saveConfig(); - loadChapter(); + saveConfig(); loadChapter(); }); - document.getElementById('image-fit').addEventListener('change', (e) => { config.manga.imageFit = e.target.value; - saveConfig(); - loadChapter(); + saveConfig(); loadChapter(); }); - 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(); + applyStyles(); saveConfig(); }); - document.getElementById('preload-count').addEventListener('change', (e) => { config.manga.preloadCount = parseInt(e.target.value); saveConfig(); }); - 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(); + saveConfig(); loadChapter(); }); }); -prevBtn.addEventListener('click', () => { - const current = parseInt(chapter); - if (current <= 0) return; - - const newChapter = String(current - 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 urlParams = new URLSearchParams(window.location.search); - let source = urlParams.get('source'); - - let src; - if (source === 'anilist') { - src= "?source=anilist" - } else { - src= `?source=${source}` - } - const newUrl = `/read/${provider}/${chapter}/${bookId}${src}`; - window.history.pushState({}, '', newUrl); -} - +// Botón "Atrás" document.getElementById('back-btn').addEventListener('click', () => { - const parts = window.location.pathname.split('/'); - const mangaId = parts[4]; - const urlParams = new URLSearchParams(window.location.search); - let source = urlParams.get('source'); + let source = urlParams.get('source')?.split('?')[0]; - if (source === 'anilist') { - window.location.href = `/book/${mangaId}`; + if (source === 'anilist' || !source) { + window.location.href = `/book/${bookId}`; } else { - window.location.href = `/book/${source}/${mangaId}`; + window.location.href = `/book/${source}/${bookId}`; } }); +// Panel de configuración settingsBtn.addEventListener('click', () => { panel.classList.add('open'); overlay.classList.add('active'); }); - closePanel.addEventListener('click', closeSettings); overlay.addEventListener('click', closeSettings); @@ -569,7 +612,6 @@ function closeSettings() { panel.classList.remove('open'); overlay.classList.remove('active'); } - document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && panel.classList.contains('open')) { closeSettings(); @@ -579,37 +621,24 @@ document.addEventListener('keydown', (e) => { function enableMangaPageNavigation() { if (currentType !== 'manga') return; const logicalPages = []; - document.querySelectorAll('.manga-container > *').forEach(el => { - if (el.classList.contains('double-container')) { - logicalPages.push(el); - } else if (el.tagName === 'IMG') { + if (el.classList.contains('double-container') || el.tagName === 'IMG') { logicalPages.push(el); } }); - if (logicalPages.length === 0) return; function scrollToLogical(index) { if (index < 0 || index >= logicalPages.length) return; - const topBar = document.querySelector('.top-bar'); const offset = topBar ? -topBar.offsetHeight : 0; - - const y = logicalPages[index].getBoundingClientRect().top - + window.pageYOffset - + offset; - - window.scrollTo({ - top: y, - behavior: 'smooth' - }); + const y = logicalPages[index].getBoundingClientRect().top + window.pageYOffset + offset; + window.scrollTo({ top: y, behavior: 'smooth' }); } function getCurrentLogicalIndex() { let closest = 0; let minDist = Infinity; - logicalPages.forEach((el, i) => { const rect = el.getBoundingClientRect(); const dist = Math.abs(rect.top); @@ -618,53 +647,36 @@ function enableMangaPageNavigation() { closest = i; } }); - return closest; } - const rtl = () => config.manga.direction === 'rtl'; document.addEventListener('keydown', (e) => { if (currentType !== 'manga') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; - const index = getCurrentLogicalIndex(); - - if (e.key === 'ArrowLeft') { - scrollToLogical(rtl() ? index + 1 : index - 1); - } - if (e.key === 'ArrowRight') { - scrollToLogical(rtl() ? index - 1 : index + 1); - } + if (e.key === 'ArrowLeft') scrollToLogical(rtl() ? index + 1 : index - 1); + if (e.key === 'ArrowRight') scrollToLogical(rtl() ? index - 1 : index + 1); }); reader.addEventListener('click', (e) => { if (currentType !== 'manga') return; - const bounds = reader.getBoundingClientRect(); const x = e.clientX - bounds.left; const half = bounds.width / 2; - const index = getCurrentLogicalIndex(); - - if (x < half) { - scrollToLogical(rtl() ? index + 1 : index - 1); - } else { - scrollToLogical(rtl() ? index - 1 : index + 1); - } + if (x < half) scrollToLogical(rtl() ? index + 1 : index - 1); + else scrollToLogical(rtl() ? index - 1 : index + 1); }); } let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); - resizeTimer = setTimeout(() => { - applyStyles(); - }, 250); + resizeTimer = setTimeout(() => { applyStyles(); }, 250); }); let progressSaved = false; - function setupProgressTracking(data, source) { progressSaved = false; @@ -677,25 +689,16 @@ function setupProgressTracking(data, source) { source: source, entry_type: data.type === 'manga' ? 'MANGA' : 'NOVEL', status: 'CURRENT', - progress: source === 'anilist' - ? Math.floor(chapterNumber) - - : chapterNumber - + progress: source === 'anilist' ? Math.floor(chapterNumber) : chapterNumber }; try { await fetch('/api/list/entry', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }); - } catch (err) { - console.error('Error updating progress:', err); - } + } catch (err) { console.error('Error updating progress:', err); } } function checkProgress() { @@ -705,25 +708,24 @@ function setupProgressTracking(data, source) { if (percent >= 0.8 && !progressSaved) { progressSaved = true; - + // Usamos el número real del capítulo, no el ID const chapterNumber = (typeof data.number !== 'undefined' && data.number !== null) ? data.number - : Number(chapter); + : 0; // Fallback si no hay numero sendProgress(chapterNumber); - window.removeEventListener('scroll', checkProgress); } } - window.removeEventListener('scroll', checkProgress); window.addEventListener('scroll', checkProgress); } -if (!bookId || !chapter || !provider) { +// Inicialización +if (!bookId || !currentChapterId || !provider) { reader.innerHTML = `
- Missing required parameters (bookId, chapter, provider) + Missing required parameters (bookId, chapterId, provider)
`; } else { diff --git a/docker/views/books/book.html b/docker/views/books/book.html index 7140a2b..8589459 100644 --- a/docker/views/books/book.html +++ b/docker/views/books/book.html @@ -14,11 +14,12 @@ - - Back to Books + + + + Back -
@@ -28,106 +29,110 @@
+
+

Loading...

- +
+
-
-
-

Loading...

- -
- - -
--% Score
-
Action
+ -
-
-

Chapters

-
+
- +
+
+

Chapters

+ +
+ + + + + + + +
+
+ +
+ + +
-
- - - - - - - - - - - - - -
#TitleProviderAction
-
-
-
+
+ + + +
+

Characters

+
+
- diff --git a/docker/views/css/books/book.css b/docker/views/css/books/book.css index d1a30b6..3c32f10 100644 --- a/docker/views/css/books/book.css +++ b/docker/views/css/books/book.css @@ -1,547 +1,190 @@ -.back-btn { - position: fixed; - top: 2rem; - left: 2rem; - z-index: 100; - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.8rem 1.5rem; - background: var(--color-glass-bg); - backdrop-filter: blur(12px); - border: var(--border-subtle); - border-radius: var(--radius-full); - color: white; - text-decoration: none; - font-weight: 600; - transition: all 0.2s ease; -} -.back-btn:hover { - background: rgba(255, 255, 255, 0.15); - transform: translateX(-5px); +:root { + --bg-card: rgba(255, 255, 255, 0.04); + --border-subtle: rgba(255, 255, 255, 0.1); + --color-primary: #8b5cf6; } -.hero-wrapper { - position: relative; - width: 100%; - height: 60vh; - overflow: hidden; -} -.hero-background { - position: absolute; - inset: 0; - z-index: 0; -} -.hero-background img { - width: 100%; - height: 100%; - object-fit: cover; - opacity: 0.4; - filter: blur(8px); - transform: scale(1.1); -} -.hero-overlay { - position: absolute; - inset: 0; - z-index: 1; - background: linear-gradient( - to bottom, - transparent 0%, - var(--color-bg-base) 100% - ); +/* --- BASICS --- */ +.back-btn { + position: fixed; top: 2rem; left: 2rem; z-index: 100; display: flex; align-items: center; gap: 0.5rem; + padding: 0.8rem 1.5rem; background: var(--color-glass-bg); backdrop-filter: blur(12px); + border: var(--border-subtle); border-radius: 8px; /* Anime style */ + color: white; text-decoration: none; font-weight: 600; transition: all 0.2s ease; } +.back-btn:hover { background: rgba(255, 255, 255, 0.15); transform: translateX(-5px); } + +.hero-wrapper { position: relative; width: 100%; height: 60vh; overflow: hidden; } +.hero-background { position: absolute; inset: 0; z-index: 0; } +.hero-background img { width: 100%; height: 100%; object-fit: cover; opacity: 0.4; filter: blur(8px); transform: scale(1.1); } +.hero-overlay { position: absolute; inset: 0; z-index: 1; background: linear-gradient(to bottom, transparent 0%, var(--color-bg-base) 100%); } .content-container { - position: relative; - z-index: 10; - max-width: 1600px; - margin: -350px auto 0 auto; + position: relative; z-index: 10; max-width: 1400px; margin: -300px auto 0 auto; padding: 0 3rem 4rem 3rem; - display: grid; - grid-template-columns: 260px 1fr; - gap: 3rem; - align-items: flex-start; - animation: slideUp 0.8s ease; } -.hero-content { - display: none; -} - -.sidebar { - display: flex; - flex-direction: column; - gap: 1.5rem; - position: sticky; - top: calc(var(--nav-height) + 2rem); - align-self: flex-start; - z-index: 20; -} - -.poster-card { - width: 100%; - aspect-ratio: 2/3; - border-radius: var(--radius-lg); - overflow: hidden; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8); - border: 1px solid rgba(255, 255, 255, 0.1); - background: #1a1a1a; -} -.poster-card img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.info-grid { - background: var(--color-bg-elevated); - border: var(--border-subtle); - border-radius: var(--radius-md); - padding: 1.25rem; - display: flex; - flex-direction: column; - gap: 1rem; -} -.info-item h4 { - margin: 0 0 0.25rem 0; - font-size: 0.8rem; - color: var(--color-text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; -} -.info-item span { - font-weight: 600; - font-size: 0.95rem; -} - -.main-content { - display: flex; - flex-direction: column; - padding-top: 4rem; - justify-content: flex-start; -} - -.book-header { - margin-bottom: 1.5rem; -} +/* --- HEADER SECTION --- */ +.book-header-section { margin-bottom: 4rem; max-width: 900px; } .book-title { - font-size: 3.5rem; - font-weight: 900; - line-height: 1.1; - margin: 0 0 1rem 0; - text-shadow: 0 4px 30px rgba(0, 0, 0, 0.8); + font-size: clamp(2.5rem, 6vw, 4.5rem); font-weight: 900; + margin-bottom: 0.5rem; text-shadow: 0 4px 30px rgba(0,0,0,0.6); line-height: 1.1; } -.meta-row { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 1.5rem; +.hero-meta-info { + display: flex; align-items: center; gap: 0.8rem; + color: rgba(255,255,255,0.7); font-weight: 600; margin-bottom: 1.2rem; flex-wrap: wrap; } -.pill { - padding: 0.4rem 1rem; - background: rgba(255, 255, 255, 0.1); - border-radius: 99px; - font-size: 0.9rem; - font-weight: 600; - border: var(--border-subtle); - backdrop-filter: blur(10px); -} -.pill.score { - background: rgba(34, 197, 94, 0.2); - color: #4ade80; - border-color: rgba(34, 197, 94, 0.2); +.meta-separator { color: rgba(255,255,255,0.3); font-size: 0.8rem; } + +.pill-local { + background: #22c55e; color: black; padding: 2px 8px; + border-radius: 4px; font-size: 0.75rem; font-weight: 900; } -#description { - display: none; +.hero-description-mini { + font-size: 1.05rem; line-height: 1.5; color: rgba(255,255,255,0.8); + margin-bottom: 1.2rem; max-width: 700px; + display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } -#year { - display: none; +.hero-tags { + display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2.5rem; + color: rgba(255,255,255,0.5); font-weight: 500; } -.action-row { - display: flex; - gap: 1rem; -} -.btn-primary { - padding: 0.8rem 2rem; - background: white; - color: black; - border: none; - border-radius: 99px; - font-weight: 800; - cursor: pointer; - transition: transform 0.2s; -} -.btn-primary:hover { - transform: scale(1.05); +/* --- BUTTONS --- */ +.action-row { display: flex; align-items: center; gap: 1rem; } +.btn-read { + padding: 0.8rem 2.2rem; background: white; color: black; + border-radius: 8px; font-weight: 800; border: none; cursor: pointer; + display: flex; align-items: center; gap: 0.6rem; transition: 0.2s ease; } +.btn-read:hover { transform: scale(1.03); filter: brightness(0.9); } -.btn-secondary { - padding: 0.8rem 2rem; - background: rgba(255, 255, 255, 0.1); - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 99px; - font-weight: 700; - cursor: pointer; - transition: 0.2s; - backdrop-filter: blur(10px); -} -.btn-secondary:hover { - background: rgba(255, 255, 255, 0.2); +.btn-add-list { + padding: 0.8rem 1.5rem; background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); color: white; + border-radius: 8px; font-weight: 700; cursor: pointer; transition: 0.2s; } +.btn-add-list:hover { background: rgba(255,255,255,0.2); } -.btn-blur { - padding: 0.8rem 2rem; - background: rgba(255, 255, 255, 0.1); - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 99px; - font-weight: 700; - cursor: pointer; - transition: 0.2s; - backdrop-filter: blur(10px); -} -.btn-blur:hover { - background: rgba(255, 255, 255, 0.2); -} -.chapters-section { - margin-top: 1rem; -} -.section-title { - display: flex; - align-items: center; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - padding-bottom: 0.8rem; - margin-bottom: 1.5rem; -} -.section-title h2 { - font-size: 1.5rem; - margin: 0; - border-left: 4px solid var(--color-primary); - padding-left: 1rem; -} +/* --- MAIN LAYOUT --- */ +.main-layout { display: grid; grid-template-columns: 280px 1fr; gap: 4rem; margin-top: 2rem; } -.chapters-table-wrapper { - background: var(--color-bg-elevated); - border-radius: var(--radius-md); - border: 1px solid rgba(255, 255, 255, 0.05); - overflow: hidden; -} -.chapters-table { - width: 100%; - border-collapse: collapse; - text-align: left; -} -.chapters-table th { - padding: 0.8rem 1.2rem; - background: rgba(255, 255, 255, 0.03); - color: var(--color-text-secondary); - font-weight: 600; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} -.chapters-table td { - padding: 1rem 1.2rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - color: var(--color-text-primary); - font-size: 0.95rem; -} -.chapters-table tr:last-child td { - border-bottom: none; -} -.chapters-table tr:hover { - background: var(--color-bg-elevated-hover); +/* Sidebar */ +.poster-section { display: flex; flex-direction: column; gap: 1.5rem; } +.poster-card { + border-radius: 12px; overflow: hidden; width: 100%; + box-shadow: 0 30px 60px rgba(0,0,0,0.5); border: 1px solid var(--border-subtle); } +.poster-card img { width: 100%; height: auto; display: block; aspect-ratio: 2/3; object-fit: cover; } -.filter-select { - appearance: none; - -webkit-appearance: none; - background-color: var(--color-bg-elevated); - color: var(--color-text-primary); - border: 1px solid rgba(255, 255, 255, 0.1); - padding: 0.5rem 2rem 0.5rem 1rem; - border-radius: 99px; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - outline: none; - background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 1rem center; +/* Sidebar Extra Info (Synonyms) */ +.sidebar-extra-info { + background: rgba(255,255,255,0.03); padding: 1.5rem; + border-radius: 16px; border: 1px solid rgba(255,255,255,0.08); } +.sidebar-label { + font-size: 0.8rem; color: #888; text-transform: uppercase; margin: 0 0 1rem 0; letter-spacing: 0.5px; +} +.synonyms-list { + list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; +} +.synonyms-list li { + font-size: 0.9rem; color: #ddd; line-height: 1.4; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 0.5rem; +} +.synonyms-list li:last-child { border-bottom: none; } -.filter-select:hover { - border-color: var(--color-primary); - background-color: var(--color-bg-elevated-hover); -} -.filter-select option { - background-color: var(--color-bg-elevated); - color: var(--color-text-primary); -} +/* --- CONTENT COLUMN --- */ +.content-section { margin-top: 4rem; } +h2, .subsection-title { font-size: 1.8rem; font-weight: 800; margin-bottom: 1.5rem; color: white; } -.read-btn-small { - background: var(--color-primary); - color: white; - border: none; - padding: 0.4rem 0.9rem; - border-radius: 6px; - font-weight: 600; - cursor: pointer; - font-size: 0.8rem; - transition: 0.2s; -} -.read-btn-small:hover { - background: #7c3aed; -} +/* Chapters Section */ +.chapters-section { border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem; } +.chapters-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; } -.pagination-controls { - display: flex; - justify-content: center; - gap: 1rem; - margin-top: 1.5rem; - align-items: center; -} -.page-btn { - background: var(--color-bg-elevated); - border: 1px solid rgba(255, 255, 255, 0.1); - color: white; - padding: 0.5rem 1rem; - border-radius: 8px; - cursor: pointer; - font-size: 0.9rem; -} -.page-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} -.page-btn:hover:not(:disabled) { - border-color: var(--color-primary); +/* Chapter Controls */ +.chapter-controls { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; } +.glass-select, .glass-input { + appearance: none; -webkit-appearance: none; + background-color: rgba(0, 0, 0, 0.4); + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='rgba(255,255,255,0.7)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; background-position: right 0.8rem center; background-size: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.9); + padding: 0.4rem 2rem 0.4rem 0.8rem; border-radius: 6px; font-size: 0.8rem; font-weight: 500; height: 34px; + outline: none; backdrop-filter: blur(4px); transition: all 0.2s ease; cursor: pointer; min-width: 110px; } +.glass-input { background-image: none; padding: 0.4rem 0.8rem; width: 180px; } +.glass-input:focus { border-color: var(--color-primary); width: 220px; } +.glass-btn-icon { width: 34px; height: 34px; border-radius: 6px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; } +.glass-btn-icon:hover { background: var(--color-primary); border-color: var(--color-primary); } -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(40px); - } - to { - opacity: 1; - transform: translateY(0); - } +/* Chapters Grid */ +.chapters-grid { display: flex; flex-direction: column; gap: 0.5rem; } +.chapter-item { + display: flex; align-items: center; justify-content: space-between; + background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05); + padding: 0.8rem 1.2rem; border-radius: 8px; + transition: all 0.2s ease; cursor: pointer; position: relative; overflow: hidden; } +.chapter-item:hover { background: rgba(255, 255, 255, 0.08); transform: translateX(5px); border-color: rgba(255, 255, 255, 0.2); } +.chapter-item::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--color-primary); opacity: 0; transition: opacity 0.2s; } +.chapter-info { display: flex; flex-direction: column; gap: 0.2rem; } +.chapter-number { font-size: 0.85rem; font-weight: 600; color: var(--color-text-secondary); text-transform: uppercase; } +.chapter-title { font-size: 1rem; font-weight: 500; color: white; } +.chapter-meta { display: flex; align-items: center; gap: 1rem; font-size: 0.85rem; color: #888; } +.lang-tag { font-size: 0.7rem; padding: 2px 6px; border-radius: 4px; background: rgba(255,255,255,0.1); text-transform: uppercase; color: #ccc; } + + +/* --- RELATIONS (Horizontal Cards) --- */ +.relations-horizontal { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } +.relation-card-horizontal { + display: flex; background: var(--bg-card); border: 1px solid var(--border-subtle); + border-radius: 10px; overflow: hidden; transition: 0.2s; cursor: pointer; +} +.relation-card-horizontal:hover { background: rgba(255,255,255,0.08); transform: translateX(5px); } +.rel-img { width: 85px; height: 110px; object-fit: cover; } +.rel-info { padding: 1rem; display: flex; flex-direction: column; justify-content: center; } +.rel-type { + font-size: 0.7rem; color: var(--color-primary); font-weight: 800; + margin-bottom: 4px; background: rgba(139, 92, 246, 0.1); width: fit-content; padding: 2px 6px; border-radius: 4px; +} +.rel-title { font-size: 0.95rem; font-weight: 700; color: #eee; } + + +/* --- CHARACTERS (Grid Style) --- */ +.characters-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1rem; } +.character-item { display: flex; align-items: center; gap: 1rem; } +.char-avatar { width: 60px; height: 60px; border-radius: 10px; overflow: hidden; flex-shrink: 0; } +.char-avatar img { width: 100%; height: 100%; object-fit: cover; } +.char-info { display: flex; flex-direction: column; gap: 2px; } +.char-name { font-size: 1rem; font-weight: 700; color: #fff; } +.char-role { font-size: 0.8rem; color: #888; font-weight: 500; } + +/* --- PAGINATION --- */ +.pagination-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 2rem; align-items: center; } +.page-btn { background: var(--color-bg-elevated); border: 1px solid rgba(255, 255, 255, 0.1); color: white; padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; font-size: 0.9rem; } +.page-btn:disabled { opacity: 0.5; cursor: not-allowed; } +.page-btn:hover:not(:disabled) { border-color: var(--color-primary); } + @media (max-width: 1024px) { - .hero-wrapper { - height: 40vh; - } - .content-container { - grid-template-columns: 1fr; - margin-top: -80px; - padding: 0 1.5rem 4rem 1.5rem; - } - .poster-card { - display: none; - } - - .main-content { - padding-top: 0; - align-items: center; - text-align: center; - } - .book-title { - font-size: 2.2rem; - } - .meta-row { - justify-content: center; - } - .action-row { - justify-content: center; - width: 100%; - } - .btn-primary, - .btn-blur { - flex: 1; - justify-content: center; - } - - .sidebar { - display: none; - } - .chapters-table th:nth-child(3), - .chapters-table td:nth-child(3) { - display: none; - } - .chapters-table th:nth-child(4), - .chapters-table td:nth-child(4) { - display: none; - } + .content-container { margin-top: -150px; padding: 0 1.5rem; } + .main-layout { grid-template-columns: 1fr; gap: 2rem; } + .poster-section { display: flex; flex-direction: column; align-items: center; } + .poster-card { width: 220px; } + .metadata-sidebar { width: 100%; max-width: 400px; } } @media (max-width: 768px) { - .hero-wrapper { - height: 35vh; - } - - .content-container { - display: flex; - flex-direction: column; - margin-top: -60px; - padding: 0 1rem 3rem 1rem; - gap: 1.5rem; - } - - .sidebar { - display: flex !important; - position: static; - width: 100%; - align-items: center; - order: 1; - } - - .poster-card { - display: block !important; - width: 160px; - margin: 0 auto; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); - border: 2px solid rgba(255, 255, 255, 0.15); - } - - .info-grid { - display: none; - } - - .main-content { - order: 2; - padding-top: 0; - text-align: center; - align-items: center; - } - - .book-title { - font-size: 2rem; - line-height: 1.2; - margin-bottom: 0.8rem; - } - - .meta-row { - justify-content: center; - gap: 0.5rem; - } - - .pill { - padding: 0.3rem 0.8rem; - font-size: 0.8rem; - } - - .action-row { - flex-direction: column; - width: 100%; - gap: 0.8rem; - } - - .btn-primary, - .btn-secondary, - .btn-blur { - width: 100%; - justify-content: center; - padding: 0.9rem; - } - - .chapters-table-wrapper { - background: transparent; - border: none; - } - - .chapters-table thead { - display: none; - } - - .chapters-table tbody, - .chapters-table tr, - .chapters-table td { - display: block; - width: 100%; - } - - .chapters-table tr { - background: var(--color-bg-elevated); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 12px; - margin-bottom: 0.8rem; - padding: 1rem; - position: relative; - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - .chapters-table td { - padding: 0; - border: none; - text-align: left; - } - - .chapters-table td:nth-child(1) { - font-size: 1rem; - font-weight: 700; - color: white; - } - - .chapters-table td:nth-child(2) { - font-size: 0.8rem; - color: var(--color-text-secondary); - order: 3; - } - - .chapters-table td:last-child { - margin-top: 0.5rem; - order: 4; - } - - .chapters-table td:last-child, - .chapters-table td:nth-child(4) { - display: block !important; - width: 100%; - margin-top: 1rem; - padding-top: 0.75rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); - } - - .read-btn-small { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - padding: 0.8rem; - font-size: 0.95rem; - background: var(--color-primary); - border: none; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); - } - - .chapters-table td:nth-child(3) { - display: block !important; - font-size: 0.8rem; - color: var(--color-text-secondary); - margin-bottom: 0.25rem; - } - - .chapters-table td:nth-child(3)::before { - content: "Provider: "; - font-weight: 700; - color: #555; - } - - .read-btn-small { - width: 100%; - padding: 0.7rem; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - } -} + .hero-wrapper { height: 40vh; } + .book-title { font-size: 2.5rem; } + .action-row { flex-direction: column; width: 100%; } + .btn-read, .btn-add-list { width: 100%; justify-content: center; } + .chapters-header { flex-direction: column; align-items: flex-start; } + .chapter-controls { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } + .search-box { grid-column: span 2; } + .glass-input { width: 100%; } +} \ No newline at end of file