Settings reorganization

This commit is contained in:
Stefan Hardegger
2025-09-17 15:06:35 +02:00
parent c0b3ae3b72
commit 64f97f5648
9 changed files with 1200 additions and 952 deletions

View File

@@ -20,6 +20,7 @@ export default function StoryReadingPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [readingProgress, setReadingProgress] = useState(0);
const [readingPercentage, setReadingPercentage] = useState(0);
const [sanitizedContent, setSanitizedContent] = useState<string>('');
const [hasScrolledToPosition, setHasScrolledToPosition] = useState(false);
const [showToc, setShowToc] = useState(false);
@@ -52,28 +53,38 @@ export default function StoryReadingPage() {
return Math.floor(scrollRatio * textLength);
}, [story]);
// Calculate reading percentage from character position
const calculateReadingPercentage = useCallback((currentPosition: number): number => {
if (!story) return 0;
const totalLength = story.contentPlain?.length || story.contentHtml.length;
if (totalLength === 0) return 0;
return Math.round((currentPosition / totalLength) * 100);
}, [story]);
// Convert character position back to scroll position for auto-scroll
const scrollToCharacterPosition = useCallback((position: number) => {
if (!contentRef.current || !story || hasScrolledToPosition) return;
const textLength = story.contentPlain?.length || story.contentHtml.length;
if (textLength === 0 || position === 0) return;
const ratio = position / textLength;
const content = contentRef.current;
const contentTop = content.offsetTop;
const contentHeight = content.scrollHeight;
const windowHeight = window.innerHeight;
// Calculate target scroll position
const targetScroll = contentTop + (ratio * contentHeight) - (windowHeight * 0.3);
// Smooth scroll to position
window.scrollTo({
top: Math.max(0, targetScroll),
behavior: 'smooth'
});
setHasScrolledToPosition(true);
}, [story, hasScrolledToPosition]);
@@ -188,17 +199,20 @@ export default function StoryReadingPage() {
// Otherwise, use saved reading position
if (story.readingPosition && story.readingPosition > 0) {
console.log('Auto-scrolling to saved position:', story.readingPosition);
const initialPercentage = calculateReadingPercentage(story.readingPosition);
setReadingPercentage(initialPercentage);
scrollToCharacterPosition(story.readingPosition);
} else {
// Even if there's no saved position, mark as ready for tracking
console.log('No saved position, starting fresh tracking');
setReadingPercentage(0);
setHasScrolledToPosition(true);
}
}, 500);
return () => clearTimeout(timeout);
}
}, [story, sanitizedContent, scrollToCharacterPosition, hasScrolledToPosition]);
}, [story, sanitizedContent, scrollToCharacterPosition, calculateReadingPercentage, hasScrolledToPosition]);
// Track reading progress and save position
useEffect(() => {
@@ -244,10 +258,12 @@ export default function StoryReadingPage() {
setShowEndOfStoryPopup(true);
}
// Save reading position (debounced)
// Save reading position and update percentage (debounced)
if (hasScrolledToPosition) { // Only save after initial auto-scroll
const characterPosition = getCharacterPositionFromScroll();
console.log('Scroll detected, character position:', characterPosition);
const percentage = calculateReadingPercentage(characterPosition);
console.log('Scroll detected, character position:', characterPosition, 'percentage:', percentage);
setReadingPercentage(percentage);
debouncedSavePosition(characterPosition);
} else {
console.log('Scroll detected but not ready for tracking yet');
@@ -263,7 +279,7 @@ export default function StoryReadingPage() {
clearTimeout(saveTimeoutRef.current);
}
};
}, [story, hasScrolledToPosition, getCharacterPositionFromScroll, debouncedSavePosition, hasReachedEnd]);
}, [story, hasScrolledToPosition, getCharacterPositionFromScroll, calculateReadingPercentage, debouncedSavePosition, hasReachedEnd]);
const handleRatingUpdate = async (newRating: number) => {
if (!story) return;
@@ -359,6 +375,11 @@ export default function StoryReadingPage() {
</div>
<div className="flex items-center gap-4">
{/* Reading percentage indicator */}
<div className="text-sm theme-text font-mono bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
{readingPercentage}%
</div>
{hasHeadings && (
<button
onClick={() => setShowToc(!showToc)}
@@ -368,12 +389,12 @@ export default function StoryReadingPage() {
📋 TOC
</button>
)}
<StoryRating
rating={story.rating || 0}
onRatingChange={handleRatingUpdate}
/>
<Link href={`/stories/${story.id}/edit`}>
<Button size="sm" variant="ghost">
Edit