251 lines
11 KiB
HTML
251 lines
11 KiB
HTML
<button class="sidebar-toggle" id="sidebar-toggle" onclick="toggleSidebar()" aria-label="Menu">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<line x1="3" y1="6" x2="21" y2="6"/>
|
|
<line x1="3" y1="12" x2="21" y2="12"/>
|
|
<line x1="3" y1="18" x2="21" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
<div class="sidebar-overlay" id="sidebar-overlay" onclick="closeSidebar()"></div>
|
|
|
|
<aside class="sidebar" id="sidebar">
|
|
<div class="sidebar-logo">
|
|
<a href="/home" style="text-decoration:none;color:inherit"><h1>No<span>vela</span></h1></a>
|
|
</div>
|
|
|
|
<ul class="sidebar-nav">
|
|
<li>
|
|
<a href="/home"{% if active == 'home' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
|
<polyline points="9 22 9 12 15 12 15 22"/>
|
|
</svg>
|
|
Home
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<hr class="sidebar-divider"/>
|
|
|
|
<div class="sidebar-section-label">Library</div>
|
|
<ul class="sidebar-nav"{% if active == 'library' %} id="lib-nav"{% endif %}>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% else %}/library{% endif %}"
|
|
{% if active == 'library' %}id="nav-all" class="active" onclick="switchView('all'); return false;"
|
|
{% elif active == 'book' %}class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
|
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
|
</svg>
|
|
All books
|
|
<span class="sidebar-count" id="count-all"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% elif active == 'book' %}/library#wtr{% else %}/library#wtr{% endif %}"
|
|
{% if active == 'library' %}id="nav-wtr" onclick="switchView('wtr'); return false;"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
|
</svg>
|
|
Want to Read
|
|
<span class="sidebar-count" id="count-wtr"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% else %}/library#new{% endif %}"
|
|
{% if active == 'library' %}id="nav-new" onclick="switchView('new'); return false;"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 3v18"/><path d="M3 12h18"/>
|
|
</svg>
|
|
New
|
|
<span class="sidebar-count" id="count-new"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% else %}/library#series{% endif %}"
|
|
{% if active == 'library' %}id="nav-series" onclick="switchView('series'); return false;"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="6" height="18" rx="1"/>
|
|
<rect x="9" y="3" width="6" height="18" rx="1"/>
|
|
<rect x="16" y="3" width="6" height="18" rx="1"/>
|
|
</svg>
|
|
Series
|
|
<span class="sidebar-count" id="count-series"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% else %}/library#authors{% endif %}"
|
|
{% if active == 'library' %}id="nav-authors" onclick="switchView('authors'); return false;"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
Authors
|
|
<span class="sidebar-count" id="count-authors"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% else %}/library#publishers{% endif %}"
|
|
{% if active == 'library' %}id="nav-publishers" onclick="switchView('publishers'); return false;"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M3 21h18"/>
|
|
<path d="M5 21V7l7-4 7 4v14"/>
|
|
<path d="M9 21v-6h6v6"/>
|
|
</svg>
|
|
Publishers
|
|
<span class="sidebar-count" id="count-publishers"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{% if active == 'library' %}#{% else %}/library#archived{% endif %}"
|
|
{% if active == 'library' %}id="nav-archived" onclick="switchView('archived'); return false;"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="21 8 21 21 3 21 3 8"/>
|
|
<rect x="1" y="3" width="22" height="5"/>
|
|
<line x1="10" y1="12" x2="14" y2="12"/>
|
|
</svg>
|
|
Archived
|
|
<span class="sidebar-count" id="count-archived"></span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/stats"{% if active == 'stats' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="20" x2="18" y2="10"/>
|
|
<line x1="12" y1="20" x2="12" y2="4"/>
|
|
<line x1="6" y1="20" x2="6" y2="14"/>
|
|
</svg>
|
|
Statistics
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<hr class="sidebar-divider"/>
|
|
|
|
<div class="sidebar-section-label">Tools</div>
|
|
<ul class="sidebar-nav">
|
|
<li>
|
|
<a href="/convert"{% if active == 'convert' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
|
|
</svg>
|
|
Convert
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/credentials-manager"{% if active == 'credentials' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 010 14.14M4.93 4.93a10 10 0 000 14.14"/>
|
|
</svg>
|
|
Credentials
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/debug"{% if active == 'debug' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
</svg>
|
|
Debug
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/backup"{% if active == 'backup' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 8v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8"/>
|
|
<polyline points="1 8 12 2 23 8"/>
|
|
<path d="M12 22v-8"/>
|
|
</svg>
|
|
Backup
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/settings"{% if active == 'settings' %} class="active"{% endif %}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>
|
|
Settings
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="sidebar-bottom">
|
|
<button class="btn-rescan" onclick="rescanLibraryGlobal()" id="rescan-btn">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
</svg>
|
|
<span id="rescan-label">Rescan library</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<script>
|
|
function toggleSidebar() {
|
|
document.getElementById('sidebar').classList.toggle('open');
|
|
document.getElementById('sidebar-overlay').classList.toggle('open');
|
|
}
|
|
function closeSidebar() {
|
|
document.getElementById('sidebar').classList.remove('open');
|
|
document.getElementById('sidebar-overlay').classList.remove('open');
|
|
}
|
|
// Close sidebar on any nav link click (mobile)
|
|
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
|
a.addEventListener('click', () => { if (window.innerWidth <= 768) closeSidebar(); });
|
|
});
|
|
|
|
function applyLibraryCounts(books) {
|
|
const active = books.filter(b => !b.archived);
|
|
const wtrCount = active.filter(b => b.want_to_read).length;
|
|
const newCount = active.filter(b => b.needs_review).length;
|
|
const seriesCount = new Set(active.filter(b => b.series).map(b => b.series)).size;
|
|
const authorCount = new Set(active.map(b => b.author).filter(Boolean)).size;
|
|
const publisherCount = new Set(active.map(b => b.publisher).filter(Boolean)).size;
|
|
const archivedCount = books.filter(b => b.archived).length;
|
|
|
|
const setCount = (id, value) => {
|
|
const el = document.getElementById(id);
|
|
if (el) el.textContent = value || '';
|
|
};
|
|
|
|
setCount('count-all', active.length);
|
|
setCount('count-wtr', wtrCount);
|
|
setCount('count-new', newCount);
|
|
setCount('count-series', seriesCount);
|
|
setCount('count-authors', authorCount);
|
|
setCount('count-publishers', publisherCount);
|
|
setCount('count-archived', archivedCount);
|
|
}
|
|
|
|
async function refreshLibraryCounts() {
|
|
try {
|
|
const resp = await fetch('/library/list');
|
|
if (!resp.ok) return;
|
|
const books = await resp.json();
|
|
applyLibraryCounts(books);
|
|
} catch (_) {
|
|
// silently ignore; sidebar remains usable without counts
|
|
}
|
|
}
|
|
|
|
async function rescanLibraryGlobal() {
|
|
const btn = document.getElementById('rescan-btn');
|
|
const label = document.getElementById('rescan-label');
|
|
if (btn) btn.disabled = true;
|
|
if (label) label.textContent = 'Scanning…';
|
|
try {
|
|
const resp = await fetch('/library/rescan', { method: 'POST' });
|
|
if (!resp.ok) throw new Error('rescan failed');
|
|
await refreshLibraryCounts();
|
|
} catch (_) {
|
|
alert('Rescan failed.');
|
|
} finally {
|
|
if (btn) btn.disabled = false;
|
|
if (label) label.textContent = 'Rescan library';
|
|
}
|
|
}
|
|
|
|
refreshLibraryCounts();
|
|
</script>
|