diff --git a/frontend/src/pages/Scan.tsx b/frontend/src/pages/Scan.tsx
index 6b518d6..71d1856 100644
--- a/frontend/src/pages/Scan.tsx
+++ b/frontend/src/pages/Scan.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef, useCallback, useEffect } from 'react'
+import { useState, useRef, useEffect } from 'react'
import { photosApi, PhotoImportRequest } from '../api/photos'
import { jobsApi, JobResponse, JobStatus } from '../api/jobs'
@@ -23,7 +23,6 @@ export default function Scan() {
total?: number
} | null>(null)
const [error, setError] = useState(null)
- const fileInputRef = useRef(null)
const eventSourceRef = useRef(null)
// Cleanup event source on unmount
@@ -35,58 +34,74 @@ export default function Scan() {
}
}, [])
- const handleFolderBrowse = () => {
- // Note: Browser security prevents direct folder selection
- // This is a workaround - user must type/paste path
- // In production, consider using Electron or a file picker library
- const path = prompt('Enter folder path to scan:')
- if (path) {
- setFolderPath(path)
- }
- }
-
- const handleDragOver = useCallback((e: React.DragEvent) => {
- e.preventDefault()
- e.stopPropagation()
- }, [])
-
- const handleDrop = useCallback((e: React.DragEvent) => {
- e.preventDefault()
- e.stopPropagation()
-
- const files = Array.from(e.dataTransfer.files)
- const imageFiles = files.filter((file) =>
- /\.(jpg|jpeg|png|bmp|tiff|tif)$/i.test(file.name)
- )
-
- if (imageFiles.length > 0) {
- handleUploadFiles(imageFiles)
- }
- }, [])
-
- const handleFileSelect = (e: React.ChangeEvent) => {
- const files = e.target.files
- if (files && files.length > 0) {
- handleUploadFiles(Array.from(files))
- }
- }
-
- const handleUploadFiles = async (files: File[]) => {
- setIsImporting(true)
- setError(null)
- setImportResult(null)
-
+ const handleFolderBrowse = async () => {
+ // Try backend API first (uses tkinter for native folder picker with full path)
try {
- const result = await photosApi.uploadPhotos(files)
- setImportResult({
- added: result.added,
- existing: result.existing,
- total: result.added + result.existing,
- })
- setIsImporting(false)
+ const result = await photosApi.browseFolder()
+ if (result.success && result.path) {
+ setFolderPath(result.path)
+ return
+ }
} catch (err: any) {
- setError(err.response?.data?.detail || err.message || 'Upload failed')
- setIsImporting(false)
+ // Backend API failed, fall back to browser picker
+ console.warn('Backend folder picker unavailable, using browser fallback:', err)
+
+ // Check if it's a display/availability issue
+ const errorMsg = err?.response?.data?.detail || err?.message || ''
+ if (errorMsg.includes('display') || errorMsg.includes('DISPLAY')) {
+ // Show user-friendly message about display issue
+ alert('Native folder picker unavailable (no display). Using browser fallback.\n\nNote: Browser picker may only show folder name. You may need to manually complete the full path.')
+ }
+ }
+
+ // Fallback: Use browser-based folder picker
+ // Use File System Access API if available (modern browsers)
+ if (typeof window !== 'undefined' && 'showDirectoryPicker' in window) {
+ try {
+ const directoryHandle = await (window as any).showDirectoryPicker()
+ // Get the folder name from the handle
+ const folderName = directoryHandle.name
+ // Note: Browsers don't expose full absolute paths for security reasons
+ setFolderPath(folderName)
+ } catch (err: any) {
+ // User cancelled the picker
+ if (err.name !== 'AbortError') {
+ console.error('Error selecting folder:', err)
+ }
+ }
+ } else {
+ // Fallback: use a hidden directory input
+ // Note: This will show a browser confirmation dialog that cannot be removed
+ const input = document.createElement('input')
+ input.type = 'file'
+ input.setAttribute('webkitdirectory', '')
+ input.setAttribute('directory', '')
+ input.setAttribute('multiple', '')
+ input.style.display = 'none'
+
+ input.onchange = (e: any) => {
+ const files = e.target.files
+ if (files && files.length > 0) {
+ const firstFile = files[0]
+ const relativePath = firstFile.webkitRelativePath
+ const pathParts = relativePath.split('/')
+ const rootFolder = pathParts[0]
+ // Note: Browsers don't expose full absolute paths for security reasons
+ setFolderPath(rootFolder)
+ }
+ if (document.body.contains(input)) {
+ document.body.removeChild(input)
+ }
+ }
+
+ input.oncancel = () => {
+ if (document.body.contains(input)) {
+ document.body.removeChild(input)
+ }
+ }
+
+ document.body.appendChild(input)
+ input.click()
}
}
@@ -245,7 +260,11 @@ export default function Scan() {
- Enter the full path to the folder containing photos
+ Enter the full path to the folder containing photos.
+
+ Click Browse to open a native folder picker. The full path will be automatically filled.
+ If the native picker is unavailable, a browser fallback will be used (may require manual path completion).
+