191 lines
6.9 KiB
Python
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)
|