Add copy link button to share reading position across devices

This commit is contained in:
Roni Laukkarinen
2026-02-01 21:41:58 +02:00
parent ed4132b650
commit b2df50749d
2 changed files with 64 additions and 2 deletions
+1
View File
@@ -2,6 +2,7 @@
* Improve mobile responsiveness and book metadata layout * Improve mobile responsiveness and book metadata layout
* Fix words per display setting and limit max to 3 * Fix words per display setting and limit max to 3
* Add copy link button to share reading position across devices
### 2026-02-01: 1.0.3 ### 2026-02-01: 1.0.3
+61
View File
@@ -10,6 +10,8 @@ import {
FileText, FileText,
Upload, Upload,
Settings, Settings,
Link,
Check,
} from "lucide-react"; } from "lucide-react";
import JSZip from "jszip"; import JSZip from "jszip";
@@ -274,6 +276,22 @@ function App() {
.filter((w) => w.length > 0); .filter((w) => w.length > 0);
}); });
const [currentIndex, setCurrentIndex] = useState(() => { const [currentIndex, setCurrentIndex] = useState(() => {
// Check URL hash for shared position
const hash = window.location.hash;
if (hash) {
const params = new URLSearchParams(hash.slice(1));
const urlPos = parseInt(params.get("pos"), 10);
if (!isNaN(urlPos) && urlPos >= 0) {
// Clear hash after reading
window.history.replaceState(null, "", window.location.pathname);
const t = savedSettings?.text || DEFAULT_TEXT;
const wordCount = t
.trim()
.split(/\s+/)
.filter((w) => w.length > 0).length;
return Math.min(Math.max(0, urlPos), Math.max(0, wordCount - 1));
}
}
const t = savedSettings?.text || DEFAULT_TEXT; const t = savedSettings?.text || DEFAULT_TEXT;
const pos = getPositionForText(t, savedSettings?.positions || {}); const pos = getPositionForText(t, savedSettings?.positions || {});
const wordCount = t const wordCount = t
@@ -301,6 +319,7 @@ function App() {
const [fetchMetadataOnline, setFetchMetadataOnline] = useState( const [fetchMetadataOnline, setFetchMetadataOnline] = useState(
() => savedSettings?.fetchMetadataOnline ?? false, () => savedSettings?.fetchMetadataOnline ?? false,
); );
const [linkCopied, setLinkCopied] = useState(false);
const timeoutRef = useRef(null); const timeoutRef = useRef(null);
const prevTextRef = useRef(text); const prevTextRef = useRef(text);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
@@ -487,6 +506,17 @@ function App() {
setWpm((prev) => Math.max(50, Math.min(1500, prev + delta))); setWpm((prev) => Math.max(50, Math.min(1500, prev + delta)));
}; };
const copyPositionUrl = async () => {
const url = `${window.location.origin}${window.location.pathname}#pos=${currentIndex}`;
try {
await navigator.clipboard.writeText(url);
setLinkCopied(true);
setTimeout(() => setLinkCopied(false), 2000);
} catch (e) {
console.error("Failed to copy URL:", e);
}
};
const handleProgressClick = (e) => { const handleProgressClick = (e) => {
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
@@ -710,9 +740,20 @@ function App() {
> >
<div style={{ ...styles.progressBar, width: `${progress}%` }} /> <div style={{ ...styles.progressBar, width: `${progress}%` }} />
</div> </div>
<div style={styles.progressRow}>
<div style={styles.progressText}> <div style={styles.progressText}>
{currentIndex + 1} / {words.length} ({Math.round(progress)}%) {currentIndex + 1} / {words.length} ({Math.round(progress)}%)
</div> </div>
<button
onClick={copyPositionUrl}
style={styles.linkBtn}
className="icon-btn"
title="Copy link to current position"
>
{linkCopied ? <Check size={14} /> : <Link size={14} />}
<span style={styles.linkBtnText}>{linkCopied ? "Copied" : "Copy link"}</span>
</button>
</div>
<div style={styles.hint} className="hint"> <div style={styles.hint} className="hint">
<kbd style={styles.kbd}>Space</kbd> play <kbd style={styles.kbd}>Space</kbd> play
@@ -1260,10 +1301,30 @@ const styles = {
backgroundColor: "#ff6b6b", backgroundColor: "#ff6b6b",
transition: "width 0.05s linear", transition: "width 0.05s linear",
}, },
progressRow: {
display: "flex",
alignItems: "center",
gap: "12px",
},
progressText: { progressText: {
fontSize: "0.7rem", fontSize: "0.7rem",
color: "rgb(98, 98, 98)", color: "rgb(98, 98, 98)",
}, },
linkBtn: {
background: "transparent",
border: "none",
borderRadius: "4px",
padding: "4px 8px",
cursor: "pointer",
color: "rgb(98, 98, 98)",
display: "flex",
alignItems: "center",
gap: "4px",
fontSize: "0.7rem",
},
linkBtnText: {
fontSize: "0.65rem",
},
hint: { hint: {
fontSize: "0.7rem", fontSize: "0.7rem",
color: "rgb(98, 98, 98)", color: "rgb(98, 98, 98)",