feat: persistent SQLite storage for bookmarks + positions keyed by content_hash

- new bookish/services/database.py: SQLite with WAL mode, tables for
  bookmarks and reading_positions, keyed by content_hash (SHA256)
- new bookish/routes/api.py: REST endpoints for CRUD bookmarks
  and save/load reading position
- upload endpoint returns content_hash (SHA256 of file)
- reader route computes SHA256 and passes BOOKISH_HASH to template
- reader.js: bookmarks + positions use API server as primary storage
  with localStorage fallback for offline/resilience
- position saves debounced (1.5s) to avoid hammering server
- .gitignore: added bookish.db
This commit is contained in:
truetype committed 2026-07-23 22:35:04 +02:00
1 parent 13e02faa8b
commit c1421665c0
8 files changed
+319 -25

No files matched your search

+114 -24
View File
@@ -20,6 +20,11 @@
// --- Shared Reader Controls ---
initReaderControls();
// Carica segnalibri e posizione dal server (se disponibile)
if (BOOKISH_HASH) {
loadBookmarksFromServer();
}
// === EPUB Reader (epub.js) ===
function initEpubReader(file) {
const viewer = document.getElementById("epubViewer");
@@ -544,30 +549,67 @@
}, 500);
}
// === Position Save/Restore ===
// === Position Save/Restore (server + localStorage fallback) ===
let _positionSaveTimer = null;
function savePosition(book, loc) {
if (!loc || !loc.start) return;
const data = {
type: "epub",
cfi: loc.start.cfi,
href: loc.start.href,
chapter_title: document.getElementById("chapterTitle").textContent,
};
// Fallback localStorage
try {
const key = `bookish_pos_${book.url() || "unknown"}`;
const data = {
cfi: loc.start ? loc.start.cfi : null,
chapter: loc.start ? loc.start.href : null,
timestamp: Date.now(),
};
localStorage.setItem(key, JSON.stringify(data));
const key = "bookish_pos_" + (BOOKISH_HASH || BOOKISH_FILE || "unknown");
localStorage.setItem(key, JSON.stringify({ ...data, timestamp: Date.now() }));
} catch (e) {}
// Server (debounced)
if (_positionSaveTimer) clearTimeout(_positionSaveTimer);
_positionSaveTimer = setTimeout(() => {
if (BOOKISH_HASH) {
fetch(`/api/position/${encodeURIComponent(BOOKISH_HASH)}`, {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(data),
}).catch(() => {});
}
}, 1500);
}
function restorePosition(book, rendition, filename) {
try {
const key = `bookish_pos_${filename}`;
const saved = localStorage.getItem(key);
if (saved) {
const data = JSON.parse(saved);
if (data.cfi) {
rendition.display(data.cfi);
async function restorePosition(book, rendition, filename) {
let cfi = null;
// Prova server
if (BOOKISH_HASH) {
try {
const resp = await fetch(`/api/position/${encodeURIComponent(BOOKISH_HASH)}`);
const data = await resp.json();
if (data.position && data.position.cfi) {
cfi = data.position.cfi;
}
}
} catch (e) {}
} catch (e) {}
}
// Fallback localStorage
if (!cfi) {
try {
const key = "bookish_pos_" + (BOOKISH_HASH || BOOKISH_FILE || filename);
const saved = localStorage.getItem(key);
if (saved) {
const data = JSON.parse(saved);
cfi = data.cfi;
}
} catch (e) {}
}
if (cfi) {
rendition.display(cfi);
}
}
// === Reader Upload ===
@@ -656,10 +698,10 @@
// === Bookmarks ===
function getBookmarksKey() {
return "bookish_bookmarks_" + (BOOKISH_FILE || "");
return "bookish_bookmarks_" + (BOOKISH_HASH || BOOKISH_FILE || "");
}
function getBookmarks() {
function getBookmarksLocal() {
try {
return JSON.parse(localStorage.getItem(getBookmarksKey()) || "[]");
} catch (e) {
@@ -667,12 +709,52 @@
}
}
function saveBookmarks(list) {
function saveBookmarksLocal(list) {
try {
localStorage.setItem(getBookmarksKey(), JSON.stringify(list));
} catch (e) {}
}
let _bookmarksCache = null;
async function loadBookmarksFromServer() {
if (!BOOKISH_HASH) return;
try {
const resp = await fetch(`/api/bookmarks/${encodeURIComponent(BOOKISH_HASH)}`);
const data = await resp.json();
if (data.bookmarks && data.bookmarks.length > 0) {
_bookmarksCache = data.bookmarks;
saveBookmarksLocal(data.bookmarks);
}
} catch (e) {
// Server unreachable — localStorage fallback handled in getBookmarks()
}
}
function getBookmarks() {
return _bookmarksCache || getBookmarksLocal();
}
async function syncBookmarkToServer(bm) {
if (!BOOKISH_HASH) return;
try {
await fetch("/api/bookmarks", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(bm),
});
} catch (e) {
// Silently fail — data is safe in localStorage
}
}
async function syncDeleteToServer(id) {
if (!BOOKISH_HASH) return;
try {
await fetch(`/api/bookmarks/${encodeURIComponent(id)}`, { method: "DELETE" });
} catch (e) {}
}
function addBookmark() {
const label = document.getElementById("chapterTitle");
const chapter = label ? label.textContent : "Posizione corrente";
@@ -709,12 +791,17 @@
}
data.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
data.chapter_title = chapter;
data.content_hash = BOOKISH_HASH || "";
const bookmarks = getBookmarks();
bookmarks.push(data);
saveBookmarks(bookmarks);
saveBookmarksLocal(bookmarks);
_bookmarksCache = bookmarks;
renderBookmarks();
// Feedback visivo
syncBookmarkToServer(data);
flashBookmarkBtn();
showToast("Segnalibro aggiunto — vedi la scheda Segnalibri nella sidebar");
}
@@ -729,8 +816,11 @@
function removeBookmark(id) {
let bookmarks = getBookmarks();
bookmarks = bookmarks.filter((b) => b.id !== id);
saveBookmarks(bookmarks);
saveBookmarksLocal(bookmarks);
_bookmarksCache = bookmarks;
renderBookmarks();
syncDeleteToServer(id);
}
function gotoBookmark(bm) {