frontend for downloading books
This commit is contained in:
@@ -200,6 +200,95 @@ function processChaptersData(chaptersData) {
|
|||||||
setupReadButton();
|
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 = `<span class="spinner">↻</span>`; // 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) {
|
async function loadChapters(targetProvider = null) {
|
||||||
const listContainer = document.getElementById('chapters-list');
|
const listContainer = document.getElementById('chapters-list');
|
||||||
const loadingMsg = document.getElementById('loading-msg');
|
const loadingMsg = document.getElementById('loading-msg');
|
||||||
@@ -317,24 +406,51 @@ function renderChapterList() {
|
|||||||
itemsToShow.forEach(chapter => {
|
itemsToShow.forEach(chapter => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'chapter-item';
|
el.className = 'chapter-item';
|
||||||
|
|
||||||
|
// El clic principal abre el lector
|
||||||
el.onclick = () => openReader(chapter.id, chapter.provider);
|
el.onclick = () => openReader(chapter.id, chapter.provider);
|
||||||
|
|
||||||
const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : '';
|
const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : '';
|
||||||
const providerLabel = chapter.provider !== 'local' ? chapter.provider : '';
|
const providerLabel = chapter.provider !== 'local' ? chapter.provider : '';
|
||||||
|
|
||||||
|
// Definimos el icono SVG de descarga para no ensuciar tanto el template string
|
||||||
|
const downloadIcon = `
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const isLocal = chapter.provider === 'local';
|
||||||
|
const downloadBtnStyle = isLocal ? 'display:none;' : '';
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="chapter-info">
|
<div class="chapter-info">
|
||||||
<span class="chapter-number">Chapter ${chapter.number}</span>
|
<span class="chapter-number">Chapter ${chapter.number}</span>
|
||||||
<span class="chapter-title">${chapter.title || ''}</span>
|
<span class="chapter-title">${chapter.title || ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="chapter-meta">
|
|
||||||
${providerLabel ? `<span class="lang-tag">${providerLabel}</span>` : ''}
|
<div class="chapter-actions" style="display: flex; align-items: center; gap: 10px;">
|
||||||
${dateStr ? `<span>${dateStr}</span>` : ''}
|
<div class="chapter-meta">
|
||||||
<svg class="chapter-play-icon" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
${providerLabel ? `<span class="lang-tag">${providerLabel}</span>` : ''}
|
||||||
|
${dateStr ? `<span>${dateStr}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="download-btn"
|
||||||
|
style="${downloadBtnStyle}"
|
||||||
|
onclick="event.stopPropagation(); downloadChapter('${chapter.id}', '${chapter.number}', '${chapter.provider}', this)"
|
||||||
|
title="Descargar">
|
||||||
|
${downloadIcon}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<svg class="chapter-play-icon" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M8 5v14l11-7z"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page');
|
chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,3 +188,41 @@ h2, .subsection-title { font-size: 1.8rem; font-weight: 800; margin-bottom: 1.5r
|
|||||||
.search-box { grid-column: span 2; }
|
.search-box { grid-column: span 2; }
|
||||||
.glass-input { width: 100%; }
|
.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;
|
||||||
|
}
|
||||||
@@ -200,6 +200,95 @@ function processChaptersData(chaptersData) {
|
|||||||
setupReadButton();
|
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 = `<span class="spinner">↻</span>`; // 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) {
|
async function loadChapters(targetProvider = null) {
|
||||||
const listContainer = document.getElementById('chapters-list');
|
const listContainer = document.getElementById('chapters-list');
|
||||||
const loadingMsg = document.getElementById('loading-msg');
|
const loadingMsg = document.getElementById('loading-msg');
|
||||||
@@ -317,24 +406,51 @@ function renderChapterList() {
|
|||||||
itemsToShow.forEach(chapter => {
|
itemsToShow.forEach(chapter => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'chapter-item';
|
el.className = 'chapter-item';
|
||||||
|
|
||||||
|
// El clic principal abre el lector
|
||||||
el.onclick = () => openReader(chapter.id, chapter.provider);
|
el.onclick = () => openReader(chapter.id, chapter.provider);
|
||||||
|
|
||||||
const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : '';
|
const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : '';
|
||||||
const providerLabel = chapter.provider !== 'local' ? chapter.provider : '';
|
const providerLabel = chapter.provider !== 'local' ? chapter.provider : '';
|
||||||
|
|
||||||
|
// Definimos el icono SVG de descarga para no ensuciar tanto el template string
|
||||||
|
const downloadIcon = `
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const isLocal = chapter.provider === 'local';
|
||||||
|
const downloadBtnStyle = isLocal ? 'display:none;' : '';
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="chapter-info">
|
<div class="chapter-info">
|
||||||
<span class="chapter-number">Chapter ${chapter.number}</span>
|
<span class="chapter-number">Chapter ${chapter.number}</span>
|
||||||
<span class="chapter-title">${chapter.title || ''}</span>
|
<span class="chapter-title">${chapter.title || ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="chapter-meta">
|
|
||||||
${providerLabel ? `<span class="lang-tag">${providerLabel}</span>` : ''}
|
<div class="chapter-actions" style="display: flex; align-items: center; gap: 10px;">
|
||||||
${dateStr ? `<span>${dateStr}</span>` : ''}
|
<div class="chapter-meta">
|
||||||
<svg class="chapter-play-icon" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
${providerLabel ? `<span class="lang-tag">${providerLabel}</span>` : ''}
|
||||||
|
${dateStr ? `<span>${dateStr}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="download-btn"
|
||||||
|
style="${downloadBtnStyle}"
|
||||||
|
onclick="event.stopPropagation(); downloadChapter('${chapter.id}', '${chapter.number}', '${chapter.provider}', this)"
|
||||||
|
title="Descargar">
|
||||||
|
${downloadIcon}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<svg class="chapter-play-icon" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M8 5v14l11-7z"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page');
|
chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,3 +188,41 @@ h2, .subsection-title { font-size: 1.8rem; font-weight: 800; margin-bottom: 1.5r
|
|||||||
.search-box { grid-column: span 2; }
|
.search-box { grid-column: span 2; }
|
||||||
.glass-input { width: 100%; }
|
.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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user