diff --git a/CHANGELOG.md b/CHANGELOG.md index 5411afb..d5f22c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### 2026-02-02: 1.0.5 + +* Add fullscreen button for desktop browsers +* Add PWA support with manifest for standalone mode +* Add iOS install banner prompting Add to Home Screen for fullscreen experience + ### 2026-02-01: 1.0.4 * Improve mobile responsiveness and book metadata layout diff --git a/index.html b/index.html index 84cfa97..c5643a3 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,13 @@ Speed reader + + + + + + diff --git a/package-lock.json b/package-lock.json index cf43386..2f49f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rsvp-speed-reader", - "version": "1.0.0", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rsvp-speed-reader", - "version": "1.0.0", + "version": "1.0.4", "dependencies": { "jszip": "^3.10.1", "lucide-react": "^0.460.0", diff --git a/package.json b/package.json index a112540..9b04925 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rsvp-speed-reader", "private": true, - "version": "1.0.4", + "version": "1.0.5", "type": "module", "scripts": { "dev": "vite", diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..1af0da0 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Speed reader", + "short_name": "Speed reader", + "description": "A web-based speed reading tool using RSVP with ORP alignment", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/src/App.jsx b/src/App.jsx index d943dbd..3010d90 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,6 +12,9 @@ import { Settings, Link, Check, + Maximize, + Minimize, + Share, } from "lucide-react"; import JSZip from "jszip"; @@ -32,6 +35,21 @@ const PauseSolid = ({ size = 24 }) => ( const DEFAULT_TEXT = `Welcome to the RSVP Speed Reader! This tool uses Rapid Serial Visual Presentation to help you read faster. Click the text icon in the top left to paste text or load an EPUB file. The reader displays one word at a time at a fixed focal point, reducing eye movement and allowing for faster reading speeds. Research suggests that RSVP can help readers achieve speeds of 500 words per minute or more with practice. Try starting at a comfortable pace and gradually increase the speed as you become more accustomed to the technique. Happy reading!`; const STORAGE_KEY = "rsvp-reader-settings"; +const IOS_BANNER_DISMISSED_KEY = "rsvp-ios-banner-dismissed"; + +// Detect iOS Safari (not in standalone mode) +function isIOSSafari() { + const ua = navigator.userAgent; + const isIOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + const isSafari = /Safari/.test(ua) && !/CriOS|FxiOS|OPiOS|mercury/.test(ua); + const isStandalone = window.navigator.standalone === true; + return isIOS && isSafari && !isStandalone; +} + +// Check if fullscreen API is supported +function isFullscreenSupported() { + return document.documentElement.requestFullscreen !== undefined; +} // Simple hash for text to use as key for positions function hashText(text) { @@ -320,6 +338,15 @@ function App() { () => savedSettings?.fetchMetadataOnline ?? false, ); const [linkCopied, setLinkCopied] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + const [showIOSBanner, setShowIOSBanner] = useState(() => { + if (!isIOSSafari()) return false; + try { + return localStorage.getItem(IOS_BANNER_DISMISSED_KEY) !== "true"; + } catch { + return true; + } + }); const timeoutRef = useRef(null); const prevTextRef = useRef(text); const fileInputRef = useRef(null); @@ -517,6 +544,36 @@ function App() { } }; + const toggleFullscreen = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } else { + await document.exitFullscreen(); + } + } catch (e) { + console.error("Fullscreen error:", e); + } + }; + + const dismissIOSBanner = () => { + setShowIOSBanner(false); + try { + localStorage.setItem(IOS_BANNER_DISMISSED_KEY, "true"); + } catch { + // Ignore storage errors + } + }; + + // Listen for fullscreen changes + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); + }, []); + const handleProgressClick = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; @@ -576,6 +633,16 @@ function App() { onChange={handleFileUpload} style={{ display: "none" }} /> + {isFullscreenSupported() && ( + + )}
@@ -964,6 +1031,23 @@ function App() {
)} + {/* iOS install banner */} + {showIOSBanner && ( +
+
+ + For fullscreen, tap Share then "Add to Home Screen" +
+ +
+ )} + {/* Settings modal */} {showSettings && (
setShowSettings(false)} role="presentation"> @@ -1538,6 +1622,39 @@ const styles = { margin: 0, marginTop: "2px", }, + + // iOS install banner + iosBanner: { + position: "fixed", + bottom: 0, + left: 0, + right: 0, + backgroundColor: "#1a1a1a", + borderTop: "1px solid #333", + padding: "12px 16px", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "12px", + zIndex: 1000, + }, + iosBannerContent: { + display: "flex", + alignItems: "center", + gap: "10px", + color: "#999", + fontSize: "0.8rem", + }, + iosBannerClose: { + background: "transparent", + border: "none", + color: "#666", + cursor: "pointer", + padding: "4px", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, }; export default App;