feat: Enhance installation script and documentation for Python tkinter support

This commit updates the `install.sh` script to include the installation of Python tkinter, which is required for the native folder picker functionality. Additionally, the README.md is modified to reflect this new requirement, providing installation instructions for various operating systems. The documentation is further enhanced with troubleshooting tips for users encountering issues with the folder picker, ensuring a smoother setup experience.
This commit is contained in:
Tanya
2026-01-06 12:29:40 -05:00
parent 1f3f35d535
commit 906e2cbe19
4 changed files with 179 additions and 19 deletions
+88 -12
View File
@@ -15,6 +15,7 @@ export default function Scan() {
const [folderPath, setFolderPath] = useState('')
const [recursive, setRecursive] = useState(true)
const [isImporting, setIsImporting] = useState(false)
const [isBrowsing, setIsBrowsing] = useState(false)
const [currentJob, setCurrentJob] = useState<JobResponse | null>(null)
const [jobProgress, setJobProgress] = useState<JobProgress | null>(null)
const [importResult, setImportResult] = useState<{
@@ -35,43 +36,115 @@ export default function Scan() {
}, [])
const handleFolderBrowse = async () => {
setIsBrowsing(true)
setError(null)
// Try backend API first (uses tkinter for native folder picker with full path)
try {
console.log('Attempting to open native folder picker...')
const result = await photosApi.browseFolder()
console.log('Backend folder picker result:', result)
if (result.success && result.path) {
setFolderPath(result.path)
return
// Ensure we have a valid absolute path (not just folder name)
const path = result.path.trim()
if (path && path.length > 0) {
// Verify it looks like an absolute path:
// - Unix/Linux: starts with / (includes mounted network shares like /mnt/...)
// - Windows local: starts with drive letter like C:\
// - Windows UNC: starts with \\ (network paths like \\server\share\folder)
const isUnixPath = path.startsWith('/')
const isWindowsLocalPath = /^[A-Za-z]:[\\/]/.test(path)
const isWindowsUncPath = path.startsWith('\\\\') || path.startsWith('//')
if (isUnixPath || isWindowsLocalPath || isWindowsUncPath) {
setFolderPath(path)
setIsBrowsing(false)
return
} else {
// Backend validated it, so trust it even if it doesn't match our patterns
// (might be a valid path format we didn't account for)
console.warn('Backend returned path with unexpected format:', path)
setFolderPath(path)
setIsBrowsing(false)
return
}
}
}
// If we get here, result.success was false or path was empty
console.warn('Backend folder picker returned no path:', result)
if (result.success === false && result.message) {
setError(result.message || 'No folder was selected. Please try again.')
} else {
setError('No folder was selected. Please try again.')
}
setIsBrowsing(false)
} catch (err: any) {
// Backend API failed, fall back to browser picker
console.warn('Backend folder picker unavailable, using browser fallback:', err)
// Extract error message from various possible locations
const errorMsg = err?.response?.data?.detail ||
err?.response?.data?.message ||
err?.message ||
String(err) ||
''
console.log('Error details:', {
status: err?.response?.status,
detail: err?.response?.data?.detail,
message: err?.message,
fullError: err
})
// Check if it's a display/availability issue
const errorMsg = err?.response?.data?.detail || err?.message || ''
if (errorMsg.includes('display') || errorMsg.includes('DISPLAY')) {
if (errorMsg.includes('display') || errorMsg.includes('DISPLAY') || errorMsg.includes('tkinter')) {
// 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.')
setError('Native folder picker unavailable. Using browser fallback.')
} else if (err?.response?.status === 503) {
// 503 Service Unavailable - likely tkinter or display issue
setError('Native folder picker unavailable (tkinter/display issue). Using browser fallback.')
} else {
// Other error - log it but continue to browser fallback
console.error('Error calling backend folder picker:', err)
setError('Native folder picker unavailable. Using browser fallback.')
}
}
// Fallback: Use browser-based folder picker
// This code runs if backend API failed or returned no path
console.log('Attempting browser fallback folder picker...')
// Use File System Access API if available (modern browsers)
if (typeof window !== 'undefined' && 'showDirectoryPicker' in window) {
try {
console.log('Using File System Access API...')
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
console.log('Selected folder name:', folderName)
// Browser picker only gives folder name, not full path
// Set the folder name and show helpful message
setFolderPath(folderName)
setError('Browser picker only shows folder name. Please enter the full absolute path manually in the input field above.')
} catch (err: any) {
// User cancelled the picker
if (err.name !== 'AbortError') {
console.error('Error selecting folder:', err)
setError('Error opening folder picker: ' + err.message)
} else {
// User cancelled - clear any previous error
setError(null)
}
} finally {
setIsBrowsing(false)
}
} else {
// Fallback: use a hidden directory input
// Note: This will show a browser confirmation dialog that cannot be removed
console.log('Using file input fallback...')
const input = document.createElement('input')
input.type = 'file'
input.setAttribute('webkitdirectory', '')
@@ -87,17 +160,24 @@ export default function Scan() {
const pathParts = relativePath.split('/')
const rootFolder = pathParts[0]
// Note: Browsers don't expose full absolute paths for security reasons
console.log('Selected folder name:', rootFolder)
// Browser picker only gives folder name, not full path
// Set the folder name and show helpful message
setFolderPath(rootFolder)
setError('Browser picker only shows folder name. Please enter the full absolute path manually in the input field above.')
}
if (document.body.contains(input)) {
document.body.removeChild(input)
}
setIsBrowsing(false)
}
input.oncancel = () => {
if (document.body.contains(input)) {
document.body.removeChild(input)
}
setIsBrowsing(false)
}
document.body.appendChild(input)
@@ -252,18 +332,14 @@ export default function Scan() {
<button
type="button"
onClick={handleFolderBrowse}
disabled={isImporting}
disabled={isImporting || isBrowsing}
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
{isBrowsing ? 'Opening...' : 'Browse'}
</button>
</div>
<p className="mt-1 text-sm text-gray-500">
Enter the full path to the folder containing photos / videos.
<span className="text-xs text-gray-400 block mt-1">
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).
</span>
Enter the full absolute path to the folder containing photos / videos.
</p>
</div>