diff --git a/src/App.tsx b/src/App.tsx index c5924ee..608e119 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -215,7 +215,7 @@ export default function App() { } }; - // Mouse activity detection for HUD fading + // Mouse/touch/key activity detection for HUD fading const resetInactivity = useCallback(() => { setIsUserActive(true); if (inactivityTimerRef.current) { @@ -224,7 +224,7 @@ export default function App() { if (isPlaying && settings.autoHideControls) { inactivityTimerRef.current = window.setTimeout(() => { setIsUserActive(false); - }, 2200); + }, 3000); } }, [isPlaying, settings.autoHideControls]); @@ -235,11 +235,28 @@ export default function App() { } return; } + + const handleInteraction = () => { + resetInactivity(); + }; + + window.addEventListener('mousemove', handleInteraction); + window.addEventListener('keydown', handleInteraction); + window.addEventListener('touchstart', handleInteraction); + window.addEventListener('pointerdown', handleInteraction); + const timer = window.setTimeout(() => { setIsUserActive(false); - }, 2200); - return () => window.clearTimeout(timer); - }, [isPlaying, settings.autoHideControls]); + }, 3000); + + return () => { + window.clearTimeout(timer); + window.removeEventListener('mousemove', handleInteraction); + window.removeEventListener('keydown', handleInteraction); + window.removeEventListener('touchstart', handleInteraction); + window.removeEventListener('pointerdown', handleInteraction); + }; + }, [isPlaying, settings.autoHideControls, resetInactivity]); // Keyboard shortcuts const anyModalOpen = @@ -278,6 +295,9 @@ export default function App() { const themeClass = `theme-${settings.theme}`; const isHudDimmed = isPlaying && !isUserActive && settings.autoHideControls; + const hudClasses = isHudDimmed + ? 'opacity-0 pointer-events-none transition-opacity duration-500' + : 'opacity-100 pointer-events-auto transition-opacity duration-300'; return (
{/* Top Header HUD */} -
+
{/* Main RSVP Focus Arena */} -
+
{isLoading ? (
@@ -330,11 +346,7 @@ export default function App() {
{/* Bottom Controls HUD */} -
+
= ({ const remainingText = formatDuration(remainingSeconds); return ( -
+
{/* Progress & Timeline Bar */} {settings.showProgressBar && ( -
-
+
+
{currentIndex + 1} / {totalTokens}{' '} ({percent.toFixed(1)}%) @@ -73,7 +73,7 @@ export const Controls: React.FC = ({
-
+
{/* Chapter tick marks on the track */} {chapters.length > 1 && chapters.map((chap, idx) => { @@ -95,19 +95,19 @@ export const Controls: React.FC = ({ max={Math.max(0, totalTokens - 1)} value={currentIndex} onChange={(e) => onJumpTo(Number(e.target.value))} - className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-rose-500 transition-all focus:outline-none" + className="w-full h-1 sm:h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-rose-500 transition-all focus:outline-none" />
)} {/* Main Playback Buttons */} -
+
{/* Restart */} @@ -116,30 +116,30 @@ export const Controls: React.FC = ({ {/* Previous Sentence */} {/* Primary Play / Pause Button */} @@ -147,44 +147,44 @@ export const Controls: React.FC = ({ {/* Forward 10 */} {/* Quick Speed fine-tuning */} -
+
- + {settings.wpm}
- {/* Preset WPM Chips */} -
+ {/* Preset WPM Chips (Hidden on short screens to prevent overflow) */} +
预设语速: diff --git a/src/components/FocusDisplay.tsx b/src/components/FocusDisplay.tsx index 3e253c2..2ea7a3e 100644 --- a/src/components/FocusDisplay.tsx +++ b/src/components/FocusDisplay.tsx @@ -43,12 +43,12 @@ const ORP_COLOR_MAP: Record = { - sm: 'text-3xl sm:text-4xl h-24 sm:h-28', - base: 'text-4xl sm:text-5xl h-28 sm:h-32', - lg: 'text-5xl sm:text-6xl h-32 sm:h-36', - xl: 'text-6xl sm:text-7xl h-36 sm:h-44', - '2xl': 'text-7xl sm:text-8xl h-44 sm:h-52', - '3xl': 'text-8xl sm:text-9xl h-52 sm:h-64', + sm: 'text-3xl sm:text-4xl h-24 sm:h-28 short:h-20 short:text-2xl', + base: 'text-4xl sm:text-5xl h-28 sm:h-32 short:h-20 short:text-3xl', + lg: 'text-5xl sm:text-6xl h-32 sm:h-36 short:h-24 short:text-3xl', + xl: 'text-6xl sm:text-7xl h-36 sm:h-44 short:h-24 short:text-4xl', + '2xl': 'text-7xl sm:text-8xl h-44 sm:h-52 short:h-24 short:text-4xl', + '3xl': 'text-8xl sm:text-9xl h-52 sm:h-64 short:h-24 short:text-4xl', }; const FONT_FAMILY_MAP: Record = { @@ -76,17 +76,17 @@ export const FocusDisplay: React.FC = ({ return (
{/* Contextual Peripheral Ghost Words (Top line) */} {showContextPreview && ( -
- +
+ {previousToken ? previousToken.text : ''} - + {nextToken ? nextToken.text : ''}
@@ -94,7 +94,7 @@ export const FocusDisplay: React.FC = ({ {/* Main RSVP Focal Chamber */}
{/* Top & Bottom ORP Crosshair Guide Notches */} {showGuideLines && ( @@ -153,7 +153,7 @@ export const FocusDisplay: React.FC = ({
{/* Punctuation / Pause indicator dot */} -
+
{token?.isParagraphEnd && ( ¶ 段落停顿 diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9ae3a40..9f716d1 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -40,7 +40,7 @@ export const Header: React.FC = ({ onCycleTheme, }) => { return ( -
+
{/* Left: App Brand & Current Book Info */}
{/* Logo */} diff --git a/src/components/PasteModal.tsx b/src/components/PasteModal.tsx index 8bca2cf..7d6a51b 100644 --- a/src/components/PasteModal.tsx +++ b/src/components/PasteModal.tsx @@ -1,5 +1,5 @@ -import { Clipboard, Play, X } from 'lucide-react'; -import React, { useState } from 'react'; +import { AlertCircle, Clipboard, Play, X } from 'lucide-react'; +import React, { useRef, useState } from 'react'; import { tokenizeText } from '../core/tokenizer'; import type { Book } from '../types'; @@ -19,22 +19,35 @@ export const PasteModal: React.FC = ({ const [content, setContent] = useState(''); const [title, setTitle] = useState(''); const [isReadingClipboard, setIsReadingClipboard] = useState(false); + const [clipboardError, setClipboardError] = useState(null); + const textareaRef = useRef(null); if (!isOpen) return null; const handlePasteFromClipboard = async () => { try { setIsReadingClipboard(true); + setClipboardError(null); + + if (!navigator.clipboard || !navigator.clipboard.readText) { + throw new Error('Clipboard API not supported'); + } + const text = await navigator.clipboard.readText(); - if (text) { + if (text && text.trim()) { setContent(text); if (!title) { const firstLine = text.trim().split('\n')[0].substring(0, 30); setTitle(firstLine || '剪贴板快读'); } + } else { + setClipboardError('剪贴板中未检测到有效文本,请直接在下方文本框手动粘贴 (Ctrl+V)'); + textareaRef.current?.focus(); } } catch (err) { - console.warn('Clipboard read failed', err); + console.warn('Clipboard auto-read failed:', err); + setClipboardError('无法自动读取剪贴板,请直接在文本框粘贴 (Ctrl+V)'); + textareaRef.current?.focus(); } finally { setIsReadingClipboard(false); } @@ -67,6 +80,7 @@ export const PasteModal: React.FC = ({ onClose(); setContent(''); setTitle(''); + setClipboardError(null); }; const charCount = content.length; @@ -94,6 +108,21 @@ export const PasteModal: React.FC = ({ {/* Body */}
+ {clipboardError && ( +
+
+ + {clipboardError} +
+ +
+ )} +