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:
1 parent
13e02faa8b
commit
c1421665c0
8 files changed
+319
-25
No files matched your search
+114
-24
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user