Add different view modes
This commit is contained in:
parent
92ee491363
commit
55919310a5
7 changed files with 346 additions and 73 deletions
|
|
@ -4,6 +4,7 @@ import { useState } from 'react';
|
|||
import { PDFUploader } from '@/components/PDFUploader';
|
||||
import { DocumentList } from '@/components/DocumentList';
|
||||
import { SettingsModal } from '@/components/SettingsModal';
|
||||
import { SettingsIcon } from '@/components/icons/Icons';
|
||||
|
||||
export default function Home() {
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
|
@ -18,25 +19,7 @@ export default function Home() {
|
|||
focus:outline-none focus:ring-2 focus:ring-accent transition-colors"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6 hover:animate-spin-slow"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<SettingsIcon className="w-6 h-6 hover:animate-spin-slow" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { useCallback, useEffect, useState } from 'react';
|
|||
import { PDFSkeleton } from '@/components/PDFSkeleton';
|
||||
import { useTTS } from '@/contexts/TTSContext';
|
||||
import { Button } from '@headlessui/react';
|
||||
import { PDFViewSettings } from '@/components/PDFViewSettings';
|
||||
import { SettingsIcon } from '@/components/icons/Icons';
|
||||
|
||||
// Dynamic import for client-side rendering only
|
||||
const PDFViewer = dynamic(
|
||||
|
|
@ -25,6 +27,7 @@ export default function PDFViewerPage() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [zoomLevel, setZoomLevel] = useState<number>(100);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
const loadDocument = useCallback(async () => {
|
||||
if (!isLoading) return; // Prevent calls when not loading new doc
|
||||
|
|
@ -72,7 +75,7 @@ export default function PDFViewerPage() {
|
|||
<>
|
||||
<div className="p-2 pb-2 border-b border-offbase">
|
||||
<div className="flex flex-wrap items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => {clearCurrDoc(); stop();}}
|
||||
|
|
@ -100,6 +103,14 @@ export default function PDFViewerPage() {
|
|||
+
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
className="rounded-full p-2 text-foreground hover:bg-offbase focus:bg-offbase
|
||||
focus:outline-none focus:ring-2 focus:ring-accent transition-colors"
|
||||
aria-label="View Settings"
|
||||
>
|
||||
<SettingsIcon className="w-5 h-5 hover:animate-spin-slow" />
|
||||
</Button>
|
||||
</div>
|
||||
<h1 className="mr-2 text-md font-semibold text-foreground truncate">
|
||||
{isLoading ? 'Loading...' : currDocName}
|
||||
|
|
@ -113,6 +124,7 @@ export default function PDFViewerPage() {
|
|||
) : (
|
||||
<PDFViewer zoomLevel={zoomLevel} />
|
||||
)}
|
||||
<PDFViewSettings isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
134
src/components/PDFViewSettings.tsx
Normal file
134
src/components/PDFViewSettings.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
'use client';
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
|
||||
import { useConfig, ViewType } from '@/contexts/ConfigContext';
|
||||
import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons';
|
||||
|
||||
interface PDFViewSettingsProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const viewTypes = [
|
||||
{ id: 'single', name: 'Single Page' },
|
||||
{ id: 'dual', name: 'Two Pages' },
|
||||
{ id: 'scroll', name: 'Continuous Scroll' },
|
||||
];
|
||||
|
||||
export function PDFViewSettings({ isOpen, setIsOpen }: PDFViewSettingsProps) {
|
||||
const { viewType, updateConfigKey } = useConfig();
|
||||
const selectedView = viewTypes.find(v => v.id === viewType) || viewTypes[0];
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/25 backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-6 text-foreground"
|
||||
>
|
||||
View Settings
|
||||
</DialogTitle>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-foreground">Mode</label>
|
||||
<Listbox
|
||||
value={selectedView}
|
||||
onChange={(newView) => updateConfigKey('viewType', newView.id as ViewType)}
|
||||
>
|
||||
<div className="relative">
|
||||
<ListboxButton className="relative w-full cursor-pointer rounded-lg bg-background py-2 pl-3 pr-10 text-left text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent">
|
||||
<span className="block truncate">{selectedView.name}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon className="h-5 w-5 text-muted" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<ListboxOptions className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-background py-1 shadow-lg ring-1 ring-black/5 focus:outline-none">
|
||||
{viewTypes.map((view) => (
|
||||
<ListboxOption
|
||||
key={view.id}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
|
||||
active ? 'bg-accent/10 text-accent' : 'text-foreground'
|
||||
}`
|
||||
}
|
||||
value={view}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
|
||||
{view.name}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
{selectedView.id === 'scroll' && (
|
||||
<p className="text-sm text-warning pt-2">
|
||||
Note: Continuous scroll may perform poorly for larger documents.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-lg bg-background px-4 py-2 text-sm
|
||||
font-medium text-foreground hover:bg-background/90 focus:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
|
||||
transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { PDFSkeleton } from './PDFSkeleton';
|
|||
import { useTTS } from '@/contexts/TTSContext';
|
||||
import { usePDF } from '@/contexts/PDFContext';
|
||||
import TTSPlayer from '@/components/player/TTSPlayer';
|
||||
import { useConfig } from '@/contexts/ConfigContext';
|
||||
|
||||
interface PDFViewerProps {
|
||||
zoomLevel: number;
|
||||
|
|
@ -17,6 +18,9 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
|||
const [containerWidth, setContainerWidth] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Config context
|
||||
const { viewType } = useConfig();
|
||||
|
||||
// TTS context
|
||||
const {
|
||||
currentSentence,
|
||||
|
|
@ -112,21 +116,36 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
|||
const [pageWidth, setPageWidth] = useState<number>(595); // default A4 width
|
||||
const [pageHeight, setPageHeight] = useState<number>(842); // default A4 height
|
||||
|
||||
// Modify scale calculation function to handle orientation
|
||||
// Calculate which pages to show based on viewType
|
||||
const leftPage = viewType === 'dual'
|
||||
? (currDocPage % 2 === 0 ? currDocPage - 1 : currDocPage)
|
||||
: currDocPage;
|
||||
const rightPage = viewType === 'dual'
|
||||
? (currDocPage % 2 === 0 ? currDocPage : currDocPage + 1)
|
||||
: null;
|
||||
|
||||
// Modify scale calculation to account for view type
|
||||
const calculateScale = useCallback((width = pageWidth, height = pageHeight): number => {
|
||||
const margin = 24; // 24px padding on each side
|
||||
const containerHeight = window.innerHeight - 100; // approximate visible height
|
||||
const targetWidth = containerWidth - margin;
|
||||
const margin = viewType === 'dual' ? 48 : 24; // adjust margin based on view type
|
||||
const containerHeight = window.innerHeight - 100;
|
||||
const targetWidth = viewType === 'dual'
|
||||
? (containerWidth - margin) / 2 // divide by 2 for dual pages
|
||||
: containerWidth - margin;
|
||||
const targetHeight = containerHeight - margin;
|
||||
|
||||
// Calculate scales based on both dimensions
|
||||
if (viewType === 'scroll') {
|
||||
// For scroll mode, use a more comfortable width-based scale
|
||||
// Use 75% of the width-based scale to make it less zoomed in
|
||||
const scaleByWidth = (targetWidth / width) * 0.75;
|
||||
return scaleByWidth * (zoomLevel / 100);
|
||||
}
|
||||
|
||||
const scaleByWidth = targetWidth / width;
|
||||
const scaleByHeight = targetHeight / height;
|
||||
|
||||
// Use the smaller scale to ensure the page fits both dimensions
|
||||
const baseScale = Math.min(scaleByWidth, scaleByHeight);
|
||||
return baseScale * (zoomLevel / 100);
|
||||
}, [containerWidth, zoomLevel, pageWidth, pageHeight]);
|
||||
}, [containerWidth, zoomLevel, pageWidth, pageHeight, viewType]);
|
||||
|
||||
// Add resize observer effect
|
||||
useEffect(() => {
|
||||
|
|
@ -151,24 +170,61 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
|
|||
file={currDocURL}
|
||||
onLoadSuccess={(pdf) => {
|
||||
onDocumentLoadSuccess(pdf);
|
||||
//handlePageChange(1); // Load first page text
|
||||
}}
|
||||
className="flex flex-col items-center m-0"
|
||||
>
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<Page
|
||||
pageNumber={currDocPage}
|
||||
renderAnnotationLayer={true}
|
||||
renderTextLayer={true}
|
||||
className="shadow-lg"
|
||||
scale={calculateScale()}
|
||||
onLoadSuccess={(page) => {
|
||||
setPageWidth(page.originalWidth);
|
||||
setPageHeight(page.originalHeight);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{viewType === 'scroll' ? (
|
||||
// Scroll mode: render all pages
|
||||
<div className="flex flex-col gap-4">
|
||||
{currDocPages && [...Array(currDocPages)].map((_, i) => (
|
||||
<Page
|
||||
key={`page_${i + 1}`}
|
||||
pageNumber={i + 1}
|
||||
renderAnnotationLayer={true}
|
||||
renderTextLayer={i + 1 === currDocPage}
|
||||
className="shadow-lg"
|
||||
scale={calculateScale()}
|
||||
onLoadSuccess={(page) => {
|
||||
setPageWidth(page.originalWidth);
|
||||
setPageHeight(page.originalHeight);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Single/Dual page mode
|
||||
<div className="flex justify-center gap-4">
|
||||
{currDocPages && leftPage > 0 && (
|
||||
<Page
|
||||
key={`page_${leftPage}`}
|
||||
pageNumber={leftPage}
|
||||
renderAnnotationLayer={true}
|
||||
renderTextLayer={leftPage === currDocPage}
|
||||
className="shadow-lg"
|
||||
scale={calculateScale()}
|
||||
onLoadSuccess={(page) => {
|
||||
setPageWidth(page.originalWidth);
|
||||
setPageHeight(page.originalHeight);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currDocPages && rightPage && rightPage <= currDocPages && viewType === 'dual' && (
|
||||
<Page
|
||||
key={`page_${rightPage}`}
|
||||
pageNumber={rightPage}
|
||||
renderAnnotationLayer={true}
|
||||
renderTextLayer={rightPage === currDocPage}
|
||||
className="shadow-lg"
|
||||
scale={calculateScale()}
|
||||
onLoadSuccess={(page) => {
|
||||
setPageWidth(page.originalWidth);
|
||||
setPageHeight(page.originalHeight);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Document>
|
||||
<TTSPlayer
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Fragment, useState, useEffect } from 'react';
|
|||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useConfig } from '@/contexts/ConfigContext';
|
||||
import { ChevronUpDownIcon, CheckIcon } from './icons/Icons';
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -70,18 +71,7 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
|||
<ListboxButton className="relative w-full cursor-pointer rounded-lg bg-background py-2 pl-3 pr-10 text-left text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent">
|
||||
<span className="block truncate">{selectedTheme.name}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 text-muted"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronUpDownIcon className="h-5 w-5 text-muted" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
<Transition
|
||||
|
|
@ -108,18 +98,7 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
|||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -128,3 +128,48 @@ export function ChevronUpDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
|||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={props.className || "w-6 h-6"}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className={props.className}
|
||||
width={props.width || "1.5em"}
|
||||
height={props.height || "1.5em"}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,39 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { getItem, indexedDBService, setItem } from '@/services/indexedDB';
|
||||
|
||||
export type ViewType = 'single' | 'dual' | 'scroll';
|
||||
interface ConfigContextType {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => Promise<void>;
|
||||
viewType: ViewType;
|
||||
voiceSpeed: number;
|
||||
voice: string;
|
||||
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>;
|
||||
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
isDBReady: boolean;
|
||||
}
|
||||
|
||||
// Add this type to help with type safety
|
||||
type ConfigValues = {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
viewType: ViewType;
|
||||
voiceSpeed: number;
|
||||
voice: string;
|
||||
};
|
||||
|
||||
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||
|
||||
export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
||||
// Config state
|
||||
const [apiKey, setApiKey] = useState<string>('');
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
const [viewType, setViewType] = useState<ViewType>('single');
|
||||
const [voiceSpeed, setVoiceSpeed] = useState<number>(1);
|
||||
const [voice, setVoice] = useState<string>('af_sarah');
|
||||
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isDBReady, setIsDBReady] = useState(false);
|
||||
|
||||
|
|
@ -29,13 +49,15 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
|||
// Now load config
|
||||
const cachedApiKey = await getItem('apiKey');
|
||||
const cachedBaseUrl = await getItem('baseUrl');
|
||||
const cachedViewType = await getItem('viewType');
|
||||
const cachedVoiceSpeed = await getItem('voiceSpeed');
|
||||
const cachedVoice = await getItem('voice');
|
||||
|
||||
if (cachedApiKey) {
|
||||
console.log('Cached API key found:', cachedApiKey);
|
||||
}
|
||||
if (cachedBaseUrl) {
|
||||
console.log('Cached base URL found:', cachedBaseUrl);
|
||||
}
|
||||
if (cachedApiKey) console.log('Cached API key found:', cachedApiKey);
|
||||
if (cachedBaseUrl) console.log('Cached base URL found:', cachedBaseUrl);
|
||||
if (cachedViewType) console.log('Cached view type found:', cachedViewType);
|
||||
if (cachedVoiceSpeed) console.log('Cached voice speed found:', cachedVoiceSpeed);
|
||||
if (cachedVoice) console.log('Cached voice found:', cachedVoice);
|
||||
|
||||
// If not in cache, use env variables
|
||||
const defaultApiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY || '';
|
||||
|
|
@ -44,6 +66,9 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
|||
// Set the values
|
||||
setApiKey(cachedApiKey || defaultApiKey);
|
||||
setBaseUrl(cachedBaseUrl || defaultBaseUrl);
|
||||
setViewType((cachedViewType || 'single') as ViewType);
|
||||
setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1'));
|
||||
setVoice(cachedVoice || 'af_sarah');
|
||||
|
||||
// If not in cache, save to cache
|
||||
if (!cachedApiKey) {
|
||||
|
|
@ -52,6 +77,9 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
|||
if (!cachedBaseUrl) {
|
||||
await setItem('baseUrl', defaultBaseUrl);
|
||||
}
|
||||
if (!cachedViewType) {
|
||||
await setItem('viewType', 'single');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing:', error);
|
||||
|
|
@ -79,8 +107,44 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
};
|
||||
|
||||
const updateConfigKey = async <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => {
|
||||
try {
|
||||
await setItem(key, value.toString());
|
||||
switch (key) {
|
||||
case 'apiKey':
|
||||
setApiKey(value as string);
|
||||
break;
|
||||
case 'baseUrl':
|
||||
setBaseUrl(value as string);
|
||||
break;
|
||||
case 'viewType':
|
||||
setViewType(value as ViewType);
|
||||
break;
|
||||
case 'voiceSpeed':
|
||||
setVoiceSpeed(value as number);
|
||||
break;
|
||||
case 'voice':
|
||||
setVoice(value as string);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating config key ${key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ apiKey, baseUrl, updateConfig, isLoading, isDBReady }}>
|
||||
<ConfigContext.Provider value={{
|
||||
apiKey,
|
||||
baseUrl,
|
||||
viewType,
|
||||
voiceSpeed,
|
||||
voice,
|
||||
updateConfig,
|
||||
updateConfigKey,
|
||||
isLoading,
|
||||
isDBReady
|
||||
}}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue