refactor(html): move reader state to route-local hook

This commit is contained in:
Richard R 2026-05-13 13:40:17 -06:00
parent 3a0ed999ac
commit 41036e1341
5 changed files with 77 additions and 129 deletions

View file

@ -4,14 +4,11 @@ import type { ReactNode } from 'react';
import { ConfigProvider } from '@/contexts/ConfigContext';
import { TTSProvider } from '@/contexts/TTSContext';
import { HTMLProvider } from '@/contexts/HTMLContext';
export default function HtmlReaderLayout({ children }: { children: ReactNode }) {
return (
<ConfigProvider>
<TTSProvider>
<HTMLProvider>{children}</HTMLProvider>
</TTSProvider>
<TTSProvider>{children}</TTSProvider>
</ConfigProvider>
);
}

View file

@ -3,7 +3,6 @@
import { useParams, useRouter } from "next/navigation";
import Link from 'next/link';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useHTML } from '@/contexts/HTMLContext';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
import { HTMLViewer } from '@/components/views/HTMLViewer';
import { DocumentSettings } from '@/components/documents/DocumentSettings';
@ -16,11 +15,12 @@ import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
import { SegmentsSidebar } from '@/components/reader/SegmentsSidebar';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useHtmlDocument } from './useHtmlDocument';
export default function HTMLPage() {
const { id } = useParams();
const router = useRouter();
const { setCurrentDocument, currDocName, clearCurrDoc } = useHTML();
const { setCurrentDocument, currDocData, currDocName, clearCurrDoc } = useHtmlDocument();
const { stop } = useTTS();
const { isAtLimit } = useAuthRateLimit();
const [error, setError] = useState<string | null>(null);
@ -168,7 +168,7 @@ export default function HTMLPage() {
</div>
) : (
<div className="h-full w-full" style={{ paddingLeft: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px`, paddingRight: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px` }}>
<HTMLViewer className="h-full" />
<HTMLViewer className="h-full" currDocData={currDocData} currDocName={currDocName} />
</div>
)}
</div>

View file

@ -0,0 +1,70 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTTS } from '@/contexts/TTSContext';
import { getDocumentMetadata } from '@/lib/client/api/documents';
import { ensureCachedDocument } from '@/lib/client/cache/documents';
interface HtmlDocumentState {
currDocData: string | undefined;
currDocName: string | undefined;
currDocText: string | undefined;
setCurrentDocument: (id: string) => Promise<void>;
clearCurrDoc: () => void;
}
export function useHtmlDocument(): HtmlDocumentState {
const { setText: setTTSText, stop } = useTTS();
const setTTSTextRef = useRef(setTTSText);
const [currDocData, setCurrDocData] = useState<string>();
const [currDocName, setCurrDocName] = useState<string>();
const [currDocText, setCurrDocText] = useState<string>();
useEffect(() => {
setTTSTextRef.current = setTTSText;
}, [setTTSText]);
const clearCurrDoc = useCallback(() => {
setCurrDocData(undefined);
setCurrDocName(undefined);
setCurrDocText(undefined);
stop();
}, [stop]);
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
try {
const meta = await getDocumentMetadata(id);
if (!meta) {
console.error('Document not found on server');
return;
}
const doc = await ensureCachedDocument(meta);
if (doc.type !== 'html') {
console.error('Document is not an HTML/TXT/MD document');
return;
}
setCurrDocName(doc.name);
setCurrDocData(doc.data);
setCurrDocText(doc.data);
setTTSTextRef.current(doc.data);
} catch (error) {
console.error('Failed to get HTML document:', error);
clearCurrDoc();
}
}, [clearCurrDoc]);
return useMemo(
() => ({
currDocData,
currDocName,
currDocText,
setCurrentDocument,
clearCurrDoc,
}),
[currDocData, currDocName, currDocText, setCurrentDocument, clearCurrDoc],
);
}

View file

@ -3,15 +3,15 @@
import { useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useHTML } from '@/contexts/HTMLContext';
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
interface HTMLViewerProps {
className?: string;
currDocData?: string;
currDocName?: string;
}
export function HTMLViewer({ className = '' }: HTMLViewerProps) {
const { currDocData, currDocName } = useHTML();
export function HTMLViewer({ className = '', currDocData, currDocName }: HTMLViewerProps) {
const containerRef = useRef<HTMLDivElement>(null);
if (!currDocData) {

View file

@ -1,119 +0,0 @@
'use client';
import {
createContext,
useContext,
useState,
ReactNode,
useCallback,
useMemo,
useEffect,
useRef,
} from 'react';
import { getDocumentMetadata } from '@/lib/client/api/documents';
import { ensureCachedDocument } from '@/lib/client/cache/documents';
import { useTTS } from '@/contexts/TTSContext';
interface HTMLContextType {
currDocData: string | undefined;
currDocName: string | undefined;
currDocText: string | undefined;
setCurrentDocument: (id: string) => Promise<void>;
clearCurrDoc: () => void;
}
const HTMLContext = createContext<HTMLContextType | undefined>(undefined);
/**
* Provider component for HTML/Markdown functionality
* Manages the state and operations for HTML document handling
* @param {Object} props - Component props
* @param {ReactNode} props.children - Child components to be wrapped by the provider
*/
export function HTMLProvider({ children }: { children: ReactNode }) {
const { setText: setTTSText, stop } = useTTS();
const setTTSTextRef = useRef(setTTSText);
// Current document state
const [currDocData, setCurrDocData] = useState<string>();
const [currDocName, setCurrDocName] = useState<string>();
const [currDocText, setCurrDocText] = useState<string>();
useEffect(() => {
setTTSTextRef.current = setTTSText;
}, [setTTSText]);
/**
* Clears all current document state and stops any active TTS
*/
const clearCurrDoc = useCallback(() => {
setCurrDocData(undefined);
setCurrDocName(undefined);
setCurrDocText(undefined);
stop();
}, [stop]);
/**
* Sets the current document based on its ID
* @param {string} id - The unique identifier of the document
* @throws {Error} When document data is empty or retrieval fails
*/
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
try {
const meta = await getDocumentMetadata(id);
if (!meta) {
console.error('Document not found on server');
return;
}
const doc = await ensureCachedDocument(meta);
if (doc.type !== 'html') {
console.error('Document is not an HTML/TXT/MD document');
return;
}
setCurrDocName(doc.name);
setCurrDocData(doc.data);
setCurrDocText(doc.data); // Use the same text for TTS
setTTSTextRef.current(doc.data);
} catch (error) {
console.error('Failed to get HTML document:', error);
clearCurrDoc();
}
}, [clearCurrDoc]);
const contextValue = useMemo(() => ({
currDocData,
currDocName,
currDocText,
setCurrentDocument,
clearCurrDoc,
}), [
currDocData,
currDocName,
currDocText,
setCurrentDocument,
clearCurrDoc,
]);
return (
<HTMLContext.Provider value={contextValue}>
{children}
</HTMLContext.Provider>
);
}
/**
* Custom hook to consume the HTML context
* @returns {HTMLContextType} The HTML context value
* @throws {Error} When used outside of HTMLProvider
*/
export function useHTML() {
const context = useContext(HTMLContext);
if (context === undefined) {
throw new Error('useHTML must be used within an HTMLProvider');
}
return context;
}