""" Drag and Drop Functionality for File Operations This module provides drag-and-drop capabilities for the AniWorld web interface, including file uploads, series reordering, and batch operations. """ class DragDropManager: """Manages drag and drop operations for the web interface.""" def __init__(self): self.supported_files = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'] self.max_file_size = 50 * 1024 * 1024 * 1024 # 50GB def get_drag_drop_js(self): """Generate JavaScript code for drag and drop functionality.""" return f""" // AniWorld Drag & Drop Manager class DragDropManager {{ constructor() {{ this.supportedFiles = {self.supported_files}; this.maxFileSize = {self.max_file_size}; this.dropZones = new Map(); this.dragData = null; this.init(); }} init() {{ this.setupGlobalDragDrop(); this.setupSeriesReordering(); this.setupBatchOperations(); this.createDropZoneOverlay(); }} setupGlobalDragDrop() {{ // Prevent default drag behaviors on document document.addEventListener('dragenter', this.handleDragEnter.bind(this)); document.addEventListener('dragover', this.handleDragOver.bind(this)); document.addEventListener('dragleave', this.handleDragLeave.bind(this)); document.addEventListener('drop', this.handleDrop.bind(this)); // Setup file drop zones this.initializeDropZones(); }} initializeDropZones() {{ // Main content area drop zone const mainContent = document.querySelector('.main-content, .container-fluid'); if (mainContent) {{ this.createDropZone(mainContent, {{ types: ['files'], accept: this.supportedFiles, multiple: true, callback: this.handleFileUpload.bind(this) }}); }} // Series list drop zone for reordering const seriesList = document.querySelector('.series-list, .anime-grid'); if (seriesList) {{ this.createDropZone(seriesList, {{ types: ['series'], callback: this.handleSeriesReorder.bind(this) }}); }} // Queue drop zone const queueArea = document.querySelector('.queue-area, .download-queue'); if (queueArea) {{ this.createDropZone(queueArea, {{ types: ['series', 'episodes'], callback: this.handleQueueOperation.bind(this) }}); }} }} createDropZone(element, options) {{ const dropZone = {{ element: element, options: options, active: false }}; this.dropZones.set(element, dropZone); // Add drop zone event listeners element.addEventListener('dragenter', (e) => this.onDropZoneEnter(e, dropZone)); element.addEventListener('dragover', (e) => this.onDropZoneOver(e, dropZone)); element.addEventListener('dragleave', (e) => this.onDropZoneLeave(e, dropZone)); element.addEventListener('drop', (e) => this.onDropZoneDrop(e, dropZone)); // Add visual indicators element.classList.add('drop-zone'); return dropZone; }} setupSeriesReordering() {{ const seriesItems = document.querySelectorAll('.series-item, .anime-card'); seriesItems.forEach(item => {{ item.draggable = true; item.addEventListener('dragstart', this.handleSeriesDragStart.bind(this)); item.addEventListener('dragend', this.handleSeriesDragEnd.bind(this)); }}); }} setupBatchOperations() {{ // Enable dragging of selected series for batch operations const selectionArea = document.querySelector('.series-selection, .selection-controls'); if (selectionArea) {{ selectionArea.addEventListener('dragstart', this.handleBatchDragStart.bind(this)); }} }} handleDragEnter(e) {{ e.preventDefault(); e.stopPropagation(); if (this.hasFiles(e)) {{ this.showDropOverlay(); }} }} handleDragOver(e) {{ e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; }} handleDragLeave(e) {{ e.preventDefault(); e.stopPropagation(); // Only hide overlay if leaving the window if (e.clientX === 0 && e.clientY === 0) {{ this.hideDropOverlay(); }} }} handleDrop(e) {{ e.preventDefault(); e.stopPropagation(); this.hideDropOverlay(); if (this.hasFiles(e)) {{ this.handleFileUpload(e.dataTransfer.files); }} }} onDropZoneEnter(e, dropZone) {{ e.preventDefault(); e.stopPropagation(); if (this.canAcceptDrop(e, dropZone)) {{ dropZone.element.classList.add('drag-over'); dropZone.active = true; }} }} onDropZoneOver(e, dropZone) {{ e.preventDefault(); e.stopPropagation(); if (dropZone.active) {{ e.dataTransfer.dropEffect = 'copy'; }} }} onDropZoneLeave(e, dropZone) {{ e.preventDefault(); // Check if we're actually leaving the drop zone const rect = dropZone.element.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {{ dropZone.element.classList.remove('drag-over'); dropZone.active = false; }} }} onDropZoneDrop(e, dropZone) {{ e.preventDefault(); e.stopPropagation(); dropZone.element.classList.remove('drag-over'); dropZone.active = false; if (dropZone.options.callback) {{ if (this.hasFiles(e)) {{ dropZone.options.callback(e.dataTransfer.files, 'files'); }} else {{ dropZone.options.callback(this.dragData, 'data'); }} }} }} canAcceptDrop(e, dropZone) {{ const types = dropZone.options.types || []; if (this.hasFiles(e) && types.includes('files')) {{ return this.validateFiles(e.dataTransfer.files, dropZone.options); }} if (this.dragData && types.includes(this.dragData.type)) {{ return true; }} return false; }} hasFiles(e) {{ return e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0; }} validateFiles(files, options) {{ const accept = options.accept || []; const maxSize = options.maxSize || this.maxFileSize; const multiple = options.multiple !== false; if (!multiple && files.length > 1) {{ return false; }} for (let file of files) {{ // Check file size if (file.size > maxSize) {{ return false; }} // Check file extension if (accept.length > 0) {{ const ext = '.' + file.name.split('.').pop().toLowerCase(); if (!accept.includes(ext)) {{ return false; }} }} }} return true; }} handleSeriesDragStart(e) {{ const seriesItem = e.target.closest('.series-item, .anime-card'); if (!seriesItem) return; this.dragData = {{ type: 'series', element: seriesItem, data: {{ id: seriesItem.dataset.seriesId || seriesItem.dataset.id, name: seriesItem.dataset.seriesName || seriesItem.querySelector('.series-name, .anime-title')?.textContent, folder: seriesItem.dataset.folder }} }}; // Create drag image const dragImage = this.createDragImage(seriesItem); e.dataTransfer.setDragImage(dragImage, 0, 0); e.dataTransfer.effectAllowed = 'move'; seriesItem.classList.add('dragging'); }} handleSeriesDragEnd(e) {{ const seriesItem = e.target.closest('.series-item, .anime-card'); if (seriesItem) {{ seriesItem.classList.remove('dragging'); }} this.dragData = null; }} handleBatchDragStart(e) {{ const selectedItems = document.querySelectorAll('.series-item.selected, .anime-card.selected'); if (selectedItems.length === 0) return; this.dragData = {{ type: 'batch', count: selectedItems.length, items: Array.from(selectedItems).map(item => ({{ id: item.dataset.seriesId || item.dataset.id, name: item.dataset.seriesName || item.querySelector('.series-name, .anime-title')?.textContent, folder: item.dataset.folder }})) }}; // Create batch drag image const dragImage = this.createBatchDragImage(selectedItems.length); e.dataTransfer.setDragImage(dragImage, 0, 0); e.dataTransfer.effectAllowed = 'move'; }} handleFileUpload(files, type = 'files') {{ if (files.length === 0) return; const validFiles = []; const errors = []; // Validate each file for (let file of files) {{ const ext = '.' + file.name.split('.').pop().toLowerCase(); if (!this.supportedFiles.includes(ext)) {{ errors.push(`Unsupported file type: ${{file.name}}`); continue; }} if (file.size > this.maxFileSize) {{ errors.push(`File too large: ${{file.name}} (${{this.formatFileSize(file.size)}})`); continue; }} validFiles.push(file); }} // Show errors if any if (errors.length > 0) {{ this.showUploadErrors(errors); }} // Process valid files if (validFiles.length > 0) {{ this.showUploadProgress(validFiles); this.uploadFiles(validFiles); }} }} handleSeriesReorder(data, type) {{ if (type !== 'data' || !data || data.type !== 'series') return; // Find drop position const seriesList = document.querySelector('.series-list, .anime-grid'); const items = seriesList.querySelectorAll('.series-item, .anime-card'); // Implement reordering logic this.reorderSeries(data.data.id, items); }} handleQueueOperation(data, type) {{ if (type === 'files') {{ // Handle file drops to queue this.addFilesToQueue(data); }} else if (type === 'data') {{ // Handle series/episode drops to queue this.addToQueue(data); }} }} createDropZoneOverlay() {{ const overlay = document.createElement('div'); overlay.id = 'drop-overlay'; overlay.className = 'drop-overlay'; overlay.innerHTML = `

Drop Files Here

Supported formats: ${{this.supportedFiles.join(', ')}}

Maximum size: ${{this.formatFileSize(this.maxFileSize)}}

`; document.body.appendChild(overlay); }} showDropOverlay() {{ const overlay = document.getElementById('drop-overlay'); if (overlay) {{ overlay.style.display = 'flex'; }} }} hideDropOverlay() {{ const overlay = document.getElementById('drop-overlay'); if (overlay) {{ overlay.style.display = 'none'; }} }} createDragImage(element) {{ const clone = element.cloneNode(true); clone.style.position = 'absolute'; clone.style.top = '-1000px'; clone.style.opacity = '0.8'; clone.style.transform = 'rotate(5deg)'; document.body.appendChild(clone); setTimeout(() => document.body.removeChild(clone), 100); return clone; }} createBatchDragImage(count) {{ const dragImage = document.createElement('div'); dragImage.className = 'batch-drag-image'; dragImage.innerHTML = ` ${{count}} items `; dragImage.style.position = 'absolute'; dragImage.style.top = '-1000px'; document.body.appendChild(dragImage); setTimeout(() => document.body.removeChild(dragImage), 100); return dragImage; }} formatFileSize(bytes) {{ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; }} showUploadErrors(errors) {{ const errorModal = document.createElement('div'); errorModal.className = 'modal fade'; errorModal.innerHTML = ` `; document.body.appendChild(errorModal); const modal = new bootstrap.Modal(errorModal); modal.show(); errorModal.addEventListener('hidden.bs.modal', () => {{ document.body.removeChild(errorModal); }}); }} showUploadProgress(files) {{ // Create upload progress modal const progressModal = document.createElement('div'); progressModal.className = 'modal fade'; progressModal.id = 'upload-progress-modal'; progressModal.setAttribute('data-bs-backdrop', 'static'); progressModal.innerHTML = ` `; document.body.appendChild(progressModal); const modal = new bootstrap.Modal(progressModal); modal.show(); return modal; }} uploadFiles(files) {{ // This would implement the actual file upload logic // For now, just simulate upload progress const progressModal = this.showUploadProgress(files); files.forEach((file, index) => {{ this.simulateFileUpload(file, index, files.length); }}); }} simulateFileUpload(file, index, total) {{ const progressList = document.getElementById('upload-progress-list'); const fileProgress = document.createElement('div'); fileProgress.className = 'mb-2'; fileProgress.innerHTML = `
${{file.name}} ${{this.formatFileSize(file.size)}}
`; progressList.appendChild(fileProgress); // Simulate progress const progressBar = fileProgress.querySelector('.progress-bar'); let progress = 0; const interval = setInterval(() => {{ progress += Math.random() * 15; if (progress > 100) progress = 100; progressBar.style.width = progress + '%'; if (progress >= 100) {{ clearInterval(interval); progressBar.classList.add('bg-success'); // Update overall progress this.updateOverallProgress(index + 1, total); }} }}, 200); }} updateOverallProgress(completed, total) {{ const overallProgress = document.getElementById('overall-progress'); const percentage = (completed / total) * 100; overallProgress.style.width = percentage + '%'; if (completed === total) {{ setTimeout(() => {{ const modal = bootstrap.Modal.getInstance(document.getElementById('upload-progress-modal')); modal.hide(); }}, 1000); }} }} reorderSeries(seriesId, items) {{ // Implement series reordering logic console.log('Reordering series:', seriesId); // This would send an API request to update the order fetch('/api/series/reorder', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ seriesId: seriesId, newPosition: Array.from(items).findIndex(item => item.classList.contains('drag-over')) }}) }}) .then(response => response.json()) .then(data => {{ if (data.success) {{ this.showToast('Series reordered successfully', 'success'); }} else {{ this.showToast('Failed to reorder series', 'error'); }} }}) .catch(error => {{ console.error('Reorder error:', error); this.showToast('Error reordering series', 'error'); }}); }} addToQueue(data) {{ // Add series or episodes to download queue let items = []; if (data.type === 'series') {{ items = [data.data]; }} else if (data.type === 'batch') {{ items = data.items; }} fetch('/api/queue/add', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ items: items }}) }}) .then(response => response.json()) .then(result => {{ if (result.success) {{ this.showToast(`Added ${{items.length}} item(s) to queue`, 'success'); }} else {{ this.showToast('Failed to add to queue', 'error'); }} }}) .catch(error => {{ console.error('Queue add error:', error); this.showToast('Error adding to queue', 'error'); }}); }} showToast(message, type = 'info') {{ // Create and show a toast notification const toast = document.createElement('div'); toast.className = `toast align-items-center text-white bg-${{type === 'error' ? 'danger' : type}}`; toast.innerHTML = `
${{message}}
`; let toastContainer = document.querySelector('.toast-container'); if (!toastContainer) {{ toastContainer = document.createElement('div'); toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3'; document.body.appendChild(toastContainer); }} toastContainer.appendChild(toast); const bsToast = new bootstrap.Toast(toast); bsToast.show(); toast.addEventListener('hidden.bs.toast', () => {{ toastContainer.removeChild(toast); }}); }} }} // Initialize drag and drop when DOM is loaded document.addEventListener('DOMContentLoaded', () => {{ window.dragDropManager = new DragDropManager(); }}); """ def get_css(self): """Generate CSS styles for drag and drop functionality.""" return """ /* Drag and Drop Styles */ .drop-zone { transition: all 0.3s ease; position: relative; } .drop-zone.drag-over { background-color: rgba(13, 110, 253, 0.1); border: 2px dashed #0d6efd; border-radius: 8px; } .drop-zone.drag-over::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(13, 110, 253, 0.05); border-radius: 6px; z-index: 1; } .drop-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: none; justify-content: center; align-items: center; z-index: 9999; } .drop-message { text-align: center; color: white; padding: 2rem; border: 3px dashed #0d6efd; border-radius: 15px; background: rgba(13, 110, 253, 0.1); backdrop-filter: blur(10px); } .drop-message i { font-size: 4rem; margin-bottom: 1rem; color: #0d6efd; } .drop-message h3 { margin-bottom: 0.5rem; } .drop-message p { margin-bottom: 0.25rem; opacity: 0.8; } .series-item.dragging, .anime-card.dragging { opacity: 0.5; transform: rotate(2deg); z-index: 1000; } .batch-drag-image { background: #0d6efd; color: white; padding: 0.5rem 1rem; border-radius: 20px; display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; box-shadow: 0 4px 8px rgba(0,0,0,0.2); } .progress-sm { height: 0.5rem; } .toast-container { z-index: 9999; } /* Drag handle for reorderable items */ .drag-handle { cursor: grab; color: #6c757d; padding: 0.25rem; } .drag-handle:hover { color: #0d6efd; } .drag-handle:active { cursor: grabbing; } /* Drop indicators */ .drop-indicator { height: 3px; background: #0d6efd; margin: 0.25rem 0; opacity: 0; transition: opacity 0.2s; } .drop-indicator.active { opacity: 1; } /* Accessibility */ @media (prefers-reduced-motion: reduce) { .drop-zone, .series-item.dragging, .anime-card.dragging { transition: none; } } """ # Export the drag drop manager drag_drop_manager = DragDropManager()