Improve text input performance with apply/cancel
This commit is contained in:
@@ -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
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
@@ -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`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user