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 && (
+
+
+
+
+ )}
+
diff --git a/src/index.css b/src/index.css
index 3d8731e..9c6bfbd 100644
--- a/src/index.css
+++ b/src/index.css
@@ -69,3 +69,73 @@ input[type="range"]::-moz-range-thumb {
cursor: pointer;
box-shadow: 0 0 6px rgba(244, 63, 94, 0.4);
}
+
+/* Short / Landscape screen optimizations (< 520px viewport height) */
+@media (max-height: 520px) {
+ .short\:py-0\.5 {
+ padding-top: 0.125rem !important;
+ padding-bottom: 0.125rem !important;
+ }
+ .short\:py-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+ .short\:py-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+ .short\:pb-1 {
+ padding-bottom: 0.25rem !important;
+ }
+ .short\:my-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+ .short\:mb-1 {
+ margin-bottom: 0.25rem !important;
+ }
+ .short\:mt-0\.5 {
+ margin-top: 0.125rem !important;
+ }
+ .short\:h-20 {
+ height: 5rem !important;
+ }
+ .short\:h-24 {
+ height: 6rem !important;
+ }
+ .short\:h-5 {
+ height: 1.25rem !important;
+ }
+ .short\:h-3 {
+ height: 0.75rem !important;
+ }
+ .short\:text-2xl {
+ font-size: 1.5rem !important;
+ line-height: 2rem !important;
+ }
+ .short\:text-3xl {
+ font-size: 1.875rem !important;
+ line-height: 2.25rem !important;
+ }
+ .short\:text-4xl {
+ font-size: 2.25rem !important;
+ line-height: 2.5rem !important;
+ }
+ .short\:text-xs {
+ font-size: 0.75rem !important;
+ line-height: 1rem !important;
+ }
+ .short\:space-y-1 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: 0.25rem !important;
+ margin-bottom: 0 !important;
+ }
+ .short\:space-y-2 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: 0.5rem !important;
+ margin-bottom: 0 !important;
+ }
+ .short\:hidden {
+ display: none !important;
+ }
+}