- Add radio buttons to choose between 'Scan from Local' and 'Scan from Network' #17
@ -22,6 +22,10 @@ apiClient.interceptors.request.use((config) => {
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
// Remove Content-Type header for FormData - axios will set it automatically with boundary
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type']
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
|
||||
@ -54,7 +54,8 @@ export const photosApi = {
|
||||
formData.append('files', file)
|
||||
})
|
||||
|
||||
// Don't set Content-Type header manually - let the browser set it with boundary
|
||||
// The interceptor will automatically remove Content-Type for FormData
|
||||
// Axios will set multipart/form-data with boundary automatically
|
||||
const { data } = await apiClient.post<UploadResponse>(
|
||||
'/api/v1/photos/import/upload',
|
||||
formData
|
||||
|
||||
@ -167,39 +167,95 @@ function ScanPageHelp({ onBack }: { onBack: () => void }) {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">Purpose</h3>
|
||||
<p className="text-gray-700">Import photos into your collection from folders or upload files</p>
|
||||
<p className="text-gray-700">Import photos into your collection from folders. Choose between scanning from your local computer or from network paths.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">Scan Modes</h3>
|
||||
<div className="mb-3">
|
||||
<p className="text-gray-700 font-medium mb-2">Scan from Local:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Select folders from your local computer using the browser</li>
|
||||
<li>Works with File System Access API (Chrome, Edge, Safari) or webkitdirectory (Firefox)</li>
|
||||
<li>The browser reads files and uploads them to the server</li>
|
||||
<li>No server-side filesystem access needed</li>
|
||||
<li>Perfect for scanning folders on your local machine</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<p className="text-gray-700 font-medium mb-2">Scan from Network:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Scan folders on network shares (UNC paths, mounted NFS/SMB shares)</li>
|
||||
<li>Type the network path directly or use "Browse Network" to navigate</li>
|
||||
<li>The server accesses the filesystem directly</li>
|
||||
<li>Requires the backend server to have access to the network path</li>
|
||||
<li>Perfect for scanning folders on network drives or mounted shares</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">Features</h3>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li><strong>Folder Selection:</strong> Browse and select folders containing photos</li>
|
||||
<li><strong>Scan Mode Selection:</strong> Choose between "Scan from Local" or "Scan from Network"</li>
|
||||
<li><strong>Local Folder Selection:</strong> Use browser's folder picker to select folders from your computer</li>
|
||||
<li><strong>Network Path Input:</strong> Type network paths directly or browse network shares</li>
|
||||
<li><strong>Recursive Scanning:</strong> Option to scan subdirectories recursively (enabled by default)</li>
|
||||
<li><strong>Duplicate Detection:</strong> Automatically detects and skips duplicate photos</li>
|
||||
<li><strong>Real-time Progress:</strong> Live progress tracking during import</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">How to Use</h3>
|
||||
<p className="text-gray-700 font-medium mb-2">Folder Scan:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Click "Browse Folder" button</li>
|
||||
<li>Select a folder containing photos</li>
|
||||
<li>Toggle "Subdirectories recursively" if you want to include subfolders (enabled by default)</li>
|
||||
<li>Click "Start Scan" button</li>
|
||||
<li>Monitor progress in the progress bar</li>
|
||||
<li>View results (photos added, existing photos skipped)</li>
|
||||
</ol>
|
||||
<div className="mb-3">
|
||||
<p className="text-gray-700 font-medium mb-2">Scan from Local:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Select "Scan from Local" radio button</li>
|
||||
<li>Click "Select Folder" button</li>
|
||||
<li>Choose a folder from your local computer using the folder picker</li>
|
||||
<li>The selected folder name will appear in the input field</li>
|
||||
<li>Toggle "Scan subdirectories recursively" if you want to include subfolders (enabled by default)</li>
|
||||
<li>Click "Start Scanning" button to begin the upload</li>
|
||||
<li>Monitor progress in the progress bar</li>
|
||||
<li>View results (photos added, existing photos skipped)</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<p className="text-gray-700 font-medium mb-2">Scan from Network:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Select "Scan from Network" radio button</li>
|
||||
<li>Either:
|
||||
<ul className="list-disc list-inside ml-4 mt-1">
|
||||
<li>Type the network path directly (e.g., <code className="text-xs bg-gray-100 px-1 py-0.5 rounded">\\server\share</code> or <code className="text-xs bg-gray-100 px-1 py-0.5 rounded">/mnt/nfs-share</code>)</li>
|
||||
<li>Or click "Browse Network" to navigate network shares visually</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Toggle "Scan subdirectories recursively" if you want to include subfolders (enabled by default)</li>
|
||||
<li>Click "Start Scanning" button</li>
|
||||
<li>Monitor progress in the progress bar</li>
|
||||
<li>View results (photos added, existing photos skipped)</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">What Happens</h3>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-600 ml-4">
|
||||
|
||||
<li><strong>Local Mode:</strong> Browser reads files from your computer and uploads them to the server via HTTP</li>
|
||||
<li><strong>Network Mode:</strong> Server accesses files directly from the network path</li>
|
||||
<li>Photos are added to database</li>
|
||||
<li>Duplicate photos are automatically skipped</li>
|
||||
<li>Faces are NOT detected yet (use Process page for that)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">Tips</h3>
|
||||
<ul className="list-disc list-inside space-y-1 text-gray-600 ml-4">
|
||||
<li>Use "Scan from Local" for folders on your computer - works in all modern browsers</li>
|
||||
<li>Use "Scan from Network" for folders on network drives or mounted shares</li>
|
||||
<li>Recursive scanning is enabled by default - uncheck if you only want the top-level folder</li>
|
||||
<li>Large folders may take time to scan - be patient and monitor the progress</li>
|
||||
<li>Duplicate detection prevents adding the same photo twice</li>
|
||||
<li>After scanning, use the Process page to detect faces in the imported photos</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageHelpLayout>
|
||||
)
|
||||
|
||||
@ -12,11 +12,64 @@ interface JobProgress {
|
||||
total?: number
|
||||
}
|
||||
|
||||
type ScanMode = 'network' | 'local'
|
||||
|
||||
// Supported image and video extensions for File System Access API
|
||||
const SUPPORTED_EXTENSIONS = [
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif',
|
||||
'.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.3gp', '.flv', '.wmv'
|
||||
]
|
||||
|
||||
// Check if File System Access API is supported
|
||||
const isFileSystemAccessSupported = (): boolean => {
|
||||
return 'showDirectoryPicker' in window
|
||||
}
|
||||
|
||||
// Check if webkitdirectory (fallback) is supported
|
||||
const isWebkitDirectorySupported = (): boolean => {
|
||||
const input = document.createElement('input')
|
||||
return 'webkitdirectory' in input
|
||||
}
|
||||
|
||||
// Recursively read all files from a directory handle
|
||||
async function readDirectoryRecursive(
|
||||
dirHandle: FileSystemDirectoryHandle,
|
||||
recursive: boolean = true
|
||||
): Promise<File[]> {
|
||||
const files: File[] = []
|
||||
|
||||
async function traverse(handle: FileSystemDirectoryHandle, path: string = '') {
|
||||
// @ts-ignore - File System Access API types may not be available
|
||||
for await (const entry of handle.values()) {
|
||||
if (entry.kind === 'file') {
|
||||
const file = await entry.getFile()
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase()
|
||||
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
||||
files.push(file)
|
||||
}
|
||||
} else if (entry.kind === 'directory' && recursive) {
|
||||
await traverse(entry, path + '/' + entry.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await traverse(dirHandle)
|
||||
return files
|
||||
}
|
||||
|
||||
export default function Scan() {
|
||||
const [scanMode, setScanMode] = useState<ScanMode>('local')
|
||||
const [folderPath, setFolderPath] = useState('')
|
||||
const [recursive, setRecursive] = useState(true)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [showFolderBrowser, setShowFolderBrowser] = useState(false)
|
||||
const [localUploadProgress, setLocalUploadProgress] = useState<{
|
||||
current: number
|
||||
total: number
|
||||
filename: string
|
||||
} | null>(null)
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [currentJob, setCurrentJob] = useState<JobResponse | null>(null)
|
||||
const [jobProgress, setJobProgress] = useState<JobProgress | null>(null)
|
||||
const [importResult, setImportResult] = useState<{
|
||||
@ -46,39 +99,175 @@ export default function Scan() {
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleScanFolder = async () => {
|
||||
if (!folderPath.trim()) {
|
||||
setError('Please enter a folder path')
|
||||
const handleLocalFolderSelect = (files: FileList | null) => {
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
setError(null)
|
||||
setImportResult(null)
|
||||
setCurrentJob(null)
|
||||
setJobProgress(null)
|
||||
setLocalUploadProgress(null)
|
||||
|
||||
// Filter to only supported files
|
||||
const fileArray = Array.from(files).filter((file) => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase()
|
||||
return SUPPORTED_EXTENSIONS.includes(ext)
|
||||
})
|
||||
|
||||
if (fileArray.length === 0) {
|
||||
setError('No supported image or video files found in the selected folder.')
|
||||
setSelectedFiles([])
|
||||
return
|
||||
}
|
||||
|
||||
// Set folder path from first file's path
|
||||
if (fileArray.length > 0) {
|
||||
const firstFile = fileArray[0]
|
||||
// Extract folder path from file path (webkitdirectory includes full path)
|
||||
const folderPath = firstFile.webkitRelativePath.split('/').slice(0, -1).join('/')
|
||||
setFolderPath(folderPath || 'Selected folder')
|
||||
}
|
||||
|
||||
// Store files for later upload
|
||||
setSelectedFiles(fileArray)
|
||||
}
|
||||
|
||||
const handleStartLocalScan = async () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
setError('Please select a folder first.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const request: PhotoImportRequest = {
|
||||
folder_path: folderPath.trim(),
|
||||
recursive,
|
||||
setIsImporting(true)
|
||||
setError(null)
|
||||
setImportResult(null)
|
||||
setCurrentJob(null)
|
||||
setJobProgress(null)
|
||||
setLocalUploadProgress(null)
|
||||
|
||||
// Upload files to backend in batches to show progress
|
||||
setLocalUploadProgress({ current: 0, total: selectedFiles.length, filename: '' })
|
||||
|
||||
// Upload files in batches to show progress
|
||||
const batchSize = 10
|
||||
let uploaded = 0
|
||||
let totalAdded = 0
|
||||
let totalExisting = 0
|
||||
|
||||
for (let i = 0; i < selectedFiles.length; i += batchSize) {
|
||||
const batch = selectedFiles.slice(i, i + batchSize)
|
||||
const response = await photosApi.uploadPhotos(batch)
|
||||
|
||||
uploaded += batch.length
|
||||
totalAdded += response.added || 0
|
||||
totalExisting += response.existing || 0
|
||||
|
||||
setLocalUploadProgress({
|
||||
current: uploaded,
|
||||
total: selectedFiles.length,
|
||||
filename: batch[batch.length - 1]?.name || '',
|
||||
})
|
||||
}
|
||||
|
||||
setImportResult({
|
||||
added: totalAdded,
|
||||
existing: totalExisting,
|
||||
total: selectedFiles.length,
|
||||
})
|
||||
|
||||
setIsImporting(false)
|
||||
setLocalUploadProgress(null)
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to upload files')
|
||||
setIsImporting(false)
|
||||
setLocalUploadProgress(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleScanFolder = async () => {
|
||||
if (scanMode === 'local') {
|
||||
// For local mode, use File System Access API if available, otherwise fallback to webkitdirectory
|
||||
if (isFileSystemAccessSupported()) {
|
||||
// Use File System Access API (Chrome, Edge, Safari)
|
||||
try {
|
||||
setIsImporting(true)
|
||||
setError(null)
|
||||
setImportResult(null)
|
||||
setCurrentJob(null)
|
||||
setJobProgress(null)
|
||||
setLocalUploadProgress(null)
|
||||
|
||||
// Show directory picker
|
||||
// @ts-ignore - File System Access API types may not be available
|
||||
const dirHandle = await window.showDirectoryPicker()
|
||||
const folderName = dirHandle.name
|
||||
setFolderPath(folderName)
|
||||
|
||||
// Read all files from the directory
|
||||
const files = await readDirectoryRecursive(dirHandle, recursive)
|
||||
|
||||
if (files.length === 0) {
|
||||
setError('No supported image or video files found in the selected folder.')
|
||||
setSelectedFiles([])
|
||||
return
|
||||
}
|
||||
|
||||
// Store files for later upload
|
||||
setSelectedFiles(files)
|
||||
} catch (err: any) {
|
||||
if (err.name === 'AbortError') {
|
||||
// User cancelled the folder picker
|
||||
setError(null)
|
||||
setSelectedFiles([])
|
||||
} else {
|
||||
setError(err.message || 'Failed to select folder')
|
||||
setSelectedFiles([])
|
||||
}
|
||||
}
|
||||
} else if (isWebkitDirectorySupported()) {
|
||||
// Fallback: Use webkitdirectory input (Firefox, older browsers)
|
||||
fileInputRef.current?.click()
|
||||
} else {
|
||||
setError('Folder selection is not supported in your browser. Please use Chrome, Edge, Safari, or Firefox.')
|
||||
}
|
||||
} else {
|
||||
// For network mode, use the existing path-based import
|
||||
if (!folderPath.trim()) {
|
||||
setError('Please enter a folder path')
|
||||
return
|
||||
}
|
||||
|
||||
const response = await photosApi.importPhotos(request)
|
||||
setCurrentJob({
|
||||
id: response.job_id,
|
||||
status: JobStatus.PENDING,
|
||||
progress: 0,
|
||||
message: response.message,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
setIsImporting(true)
|
||||
setError(null)
|
||||
setImportResult(null)
|
||||
setCurrentJob(null)
|
||||
setJobProgress(null)
|
||||
|
||||
// Start SSE stream for job progress
|
||||
startJobProgressStream(response.job_id)
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Import failed')
|
||||
setIsImporting(false)
|
||||
try {
|
||||
const request: PhotoImportRequest = {
|
||||
folder_path: folderPath.trim(),
|
||||
recursive,
|
||||
}
|
||||
|
||||
const response = await photosApi.importPhotos(request)
|
||||
setCurrentJob({
|
||||
id: response.job_id,
|
||||
status: JobStatus.PENDING,
|
||||
progress: 0,
|
||||
message: response.message,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// Start SSE stream for job progress
|
||||
startJobProgressStream(response.job_id)
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Import failed')
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,34 +362,123 @@ export default function Scan() {
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Scan Mode Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Scan Mode
|
||||
</label>
|
||||
<div className="flex gap-6">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="scan-mode"
|
||||
value="local"
|
||||
checked={scanMode === 'local'}
|
||||
onChange={() => {
|
||||
setScanMode('local')
|
||||
setFolderPath('')
|
||||
setSelectedFiles([])
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isImporting}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-700">Scan from Local</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="scan-mode"
|
||||
value="network"
|
||||
checked={scanMode === 'network'}
|
||||
onChange={() => {
|
||||
setScanMode('network')
|
||||
setFolderPath('')
|
||||
setSelectedFiles([])
|
||||
setError(null)
|
||||
}}
|
||||
disabled={isImporting}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-700">Scan from Network</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="folder-path"
|
||||
className="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
Folder Path
|
||||
{scanMode === 'local' ? 'Selected Folder' : 'Folder or File Path'}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
id="folder-path"
|
||||
type="text"
|
||||
value={folderPath}
|
||||
onChange={(e) => setFolderPath(e.target.value)}
|
||||
placeholder="/path/to/photos"
|
||||
className="w-1/2 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFolderBrowse}
|
||||
disabled={isImporting}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
{scanMode === 'local' ? (
|
||||
<>
|
||||
<input
|
||||
id="folder-path"
|
||||
type="text"
|
||||
value={folderPath || 'No folder selected'}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 text-gray-600"
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
{...({ webkitdirectory: '', directory: '' } as any)}
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleLocalFolderSelect(e.target.files)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleScanFolder}
|
||||
disabled={isImporting || (!isFileSystemAccessSupported() && !isWebkitDirectorySupported())}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Select folder from your local computer"
|
||||
>
|
||||
{isImporting ? 'Scanning...' : 'Select Folder'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
id="folder-path"
|
||||
type="text"
|
||||
value={folderPath}
|
||||
onChange={(e) => setFolderPath(e.target.value)}
|
||||
placeholder="Type network path: \\\\server\\share or /mnt/nfs-share"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFolderBrowse}
|
||||
disabled={isImporting}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Browse network paths (UNC paths, mounted shares)"
|
||||
>
|
||||
Browse Network
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Enter the full absolute path to the folder containing photos / videos.
|
||||
{scanMode === 'local' ? (
|
||||
<>
|
||||
Click "Select Folder" to choose a folder from your local computer. The browser will read the files and upload them to the server.
|
||||
{!isFileSystemAccessSupported() && !isWebkitDirectorySupported() && (
|
||||
<span className="text-orange-600 block mt-1">
|
||||
⚠️ Folder selection is not supported in your browser. Please use Chrome, Edge, Safari, or Firefox.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Type a network folder path directly (e.g., <code className="text-xs bg-gray-100 px-1 py-0.5 rounded">\\server\share</code> or <code className="text-xs bg-gray-100 px-1 py-0.5 rounded">/mnt/nfs-share</code>), or click "Browse Network" to navigate network shares.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -221,17 +499,61 @@ export default function Scan() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleScanFolder}
|
||||
disabled={isImporting || !folderPath.trim()}
|
||||
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isImporting ? 'Scanning...' : 'Start Scanning'}
|
||||
</button>
|
||||
{scanMode === 'local' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStartLocalScan}
|
||||
disabled={isImporting || selectedFiles.length === 0}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isImporting ? 'Scanning...' : 'Start Scanning'}
|
||||
</button>
|
||||
)}
|
||||
{scanMode === 'network' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleScanFolder}
|
||||
disabled={isImporting || !folderPath.trim()}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isImporting ? 'Scanning...' : 'Start Scanning'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Local Upload Progress Section */}
|
||||
{localUploadProgress && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Upload Progress
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-blue-600">
|
||||
Uploading files...
|
||||
</span>
|
||||
<span className="text-sm text-gray-600">
|
||||
{localUploadProgress.current} / {localUploadProgress.total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${(localUploadProgress.current / localUploadProgress.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{localUploadProgress.filename && (
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>Current file: {localUploadProgress.filename}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Section */}
|
||||
{(currentJob || jobProgress) && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user