""" Undo/Redo Functionality Manager This module provides undo/redo capabilities for operations in the AniWorld web interface, including operation history, rollback functionality, and state management. """ import json import time from typing import Dict, List, Any, Optional, Callable from datetime import datetime, timedelta from flask import Blueprint, request, jsonify, session from dataclasses import dataclass, asdict from enum import Enum import threading import copy class OperationType(Enum): """Types of operations that can be undone/redone.""" DOWNLOAD = "download" DELETE = "delete" UPDATE = "update" ORGANIZE = "organize" RENAME = "rename" MOVE = "move" SETTINGS_CHANGE = "settings_change" BULK_OPERATION = "bulk_operation" SEARCH_FILTER = "search_filter" USER_PREFERENCE = "user_preference" @dataclass class UndoableOperation: """Represents an operation that can be undone/redone.""" id: str type: OperationType description: str timestamp: float user_session: str forward_action: Dict[str, Any] # Data needed to perform the operation backward_action: Dict[str, Any] # Data needed to undo the operation affected_items: List[str] # IDs of affected series/episodes success: bool = False error: Optional[str] = None metadata: Optional[Dict[str, Any]] = None class UndoRedoManager: """Manages undo/redo operations for the application.""" def __init__(self, app=None): self.app = app self.operation_history: Dict[str, List[UndoableOperation]] = {} # per session self.redo_stack: Dict[str, List[UndoableOperation]] = {} # per session self.max_history_size = 50 self.operation_timeout = 3600 # 1 hour self.operation_handlers = {} self.lock = threading.Lock() # Register default operation handlers self._register_default_handlers() def init_app(self, app): """Initialize with Flask app.""" self.app = app def _register_default_handlers(self): """Register default operation handlers.""" self.operation_handlers = { OperationType.DOWNLOAD: { 'undo': self._undo_download, 'redo': self._redo_download }, OperationType.DELETE: { 'undo': self._undo_delete, 'redo': self._redo_delete }, OperationType.UPDATE: { 'undo': self._undo_update, 'redo': self._redo_update }, OperationType.ORGANIZE: { 'undo': self._undo_organize, 'redo': self._redo_organize }, OperationType.RENAME: { 'undo': self._undo_rename, 'redo': self._redo_rename }, OperationType.MOVE: { 'undo': self._undo_move, 'redo': self._redo_move }, OperationType.SETTINGS_CHANGE: { 'undo': self._undo_settings_change, 'redo': self._redo_settings_change }, OperationType.BULK_OPERATION: { 'undo': self._undo_bulk_operation, 'redo': self._redo_bulk_operation }, OperationType.USER_PREFERENCE: { 'undo': self._undo_user_preference, 'redo': self._redo_user_preference } } def get_session_id(self) -> str: """Get current session ID.""" return session.get('session_id', 'default') def record_operation(self, operation: UndoableOperation) -> str: """Record an operation for potential undo.""" with self.lock: session_id = operation.user_session # Initialize session history if needed if session_id not in self.operation_history: self.operation_history[session_id] = [] self.redo_stack[session_id] = [] # Add to history self.operation_history[session_id].append(operation) # Clear redo stack when new operation is performed self.redo_stack[session_id].clear() # Limit history size if len(self.operation_history[session_id]) > self.max_history_size: self.operation_history[session_id].pop(0) # Clean up old operations self._cleanup_old_operations(session_id) return operation.id def can_undo(self, session_id: Optional[str] = None) -> bool: """Check if undo is possible.""" session_id = session_id or self.get_session_id() return (session_id in self.operation_history and len(self.operation_history[session_id]) > 0) def can_redo(self, session_id: Optional[str] = None) -> bool: """Check if redo is possible.""" session_id = session_id or self.get_session_id() return (session_id in self.redo_stack and len(self.redo_stack[session_id]) > 0) def get_last_operation(self, session_id: Optional[str] = None) -> Optional[UndoableOperation]: """Get the last operation that can be undone.""" session_id = session_id or self.get_session_id() if self.can_undo(session_id): return self.operation_history[session_id][-1] return None def get_next_redo_operation(self, session_id: Optional[str] = None) -> Optional[UndoableOperation]: """Get the next operation that can be redone.""" session_id = session_id or self.get_session_id() if self.can_redo(session_id): return self.redo_stack[session_id][-1] return None async def undo_last_operation(self, session_id: Optional[str] = None) -> Dict[str, Any]: """Undo the last operation.""" session_id = session_id or self.get_session_id() if not self.can_undo(session_id): return {'success': False, 'error': 'No operation to undo'} with self.lock: operation = self.operation_history[session_id].pop() try: # Execute undo operation handler = self.operation_handlers.get(operation.type, {}).get('undo') if not handler: return {'success': False, 'error': f'No undo handler for {operation.type}'} result = await handler(operation) if result.get('success', False): # Move to redo stack with self.lock: self.redo_stack[session_id].append(operation) return { 'success': True, 'operation': asdict(operation), 'message': f'Undid: {operation.description}' } else: # Put operation back if undo failed with self.lock: self.operation_history[session_id].append(operation) return {'success': False, 'error': result.get('error', 'Undo failed')} except Exception as e: # Put operation back on error with self.lock: self.operation_history[session_id].append(operation) return {'success': False, 'error': str(e)} async def redo_last_operation(self, session_id: Optional[str] = None) -> Dict[str, Any]: """Redo the last undone operation.""" session_id = session_id or self.get_session_id() if not self.can_redo(session_id): return {'success': False, 'error': 'No operation to redo'} with self.lock: operation = self.redo_stack[session_id].pop() try: # Execute redo operation handler = self.operation_handlers.get(operation.type, {}).get('redo') if not handler: return {'success': False, 'error': f'No redo handler for {operation.type}'} result = await handler(operation) if result.get('success', False): # Move back to history with self.lock: self.operation_history[session_id].append(operation) return { 'success': True, 'operation': asdict(operation), 'message': f'Redid: {operation.description}' } else: # Put operation back if redo failed with self.lock: self.redo_stack[session_id].append(operation) return {'success': False, 'error': result.get('error', 'Redo failed')} except Exception as e: # Put operation back on error with self.lock: self.redo_stack[session_id].append(operation) return {'success': False, 'error': str(e)} def get_operation_history(self, session_id: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]: """Get operation history for a session.""" session_id = session_id or self.get_session_id() if session_id not in self.operation_history: return [] history = self.operation_history[session_id][-limit:] return [asdict(op) for op in reversed(history)] def get_redo_history(self, session_id: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]: """Get redo stack for a session.""" session_id = session_id or self.get_session_id() if session_id not in self.redo_stack: return [] redo_stack = self.redo_stack[session_id][-limit:] return [asdict(op) for op in reversed(redo_stack)] def clear_history(self, session_id: Optional[str] = None): """Clear operation history for a session.""" session_id = session_id or self.get_session_id() with self.lock: if session_id in self.operation_history: self.operation_history[session_id].clear() if session_id in self.redo_stack: self.redo_stack[session_id].clear() def _cleanup_old_operations(self, session_id: str): """Clean up operations older than timeout.""" current_time = time.time() cutoff_time = current_time - self.operation_timeout # Clean history self.operation_history[session_id] = [ op for op in self.operation_history[session_id] if op.timestamp > cutoff_time ] # Clean redo stack self.redo_stack[session_id] = [ op for op in self.redo_stack[session_id] if op.timestamp > cutoff_time ] # Operation handlers async def _undo_download(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a download operation.""" try: # This would implement actual download cancellation/cleanup # For now, simulate the operation await self._simulate_operation_delay() return { 'success': True, 'message': f'Cancelled download for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_download(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a download operation.""" try: # This would implement actual download restart await self._simulate_operation_delay() return { 'success': True, 'message': f'Restarted download for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_delete(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a delete operation.""" try: # This would implement actual file/series restoration await self._simulate_operation_delay() return { 'success': True, 'message': f'Restored {len(operation.affected_items)} deleted items' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_delete(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a delete operation.""" try: # This would implement actual deletion again await self._simulate_operation_delay() return { 'success': True, 'message': f'Re-deleted {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_update(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo an update operation.""" try: # Restore previous metadata/information await self._simulate_operation_delay() return { 'success': True, 'message': f'Reverted updates for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_update(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo an update operation.""" try: # Reapply updates await self._simulate_operation_delay() return { 'success': True, 'message': f'Reapplied updates for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_organize(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo an organize operation.""" try: # Restore original organization original_paths = operation.backward_action.get('original_paths', {}) # This would implement actual file/folder restoration await self._simulate_operation_delay() return { 'success': True, 'message': f'Restored original organization for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_organize(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo an organize operation.""" try: # Reapply organization await self._simulate_operation_delay() return { 'success': True, 'message': f'Reapplied organization for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_rename(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a rename operation.""" try: # Restore original names original_names = operation.backward_action.get('original_names', {}) await self._simulate_operation_delay() return { 'success': True, 'message': f'Restored original names for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_rename(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a rename operation.""" try: # Reapply renames await self._simulate_operation_delay() return { 'success': True, 'message': f'Reapplied renames for {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_move(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a move operation.""" try: # Restore original locations original_locations = operation.backward_action.get('original_locations', {}) await self._simulate_operation_delay() return { 'success': True, 'message': f'Moved {len(operation.affected_items)} items back to original locations' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_move(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a move operation.""" try: # Reapply moves await self._simulate_operation_delay() return { 'success': True, 'message': f'Re-moved {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_settings_change(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a settings change.""" try: # Restore previous settings previous_settings = operation.backward_action.get('previous_settings', {}) # This would implement actual settings restoration await self._simulate_operation_delay() return { 'success': True, 'message': 'Restored previous settings' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_settings_change(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a settings change.""" try: # Reapply settings await self._simulate_operation_delay() return { 'success': True, 'message': 'Reapplied settings changes' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_bulk_operation(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a bulk operation.""" try: # Undo each sub-operation sub_operations = operation.backward_action.get('sub_operations', []) await self._simulate_operation_delay() return { 'success': True, 'message': f'Undid bulk operation affecting {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_bulk_operation(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a bulk operation.""" try: # Redo each sub-operation await self._simulate_operation_delay() return { 'success': True, 'message': f'Redid bulk operation affecting {len(operation.affected_items)} items' } except Exception as e: return {'success': False, 'error': str(e)} async def _undo_user_preference(self, operation: UndoableOperation) -> Dict[str, Any]: """Undo a user preference change.""" try: # Restore previous preference value previous_value = operation.backward_action.get('previous_value') preference_key = operation.backward_action.get('preference_key') # This would implement actual preference restoration await self._simulate_operation_delay() return { 'success': True, 'message': f'Restored previous value for {preference_key}' } except Exception as e: return {'success': False, 'error': str(e)} async def _redo_user_preference(self, operation: UndoableOperation) -> Dict[str, Any]: """Redo a user preference change.""" try: # Reapply preference change await self._simulate_operation_delay() return { 'success': True, 'message': 'Reapplied preference change' } except Exception as e: return {'success': False, 'error': str(e)} async def _simulate_operation_delay(self): """Simulate operation processing delay.""" import asyncio await asyncio.sleep(0.1) # Small delay to simulate work def get_undo_redo_js(self): """Generate JavaScript code for undo/redo functionality.""" return """ // AniWorld Undo/Redo Manager class UndoRedoManager { constructor() { this.isUndoing = false; this.isRedoing = false; this.historyVisible = false; this.init(); } init() { this.createUndoRedoInterface(); this.setupKeyboardShortcuts(); this.setupEventListeners(); this.updateButtonStates(); // Update states periodically setInterval(() => { this.updateButtonStates(); }, 1000); } createUndoRedoInterface() { this.createUndoRedoButtons(); this.createHistoryPanel(); } createUndoRedoButtons() { // Check if buttons already exist if (document.querySelector('.undo-redo-controls')) return; const controlsContainer = document.createElement('div'); controlsContainer.className = 'undo-redo-controls position-fixed'; controlsContainer.style.cssText = ` bottom: 20px; left: 20px; z-index: 1050; display: flex; gap: 0.5rem; `; controlsContainer.innerHTML = `
No operations in history
'; return; } let html = ''; // Redo operations (future) if (data.redo_history.length > 0) { html += 'Error loading history
'; } } createHistoryItem(operation, type) { const timestamp = new Date(operation.timestamp * 1000); const timeStr = this.formatTimestamp(timestamp); let iconClass = 'fa-cog'; let actionClass = ''; let actionAttr = ''; switch (operation.type) { case 'download': iconClass = 'fa-download'; break; case 'delete': iconClass = 'fa-trash'; break; case 'update': iconClass = 'fa-sync'; break; case 'organize': iconClass = 'fa-folder-open'; break; case 'rename': iconClass = 'fa-edit'; break; case 'move': iconClass = 'fa-arrows-alt'; break; case 'settings_change': iconClass = 'fa-cogs'; break; case 'bulk_operation': iconClass = 'fa-list'; break; } if (type === 'redo') { actionClass = 'text-success cursor-pointer'; actionAttr = `data-action="redo" data-operation-id="${operation.id}"`; } else if (type === 'last') { actionClass = 'text-primary cursor-pointer border-start border-primary border-3'; actionAttr = `data-action="undo" data-operation-id="${operation.id}"`; } else { actionClass = 'text-muted'; } return `