From 4b9ee40c8d8c3a45ef7ac5a8de908c4aa73c8b68 Mon Sep 17 00:00:00 2001 From: Reece Date: Thu, 2 Apr 2026 18:25:15 +0100 Subject: [PATCH] local folder as source --- .../SmartFolderManagementModal.tsx | 130 ++++++++++++++++-- .../smartFolders/SmartFolderWorkbenchView.tsx | 20 +++ .../src/core/hooks/useLocalFolderPoller.ts | 115 ++++++++++++++++ .../services/folderDirectoryHandleStorage.ts | 55 +++++++- .../core/services/folderSeenFilesStorage.ts | 59 ++++++++ frontend/src/core/utils/fsAccessCapability.ts | 30 ++++ 6 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 frontend/src/core/hooks/useLocalFolderPoller.ts create mode 100644 frontend/src/core/services/folderSeenFilesStorage.ts create mode 100644 frontend/src/core/utils/fsAccessCapability.ts diff --git a/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx b/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx index ff1dc2b4dd..e0ee582dba 100644 --- a/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx +++ b/frontend/src/core/components/smartFolders/SmartFolderManagementModal.tsx @@ -13,6 +13,7 @@ import { Select, Box, Collapse, + Tooltip, } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { SmartFolder } from '@app/types/smartFolders'; @@ -28,6 +29,12 @@ import { deleteServerFolder, } from '@app/services/serverFolderApiService'; import { buildPipelineJson } from '@app/utils/automationExecutor'; +import { + canReadLocalFolder, + canWriteLocalFolder, + FS_READ_UNSUPPORTED_MSG, + FS_WRITE_UNSUPPORTED_MSG, +} from '@app/utils/fsAccessCapability'; import FolderSpecialIcon from '@mui/icons-material/FolderSpecial'; const ACCENT_SWATCHES = [ @@ -111,6 +118,8 @@ export function SmartFolderManagementModal({ const [saving, setSaving] = useState(false); const [outputDirName, setOutputDirName] = useState(editFolder?.hasOutputDirectory ? '(loading…)' : null); const pendingDirHandle = useRef(null); + const [inputDirName, setInputDirName] = useState(editFolder?.inputSource === 'local-folder' ? '(loading…)' : null); + const pendingInputDirHandle = useRef(null); const [nameError, setNameError] = useState(''); const [automationError, setAutomationError] = useState(''); const [saveError, setSaveError] = useState(null); @@ -128,6 +137,7 @@ export function SmartFolderManagementModal({ setOutputName(editFolder?.outputName ?? editFolder?.name ?? ''); setOutputNamePosition((editFolder?.outputNamePosition as 'prefix' | 'suffix' | 'auto-number') ?? 'prefix'); setInputSource(editFolder?.inputSource ?? 'idb'); + setInputDirName(editFolder?.inputSource === 'local-folder' ? '(loading…)' : null); setOutputTtlHours(editFolder?.outputTtlHours != null ? String(editFolder.outputTtlHours) : 'forever'); setDeleteOutputOnDownload(editFolder?.deleteOutputOnDownload ?? false); outputNameDirty.current = !!editFolder?.outputName; @@ -142,11 +152,17 @@ export function SmartFolderManagementModal({ if (opened) { resetState(); pendingDirHandle.current = null; + pendingInputDirHandle.current = null; if (editFolder?.hasOutputDirectory && editFolder.id) { folderDirectoryHandleStorage.get(editFolder.id).then(h => setOutputDirName(h?.name ?? null)); } else { setOutputDirName(null); } + if (editFolder?.inputSource === 'local-folder' && editFolder.id) { + folderDirectoryHandleStorage.getInput(editFolder.id).then(h => setInputDirName(h?.name ?? null)); + } else { + setInputDirName(null); + } } }, [opened, resetState, editFolder]); @@ -189,12 +205,20 @@ export function SmartFolderManagementModal({ if (isEditMode && editFolder) { const wasServerFolder = editFolder.inputSource === 'server-folder'; + const wasLocalFolder = editFolder.inputSource === 'local-folder'; await smartFolderStorage.updateFolder({ ...editFolder, ...folderData }); if (pendingDirHandle.current) { await folderDirectoryHandleStorage.set(editFolder.id, pendingDirHandle.current); } else if (!hasOutputDirectory) { await folderDirectoryHandleStorage.remove(editFolder.id); } + if (inputSource === 'local-folder') { + if (pendingInputDirHandle.current) { + await folderDirectoryHandleStorage.setInput(editFolder.id, pendingInputDirHandle.current); + } + } else if (wasLocalFolder) { + await folderDirectoryHandleStorage.removeInput(editFolder.id); + } if (isServerFolder && configJson) { if (wasServerFolder) { await updateServerFolder(editFolder.id, trimmedName, configJson, ttlHoursNum, deleteOutputOnDownload); @@ -209,6 +233,9 @@ export function SmartFolderManagementModal({ if (pendingDirHandle.current) { await folderDirectoryHandleStorage.set(newFolder.id, pendingDirHandle.current); } + if (inputSource === 'local-folder' && pendingInputDirHandle.current) { + await folderDirectoryHandleStorage.setInput(newFolder.id, pendingInputDirHandle.current); + } if (isServerFolder && configJson) { await createServerFolder(newFolder.id, trimmedName, configJson, ttlHoursNum, deleteOutputOnDownload); } @@ -356,12 +383,79 @@ export function SmartFolderManagementModal({ onChange={(v) => v && setInputSource(v as NonNullable)} data={[ { value: 'idb', label: 'Browser — drop files here' }, + { + value: 'local-folder', + label: canReadLocalFolder + ? 'Local folder (auto-scan)' + : 'Local folder (auto-scan) — Chrome/Edge only', + disabled: !canReadLocalFolder, + }, { value: 'server-folder', label: 'Server watch folder' }, ]} size="sm" comboboxProps={{ withinPortal: true, zIndex: 400 }} /> + {/* Local-folder input directory picker */} + {inputSource === 'local-folder' && ( + + + + + + Input folder + + {inputDirName ?? 'No folder chosen — required for auto-scan'} + + + + {inputDirName && ( + + )} + + + + New PDF files in this folder are processed automatically every 10 seconds. + + + )} + {/* Server-specific options, visually indented under the select */} {inputSource === 'server-folder' && ( Local output folder - {outputDirName ?? 'Not set — outputs stay in app'} + {!canWriteLocalFolder + ? 'Not supported in this browser' + : (outputDirName ?? 'Not set — outputs stay in app')} - + + {outputDirName && (