""" Sync Widget for GUI - Manages sync profiles and operations """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QMessageBox, QGroupBox, QHeaderView, QAbstractItemView ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont, QColor from loguru import logger from datetime import datetime from core.database import Database from core.config import SchedulerConfig class SyncWidget(QWidget): """Widget for managing sync profiles and operations""" profile_changed = pyqtSignal() def __init__(self, db: Database, config: SchedulerConfig): super().__init__() self.db = db self.config = config self._setup_ui() self.refresh() def _setup_ui(self): """Setup sync UI components""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(15) # Header header_layout = QHBoxLayout() title_label = QLabel("📁 Directory Sync Profiles") title_font = QFont() title_font.setPointSize(14) title_font.setBold(True) title_label.setFont(title_font) header_layout.addWidget(title_label) header_layout.addStretch() # Action buttons self.new_btn = QPushButton("➕ New Profile") self.new_btn.clicked.connect(self._create_profile) header_layout.addWidget(self.new_btn) self.edit_btn = QPushButton("✏ Edit") self.edit_btn.clicked.connect(self._edit_profile) self.edit_btn.setEnabled(False) header_layout.addWidget(self.edit_btn) self.schedule_btn = QPushButton("📅 Schedule") self.schedule_btn.clicked.connect(self._schedule_sync) self.schedule_btn.setEnabled(False) header_layout.addWidget(self.schedule_btn) self.run_btn = QPushButton("▶ Run Now") self.run_btn.setEnabled(False) self.run_btn.clicked.connect(self._run_sync) header_layout.addWidget(self.run_btn) self.delete_btn = QPushButton("🗑 Delete") self.delete_btn.clicked.connect(self._delete_profile) self.delete_btn.setEnabled(False) header_layout.addWidget(self.delete_btn) self.refresh_btn = QPushButton("🔄 Refresh") self.refresh_btn.clicked.connect(self.refresh) header_layout.addWidget(self.refresh_btn) layout.addLayout(header_layout) # Profiles table profiles_group = QGroupBox("Sync Profiles") profiles_layout = QVBoxLayout(profiles_group) self.profiles_table = QTableWidget() self.profiles_table.setColumnCount(5) self.profiles_table.setHorizontalHeaderLabels([ "ID", "Name", "Source", "Destination", "Enabled" ]) self.profiles_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.profiles_table.horizontalHeader().setStretchLastSection(True) self.profiles_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.profiles_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.profiles_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.profiles_table.itemSelectionChanged.connect(self._on_profile_selected) self.profiles_table.doubleClicked.connect(self._edit_profile) # Set column widths self.profiles_table.setColumnWidth(0, 50) self.profiles_table.setColumnWidth(1, 200) self.profiles_table.setColumnWidth(2, 300) self.profiles_table.setColumnWidth(3, 300) self.profiles_table.setColumnWidth(4, 80) profiles_layout.addWidget(self.profiles_table) layout.addWidget(profiles_group, stretch=2) # Recent operations operations_group = QGroupBox("Recent Sync Operations") operations_layout = QVBoxLayout(operations_group) self.operations_table = QTableWidget() self.operations_table.setColumnCount(7) self.operations_table.setHorizontalHeaderLabels([ "ID", "Profile", "Status", "Start Time", "Duration", "Copied", "Deleted" ]) self.operations_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.operations_table.horizontalHeader().setStretchLastSection(True) self.operations_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.operations_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Set column widths self.operations_table.setColumnWidth(0, 50) self.operations_table.setColumnWidth(1, 150) self.operations_table.setColumnWidth(2, 100) self.operations_table.setColumnWidth(3, 150) self.operations_table.setColumnWidth(4, 100) self.operations_table.setColumnWidth(5, 100) self.operations_table.setColumnWidth(6, 100) operations_layout.addWidget(self.operations_table) layout.addWidget(operations_group, stretch=1) def refresh(self): """Refresh all data""" self._load_profiles() self._load_operations() def _load_profiles(self): """Load sync profiles from database""" try: profiles = self.db.get_all_sync_profiles() self.profiles_table.setRowCount(len(profiles)) for row, profile in enumerate(profiles): # ID id_item = QTableWidgetItem(str(profile['id'])) id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.profiles_table.setItem(row, 0, id_item) # Name name_item = QTableWidgetItem(profile['name']) self.profiles_table.setItem(row, 1, name_item) # Source source_item = QTableWidgetItem(profile['source_path']) self.profiles_table.setItem(row, 2, source_item) # Destination dest_item = QTableWidgetItem(profile['destination_path']) self.profiles_table.setItem(row, 3, dest_item) # Enabled enabled_text = "✓ Yes" if profile['enabled'] else "✗ No" enabled_item = QTableWidgetItem(enabled_text) enabled_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) if profile['enabled']: enabled_item.setForeground(QColor('green')) else: enabled_item.setForeground(QColor('red')) self.profiles_table.setItem(row, 4, enabled_item) except Exception as e: logger.error(f"Error loading sync profiles: {e}") QMessageBox.critical(self, "Error", f"Failed to load sync profiles: {e}") def _load_operations(self): """Load recent sync operations""" try: operations = self.db.get_recent_sync_operations(limit=20) self.operations_table.setRowCount(len(operations)) for row, op in enumerate(operations): # ID id_item = QTableWidgetItem(str(op['id'])) id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.operations_table.setItem(row, 0, id_item) # Profile name profile = self.db.get_sync_profile(op['profile_id']) profile_name = profile['name'] if profile else f"Profile {op['profile_id']}" profile_item = QTableWidgetItem(profile_name) self.operations_table.setItem(row, 1, profile_item) # Status status_text = op['status'] status_item = QTableWidgetItem(status_text) status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # Color code status if op['status'] == 'completed': status_item.setForeground(QColor('green')) elif op['status'] == 'failed': status_item.setForeground(QColor('red')) elif op['status'] == 'running': status_item.setForeground(QColor('orange')) elif op['status'] == 'completed_with_errors': status_item.setForeground(QColor('darkorange')) self.operations_table.setItem(row, 2, status_item) # Start time start_time = op['start_time'] if start_time: try: dt = datetime.fromisoformat(start_time) time_str = dt.strftime('%Y-%m-%d %H:%M:%S') except: time_str = start_time[:19] if len(start_time) > 19 else start_time else: time_str = "N/A" time_item = QTableWidgetItem(time_str) self.operations_table.setItem(row, 3, time_item) # Duration if op.get('end_time') and op.get('start_time'): try: start = datetime.fromisoformat(op['start_time']) end = datetime.fromisoformat(op['end_time']) duration = (end - start).total_seconds() duration_str = f"{duration:.1f}s" except: duration_str = "N/A" else: duration_str = "Running..." if op['status'] == 'running' else "N/A" duration_item = QTableWidgetItem(duration_str) duration_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.operations_table.setItem(row, 4, duration_item) # Files copied files_copied = op.get('files_copied', 0) copied_item = QTableWidgetItem(str(files_copied)) copied_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.operations_table.setItem(row, 5, copied_item) # Files deleted files_deleted = op.get('files_deleted', 0) deleted_item = QTableWidgetItem(str(files_deleted)) deleted_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.operations_table.setItem(row, 6, deleted_item) except Exception as e: logger.error(f"Error loading sync operations: {e}") def _on_profile_selected(self): """Handle profile selection""" has_selection = len(self.profiles_table.selectedItems()) > 0 self.edit_btn.setEnabled(has_selection) self.schedule_btn.setEnabled(has_selection) self.run_btn.setEnabled(has_selection) self.delete_btn.setEnabled(has_selection) def _get_selected_profile_id(self): """Get selected profile ID""" selected = self.profiles_table.selectedItems() if not selected: return None row = selected[0].row() return int(self.profiles_table.item(row, 0).text()) def _create_profile(self): """Create new sync profile""" from gui.sync_profile_dialog import SyncProfileDialog from PyQt6.QtWidgets import QDialog dialog = SyncProfileDialog(self, self.db) if dialog.exec() == QDialog.DialogCode.Accepted: logger.info("New sync profile created") self.refresh() self.profile_changed.emit() def _edit_profile(self): """Edit selected sync profile""" from gui.sync_profile_dialog import SyncProfileDialog from PyQt6.QtWidgets import QDialog profile_id = self._get_selected_profile_id() if not profile_id: QMessageBox.warning(self, "No Selection", "Please select a profile to edit") return dialog = SyncProfileDialog(self, self.db, profile_id) if dialog.exec() == QDialog.DialogCode.Accepted: logger.info(f"Sync profile {profile_id} updated") self.refresh() self.profile_changed.emit() def _schedule_sync(self): """Open schedule dialog for selected sync profile""" from gui.schedule_editor_dialog import ScheduleEditorDialog from PyQt6.QtWidgets import QDialog profile_id = self._get_selected_profile_id() if not profile_id: QMessageBox.warning(self, "No Selection", "Please select a profile to schedule") return profile = self.db.get_sync_profile(profile_id) if not profile: QMessageBox.warning(self, "Error", f"Profile {profile_id} not found") return # Open schedule dialog with sync profile pre-selected dialog = ScheduleEditorDialog(self, self.db, self.config) # Pre-populate schedule name dialog.name_input.setText(f"Sync: {profile['name']}") # Select sync profile radio and set combo dialog.sync_radio.setChecked(True) index = dialog.sync_combo.findData(profile_id) if index >= 0: dialog.sync_combo.setCurrentIndex(index) if dialog.exec() == QDialog.DialogCode.Accepted: logger.info(f"Created schedule for sync profile {profile_id}") QMessageBox.information( self, "Success", f"Schedule created successfully for '{profile['name']}'.\n\n" "Check the Schedules tab to view and manage it." ) def _delete_profile(self): """Delete selected sync profile""" profile_id = self._get_selected_profile_id() if not profile_id: return reply = QMessageBox.question( self, "Delete Profile", f"Are you sure you want to delete this sync profile?\n\n" "This will also delete all associated sync operations.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: self.db.delete_sync_profile(profile_id) logger.info(f"Deleted sync profile {profile_id}") self.refresh() self.profile_changed.emit() except Exception as e: logger.error(f"Error deleting profile: {e}") QMessageBox.critical(self, "Error", f"Failed to delete profile: {e}") def _run_sync(self): """Execute sync for selected profile immediately""" profile_id = self._get_selected_profile_id() if not profile_id: return profile = self.db.get_sync_profile(profile_id) if not profile: return reply = QMessageBox.question( self, "Run Sync", f"Execute sync profile '{profile['name']}' now?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return try: from modules.sync.sync_module import SyncModule sync_module = SyncModule(self.db) result = sync_module.execute_sync(profile_id) if result.get('success'): stats = result.get('stats', {}) QMessageBox.information( self, "Sync Completed", f"Sync '{profile['name']}' completed successfully!\n\n" f"Files scanned: {stats.get('files_scanned', 0)}\n" f"Files copied: {stats.get('files_copied', 0)}\n" f"Files deleted: {stats.get('files_deleted', 0)}\n" f"Duration: {stats.get('duration_seconds', 0):.1f}s" ) else: QMessageBox.warning( self, "Sync Failed", f"Sync '{profile['name']}' failed:\n{result.get('error', 'Unknown error')}" ) self.refresh() except Exception as e: logger.error(f"Error executing sync: {e}") QMessageBox.critical(self, "Error", f"Failed to execute sync: {e}")