Iconos SVG en el menú y en los botones
Iconos en línea (estilo Lucide, sin dependencias) en el menú superior, pestañas, ficha del manga, lector, historial, descargas y ajustes. En móvil el menú muestra solo los iconos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+61
-29
@@ -1,4 +1,36 @@
|
||||
const app = document.getElementById('app')
|
||||
// Iconos SVG en línea (trazo estilo Lucide), heredan el color del texto.
|
||||
const ICONS = {
|
||||
home: '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/><path d="M10 21v-6h4v6"/>',
|
||||
history: '<path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l3 2"/>',
|
||||
download: '<path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/>',
|
||||
settings: '<path d="M4 6h9M17 6h3M4 12h3M11 12h9M4 18h11M19 18h1"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="17" cy="18" r="2"/>',
|
||||
play: '<path d="M7 4v16l13-8z"/>',
|
||||
book: '<path d="M2 5h6a4 4 0 0 1 4 4v12a3 3 0 0 0-3-3H2z"/><path d="M22 5h-6a4 4 0 0 0-4 4v12a3 3 0 0 1 3-3h7z"/>',
|
||||
stopRead: '<circle cx="12" cy="12" r="9"/><path d="m15 9-6 6M9 9l6 6"/>',
|
||||
stop: '<rect x="6" y="6" width="12" height="12" rx="1.5"/>',
|
||||
trash: '<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M6 6l1 15h10l1-15"/><path d="M10 11v6M14 11v6"/>',
|
||||
refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.4L21 8"/><path d="M21 3v5h-5"/>',
|
||||
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
|
||||
sort: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
||||
back: '<path d="M19 12H5"/><path d="m12 19-7-7 7-7"/>',
|
||||
prev: '<path d="m15 18-6-6 6-6"/>',
|
||||
next: '<path d="m9 18 6-6-6-6"/>',
|
||||
more: '<path d="m6 9 6 6 6-6"/>',
|
||||
check: '<path d="M20 6 9 17l-5-5"/>',
|
||||
login: '<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/>',
|
||||
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
|
||||
userPlus: '<circle cx="9" cy="8" r="4"/><path d="M2 21a7 7 0 0 1 14 0"/><path d="M19 8v6M16 11h6"/>',
|
||||
flame: '<path d="M12 22c3.9 0 7-2.8 7-7 0-3.5-2.4-5.6-3.6-8.6-.9 1.8-1.9 2.9-3 3.2.2-3.1-.9-5.6-3-7.6.3 4-4.4 6.8-4.4 12.6C5 19.2 8.1 22 12 22z"/>',
|
||||
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
grid: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
|
||||
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z"/>',
|
||||
key: '<circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M17 6l3 3"/>',
|
||||
}
|
||||
const icon = name => `<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name]}</svg>`
|
||||
// Iconos del menú superior (index.html).
|
||||
document.querySelectorAll('#topbar nav a[data-icon]').forEach(a => { a.innerHTML = `${icon(a.dataset.icon)}<span>${a.textContent}</span>` })
|
||||
|
||||
const SOURCES = { lc: 'LeerCapitulo (español)', md: 'MangaDex (multi-idioma)' }
|
||||
// Filtros de imagen del lector (CSS filter); el brillo se suma aparte.
|
||||
const FILTERS = {
|
||||
@@ -114,8 +146,8 @@ function viewAuth(signup) {
|
||||
<input type="text" name="username" placeholder="Usuario" autocomplete="username" required>
|
||||
<input type="password" name="password" placeholder="Contraseña" autocomplete="current-password" required>
|
||||
<p class="error"></p>
|
||||
<button class="btn primary" value="login">Entrar</button>
|
||||
${signup ? '<button class="btn" value="register">Crear cuenta</button>' : ''}
|
||||
<button class="btn primary" value="login">${icon('login')} Entrar</button>
|
||||
${signup ? `<button class="btn" value="register">${icon('userPlus')} Crear cuenta</button>` : ''}
|
||||
</form>
|
||||
</div>`
|
||||
const form = $('form', app)
|
||||
@@ -160,15 +192,15 @@ async function viewHome(params) {
|
||||
app.innerHTML = `
|
||||
<section id="cont"></section>
|
||||
<div class="tabs" id="sources">${Object.entries(SOURCES).map(([k, v]) => `<button data-s="${k}" class="${k === source ? 'on' : ''}">${v}</button>`).join('')}</div>
|
||||
<form class="search"><input type="search" name="q" placeholder="Buscar ${tag ? `en «${esc(tagName)}»` : `en ${esc(SOURCES[source])}`}…" value="${esc(q)}"><button class="btn primary">Buscar</button></form>
|
||||
<form class="search"><input type="search" name="q" placeholder="Buscar ${tag ? `en «${esc(tagName)}»` : `en ${esc(SOURCES[source])}`}…" value="${esc(q)}"><button class="btn primary">${icon('search')}<span class="lbl">Buscar</span></button></form>
|
||||
<label class="muted row" style="font-size:.88rem" ${source === 'md' ? '' : 'hidden'}><input type="checkbox" name="any" ${anyLang ? 'checked' : ''}> Incluir mangas sin traducción a ${esc(LANGS[me.lang] || me.lang)}</label>
|
||||
${q && !tag ? `<h2>Resultados para “${esc(q)}”</h2>` : `<div class="tabs" id="modes">
|
||||
<button data-m="popular" class="${mode === 'popular' ? 'on' : ''}">Populares</button>
|
||||
<button data-m="latest" class="${mode === 'latest' ? 'on' : ''}">Actualizados recientemente</button>
|
||||
<button data-m="cats" class="${mode === 'cats' ? 'on' : ''}">Categorías</button></div>`}
|
||||
<button data-m="popular" class="${mode === 'popular' ? 'on' : ''}">${icon('flame')} Populares</button>
|
||||
<button data-m="latest" class="${mode === 'latest' ? 'on' : ''}">${icon('clock')} Actualizados recientemente</button>
|
||||
<button data-m="cats" class="${mode === 'cats' ? 'on' : ''}">${icon('grid')} Categorías</button></div>`}
|
||||
${tag ? `<h2>Categoría: <a class="tag on" href="#/?source=${source}&mode=cats" title="Quitar filtro">${esc(tagName)} ✕</a>${q ? ` · “${esc(q)}”` : ''}</h2>` : ''}
|
||||
<div id="results" class="loading">Cargando…</div>
|
||||
<div class="row" style="justify-content:center;margin:16px 0"><button class="btn" id="more" hidden>Cargar más</button></div>`
|
||||
<div class="row" style="justify-content:center;margin:16px 0"><button class="btn" id="more" hidden>${icon('more')} Cargar más</button></div>`
|
||||
const form = $('form', app)
|
||||
const go = (extra = {}) => {
|
||||
const p = new URLSearchParams({ source, ...(form.q.value ? { q: form.q.value } : { mode }), ...(tag ? { tag, tagName } : {}), ...($('[name=any]').checked ? { any: '1' } : {}), ...extra })
|
||||
@@ -230,11 +262,11 @@ async function viewManga(id, langOverride) {
|
||||
const first = chapters[0]
|
||||
let mainBtn = ''
|
||||
if (progress && chapters.some(c => c.id === progress.chapter_id)) {
|
||||
mainBtn = `<a class="btn primary" href="${readUrl(id, progress.chapter_id, progress.page)}">Continuar · ${progress.chapter_num ? `Cap. ${esc(progress.chapter_num)}` : 'Oneshot'}, pág. ${progress.page + 1}</a>`
|
||||
mainBtn = `<a class="btn primary" href="${readUrl(id, progress.chapter_id, progress.page)}">${icon('play')} Continuar · ${progress.chapter_num ? `Cap. ${esc(progress.chapter_num)}` : 'Oneshot'}, pág. ${progress.page + 1}</a>`
|
||||
} else if (progress) {
|
||||
mainBtn = `<a class="btn primary" href="${readUrl(id, progress.chapter_id, progress.page)}">Continuar · Cap. ${esc(progress.chapter_num ?? '?')} (${esc(LANGS[progress.lang] || progress.lang || '')}), pág. ${progress.page + 1}</a>`
|
||||
mainBtn = `<a class="btn primary" href="${readUrl(id, progress.chapter_id, progress.page)}">${icon('play')} Continuar · Cap. ${esc(progress.chapter_num ?? '?')} (${esc(LANGS[progress.lang] || progress.lang || '')}), pág. ${progress.page + 1}</a>`
|
||||
}
|
||||
if (first && !mainBtn) mainBtn = `<a class="btn primary" href="${readUrl(id, first.id)}">Empezar a leer</a>`
|
||||
if (first && !mainBtn) mainBtn = `<a class="btn primary" href="${readUrl(id, first.id)}">${icon('book')} Empezar a leer</a>`
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="detail">
|
||||
@@ -245,9 +277,9 @@ async function viewManga(id, langOverride) {
|
||||
<div class="tags">${manga.tags.map(t => (t.id ? `<a class="tag" href="${tagHref(manga.source, t)}">${esc(t.name)}</a>` : `<span class="tag">${esc(t.name ?? t)}</span>`)).join('')}</div>
|
||||
<p class="desc">${esc(manga.description)}</p>
|
||||
<div class="row">${mainBtn}
|
||||
${progress || reads.length ? '<button class="btn danger" id="drop">Dejar de leer</button>' : ''}
|
||||
${progress || reads.length ? `<button class="btn danger" id="drop">${icon('stopRead')} Dejar de leer</button>` : ''}
|
||||
${isLc ? '' : `<select id="lang">${langOpts.map(l => `<option value="${esc(l)}" ${l === lang ? 'selected' : ''}>${esc(LANGS[l] || l)}</option>`).join('')}</select>
|
||||
<a class="btn" href="${altSearch}">Buscar en LeerCapitulo</a>`}
|
||||
<a class="btn" href="${altSearch}">${icon('search')} Buscar en LeerCapitulo</a>`}
|
||||
</div>
|
||||
<div class="muted" style="font-size:.85rem;margin-top:6px">Fuente: ${esc(SOURCES[manga.source])}${download ? ` · Carpeta: mangas/${esc(download.folder)}` : ''}</div>
|
||||
${offline ? '<p class="error">Sin conexión con la fuente: mostrando solo los capítulos descargados.</p>' : ''}
|
||||
@@ -256,7 +288,7 @@ async function viewManga(id, langOverride) {
|
||||
</div>
|
||||
</div>
|
||||
<h2>Capítulos <span class="muted">(${chapters.length})</span>
|
||||
${chapters.length > 1 ? '<button class="btn small" id="order" style="float:right">Invertir orden</button>' : ''}</h2>
|
||||
${chapters.length > 1 ? `<button class="btn small" id="order" style="float:right">${icon('sort')} Invertir orden</button>` : ''}</h2>
|
||||
${chapters.length ? '<ul class="chapters"></ul>' : isLc ? '<p class="muted">Este manga no tiene capítulos.</p>' : `<p class="muted">No hay capítulos legibles en ${esc(LANGS[lang] || lang)} (MangaDex retira algunos por licencia).
|
||||
Prueba a <a class="tag" href="${altSearch}">buscarlo en LeerCapitulo</a>.
|
||||
${chapterLangs.length ? `Disponibles en: ${chapterLangs.map(l => `<a class="tag" href="#" data-lang="${esc(l)}">${esc(LANGS[l] || l)}</a>`).join(' ')}` : 'No hay capítulos en ningún idioma.'}</p>`}`
|
||||
@@ -266,12 +298,12 @@ async function viewManga(id, langOverride) {
|
||||
const box = $('#dl')
|
||||
if (!box) return
|
||||
const active = d && DL_ACTIVE.includes(d.status)
|
||||
const delBtn = d ? `<button class="btn small danger" id="dldel">Eliminar copia local${d.size ? ` (${fmtSize(d.size)})` : ''}</button>` : ''
|
||||
const delBtn = d ? `<button class="btn small danger" id="dldel">${icon('trash')} Eliminar copia local${d.size ? ` (${fmtSize(d.size)})` : ''}</button>` : ''
|
||||
const bar = d && (active || d.done < d.total) ? `<div class="bar dlbar"><i style="width:${downloadPct(d).toFixed(1)}%"></i></div>` : ''
|
||||
let html
|
||||
if (!d || d.status === 'cancelled') html = `<div class="row"><button class="btn" id="dlgo">⬇ Descargar${d ? ` (llevas ${d.done}/${d.total})` : ''}</button>${delBtn}</div>${bar}`
|
||||
else if (active) html = `<div class="row"><span>${downloadLabel(d)} · ${Math.floor(downloadPct(d))}%</span><button class="btn small" id="dlcancel">Cancelar</button>${delBtn}</div>${bar}<div class="muted" style="font-size:.8rem">${downloadDetail(d)}</div>`
|
||||
else html = `<div class="row"><span class="muted">✓ ${downloadLabel(d)}</span><button class="btn small" id="dlgo" title="Descarga los capítulos que falten">${d.status === 'error' ? 'Reintentar' : 'Buscar nuevos'}</button>${delBtn}</div>${bar}`
|
||||
if (!d || d.status === 'cancelled') html = `<div class="row"><button class="btn" id="dlgo">${icon('download')} Descargar${d ? ` (llevas ${d.done}/${d.total})` : ''}</button>${delBtn}</div>${bar}`
|
||||
else if (active) html = `<div class="row"><span>${downloadLabel(d)} · ${Math.floor(downloadPct(d))}%</span><button class="btn small" id="dlcancel">${icon('stop')} Cancelar</button>${delBtn}</div>${bar}<div class="muted" style="font-size:.8rem">${downloadDetail(d)}</div>`
|
||||
else html = `<div class="row"><span class="muted row" style="gap:4px">${icon('check')} ${downloadLabel(d)}</span><button class="btn small" id="dlgo" title="Descarga los capítulos que falten">${icon('refresh')} ${d.status === 'error' ? 'Reintentar' : 'Buscar nuevos'}</button>${delBtn}</div>${bar}`
|
||||
box.innerHTML = offline ? (d ? `<span class="muted">${downloadLabel(d)}</span>` : '') : html
|
||||
$('#dlerr').textContent = d?.status === 'error' ? d.error || '' : ''
|
||||
$('#dlgo')?.addEventListener('click', async () => {
|
||||
@@ -312,7 +344,7 @@ async function viewManga(id, langOverride) {
|
||||
const r = readMap.get(c.id)
|
||||
const done = r && r.total_pages && r.page >= r.total_pages - 1
|
||||
const cls = progress?.chapter_id === c.id ? 'current' : done ? 'read' : ''
|
||||
const meta = (dlSet.has(c.id) ? '<span title="Descargado">⬇</span> ' : '') + (r ? (done ? 'Leído' : `pág. ${r.page + 1}/${r.total_pages}`) : esc(c.group))
|
||||
const meta = (dlSet.has(c.id) ? `<span title="Descargado">${icon('download')}</span> ` : '') + (r ? (done ? 'Leído' : `pág. ${r.page + 1}/${r.total_pages}`) : esc(c.group))
|
||||
return `<li><a class="${cls}" href="${readUrl(id, c.id, r && !done ? r.page : (done ? 0 : null))}"><span>${esc(chapLabel(c))}</span><span class="meta">${meta}</span></a></li>`
|
||||
}).join('')
|
||||
}
|
||||
@@ -358,7 +390,7 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
app.innerHTML = `
|
||||
<div class="reader">
|
||||
<div class="rbar top">
|
||||
<a class="btn small" href="#/manga/${mangaId}">←</a>
|
||||
<a class="btn small" href="#/manga/${mangaId}" title="Volver a la ficha">${icon('back')}</a>
|
||||
<div class="ttl"><b>${esc(manga.title)}</b></div>
|
||||
<select id="chap" style="max-width:40vw">${chapOpts}</select>
|
||||
<select id="mode"><option value="paged">Por página</option><option value="vertical">Vertical</option></select>
|
||||
@@ -370,10 +402,10 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
</div>
|
||||
<div id="pages"></div>
|
||||
<div class="rbar bottom">
|
||||
${prev ? `<a class="btn small" href="${readUrl(mangaId, prev.id)}">« Cap. ant.</a>` : ''}
|
||||
${prev ? `<a class="btn small" href="${readUrl(mangaId, prev.id)}">${icon('prev')} Cap. ant.</a>` : ''}
|
||||
<span id="pnum" class="muted"></span>
|
||||
<input type="range" id="slider" min="1" max="${total}" value="${page + 1}">
|
||||
${next ? `<a class="btn small" href="${readUrl(mangaId, next.id)}">Cap. sig. »</a>` : ''}
|
||||
${next ? `<a class="btn small" href="${readUrl(mangaId, next.id)}">Cap. sig. ${icon('next')}</a>` : ''}
|
||||
</div>
|
||||
</div>`
|
||||
const reader = $('.reader', app)
|
||||
@@ -391,8 +423,8 @@ async function viewReader(mangaId, chapterId, startPage) {
|
||||
applyFilter()
|
||||
if ($('#saver')) $('#saver').checked = settings.saver
|
||||
const endBlock = `<div class="chapter-end"><p class="muted">Fin del capítulo ${esc(chapter.num ?? '')}</p>
|
||||
${next ? `<a class="btn primary" href="${readUrl(mangaId, next.id)}">Siguiente: ${esc(chapLabel(next))}</a>` : '<p>No hay más capítulos disponibles.</p>'}
|
||||
<a class="btn" href="#/manga/${mangaId}">Volver a la ficha</a></div>`
|
||||
${next ? `<a class="btn primary" href="${readUrl(mangaId, next.id)}">${icon('next')} Siguiente: ${esc(chapLabel(next))}</a>` : '<p>No hay más capítulos disponibles.</p>'}
|
||||
<a class="btn" href="#/manga/${mangaId}">${icon('back')} Volver a la ficha</a></div>`
|
||||
|
||||
const setPageLabel = () => { $('#pnum').textContent = `${page + 1} / ${total}`; slider.value = page + 1 }
|
||||
let observer = null
|
||||
@@ -509,8 +541,8 @@ async function viewHistory() {
|
||||
<div class="cont">
|
||||
${continueCard(h).replace('class="cont"', 'style="display:flex;gap:12px;flex:1;min-width:0;align-items:center"')}
|
||||
<div class="row" style="flex:none">
|
||||
<a class="btn small" href="#/manga/${h.manga_id}">Ficha</a>
|
||||
<button class="btn small danger" data-del="${h.manga_id}">Borrar</button>
|
||||
<a class="btn small" href="#/manga/${h.manga_id}">${icon('book')} Ficha</a>
|
||||
<button class="btn small danger" data-del="${h.manga_id}">${icon('trash')} Borrar</button>
|
||||
</div>
|
||||
</div>`).join('')}</div>`
|
||||
app.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', async () => {
|
||||
@@ -538,8 +570,8 @@ async function viewDownloads() {
|
||||
${d.status === 'error' ? `<div class="error" style="font-size:.8rem">${esc(d.error || '')}</div>` : ''}
|
||||
<div class="bar dlbar"><i style="width:${downloadPct(d).toFixed(1)}%"></i></div></a>
|
||||
<div class="row" style="flex:none">
|
||||
${active ? `<button class="btn small" data-cancel="${esc(d.manga_id)}">Cancelar</button>` : ''}
|
||||
<button class="btn small danger" data-del="${esc(d.manga_id)}" data-title="${esc(d.title)}" data-size="${d.size || 0}">Eliminar</button>
|
||||
${active ? `<button class="btn small" data-cancel="${esc(d.manga_id)}">${icon('stop')} Cancelar</button>` : ''}
|
||||
<button class="btn small danger" data-del="${esc(d.manga_id)}" data-title="${esc(d.title)}" data-size="${d.size || 0}">${icon('trash')} Eliminar</button>
|
||||
</div></div>`
|
||||
}).join('')}</div>`
|
||||
app.querySelectorAll('[data-cancel]').forEach(b => b.addEventListener('click', async () => { await api(`/api/download/${b.dataset.cancel}/cancel`, { method: 'POST' }); viewDownloads() }))
|
||||
@@ -560,14 +592,14 @@ function viewSettings() {
|
||||
app.innerHTML = `<h1>Ajustes</h1>
|
||||
<div class="settings">
|
||||
<section><b>Cuenta</b><div class="muted">Sesión iniciada como <b>${esc(me.username)}</b></div>
|
||||
<button class="btn" id="logout">Cerrar sesión</button></section>
|
||||
<button class="btn" id="logout">${icon('logout')} Cerrar sesión</button></section>
|
||||
<section><b>Idioma preferido de los capítulos</b>
|
||||
<select id="lang">${Object.entries(LANGS).map(([k, v]) => `<option value="${k}" ${k === me.lang ? 'selected' : ''}>${v}</option>`).join('')}</select></section>
|
||||
<section><b>Cambiar contraseña</b>
|
||||
<form id="pw" style="display:grid;gap:8px">
|
||||
<input type="password" name="old" placeholder="Contraseña actual" autocomplete="current-password" required>
|
||||
<input type="password" name="new" placeholder="Nueva contraseña" autocomplete="new-password" required minlength="6">
|
||||
<button class="btn primary">Guardar</button></form></section>
|
||||
<button class="btn primary">${icon('key')} Guardar</button></form></section>
|
||||
</div>`
|
||||
$('#logout').addEventListener('click', async () => { await api('/api/logout', { method: 'POST' }); me = null; location.hash = '#/login'; router() })
|
||||
$('#lang').addEventListener('change', async e => { await api('/api/settings', { method: 'POST', body: { lang: e.target.value } }); me.lang = e.target.value; toast('Idioma guardado') })
|
||||
|
||||
Reference in New Issue
Block a user