Colas por usuario y sin pantalla negra mientras carga la página siguiente
- Descargas y reescalados tienen usuarios (download_users): Colas y la biblioteca descargada muestran solo lo de cada uno. Un manga ya descargado por otro se lee desde el disco y entra en tu biblioteca al empezar a leerlo; borrar uno compartido solo lo quita de la tuya. Lo anterior se asigna a quien lo lee, sigue o tiene en una lista (o al primer usuario). - Lector: si la página siguiente no ha llegado, la actual se queda con un indicador hasta que llega (máx. 4 s); si no, la nueva aparece con fundido y un indicador en vez de negro. Precarga de 6 páginas por delante, ya decodificadas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+61
-12
@@ -905,9 +905,10 @@ async function viewManga(id, langOverride) {
|
||||
$('#dlopen')?.addEventListener('click', () => setSel(sel?.kind === 'dl' ? null : { kind: 'dl', mode: 'range' }))
|
||||
$('#dlcancel')?.addEventListener('click', async () => renderDl(await api(`/api/download/${id}/cancel`, { method: 'POST' })))
|
||||
$('#dldel')?.addEventListener('click', async () => {
|
||||
if (!confirm(`¿Eliminar la copia local de «${manga.title}»?\nSe borrará la carpeta mangas/${d.folder}${d.size ? ` (${fmtSize(d.size)})` : ''}, incluida la versión HD.\nTu historial de lectura no se toca.`)) return
|
||||
await api(`/api/download/${id}`, { method: 'DELETE' })
|
||||
toast('Copia local eliminada')
|
||||
if (!confirm(d.shared ? `¿Quitar «${manga.title}» de tu biblioteca descargada?\nOtro usuario también lo tiene: los archivos se quedan para él y lo puedes seguir leyendo.`
|
||||
: `¿Eliminar la copia local de «${manga.title}»?\nSe borrará la carpeta mangas/${d.folder}${d.size ? ` (${fmtSize(d.size)})` : ''}, incluida la versión HD.\nTu historial de lectura no se toca.`)) return
|
||||
const r = await api(`/api/download/${id}`, { method: 'DELETE' })
|
||||
toast(r.kept ? 'Quitado de tu biblioteca' : 'Copia local eliminada')
|
||||
viewManga(id, langOverride)
|
||||
})
|
||||
const wantedBefore = wantedSet.size
|
||||
@@ -1412,24 +1413,67 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
}
|
||||
return [i]
|
||||
}
|
||||
// Las 4 siguientes y las 2 anteriores (para que al retroceder la hoja tenga ya su reverso).
|
||||
// Las 6 siguientes y las 2 anteriores (para que al retroceder la hoja tenga ya su reverso), ya
|
||||
// decodificadas. Se guardan las referencias para que la descarga no se pierda si se recoge la basura.
|
||||
const preloaded = new Map()
|
||||
function preload() {
|
||||
const load = i => {
|
||||
const want = []
|
||||
for (let k = 1; k <= 6 && shown.at(-1) + k < total; k++) want.push(shown.at(-1) + k)
|
||||
for (let k = 1; k <= 2 && shown[0] - k >= 0; k++) want.push(shown[0] - k)
|
||||
for (const i of want) {
|
||||
if (preloaded.has(i)) continue
|
||||
const im = new Image()
|
||||
im.onload = () => isWide(im) && wide.add(i)
|
||||
im.src = pages[i]
|
||||
im.decode?.().catch(() => {})
|
||||
preloaded.set(i, im)
|
||||
}
|
||||
for (let k = 1; k <= 4 && shown.at(-1) + k < total; k++) load(shown.at(-1) + k)
|
||||
for (let k = 1; k <= 2 && shown[0] - k >= 0; k++) load(shown[0] - k)
|
||||
for (const i of preloaded.keys()) if (i < shown[0] - 4 || i > shown.at(-1) + 10) preloaded.delete(i)
|
||||
}
|
||||
const imgReady = im => im.complete && im.naturalWidth > 0
|
||||
// Hacia una página que aún no ha llegado, la actual se queda (con un indicador tras un momento) hasta que
|
||||
// llegue o pasen 4 s, en vez de pasar la hoja y dejar negro debajo. Si se pide otra página mientras,
|
||||
// manda la última.
|
||||
let showSeq = 0, waitTimer = 0
|
||||
// dir: 1 hacia delante, -1 hacia atrás, 0 sin animación (salto con la barra, cambio de modo…).
|
||||
function showPaged(dir = 0) {
|
||||
const seq = ++showSeq
|
||||
clearTimeout(waitTimer)
|
||||
box.classList.remove('waiting')
|
||||
finishTurn()
|
||||
const old = $('.stage', box)
|
||||
const stage = document.createElement('div')
|
||||
stage.className = 'stage'
|
||||
stage.classList.toggle('two', shown.length === 2)
|
||||
stage.innerHTML = shown.map(i => `<img data-i="${i}" src="${pages[i]}" alt="" draggable="false">`).join('')
|
||||
const imgs = [...stage.querySelectorAll('img')]
|
||||
if ($('.stage', box) && dir && !wantDrag && !imgs.every(imgReady)) {
|
||||
waitTimer = setTimeout(() => box.classList.add('waiting'), 200)
|
||||
const arrived = Promise.all(imgs.map(im => (im.decode ? im.decode() : new Promise(r => { im.onload = im.onerror = r })).catch(() => {})))
|
||||
Promise.race([arrived, new Promise(r => setTimeout(r, 4000))]).then(() => {
|
||||
if (seq !== showSeq) return
|
||||
clearTimeout(waitTimer)
|
||||
box.classList.remove('waiting')
|
||||
if (!atEnd && isPaged() && box.isConnected) presentPaged(stage, dir)
|
||||
})
|
||||
return
|
||||
}
|
||||
presentPaged(stage, dir)
|
||||
}
|
||||
function presentPaged(stage, dir) {
|
||||
const old = $('.stage', box)
|
||||
// Lo que aún no ha llegado se ve con un indicador de carga y aparece con un fundido, no de golpe.
|
||||
const imgs = [...stage.querySelectorAll('img')]
|
||||
const pending = imgs.filter(im => !imgReady(im))
|
||||
pending.forEach(im => {
|
||||
im.classList.add('pending')
|
||||
const done = () => {
|
||||
im.classList.remove('pending')
|
||||
if (!stage.querySelector('img.pending')) stage.classList.remove('loading')
|
||||
}
|
||||
im.addEventListener('load', done, { once: true })
|
||||
im.addEventListener('error', done, { once: true })
|
||||
})
|
||||
stage.classList.toggle('loading', pending.length > 0)
|
||||
// Al arrastrar, el sonido sale al soltar (si la página llega a pasar).
|
||||
if (old && dir && !wantDrag) turnSound(settings.sfx, turnPan(dir))
|
||||
if (old && dir && settings.anim !== 'none' && zoom.s === 1) turnPage(old, stage, dir, wantDrag)
|
||||
@@ -1672,6 +1716,9 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
}
|
||||
function showEnd() {
|
||||
finishTurn()
|
||||
showSeq++ // descarta una página que estuviera esperando a cargar
|
||||
clearTimeout(waitTimer)
|
||||
box.classList.remove('waiting')
|
||||
atEnd = true
|
||||
resetZoom()
|
||||
box.innerHTML = endBlock
|
||||
@@ -2468,8 +2515,8 @@ async function viewQueues() {
|
||||
pct: d.total ? Math.min(100, (d.done / d.total) * 100) : null,
|
||||
lines: `<div>${esc(downloadLabel(d))}</div><div>${fmtSize(d.size)}</div>
|
||||
${d.status === 'error' ? `<div class="error" title="${esc(d.error || '')}">Con errores</div>` : ''}`,
|
||||
del: `<button class="hdel" data-del="${esc(d.manga_id)}" data-title="${esc(d.title)}" data-size="${d.size || 0}" title="Eliminar la copia local (mangas/${esc(d.folder)})" aria-label="Eliminar la copia local">${icon('trash')}</button>`,
|
||||
})).join('')}</div>` : `<p class="muted">${list.length ? 'Todo lo descargado está aún en la cola.' : 'Aún no has descargado nada. Usa “Descargar” en la ficha de un manga.'}</p>`}
|
||||
del: `<button class="hdel" data-del="${esc(d.manga_id)}" data-title="${esc(d.title)}" data-size="${d.size || 0}" ${d.shared ? 'data-shared="1"' : ''} title="${d.shared ? 'Quitar de tu biblioteca (otro usuario también lo tiene)' : `Eliminar la copia local (mangas/${esc(d.folder)})`}" aria-label="Eliminar la copia local">${icon('trash')}</button>`,
|
||||
})).join('')}</div>` : `<p class="muted">${list.length ? 'Todo lo descargado está aún en la cola.' : 'Aún no tienes nada descargado. Usa “Descargar” en la ficha de un manga; lo que ya descargó otro usuario aparece aquí en cuanto lo empiezas a leer.'}</p>`}
|
||||
</section>`
|
||||
// Refresco sin parpadeo: se actualiza solo lo que cambia (las portadas no se vuelven a cargar).
|
||||
let wrap = $('#qwrap')
|
||||
@@ -2486,8 +2533,10 @@ async function viewQueues() {
|
||||
else if (b.dataset.devcancel) { offCancel(b.dataset.devcancel); toast('Guardado cancelado') }
|
||||
else if (b.dataset.retry) act(() => api(`/api/upscale/${b.dataset.retry}`, { method: 'POST', body: { chapterIds: b.dataset.ids.split(' ') } }), 'Vuelto a la cola')
|
||||
else if (b.dataset.del) {
|
||||
if (!confirm(`¿Eliminar la copia local de «${b.dataset.title}» (${fmtSize(Number(b.dataset.size))})?\nTu historial de lectura no se toca.`)) return
|
||||
act(() => api(`/api/download/${b.dataset.del}`, { method: 'DELETE' }), 'Copia local eliminada')
|
||||
const shared = !!b.dataset.shared
|
||||
if (!confirm(shared ? `¿Quitar «${b.dataset.title}» de tu biblioteca descargada?\nOtro usuario también lo tiene: los archivos se quedan.`
|
||||
: `¿Eliminar la copia local de «${b.dataset.title}» (${fmtSize(Number(b.dataset.size))})?\nTu historial de lectura no se toca.`)) return
|
||||
act(() => api(`/api/download/${b.dataset.del}`, { method: 'DELETE' }), shared ? 'Quitado de tu biblioteca' : 'Copia local eliminada')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -101,7 +101,14 @@ input:focus, select:focus { border-color: var(--accent); }
|
||||
.rbar select { padding: 5px 8px; }
|
||||
.rbar input[type=range] { flex: 1; max-width: 520px; accent-color: var(--accent); }
|
||||
|
||||
#pages img { filter: var(--page-filter, none); transition: filter .2s; }
|
||||
#pages img { filter: var(--page-filter, none); transition: filter .2s, opacity .25s; }
|
||||
/* Página que aún no ha llegado: indicador de carga y fundido al aparecer */
|
||||
#pages .stage img.pending { opacity: 0; }
|
||||
.paged.waiting::after, .paged .stage.loading::after { content: ''; position: absolute; left: 50%; top: 50%; width: 34px; height: 34px;
|
||||
margin: -17px 0 0 -17px; border-radius: 50%; border: 3px solid rgba(255,255,255,.18); border-top-color: var(--accent);
|
||||
animation: pgspin .8s linear infinite; z-index: 6; pointer-events: none; }
|
||||
.paged .stage.loading { min-height: 100%; }
|
||||
@keyframes pgspin { to { transform: rotate(360deg); } }
|
||||
.paged { height: 100vh; height: 100dvh; display: flex; align-items: center; justify-content: center; user-select: none; }
|
||||
.paged img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
||||
.paged.fit-width { height: auto; min-height: 100vh; align-items: flex-start; }
|
||||
|
||||
Reference in New Issue
Block a user