Save document list state to confid db + add unified document types
This commit is contained in:
parent
a6915ee51d
commit
cc654f5f1d
6 changed files with 138 additions and 53 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { PDFIcon, EPUBIcon, ChevronUpDownIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon, ChevronUpDownIcon } from '@/components/icons/Icons';
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||||
|
|
@ -16,32 +16,23 @@ import {
|
||||||
PopoverPanel,
|
PopoverPanel,
|
||||||
Button,
|
Button,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
|
import { getDocumentListState, saveDocumentListState } from '@/utils/indexedDB';
|
||||||
|
import {
|
||||||
|
DocumentType,
|
||||||
|
DocumentListDocument,
|
||||||
|
Folder,
|
||||||
|
DocumentListState,
|
||||||
|
SortBy,
|
||||||
|
SortDirection
|
||||||
|
} from '@/types/documents';
|
||||||
|
|
||||||
type DocumentToDelete = {
|
type DocumentToDelete = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'pdf' | 'epub';
|
type: DocumentType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Folder = {
|
const generateDefaultFolderName = (doc1: DocumentListDocument, doc2: DocumentListDocument) => {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
documents: Array<Document>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Document = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: 'pdf' | 'epub';
|
|
||||||
size: number;
|
|
||||||
lastModified: number;
|
|
||||||
folderId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SortBy = 'name' | 'type' | 'date' | 'size';
|
|
||||||
type SortDirection = 'asc' | 'desc';
|
|
||||||
|
|
||||||
const generateDefaultFolderName = (doc1: Document, doc2: Document) => {
|
|
||||||
// Try to find common words between the two document names
|
// Try to find common words between the two document names
|
||||||
const words1 = doc1.name.toLowerCase().split(/[\s-_\.]+/);
|
const words1 = doc1.name.toLowerCase().split(/[\s-_\.]+/);
|
||||||
const words2 = doc2.name.toLowerCase().split(/[\s-_\.]+/);
|
const words2 = doc2.name.toLowerCase().split(/[\s-_\.]+/);
|
||||||
|
|
@ -74,16 +65,55 @@ export function DocumentList() {
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||||
const [folders, setFolders] = useState<Folder[]>([]);
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
const [documentToDelete, setDocumentToDelete] = useState<DocumentToDelete | null>(null);
|
const [documentToDelete, setDocumentToDelete] = useState<DocumentToDelete | null>(null);
|
||||||
const [draggedDoc, setDraggedDoc] = useState<Document | null>(null);
|
const [draggedDoc, setDraggedDoc] = useState<DocumentListDocument | null>(null);
|
||||||
const [dropTargetDoc, setDropTargetDoc] = useState<Document | null>(null);
|
const [dropTargetDoc, setDropTargetDoc] = useState<DocumentListDocument | null>(null);
|
||||||
const [newFolderName, setNewFolderName] = useState('');
|
const [newFolderName, setNewFolderName] = useState('');
|
||||||
const [pendingFolderDocs, setPendingFolderDocs] = useState<{ source: Document, target: Document } | null>(null);
|
const [pendingFolderDocs, setPendingFolderDocs] = useState<{ source: DocumentListDocument, target: DocumentListDocument } | null>(null);
|
||||||
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set());
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set());
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
const allDocuments: Document[] = [
|
useEffect(() => {
|
||||||
...pdfDocs.map(doc => ({ ...doc, type: 'pdf' as const })),
|
// Load saved state
|
||||||
...epubDocs.map(doc => ({ ...doc, type: 'epub' as const })),
|
const loadState = async () => {
|
||||||
|
const savedState = await getDocumentListState();
|
||||||
|
if (savedState) {
|
||||||
|
setSortBy(savedState.sortBy);
|
||||||
|
setSortDirection(savedState.sortDirection);
|
||||||
|
setFolders(savedState.folders);
|
||||||
|
setCollapsedFolders(new Set(savedState.collapsedFolders));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saveState = async () => {
|
||||||
|
const state: DocumentListState = {
|
||||||
|
sortBy,
|
||||||
|
sortDirection,
|
||||||
|
folders,
|
||||||
|
collapsedFolders: Array.from(collapsedFolders)
|
||||||
|
};
|
||||||
|
await saveDocumentListState(state);
|
||||||
|
};
|
||||||
|
saveState();
|
||||||
|
}, [sortBy, sortDirection, folders, collapsedFolders]);
|
||||||
|
|
||||||
|
const allDocuments: DocumentListDocument[] = [
|
||||||
|
...pdfDocs.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
name: doc.name,
|
||||||
|
size: doc.size,
|
||||||
|
lastModified: doc.lastModified,
|
||||||
|
type: 'pdf' as const,
|
||||||
|
})),
|
||||||
|
...epubDocs.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
name: doc.name,
|
||||||
|
size: doc.size,
|
||||||
|
lastModified: doc.lastModified,
|
||||||
|
type: 'epub' as const,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
const sortOptions: Array<{ value: SortBy; label: string }> = [
|
const sortOptions: Array<{ value: SortBy; label: string }> = [
|
||||||
|
|
@ -93,7 +123,7 @@ export function DocumentList() {
|
||||||
{ value: 'size', label: 'Size' },
|
{ value: 'size', label: 'Size' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const sortDocuments = useCallback((docs: Document[]) => {
|
const sortDocuments = useCallback((docs: DocumentListDocument[]) => {
|
||||||
return [...docs].sort((a, b) => {
|
return [...docs].sort((a, b) => {
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'name':
|
case 'name':
|
||||||
|
|
@ -116,7 +146,7 @@ export function DocumentList() {
|
||||||
});
|
});
|
||||||
}, [sortBy, sortDirection]);
|
}, [sortBy, sortDirection]);
|
||||||
|
|
||||||
const handleDragStart = (doc: Document) => {
|
const handleDragStart = (doc: DocumentListDocument) => {
|
||||||
// Only allow dragging documents that aren't in folders
|
// Only allow dragging documents that aren't in folders
|
||||||
if (!doc.folderId) {
|
if (!doc.folderId) {
|
||||||
setDraggedDoc(doc);
|
setDraggedDoc(doc);
|
||||||
|
|
@ -128,7 +158,7 @@ export function DocumentList() {
|
||||||
setDropTargetDoc(null);
|
setDropTargetDoc(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent, doc: Document) => {
|
const handleDragOver = (e: React.DragEvent, doc: DocumentListDocument) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) {
|
if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) {
|
||||||
// Only highlight target if neither document is in a folder
|
// Only highlight target if neither document is in a folder
|
||||||
|
|
@ -143,7 +173,7 @@ export function DocumentList() {
|
||||||
setDropTargetDoc(null);
|
setDropTargetDoc(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent, targetDoc: Document) => {
|
const handleDrop = useCallback((e: React.DragEvent, targetDoc: DocumentListDocument) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return;
|
if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return;
|
||||||
|
|
||||||
|
|
@ -214,7 +244,7 @@ export function DocumentList() {
|
||||||
}, [documentToDelete, removePDF, removeEPUB]);
|
}, [documentToDelete, removePDF, removeEPUB]);
|
||||||
|
|
||||||
const toggleFolderCollapse = (folderId: string) => {
|
const toggleFolderCollapse = (folderId: string) => {
|
||||||
setCollapsedFolders(prev => {
|
setCollapsedFolders((prev: Set<string>) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(folderId)) {
|
if (next.has(folderId)) {
|
||||||
next.delete(folderId);
|
next.delete(folderId);
|
||||||
|
|
@ -259,7 +289,7 @@ export function DocumentList() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDocument = (doc: Document) => (
|
const renderDocument = (doc: DocumentListDocument) => (
|
||||||
<div
|
<div
|
||||||
key={`${doc.type}-${doc.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
draggable={!doc.folderId} // Only make unfoldered documents draggable
|
draggable={!doc.folderId} // Only make unfoldered documents draggable
|
||||||
|
|
@ -314,7 +344,7 @@ export function DocumentList() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add this helper function before renderFolder
|
// Add this helper function before renderFolder
|
||||||
const calculateFolderSize = (documents: Document[]) => {
|
const calculateFolderSize = (documents: DocumentListDocument[]) => {
|
||||||
return documents.reduce((total, doc) => total + doc.size, 0);
|
return documents.reduce((total, doc) => total + doc.size, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { createContext, useContext, ReactNode } from 'react';
|
import { createContext, useContext, ReactNode } from 'react';
|
||||||
import { usePDFDocuments } from '@/hooks/usePDFDocuments';
|
import { usePDFDocuments } from '@/hooks/usePDFDocuments';
|
||||||
import { useEPUBDocuments } from '@/hooks/useEPUBDocuments';
|
import { useEPUBDocuments } from '@/hooks/useEPUBDocuments';
|
||||||
import { PDFDocument, EPUBDocument } from '@/utils/indexedDB';
|
import { PDFDocument, EPUBDocument } from '@/types/documents';
|
||||||
|
|
||||||
interface DocumentContextType {
|
interface DocumentContextType {
|
||||||
// PDF Documents
|
// PDF Documents
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { indexedDBService, type EPUBDocument } from '@/utils/indexedDB';
|
import { indexedDBService } from '@/utils/indexedDB';
|
||||||
|
import type { EPUBDocument } from '@/types/documents';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
|
|
||||||
export function useEPUBDocuments() {
|
export function useEPUBDocuments() {
|
||||||
|
|
@ -36,6 +37,7 @@ export function useEPUBDocuments() {
|
||||||
|
|
||||||
const newDoc: EPUBDocument = {
|
const newDoc: EPUBDocument = {
|
||||||
id,
|
id,
|
||||||
|
type: 'epub',
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
lastModified: file.lastModified,
|
lastModified: file.lastModified,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { indexedDBService, type PDFDocument } from '@/utils/indexedDB';
|
import { indexedDBService } from '@/utils/indexedDB';
|
||||||
|
import type { PDFDocument } from '@/types/documents';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
|
|
||||||
export function usePDFDocuments() {
|
export function usePDFDocuments() {
|
||||||
|
|
@ -40,6 +41,7 @@ export function usePDFDocuments() {
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const newDoc: PDFDocument = {
|
const newDoc: PDFDocument = {
|
||||||
id,
|
id,
|
||||||
|
type: 'pdf',
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
lastModified: file.lastModified,
|
lastModified: file.lastModified,
|
||||||
|
|
|
||||||
40
src/types/documents.ts
Normal file
40
src/types/documents.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
export type DocumentType = 'pdf' | 'epub';
|
||||||
|
|
||||||
|
export interface BaseDocument {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: number;
|
||||||
|
type: DocumentType;
|
||||||
|
folderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PDFDocument extends BaseDocument {
|
||||||
|
type: 'pdf';
|
||||||
|
data: Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EPUBDocument extends BaseDocument {
|
||||||
|
type: 'epub';
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentListDocument extends BaseDocument {
|
||||||
|
type: DocumentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Folder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
documents: DocumentListDocument[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortBy = 'name' | 'type' | 'date' | 'size';
|
||||||
|
export type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
|
export interface DocumentListState {
|
||||||
|
sortBy: SortBy;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
folders: Folder[];
|
||||||
|
collapsedFolders: string[];
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,11 @@
|
||||||
|
import { PDFDocument, EPUBDocument, DocumentListState } from '@/types/documents';
|
||||||
|
|
||||||
const DB_NAME = 'openreader-db';
|
const DB_NAME = 'openreader-db';
|
||||||
const DB_VERSION = 2; // Increased version number
|
const DB_VERSION = 2; // Increased version number
|
||||||
const PDF_STORE_NAME = 'pdf-documents';
|
const PDF_STORE_NAME = 'pdf-documents';
|
||||||
const EPUB_STORE_NAME = 'epub-documents';
|
const EPUB_STORE_NAME = 'epub-documents';
|
||||||
const CONFIG_STORE_NAME = 'config';
|
const CONFIG_STORE_NAME = 'config';
|
||||||
|
|
||||||
export interface PDFDocument {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
lastModified: number;
|
|
||||||
data: Blob;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EPUBDocument {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
lastModified: number;
|
|
||||||
data: ArrayBuffer; // Changed from Blob to ArrayBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -542,6 +528,7 @@ class IndexedDBService {
|
||||||
const blob = new Blob([new Uint8Array(doc.data)], { type: 'application/pdf' });
|
const blob = new Blob([new Uint8Array(doc.data)], { type: 'application/pdf' });
|
||||||
await this.addDocument({
|
await this.addDocument({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
|
type: doc.type,
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
size: doc.size,
|
size: doc.size,
|
||||||
lastModified: doc.lastModified,
|
lastModified: doc.lastModified,
|
||||||
|
|
@ -552,6 +539,7 @@ class IndexedDBService {
|
||||||
const uint8Array = new Uint8Array(doc.data);
|
const uint8Array = new Uint8Array(doc.data);
|
||||||
await this.addEPUBDocument({
|
await this.addEPUBDocument({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
|
type: doc.type,
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
size: doc.size,
|
size: doc.size,
|
||||||
lastModified: doc.lastModified,
|
lastModified: doc.lastModified,
|
||||||
|
|
@ -620,6 +608,21 @@ class IndexedDBService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveDocumentListState(state: DocumentListState): Promise<void> {
|
||||||
|
return this.setConfigItem('documentListState', JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDocumentListState(): Promise<DocumentListState | null> {
|
||||||
|
const stateStr = await this.getConfigItem('documentListState');
|
||||||
|
if (!stateStr) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(stateStr);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing document list state:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we export a singleton instance
|
// Make sure we export a singleton instance
|
||||||
|
|
@ -644,4 +647,12 @@ export async function getLastDocumentLocation(docId: string): Promise<string | n
|
||||||
export async function setLastDocumentLocation(docId: string, location: string): Promise<void> {
|
export async function setLastDocumentLocation(docId: string, location: string): Promise<void> {
|
||||||
const key = `lastLocation_${docId}`;
|
const key = `lastLocation_${docId}`;
|
||||||
return indexedDBService.setConfigItem(key, location);
|
return indexedDBService.setConfigItem(key, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDocumentListState(): Promise<DocumentListState | null> {
|
||||||
|
return indexedDBService.getDocumentListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDocumentListState(state: DocumentListState): Promise<void> {
|
||||||
|
return indexedDBService.saveDocumentListState(state);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue