From a02db9550195301de94a1054f4e9ba6bed9539be Mon Sep 17 00:00:00 2001 From: lenafx Date: Thu, 1 Jan 2026 02:59:45 +0100 Subject: [PATCH] frontend for downloading books --- desktop/src/scripts/books/book.js | 124 +++++++++++++++++++++++++++++- desktop/views/css/books/book.css | 38 +++++++++ docker/src/scripts/books/book.js | 124 +++++++++++++++++++++++++++++- docker/views/css/books/book.css | 38 +++++++++ 4 files changed, 316 insertions(+), 8 deletions(-) diff --git a/desktop/src/scripts/books/book.js b/desktop/src/scripts/books/book.js index 6ac1337..e7c6aee 100644 --- a/desktop/src/scripts/books/book.js +++ b/desktop/src/scripts/books/book.js @@ -200,6 +200,95 @@ function processChaptersData(chaptersData) { setupReadButton(); } +function buildProxyUrl(url, headers = {}) { + const origin = window.location.origin; + + 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 `${origin}/api/proxy?${params.toString()}`; +} + +async function downloadChapter(chapterId, chapterNumber, provider, btnElement) { + // Validamos que tengamos bookId y un provider válido + if (!bookId || !provider) return showError("Error: Faltan datos del capítulo"); + + // Feedback visual + const originalText = btnElement.innerHTML; + btnElement.innerHTML = ``; // Spinner pequeño + btnElement.disabled = true; + + try { + // 1. OBTENER CONTENIDO USANDO EL PROVIDER DEL CAPÍTULO + // CAMBIO AQUÍ: Usamos 'provider' en lugar de 'extensionName' + const fetchUrl = `/api/book/${bookId}/${chapterId}/${provider}?source=${extensionName || 'anilist'}&lang=none`; + + const contentRes = await fetch(fetchUrl); + + if (!contentRes.ok) throw new Error("Error obteniendo contenido del capítulo"); + const chapterData = await contentRes.json(); + + // 2. PREPARAR BODY (Misma lógica) + let payload = { + anilist_id: parseInt(bookId), + chapter_number: parseFloat(chapterNumber), + format: "", + }; + + if (chapterData.pages && Array.isArray(chapterData.pages)) { + payload.format = "manga"; + payload.images = chapterData.pages.map((img, index) => { + let finalUrl = img.url; + if (img.headers && Object.keys(img.headers).length > 0) { + finalUrl = buildProxyUrl(img.url, img.headers); + } + return { index: index, url: finalUrl }; + }); + } + else if (chapterData.content) { + payload.format = "novel"; + payload.content = chapterData.content; + } else { + throw new Error("Formato desconocido"); + } + + const downloadRes = await fetch('/api/library/download/book', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const downloadData = await downloadRes.json(); + + if (downloadRes.status === 200) { + btnElement.innerHTML = "✓"; + btnElement.style.color = "#22c55e"; // Icono verde + } else if (downloadRes.status === 409) { + btnElement.innerHTML = "✓"; + btnElement.style.color = "#3b82f6"; // Icono azul (ya existe) + } else { + throw new Error(downloadData.message || "Error"); + } + + } catch (err) { + console.error("Download Error:", err); + btnElement.innerHTML = "✕"; // X roja + btnElement.style.color = "#ef4444"; + btnElement.disabled = false; + + // Restaurar icono original después de 3 seg + setTimeout(() => { + btnElement.innerHTML = originalText; + btnElement.style.color = ""; + }, 3000); + } +} + async function loadChapters(targetProvider = null) { const listContainer = document.getElementById('chapters-list'); const loadingMsg = document.getElementById('loading-msg'); @@ -317,24 +406,51 @@ function renderChapterList() { itemsToShow.forEach(chapter => { const el = document.createElement('div'); el.className = 'chapter-item'; + + // El clic principal abre el lector el.onclick = () => openReader(chapter.id, chapter.provider); const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : ''; const providerLabel = chapter.provider !== 'local' ? chapter.provider : ''; + // Definimos el icono SVG de descarga para no ensuciar tanto el template string + const downloadIcon = ` + + + + + `; + + const isLocal = chapter.provider === 'local'; + const downloadBtnStyle = isLocal ? 'display:none;' : ''; + el.innerHTML = `
Chapter ${chapter.number} ${chapter.title || ''}
-
- ${providerLabel ? `${providerLabel}` : ''} - ${dateStr ? `${dateStr}` : ''} - + +
+
+ ${providerLabel ? `${providerLabel}` : ''} + ${dateStr ? `${dateStr}` : ''} +
+ + + + + +
`; container.appendChild(el); }); + chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page'); } diff --git a/desktop/views/css/books/book.css b/desktop/views/css/books/book.css index 3c32f10..5e2b3bc 100644 --- a/desktop/views/css/books/book.css +++ b/desktop/views/css/books/book.css @@ -187,4 +187,42 @@ h2, .subsection-title { font-size: 1.8rem; font-weight: 800; margin-bottom: 1.5r .chapter-controls { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } .search-box { grid-column: span 2; } .glass-input { width: 100%; } +} + +.chapter-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.chapter-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.download-btn { + background: transparent; + border: 1px solid rgba(255,255,255,0.3); + color: #ccc; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.download-btn:hover { + background: rgba(255,255,255,0.1); + border-color: #fff; + color: #fff; +} + +.download-btn:disabled { + opacity: 0.7; + cursor: wait; } \ No newline at end of file diff --git a/docker/src/scripts/books/book.js b/docker/src/scripts/books/book.js index 6ac1337..e7c6aee 100644 --- a/docker/src/scripts/books/book.js +++ b/docker/src/scripts/books/book.js @@ -200,6 +200,95 @@ function processChaptersData(chaptersData) { setupReadButton(); } +function buildProxyUrl(url, headers = {}) { + const origin = window.location.origin; + + 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 `${origin}/api/proxy?${params.toString()}`; +} + +async function downloadChapter(chapterId, chapterNumber, provider, btnElement) { + // Validamos que tengamos bookId y un provider válido + if (!bookId || !provider) return showError("Error: Faltan datos del capítulo"); + + // Feedback visual + const originalText = btnElement.innerHTML; + btnElement.innerHTML = ``; // Spinner pequeño + btnElement.disabled = true; + + try { + // 1. OBTENER CONTENIDO USANDO EL PROVIDER DEL CAPÍTULO + // CAMBIO AQUÍ: Usamos 'provider' en lugar de 'extensionName' + const fetchUrl = `/api/book/${bookId}/${chapterId}/${provider}?source=${extensionName || 'anilist'}&lang=none`; + + const contentRes = await fetch(fetchUrl); + + if (!contentRes.ok) throw new Error("Error obteniendo contenido del capítulo"); + const chapterData = await contentRes.json(); + + // 2. PREPARAR BODY (Misma lógica) + let payload = { + anilist_id: parseInt(bookId), + chapter_number: parseFloat(chapterNumber), + format: "", + }; + + if (chapterData.pages && Array.isArray(chapterData.pages)) { + payload.format = "manga"; + payload.images = chapterData.pages.map((img, index) => { + let finalUrl = img.url; + if (img.headers && Object.keys(img.headers).length > 0) { + finalUrl = buildProxyUrl(img.url, img.headers); + } + return { index: index, url: finalUrl }; + }); + } + else if (chapterData.content) { + payload.format = "novel"; + payload.content = chapterData.content; + } else { + throw new Error("Formato desconocido"); + } + + const downloadRes = await fetch('/api/library/download/book', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const downloadData = await downloadRes.json(); + + if (downloadRes.status === 200) { + btnElement.innerHTML = "✓"; + btnElement.style.color = "#22c55e"; // Icono verde + } else if (downloadRes.status === 409) { + btnElement.innerHTML = "✓"; + btnElement.style.color = "#3b82f6"; // Icono azul (ya existe) + } else { + throw new Error(downloadData.message || "Error"); + } + + } catch (err) { + console.error("Download Error:", err); + btnElement.innerHTML = "✕"; // X roja + btnElement.style.color = "#ef4444"; + btnElement.disabled = false; + + // Restaurar icono original después de 3 seg + setTimeout(() => { + btnElement.innerHTML = originalText; + btnElement.style.color = ""; + }, 3000); + } +} + async function loadChapters(targetProvider = null) { const listContainer = document.getElementById('chapters-list'); const loadingMsg = document.getElementById('loading-msg'); @@ -317,24 +406,51 @@ function renderChapterList() { itemsToShow.forEach(chapter => { const el = document.createElement('div'); el.className = 'chapter-item'; + + // El clic principal abre el lector el.onclick = () => openReader(chapter.id, chapter.provider); const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : ''; const providerLabel = chapter.provider !== 'local' ? chapter.provider : ''; + // Definimos el icono SVG de descarga para no ensuciar tanto el template string + const downloadIcon = ` + + + + + `; + + const isLocal = chapter.provider === 'local'; + const downloadBtnStyle = isLocal ? 'display:none;' : ''; + el.innerHTML = `
Chapter ${chapter.number} ${chapter.title || ''}
-
- ${providerLabel ? `${providerLabel}` : ''} - ${dateStr ? `${dateStr}` : ''} - + +
+
+ ${providerLabel ? `${providerLabel}` : ''} + ${dateStr ? `${dateStr}` : ''} +
+ + + + + +
`; container.appendChild(el); }); + chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page'); } diff --git a/docker/views/css/books/book.css b/docker/views/css/books/book.css index 3c32f10..5e2b3bc 100644 --- a/docker/views/css/books/book.css +++ b/docker/views/css/books/book.css @@ -187,4 +187,42 @@ h2, .subsection-title { font-size: 1.8rem; font-weight: 800; margin-bottom: 1.5r .chapter-controls { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } .search-box { grid-column: span 2; } .glass-input { width: 100%; } +} + +.chapter-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.chapter-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.download-btn { + background: transparent; + border: 1px solid rgba(255,255,255,0.3); + color: #ccc; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.download-btn:hover { + background: rgba(255,255,255,0.1); + border-color: #fff; + color: #fff; +} + +.download-btn:disabled { + opacity: 0.7; + cursor: wait; } \ No newline at end of file