diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6a728..52114b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 2026-02-01: 1.0.3 + +* Add Open Library API integration for missing book metadata (opt-in) + ### 2026-02-01: 1.0.2 * Convert book metadata to semantic HTML for accessibility diff --git a/README.md b/README.md index 688b3bb..63e8b36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ⚡ Speed reader -![Version](https://img.shields.io/badge/version-1.0.2-blue?style=for-the-badge) +![Version](https://img.shields.io/badge/version-1.0.3-blue?style=for-the-badge) ![React](https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black) ![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white) ![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black) @@ -22,6 +22,8 @@ A web-based speed reading tool using Rapid Serial Visual Presentation (RSVP) wit Everything runs locally in your browser. Your text and files never leave your computer - no servers, no tracking, no analytics. EPUB and TXT files are processed entirely client-side. Settings and reading progress are stored in localStorage. Works offline once loaded. +Optional: If enabled in settings, missing book metadata (title, author, cover) can be fetched from [Open Library API](https://openlibrary.org/developers/api). This sends the book title to their servers. + ## What is RSVP? RSVP (Rapid Serial Visual Presentation) is a reading technique that displays text one word at a time at a fixed focal point. This eliminates saccades (eye movements) that normally slow down reading, allowing for significantly faster reading speeds. diff --git a/package.json b/package.json index 794dc57..5ebda4b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rsvp-speed-reader", "private": true, - "version": "1.0.2", + "version": "1.0.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.jsx b/src/App.jsx index 4b9f758..f8135ee 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -233,6 +233,33 @@ function formatReadingTime(minutes) { return `${hours}h ${mins}m`; } +// Fetch book metadata from Open Library API +async function fetchMetadataFromOpenLibrary(title, author) { + if (!title && !author) return null; + + try { + const query = [title, author].filter(Boolean).join(" "); + const url = `https://openlibrary.org/search.json?q=${encodeURIComponent(query)}&limit=1&fields=title,author_name,cover_i`; + const response = await fetch(url); + if (!response.ok) return null; + + const data = await response.json(); + if (!data.docs || data.docs.length === 0) return null; + + const book = data.docs[0]; + return { + title: book.title || null, + author: book.author_name?.[0] || null, + cover: book.cover_i + ? `https://covers.openlibrary.org/b/id/${book.cover_i}-M.jpg` + : null, + }; + } catch (e) { + console.error("Failed to fetch from Open Library:", e); + return null; + } +} + function App() { // Load settings only once on mount const [savedSettings] = useState(() => loadSettings()); @@ -271,6 +298,9 @@ function App() { const [wordAmount, setWordAmount] = useState( () => savedSettings?.wordAmount ?? 1, ); + const [fetchMetadataOnline, setFetchMetadataOnline] = useState( + () => savedSettings?.fetchMetadataOnline ?? false, + ); const timeoutRef = useRef(null); const prevTextRef = useRef(text); const fileInputRef = useRef(null); @@ -284,7 +314,23 @@ function App() { if (file.name.endsWith(".epub")) { const result = await parseEpub(file); setText(result.text); - setBookMetadata(result.metadata); + + let metadata = result.metadata; + // Fetch missing metadata from Open Library if enabled + if (fetchMetadataOnline && (!metadata.title || !metadata.cover)) { + const onlineMetadata = await fetchMetadataFromOpenLibrary( + metadata.title, + metadata.author, + ); + if (onlineMetadata) { + metadata = { + title: metadata.title || onlineMetadata.title, + author: metadata.author || onlineMetadata.author, + cover: metadata.cover || onlineMetadata.cover, + }; + } + } + setBookMetadata(metadata); } else if (file.name.endsWith(".txt")) { const textContent = await file.text(); setText(textContent); @@ -335,8 +381,9 @@ function App() { sideOpacity, wordAmount, bookMetadata, + fetchMetadataOnline, }); - }, [wpm, text, currentIndex, sideOpacity, wordAmount, bookMetadata]); + }, [wpm, text, currentIndex, sideOpacity, wordAmount, bookMetadata, fetchMetadataOnline]); const getBaseDelay = useCallback(() => { return (60 / wpm) * 1000; @@ -890,6 +937,31 @@ function App() { +
+ +
+ +
+
@@ -1243,6 +1315,8 @@ const styles = { settingLabel: { fontSize: "0.875rem", color: "rgb(98, 98, 98)", + display: "flex", + flexDirection: "column", }, settingControl: { display: "flex", @@ -1270,6 +1344,31 @@ const styles = { width: "120px", accentColor: "#ff6b6b", }, + settingHint: { + display: "block", + fontSize: "0.7rem", + color: "#555", + marginTop: "2px", + }, + toggleBtn: { + width: "40px", + height: "24px", + borderRadius: "12px", + border: "none", + cursor: "pointer", + position: "relative", + transition: "background-color 0.2s", + }, + toggleKnob: { + position: "absolute", + top: "3px", + left: "3px", + width: "18px", + height: "18px", + borderRadius: "50%", + backgroundColor: "#fff", + transition: "transform 0.2s", + }, shortcutList: { display: "flex", flexDirection: "column",