new components: FileDrop, ImageDrop
This commit is contained in:
187
src/niceguiex/components/file_drop.py
Normal file
187
src/niceguiex/components/file_drop.py
Normal file
@@ -0,0 +1,187 @@
|
||||
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):
|
||||
# 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)
|
||||
Reference in New Issue
Block a user