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 || ''}
-