Skip to content

Commit 5591931

Browse files
committed
Add different view modes
1 parent 92ee491 commit 5591931

File tree

7 files changed

+346
-73
lines changed

7 files changed

+346
-73
lines changed

src/app/page.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from 'react';
44
import { PDFUploader } from '@/components/PDFUploader';
55
import { DocumentList } from '@/components/DocumentList';
66
import { SettingsModal } from '@/components/SettingsModal';
7+
import { SettingsIcon } from '@/components/icons/Icons';
78

89
export default function Home() {
910
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
@@ -18,25 +19,7 @@ export default function Home() {
1819
focus:outline-none focus:ring-2 focus:ring-accent transition-colors"
1920
aria-label="Settings"
2021
>
21-
<svg
22-
xmlns="http://www.w3.org/2000/svg"
23-
fill="none"
24-
viewBox="0 0 24 24"
25-
strokeWidth={1.5}
26-
stroke="currentColor"
27-
className="w-6 h-6 hover:animate-spin-slow"
28-
>
29-
<path
30-
strokeLinecap="round"
31-
strokeLinejoin="round"
32-
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"
33-
/>
34-
<path
35-
strokeLinecap="round"
36-
strokeLinejoin="round"
37-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
38-
/>
39-
</svg>
22+
<SettingsIcon className="w-6 h-6 hover:animate-spin-slow" />
4023
</button>
4124
</div>
4225
<div className="flex flex-col items-center gap-6">

src/app/pdf/[id]/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { useCallback, useEffect, useState } from 'react';
88
import { PDFSkeleton } from '@/components/PDFSkeleton';
99
import { useTTS } from '@/contexts/TTSContext';
1010
import { Button } from '@headlessui/react';
11+
import { PDFViewSettings } from '@/components/PDFViewSettings';
12+
import { SettingsIcon } from '@/components/icons/Icons';
1113

1214
// Dynamic import for client-side rendering only
1315
const PDFViewer = dynamic(
@@ -25,6 +27,7 @@ export default function PDFViewerPage() {
2527
const [error, setError] = useState<string | null>(null);
2628
const [isLoading, setIsLoading] = useState(true);
2729
const [zoomLevel, setZoomLevel] = useState<number>(100);
30+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
2831

2932
const loadDocument = useCallback(async () => {
3033
if (!isLoading) return; // Prevent calls when not loading new doc
@@ -72,7 +75,7 @@ export default function PDFViewerPage() {
7275
<>
7376
<div className="p-2 pb-2 border-b border-offbase">
7477
<div className="flex flex-wrap items-center justify-between">
75-
<div className="flex items-center gap-4">
78+
<div className="flex items-center gap-2">
7679
<Link
7780
href="/"
7881
onClick={() => {clearCurrDoc(); stop();}}
@@ -100,6 +103,14 @@ export default function PDFViewerPage() {
100103
101104
</Button>
102105
</div>
106+
<Button
107+
onClick={() => setIsSettingsOpen(true)}
108+
className="rounded-full p-2 text-foreground hover:bg-offbase focus:bg-offbase
109+
focus:outline-none focus:ring-2 focus:ring-accent transition-colors"
110+
aria-label="View Settings"
111+
>
112+
<SettingsIcon className="w-5 h-5 hover:animate-spin-slow" />
113+
</Button>
103114
</div>
104115
<h1 className="mr-2 text-md font-semibold text-foreground truncate">
105116
{isLoading ? 'Loading...' : currDocName}
@@ -113,6 +124,7 @@ export default function PDFViewerPage() {
113124
) : (
114125
<PDFViewer zoomLevel={zoomLevel} />
115126
)}
127+
<PDFViewSettings isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
116128
</>
117129
);
118130
}

src/components/PDFViewSettings.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
'use client';
2+
3+
import { Fragment } from 'react';
4+
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
5+
import { useConfig, ViewType } from '@/contexts/ConfigContext';
6+
import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons';
7+
8+
interface PDFViewSettingsProps {
9+
isOpen: boolean;
10+
setIsOpen: (isOpen: boolean) => void;
11+
}
12+
13+
const viewTypes = [
14+
{ id: 'single', name: 'Single Page' },
15+
{ id: 'dual', name: 'Two Pages' },
16+
{ id: 'scroll', name: 'Continuous Scroll' },
17+
];
18+
19+
export function PDFViewSettings({ isOpen, setIsOpen }: PDFViewSettingsProps) {
20+
const { viewType, updateConfigKey } = useConfig();
21+
const selectedView = viewTypes.find(v => v.id === viewType) || viewTypes[0];
22+
23+
return (
24+
<Transition appear show={isOpen} as={Fragment}>
25+
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
26+
<TransitionChild
27+
as={Fragment}
28+
enter="ease-out duration-300"
29+
enterFrom="opacity-0"
30+
enterTo="opacity-100"
31+
leave="ease-in duration-200"
32+
leaveFrom="opacity-100"
33+
leaveTo="opacity-0"
34+
>
35+
<div className="fixed inset-0 bg-black/25 backdrop-blur-sm" />
36+
</TransitionChild>
37+
38+
<div className="fixed inset-0 overflow-y-auto">
39+
<div className="flex min-h-full items-center justify-center p-4 text-center">
40+
<TransitionChild
41+
as={Fragment}
42+
enter="ease-out duration-300"
43+
enterFrom="opacity-0 scale-95"
44+
enterTo="opacity-100 scale-100"
45+
leave="ease-in duration-200"
46+
leaveFrom="opacity-100 scale-100"
47+
leaveTo="opacity-0 scale-95"
48+
>
49+
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
50+
<DialogTitle
51+
as="h3"
52+
className="text-lg font-semibold leading-6 text-foreground"
53+
>
54+
View Settings
55+
</DialogTitle>
56+
<div className="mt-4">
57+
<div className="space-y-4">
58+
<div className="space-y-2">
59+
<label className="block text-sm font-medium text-foreground">Mode</label>
60+
<Listbox
61+
value={selectedView}
62+
onChange={(newView) => updateConfigKey('viewType', newView.id as ViewType)}
63+
>
64+
<div className="relative">
65+
<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">
66+
<span className="block truncate">{selectedView.name}</span>
67+
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
68+
<ChevronUpDownIcon className="h-5 w-5 text-muted" />
69+
</span>
70+
</ListboxButton>
71+
<Transition
72+
as={Fragment}
73+
leave="transition ease-in duration-100"
74+
leaveFrom="opacity-100"
75+
leaveTo="opacity-0"
76+
>
77+
<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">
78+
{viewTypes.map((view) => (
79+
<ListboxOption
80+
key={view.id}
81+
className={({ active }) =>
82+
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
83+
active ? 'bg-accent/10 text-accent' : 'text-foreground'
84+
}`
85+
}
86+
value={view}
87+
>
88+
{({ selected }) => (
89+
<>
90+
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
91+
{view.name}
92+
</span>
93+
{selected ? (
94+
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
95+
<CheckIcon className="h-5 w-5" />
96+
</span>
97+
) : null}
98+
</>
99+
)}
100+
</ListboxOption>
101+
))}
102+
</ListboxOptions>
103+
</Transition>
104+
</div>
105+
</Listbox>
106+
{selectedView.id === 'scroll' && (
107+
<p className="text-sm text-warning pt-2">
108+
Note: Continuous scroll may perform poorly for larger documents.
109+
</p>
110+
)}
111+
</div>
112+
</div>
113+
</div>
114+
115+
<div className="mt-3 flex justify-end">
116+
<button
117+
type="button"
118+
className="inline-flex justify-center rounded-lg bg-background px-4 py-2 text-sm
119+
font-medium text-foreground hover:bg-background/90 focus:outline-none
120+
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
121+
transition-colors"
122+
onClick={() => setIsOpen(false)}
123+
>
124+
Close
125+
</button>
126+
</div>
127+
</DialogPanel>
128+
</TransitionChild>
129+
</div>
130+
</div>
131+
</Dialog>
132+
</Transition>
133+
);
134+
}

src/components/PDFViewer.tsx

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PDFSkeleton } from './PDFSkeleton';
88
import { useTTS } from '@/contexts/TTSContext';
99
import { usePDF } from '@/contexts/PDFContext';
1010
import TTSPlayer from '@/components/player/TTSPlayer';
11+
import { useConfig } from '@/contexts/ConfigContext';
1112

1213
interface PDFViewerProps {
1314
zoomLevel: number;
@@ -17,6 +18,9 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
1718
const [containerWidth, setContainerWidth] = useState<number>(0);
1819
const containerRef = useRef<HTMLDivElement>(null);
1920

21+
// Config context
22+
const { viewType } = useConfig();
23+
2024
// TTS context
2125
const {
2226
currentSentence,
@@ -112,21 +116,36 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
112116
const [pageWidth, setPageWidth] = useState<number>(595); // default A4 width
113117
const [pageHeight, setPageHeight] = useState<number>(842); // default A4 height
114118

115-
// Modify scale calculation function to handle orientation
119+
// Calculate which pages to show based on viewType
120+
const leftPage = viewType === 'dual'
121+
? (currDocPage % 2 === 0 ? currDocPage - 1 : currDocPage)
122+
: currDocPage;
123+
const rightPage = viewType === 'dual'
124+
? (currDocPage % 2 === 0 ? currDocPage : currDocPage + 1)
125+
: null;
126+
127+
// Modify scale calculation to account for view type
116128
const calculateScale = useCallback((width = pageWidth, height = pageHeight): number => {
117-
const margin = 24; // 24px padding on each side
118-
const containerHeight = window.innerHeight - 100; // approximate visible height
119-
const targetWidth = containerWidth - margin;
129+
const margin = viewType === 'dual' ? 48 : 24; // adjust margin based on view type
130+
const containerHeight = window.innerHeight - 100;
131+
const targetWidth = viewType === 'dual'
132+
? (containerWidth - margin) / 2 // divide by 2 for dual pages
133+
: containerWidth - margin;
120134
const targetHeight = containerHeight - margin;
121135

122-
// Calculate scales based on both dimensions
136+
if (viewType === 'scroll') {
137+
// For scroll mode, use a more comfortable width-based scale
138+
// Use 75% of the width-based scale to make it less zoomed in
139+
const scaleByWidth = (targetWidth / width) * 0.75;
140+
return scaleByWidth * (zoomLevel / 100);
141+
}
142+
123143
const scaleByWidth = targetWidth / width;
124144
const scaleByHeight = targetHeight / height;
125145

126-
// Use the smaller scale to ensure the page fits both dimensions
127146
const baseScale = Math.min(scaleByWidth, scaleByHeight);
128147
return baseScale * (zoomLevel / 100);
129-
}, [containerWidth, zoomLevel, pageWidth, pageHeight]);
148+
}, [containerWidth, zoomLevel, pageWidth, pageHeight, viewType]);
130149

131150
// Add resize observer effect
132151
useEffect(() => {
@@ -151,24 +170,61 @@ export function PDFViewer({ zoomLevel }: PDFViewerProps) {
151170
file={currDocURL}
152171
onLoadSuccess={(pdf) => {
153172
onDocumentLoadSuccess(pdf);
154-
//handlePageChange(1); // Load first page text
155173
}}
156174
className="flex flex-col items-center m-0"
157175
>
158176
<div>
159-
<div className="flex justify-center">
160-
<Page
161-
pageNumber={currDocPage}
162-
renderAnnotationLayer={true}
163-
renderTextLayer={true}
164-
className="shadow-lg"
165-
scale={calculateScale()}
166-
onLoadSuccess={(page) => {
167-
setPageWidth(page.originalWidth);
168-
setPageHeight(page.originalHeight);
169-
}}
170-
/>
171-
</div>
177+
{viewType === 'scroll' ? (
178+
// Scroll mode: render all pages
179+
<div className="flex flex-col gap-4">
180+
{currDocPages && [...Array(currDocPages)].map((_, i) => (
181+
<Page
182+
key={`page_${i + 1}`}
183+
pageNumber={i + 1}
184+
renderAnnotationLayer={true}
185+
renderTextLayer={i + 1 === currDocPage}
186+
className="shadow-lg"
187+
scale={calculateScale()}
188+
onLoadSuccess={(page) => {
189+
setPageWidth(page.originalWidth);
190+
setPageHeight(page.originalHeight);
191+
}}
192+
/>
193+
))}
194+
</div>
195+
) : (
196+
// Single/Dual page mode
197+
<div className="flex justify-center gap-4">
198+
{currDocPages && leftPage > 0 && (
199+
<Page
200+
key={`page_${leftPage}`}
201+
pageNumber={leftPage}
202+
renderAnnotationLayer={true}
203+
renderTextLayer={leftPage === currDocPage}
204+
className="shadow-lg"
205+
scale={calculateScale()}
206+
onLoadSuccess={(page) => {
207+
setPageWidth(page.originalWidth);
208+
setPageHeight(page.originalHeight);
209+
}}
210+
/>
211+
)}
212+
{currDocPages && rightPage && rightPage <= currDocPages && viewType === 'dual' && (
213+
<Page
214+
key={`page_${rightPage}`}
215+
pageNumber={rightPage}
216+
renderAnnotationLayer={true}
217+
renderTextLayer={rightPage === currDocPage}
218+
className="shadow-lg"
219+
scale={calculateScale()}
220+
onLoadSuccess={(page) => {
221+
setPageWidth(page.originalWidth);
222+
setPageHeight(page.originalHeight);
223+
}}
224+
/>
225+
)}
226+
</div>
227+
)}
172228
</div>
173229
</Document>
174230
<TTSPlayer

0 commit comments

Comments
 (0)