| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createLogger } from '@automaker/utils/logger'; |
|
|
| const logger = createLogger('FilePicker'); |
|
|
| |
| |
| |
| export interface DirectoryPickerResult { |
| directoryName: string; |
| sampleFiles: string[]; |
| fileCount: number; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function openDirectoryPicker(): Promise<DirectoryPickerResult | null> { |
| |
| return new Promise<DirectoryPickerResult | null>((resolve) => { |
| let resolved = false; |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.webkitdirectory = true; |
| input.style.display = 'none'; |
|
|
| const cleanup = () => { |
| if (input.parentNode) { |
| document.body.removeChild(input); |
| } |
| }; |
|
|
| let changeEventFired = false; |
| let focusTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
| const safeResolve = (value: DirectoryPickerResult | null) => { |
| if (!resolved) { |
| resolved = true; |
| changeEventFired = true; |
| if (focusTimeout) { |
| clearTimeout(focusTimeout); |
| focusTimeout = null; |
| } |
| cleanup(); |
| resolve(value); |
| } |
| }; |
|
|
| input.addEventListener('change', () => { |
| changeEventFired = true; |
| if (focusTimeout) { |
| clearTimeout(focusTimeout); |
| focusTimeout = null; |
| } |
|
|
| logger.info('Change event fired'); |
| const files = input.files; |
| logger.info('Files selected:', files?.length || 0); |
|
|
| if (!files || files.length === 0) { |
| logger.info('No files selected'); |
| safeResolve(null); |
| return; |
| } |
|
|
| const firstFile = files[0]; |
| logger.info('First file:', { |
| name: firstFile.name, |
| webkitRelativePath: firstFile.webkitRelativePath, |
| |
| path: firstFile.path, |
| }); |
|
|
| |
| |
| let directoryName = 'Selected Directory'; |
|
|
| |
| |
| if (firstFile.path) { |
| |
| const filePath = firstFile.path as string; |
| logger.info('Found file.path:', filePath); |
| |
| const lastSeparator = Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')); |
| if (lastSeparator > 0) { |
| const absolutePath = filePath.substring(0, lastSeparator); |
| logger.info('Found absolute path:', absolutePath); |
| |
| directoryName = absolutePath; |
| } |
| } |
|
|
| |
| if (directoryName === 'Selected Directory' && firstFile.webkitRelativePath) { |
| const relativePath = firstFile.webkitRelativePath; |
| logger.info('Using webkitRelativePath:', relativePath); |
| const pathParts = relativePath.split('/'); |
| if (pathParts.length > 0) { |
| directoryName = pathParts[0]; |
| logger.info('Extracted directory name:', directoryName); |
| } |
| } |
|
|
| |
| |
| const sampleFiles: string[] = []; |
| const maxSamples = 10; |
| for (let i = 0; i < Math.min(files.length, maxSamples); i++) { |
| const file = files[i]; |
| if (file.webkitRelativePath) { |
| sampleFiles.push(file.webkitRelativePath); |
| } else if (file.name) { |
| sampleFiles.push(file.name); |
| } |
| } |
|
|
| logger.info('Directory info:', { |
| directoryName, |
| fileCount: files.length, |
| sampleFiles: sampleFiles.slice(0, 5), |
| }); |
|
|
| safeResolve({ |
| directoryName, |
| sampleFiles, |
| fileCount: files.length, |
| }); |
| }); |
|
|
| |
| |
| const handleFocus = () => { |
| |
| |
| focusTimeout = setTimeout(() => { |
| if (!resolved && !changeEventFired && (!input.files || input.files.length === 0)) { |
| logger.info('Dialog canceled (no files after focus and no change event)'); |
| safeResolve(null); |
| } |
| }, 2000); |
| }; |
|
|
| |
| document.body.appendChild(input); |
| logger.info('Opening directory picker...'); |
|
|
| |
| |
| if ( |
| 'showPicker' in input && |
| typeof (input as { showPicker?: () => void }).showPicker === 'function' |
| ) { |
| try { |
| (input as { showPicker: () => void }).showPicker(); |
| logger.info('Using showPicker()'); |
| } catch (error) { |
| logger.info('showPicker() failed, using click()', error); |
| input.click(); |
| } |
| } else { |
| logger.info('Using click()'); |
| input.click(); |
| } |
|
|
| |
| |
| window.addEventListener('focus', handleFocus, { once: true }); |
|
|
| |
| window.addEventListener( |
| 'blur', |
| () => { |
| |
| setTimeout(() => { |
| window.addEventListener('focus', handleFocus, { once: true }); |
| }, 100); |
| }, |
| { once: true } |
| ); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function openFilePicker(options?: { |
| multiple?: boolean; |
| accept?: string; |
| }): Promise<string | string[] | null> { |
| |
| return new Promise<string | string[] | null>((resolve) => { |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.multiple = options?.multiple ?? false; |
| if (options?.accept) { |
| input.accept = options.accept; |
| } |
| input.style.display = 'none'; |
|
|
| const cleanup = () => { |
| if (input.parentNode) { |
| document.body.removeChild(input); |
| } |
| }; |
|
|
| input.addEventListener('change', () => { |
| const files = input.files; |
| if (!files || files.length === 0) { |
| cleanup(); |
| resolve(null); |
| return; |
| } |
|
|
| |
| const extractPath = (file: File): string => { |
| |
| |
| if (file.path) { |
| |
| return file.path as string; |
| } |
| |
| return file.name; |
| }; |
|
|
| if (options?.multiple) { |
| const paths = Array.from(files).map(extractPath); |
| cleanup(); |
| resolve(paths); |
| } else { |
| const path = extractPath(files[0]); |
| cleanup(); |
| resolve(path); |
| } |
| }); |
|
|
| |
| const handleFocus = () => { |
| setTimeout(() => { |
| if (!input.files || input.files.length === 0) { |
| cleanup(); |
| resolve(null); |
| } |
| }, 200); |
| }; |
|
|
| |
| document.body.appendChild(input); |
|
|
| |
| |
| if ( |
| 'showPicker' in input && |
| typeof (input as { showPicker?: () => void }).showPicker === 'function' |
| ) { |
| try { |
| (input as { showPicker: () => void }).showPicker(); |
| } catch { |
| |
| input.click(); |
| } |
| } else { |
| input.click(); |
| } |
|
|
| |
| window.addEventListener('focus', handleFocus, { once: true }); |
| }); |
| } |
|
|