Search / static /reader-store.js
vomebook's picture
Improve Reader bookmarks and navigation
8554abc verified
Raw
History Blame Contribute Delete
6.26 kB
(function (root) {
"use strict";
const DB_NAME = "voiceofml-reader";
const STORE_NAME = "entries";
const BOOKMARK_STORE_NAME = "bookmarks";
const MAX_ENTRIES = 200;
let databasePromise = null;
function openDatabase() {
if (databasePromise) return databasePromise;
const opening = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 2);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains(STORE_NAME)) {
const store = database.createObjectStore(STORE_NAME, { keyPath: "url" });
store.createIndex("lastReadAt", "lastReadAt");
}
if (!database.objectStoreNames.contains(BOOKMARK_STORE_NAME)) {
const bookmarks = database.createObjectStore(BOOKMARK_STORE_NAME, { keyPath: "id" });
bookmarks.createIndex("url", "url");
bookmarks.createIndex("createdAt", "createdAt");
}
};
request.onsuccess = () => {
const database = request.result;
database.onversionchange = () => { database.close(); if (databasePromise === opening) databasePromise = null; };
if (databasePromise !== opening) { database.close(); return; }
resolve(database);
};
request.onblocked = () => { if (databasePromise === opening) databasePromise = null; reject(new Error("Reader database upgrade was blocked by another tab")); };
request.onerror = () => { if (databasePromise === opening) databasePromise = null; reject(request.error); };
});
databasePromise = opening;
return databasePromise;
}
async function transaction(mode, callback) {
const database = await openDatabase();
return new Promise((resolve, reject) => {
const tx = database.transaction(STORE_NAME, mode);
const result = callback(tx.objectStore(STORE_NAME));
tx.oncomplete = () => resolve(result);
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
});
}
async function get(url) {
const database = await openDatabase();
return new Promise((resolve, reject) => {
const tx = database.transaction(STORE_NAME, "readonly");
const request = tx.objectStore(STORE_NAME).get(url);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
}
async function put(entry) {
const database = await openDatabase();
await new Promise((resolve, reject) => {
const tx = database.transaction(STORE_NAME, "readwrite");
const store = tx.objectStore(STORE_NAME);
const existing = store.get(entry.url);
existing.onsuccess = () => {
if (existing.result) {
store.put(entry);
return;
}
const count = store.count();
count.onsuccess = () => {
if (count.result < MAX_ENTRIES) {
store.put(entry);
return;
}
const oldest = store.index("lastReadAt").openCursor();
oldest.onsuccess = () => {
if (oldest.result) oldest.result.delete();
store.put(entry);
};
};
};
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
});
}
async function list(limit = MAX_ENTRIES) {
const database = await openDatabase();
return new Promise((resolve, reject) => {
const entries = [];
const tx = database.transaction(STORE_NAME, "readonly");
const request = tx.objectStore(STORE_NAME).index("lastReadAt").openCursor(null, "prev");
request.onsuccess = () => {
const cursor = request.result;
if (!cursor || entries.length >= limit) return resolve(entries);
entries.push(cursor.value); cursor.continue();
};
request.onerror = () => reject(request.error);
});
}
async function remove(url) { await transaction("readwrite", (store) => store.delete(url)); }
async function clearHistory() { await transaction("readwrite", (store) => store.clear()); }
async function putBookmark(entry) {
const database = await openDatabase();
await new Promise((resolve, reject) => {
const tx = database.transaction(BOOKMARK_STORE_NAME, "readwrite");
tx.objectStore(BOOKMARK_STORE_NAME).put(entry);
tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error);
});
}
async function listBookmarks(url) {
const database = await openDatabase();
return new Promise((resolve, reject) => {
const entries = [];
const tx = database.transaction(BOOKMARK_STORE_NAME, "readonly");
const request = tx.objectStore(BOOKMARK_STORE_NAME).index("url").openCursor(IDBKeyRange.only(url));
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) return resolve(entries.sort((a, b) => b.createdAt - a.createdAt));
entries.push(cursor.value); cursor.continue();
};
request.onerror = () => reject(request.error);
});
}
async function listAllBookmarks() {
const database = await openDatabase();
return new Promise((resolve, reject) => {
const entries = [];
const tx = database.transaction(BOOKMARK_STORE_NAME, "readonly");
const request = tx.objectStore(BOOKMARK_STORE_NAME).index("createdAt").openCursor(null, "prev");
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) return resolve(entries);
entries.push(cursor.value); cursor.continue();
};
request.onerror = () => reject(request.error);
});
}
async function removeBookmark(id) {
const database = await openDatabase();
await new Promise((resolve, reject) => {
const tx = database.transaction(BOOKMARK_STORE_NAME, "readwrite");
tx.objectStore(BOOKMARK_STORE_NAME).delete(id);
tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error);
});
}
root.VoiceOfMLReaderStore = Object.freeze({ DB_NAME, MAX_ENTRIES, get, put, list, remove, clearHistory, putBookmark, listBookmarks, listAllBookmarks, removeBookmark });
})(typeof self !== "undefined" ? self : window);