refactor(html): move reader state to route-local hook
This commit is contained in:
parent
3a0ed999ac
commit
41036e1341
5 changed files with 77 additions and 129 deletions
|
|
@ -4,14 +4,11 @@ import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import { ConfigProvider } from '@/contexts/ConfigContext';
|
import { ConfigProvider } from '@/contexts/ConfigContext';
|
||||||
import { TTSProvider } from '@/contexts/TTSContext';
|
import { TTSProvider } from '@/contexts/TTSContext';
|
||||||
import { HTMLProvider } from '@/contexts/HTMLContext';
|
|
||||||
|
|
||||||
export default function HtmlReaderLayout({ children }: { children: ReactNode }) {
|
export default function HtmlReaderLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<TTSProvider>
|
<TTSProvider>{children}</TTSProvider>
|
||||||
<HTMLProvider>{children}</HTMLProvider>
|
|
||||||
</TTSProvider>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useHTML } from '@/contexts/HTMLContext';
|
|
||||||
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
||||||
import { HTMLViewer } from '@/components/views/HTMLViewer';
|
import { HTMLViewer } from '@/components/views/HTMLViewer';
|
||||||
import { DocumentSettings } from '@/components/documents/DocumentSettings';
|
import { DocumentSettings } from '@/components/documents/DocumentSettings';
|
||||||
|
|
@ -16,11 +15,12 @@ import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
|
||||||
import { SegmentsSidebar } from '@/components/reader/SegmentsSidebar';
|
import { SegmentsSidebar } from '@/components/reader/SegmentsSidebar';
|
||||||
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
||||||
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
|
import { useHtmlDocument } from './useHtmlDocument';
|
||||||
|
|
||||||
export default function HTMLPage() {
|
export default function HTMLPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { setCurrentDocument, currDocName, clearCurrDoc } = useHTML();
|
const { setCurrentDocument, currDocData, currDocName, clearCurrDoc } = useHtmlDocument();
|
||||||
const { stop } = useTTS();
|
const { stop } = useTTS();
|
||||||
const { isAtLimit } = useAuthRateLimit();
|
const { isAtLimit } = useAuthRateLimit();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -168,7 +168,7 @@ export default function HTMLPage() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full" style={{ paddingLeft: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px`, paddingRight: `${Math.round(maxPadPx * ((100 - padPct) / 100))}px` }}>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
70
src/app/(app)/html/[id]/useHtmlDocument.ts
Normal file
70
src/app/(app)/html/[id]/useHtmlDocument.ts
Normal 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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import { useHTML } from '@/contexts/HTMLContext';
|
|
||||||
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton';
|
||||||
|
|
||||||
interface HTMLViewerProps {
|
interface HTMLViewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
currDocData?: string;
|
||||||
|
currDocName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HTMLViewer({ className = '' }: HTMLViewerProps) {
|
export function HTMLViewer({ className = '', currDocData, currDocName }: HTMLViewerProps) {
|
||||||
const { currDocData, currDocName } = useHTML();
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
if (!currDocData) {
|
if (!currDocData) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue