new components: FileDrop, ImageDrop
This commit is contained in:
@@ -1,217 +1,19 @@
|
||||
from nicegui import ui
|
||||
from typing import Optional, Callable, List, Dict, Any
|
||||
from typing import Dict, Any, List
|
||||
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)
|
||||
from PIL import Image
|
||||
import io
|
||||
from niceguiex.components import FileDrop, ImageDrop
|
||||
|
||||
|
||||
@ui.page('/')
|
||||
async def main_page():
|
||||
ui.label('File Drop Demo').classes('text-h4 mb-4')
|
||||
|
||||
# Example 1: Multiple files with content
|
||||
uploaded_files = ui.column().classes('w-full mt-4')
|
||||
|
||||
def handle_files(files: List[Dict[str, Any]]):
|
||||
def handle_multiple_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:
|
||||
@@ -220,32 +22,73 @@ async def main_page():
|
||||
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')
|
||||
|
||||
ui.label('Multiple Files (returns list)').classes('text-h6 mb-2')
|
||||
FileDrop(
|
||||
on_upload=handle_files,
|
||||
on_upload=handle_multiple_files,
|
||||
multiple=True,
|
||||
accept='.pdf,.docx,.txt,.jpg,.png'
|
||||
accept='.pdf,.docx,.txt,.jpg,.png',
|
||||
max_size=5 # 5MB limit
|
||||
).classes('w-full max-w-xl mx-auto')
|
||||
|
||||
ui.separator().classes('my-8')
|
||||
|
||||
ui.label('Single Image Drop Zone').classes('text-h6 mb-2')
|
||||
# Example 2: Single file (returns dict directly)
|
||||
single_file_info = ui.column().classes('w-full mt-4')
|
||||
|
||||
def handle_single_file(file: Dict[str, Any]):
|
||||
single_file_info.clear()
|
||||
with single_file_info:
|
||||
ui.label(f"Single file uploaded: {file['name']}").classes('font-bold')
|
||||
ui.label(f"Size: {file['size']:,} bytes")
|
||||
|
||||
ui.label('Single File (returns dict)').classes('text-h6 mb-2')
|
||||
FileDrop(
|
||||
on_upload=handle_single_file,
|
||||
multiple=False,
|
||||
accept='.pdf,.docx,.txt'
|
||||
).classes('w-full max-w-xl mx-auto')
|
||||
|
||||
ui.separator().classes('my-8')
|
||||
|
||||
# Example 3: Large files with temp path
|
||||
large_file_info = ui.column().classes('w-full mt-4')
|
||||
|
||||
def handle_large_file(file: Dict[str, Any]):
|
||||
large_file_info.clear()
|
||||
with large_file_info:
|
||||
ui.label(f"Large file saved to: {file['path']}").classes('font-bold')
|
||||
ui.label(f"Name: {file['name']}")
|
||||
ui.label(f"Size: {file['size']:,} bytes")
|
||||
|
||||
ui.label('Large Files (returns temp path)').classes('text-h6 mb-2')
|
||||
FileDrop(
|
||||
on_upload=handle_large_file,
|
||||
multiple=False,
|
||||
return_content=False, # Returns path instead of content
|
||||
accept='video/*'
|
||||
).classes('w-full max-w-xl mx-auto')
|
||||
|
||||
ui.separator().classes('my-8')
|
||||
|
||||
# Example 4: Image drop with preview
|
||||
image_preview = ui.column().classes('w-full mt-4')
|
||||
|
||||
def handle_image(files: List[Dict[str, Any]]):
|
||||
def handle_image(img: Image.Image):
|
||||
print(f"Image uploaded: {img.format}, Size: {img.size}")
|
||||
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'<img src="data:{file["type"]};base64,{img_data}" class="max-w-full rounded">')
|
||||
with image_preview:
|
||||
ui.label(f"Image: {img.format} - {img.size[0]}x{img.size[1]}px").classes('mb-2')
|
||||
# Convert PIL image back to base64 for display
|
||||
buffered = io.BytesIO()
|
||||
img.save(buffered, format=img.format or 'PNG')
|
||||
img_data = base64.b64encode(buffered.getvalue()).decode()
|
||||
ui.html(f'<img src="data:image/{img.format.lower() if img.format else "png"};base64,{img_data}" class="max-w-full rounded">')
|
||||
|
||||
FileDrop(
|
||||
ui.label('Image Drop (returns PIL Image)').classes('text-h6 mb-2')
|
||||
ImageDrop(
|
||||
on_upload=handle_image,
|
||||
multiple=False,
|
||||
accept='image/*'
|
||||
max_size=15 # 15MB limit for images
|
||||
).classes('w-full max-w-xl mx-auto').style('min-height: 150px')
|
||||
|
||||
if __name__ in {"__main__", "__mp_main__"}:
|
||||
@@ -255,4 +98,4 @@ if __name__ in {"__main__", "__mp_main__"}:
|
||||
show=False,
|
||||
dark=False,
|
||||
port=8083
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user