diff --git a/src/components/doclist/DocumentList.tsx b/src/components/doclist/DocumentList.tsx index 344ccaa..8d071ee 100644 --- a/src/components/doclist/DocumentList.tsx +++ b/src/components/doclist/DocumentList.tsx @@ -12,7 +12,11 @@ import type { SortDirection, ViewMode, } from '@/types/documents'; -import { getDocumentListState, saveDocumentListState } from '@/lib/client/dexie'; +import { + getDocumentListState, + getDocumentRecentlyOpenedMap, + saveDocumentListState, +} from '@/lib/client/dexie'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { CreateFolderDialog } from '@/components/doclist/CreateFolderDialog'; import { DocumentListSkeleton } from '@/components/doclist/DocumentListSkeleton'; @@ -126,6 +130,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { const [sidebarOpen, setSidebarOpen] = useState(true); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [query, setQuery] = useState(''); + const [recentlyOpenedById, setRecentlyOpenedById] = useState>({}); const [isInitialized, setIsInitialized] = useState(false); @@ -214,7 +219,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { }, [isNarrow]); // Build the union document list. - const allDocuments: DocumentListDocument[] = useMemo( + const rawDocuments: DocumentListDocument[] = useMemo( () => [ ...pdfDocs.map((d) => ({ ...d, type: 'pdf' as const })), ...epubDocs.map((d) => ({ ...d, type: 'epub' as const })), @@ -222,6 +227,35 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { ], [pdfDocs, epubDocs, htmlDocs], ); + const rawDocumentIdsKey = useMemo( + () => rawDocuments.map((d) => d.id).sort().join('|'), + [rawDocuments], + ); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const recentMap = await getDocumentRecentlyOpenedMap(); + if (cancelled) return; + setRecentlyOpenedById(recentMap); + } catch (err) { + console.warn('Failed to load recently opened cache metadata:', err); + } + })(); + return () => { + cancelled = true; + }; + }, [rawDocumentIdsKey]); + + const allDocuments: DocumentListDocument[] = useMemo( + () => + rawDocuments.map((doc) => ({ + ...doc, + recentlyOpenedAt: recentlyOpenedById[doc.id] ?? 0, + })), + [rawDocuments, recentlyOpenedById], + ); // Reconcile folders against server. useEffect(() => { @@ -244,7 +278,8 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { else if (sidebarFilter === 'html') docs = docs.filter((d) => d.type === 'html'); else if (sidebarFilter === 'recents') { docs = [...docs] - .sort((a, b) => b.lastModified - a.lastModified) + .filter((d) => (d.recentlyOpenedAt ?? 0) > 0) + .sort((a, b) => (b.recentlyOpenedAt ?? 0) - (a.recentlyOpenedAt ?? 0)) .slice(0, 20); } else if (sidebarFilter.startsWith('folder:')) { const fid = sidebarFilter.slice('folder:'.length); @@ -256,14 +291,13 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { }, [allDocuments, sidebarFilter, query, folders]); // Apply sort. - const sortedVisible = useMemo( - () => sortDocs(visibleAll, sortBy, sortDirection), - [visibleAll, sortBy, sortDirection], - ); + const sortedVisible = useMemo(() => { + if (sidebarFilter === 'recents') return visibleAll; + return sortDocs(visibleAll, sortBy, sortDirection); + }, [visibleAll, sidebarFilter, sortBy, sortDirection]); // Split into folders + unfoldered for the current filter context. - const showAllScope = - sidebarFilter === 'all' || sidebarFilter === 'recents'; + const showAllScope = sidebarFilter === 'all'; const visibleFolders = useMemo(() => { if (!showAllScope) return []; // Within a kind-filter (pdf/epub/html), still show folders that contain matches. @@ -280,10 +314,11 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { }, [folders, showAllScope, query, sortBy, sortDirection]); const unfolderedDocs = useMemo(() => { + if (sidebarFilter === 'recents') return sortedVisible; const inFolder = new Set(); folders.forEach((f) => f.documents.forEach((d) => inFolder.add(d.id))); return sortedVisible.filter((d) => !inFolder.has(d.id)); - }, [folders, sortedVisible]); + }, [folders, sidebarFilter, sortedVisible]); const counts = useMemo( () => ({ diff --git a/src/components/doclist/views/IconsView.tsx b/src/components/doclist/views/IconsView.tsx index 5dfb602..0a09c37 100644 --- a/src/components/doclist/views/IconsView.tsx +++ b/src/components/doclist/views/IconsView.tsx @@ -34,17 +34,23 @@ const TILE_WIDTH_PX: Record = { xl: 192, }; -function responsiveGridTemplate(iconSize: IconSize): string { +const SMALL_GRID_ITEM_COUNT = 3; + +function responsiveGridTemplate(iconSize: IconSize, itemCount: number): string { const width = TILE_WIDTH_PX[iconSize]; + if (itemCount <= SMALL_GRID_ITEM_COUNT) { + return `repeat(auto-fill, minmax(${width}px, ${width}px))`; + } return `repeat(auto-fit, minmax(${width}px, 1fr))`; } const gridGap = '12px'; -function gridStyle(iconSize: IconSize): React.CSSProperties { +function gridStyle(iconSize: IconSize, itemCount: number): React.CSSProperties { return { - gridTemplateColumns: responsiveGridTemplate(iconSize), + gridTemplateColumns: responsiveGridTemplate(iconSize, itemCount), gap: gridGap, + justifyContent: itemCount <= SMALL_GRID_ITEM_COUNT ? 'start' : undefined, }; } @@ -136,7 +142,7 @@ function FolderGridRow({ >
{folder.documents.map((doc) => (
{folders.map((folder) => ( onFilterChange('recents')} icon={} - label="Recents" + label="Recently Opened" /> Kinds diff --git a/src/lib/client/dexie.ts b/src/lib/client/dexie.ts index 3bb4d0a..cd1baf1 100644 --- a/src/lib/client/dexie.ts +++ b/src/lib/client/dexie.ts @@ -825,6 +825,29 @@ export async function clearHtmlDocuments(): Promise { }); } +export async function getDocumentRecentlyOpenedMap(): Promise> { + return withDB(async () => { + const [pdfRows, epubRows, htmlRows] = await Promise.all([ + db[PDF_TABLE].toArray(), + db[EPUB_TABLE].toArray(), + db[HTML_TABLE].toArray(), + ]); + + const byId: Record = {}; + const write = (id: string, ts: unknown) => { + const value = toPositiveInt(ts, 0); + if (value <= 0) return; + if (!byId[id] || value > byId[id]) byId[id] = value; + }; + + pdfRows.forEach((row) => write(row.id, row.cacheAccessedAt)); + epubRows.forEach((row) => write(row.id, row.cacheAccessedAt)); + htmlRows.forEach((row) => write(row.id, row.cacheAccessedAt)); + + return byId; + }); +} + export async function getAppConfig(): Promise { return withDB(async () => { const row = await db[APP_CONFIG_TABLE].get('singleton'); diff --git a/src/types/documents.ts b/src/types/documents.ts index 5d2b61b..029a41a 100644 --- a/src/types/documents.ts +++ b/src/types/documents.ts @@ -5,6 +5,7 @@ export interface BaseDocument { name: string; size: number; lastModified: number; + recentlyOpenedAt?: number; type: DocumentType; parseStatus?: 'pending' | 'running' | 'ready' | 'failed' | null; parsedJsonKey?: string | null;