diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7f2e3f5..1ea0ce0 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -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"
>
-
+
diff --git a/src/app/pdf/[id]/page.tsx b/src/app/pdf/[id]/page.tsx
index 1057f23..fca825c 100644
--- a/src/app/pdf/[id]/page.tsx
+++ b/src/app/pdf/[id]/page.tsx
@@ -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
(null);
const [isLoading, setIsLoading] = useState(true);
const [zoomLevel, setZoomLevel] = useState(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() {
<>
-
+
{clearCurrDoc(); stop();}}
@@ -100,6 +103,14 @@ export default function PDFViewerPage() {
+
+
{isLoading ? 'Loading...' : currDocName}
@@ -113,6 +124,7 @@ export default function PDFViewerPage() {
) : (
)}
+
>
);
}
diff --git a/src/components/PDFViewSettings.tsx b/src/components/PDFViewSettings.tsx
new file mode 100644
index 0000000..b32ae03
--- /dev/null
+++ b/src/components/PDFViewSettings.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/src/components/PDFViewer.tsx b/src/components/PDFViewer.tsx
index e07b6cf..314bb82 100644
--- a/src/components/PDFViewer.tsx
+++ b/src/components/PDFViewer.tsx
@@ -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(0);
const containerRef = useRef(null);
+ // Config context
+ const { viewType } = useConfig();
+
// TTS context
const {
currentSentence,
@@ -112,21 +116,36 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
const [pageWidth, setPageWidth] = useState(595); // default A4 width
const [pageHeight, setPageHeight] = useState(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"
>
-
-
{
- setPageWidth(page.originalWidth);
- setPageHeight(page.originalHeight);
- }}
- />
-
+ {viewType === 'scroll' ? (
+ // Scroll mode: render all pages
+
+ {currDocPages && [...Array(currDocPages)].map((_, i) => (
+
{
+ setPageWidth(page.originalWidth);
+ setPageHeight(page.originalHeight);
+ }}
+ />
+ ))}
+
+ ) : (
+ // Single/Dual page mode
+
+ {currDocPages && leftPage > 0 && (
+
{
+ setPageWidth(page.originalWidth);
+ setPageHeight(page.originalHeight);
+ }}
+ />
+ )}
+ {currDocPages && rightPage && rightPage <= currDocPages && viewType === 'dual' && (
+ {
+ setPageWidth(page.originalWidth);
+ setPageHeight(page.originalHeight);
+ }}
+ />
+ )}
+
+ )}
{selectedTheme.name}
-
+
{selected ? (
-
+
) : null}
>
diff --git a/src/components/icons/Icons.tsx b/src/components/icons/Icons.tsx
index 784906f..f7ae714 100644
--- a/src/components/icons/Icons.tsx
+++ b/src/components/icons/Icons.tsx
@@ -128,3 +128,48 @@ export function ChevronUpDownIcon(props: React.SVGProps) {
);
}
+
+export function SettingsIcon(props: React.SVGProps) {
+ return (
+
+ );
+}
+
+export function CheckIcon(props: React.SVGProps) {
+ return (
+
+ );
+}
diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx
index 697958f..66041c2 100644
--- a/src/contexts/ConfigContext.tsx
+++ b/src/contexts/ConfigContext.tsx
@@ -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;
+ viewType: ViewType;
+ voiceSpeed: number;
+ voice: string;
+ updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise;
+ updateConfigKey: (key: K, value: ConfigValues[K]) => Promise;
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(undefined);
export function ConfigProvider({ children }: { children: React.ReactNode }) {
+ // Config state
const [apiKey, setApiKey] = useState('');
const [baseUrl, setBaseUrl] = useState('');
+ const [viewType, setViewType] = useState('single');
+ const [voiceSpeed, setVoiceSpeed] = useState(1);
+ const [voice, setVoice] = useState('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 (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 (
-
+
{children}
);