feat: add bookmarks with toolbar button, sidebar list, and keyboard shortcut

- bookmark button 🔖 in toolbar (Ctrl+B / Cmd+B shortcut)
- addBookmark() saves current position (CFI for epub, page for pdf, scroll% for text)
- bookmarks stored in localStorage per-file (bookish_bookmarks_<filename>)
- sidebar Segnalibri tab shows clickable list with delete button
- gotoBookmark() restores the exact position per format type
- CSS styles for bm-entry, bm-label, bm-time, bm-delete
This commit is contained in:
truetype committed 2026-07-22 23:06:59 +02:00
1 parent af27d47d58
commit 069d059ef1
3 files changed
+189 -1

No files matched your search

+46 -1
View File
@@ -584,10 +584,55 @@ body {
}
#bookmarksList li {
padding: 0.5rem;
padding: 0.4rem 0.5rem;
font-size: 0.85rem;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.3rem;
}
#bookmarksList li:hover {
background: #f8f8fa;
}
.bm-entry {
flex: 1;
cursor: pointer;
padding: 0.1rem 0;
min-width: 0;
}
.bm-label {
display: block;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bm-time {
display: block;
font-size: 0.7rem;
color: #aaa;
}
.bm-delete {
background: none;
border: none;
cursor: pointer;
color: #ccc;
font-size: 0.8rem;
padding: 0.2rem 0.3rem;
border-radius: 3px;
flex-shrink: 0;
transition: color 0.2s;
}
.bm-delete:hover {
color: #e74c3c;
background: #fef0ef;
}
+142
View File
@@ -307,6 +307,20 @@
});
}
// Bookmark button
const bookmarkToggle = document.getElementById("bookmarkToggle");
if (bookmarkToggle) {
bookmarkToggle.addEventListener("click", () => addBookmark());
}
// Keyboard shortcut for bookmark (Ctrl+B / Cmd+B)
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "b") {
e.preventDefault();
addBookmark();
}
});
// Load saved theme
const savedTheme = localStorage.getItem("bookish_theme");
if (savedTheme) {
@@ -315,6 +329,9 @@
applyTheme(t);
} catch (e) {}
}
// Init bookmarks list
renderBookmarks();
}
function applyTheme(t) {
@@ -611,6 +628,131 @@
}
}
// === Bookmarks ===
function getBookmarksKey() {
return "bookish_bookmarks_" + (BOOKISH_FILE || "");
}
function getBookmarks() {
try {
return JSON.parse(localStorage.getItem(getBookmarksKey()) || "[]");
} catch (e) {
return [];
}
}
function saveBookmarks(list) {
try {
localStorage.setItem(getBookmarksKey(), JSON.stringify(list));
} catch (e) {}
}
function addBookmark() {
const label = document.getElementById("chapterTitle");
const chapter = label ? label.textContent : "Posizione corrente";
let data = { label: chapter || "Segnalibro", created: Date.now() };
if (ext === "epub" && window._rendition) {
const loc = window._rendition.currentLocation();
if (loc && loc.start) {
data.type = "epub";
data.cfi = loc.start.cfi;
data.href = loc.start.href;
data.page = loc.start.displayed ? loc.start.displayed.page : 0;
data.label = chapter + " — Pag. " + data.page;
} else {
return;
}
} else if (ext === "pdf" && window._pdfViewer) {
const page = window._pdfViewer.getCurrentPage();
data.type = "pdf";
data.page = page;
data.totalPages = window._pdfViewer.totalPages;
data.label = "Pag. " + page + " di " + data.totalPages;
} else if (["md", "txt", "html", "fb2", "docx"].includes(ext)) {
const rv = document.getElementById("readerView");
if (!rv) return;
const maxScroll = rv.scrollHeight - rv.clientHeight;
const pct = maxScroll > 0 ? Math.round((rv.scrollTop / maxScroll) * 100) : 0;
data.type = "text";
data.scrollPct = pct;
data.label = chapter + " — " + pct + "%";
} else {
return;
}
data.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
const bookmarks = getBookmarks();
bookmarks.push(data);
saveBookmarks(bookmarks);
renderBookmarks();
}
function removeBookmark(id) {
let bookmarks = getBookmarks();
bookmarks = bookmarks.filter((b) => b.id !== id);
saveBookmarks(bookmarks);
renderBookmarks();
}
function gotoBookmark(bm) {
if (bm.type === "epub" && window._rendition && bm.cfi) {
window._rendition.display(bm.cfi);
} else if (bm.type === "pdf" && window._pdfViewer && bm.page) {
window._pdfViewer.currentPage = bm.page;
window._pdfViewer.renderPage(bm.page);
updatePageInfoPdf(bm.page, window._pdfViewer.totalPages);
} else if (bm.type === "text" && bm.scrollPct !== undefined) {
const rv = document.getElementById("readerView");
if (rv) {
const maxScroll = rv.scrollHeight - rv.clientHeight;
rv.scrollTop = (bm.scrollPct / 100) * maxScroll;
}
}
}
function renderBookmarks() {
const list = document.getElementById("bookmarksList");
if (!list) return;
const bookmarks = getBookmarks();
if (bookmarks.length === 0) {
list.innerHTML = '<li style="color:#999;cursor:default;font-style:italic;display:block;padding:0.5rem">Nessun segnalibro</li>';
return;
}
list.innerHTML = bookmarks
.map(
(bm) =>
`<li data-id="${bm.id}">
<span class="bm-entry" title="Vai a questo segnalibro">
<span class="bm-label">${escapeHtml(bm.label || "Segnalibro")}</span>
<span class="bm-time">${new Date(bm.created).toLocaleDateString("it")}</span>
</span>
<button class="bm-delete" title="Elimina">✕</button>
</li>`
)
.join("");
list.querySelectorAll(".bm-entry").forEach((el) => {
el.addEventListener("click", () => {
const id = el.closest("li").dataset.id;
const bm = bookmarks.find((b) => b.id === id);
if (bm) gotoBookmark(bm);
});
});
list.querySelectorAll(".bm-delete").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = btn.closest("li").dataset.id;
removeBookmark(id);
});
});
}
// === Utils ===
function debounce(fn, ms) {
let timer;
+1
View File
@@ -26,6 +26,7 @@
<button class="tool-btn" id="zoomIn" title="Zoom +">A+</button>
<button class="tool-btn" id="themeToggle" title="Tema">☀️</button>
<button class="tool-btn" id="searchToggle" title="Cerca">🔍</button>
<button class="tool-btn" id="bookmarkToggle" title="Segnalibro (Ctrl+B)">🔖</button>
<button class="tool-btn" id="fullscreenToggle" title="Schermo intero">⛶</button>
</div>
</div>