from nicegui import ui from typing import Optional, Callable, List, Dict, Any import base64 class FileDrop(ui.element): def __init__(self, on_upload: Optional[Callable[[List[Dict[str, Any]]], None]] = None, multiple: bool = True, accept: Optional[str] = None, *args, **kwargs): super().__init__(tag='div', *args, **kwargs) self.on_upload_callback = on_upload self.multiple = multiple self.accept = accept # Create the drop zone with inline styling and JavaScript self.classes('border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer transition-colors') self.classes('hover:border-gray-400 hover:bg-gray-50') self.style('min-height: 120px; display: flex; flex-direction: column; align-items: center; justify-content: center;') with self: ui.icon('cloud_upload', size='3em').classes('text-gray-400 mb-4') ui.label('Drop files here or click to browse').classes('text-lg text-gray-600 mb-2') ui.label('Drag and drop your files').classes('text-sm text-gray-400') # Hidden file input for click functionality self.file_input = ui.upload( on_upload=self._handle_file_input, multiple=multiple, auto_upload=True, label='' ).props('hidden').classes('hidden') if accept: self.file_input.props(f'accept="{accept}"') # Setup drag and drop with JavaScript self._setup_drag_drop() self.on('click', lambda: self.file_input.run_method('pickFiles')) # Register for the actual drop event self.on('drop', self._handle_drop_event) print(f"DEBUG: Registered drop handler for element {self.id}") def _setup_drag_drop(self): # Use timer to ensure element is rendered ui.timer(0.1, lambda: self._add_drag_handlers(), once=True) def _add_drag_handlers(self): print(f"DEBUG: About to add drag handlers for element {self.id}") ui.run_javascript(f''' console.log("JavaScript executing for element {self.id}"); (() => {{ const element = document.getElementById("c{self.id}") || document.getElementById("{self.id}"); if (!element) {{ console.error("Element not found for drag setup: c{self.id} or {self.id}"); return; }} console.log("Setting up drag handlers for:", element.id); let dragCounter = 0; element.addEventListener('dragenter', (e) => {{ e.preventDefault(); dragCounter++; element.classList.add('border-blue-500', 'bg-blue-50'); element.classList.remove('border-gray-300'); }}); element.addEventListener('dragleave', (e) => {{ e.preventDefault(); dragCounter--; if (dragCounter === 0) {{ element.classList.remove('border-blue-500', 'bg-blue-50'); element.classList.add('border-gray-300'); }} }}); element.addEventListener('dragover', (e) => {{ e.preventDefault(); }}); element.addEventListener('drop', async (e) => {{ e.preventDefault(); dragCounter = 0; element.classList.remove('border-blue-500', 'bg-blue-50'); element.classList.add('border-gray-300'); const files = Array.from(e.dataTransfer.files); if (files.length === 0) return; console.log("Files dropped:", files.length); // Process files const maxFiles = {str(self.multiple).lower()} ? files.length : 1; const filesToProcess = files.slice(0, maxFiles); const fileData = []; for (const file of filesToProcess) {{ const reader = new FileReader(); const content = await new Promise((resolve) => {{ reader.onload = () => resolve(reader.result.split(',')[1]); reader.readAsDataURL(file); }}); fileData.push({{ name: file.name, size: file.size, type: file.type || 'application/octet-stream', content: content }}); }} // Store data for Python to access window.dropData_{self.id} = fileData; console.log("Stored file data with", fileData.length, "files"); // Let the natural drop event bubble up to Python }}); console.log("Drag handlers setup complete"); }})(); ''') def _handle_drop_event(self, e): """Handle the native drop event""" print(f"DEBUG: Drop event received!") print(f"DEBUG: Event type: {type(e)}") print(f"DEBUG: Event args: {getattr(e, 'args', None)}") async def get_and_process(): try: # First check if data exists has_data = await ui.run_javascript(f'return window.dropData_{self.id} !== undefined && window.dropData_{self.id} !== null', timeout=1.0) print(f"DEBUG: Data exists: {has_data}") if has_data: # Try to get debug info about the data first data_info = await ui.run_javascript(f''' return {{ exists: window.dropData_{self.id} !== undefined, type: typeof window.dropData_{self.id}, length: window.dropData_{self.id} ? window.dropData_{self.id}.length : 0 }} ''', timeout=1.0) print(f"DEBUG: Data info: {data_info}") # Get just the metadata first (without content) metadata = await ui.run_javascript(f''' return window.dropData_{self.id}.map(file => ({{ name: file.name, size: file.size, type: file.type }})) ''', timeout=2.0) print(f"DEBUG: Retrieved metadata: {metadata}") # Get the actual data with content data = await ui.run_javascript(f'return window.dropData_{self.id}', timeout=5.0) print(f"DEBUG: Retrieved full data: {len(data) if data else 0} files") # Clear the data await ui.run_javascript(f'window.dropData_{self.id} = null; return true;', timeout=1.0) else: print("DEBUG: No data found in JavaScript") data = None if data and self.on_upload_callback: files = [] for file_data in data: content = base64.b64decode(file_data['content']) files.append({ 'name': file_data['name'], 'size': file_data['size'], 'type': file_data.get('type', 'application/octet-stream'), 'content': content }) print(f"DEBUG: Calling callback with {len(files)} dropped files") self.on_upload_callback(files) # Data already cleared in the retrieval except Exception as ex: print(f"DEBUG: Error processing dropped files: {ex}") # Give JavaScript time to process files before retrieving data ui.timer(0.1, get_and_process, once=True) def _handle_file_input(self, e): """Handle files from the file picker""" print(f"DEBUG: File input upload") if self.on_upload_callback and hasattr(e, 'content'): content = e.content.read() files = [{ 'name': e.name if hasattr(e, 'name') else 'unknown', 'size': len(content), 'type': e.type if hasattr(e, 'type') else 'application/octet-stream', 'content': content }] print(f"DEBUG: Calling callback with {len(files)} files from input") self.on_upload_callback(files) @ui.page('/') async def main_page(): ui.label('File Drop Demo').classes('text-h4 mb-4') uploaded_files = ui.column().classes('w-full mt-4') def handle_files(files: List[Dict[str, Any]]): with uploaded_files: ui.label(f'Uploaded {len(files)} file(s):').classes('font-bold mb-2') for file in files: with ui.card().classes('p-2 mb-2'): ui.label(f"📄 {file['name']}").classes('font-medium') ui.label(f"Size: {file['size']:,} bytes").classes('text-sm text-gray-600') ui.label(f"Type: {file['type']}").classes('text-sm text-gray-600') FileDrop( on_upload=handle_files, multiple=True, accept='.pdf,.docx,.txt,.jpg,.png' ).classes('w-full max-w-xl mx-auto') ui.separator().classes('my-8') ui.label('Single Image Drop Zone').classes('text-h6 mb-2') image_preview = ui.column().classes('w-full mt-4') def handle_image(files: List[Dict[str, Any]]): image_preview.clear() if files: file = files[0] with image_preview: ui.label(f"Uploaded: {file['name']}").classes('mb-2') if file['type'].startswith('image/'): img_data = base64.b64encode(file['content']).decode() ui.html(f'') FileDrop( on_upload=handle_image, multiple=False, accept='image/*' ).classes('w-full max-w-xl mx-auto').style('min-height: 150px') if __name__ in {"__main__", "__mp_main__"}: ui.run( title='File Drop Demo', favicon='📁', show=False, dark=False, port=8083 )