diff --git a/src/components/doclist/DocumentList.tsx b/src/components/doclist/DocumentList.tsx index 684c102..073ced9 100644 --- a/src/components/doclist/DocumentList.tsx +++ b/src/components/doclist/DocumentList.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; +import { useLiveQuery } from 'dexie-react-hooks'; import { useDocuments } from '@/contexts/DocumentContext'; import type { DocumentListDocument, @@ -134,7 +135,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { const [sidebarOpen, setSidebarOpen] = useState(!(cachedState?.sidebarCollapsed ?? false)); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [query, setQuery] = useState(''); - const [recentlyOpenedById, setRecentlyOpenedById] = useState>({}); const [isInitialized, setIsInitialized] = useState(cachedState !== null); @@ -247,22 +247,18 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { () => rawDocuments.map((d) => d.id).sort().join('|'), [rawDocuments], ); - - useEffect(() => { - let cancelled = false; - (async () => { + const recentlyOpenedById = useLiveQuery, Record>( + async () => { try { - const recentMap = await getDocumentRecentlyOpenedMap(); - if (cancelled) return; - setRecentlyOpenedById(recentMap); + return await getDocumentRecentlyOpenedMap(); } catch (err) { console.warn('Failed to load recently opened cache metadata:', err); + return {}; } - })(); - return () => { - cancelled = true; - }; - }, [rawDocumentIdsKey]); + }, + [rawDocumentIdsKey], + {}, + ); const allDocuments: DocumentListDocument[] = useMemo( () => @@ -291,6 +287,15 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { [folders, allDocumentsById], ); + const folderNameById = useMemo( + () => + foldersWithLiveDocs.reduce>((acc, folder) => { + acc[folder.id] = folder.name; + return acc; + }, {}), + [foldersWithLiveDocs], + ); + const folderIdByDocId = useMemo(() => { const map = new Map(); for (const folder of foldersWithLiveDocs) { @@ -471,6 +476,10 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { () => allDocuments.reduce((acc, d) => acc + d.size, 0), [allDocuments], ); + const visibleSelectedCount = useMemo( + () => sortedVisible.reduce((count, doc) => count + (selection.isSelected(doc) ? 1 : 0), 0), + [sortedVisible, selection], + ); const isLoading = isPDFLoading || isEPUBLoading || isHTMLLoading; @@ -499,6 +508,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { : setSidebarOpen((p) => !p) } isSidebarOpen={effectiveSidebarOpen} + showSortControls={sidebarFilter !== 'recents'} leftSlot={brand} /> } @@ -519,12 +529,15 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { onWidthChange={setSidebarWidth} topSlot={} bottomSlot={appActions} + onRowAction={() => { + if (isNarrow) setMobileSidebarOpen(false); + }} /> } statusBar={ @@ -589,6 +602,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { {fallbackViewMode === 'gallery' && ( diff --git a/src/components/doclist/views/GalleryView.tsx b/src/components/doclist/views/GalleryView.tsx index 5d34f9b..e931c67 100644 --- a/src/components/doclist/views/GalleryView.tsx +++ b/src/components/doclist/views/GalleryView.tsx @@ -12,10 +12,30 @@ import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes'; interface GalleryViewProps { documents: DocumentListDocument[]; + folderNameById?: Record; onDeleteDoc: (doc: DocumentListDocument) => void; onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void; } +function formatDateTime(value: number | undefined): string { + if (!value || !Number.isFinite(value) || value <= 0) return 'Never'; + return new Date(value).toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); +} + +function formatParseStatus(status: DocumentListDocument['parseStatus']): string { + if (!status) return 'N/A'; + if (status === 'pending') return 'Pending'; + if (status === 'running') return 'Running'; + if (status === 'ready') return 'Ready'; + return 'Failed'; +} + function KindIcon({ doc, className }: { doc: DocumentListDocument; className?: string }) { if (doc.type === 'pdf') return ; if (doc.type === 'epub') return ; @@ -107,6 +127,7 @@ function GalleryThumb({ export function GalleryView({ documents, + folderNameById, onDeleteDoc, onMergeIntoFolder, }: GalleryViewProps) { @@ -150,34 +171,60 @@ export function GalleryView({
{activeDoc ? ( -
-
- -
-
-

- {activeDoc.name} -

-

- {activeDoc.type.toUpperCase()} • {formatDocumentSize(activeDoc.size)} -

-
-
- - Open - - +
+
+
+ +
+
+

+ {activeDoc.name} +

+

+ {activeDoc.type.toUpperCase()} • {formatDocumentSize(activeDoc.size)} +

+
+
+ + Open + + +
+
+
Type
+
{activeDoc.type}
+
Size
+
{formatDocumentSize(activeDoc.size)}
+
Last opened
+
{formatDateTime(activeDoc.recentlyOpenedAt)}
+
Last modified
+
{formatDateTime(activeDoc.lastModified)}
+ {activeDoc.folderId && ( + <> +
Folder
+
+ {folderNameById?.[activeDoc.folderId] ?? activeDoc.folderId} +
+ + )} + {activeDoc.type === 'pdf' && ( + <> +
Parse status
+
{formatParseStatus(activeDoc.parseStatus)}
+ + )} +
) : (

No documents to show

diff --git a/src/components/doclist/window/FinderSidebar.tsx b/src/components/doclist/window/FinderSidebar.tsx index c8e4c82..a482a6f 100644 --- a/src/components/doclist/window/FinderSidebar.tsx +++ b/src/components/doclist/window/FinderSidebar.tsx @@ -23,6 +23,8 @@ interface FinderSidebarProps { onWidthChange: (px: number) => void; topSlot?: ReactNode; bottomSlot?: ReactNode; + /** Fired for explicit row/button actions (used to close mobile drawer). */ + onRowAction?: () => void; } const MIN_WIDTH = 168; @@ -182,6 +184,7 @@ export function FinderSidebar({ onWidthChange, topSlot, bottomSlot, + onRowAction, }: FinderSidebarProps) { const startRef = useRef<{ x: number; w: number } | null>(null); @@ -215,14 +218,20 @@ export function FinderSidebar({ Library onFilterChange('all')} + onClick={() => { + onFilterChange('all'); + onRowAction?.(); + }} icon={} label="All Documents" count={counts.all} /> onFilterChange('recents')} + onClick={() => { + onFilterChange('recents'); + onRowAction?.(); + }} icon={} label="Recently Opened" /> @@ -230,21 +239,30 @@ export function FinderSidebar({ Kinds onFilterChange('pdf')} + onClick={() => { + onFilterChange('pdf'); + onRowAction?.(); + }} icon={} label="PDF" count={counts.pdf} /> onFilterChange('epub')} + onClick={() => { + onFilterChange('epub'); + onRowAction?.(); + }} icon={} label="EPUB" count={counts.epub} /> onFilterChange('html')} + onClick={() => { + onFilterChange('html'); + onRowAction?.(); + }} icon={} label="Text" count={counts.html} @@ -277,7 +295,10 @@ export function FinderSidebar({ {({ active }) => (
-
- - - +
+ {directionLabel} + + + + {currentSort.label} + + + + {SORT_OPTIONS.map((opt) => ( + + `cursor-pointer select-none rounded-sm py-1.5 px-2.5 text-xs ${ + active ? 'bg-offbase text-accent' : 'text-foreground' + } ${selected ? 'font-semibold' : ''}` + } + > + {opt.label} + + ))} + + +
+ )}
diff --git a/src/components/doclist/window/FinderWindow.tsx b/src/components/doclist/window/FinderWindow.tsx index 86736d6..c7b8db2 100644 --- a/src/components/doclist/window/FinderWindow.tsx +++ b/src/components/doclist/window/FinderWindow.tsx @@ -87,7 +87,6 @@ export function FinderWindow({ > onSidebarOpenChange(false)} > {sidebar}