diff --git a/bookish/static/css/style.css b/bookish/static/css/style.css index 38e2d8e..f494252 100644 --- a/bookish/static/css/style.css +++ b/bookish/static/css/style.css @@ -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; } diff --git a/bookish/static/js/reader.js b/bookish/static/js/reader.js index 6f11f7a..26eff62 100644 --- a/bookish/static/js/reader.js +++ b/bookish/static/js/reader.js @@ -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 = '
  • Nessun segnalibro
  • '; + return; + } + + list.innerHTML = bookmarks + .map( + (bm) => + `
  • + + ${escapeHtml(bm.label || "Segnalibro")} + ${new Date(bm.created).toLocaleDateString("it")} + + +
  • ` + ) + .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; diff --git a/bookish/templates/reader.html b/bookish/templates/reader.html index abf7fae..7c0c983 100644 --- a/bookish/templates/reader.html +++ b/bookish/templates/reader.html @@ -26,6 +26,7 @@ +