Download static/reader-store.js from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 8.93 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader-store.js
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader-store.js
-
curl -L -o reader-store.js https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader-store.js
8.93 kB
| (function (root) { | |
| "use strict"; | |
| const DB_NAME = "voiceofml-reader"; | |
| const STORE_NAME = "entries"; | |
| const BOOKMARK_STORE_NAME = "bookmarks"; | |
| const MAX_ENTRIES = 200; | |
| const SCHEMA_VERSION = 1; | |
| const DB_VERSION = 3; | |
| let databasePromise = null; | |
| const subscribers = new Set(); | |
| const storeChannel = | |
| typeof root.BroadcastChannel === "function" | |
| ? new root.BroadcastChannel("voiceofml-reader-store-v1") | |
| : null; | |
| let disposed = false; | |
| if (storeChannel) | |
| storeChannel.onmessage = (event) => { | |
| if (disposed) return; | |
| for (const listener of subscribers) listener({ ...event.data, remote: true }); | |
| }; | |
| function notify(change) { | |
| if (disposed) return; | |
| try { | |
| storeChannel?.postMessage(change); | |
| } catch (_) {} | |
| for (const listener of subscribers) listener({ ...change, remote: false }); | |
| } | |
| function subscribe(listener) { | |
| if (typeof listener !== "function") | |
| throw new TypeError("Reader store subscriber must be a function"); | |
| if (disposed) return () => {}; | |
| subscribers.add(listener); | |
| return () => subscribers.delete(listener); | |
| } | |
| function dispose() { | |
| if (disposed) return; | |
| disposed = true; | |
| subscribers.clear(); | |
| try { | |
| storeChannel?.close?.(); | |
| } catch (_) {} | |
| } | |
| function openDatabase() { | |
| if (databasePromise) return databasePromise; | |
| const opening = new Promise((resolve, reject) => { | |
| const request = indexedDB.open(DB_NAME, DB_VERSION); | |
| request.onupgradeneeded = () => { | |
| const database = request.result; | |
| for (const name of Array.from(database.objectStoreNames)) database.deleteObjectStore(name); | |
| const store = database.createObjectStore(STORE_NAME, { keyPath: "url" }); | |
| store.createIndex("lastReadAt", "lastReadAt"); | |
| 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); | |
| }); | |
| } | |
| function normalizeHistoryEntry(entry) { | |
| if (!entry || typeof entry.url !== "string" || !entry.url) | |
| return null; | |
| return { | |
| ...entry, | |
| schemaVersion: SCHEMA_VERSION, | |
| title: String(entry.title || ""), | |
| extension: String(entry.extension || ""), | |
| lastReadAt: Number.isFinite(Number(entry.lastReadAt)) ? Number(entry.lastReadAt) : Date.now() | |
| }; | |
| } | |
| function normalizeBookmarkEntry(entry) { | |
| if ( | |
| !entry || | |
| typeof entry.id !== "string" || | |
| !entry.id || | |
| typeof entry.url !== "string" || | |
| !entry.url | |
| ) | |
| return null; | |
| return { | |
| ...entry, | |
| schemaVersion: SCHEMA_VERSION, | |
| label: String(entry.label || "未命名书签"), | |
| createdAt: Number.isFinite(Number(entry.createdAt)) ? Number(entry.createdAt) : Date.now() | |
| }; | |
| } | |
| async function get(url) { | |
| const database = await openDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const tx = database.transaction(STORE_NAME, "readwrite"), | |
| store = tx.objectStore(STORE_NAME); | |
| const request = store.get(url); | |
| request.onsuccess = () => { | |
| const raw = request.result || null, | |
| entry = raw?.schemaVersion === SCHEMA_VERSION ? normalizeHistoryEntry(raw) : null; | |
| if (raw && !entry) { | |
| store.delete(url); | |
| resolve(null); | |
| } else { | |
| resolve(entry); | |
| } | |
| }; | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| async function put(entry) { | |
| entry = normalizeHistoryEntry(entry); | |
| if (!entry) throw new TypeError("Invalid reader history 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) { | |
| if (Number(existing.result.lastReadAt) > Number(entry.lastReadAt)) return; | |
| 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 = () => { | |
| notify({ type: "history", url: entry.url, lastReadAt: entry.lastReadAt }); | |
| resolve(); | |
| }; | |
| tx.onerror = () => reject(tx.error); | |
| tx.onabort = () => reject(tx.error); | |
| }); | |
| } | |
| async function listEntries( | |
| storeName, | |
| indexName, | |
| normalize, | |
| { query = null, direction = "next", limit = Infinity } = {} | |
| ) { | |
| const database = await openDatabase(); | |
| return new Promise((resolve, reject) => { | |
| const entries = []; | |
| const tx = database.transaction(storeName, "readwrite"), | |
| store = tx.objectStore(storeName); | |
| const request = store.index(indexName).openCursor(query, direction); | |
| request.onsuccess = () => { | |
| const cursor = request.result; | |
| if (!cursor || entries.length >= limit) return resolve(entries); | |
| const entry = cursor.value.schemaVersion === SCHEMA_VERSION ? normalize(cursor.value) : null; | |
| if (entry) { | |
| entries.push(entry); | |
| } else cursor.delete(); | |
| cursor.continue(); | |
| }; | |
| request.onerror = () => reject(request.error); | |
| }); | |
| } | |
| function list(limit = MAX_ENTRIES) { | |
| return listEntries(STORE_NAME, "lastReadAt", normalizeHistoryEntry, { | |
| direction: "prev", | |
| limit | |
| }); | |
| } | |
| async function remove(url) { | |
| await transaction("readwrite", (store) => store.delete(url)); | |
| notify({ type: "history-remove", url }); | |
| } | |
| async function clearHistory() { | |
| await transaction("readwrite", (store) => store.clear()); | |
| notify({ type: "history-clear" }); | |
| } | |
| async function putBookmark(entry) { | |
| entry = normalizeBookmarkEntry(entry); | |
| if (!entry) throw new TypeError("Invalid reader bookmark 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 = () => { | |
| notify({ type: "bookmark", url: entry.url, id: entry.id }); | |
| resolve(); | |
| }; | |
| tx.onerror = () => reject(tx.error); | |
| tx.onabort = () => reject(tx.error); | |
| }); | |
| } | |
| async function listBookmarks(url) { | |
| const entries = await listEntries(BOOKMARK_STORE_NAME, "url", normalizeBookmarkEntry, { | |
| query: IDBKeyRange.only(url) | |
| }); | |
| return entries.sort((a, b) => b.createdAt - a.createdAt); | |
| } | |
| function listAllBookmarks() { | |
| return listEntries(BOOKMARK_STORE_NAME, "createdAt", normalizeBookmarkEntry, { | |
| direction: "prev" | |
| }); | |
| } | |
| 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 = () => { | |
| notify({ type: "bookmark-remove", id }); | |
| resolve(); | |
| }; | |
| tx.onerror = () => reject(tx.error); | |
| tx.onabort = () => reject(tx.error); | |
| }); | |
| } | |
| root.VoiceOfMLReaderStore = Object.freeze({ | |
| DB_NAME, | |
| DB_VERSION, | |
| MAX_ENTRIES, | |
| SCHEMA_VERSION, | |
| get, | |
| put, | |
| list, | |
| remove, | |
| clearHistory, | |
| putBookmark, | |
| listBookmarks, | |
| listAllBookmarks, | |
| removeBookmark, | |
| subscribe, | |
| dispose | |
| }); | |
| })(typeof self !== "undefined" ? self : window); | |