258 lines
10 KiB
Python
258 lines
10 KiB
Python
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
|
|
) |