Better mobile styling
This commit is contained in:
parent
610167f058
commit
ca1e3a83a9
10 changed files with 98 additions and 52 deletions
|
|
@ -11,17 +11,17 @@ export default function Home() {
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-3 md:p-5'>
|
<div className='p-3.5 sm:p-5'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsSettingsOpen(true)}
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
className="rounded-full p-2 text-foreground hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.1] hover:text-accent absolute top-3 right-3"
|
className="rounded-full p-2 text-foreground hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.1] hover:text-accent absolute top-1 right-1 sm:top-3 sm:right-3"
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-6 h-6 hover:animate-spin-slow" />
|
<SettingsIcon className="w-4 h-4 sm:w-5 sm:h-5 hover:animate-spin-slow" />
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-xl font-bold text-center flex-grow">OpenReader WebUI</h1>
|
<h1 className="text-xl font-bold text-center flex-grow">OpenReader WebUI</h1>
|
||||||
<p className="text-sm text-center text-muted mb-5">{'A "bring your own text-to-speech api" web interface for reading documents with high quality voices.'}</p>
|
<p className="text-sm mt-1 text-center text-muted mb-5">A bring your own text-to-speech api web interface for reading documents with high quality voices.</p>
|
||||||
<div className="flex flex-col items-center gap-5">
|
<div className="flex flex-col items-center gap-5">
|
||||||
<PDFUploader className='max-w-xl' />
|
<PDFUploader className='max-w-xl' />
|
||||||
<DocumentList />
|
<DocumentList />
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export default function PDFViewerPage() {
|
||||||
<SettingsIcon className="w-5 h-5 hover:animate-spin-slow" />
|
<SettingsIcon className="w-5 h-5 hover:animate-spin-slow" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mr-2 text-md font-semibold text-foreground truncate">
|
<h1 className="ml-2 mr-2 text-md font-semibold text-foreground truncate">
|
||||||
{isLoading ? 'Loading...' : currDocName}
|
{isLoading ? 'Loading...' : currDocName}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,20 +53,20 @@ export function DocumentList() {
|
||||||
leaveTo="transform scale-95 opacity-0"
|
leaveTo="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between hover:bg-base p-2 rounded-lg transition-colors"
|
className="flex items-center justify-between hover:bg-base p-1 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/pdf/${encodeURIComponent(doc.id)}`}
|
href={`/pdf/${encodeURIComponent(doc.id)}`}
|
||||||
className="flex items-center space-x-4 flex-1 min-w-0"
|
className="flex items-center space-x-4 flex-1 min-w-0"
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<svg className="w-8 h-8 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6 sm:w-8 sm:h-8 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 transform transition-transform duration-200 ease-in-out hover:scale-[1.02]">
|
<div className="flex-1 min-w-0 transform transition-transform duration-200 ease-in-out hover:scale-[1.02]">
|
||||||
<p className="text-foreground font-medium truncate">{doc.name}</p>
|
<p className="text-sm sm:text-md text-foreground font-medium truncate">{doc.name}</p>
|
||||||
<p className="text-sm text-muted truncate">
|
<p className="text-xs sm:text-sm text-muted truncate">
|
||||||
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="m-8 text-sm text-muted">
|
<footer className="m-8 text-sm text-muted">
|
||||||
<div className="flex flex-col items-center space-y-2">
|
<div className="flex flex-col items-center space-y-2">
|
||||||
<div className="flex flex-wrap md:flex-nowrap items-center justify-center text-center md:space-x-3">
|
<div className="flex flex-wrap sm:flex-nowrap items-center justify-center text-center sm:space-x-3">
|
||||||
<Popover className="flex relative">
|
<Popover className="flex relative">
|
||||||
<PopoverButton className="hover:text-foreground transition-colors">
|
<PopoverButton className="hover:text-foreground transition-colors">
|
||||||
Privacy info
|
Privacy info
|
||||||
|
|
@ -14,7 +14,7 @@ export function Footer() {
|
||||||
<p className='mt-3'>Each sentence of the document you are viewing is sent to my FastAPI server for audio generation, no requests or data is collected.</p>
|
<p className='mt-3'>Each sentence of the document you are viewing is sent to my FastAPI server for audio generation, no requests or data is collected.</p>
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
<span className='w-full md:w-fit'>•</span>
|
<span className='w-full sm:w-fit'>•</span>
|
||||||
<span>
|
<span>
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
<a
|
<a
|
||||||
|
|
|
||||||
|
|
@ -12,25 +12,14 @@ export function PDFSkeleton() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full">
|
<div className="flex flex-col items-center justify-center min-h-[50vh] w-full">
|
||||||
{showNotification && (
|
{showNotification && (
|
||||||
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-2 rounded shadow-lg z-50">
|
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-2 rounded shadow-lg z-50">
|
||||||
There might be an issue with the file import. Please try again.
|
There might be an issue with the file import. Please try again.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col items-center w-full animate-pulse">
|
<div className="flex flex-col items-center gap-4">
|
||||||
{/* Show 3 skeleton pages by default */}
|
<div className="w-12 h-12 border-4 border-accent border-t-transparent rounded-full animate-spin"></div>
|
||||||
{[1, 2, 3].map((index) => (
|
|
||||||
<div key={`skeleton_${index}`}>
|
|
||||||
{/* Page content skeleton */}
|
|
||||||
<div className="flex flex-col justify-center my-4">
|
|
||||||
<div className="bg-gray-200 shadow-lg">
|
|
||||||
{/* Approximate dimensions of a PDF page */}
|
|
||||||
<div className="w-[595px] h-[842px]"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,18 @@ export function PDFUploader({ className = '' }: PDFUploaderProps) {
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<div className="flex flex-col items-center justify-center text-center">
|
<div className="flex flex-col items-center justify-center text-center">
|
||||||
<UploadIcon className="w-10 h-10 mb-2 text-muted" />
|
<UploadIcon className="w-7 h-7 sm:w-10 sm:h-10 mb-2 text-muted" />
|
||||||
|
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<p className="text-lg font-semibold text-foreground">
|
<p className="text-sm sm:text-lg font-semibold text-foreground">
|
||||||
Uploading PDF...
|
Uploading PDF...
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="mb-2 text-lg font-semibold text-foreground">
|
<p className="mb-2 text-sm sm:text-lg font-semibold text-foreground">
|
||||||
{isDragActive ? 'Drop your PDF here' : 'Drop your PDF here, or click to select'}
|
{isDragActive ? 'Drop your PDF here' : 'Drop your PDF here, or click to select'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted">
|
<p className="text-xs sm:text-sm text-muted">
|
||||||
Only PDF files are currently accepted
|
Only PDF files are currently accepted
|
||||||
</p>
|
</p>
|
||||||
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
|
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
|
||||||
|
|
|
||||||
|
|
@ -30,21 +30,20 @@ export const SpeedControl = ({ setSpeedAndRestart }: {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox value={currentSpeed} onChange={setSpeedAndRestart}>
|
<Listbox value={currentSpeed} onChange={setSpeedAndRestart}>
|
||||||
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
|
<ListboxButton className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1">
|
||||||
<span>{currentSpeed}x</span>
|
<span>{currentSpeed}x</span>
|
||||||
<ChevronUpDownIcon className="h-3 w-3" />
|
<ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||||
</ListboxButton>
|
</ListboxButton>
|
||||||
<ListboxOptions className="absolute bottom-full mb-1 w-24 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<ListboxOptions anchor='top start' className="absolute z-50 w-20 sm:w-24 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
{speedOptions.map((option) => (
|
{speedOptions.map((option) => (
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`relative cursor-pointer select-none py-2 px-3 ${active ? 'bg-offbase' : ''
|
`relative cursor-pointer select-none py-0.5 px-1.5 sm:py-2 sm:px-3 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium' : ''}`
|
||||||
} ${selected ? 'font-medium' : ''}`
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{option.label}
|
<span className='text-xs sm:text-sm'>{option.label}</span>
|
||||||
</ListboxOption>
|
</ListboxOption>
|
||||||
))}
|
))}
|
||||||
</ListboxOptions>
|
</ListboxOptions>
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
||||||
} = useTTS();
|
} = useTTS();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 transition-opacity duration-300`}>
|
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-49 transition-opacity duration-300`}>
|
||||||
<div className="bg-base dark:bg-base rounded-full shadow-lg px-4 py-1 flex items-center space-x-1 relative">
|
<div className="bg-base dark:bg-base rounded-full shadow-lg px-3 sm:px-4 py-0.5 sm:py-1 flex items-center space-x-0.5 sm:space-x-1 relative scale-90 sm:scale-100">
|
||||||
{/* Speed control */}
|
{/* Speed control */}
|
||||||
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
|
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,20 @@ export const VoicesControl = ({ availableVoices, setVoiceAndRestart }: {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox value={currentVoice} onChange={setVoiceAndRestart}>
|
<Listbox value={currentVoice} onChange={setVoiceAndRestart}>
|
||||||
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
|
<ListboxButton className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1">
|
||||||
<span>{currentVoice}</span>
|
<span>{currentVoice}</span>
|
||||||
<ChevronUpDownIcon className="h-3 w-3" />
|
<ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||||
</ListboxButton>
|
</ListboxButton>
|
||||||
<ListboxOptions className="absolute bottom-full mb-1 w-32 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<ListboxOptions anchor='top end' className="absolute z-50 w-28 sm:w-32 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
{availableVoices.map((voiceId) => (
|
{availableVoices.map((voiceId) => (
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={voiceId}
|
key={voiceId}
|
||||||
value={voiceId}
|
value={voiceId}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`relative cursor-pointer select-none py-2 px-3 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium' : ''}`
|
`relative cursor-pointer select-none py-0.5 px-1.5 sm:py-2 sm:px-3 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium' : ''}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>{voiceId}</span>
|
<span className='text-xs sm:text-sm'>{voiceId}</span>
|
||||||
</ListboxOption>
|
</ListboxOption>
|
||||||
))}
|
))}
|
||||||
</ListboxOptions>
|
</ListboxOptions>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Text-to-Speech (TTS) Context Provider
|
||||||
|
*
|
||||||
|
* This module provides a React context for managing text-to-speech functionality.
|
||||||
|
* It handles audio playback, sentence processing, and integration with OpenAI's TTS API.
|
||||||
|
*
|
||||||
|
* Key features:
|
||||||
|
* - Audio playback control (play/pause/skip)
|
||||||
|
* - Sentence-by-sentence processing
|
||||||
|
* - Audio caching for better performance
|
||||||
|
* - Voice and speed control
|
||||||
|
* - Document navigation
|
||||||
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
|
|
@ -23,10 +37,16 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definition for AudioContext to handle browser compatibility
|
||||||
|
*/
|
||||||
type AudioContextType = typeof window extends undefined
|
type AudioContextType = typeof window extends undefined
|
||||||
? never
|
? never
|
||||||
: (AudioContext);
|
: (AudioContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining all available methods and properties in the TTS context
|
||||||
|
*/
|
||||||
interface TTSContextType {
|
interface TTSContextType {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
currentText: string;
|
currentText: string;
|
||||||
|
|
@ -58,9 +78,17 @@ interface TTSContextType {
|
||||||
skipToPage: (page: number) => void;
|
skipToPage: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the context
|
||||||
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTSProvider Component
|
||||||
|
*
|
||||||
|
* Main provider component that manages the TTS state and functionality.
|
||||||
|
* Handles initialization of OpenAI client, audio context, and media session.
|
||||||
|
*/
|
||||||
export function TTSProvider({ children }: { children: React.ReactNode }) {
|
export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
// Configuration context consumption
|
||||||
const {
|
const {
|
||||||
apiKey: openApiKey,
|
apiKey: openApiKey,
|
||||||
baseUrl: openApiBaseUrl,
|
baseUrl: openApiBaseUrl,
|
||||||
|
|
@ -70,9 +98,17 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
updateConfigKey
|
updateConfigKey
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
// Move openai initialization to a ref to avoid breaking hooks rules
|
// OpenAI client reference
|
||||||
const openaiRef = useRef<OpenAI | null>(null);
|
const openaiRef = useRef<OpenAI | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State Management
|
||||||
|
* - Playback control
|
||||||
|
* - Text and sentence management
|
||||||
|
* - Audio processing
|
||||||
|
* - Voice and speed settings
|
||||||
|
* - Document navigation
|
||||||
|
*/
|
||||||
// All existing state declarations and refs stay at the top
|
// All existing state declarations and refs stay at the top
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [currentText, setCurrentText] = useState('');
|
const [currentText, setCurrentText] = useState('');
|
||||||
|
|
@ -90,15 +126,16 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [currDocPages, setCurrDocPages] = useState<number>();
|
const [currDocPages, setCurrDocPages] = useState<number>();
|
||||||
const [nextPageLoading, setNextPageLoading] = useState(false);
|
const [nextPageLoading, setNextPageLoading] = useState(false);
|
||||||
|
|
||||||
// Audio cache using LRUCache with a maximum size of 50 entries
|
/**
|
||||||
|
* Audio Cache
|
||||||
|
* LRU cache to store processed audio buffers and improve performance
|
||||||
|
*/
|
||||||
const audioCacheRef = useRef(new LRUCache<string, AudioBuffer>({ max: 50 }));
|
const audioCacheRef = useRef(new LRUCache<string, AudioBuffer>({ max: 50 }));
|
||||||
|
|
||||||
// Update local state when config changes
|
/**
|
||||||
useEffect(() => {
|
* Text Processing Functions
|
||||||
setSpeed(voiceSpeed);
|
* Handle text input and sentence splitting
|
||||||
setVoice(configVoice);
|
*/
|
||||||
}, [voiceSpeed, configVoice]);
|
|
||||||
|
|
||||||
const setText = useCallback((text: string) => {
|
const setText = useCallback((text: string) => {
|
||||||
setCurrentText(text);
|
setCurrentText(text);
|
||||||
console.log('Setting page text:', text);
|
console.log('Setting page text:', text);
|
||||||
|
|
@ -118,6 +155,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [activeHowl]);
|
}, [activeHowl]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playback Control Functions
|
||||||
|
* Manage audio playback, navigation, and state
|
||||||
|
*/
|
||||||
const togglePlay = useCallback(() => {
|
const togglePlay = useCallback(() => {
|
||||||
setIsPlaying((prev) => {
|
setIsPlaying((prev) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
|
|
@ -190,6 +231,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}, [abortAudio, advance]);
|
}, [abortAudio, advance]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio Processing Functions
|
||||||
|
* Handle audio generation, caching, and playback
|
||||||
|
*/
|
||||||
// Initialize OpenAI instance when config loads
|
// Initialize OpenAI instance when config loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVoices = async () => {
|
const fetchVoices = async () => {
|
||||||
|
|
@ -222,6 +267,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [configIsLoading, openApiKey, openApiBaseUrl]);
|
}, [configIsLoading, openApiKey, openApiBaseUrl]);
|
||||||
|
|
||||||
|
// Initialize AudioContext
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/*
|
/*
|
||||||
* Initializes the AudioContext for text-to-speech playback.
|
* Initializes the AudioContext for text-to-speech playback.
|
||||||
|
|
@ -251,7 +297,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [audioContext]);
|
}, [audioContext]);
|
||||||
|
|
||||||
// Now the MediaSession effect can use these functions
|
// Set up MediaSession API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
|
@ -376,7 +422,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
await playSentenceWithHowl(sentences[currentIndex]);
|
await playSentenceWithHowl(sentences[currentIndex]);
|
||||||
}, [sentences, currentIndex, playSentenceWithHowl]);
|
}, [sentences, currentIndex, playSentenceWithHowl]);
|
||||||
|
|
||||||
// main driver useEffect
|
/**
|
||||||
|
* Main Playback Driver
|
||||||
|
* Controls the flow of audio playback and sentence processing
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPlaying) return; // Don't proceed if stopped
|
if (!isPlaying) return; // Don't proceed if stopped
|
||||||
if (isProcessing) return; // Don't proceed if processing audio
|
if (isProcessing) return; // Don't proceed if processing audio
|
||||||
|
|
@ -454,6 +503,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [isPlaying, abortAudio, updateConfigKey]);
|
}, [isPlaying, abortAudio, updateConfigKey]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context Value
|
||||||
|
* Aggregate all functions and state to be provided to consumers
|
||||||
|
*/
|
||||||
const value = {
|
const value = {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
currentText,
|
currentText,
|
||||||
|
|
@ -485,6 +538,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
skipToPage,
|
skipToPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Render provider with value
|
||||||
if (configIsLoading) {
|
if (configIsLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -496,6 +550,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to consume the TTS context
|
||||||
|
* Ensures the context is used within a provider
|
||||||
|
*/
|
||||||
export function useTTS() {
|
export function useTTS() {
|
||||||
const context = useContext(TTSContext);
|
const context = useContext(TTSContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue