Improve text input performance with apply/cancel

This commit is contained in:
2026-02-07 22:39:06 +01:00
parent fb4a70fec7
commit b7a680d7bb
5 changed files with 119 additions and 7 deletions
+1
View File
@@ -2,6 +2,7 @@
- Split the big app file into smaller TypeScript modules and add `tsc` typechecking
- Add PDF upload support (extract all text client-side for speed reading)
- Improve text editor performance by applying changes explicitly instead of reprocessing on each keystroke
### 2026-02-02: 1.0.5
+32 -3
View File
@@ -42,6 +42,7 @@ export default function App() {
const positionsRef = useRef<PositionsMap>(savedSettings?.positions || {});
const [text, setText] = useState(() => savedSettings?.text || DEFAULT_TEXT);
const [draftText, setDraftText] = useState(() => savedSettings?.text || DEFAULT_TEXT);
const [words, setWords] = useState(() =>
splitWords(savedSettings?.text || DEFAULT_TEXT),
);
@@ -100,6 +101,24 @@ export default function App() {
const prevTextRef = useRef(text);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const toggleTextInput = useCallback(() => {
setShowTextInput((prev) => {
const next = !prev;
if (next) setDraftText(text);
return next;
});
}, [text]);
const applyDraftText = useCallback(() => {
setText(draftText);
setShowTextInput(false);
}, [draftText]);
const cancelDraftText = useCallback(() => {
setDraftText(text);
setShowTextInput(false);
}, [text]);
const togglePlay = useCallback(() => {
if (currentIndex >= words.length - 1) {
setCurrentIndex(0);
@@ -281,6 +300,7 @@ export default function App() {
case "Escape":
setShowInfo(false);
setShowShortcuts(false);
setDraftText(text);
setShowTextInput(false);
setShowSettings(false);
break;
@@ -389,7 +409,7 @@ export default function App() {
<div style={styles.container}>
<TopBar
showTextInput={showTextInput}
onToggleTextInput={() => setShowTextInput((prev) => !prev)}
onToggleTextInput={toggleTextInput}
fileInputRef={fileInputRef}
onFileUpload={handleFileUpload}
fileAccept=".epub,.pdf,.txt"
@@ -406,8 +426,17 @@ export default function App() {
{showTextInput && (
<TextInputOverlay
text={text}
onChangeText={(e) => setText(e.target.value)}
text={draftText}
onChangeText={(e) => setDraftText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
applyDraftText();
}
}}
isDirty={draftText !== text}
onApply={applyDraftText}
onCancel={cancelDraftText}
/>
)}
+43 -3
View File
@@ -1,25 +1,65 @@
import type { ChangeEventHandler } from "react";
import type { ChangeEventHandler, KeyboardEventHandler } from "react";
import { styles } from "../styles";
type Props = {
text: string;
onChangeText: ChangeEventHandler<HTMLTextAreaElement>;
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
isDirty: boolean;
onApply: () => void;
onCancel: () => void;
};
export function TextInputOverlay({ text, onChangeText }: Props) {
export function TextInputOverlay({
text,
onChangeText,
onKeyDown,
isDirty,
onApply,
onCancel,
}: Props) {
return (
<div style={styles.textInputOverlay}>
<div style={styles.textInputPanel}>
<textarea
value={text}
onChange={onChangeText}
onKeyDown={onKeyDown}
style={styles.textarea}
placeholder="Paste your text here..."
rows={8}
autoFocus
/>
<div style={styles.textInputActions}>
<div style={styles.textInputHint}>
Changes won't affect the reader until you apply.
</div>
<div style={styles.textInputButtons}>
<button
onClick={onCancel}
style={styles.textInputBtnSecondary}
className="icon-btn"
type="button"
>
Cancel
</button>
<button
onClick={onApply}
style={{
...styles.textInputBtnPrimary,
...(isDirty ? {} : styles.textInputBtnPrimaryDisabled),
}}
className="icon-btn"
type="button"
disabled={!isDirty}
title={isDirty ? "Apply changes" : "No changes to apply"}
>
Apply
</button>
</div>
</div>
</div>
</div>
);
}
+1 -1
View File
@@ -21,6 +21,7 @@ export function getWordDelay(word: string, baseDelayMs: number): number {
}
export function formatReadingTime(minutes: number): string {
minutes = Math.round(minutes);
if (minutes < 1) return "< 1 min";
if (minutes < 60) return `${Math.round(minutes)} min`;
const hours = Math.floor(minutes / 60);
@@ -28,4 +29,3 @@ export function formatReadingTime(minutes: number): string {
if (mins === 0) return `${hours}h`;
return `${hours}h ${mins}m`;
}
+42
View File
@@ -117,6 +117,48 @@ export const styles: Record<string, CSSProperties> =
padding: "4px",
border: "1px solid #222",
},
textInputActions: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
padding: "10px 12px 12px 12px",
borderTop: "1px solid #1a1a1a",
},
textInputHint: {
fontSize: "0.75rem",
color: "#888",
lineHeight: 1.3,
},
textInputButtons: {
display: "flex",
alignItems: "center",
gap: "10px",
},
textInputBtnPrimary: {
backgroundColor: "#ff6b6b",
color: "#0a0a0a",
border: "none",
borderRadius: "8px",
padding: "10px 12px",
fontSize: "0.85rem",
fontWeight: 700,
cursor: "pointer",
},
textInputBtnPrimaryDisabled: {
opacity: 0.45,
cursor: "not-allowed",
},
textInputBtnSecondary: {
backgroundColor: "transparent",
color: "#bbb",
border: "1px solid #2a2a2a",
borderRadius: "8px",
padding: "10px 12px",
fontSize: "0.85rem",
fontWeight: 600,
cursor: "pointer",
},
textarea: {
width: "100%",
padding: "16px",