Files
NiceGUIEx/src/niceguiex/components/file_drop.py

191 lines
6.9 KiB
Python

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)