from nicegui import ui from typing import Optional, Callable from pathlib import Path import tempfile from PIL import Image import io class FileDrop(ui.element): def __init__(self, on_upload: Optional[Callable] = None, multiple: bool = False, accept: Optional[str] = None, max_size: Optional[int] = None, # Max size in MB return_content: bool = True, # If False, returns temp file path *args, **kwargs): super().__init__(tag='div', *args, **kwargs) self.on_upload_callback = on_upload self.multiple = multiple self.accept = accept self.max_size = max_size self.return_content = return_content # Style the container self.classes('relative w-full') with self: # Use the upload component directly and style it self.upload = ui.upload( on_upload=self._handle_upload, multiple=multiple, auto_upload=True, label='' ).classes('w-full border-2 border-dashed bg-gray-200/50').props('flat color=transparent no-thumbnails') # 'style="min-height: 120px; border: 2px dashed #d1d5db; border-radius: 0.5rem; background: transparent;" ' if accept: self.upload.props(f'accept="{accept}"') # Remove the default upload button and add custom content with self.upload.add_slot('header'): with ui.column().classes( 'w-full h-full min-h-[120px] items-center justify-center p-8' ): 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') """with self.upload.add_slot('list'): with ui.column().classes('w-full'): ui.label('Testing')""" # Add drag over styling with JavaScript ui.timer(0.1, lambda: self._add_drag_styling(), once=True) def _add_drag_styling(self): """Add visual feedback for drag over""" ui.run_javascript(f''' (() => {{ const uploadEl = getElement({self.upload.id}); if (!uploadEl || !uploadEl.$el) return; const el = uploadEl.$el; let dragCounter = 0; el.addEventListener('dragenter', (e) => {{ e.preventDefault(); dragCounter++; el.style.borderColor = '#3b82f6'; el.style.backgroundColor = '#eff6ff'; }}); el.addEventListener('dragleave', (e) => {{ e.preventDefault(); dragCounter--; if (dragCounter === 0) {{ el.style.borderColor = '#d1d5db'; el.style.backgroundColor = 'transparent'; }} }}); el.addEventListener('dragover', (e) => {{ e.preventDefault(); }}); el.addEventListener('drop', (e) => {{ dragCounter = 0; el.style.borderColor = '#d1d5db'; el.style.backgroundColor = 'transparent'; }}); }})(); ''') def _handle_upload(self, e): """Handle uploaded files""" if not self.on_upload_callback: return # Process single file upload if hasattr(e, 'content'): file_data = self._process_file(e) if file_data: # For single file mode, return the dict directly if not self.multiple: self.on_upload_callback(file_data) else: self.on_upload_callback([file_data]) # Handle multiple files elif hasattr(e, 'files'): files = [] for file_info in e.files: file_data = self._process_file(file_info) if file_data: files.append(file_data) if files: self.on_upload_callback(files) def _process_file(self, file_obj): """Process a single file object""" if not hasattr(file_obj, 'content'): return None content = file_obj.content.read() size = len(content) # Check file size if max_size is set if self.max_size and size > self.max_size * 1024 * 1024: ui.notify(f"File too large. Max size is {self.max_size}MB", type='negative') return None file_data = { 'name': file_obj.name if hasattr(file_obj, 'name') else 'unknown', 'size': size, 'type': file_obj.type if hasattr(file_obj, 'type') else 'application/octet-stream', } # Return content or temp file path based on settings if self.return_content: file_data['content'] = content else: # Save to temp file and return path suffix = Path(file_data['name']).suffix with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: tmp.write(content) file_data['path'] = Path(tmp.name) return file_data class ImageDrop(FileDrop): """Specialized file drop for images that returns PIL Image objects""" def __init__(self, on_upload: Optional[Callable] = None, multiple: bool = False, max_size: int = 50, # Default 50MB for images *args, **kwargs): # Store the user's callback self._user_callback = on_upload # Use our own handler that converts to PIL images super().__init__( on_upload=self._handle_image_upload if on_upload else None, multiple=multiple, accept='image/*', max_size=max_size, return_content=True, # We need content to create PIL images *args, **kwargs ) def _handle_image_upload(self, data): """Convert file data to PIL Images before calling user callback""" if self._user_callback: if isinstance(data, list) and len(data) == 1: img = Image.open(io.BytesIO(data[0]['content'])) self._user_callback(img) elif isinstance(data, list): # Multiple images - convert each to PIL Image images = [] for file_data in data: img = Image.open(io.BytesIO(file_data['content'])) images.append(img) self._user_callback(images) else: # Single image - convert to PIL Image img = Image.open(io.BytesIO(data['content'])) self._user_callback(img)