This commit is contained in:
2025-09-23 04:16:05 +02:00
parent 244bfa11cb
commit 01d9dc9fa2
16 changed files with 2604 additions and 1 deletions

258
example_file_drop.py Normal file
View File

@@ -0,0 +1,258 @@
from nicegui import ui
from typing import Optional, Callable, List, Dict, Any
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)
@ui.page('/')
async def main_page():
ui.label('File Drop Demo').classes('text-h4 mb-4')
uploaded_files = ui.column().classes('w-full mt-4')
def handle_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:
with ui.card().classes('p-2 mb-2'):
ui.label(f"📄 {file['name']}").classes('font-medium')
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')
FileDrop(
on_upload=handle_files,
multiple=True,
accept='.pdf,.docx,.txt,.jpg,.png'
).classes('w-full max-w-xl mx-auto')
ui.separator().classes('my-8')
ui.label('Single Image Drop Zone').classes('text-h6 mb-2')
image_preview = ui.column().classes('w-full mt-4')
def handle_image(files: List[Dict[str, Any]]):
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">')
FileDrop(
on_upload=handle_image,
multiple=False,
accept='image/*'
).classes('w-full max-w-xl mx-auto').style('min-height: 150px')
if __name__ in {"__main__", "__mp_main__"}:
ui.run(
title='File Drop Demo',
favicon='📁',
show=False,
dark=False,
port=8083
)