Skip to content

Commit 82bb91a

Browse files
committed
Add TXT and MD support with and HTML viewer with react markdown + many refactors to support it
1 parent b0a3e2f commit 82bb91a

File tree

18 files changed

+2260
-73
lines changed

18 files changed

+2260
-73
lines changed

package-lock.json

Lines changed: 1550 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@
2626
"react-dom": "^19.0.0",
2727
"react-dropzone": "^14.3.8",
2828
"react-hot-toast": "^2.5.2",
29+
"react-markdown": "^10.1.0",
2930
"react-pdf": "^9.2.1",
3031
"react-reader": "^2.0.12",
32+
"remark-gfm": "^4.0.1",
3133
"string-similarity": "^4.0.4",
3234
"uuid": "^11.1.0"
3335
},
3436
"devDependencies": {
3537
"@eslint/eslintrc": "^3",
3638
"@playwright/test": "^1.50.1",
39+
"@tailwindcss/typography": "^0.5.16",
3740
"@types/node": "^20",
3841
"@types/react": "^19",
3942
"@types/react-dom": "^19",

src/app/globals.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@tailwind base;
22
@tailwind components;
33
@tailwind utilities;
4+
@plugin "@tailwindcss/typography";
45

56
:root,
67
html.light {

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use client';
2+
3+
import { useParams } from "next/navigation";
4+
import Link from 'next/link';
5+
import { useCallback, useEffect, useState } from 'react';
6+
import { useHTML } from '@/contexts/HTMLContext';
7+
import { DocumentSkeleton } from '@/components/DocumentSkeleton';
8+
import { HTMLViewer } from '@/components/HTMLViewer';
9+
import { Button } from '@headlessui/react';
10+
import { DocumentSettings } from '@/components/DocumentSettings';
11+
import { SettingsIcon } from '@/components/icons/Icons';
12+
import { useTTS } from "@/contexts/TTSContext";
13+
14+
export default function HTMLPage() {
15+
const { id } = useParams();
16+
const { setCurrentDocument, currDocName, clearCurrDoc } = useHTML();
17+
const { stop } = useTTS();
18+
const [error, setError] = useState<string | null>(null);
19+
const [isLoading, setIsLoading] = useState(true);
20+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
21+
22+
const loadDocument = useCallback(async () => {
23+
if (!isLoading) return;
24+
console.log('Loading new HTML document (from page.tsx)');
25+
stop();
26+
try {
27+
if (!id) {
28+
setError('Document not found');
29+
return;
30+
}
31+
await setCurrentDocument(id as string);
32+
} catch (err) {
33+
console.error('Error loading document:', err);
34+
setError('Failed to load document');
35+
} finally {
36+
setIsLoading(false);
37+
}
38+
}, [isLoading, id, setCurrentDocument, stop]);
39+
40+
useEffect(() => {
41+
loadDocument();
42+
}, [loadDocument]);
43+
44+
if (error) {
45+
return (
46+
<div className="flex flex-col items-center justify-center min-h-screen">
47+
<p className="text-red-500 mb-4">{error}</p>
48+
<Link
49+
href="/"
50+
onClick={() => {clearCurrDoc();}}
51+
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
52+
>
53+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
55+
</svg>
56+
Back to Documents
57+
</Link>
58+
</div>
59+
);
60+
}
61+
62+
return (
63+
<>
64+
<div className="p-2 pb-2 border-b border-offbase">
65+
<div className="flex flex-wrap items-center justify-between">
66+
<div className="flex items-center gap-1">
67+
<Link
68+
href="/"
69+
onClick={() => {clearCurrDoc();}}
70+
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.02]"
71+
>
72+
<svg className="w-4 h-4 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24">
73+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
74+
</svg>
75+
Documents
76+
</Link>
77+
<Button
78+
onClick={() => setIsSettingsOpen(true)}
79+
className="rounded-full p-1 text-foreground hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.1] hover:text-accent"
80+
aria-label="View Settings"
81+
>
82+
<SettingsIcon className="w-5 h-5 hover:animate-spin-slow" />
83+
</Button>
84+
</div>
85+
<h1 className="ml-2 mr-2 text-md font-semibold text-foreground truncate">
86+
{isLoading ? 'Loading...' : currDocName}
87+
</h1>
88+
</div>
89+
</div>
90+
{isLoading ? (
91+
<div className="p-4">
92+
<DocumentSkeleton />
93+
</div>
94+
) : (
95+
<HTMLViewer className="p-4" />
96+
)}
97+
<DocumentSettings html isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
98+
</>
99+
);
100+
}

src/app/providers.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { EPUBProvider } from '@/contexts/EPUBContext';
88
import { TTSProvider } from '@/contexts/TTSContext';
99
import { ThemeProvider } from '@/contexts/ThemeContext';
1010
import { ConfigProvider } from '@/contexts/ConfigContext';
11+
import { HTMLProvider } from '@/contexts/HTMLContext';
1112

1213
export function Providers({ children }: { children: ReactNode }) {
1314
return (
@@ -17,7 +18,9 @@ export function Providers({ children }: { children: ReactNode }) {
1718
<TTSProvider>
1819
<PDFProvider>
1920
<EPUBProvider>
20-
{children}
21+
<HTMLProvider>
22+
{children}
23+
</HTMLProvider>
2124
</EPUBProvider>
2225
</PDFProvider>
2326
</TTSProvider>

src/components/DocumentSettings.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ import { ProgressPopup } from '@/components/ProgressPopup';
1111

1212
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
1313

14-
interface DocViewSettingsProps {
15-
isOpen: boolean;
16-
setIsOpen: (isOpen: boolean) => void;
17-
epub?: boolean;
18-
}
19-
2014
const viewTypes = [
2115
{ id: 'single', name: 'Single Page' },
2216
{ id: 'dual', name: 'Two Pages' },
@@ -28,7 +22,12 @@ const audioFormats = [
2822
{ id: 'm4b', name: 'M4B' },
2923
];
3024

31-
export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsProps) {
25+
export function DocumentSettings({ isOpen, setIsOpen, epub, html }: {
26+
isOpen: boolean,
27+
setIsOpen: (isOpen: boolean) => void,
28+
epub?: boolean,
29+
html?: boolean
30+
}) {
3231
const {
3332
viewType,
3433
skipBlank,
@@ -203,7 +202,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro
203202
</div>}
204203

205204
<div className="space-y-4">
206-
{!epub && <div className="space-y-6">
205+
{!epub && !html && <div className="space-y-6">
207206
<div className="space-y-2">
208207
<label className="block text-sm font-medium text-foreground">
209208
Text extraction margins
@@ -347,7 +346,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro
347346

348347
</div>}
349348

350-
<div className="space-y-1">
349+
{!html && <div className="space-y-1">
351350
<label className="flex items-center space-x-2">
352351
<input
353352
type="checkbox"
@@ -360,7 +359,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub }: DocViewSettingsPro
360359
<p className="text-sm text-muted pl-6">
361360
Automatically skip pages with no text content
362361
</p>
363-
</div>
362+
</div>}
364363
{epub && (
365364
<div className="space-y-1">
366365
<label className="flex items-center space-x-2">

src/components/DocumentUploader.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ interface DocumentUploaderProps {
1212
}
1313

1414
export function DocumentUploader({ className = '' }: DocumentUploaderProps) {
15-
const { addPDFDocument: addPDF, addEPUBDocument: addEPUB } = useDocuments();
15+
const {
16+
addPDFDocument: addPDF,
17+
addEPUBDocument: addEPUB,
18+
addHTMLDocument: addHTML
19+
} = useDocuments();
1620
const [isUploading, setIsUploading] = useState(false);
1721
const [isConverting, setIsConverting] = useState(false);
1822
const [error, setError] = useState<string | null>(null);
@@ -48,6 +52,8 @@ export function DocumentUploader({ className = '' }: DocumentUploaderProps) {
4852
await addPDF(file);
4953
} else if (file.type === 'application/epub+zip') {
5054
await addEPUB(file);
55+
} else if (file.type === 'text/plain' || file.type === 'text/markdown' || file.name.endsWith('.md')) {
56+
await addHTML(file);
5157
} else if (isDev && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
5258
setIsUploading(false);
5359
setIsConverting(true);
@@ -61,13 +67,15 @@ export function DocumentUploader({ className = '' }: DocumentUploaderProps) {
6167
setIsUploading(false);
6268
setIsConverting(false);
6369
}
64-
}, [addPDF, addEPUB]);
70+
}, [addHTML, addPDF, addEPUB]);
6571

6672
const { getRootProps, getInputProps, isDragActive } = useDropzone({
6773
onDrop,
6874
accept: {
6975
'application/pdf': ['.pdf'],
7076
'application/epub+zip': ['.epub'],
77+
'text/plain': ['.txt'],
78+
'text/markdown': ['.md'],
7179
...(isDev ? {
7280
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx']
7381
} : {})
@@ -105,7 +113,7 @@ export function DocumentUploader({ className = '' }: DocumentUploaderProps) {
105113
{isDragActive ? 'Drop your file here' : 'Drop your file here, or click to select'}
106114
</p>
107115
<p className="text-xs sm:text-sm text-muted">
108-
{isDev ? 'PDF, EPUB, and DOCX files are accepted' : 'PDF and EPUB files are accepted'}
116+
{isDev ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
109117
</p>
110118
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
111119
</>

src/components/HTMLViewer.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import { useRef } from 'react';
4+
import ReactMarkdown from 'react-markdown';
5+
import remarkGfm from 'remark-gfm';
6+
import { useHTML } from '@/contexts/HTMLContext';
7+
import TTSPlayer from '@/components/player/TTSPlayer';
8+
import { DocumentSkeleton } from '@/components/DocumentSkeleton';
9+
10+
interface HTMLViewerProps {
11+
className?: string;
12+
}
13+
14+
export function HTMLViewer({ className = '' }: HTMLViewerProps) {
15+
const { currDocData, currDocName } = useHTML();
16+
const containerRef = useRef<HTMLDivElement>(null);
17+
18+
if (!currDocData) {
19+
return <DocumentSkeleton />;
20+
}
21+
22+
// Check if the file is a txt file
23+
const isTxtFile = currDocName?.toLowerCase().endsWith('.txt');
24+
25+
return (
26+
<div className={`h-screen flex flex-col ${className}`} ref={containerRef}>
27+
<div className="z-10">
28+
<TTSPlayer />
29+
</div>
30+
<div className="flex-1 overflow-auto">
31+
<div className={`px-4 ${isTxtFile ? 'whitespace-pre-wrap font-mono text-sm' : 'prose dark:prose-invert'}`}>
32+
{isTxtFile ? (
33+
currDocData
34+
) : (
35+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
36+
{currDocData}
37+
</ReactMarkdown>
38+
)}
39+
</div>
40+
</div>
41+
</div>
42+
);
43+
}

src/components/doclist/DocumentList.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const generateDefaultFolderName = (doc1: DocumentListDocument, doc2: DocumentLis
3131
if (significant) {
3232
if (significant === 'pdf') return 'PDFs';
3333
if (significant === 'epub') return 'EPUBs';
34+
if (significant === 'txt' || significant === 'md') return 'Documents';
3435
return `${significant.charAt(0).toUpperCase()}${significant.slice(1)}`;
3536
}
3637
}
@@ -61,6 +62,9 @@ export function DocumentList() {
6162
epubDocs,
6263
removeEPUBDocument: removeEPUB,
6364
isEPUBLoading,
65+
htmlDocs,
66+
removeHTMLDocument: removeHTML,
67+
isHTMLLoading,
6468
} = useDocuments();
6569

6670
useEffect(() => {
@@ -112,6 +116,13 @@ export function DocumentList() {
112116
lastModified: doc.lastModified,
113117
type: 'epub' as const,
114118
})),
119+
...htmlDocs.map(doc => ({
120+
id: doc.id,
121+
name: doc.name,
122+
size: doc.size,
123+
lastModified: doc.lastModified,
124+
type: 'html' as const,
125+
})),
115126
];
116127

117128
const sortDocuments = useCallback((docs: DocumentListDocument[]) => {
@@ -143,8 +154,10 @@ export function DocumentList() {
143154
try {
144155
if (documentToDelete.type === 'pdf') {
145156
await removePDF(documentToDelete.id);
146-
} else {
157+
} else if (documentToDelete.type === 'epub') {
147158
await removeEPUB(documentToDelete.id);
159+
} else if (documentToDelete.type === 'html') {
160+
await removeHTML(documentToDelete.id);
148161
}
149162

150163
// Remove from folders if document is in one
@@ -159,7 +172,7 @@ export function DocumentList() {
159172
} catch (err) {
160173
console.error('Failed to remove document:', err);
161174
}
162-
}, [documentToDelete, removePDF, removeEPUB]);
175+
}, [documentToDelete, removePDF, removeEPUB, removeHTML]);
163176

164177
const handleDragStart = useCallback((doc: DocumentListDocument) => {
165178
if (!doc.folderId) {
@@ -272,7 +285,7 @@ export function DocumentList() {
272285
}
273286
}, [createFolder]);
274287

275-
if (isPDFLoading || isEPUBLoading) {
288+
if (isPDFLoading || isEPUBLoading || isHTMLLoading) {
276289
return <div className="w-full text-center text-muted">Loading documents...</div>;
277290
}
278291

src/components/doclist/DocumentListItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Link from 'next/link';
22
import { DragEvent } from 'react';
33
import { Button } from '@headlessui/react';
4-
import { PDFIcon, EPUBIcon } from '@/components/icons/Icons';
4+
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
55
import { DocumentListDocument } from '@/types/documents';
66

77
interface DocumentListItemProps {
@@ -52,7 +52,7 @@ export function DocumentListItem({
5252
className="document-link flex items-center align-center space-x-4 w-full truncate hover:bg-base rounded-lg p-0.5 sm:p-1 transition-colors"
5353
>
5454
<div className="flex-shrink-0">
55-
{doc.type === 'pdf' ? <PDFIcon /> : <EPUBIcon />}
55+
{doc.type === 'pdf' ? <PDFIcon /> : doc.type === 'epub' ? <EPUBIcon /> : <FileIcon />}
5656
</div>
5757
<div className="flex flex-col min-w-0 transform transition-transform duration-200 ease-in-out hover:scale-[1.02] w-full truncate">
5858
<p className="text-sm sm:text-md text-foreground font-medium truncate">{doc.name}</p>

0 commit comments

Comments
 (0)