""" 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 = `
`; 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 = `