import sys import uuid import os import signal import logging mpl_logger = logging.getLogger('matplotlib') mpl_logger.setLevel(logging.WARNING) import re import psutil import shutil from pathlib import Path import subprocess import importlib import time import json import win32process import csv import ctypes import urllib.request, requests from datetime import datetime, timedelta logger = logging.getLogger(__name__) import traceback import glob sys.path.append(r"C:\AUTOLOOK\modules") from modules.module_outlook_2021 import OutlookWidget, OutlookHandler, ImapHandler from modules.ha2 import BrowserWindow from module_thunderbird import ThunderbirdHandler from modules.debug_trace import start_trace, stop_trace import imaplib import platform import ctypes import cryptography # Imports Windows spécifiques if sys.platform == 'win32': import win32gui import win32con import win32process import win32api import win32security import ntsecuritycon as security import win32com.client import pythoncom import winreg import threading import stat from PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QPrintPreviewDialog from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudio from PyQt5.QtCore import (Qt, QCoreApplication, QPropertyAnimation, QEasingCurve, QTimer, QDateTime, QDate, QRect, QSettings, QSizeF, QMarginsF, QUrl, pyqtSignal, QThread, QTime, QCalendar, pyqtSlot, QObject) from PyQt5.QtChart import QChart, QChartView, QBarSeries, QBarSet, QCategoryAxis from PyQt5.QtGui import QFont, QTextDocument, QFont, QTextCharFormat, QTextCursor, QColor, QPixmap, QMovie, QPainter, \ QPen, QCursor, QBrush, QIcon from PyQt5.QtWidgets import (QScrollBar, QButtonGroup, QRadioButton, QProgressBar, QTreeWidget, QTreeWidgetItem, QScrollArea, QListWidget, QPushButton, QHBoxLayout, QDesktopWidget, QDialog, QMessageBox, QGraphicsBlurEffect, QToolBar, QHeaderView, QSlider, QFrame, QComboBox, QMenu, QAction, QFileDialog, QGridLayout, QCheckBox, QGraphicsOpacityEffect, QFormLayout, QTextEdit, QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QListWidgetItem, QTableWidget, QTableWidgetItem, QTabWidget, QProgressDialog, QDialogButtonBox, QGroupBox, QFrame, QInputDialog, QCalendarWidget, QTimeEdit, QSpinBox, QLineEdit, QStackedWidget, QSizePolicy, QAbstractItemView) import pandas as pd from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import numpy as np import zipfile from datetime import timedelta start_trace("main.py", print_vars=False) DEBUG_DIR = r"C:\AUTOLOOK\debug" os.makedirs(DEBUG_DIR, exist_ok=True) debug_logs = [] import os import sys import json # === Dossier de trace === TRACE_DIR = r"C:\AUTOLOOK\trace" os.makedirs(TRACE_DIR, exist_ok=True) # === Rediriger la sortie console === trace_file_path = os.path.join(TRACE_DIR, f"trace_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") trace_file = open(trace_file_path, "w", encoding="utf-8") sys.stdout = sys.stderr = trace_file # Tout va dans ce fichier # Supprimer les messages Unknown property de PyQt5 os.environ["QT_LOGGING_RULES"] = "qt.qpa.xcb.warning=false" os.environ["QT_LOGGING_RULES"] += ";*.debug=false" def resource_path(relative_path): """ Retourne le chemin absolu vers un fichier ressource embarqué, compatible avec une exécution normale et via PyInstaller (.exe). """ if hasattr(sys, '_MEIPASS'): # En mode PyInstaller (exécutable) base_path = sys._MEIPASS else: # En mode normal (Python script) base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) # Chargement du fichier de configuration config.json try: config_path = resource_path("config.json") with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) except Exception as e: print(f"❌ Erreur lors du chargement de config.json : {e}") config = {} class DPIScaler: """ Système de mise à l'échelle DPI pour adapter l'interface PyQt5 aux écrans à haute résolution, amélioré pour les très hautes résolutions (4K, 8K). """ def __init__(self, app): self.app = app self.base_dpi = 96 # Facteur d'échelle initial amélioré self.scale_factor = self.get_initial_scale_factor() # Minimum de facteur d'échelle pour éviter une interface trop petite if self.scale_factor < 1.25 and self.detect_high_res_monitor(): self.scale_factor = 1.25 logger.info(f"Facteur d'échelle minimum appliqué pour haute résolution: {self.scale_factor}") logger.info(f"Facteur d'échelle initial: {self.scale_factor}") # Application du facteur d'échelle global à l'application self.apply_global_scaling() # Tailles originales de police qui seront mises à l'échelle self.font_sizes = { 'normal': 9, 'large': 12, 'title': 14, 'header': 16, 'small': 8, 'tiny': 7, 'monospace': 9 } # Dictionnaire des polices mises à l'échelle self.fonts = self.create_scaled_fonts() def detect_high_res_monitor(self): """Détecte si l'un des moniteurs connectés est à haute résolution (≥ 2K)""" try: if platform.system() == 'Windows': from win32api import GetSystemMetrics screen_width = GetSystemMetrics(0) screen_height = GetSystemMetrics(1) # Vérifier si la résolution est ≥ 2K (2560×1440 ou plus) if screen_width >= 2560 or screen_height >= 1440: return True # Vérifier les résolutions de tous les moniteurs connectés try: from PyQt5.QtWidgets import QDesktopWidget desktop = QDesktopWidget() for i in range(desktop.screenCount()): screen_rect = desktop.screenGeometry(i) if screen_rect.width() >= 2560 or screen_rect.height() >= 1440: return True except: pass return False except Exception as e: logger.error(f"Erreur lors de la détection des moniteurs à haute résolution: {e}") return False def get_initial_scale_factor(self): """Détermine le facteur d'échelle initial en fonction des préférences et de la résolution""" # Essayer de charger depuis les paramètres de l'application settings = QSettings("Snoown2k", "MyApp") saved_factor = settings.value("scale_factor", type=float) if saved_factor is not None: logger.info(f"Facteur d'échelle chargé depuis les paramètres: {saved_factor}") return saved_factor # Si aucune préférence, détecter automatiquement try: # Méthode spécifique à Windows if platform.system() == 'Windows': try: # Première méthode: GetDpiForSystem (Windows 10+) user32 = ctypes.windll.user32 try: dpi = user32.GetDpiForSystem() scale_factor = dpi / self.base_dpi logger.info(f"DPI système détecté: {dpi}, facteur d'échelle: {scale_factor}") return scale_factor except AttributeError: # Pour les versions plus anciennes de Windows pass # Deuxième méthode: GetDeviceCaps try: import win32gui, win32con dc = win32gui.GetDC(0) dpiX = win32gui.GetDeviceCaps(dc, win32con.LOGPIXELSX) win32gui.ReleaseDC(0, dc) scale_factor = dpiX / self.base_dpi logger.info(f"DPI X détecté: {dpiX}, facteur d'échelle: {scale_factor}") return scale_factor except: pass # Méthode alternative basée sur la résolution d'écran screen_width = user32.GetSystemMetrics(0) screen_height = user32.GetSystemMetrics(1) # Estimation du facteur d'échelle basée sur la résolution scale_factor = 1.0 # Par défaut if screen_width >= 7680 or screen_height >= 4320: # 8K scale_factor = 3.0 elif screen_width >= 3840 or screen_height >= 2160: # 4K scale_factor = 2.0 elif screen_width >= 2560 or screen_height >= 1440: # 2K/QHD scale_factor = 1.5 elif screen_width >= 1920 or screen_height >= 1080: # Full HD scale_factor = 1.25 logger.info(f"Résolution écran: {screen_width}x{screen_height}, facteur d'échelle: {scale_factor}") return scale_factor except Exception as win_err: logger.error(f"Erreur lors de la détection DPI sous Windows: {win_err}") return 1.5 # Valeur de repli plus élevée pour les hautes résolutions else: # Méthode pour les autres systèmes (Linux, macOS) try: from PyQt5.QtWidgets import QApplication screen = QApplication.instance().primaryScreen() dpi = screen.physicalDotsPerInch() scale_factor = dpi / self.base_dpi logger.info(f"DPI physique détecté: {dpi}, facteur d'échelle: {scale_factor}") # Limitation pour éviter des échelles trop petites ou trop grandes if scale_factor < 1.0: scale_factor = 1.0 elif scale_factor > 3.0: scale_factor = 3.0 return scale_factor except: logger.warning("Impossible de détecter le DPI physique") # Méthode de dernier recours basée sur Qt try: from PyQt5.QtCore import QCoreApplication pixel_ratio = QCoreApplication.instance().devicePixelRatio() logger.info(f"Ratio de pixels détecté: {pixel_ratio}") return max(1.0, pixel_ratio) except: pass return 1.5 # Valeur par défaut plus élevée except Exception as e: logger.error(f"Erreur lors de la détection DPI: {str(e)}") return 1.5 # Valeur de repli plus élevée def apply_global_scaling(self): """Applique le facteur d'échelle global à l'application Qt""" # Assurer un facteur minimum pour les très hautes résolutions if self.scale_factor < 1.25 and self.detect_high_res_monitor(): self.scale_factor = 1.25 logger.info(f"Facteur d'échelle minimum appliqué: {self.scale_factor}") # Variables d'environnement pour Qt (à définir avant création de l'application) if self.scale_factor > 1.0: os.environ["QT_SCALE_FACTOR"] = str(self.scale_factor) os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" os.environ["QT_SCREEN_SCALE_FACTORS"] = str(self.scale_factor) # PyQt5 a une méthode native pour la mise à l'échelle des polices et des icônes self.app.setAttribute(Qt.AA_EnableHighDpiScaling, True) self.app.setAttribute(Qt.AA_UseHighDpiPixmaps, True) # Mise à l'échelle de la police par défaut de l'application default_font = self.app.font() default_font.setPointSize(int(default_font.pointSize() * self.scale_factor)) self.app.setFont(default_font) logger.info(f"Mise à l'échelle globale appliquée: {self.scale_factor}") def create_scaled_fonts(self): """Crée des polices avec tailles mises à l'échelle""" fonts = {} for name, size in self.font_sizes.items(): scaled_size = self.scale(size) # Création des polices PyQt if name == 'normal': font = QFont("Arial", scaled_size) fonts[name] = font elif name == 'large': font = QFont("Arial", scaled_size) font.setBold(True) fonts[name] = font elif name == 'title': font = QFont("Arial", scaled_size) font.setBold(True) fonts[name] = font elif name == 'header': font = QFont("Arial", scaled_size) font.setBold(True) fonts[name] = font elif name == 'small' or name == 'tiny': font = QFont("Arial", scaled_size) fonts[name] = font elif name == 'monospace': font = QFont("Consolas", scaled_size) fonts[name] = font # Polices spéciales pour widgets spécifiques button_font = QFont("Arial", self.scale(9)) button_font.setBold(True) fonts['button'] = button_font fonts['label'] = QFont("Arial", self.scale(9)) fonts['input'] = QFont("Arial", self.scale(9)) return fonts def scale(self, value): """Applique le facteur d'échelle à une valeur""" if isinstance(value, (int, float)): return int(value * self.scale_factor) return value def save_scaling_preference(self): """Sauvegarde le facteur d'échelle dans les paramètres de l'application""" settings = QSettings("Snoown2k", "MyApp") settings.setValue("scale_factor", self.scale_factor) settings.sync() # Forcer la synchronisation immédiate logger.info(f"Facteur d'échelle sauvegardé: {self.scale_factor}") def update_scale_factor(self, new_factor): """Met à jour le facteur d'échelle et recalcule les polices""" # Valider le facteur d'échelle new_factor = float(new_factor) if new_factor < 1.0: new_factor = 1.0 elif new_factor > 3.0: new_factor = 3.0 self.scale_factor = new_factor self.fonts = self.create_scaled_fonts() self.save_scaling_preference() # Propager le changement à toutes les interfaces if hasattr(self, 'main_window') and self.main_window: if hasattr(self.main_window, 'apply_scaling_to_all_interfaces'): self.main_window.apply_scaling_to_all_interfaces() # Avertir qu'un redémarrage est nécessaire from PyQt5.QtWidgets import QMessageBox QMessageBox.information(None, "Redémarrage requis", f"Le nouveau facteur d'échelle ({new_factor:.2f}) sera pleinement appliqué au prochain démarrage de l'application.") logger.info(f"Facteur d'échelle mis à jour: {new_factor}") def get_scaled_icon(self, icon_path, base_size=16): """Retourne une icône mise à l'échelle""" from PyQt5.QtGui import QIcon icon_size = self.scale(base_size) icon = QIcon(icon_path) return icon def get_scaled_pixmap(self, image_path, width, height): """Retourne une image mise à l'échelle""" from PyQt5.QtGui import QPixmap pixmap = QPixmap(image_path) return pixmap.scaled( self.scale(width), self.scale(height), Qt.KeepAspectRatio, Qt.SmoothTransformation ) # Fonction pour ajouter un contrôle de mise à l'échelle dans l'interface def add_dpi_scaling_controls(main_window, dpi_scaler): """ Ajoute des contrôles pour ajuster la mise à l'échelle DPI dans l'interface """ try: # Créer un groupe pour les contrôles d'échelle scale_group = QGroupBox("Mise à l'échelle de l'interface") scale_group.setStyleSheet(""" QGroupBox { color: white; border: 1px solid #34495e; border-radius: 4px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { color: white; subcontrol-origin: margin; padding: 0 3px; } """) scale_layout = QVBoxLayout(scale_group) # Label d'information info_label = QLabel("Ajustez l'échelle pour les écrans à haute résolution") info_label.setStyleSheet("color: white; font-size: 12px;") info_label.setWordWrap(True) scale_layout.addWidget(info_label) # Contrôles d'échelle slider_layout = QHBoxLayout() # Label pour le facteur actuel current_scale_label = QLabel(f"Facteur d'échelle: {dpi_scaler.scale_factor:.2f}x") current_scale_label.setStyleSheet("color: #3498db; font-weight: bold;") # Slider pour ajuster l'échelle scale_slider = QSlider(Qt.Horizontal) scale_slider.setMinimum(100) # 1.0x scale_slider.setMaximum(300) # 3.0x scale_slider.setValue(int(dpi_scaler.scale_factor * 100)) scale_slider.setTickPosition(QSlider.TicksBelow) scale_slider.setTickInterval(25) # Mettre à jour le label quand le slider change def update_scale_label(value): factor = value / 100.0 current_scale_label.setText(f"Facteur d'échelle: {factor:.2f}x") scale_slider.valueChanged.connect(update_scale_label) # Bouton d'application apply_button = QPushButton("Appliquer") apply_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; border: none; padding: 5px 10px; border-radius: 3px; } QPushButton:hover { background-color: #2ecc71; } """) # Appliquer le changement apply_button.clicked.connect(lambda: dpi_scaler.update_scale_factor(scale_slider.value() / 100.0)) # Ajouter les widgets au layout slider_layout.addWidget(scale_slider) slider_layout.addWidget(current_scale_label) scale_layout.addLayout(slider_layout) scale_layout.addWidget(apply_button) # Retourner le groupe pour l'ajouter à l'interface return scale_group except Exception as e: logger.error(f"Erreur lors de l'ajout des contrôles d'échelle: {e}") return None def apply_scaling_to_all_interfaces(self): """Applique le facteur d'échelle à toutes les interfaces enfants""" if not hasattr(self, 'dpi_scaler'): logger.warning("DPIScaler non disponible, impossible d'appliquer l'échelle globalement") return scale_factor = self.dpi_scaler.scale_factor logger.info(f"Application du facteur d'échelle {scale_factor} à toutes les interfaces") # Appliquer au module OutlookWidget s'il existe if hasattr(self, 'outlook_widget') and self.outlook_widget: try: # Importation locale pour éviter les imports circulaires from module_outlook_2021 import update_outlook_dpi_scaling update_outlook_dpi_scaling(self.outlook_widget, scale_factor) logger.info("Facteur d'échelle appliqué à OutlookWidget") except Exception as e: logger.error(f"Erreur lors de l'application de l'échelle à OutlookWidget: {str(e)}") # Appliquer à d'autres widgets si nécessaire ici # Forcer le rafraîchissement de l'interface self.repaint() QApplication.processEvents() def check_system_resources(): """ Vérifie les ressources système disponibles et détermine si des optimisations sont nécessaires Retourne un dictionnaire avec les paramètres d'optimisation appropriés """ import psutil import platform try: # Obtenir les informations système ram_gb = psutil.virtual_memory().total / (1024 ** 3) # RAM totale en GB cpu_count = psutil.cpu_count() system = platform.system() # Configuration par défaut config = { 'needs_optimization': False, 'memory_limit': None, 'cpu_limit': None, 'priority_level': None, 'gc_aggressive': False, 'disable_effects': False } # Vérifier si le système a besoin d'optimisations if ram_gb <= 4: # 4 GB ou moins de RAM config.update({ 'needs_optimization': True, 'memory_limit': int(psutil.virtual_memory().total * 0.15), # 15% de la RAM 'cpu_limit': 1, # Limite à un cœur 'priority_level': 'low', 'gc_aggressive': True, 'disable_effects': True }) elif ram_gb <= 8: # Entre 4 et 8 GB de RAM config.update({ 'needs_optimization': True, 'memory_limit': int(psutil.virtual_memory().total * 0.25), # 25% de la RAM 'cpu_limit': min(2, cpu_count), # Maximum 2 cœurs 'priority_level': 'below_normal', 'gc_aggressive': True, 'disable_effects': False }) # Ajouter des informations système au log logger.info(f"Configuration système détectée :") logger.info(f"RAM: {ram_gb:.2f} GB") logger.info(f"CPU cores: {cpu_count}") logger.info(f"Système: {system}") logger.info(f"Optimisations nécessaires: {config['needs_optimization']}") return config except Exception as e: logger.error(f"Erreur lors de la vérification des ressources système: {str(e)}") return None def apply_system_optimizations(app, config): """ Applique les optimisations en fonction de la configuration détectée """ try: if not config or not config['needs_optimization']: return logger.info("Application des optimisations système...") # Configuration du processus principal current_process = psutil.Process() if sys.platform == 'win32': import win32api import win32process # Appliquer la priorité au processus principal handle = win32api.OpenProcess(win32process.PROCESS_ALL_ACCESS, False, os.getpid()) priority_class = win32process.IDLE_PRIORITY_CLASS if config[ 'priority_level'] == 'low' else win32process.BELOW_NORMAL_PRIORITY_CLASS win32process.SetPriorityClass(handle, priority_class) # Chercher et configurer les processus QtWebEngine for proc in psutil.process_iter(['name', 'pid']): try: if 'QtWebEngineProcess' in proc.info['name']: web_handle = win32api.OpenProcess(win32process.PROCESS_ALL_ACCESS, False, proc.info['pid']) win32process.SetPriorityClass(web_handle, priority_class) logger.info(f"Priorité appliquée au processus QtWebEngine (PID: {proc.info['pid']})") except (psutil.NoSuchProcess, psutil.AccessDenied): continue # Optimisation mémoire Windows win32process.SetProcessWorkingSetSize(handle, -1, -1) else: # Configuration Linux/Unix os.nice(10 if config['priority_level'] == 'low' else 5) # Chercher et configurer les processus QtWebEngine sous Linux for proc in psutil.process_iter(['name', 'pid']): try: if 'QtWebEngineProcess' in proc.info['name']: os.setpriority(os.PRIO_PROCESS, proc.info['pid'], 10 if config['priority_level'] == 'low' else 5) logger.info(f"Priorité appliquée au processus QtWebEngine (PID: {proc.info['pid']})") except (psutil.NoSuchProcess, psutil.AccessDenied): continue # Limite CPU if config['cpu_limit']: available_cores = list(range(psutil.cpu_count())) limited_cores = available_cores[:config['cpu_limit']] current_process.cpu_affinity(limited_cores) # Configuration de la mémoire if config['memory_limit']: app.setProperty("maximum_heap_size", config['memory_limit']) # Optimisation du garbage collector if config['gc_aggressive']: import gc gc.set_threshold(100, 5, 5) app.setProperty("gc_interval", 300) # Désactiver les effets visuels si nécessaire if config['disable_effects']: app.setAttribute(Qt.AA_UseHighDpiPixmaps, False) app.setAttribute(Qt.AA_EnableHighDpiScaling, False) app.setStyle('Fusion') # Style plus léger # Configurer un Timer pour vérifier périodiquement les nouveaux processus QtWebEngine def check_new_webengine_processes(): for proc in psutil.process_iter(['name', 'pid']): try: if 'QtWebEngineProcess' in proc.info['name']: if sys.platform == 'win32': web_handle = win32api.OpenProcess(win32process.PROCESS_ALL_ACCESS, False, proc.info['pid']) current_priority = win32process.GetPriorityClass(web_handle) if current_priority != priority_class: win32process.SetPriorityClass(web_handle, priority_class) logger.info( f"Priorité mise à jour pour nouveau processus QtWebEngine (PID: {proc.info['pid']})") else: current_priority = os.getpriority(os.PRIO_PROCESS, proc.info['pid']) target_priority = 10 if config['priority_level'] == 'low' else 5 if current_priority != target_priority: os.setpriority(os.PRIO_PROCESS, proc.info['pid'], target_priority) logger.info( f"Priorité mise à jour pour nouveau processus QtWebEngine (PID: {proc.info['pid']})") except (psutil.NoSuchProcess, psutil.AccessDenied): continue timer = QTimer() timer.timeout.connect(check_new_webengine_processes) timer.start(5000) # Vérifier toutes les 5 secondes logger.info("Optimisations système appliquées avec succès") return True except Exception as e: logger.error(f"Erreur lors de l'application des optimisations: {str(e)}") return False def add_debug_log(action, message): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_entry = f"{timestamp} - Action: {action} - Message: {message}\n" debug_logs.append(log_entry) # Écriture immédiate dans le fichier debug_file = os.path.join(DEBUG_DIR, f"debug_{datetime.now().strftime('%Y%m%d')}.txt") with open(debug_file, "a", encoding="utf-8") as f: f.write(log_entry) def write_debug_file(): if debug_logs: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") debug_file = os.path.join(DEBUG_DIR, f"debug_{timestamp}.txt") with open(debug_file, "w", encoding="utf-8") as f: # ✅ encodage utf-8 ajouté for log in debug_logs: f.write(f"{log}\n") debug_logs.clear() def exception_hook(exctype, value, tb): from traceback import format_exception error_msg = ''.join(format_exception(exctype, value, tb)) # Journaliser dans un fichier spécial crash_dir = r"c:\autolook\crash_report" os.makedirs(crash_dir, exist_ok=True) # Crée le répertoire s'il n'existe pas crash_file = os.path.join(crash_dir, f"crash_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") with open(crash_file, "w", encoding="utf-8") as f: f.write("💥 Crash inattendu détecté !\n\n") f.write(error_msg) # Ajouter aussi les derniers logs si disponibles if debug_logs: f.write("\n📋 Derniers logs enregistrés :\n") for log in debug_logs[-50:]: # Les 50 derniers f.write(log) print(f"🚨 Crash détecté — journal sauvegardé dans {crash_file}") # Activer le gestionnaire de crash import sys sys.excepthook = exception_hook def clear_pycache(): pycache_dir = r"C:\AUTOLOOK\modules\__pycache__" if os.path.exists(pycache_dir): try: for filename in os.listdir(pycache_dir): file_path = os.path.join(pycache_dir, filename) try: if os.path.isfile(file_path) or os.path.islink(file_path): os.unlink(file_path) elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: print(f"Échec de la suppression de {file_path}. Raison: {e}") print("Contenu de __pycache__ supprimé avec succès.") except Exception as e: print(f"Erreur lors de la suppression du contenu de __pycache__: {e}") else: print("Le répertoire __pycache__ n'existe pas.") AUTOLOOK_DIR = r"C:\autolook" MODULES_DIR = os.path.join(AUTOLOOK_DIR, "modules") LOG_DIR = os.path.join(AUTOLOOK_DIR, "log") # CREATE DIRECTORY IF NO EXIST for directory in [AUTOLOOK_DIR, MODULES_DIR, LOG_DIR]: os.makedirs(directory, exist_ok=True) # Configuration du format de date pour le nom du fichier current_file = os.path.basename(__file__).replace('.py', '') current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") log_file = os.path.join(LOG_DIR, f"{current_file}_{current_time}.txt") # Configuration du logger principal logger = logging.getLogger() # Root logger logger.setLevel(logging.DEBUG) # Formatter personnalisé avec nom de fichier class FileNameFilter(logging.Filter): def filter(self, record): record.filename_only = os.path.basename(record.pathname) return True # Formatter personnalisé formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(message)s') # Configuration du root logger root_logger = logging.getLogger() root_logger.setLevel(logging.DEBUG) # Handler pour le fichier file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) # Handler pour la console console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) # Configuration des loggers spécifiques def setup_module_logger(name): logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) return logger # Logger pour l'AudioPlayer audio_logger = setup_module_logger('AudioPlayer') # Réduire le niveau de log pour certains modules logging.getLogger('comtypes').setLevel(logging.WARNING) logging.getLogger('matplotlib').setLevel(logging.WARNING) # Journalisation du démarrage logger = logging.getLogger(__name__) logger.info(f"Démarrage de l'application") logger.info(f"Fichier de log créé : {log_file}") # Réduire spécifiquement le niveau de log pour comtypes logging.getLogger('comtypes').setLevel(logging.WARNING) logging.getLogger('matplotlib').setLevel(logging.WARNING) sys.path.insert(0, MODULES_DIR) QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) # Constantes pour manipuler le style de la fenêtre console GWL_EXSTYLE = -20 WS_EX_TOOLWINDOW = 0x00000080 # enlève de la barre des tâches WS_EX_APPWINDOW = 0x00040000 # remet dans la barre des tâches SWP_NOSIZE = 0x0001 SWP_NOMOVE = 0x0002 SWP_FRAMECHANGED = 0x0020 def hide_console(): if sys.platform == 'win32': console_window = win32gui.GetForegroundWindow() win32gui.ShowWindow(console_window, win32con.SW_HIDE) def hide_console(): if sys.platform == 'win32': console_window = win32gui.GetForegroundWindow() win32gui.ShowWindow(console_window, win32con.SW_HIDE) def hide_console_from_taskbar(): if sys.platform != 'win32': return hwnd = ctypes.windll.kernel32.GetConsoleWindow() if not hwnd: return exstyle = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE) # ajouter TOOLWINDOW (cache dans la barre) et retirer APPWINDOW exstyle = (exstyle | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, exstyle) ctypes.windll.user32.SetWindowPos( hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED ) def restore_console_to_taskbar_and_show(): if sys.platform != 'win32': return hwnd = ctypes.windll.kernel32.GetConsoleWindow() if not hwnd: return # remettre dans la barre des tâches exstyle = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE) exstyle = (exstyle & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, exstyle) ctypes.windll.user32.SetWindowPos( hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED ) win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) win32gui.SetForegroundWindow(hwnd) def minimize_console_to_taskbar(): """Minimise la console mais la garde visible dans la barre des tâches""" if sys.platform != 'win32': return hwnd = ctypes.windll.kernel32.GetConsoleWindow() if not hwnd: return # S'assurer que la console est visible dans la barre des tâches exstyle = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE) exstyle = (exstyle & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, exstyle) ctypes.windll.user32.SetWindowPos( hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED ) # Minimiser la fenêtre win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) # Version de l'application (à mettre à jour manuellement avec chaque release) APP_VERSION = "1.0.0" # ← METTEZ À JOUR CETTE LIGNE DANS CHAQUE NOUVELLE VERSION def get_current_version(): """Récupère la version depuis le système JSON avec migration automatique""" try: version_file = r"C:\AUTOLOOK\version.json" # Si le fichier version.json n'existe pas, initialiser le système if not os.path.exists(version_file): return initialize_version_system() # Lire la version existante with open(version_file, 'r', encoding='utf-8') as f: data = json.load(f) json_version = data.get('version', '1.0.0') # Vérifier si une mise à jour est nécessaire if detect_version_update_needed(json_version): return perform_automatic_update(json_version) return json_version except Exception as e: logger.error(f"Erreur lors du chargement de la version: {str(e)}") return initialize_version_system() def detect_version_update_needed(json_version): """Détecte si une mise à jour est nécessaire""" try: # Comparer APP_VERSION avec la version dans le JSON if APP_VERSION != json_version: logger.info(f"Mise à jour détectée: {json_version} → {APP_VERSION}") return True # Vérifier si les fichiers principaux sont plus récents que version.json main_files = [ r"C:\AUTOLOOK\main.py", r"C:\AUTOLOOK\modules\module_outlook_2021.py" ] version_file = r"C:\AUTOLOOK\version.json" if os.path.exists(version_file): version_time = os.path.getmtime(version_file) for file_path in main_files: if os.path.exists(file_path): file_time = os.path.getmtime(file_path) # Si fichier modifié plus récemment (avec marge de 2 minutes) if file_time > version_time + 120: logger.info(f"Fichier plus récent détecté: {file_path}") return True return False except Exception as e: logger.error(f"Erreur lors de la détection de mise à jour: {str(e)}") return False def perform_automatic_update(old_version): """Effectue la mise à jour automatique du système""" try: logger.info(f"🔄 Début de la mise à jour automatique: {old_version} → {APP_VERSION}") # 1. Nettoyer l'ancien système cleanup_old_changelog_system() # 2. Créer/mettre à jour version.json version_data = { 'version': APP_VERSION, 'last_updated': datetime.now().isoformat(), 'previous_version': old_version, 'auto_updated': True, 'update_note': f'Mise à jour automatique de {old_version} vers {APP_VERSION}' } version_file = r"C:\AUTOLOOK\version.json" with open(version_file, 'w', encoding='utf-8') as f: json.dump(version_data, f, indent=4, ensure_ascii=False) # 3. Mettre à jour changelog.json update_changelog_for_automatic_update(old_version, APP_VERSION) logger.info(f"✅ Mise à jour automatique terminée: {APP_VERSION}") return APP_VERSION except Exception as e: logger.error(f"Erreur lors de la mise à jour automatique: {str(e)}") return old_version def cleanup_old_changelog_system(): """Supprime les anciens fichiers de changelog (.txt)""" try: old_files = [ r"C:\AUTOLOOK\changelog", r"C:\AUTOLOOK\changelog.txt", r"C:\AUTOLOOK\changelog\changelog.txt" ] removed_files = [] for file_path in old_files: if os.path.exists(file_path): os.remove(file_path) removed_files.append(os.path.basename(file_path)) logger.info(f"Ancien fichier supprimé: {file_path}") # Supprimer le dossier changelog s'il est vide old_changelog_dir = r"C:\AUTOLOOK\changelog" if os.path.exists(old_changelog_dir) and not os.listdir(old_changelog_dir): os.rmdir(old_changelog_dir) logger.info("Ancien dossier changelog supprimé") except Exception as e: logger.error(f"Erreur lors du nettoyage: {str(e)}") def update_changelog_for_automatic_update(old_version, new_version): """Met à jour le changelog lors d'une mise à jour automatique""" try: changelog_file = r"C:\AUTOLOOK\changelog.json" # Charger l'historique existant ou créer un nouveau changelog_history = [] if os.path.exists(changelog_file): with open(changelog_file, 'r', encoding='utf-8') as f: changelog_history = json.load(f) # Créer une nouvelle entrée new_entry = { 'version': new_version, 'date': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), 'previous_version': old_version, 'changes': { 'major': [], 'minor': [f'Mise à jour automatique vers {new_version}'], 'patch': ['Migration vers le nouveau système de versioning'] }, 'auto_updated': True } # Ajouter en tête de liste changelog_history.insert(0, new_entry) # Sauvegarder with open(changelog_file, 'w', encoding='utf-8') as f: json.dump(changelog_history, f, indent=4, ensure_ascii=False) logger.info("Changelog mis à jour automatiquement") except Exception as e: logger.error(f"Erreur lors de la mise à jour du changelog: {str(e)}") def initialize_version_system(): """Initialise le système de versioning pour la première fois""" try: logger.info("🔧 Initialisation du système de versioning") # Nettoyer l'ancien système cleanup_old_changelog_system() # Créer version.json initial version_data = { 'version': APP_VERSION, 'last_updated': datetime.now().isoformat(), 'previous_version': None, 'initialized': True, 'note': 'Système de versioning initialisé' } version_file = r"C:\AUTOLOOK\version.json" with open(version_file, 'w', encoding='utf-8') as f: json.dump(version_data, f, indent=4, ensure_ascii=False) # Créer changelog.json initial initial_changelog = [{ 'version': APP_VERSION, 'date': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), 'previous_version': None, 'changes': { 'major': ['Initialisation du nouveau système de versioning'], 'minor': [], 'patch': [] }, 'initialized': True }] changelog_file = r"C:\AUTOLOOK\changelog.json" with open(changelog_file, 'w', encoding='utf-8') as f: json.dump(initial_changelog, f, indent=4, ensure_ascii=False) logger.info(f"✅ Système de versioning initialisé: {APP_VERSION}") return APP_VERSION except Exception as e: logger.error(f"Erreur lors de l'initialisation: {str(e)}") return "1.0.0" # Variable VERSION mise à jour depuis le nouveau système VERSION = get_current_version() def load_module(module_name): try: module = importlib.import_module(module_name) logger.info(f"Module {module_name} chargé avec succès") return module except ImportError as e: logger.error(f"Erreur lors du chargement du module {module_name}: {e}") raise except Exception as e: logger.error(f"Erreur inattendue lors du chargement du module {module_name}: {e}") return None def load_modules(): modules = {} config = load_config() for module_name, module_data in config.items(): if isinstance(module_data, dict) and module_data.get('installed', False): try: module = load_module(module_name) if module: modules[module_name] = module logger.info(f"Module {module_name} chargé et ajouté") print(f"Module {module_name} chargé avec succès!") except Exception as e: logger.error(f"Erreur lors du chargement du module {module_name}: {str(e)}") return modules def load_config(): try: config_path = r"C:\autolook\config.json" if os.path.exists(config_path): with open(config_path, 'r') as f: config = json.load(f) if not isinstance(config, dict): logger.error("Le fichier de configuration n'est pas un dictionnaire valide") return {} return config logger.warning("Fichier de configuration non trouvé") return {} except json.JSONDecodeError: logger.error(f"Erreur de décodage JSON pour le fichier {config_path}") return {} except Exception as e: logger.error(f"Erreur lors du chargement de la configuration : {str(e)}") return {} class StartupDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Démarrage de l'application") self.setFixedSize(400, 400) # Réduction de la taille self.countdown = 60 self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; font-size: 16px; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 10px 20px; border-radius: 4px; font-size: 12px; font-weight: bold; min-width: 300px; max-width: 300px; margin: 5px; } QPushButton:hover { background-color: #007B9A; } QPushButton:disabled { background-color: #808080; /* Gris pour l'état désactivé */ color: #D3D3D3; /* Texte plus clair */ opacity: 0.7; cursor: not-allowed; } #countdown_label { color: #03699c; font-size: 12px; margin-top: 0px; } """) # Layout principal layout = QVBoxLayout(self) layout.setSpacing(15) layout.setContentsMargins(20, 20, 20, 20) # Logo ou titre title = QLabel("M.A.2C") title.setAlignment(Qt.AlignCenter) title.setStyleSheet("font-size: 18px; font-weight: bold;") layout.addWidget(title) # Description desc = QLabel("Sélectionnez votre méthode de connexion :", self) desc.setAlignment(Qt.AlignCenter) layout.addWidget(desc) # Container pour les boutons avec centrage button_container = QWidget() button_layout = QVBoxLayout(button_container) button_layout.setAlignment(Qt.AlignCenter) button_layout.setSpacing(15) # Bouton Office self.office_button = QPushButton("Démarrer MA2C avec Outlook Office") self.office_button.setEnabled(True) self.office_button.setToolTip("Office 2010 à 2021 pris en charge") self.office_button.clicked.connect(lambda: self.accept_with_version('office')) button_layout.addWidget(self.office_button, 0, Qt.AlignCenter) # Bouton Thunderbird self.thunderbird_button = QPushButton("Démarrer MA2C avec Mozilla Thunderbird") self.thunderbird_button.setEnabled(False) # Désactiver le bouton self.thunderbird_button.setToolTip("Option non disponible actuellement") # Ajouter un tooltip explicatif button_layout.addWidget(self.thunderbird_button, 0, Qt.AlignCenter) # Bouton Imap self.imap_button = QPushButton("Démarrer MA2C avec une connexion directe") self.imap_button.setEnabled(False) self.imap_button.setToolTip("Démarrer sans messagerie en connexion IMAP") self.imap_button.clicked.connect(lambda: self.accept_with_version('imap')) button_layout.addWidget(self.imap_button, 0, Qt.AlignCenter) layout.addWidget(button_container) # Label pour le compte à rebours self.title_label = QLabel("Démarrage automatique avec Outlook Office") self.countdown_label = QLabel(f"dans {self.countdown} secondes...") self.countdown_label.setObjectName("countdown_label") self.title_label.setStyleSheet("color: #33C9FF; font-size: 14px;") self.title_label.setAlignment(Qt.AlignCenter) self.countdown_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.title_label) layout.addWidget(self.countdown_label) # Timer pour le compte à rebours self.timer = QTimer(self) self.timer.timeout.connect(self.update_countdown) self.timer.start(500) # Timer pour l'effet zoom (plus rapide) self.zoom_timer = QTimer(self) self.zoom_timer.timeout.connect(self.update_zoom_effect) self.selected_version = 'office' # Par défaut self.zoom_state = False # Pour l'effet zoom def update_countdown(self): """Met à jour le compte à rebours et accepte automatiquement après 5 secondes""" self.countdown -= 1 self.update_countdown_color() if self.countdown > 0: self.countdown_label.setText(f"dans {self.countdown} secondes...") else: self.timer.stop() self.accept_with_version('office') def update_countdown_color(self): """Met à jour la couleur du compte à rebours en fonction du temps restant""" # Calcul du pourcentage de temps restant (0 à 100) percentage = (self.countdown / 60) * 100 # Transition de couleur du bleu vers le rouge if percentage > 66: # 40-60s : Bleu color = "#33C9FF" font_size = 12 elif percentage > 33: # 20-40s : Orange color = "#ff8c00" font_size = 12 else: # 0-20s : Rouge color = "#ff0000" font_size = 12 # Effet zoom pour les 10 dernières secondes if self.countdown <= 10: self.zoom_state = not self.zoom_state # Alterne entre True et False if self.zoom_state: font_size = 16 # Taille agrandie else: font_size = 12 # Taille normale # Appliquer la couleur et la taille au label self.countdown_label.setStyleSheet( f"color: {color}; font-size: {font_size}px; margin-top: 0px; font-weight: bold;") def update_zoom_effect(self): """Met à jour uniquement l'effet zoom pendant les 5 dernières secondes""" if self.countdown <= 5: self.update_countdown_color() def accept_with_version(self, version): """Accepte le dialogue avec la version sélectionnée""" self.selected_version = version self.accept() def accept_with_thunderbird(self, version): """Accepte le dialogue avec la version sélectionnée""" self.selected_version = version self.accept() def get_selected_version(self): """Retourne la version sélectionnée""" return self.selected_version class AudioPlayer: def __init__(self): self.player = QMediaPlayer() self.logger = logging.getLogger('AudioPlayer') self.player.setVolume(70) self.player.error.connect(self.handle_error) def play_startup_sound(self): try: audio_path = os.path.join(r"C:\autolook\voice", f"EN_ElevenLabs_Text_to_Speech_audio.mp3") self.logger.info(f"Tentative de lecture du fichier audio: {audio_path}") if os.path.exists(audio_path): url = QUrl.fromLocalFile(audio_path) content = QMediaContent(url) self.player.setMedia(content) self.player.stop() self.player.play() self.logger.info(f"Lecture du fichier audio démarrée: {audio_path}") else: self.logger.warning(f"Fichier audio non trouvé: {audio_path}") except Exception as e: self.logger.error(f"Erreur lors de la lecture audio: {str(e)}") def stop(self): try: self.player.stop() self.logger.info("Lecture audio arrêtée") except Exception as e: self.logger.error(f"Erreur lors de l'arrêt de la lecture: {str(e)}") def handle_error(self, error): error_messages = { QMediaPlayer.NoError: "Pas d'erreur", QMediaPlayer.ResourceError: "Erreur de ressource", QMediaPlayer.FormatError: "Format non supporté", QMediaPlayer.NetworkError: "Erreur réseau", QMediaPlayer.AccessDeniedError: "Accès refusé" } error_msg = error_messages.get(error, "Erreur inconnue") self.logger.error(f"Erreur de lecture audio: {error_msg}") def find_folder(mailbox, folder_name): """Recherche récursive d'un dossier par son nom.""" for folder in mailbox.Folders: if folder.Name.strip() == folder_name.strip(): return folder # Recherche dans les sous-dossiers subfolder = find_folder(folder, folder_name) if subfolder: return subfolder return None def count_emails_from_json_profiles(profiles_path): pythoncom.CoInitialize() results = [] profile_data = load_profile_data() # Charger les données existantes today_date = datetime.now().date() try: outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") except Exception as e: return [f"Erreur de connexion à Outlook : {e}"] profile_files = [f for f in os.listdir(profiles_path) if f.endswith('.json')] for profile_file in profile_files: profile_path = os.path.join(profiles_path, profile_file) profile_name = os.path.splitext(profile_file)[0] try: with open(profile_path, 'r') as f: profile_data_file = json.load(f) outlook_settings = profile_data_file.get("outlook_settings", {}) mailbox_name = outlook_settings.get("mailbox", "").strip() folder_name = outlook_settings.get("folder", "").strip() if not mailbox_name or not folder_name: results.append([profile_name, "Paramètres manquants", "", 0, 0]) continue try: mailbox = outlook.Folders.Item(mailbox_name) target_folder = find_folder(mailbox, folder_name) if target_folder: total_emails = target_folder.Items.Count emails_today = 0 # Comptage des e-mails d'aujourd'hui for item in target_folder.Items: try: if item.ReceivedTime.date() == today_date: emails_today += 1 except AttributeError: continue # Mise à jour des données enregistrées if profile_name not in profile_data: profile_data[profile_name] = { "total": total_emails, "processed_today": emails_today, "last_updated": str(today_date) } else: # Si la date a changé, réinitialiser les mails du jour if profile_data[profile_name]["last_updated"] != str(today_date): profile_data[profile_name]["total"] += profile_data[profile_name]["processed_today"] profile_data[profile_name]["processed_today"] = emails_today profile_data[profile_name]["last_updated"] = str(today_date) else: # Sinon, simplement mettre à jour les mails du jour profile_data[profile_name]["processed_today"] = emails_today results.append( [profile_name, mailbox_name, folder_name, profile_data[profile_name]["total"], emails_today]) else: results.append([profile_name, mailbox_name, "Dossier introuvable", 0, 0]) except Exception as e: results.append([profile_name, mailbox_name, f"Erreur : {e}", 0, 0]) except Exception as e: results.append([profile_name, "Erreur de lecture", str(e), 0, 0]) save_profile_data(profile_data) # Sauvegarder les mises à jour return results def save_profile_data(data): """Sauvegarde les données des profils dans un fichier JSON.""" with open("profile_data.json", "w") as f: json.dump(data, f, indent=4) class EnhancedLoadingDialog(QDialog): def __init__(self, parent=None, countdown_seconds=60): super().__init__(parent) self.setWindowTitle("Chargement") self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) self.setFixedSize(400, 200) self.countdown_seconds = countdown_seconds self.dashboard = None self.current_banner_index = 0 self.banners = [ r"C:\autolook\Banniere\sotam125.gif", r"C:\autolook\Banniere\tauctionlist125.gif", r"C:\autolook\Banniere\clickgen125.gif" ] self.init_ui() def init_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QProgressBar { border: 2px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #34495e; color: white; height: 25px; } QProgressBar::chunk { border-radius: 4px; background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2196F3, stop:0.4 #03A9F4, stop:0.6 #00BCD4, stop:1 #3F51B5 ); } """) self.message_label = QLabel("Chargement du tableau de bord...") self.message_label.setAlignment(Qt.AlignCenter) self.message_label.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") layout.addWidget(self.message_label) self.countdown_label = QLabel(f"Temps restant : {self.countdown_seconds} s") self.countdown_label.setAlignment(Qt.AlignCenter) self.countdown_label.setStyleSheet("color: #03699c; font-size: 14px;") layout.addWidget(self.countdown_label) self.banner_label = QLabel() width, height = self.get_banner_dimensions() self.banner_label.setFixedSize(width, height) self.banner_label.setAlignment(Qt.AlignCenter) self.banner_label.setStyleSheet(""" QLabel { background-color: #2c3e50; border-radius: 8px; padding: 5px; } """) layout.addWidget(self.banner_label, alignment=Qt.AlignCenter) self.load_current_banner() self.progress_bar = QProgressBar() self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(100) self.progress_bar.setValue(0) layout.addWidget(self.progress_bar) self.countdown_timer = QTimer(self) self.countdown_timer.timeout.connect(self.update_countdown) self.countdown_timer.start(1000) self.banner_timer = QTimer(self) self.banner_timer.timeout.connect(self.rotate_banner) self.banner_timer.start(3000) def get_banner_dimensions(self): for banner_path in self.banners: if os.path.exists(banner_path): movie = QMovie(banner_path) if movie.isValid(): movie.jumpToFrame(0) return movie.currentImage().width(), 125 return 125, 125 def load_current_banner(self): current_banner = self.banners[self.current_banner_index] if os.path.exists(current_banner): movie = QMovie(current_banner) movie.setScaledSize(self.banner_label.size()) self.banner_label.setMovie(movie) movie.start() else: self.banner_label.setText(f"[Banner Error] {current_banner}") self.banner_label.setStyleSheet("color: white;") def rotate_banner(self): self.current_banner_index = (self.current_banner_index + 1) % len(self.banners) self.load_current_banner() def update_countdown(self): self.countdown_seconds -= 1 if self.countdown_seconds <= 0: logger.info("Décompte terminé, préparation à l'affichage du tableau de bord") self.countdown_timer.stop() self.banner_timer.stop() if self.dashboard: logger.info("Affichage du tableau de bord") self.dashboard.show() logger.info("Tableau de bord affiché") else: logger.error("Dashboard non défini!") self.accept() else: self.countdown_label.setText(f"Temps restant : {self.countdown_seconds} s") progress_value = ((60 - self.countdown_seconds) * 100) // 60 self.progress_bar.setValue(progress_value) def set_progress(self, value): self.progress_bar.setValue(value) def set_dashboard(self, dashboard): logger.info("Configuration du tableau de bord") self.dashboard = dashboard if self.dashboard: logger.info("Tableau de bord configuré avec succès") else: logger.error("Échec de la configuration du tableau de bord") def closeEvent(self, event): logger.info("Fermeture de la fenêtre de chargement") self.countdown_timer.stop() self.banner_timer.stop() super().closeEvent(event) class LinksDashboardDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Tableau de bord des liens") self.setMinimumSize(1000, 800) self.setup_ui() self.load_profiles() def setup_ui(self): """Configure l'interface utilisateur.""" self.layout = QVBoxLayout(self) # Style du dialogue self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } QTableWidget { background-color: #03699c; border-radius: 4px; } QTableWidget::item { background-color: white; color: black; } """) # Boutons en haut button_layout = QHBoxLayout() predict_button = QPushButton("Prédiction des Emails") hyperlink_button = QPushButton("Liens Hypertextes") rule_button = QPushButton("Créer Règle Outlook") refresh_button = QPushButton("🔄 Rafraîchir") refresh_button.clicked.connect(self.load_profiles) for btn in [predict_button, hyperlink_button, rule_button, refresh_button]: button_layout.addWidget(btn) self.layout.addLayout(button_layout) # Tableau pour afficher les résultats self.table = QTableWidget(0, 5) self.table.setHorizontalHeaderLabels([ "Profil", "Emails Restants", "Emails du Jour", "Emails Traités", "Total" ]) self.table.horizontalHeader().setStretchLastSection(True) self.layout.addWidget(self.table) # Graphique self.graph = EmailGraph(self) self.layout.addWidget(self.graph) def load_profiles(self): """Charge les profils et met à jour le tableau""" profiles_directory = r"C:\AUTOLOOK\profiles" if not os.path.exists(profiles_directory): QMessageBox.critical(self, "Erreur", "Le dossier des profils n'existe pas.") return results = count_emails_from_json_profiles(profiles_directory) self.table.setRowCount(len(results)) all_data = [] for row, data in enumerate(results): profile_name = data[0] total_emails = data[3] emails_today = data[4] processed_emails = self.get_processed_today(profile_name) # Ajouter les données dans le tableau self.table.setItem(row, 0, QTableWidgetItem(profile_name)) self.table.setItem(row, 1, QTableWidgetItem(str(total_emails))) self.table.setItem(row, 2, QTableWidgetItem(str(emails_today))) self.table.setItem(row, 3, QTableWidgetItem(str(processed_emails))) self.table.setItem(row, 4, QTableWidgetItem(str(total_emails + processed_emails))) all_data.append({ "Profil": profile_name, "Emails Restants": total_emails, "Emails du Jour": emails_today, "Emails Traités": processed_emails }) # Mettre à jour le graphique self.graph.update_data(pd.DataFrame(all_data)) def get_processed_today(self, profile_name): """Récupère le nombre d'emails traités aujourd'hui pour un profil""" try: # Récupérer les données depuis le fichier profile_data.json profile_data_path = os.path.join(r"C:\AUTOLOOK", "profile_data.json") if os.path.exists(profile_data_path): with open(profile_data_path, 'r') as f: profile_data = json.load(f) if profile_name in profile_data: # Vérifier si les données sont d'aujourd'hui today = str(datetime.now().date()) if profile_data[profile_name].get('last_updated') == today: return profile_data[profile_name].get('processed_today', 0) # Si le fichier n'existe pas ou si les données ne sont pas d'aujourd'hui, retourner 0 return 0 except Exception as e: logger.error(f"Erreur lors de la récupération des emails traités : {str(e)}") return 0 class EmailGraph(FigureCanvas): def __init__(self, parent=None): self.fig = Figure(figsize=(10, 5)) super().__init__(self.fig) self.setParent(parent) self.setup_graph() def setup_graph(self): """Configure le graphique initial""" self.ax = self.fig.add_subplot(111) self.fig.set_facecolor('#1a2a3a') self.ax.set_facecolor('#1a2a3a') self.ax.tick_params(colors='white') self.ax.set_title("Répartition des Emails", color='white') self.ax.set_xlabel("Profil", color='white') self.ax.set_ylabel("Nombre d'Emails", color='white') for spine in self.ax.spines.values(): spine.set_color('white') def update_data(self, df): """Met à jour le graphique avec les nouvelles données""" self.ax.clear() self.setup_graph() x = range(len(df)) width = 0.25 self.ax.bar([i - width for i in x], df["Emails Restants"], width, label="Emails Restants", color="pink", alpha=0.7) self.ax.bar([i for i in x], df["Emails du Jour"], width, label="Emails du Jour", color="lightblue", alpha=0.7) self.ax.bar([i + width for i in x], df["Emails Traités"], width, label="Emails Traités", color="lightgreen", alpha=0.7) self.ax.set_xticks(x) self.ax.set_xticklabels(df["Profil"], rotation=45) self.ax.legend() self.draw() class ReportDialog(QDialog): def __init__(self, report_content, parent=None): super().__init__(parent) self.setWindowTitle("Rapport de traitement") self.setGeometry(100, 100, 1000, 900) self.setWindowFlags(self.windowFlags() | Qt.Window) self.is_closing = False # Style général de la fenêtre self.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; min-width: 100px; margin: 5px; } QPushButton:hover { background-color: #007B9A; } QPushButton:pressed { background-color: #006A89; } QTextEdit { background-color: #03699c; color: white; border: 1px solid #34495e; border-radius: 4px; padding: 10px; font-family: "Courier New"; font-size: 12px; } """) # Layout principal layout = QVBoxLayout(self) layout.setSpacing(10) layout.setContentsMargins(5, 5, 5, 5) # Barre supérieure top_bar = QWidget() top_bar.setStyleSheet("background-color: #2c3e50; padding: 5px;") top_bar_layout = QHBoxLayout(top_bar) title_label = QLabel("Détail de traitement") title_label.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") top_bar_layout.addWidget(title_label) layout.addWidget(top_bar) # Zone de texte du rapport self.text_edit = QTextEdit() self.text_edit.setReadOnly(True) self.text_edit.setText(report_content) layout.addWidget(self.text_edit) # Création d'un document séparé pour l'impression self.print_document = QTextDocument() self.format_print_document(report_content) # Barre d'outils inférieure button_panel = QWidget() button_panel.setStyleSheet("background-color: #2c3e50; padding: 10px;") button_layout = QHBoxLayout(button_panel) button_layout.setContentsMargins(10, 5, 10, 5) # Boutons avec la nouvelle couleur #008CBA save_button = QPushButton("💾 Enregistrer") print_button = QPushButton("🖨️ Imprimer") preview_button = QPushButton("👀 Aperçu") close_button = QPushButton("❌ Fermer") # Style spécifique pour les boutons button_style = """ QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; min-width: 100px; } QPushButton:hover { background-color: #007B9A; } QPushButton:pressed { background-color: #006A89; } """ for button in [save_button, print_button, preview_button, close_button]: button.setStyleSheet(button_style) button_layout.addWidget(save_button) button_layout.addWidget(preview_button) button_layout.addWidget(print_button) button_layout.addStretch() button_layout.addWidget(close_button) layout.addWidget(button_panel) # Connexions des boutons save_button.clicked.connect(self.save_report) print_button.clicked.connect(self.print_report) preview_button.clicked.connect(self.preview_report) close_button.clicked.connect(self.close_dialog) def close_dialog(self): """Fermeture propre du dialogue en évitant la réinitialisation.""" self.is_closing = True # Marquer comme fermé self.close() def closeEvent(self, event): """Empêche la réinitialisation du texte après la fermeture.""" if not self.is_closing: event.ignore() else: super().closeEvent(event) def update_report_status(self, status): """Met à jour le texte dans QTextEdit sans réinitialisation indésirable.""" if not self.is_closing: self.text_edit.setText(status) def format_print_document(self, content): """Formate le document pour l'impression avec un en-tête et mise en page.""" self.print_document.clear() cursor = QTextCursor(self.print_document) # Format pour l'en-tête header_format = QTextCharFormat() header_format.setFont(QFont("Arial", 14, QFont.Bold)) # En-tête avec date et titre cursor.insertText("RAPPORT DE TRAITEMENT\n", header_format) cursor.insertText(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}\n\n") # Contenu principal cursor.insertText(content) def preview_report(self): """Affiche un aperçu avant impression.""" try: printer = QPrinter(QPrinter.HighResolution) printer.setPageSize(QPrinter.A4) preview_dialog = QPrintPreviewDialog(printer, self) preview_dialog.setWindowTitle("Aperçu avant impression") preview_dialog.paintRequested.connect(self.handle_print_preview) preview_dialog.exec_() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'aperçu : {str(e)}") print(f"Erreur détaillée: {str(e)}") # Pour le débogage def handle_print_preview(self, printer): """Gère l'affichage de l'aperçu avec le texte formaté.""" try: document = QTextDocument() document.setDefaultFont(QFont("Arial", 12)) # Création du contenu formaté content = "" content += "RAPPORT DE TRAITEMENT\n" content += "=" * 50 + "\n\n" if not hasattr(self, 'text_edit') or not self.text_edit.toPlainText(): content += "Aucune donnée de traitement n'a été enregistrée.\n" else: text = self.text_edit.toPlainText() # Formater le texte pour l'impression lines = text.split('\n') for line in lines: content += f"{line}\n" # Définir le contenu du document document.setPlainText(content) # Configurer les marges et la mise en page margin = 20 fmt = document.rootFrame().frameFormat() fmt.setMargin(margin) document.rootFrame().setFrameFormat(fmt) # Imprimer le document document.print_(printer) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la génération de l'aperçu : {str(e)}") print(f"Erreur détaillée: {str(e)}") # Pour le débogage def print_report(self): """Imprime le rapport avec le format texte correct.""" try: printer = QPrinter(QPrinter.HighResolution) printer.setPageSize(QPrinter.A4) printer.setPageMargins(20, 20, 20, 20, QPrinter.Millimeter) print_dialog = QPrintDialog(printer, self) print_dialog.setWindowTitle("Imprimer le rapport") if print_dialog.exec_() == QPrintDialog.Accepted: # Utiliser la même méthode que pour l'aperçu document = QTextDocument() document.setDefaultFont(QFont("Arial", 12)) # Création du contenu formaté comme dans handle_print_preview content = "" content += "RAPPORT DE TRAITEMENT\n" content += "=" * 50 + "\n\n" if not hasattr(self, 'text_edit') or not self.text_edit.toPlainText(): content += "Aucune donnée de traitement n'a été enregistrée.\n" else: text = self.text_edit.toPlainText() # Formater le texte pour l'impression lines = text.split('\n') for line in lines: content += f"{line}\n" # Définir le contenu du document document.setPlainText(content) # Configurer les marges et la mise en page margin = 20 fmt = document.rootFrame().frameFormat() fmt.setMargin(margin) document.rootFrame().setFrameFormat(fmt) # Imprimer le document document.print_(printer) QMessageBox.information(self, "Succès", "Impression envoyée avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'impression : {str(e)}") def save_report(self): """Sauvegarde le rapport dans un fichier.""" file_path, _ = QFileDialog.getSaveFileName( self, "Enregistrer le rapport", f"rapport_{datetime.now().strftime('%Y%m%d_%H%M')}.txt", "Fichiers texte (*.txt);;Tous les fichiers (*.*)" ) if file_path: try: with open(file_path, 'w', encoding='utf-8') as f: f.write(self.text_edit.toPlainText()) QMessageBox.information(self, "Succès", "Rapport enregistré avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'enregistrement : {str(e)}") class RangeSlider(QWidget): valueChanged = pyqtSignal(int, int) def __init__(self, min_value=5, max_value=40, parent=None): super().__init__(parent) self.min_value = min_value self.max_value = max_value self._min_pos = min_value self._max_pos = max_value self.setMinimumHeight(35) # Hauteur fixe pour tous les sliders self.setMinimumWidth(200) self.moving = "none" self.grab_padding = 10 # Zone de capture plus large # Style configuration self.groove_color = QColor("#34495e") self.filled_color = QColor("#2980b9") self.handle_color = QColor("#03699c") self.handle_border_color = QColor("#008CBA") # Dimensions uniformes self.groove_height = 4 self.handle_width = 12 self.handle_height = 12 # Labels pour afficher les valeurs self.layout = QHBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.min_label = QLabel(str(self._min_pos)) self.max_label = QLabel(str(self._max_pos)) self.min_label.setStyleSheet("color: white; font-size: 10px;") self.max_label.setStyleSheet("color: white; font-size: 10px;") self.min_label.setFixedWidth(25) self.max_label.setFixedWidth(25) self.min_label.setAlignment(Qt.AlignCenter) self.max_label.setAlignment(Qt.AlignCenter) self.layout.addStretch() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: width = self.width() - 2 * self.handle_width offset = self.handle_width # Convertir les positions en pixels left_x = (self._min_pos - self.min_value) * width / (self.max_value - self.min_value) + offset right_x = (self._max_pos - self.min_value) * width / (self.max_value - self.min_value) + offset click_x = event.pos().x() # Distance aux poignées dist_to_left = abs(click_x - left_x) dist_to_right = abs(click_x - right_x) # Sélectionner la poignée la plus proche si dans la zone de capture if dist_to_left <= self.grab_padding or dist_to_right <= self.grab_padding: self.moving = "min" if dist_to_left < dist_to_right else "max" self.mouseMoveEvent(event) def mouseMoveEvent(self, event): if self.moving != "none": width = self.width() - 2 * self.handle_width offset = self.handle_width pixel_pos = event.pos().x() - offset # Convertir la position en pixel en valeur avec limites value_range = self.max_value - self.min_value value = round(self.min_value + (pixel_pos * value_range) / width) # Appliquer les limites pour chaque poignée if self.moving == "min": # La poignée min ne peut pas dépasser max-1 value = max(self.min_value, min(value, self._max_pos - 1)) self._min_pos = value self.min_label.setText(str(value)) else: # La poignée max ne peut pas être inférieure à min+1 value = max(self._min_pos + 1, min(value, self.max_value)) self._max_pos = value self.max_label.setText(str(value)) self.valueChanged.emit(self._min_pos, self._max_pos) self.update() def mouseReleaseEvent(self, event): self.moving = "none" def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) width = self.width() - 2 * self.handle_width height = self.height() offset = self.handle_width groove_y = height // 2 - self.groove_height // 2 # Rainure principale groove = QRect(offset, groove_y, width, self.groove_height) painter.setPen(Qt.NoPen) painter.setBrush(self.groove_color) painter.drawRoundedRect(groove, 2, 2) # Calculer les positions left_x = (self._min_pos - self.min_value) * width / (self.max_value - self.min_value) + offset right_x = (self._max_pos - self.min_value) * width / (self.max_value - self.min_value) + offset # Partie remplie filled = QRect(int(left_x), groove_y, int(right_x - left_x), self.groove_height) painter.setBrush(self.filled_color) painter.drawRoundedRect(filled, 2, 2) # Poignées handle_y = height // 2 - self.handle_height // 2 for x in [left_x, right_x]: handle = QRect(int(x - self.handle_width / 2), handle_y, self.handle_width, self.handle_height) painter.setPen(QPen(self.handle_border_color, 1)) painter.setBrush(self.handle_color) painter.drawEllipse(handle) def setValues(self, min_pos, max_pos, emit_signal=True): """ Met à jour les positions min et max du slider. :param min_pos: Position minimum :param max_pos: Position maximum :param emit_signal: Si True, émet le signal valueChanged ; sinon, met juste à jour sans signal """ # Assurer que les valeurs restent dans les limites self._min_pos = max(self.min_value, min(min_pos, self.max_value - 1)) self._max_pos = max(self._min_pos + 1, min(max_pos, self.max_value)) # Mettre à jour les labels self.min_label.setText(str(self._min_pos)) self.max_label.setText(str(self._max_pos)) # Redessiner le slider self.update() # Émettre le signal si demandé if emit_signal: self.valueChanged.emit(self._min_pos, self._max_pos) def values(self): return self._min_pos, self._max_pos class DoubleSlider(QWidget): valueChanged = pyqtSignal(int, int) def __init__(self, parent=None): super().__init__(parent) self.setupUi() def setupUi(self): layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) self.slider = RangeSlider(5, 40) self.slider.valueChanged.connect(self.valueChanged.emit) layout.addWidget(self.slider) # Création des attributs compatibles avec l'ancien code self.min_slider = self.ProxySlider(self.slider, "min") self.max_slider = self.ProxySlider(self.slider, "max") def values(self): return self.slider.values() def setValues(self, min_value, max_value, emit_signal=True): self.slider.setValues(min_value, max_value, emit_signal=emit_signal) # Classe proxy pour maintenir la compatibilité class ProxySlider: def __init__(self, range_slider, slider_type): self.range_slider = range_slider self.slider_type = slider_type def setMinimum(self, value): if self.slider_type == "min": self.range_slider.min_value = value def setMaximum(self, value): if self.slider_type == "max": self.range_slider.max_value = value def setValue(self, value): if self.slider_type == "min": self.range_slider.setValues(value, self.range_slider._max_pos) else: self.range_slider.setValues(self.range_slider._min_pos, value) def value(self): return self.range_slider._min_pos if self.slider_type == "min" else self.range_slider._max_pos class TaskSchedulerDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.setWindowTitle("Planificateur de tâches") self.setMinimumSize(1000, 900) self.schedules = [] self.setup_ui() self.load_schedules() # Chargement initial self.sync_profiles_with_main() def sync_profiles_with_main(self): """Synchronise les profils avec la fenêtre principale""" try: if not hasattr(self, 'profile_combo'): logger.error("profile_combo n'est pas initialisé") return self.profile_combo.clear() # Obtenir les profils depuis la fenêtre principale if hasattr(self.parent, 'profile_list'): profiles = [] # La fenêtre principale utilise QComboBox pour profile_list for i in range(self.parent.profile_list.count()): profile_text = self.parent.profile_list.itemText(i) # Utiliser itemText au lieu de item if profile_text: profiles.append(profile_text) if profiles: self.profile_combo.addItems(profiles) self.schedule_button.setEnabled(True) logger.info(f"{len(profiles)} profils synchronisés depuis la gestion des profils") else: self.profile_combo.addItem("Aucun profil disponible") self.schedule_button.setEnabled(False) logger.warning("Aucun profil trouvé dans la gestion des profils") else: logger.error("La liste des profils n'est pas accessible dans la fenêtre principale") self.profile_combo.addItem("Erreur de synchronisation") self.schedule_button.setEnabled(False) except Exception as e: logger.error(f"Erreur lors de la synchronisation des profils: {str(e)}") self.profile_combo.addItem("Erreur de chargement") self.schedule_button.setEnabled(False) def sync_toggle_with_checkbox(self, state): if hasattr(self.parent, 'report_toggle'): self.parent.report_toggle.setChecked(state == Qt.Checked) def sync_checkbox_with_toggle(self, checked): if hasattr(self, 'report_checkbox'): self.report_checkbox.setChecked(checked) def setup_ui(self): # Layout principal layout = QVBoxLayout(self) # Zone supérieure pour le calendrier et les contrôles top_section = QHBoxLayout() # Calendrier à gauche calendar_widget = QWidget() calendar_layout = QVBoxLayout(calendar_widget) self.calendar = QCalendarWidget() self.calendar.setGridVisible(True) self.calendar.clicked.connect(self.date_selected) calendar_layout.addWidget(self.calendar) top_section.addWidget(calendar_widget) # Contrôles à droite controls_widget = QWidget() controls_layout = QVBoxLayout(controls_widget) # Sélection du profil profile_label = QLabel("Profil:") self.profile_combo = QComboBox() self.profile_combo.setMinimumWidth(200) controls_layout.addWidget(profile_label) controls_layout.addWidget(self.profile_combo) # Sélection de l'heure time_label = QLabel("Heure:") self.time_picker = CustomTimePicker() controls_layout.addWidget(time_label) controls_layout.addWidget(self.time_picker) # Type de récurrence et nombre de mails sur la même ligne recurrence_container = QWidget() recurrence_layout = QHBoxLayout(recurrence_container) recurrence_layout.setContentsMargins(0, 0, 0, 0) # Type de récurrence recurrence_label = QLabel("Récurrence:") self.recurrence_combo = QComboBox() self.recurrence_combo.addItems(["Une fois", "Horaire", "Quotidien", "Hebdomadaire", "Mensuel"]) recurrence_layout.addWidget(recurrence_label) recurrence_layout.addWidget(self.recurrence_combo) # Espacement recurrence_layout.addSpacing(20) # Nombre de mails mail_count_label = QLabel("Nbre de mails:") self.mail_count_spinbox = QSpinBox() self.mail_count_spinbox.setMinimum(1) self.mail_count_spinbox.setMaximum(999) self.mail_count_spinbox.setValue(1) self.mail_count_spinbox.setStyleSheet(""" QSpinBox { background-color: #03699c; color: white; padding: 5px; border-radius: 4px; } """) recurrence_layout.addWidget(mail_count_label) recurrence_layout.addWidget(self.mail_count_spinbox) controls_layout.addWidget(recurrence_container) # Case à cocher pour le rapport de traitement self.report_checkbox = QCheckBox("Générer un rapport de traitement") self.report_checkbox.setChecked(False) self.report_checkbox.setStyleSheet("color: white;") controls_layout.addWidget(self.report_checkbox) # Zone des boutons buttons_widget = QWidget() buttons_layout = QVBoxLayout(buttons_widget) buttons_layout.setSpacing(10) # Espacement entre les boutons # Bouton Planifier self.schedule_button = QPushButton("Planifier") self.schedule_button.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; margin-top: 10px; } QPushButton:hover { background-color: #007B9A; } """) self.schedule_button.clicked.connect(self.add_schedule) buttons_layout.addWidget(self.schedule_button) # Bouton Journal self.journal_button = QPushButton("📋 Journal") self.journal_button.setStyleSheet(""" QPushButton { background-color: #2ecc71; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; margin-top: 5px; } QPushButton:hover { background-color: #27ae60; } """) self.journal_button.clicked.connect(self.show_journal) buttons_layout.addWidget(self.journal_button) # Bouton Annuler tâche self.cancel_button = QPushButton("Annuler tâche") self.cancel_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; margin-top: 5px; } QPushButton:hover { background-color: #d32f2f; } """) self.cancel_button.clicked.connect(self.cancel_task) buttons_layout.addWidget(self.cancel_button) # Ajouter les boutons au layout des contrôles controls_layout.addWidget(buttons_widget) controls_layout.addStretch() # Ajouter les contrôles à la section supérieure top_section.addWidget(controls_widget) # Ajouter la section supérieure au layout principal layout.addLayout(top_section) # Liste des tâches planifiées tasks_group = QGroupBox("Tâches planifiées") tasks_layout = QVBoxLayout() # Liste déroulante des tâches self.tasks_combo = QComboBox() self.tasks_combo.setStyleSheet(""" QComboBox { background-color: #03699c; color: white; padding: 5px; border-radius: 4px; margin-bottom: 5px; } """) tasks_layout.addWidget(self.tasks_combo) # Zone de défilement scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setFixedHeight(200) scroll_area.setStyleSheet(""" QScrollArea { border: none; background-color: transparent; } QScrollBar:vertical { background-color: #2c3e50; width: 12px; } QScrollBar::handle:vertical { background-color: #34495e; border-radius: 6px; } """) container = QWidget() container_layout = QVBoxLayout(container) container_layout.setSpacing(0) container_layout.setContentsMargins(0, 0, 0, 0) self.tasks_label = QLabel() self.tasks_label.setStyleSheet(""" QLabel { background-color: white; color: black; padding: 5px; border-radius: 4px; } """) self.tasks_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.tasks_label.setWordWrap(True) container_layout.addWidget(self.tasks_label) container_layout.addStretch() scroll_area.setWidget(container) tasks_layout.addWidget(scroll_area) tasks_group.setLayout(tasks_layout) layout.addWidget(tasks_group) self.report_checkbox.stateChanged.connect(self.sync_toggle_with_checkbox) if hasattr(self.parent, 'report_toggle'): self.parent.report_toggle.toggled.connect(self.sync_checkbox_with_toggle) def show_journal(self): """Affiche le journal des tâches""" try: journal_dialog = TaskJournalDialog(self) journal_dialog.exec_() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture du journal : {str(e)}") logger.error(f"Erreur lors de l'ouverture du journal : {str(e)}") def update_tasks_combo(self): """Met à jour la liste déroulante des tâches""" try: self.tasks_combo.clear() # Recharger les tâches depuis le fichier schedules_file = os.path.join(r"C:\autolook", "schedules.json") if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: self.schedules = json.load(f) # Si pas de tâches, désactiver le bouton et sortir de la fonction if not self.schedules: self.cancel_button.setEnabled(False) return # Le return est maintenant dans une fonction current_time = datetime.now() for idx, schedule in enumerate(self.schedules): task_date = datetime.fromisoformat(schedule['date']) task_time = datetime.strptime(schedule['time'], '%H:%M:%S').time() task_datetime = datetime.combine(task_date, task_time) formatted_date = task_date.strftime('%d/%m/%Y') formatted_time = task_time.strftime('%H:%M') status = "À venir" if task_datetime > current_time else "Passée" task_text = f"{schedule['profile']} - {formatted_date} {formatted_time} - {schedule['recurrence']} ({status})" self.tasks_combo.addItem(task_text, idx) # Activer le bouton d'annulation s'il y a des tâches self.cancel_button.setEnabled(self.tasks_combo.count() > 0) except Exception as e: logger.error(f"Erreur lors de la mise à jour de la liste des tâches : {str(e)}") def cancel_task(self): """Annule une tâche planifiée""" try: # Recharger les tâches depuis le fichier schedules_file = os.path.join(r"C:\autolook", "schedules.json") if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: self.schedules = json.load(f) if not self.schedules: QMessageBox.warning(self, "Attention", "Aucune tâche à annuler") return current_index = self.tasks_combo.currentIndex() if current_index < 0: QMessageBox.warning(self, "Attention", "Veuillez sélectionner une tâche à annuler") return original_index = self.tasks_combo.itemData(current_index) selected_task = self.schedules[original_index] reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment annuler la tâche suivante ?\n\n" f"Profil : {selected_task['profile']}\n" f"Date : {datetime.fromisoformat(selected_task['date']).strftime('%d/%m/%Y')}\n" f"Heure : {selected_task['time']}\n" f"Récurrence : {selected_task['recurrence']}", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: # Supprimer la tâche self.schedules.pop(original_index) # Sauvegarder les modifications with open(schedules_file, 'w') as f: json.dump(self.schedules, f, indent=4) # Mettre à jour l'interface self.update_tasks_combo() self.update_tasks_display() # Mettre à jour l'état du bouton self.cancel_button.setEnabled(bool(self.schedules)) QMessageBox.information(self, "Succès", "La tâche a été annulée avec succès") # Informer le gestionnaire de tâches principal if hasattr(self.parent, 'task_manager'): self.parent.task_manager.check_scheduled_tasks() except Exception as e: logger.error(f"Erreur lors de l'annulation de la tâche : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'annulation : {str(e)}") def date_selected(self, date): self.selected_date = date self.update_tasks_display() def add_schedule(self): try: logger.info("Début de la procédure de planification") # Récupérer les informations de planification profile = self.profile_combo.currentText().replace("⭐ ", "") selected_date = self.calendar.selectedDate().toPyDate() selected_time = self.time_picker.getTime() recurrence = self.recurrence_combo.currentText() mail_count = self.mail_count_spinbox.value() generate_report = self.report_checkbox.isChecked() logger.info( f"Informations de planification - Profil: {profile}, " f"Date: {selected_date}, Heure: {selected_time}, " f"Récurrence: {recurrence}, Mail count: {mail_count}") if not profile: logger.warning("Aucun profil sélectionné") QMessageBox.warning(self, "Erreur", "Veuillez sélectionner un profil") return # Créer le nouveau schedule new_schedule = { 'profile': profile, 'date': selected_date.isoformat(), 'time': selected_time, 'recurrence': recurrence, 'mail_count': mail_count, 'generate_report': generate_report } logger.info(f"Nouveau schedule créé: {json.dumps(new_schedule, indent=2)}") # Vérifier les conflits if self.check_schedule_conflicts(new_schedule): logger.warning("Conflit détecté avec une tâche existante") QMessageBox.warning(self, "Conflit", "Une tâche est déjà planifiée à cette date et heure") return # Sauvegarder la tâche logger.info("Sauvegarde de la tâche principale") self.save_schedule(new_schedule) # Créer les tâches récurrentes si nécessaire if recurrence != "Une fois": logger.info(f"Création des tâches récurrentes pour le type: {recurrence}") self.create_recurring_tasks(new_schedule) # Mise à jour de l'interface logger.info("Mise à jour de l'affichage des tâches") self.update_tasks_display() self.update_tasks_combo() # Notification de succès success_message = (f"Tâche planifiée avec succès pour le {selected_date} " f"à {selected_time}") logger.info(success_message) QMessageBox.information(self, "Succès", success_message) # Après avoir sauvegardé la nouvelle tâche if hasattr(self.parent, 'outlook_widget'): self.parent.outlook_widget.update_next_task_display() except Exception as e: error_msg = f"Erreur lors de la planification : {str(e)}" logger.error(error_msg, exc_info=True) QMessageBox.critical(self, "Erreur", error_msg) def check_schedule_conflicts(self, new_schedule): """Vérifie s'il y a des conflits avec les tâches existantes""" new_datetime = datetime.fromisoformat(f"{new_schedule['date']}T{new_schedule['time']}") for schedule in self.schedules: schedule_datetime = datetime.fromisoformat(f"{schedule['date']}T{schedule['time']}") # On considère qu'il y a conflit si deux tâches sont à moins de 5 minutes d'intervalle if abs((schedule_datetime - new_datetime).total_seconds()) < 300: # 5 minutes return True return False def create_recurring_tasks(self, base_schedule): """Crée les occurrences futures pour une tâche récurrente""" try: recurrence_type = base_schedule['recurrence'] base_date = datetime.fromisoformat(base_schedule['date']) base_time = datetime.strptime(base_schedule['time'], '%H:%M:%S').time() base_datetime = datetime.combine(base_date, base_time) new_schedules = [] # Déterminer la période de récurrence if recurrence_type == "Horaire": # Pour la récurrence horaire, créer des tâches sur 24 heures seulement end_datetime = base_datetime + timedelta(hours=24) current_datetime = base_datetime + timedelta(hours=1) # Commencer à l'heure suivante else: # Pour les autres types de récurrence, créer sur 6 mois end_datetime = base_datetime + timedelta(days=180) current_datetime = base_datetime # Créer les occurrences récurrentes while current_datetime <= end_datetime: if recurrence_type == "Horaire": # Avancer d'une heure next_datetime = current_datetime + timedelta(hours=1) elif recurrence_type == "Quotidien": # Avancer d'un jour next_datetime = current_datetime + timedelta(days=1) elif recurrence_type == "Hebdomadaire": # Avancer d'une semaine next_datetime = current_datetime + timedelta(weeks=1) elif recurrence_type == "Mensuel": # Gestion correcte du passage au mois suivant year = current_datetime.year month = current_datetime.month + 1 if month > 12: month = 1 year += 1 # Créer la date du mois suivant en conservant l'heure next_datetime = current_datetime.replace(year=year, month=month) # Créer le nouveau schedule new_schedule = base_schedule.copy() new_schedule['date'] = next_datetime.date().isoformat() new_schedule['time'] = next_datetime.time().strftime('%H:%M:%S') new_schedules.append(new_schedule) # Préparer pour la prochaine itération current_datetime = next_datetime # Sauvegarde des nouvelles occurrences for schedule in new_schedules: if not self.check_schedule_conflicts(schedule): self.save_schedule(schedule) except Exception as e: logger.error(f"Erreur lors de la création des tâches récurrentes : {str(e)}") # Afficher l'erreur à l'utilisateur QMessageBox.critical(self, "Erreur", f"Erreur lors de la création des tâches récurrentes : {str(e)}") def save_schedule(self, schedule): """Sauvegarde une tâche planifiée""" schedules_file = os.path.join(r"C:\autolook", "schedules.json") schedules = [] if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: schedules = json.load(f) schedules.append(schedule) with open(schedules_file, 'w') as f: json.dump(schedules, f, indent=4) def load_schedules(self): """Charge toutes les tâches planifiées, qu'elles soient passées ou à venir""" try: schedules_file = os.path.join(r"C:\autolook", "schedules.json") if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: self.schedules = json.load(f) # Mise à jour des anciennes tâches qui n'ont pas de nombre de mails for schedule in self.schedules: if 'mail_count' not in schedule: schedule['mail_count'] = 1 # Sauvegarde des mises à jour with open(schedules_file, 'w') as f: json.dump(self.schedules, f, indent=4) else: self.schedules = [] self.update_tasks_display() self.update_tasks_combo() # Activer le bouton d'annulation s'il y a des tâches self.cancel_button.setEnabled(bool(self.schedules)) logger.info(f"Nombre total de tâches chargées : {len(self.schedules)}") except Exception as e: logger.error(f"Erreur lors du chargement des tâches : {str(e)}") self.schedules = [] def format_task_display(self, schedule, task_datetime, status, bg_color, time_remaining): """Formate l'affichage d'une tâche""" return f"""
Profil: {schedule['profile']}
Date: {task_datetime.strftime('%d/%m/%Y %H:%M')}
Récurrence: {schedule['recurrence']}
Nombre de mails: {schedule.get('mail_count', 1)}
Statut: {status} {time_remaining}
""" def update_tasks_display(self): """Met à jour l'affichage détaillé des tâches""" try: # Recharger les tâches depuis le fichier pour avoir les données les plus récentes schedules_file = os.path.join(r"C:\autolook", "schedules.json") if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: self.schedules = json.load(f) if not self.schedules: self.tasks_label.setText("

Aucune tâche planifiée

") return # Tri des tâches par date/heure sorted_tasks = sorted( self.schedules, key=lambda x: datetime.fromisoformat(f"{x['date']}T{x['time']}") ) # Création du HTML pour toutes les tâches tasks_html = ["
"] current_time = datetime.now() for schedule in sorted_tasks: try: task_datetime = datetime.fromisoformat(f"{schedule['date']}T{schedule['time']}") status = "À venir" if task_datetime > current_time else "Passée" # Calcul du temps restant time_remaining = "" if status == "À venir": delta = task_datetime - current_time days = delta.days hours = delta.seconds // 3600 minutes = (delta.seconds % 3600) // 60 time_remaining = f"(Dans {days}j {hours}h {minutes}m)" # Calcul du temps de traitement estimé mail_count = schedule.get('mail_count', 1) # Récupérer les temps de pause depuis le profil profile_path = os.path.join(r"C:\autolook\profiles", f"{schedule['profile']}.json") display_time_min = 15 # Valeurs par défaut display_time_max = 25 between_pages_min = 5 between_pages_max = 10 if os.path.exists(profile_path): with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) if 'settings' in profile_data: settings = profile_data['settings'] if 'pause_time_range' in settings: display_time_min = settings['pause_time_range']['min'] display_time_max = settings['pause_time_range']['max'] if 'between_pages_time_range' in settings: between_pages_min = settings['between_pages_time_range']['min'] between_pages_max = settings['between_pages_time_range']['max'] # Calcul des estimations avg_display_time = (display_time_min + display_time_max) / 2 avg_between_time = (between_pages_min + between_pages_max) / 2 # Temps total estimé en secondes total_time_sec = mail_count * (avg_display_time + avg_between_time) # Conversion en heures, minutes est_hours = int(total_time_sec // 3600) est_minutes = int((total_time_sec % 3600) // 60) # Création de la chaîne d'estimation est_time = "" if est_hours > 0: est_time += f"{est_hours}h " est_time += f"{est_minutes}min" bg_color = "#e3f2fd" if status == "À venir" else "#ffebee" # Ajouter ce code juste après la ligne qui calcule la variable est_time special_note = "" if schedule['recurrence'] == "Horaire": special_note = "Note: Cette tâche se répète toutes les heures sur 24h
" # Puis modifier le task_html pour inclure cette note spéciale: task_html = f"""
Profil: {schedule['profile']}
Date: {task_datetime.strftime('%d/%m/%Y %H:%M')}
Récurrence: {schedule['recurrence']}
Nombre de mails: {mail_count}
Temps de traitement estimé: {est_time}
{special_note} Paramètres: Affichage {display_time_min}-{display_time_max}s, Pause {between_pages_min}-{between_pages_max}s
Rapport de traitement: {"Demandé" if schedule.get('generate_report', False) else "Non demandé"}
Statut: {status} {time_remaining}
""" tasks_html.append(task_html) except Exception as e: logger.error(f"Erreur lors de l'affichage d'une tâche : {str(e)}") continue tasks_html.append("
") final_html = "\n".join(tasks_html) self.tasks_label.setText(final_html) self.tasks_label.setTextFormat(Qt.RichText) except Exception as e: logger.error(f"Erreur lors de la mise à jour de l'affichage : {str(e)}") class CustomTimePicker(QWidget): timeChanged = pyqtSignal(QTime) def __init__(self, parent=None): super().__init__(parent) self.use_24h = False # Format par défaut: 12h self.current_time = QTime.currentTime() self.temp_time = self.current_time # Initialiser temp_time ici self.initUI() def initUI(self): main_layout = QVBoxLayout(self) main_layout.setSpacing(5) main_layout.setContentsMargins(5, 5, 5, 5) # En-tête avec format 12h/24h header_layout = QHBoxLayout() self.format_toggle = QPushButton("Format 12h") self.format_toggle.setFixedSize(80, 25) self.format_toggle.setCheckable(True) self.format_toggle.clicked.connect(self.toggleTimeFormat) header_layout.addStretch() header_layout.addWidget(self.format_toggle) main_layout.addLayout(header_layout) # Container pour les sélecteurs time_container = QWidget() time_layout = QHBoxLayout(time_container) time_layout.setAlignment(Qt.AlignCenter) # Sélecteur d'heures hours_group = QGroupBox("Heures") hours_layout = QVBoxLayout() hours_layout.setAlignment(Qt.AlignCenter) # Bouton Up pour les heures self.hour_up = QPushButton("▲") self.hour_up.setFixedSize(45, 20) self.hour_up.clicked.connect(self.incrementHour) # Affichage de l'heure self.hour_display = QLabel("00") self.hour_display.setAlignment(Qt.AlignCenter) self.hour_display.setFixedSize(45, 30) # Bouton Down pour les heures self.hour_down = QPushButton("▼") self.hour_down.setFixedSize(45, 20) self.hour_down.clicked.connect(self.decrementHour) hours_layout.addWidget(self.hour_up) hours_layout.addWidget(self.hour_display) hours_layout.addWidget(self.hour_down) hours_group.setLayout(hours_layout) # Sélecteur de minutes minutes_group = QGroupBox("Minutes") minutes_layout = QVBoxLayout() minutes_layout.setAlignment(Qt.AlignCenter) # Bouton Up pour les minutes self.minute_up = QPushButton("▲") self.minute_up.setFixedSize(45, 20) self.minute_up.clicked.connect(self.incrementMinute) # Affichage des minutes self.minute_display = QLabel("00") self.minute_display.setAlignment(Qt.AlignCenter) self.minute_display.setFixedSize(45, 30) # Bouton Down pour les minutes self.minute_down = QPushButton("▼") self.minute_down.setFixedSize(45, 20) self.minute_down.clicked.connect(self.decrementMinute) minutes_layout.addWidget(self.minute_up) minutes_layout.addWidget(self.minute_display) minutes_layout.addWidget(self.minute_down) minutes_group.setLayout(minutes_layout) time_layout.addWidget(hours_group) time_layout.addWidget(minutes_group) # AM/PM selector self.ampm_widget = QGroupBox() ampm_layout = QVBoxLayout() ampm_layout.setAlignment(Qt.AlignCenter) self.am_button = QPushButton("AM") self.pm_button = QPushButton("PM") self.am_button.setCheckable(True) self.pm_button.setCheckable(True) self.am_button.setChecked(True) self.am_button.clicked.connect(lambda: self.setPeriod("AM")) self.pm_button.clicked.connect(lambda: self.setPeriod("PM")) for btn in [self.am_button, self.pm_button]: btn.setFixedSize(45, 25) ampm_layout.addWidget(btn) self.ampm_widget.setLayout(ampm_layout) time_layout.addWidget(self.ampm_widget) time_layout.addStretch() main_layout.addWidget(time_container) self.updateDisplays() # Style self.setStyleSheet(""" QGroupBox { color: white; border: 1px solid #2c3e50; border-radius: 3px; margin-top: 8px; padding-top: 8px; } QGroupBox::title { subcontrol-position: top center; padding: 0 3px; } QPushButton { background-color: #008CBA; color: white; border: none; border-radius: 2px; } QPushButton:hover { background-color: #007B9A; } QPushButton:checked { background-color: #005F7F; } QLabel { background-color: #03699c; color: white; font-size: 14px; font-weight: bold; border-radius: 2px; } """) # Appeler cette fonction après l'initialisation de l'interface utilisateur #setup_system_tools(self) def incrementHour(self): hour = int(self.hour_display.text()) if self.use_24h: hour = (hour + 1) % 24 else: hour = (hour % 12) + 1 self.hour_display.setText(f"{hour:02d}") self.updateTime() def decrementHour(self): hour = int(self.hour_display.text()) if self.use_24h: hour = (hour - 1) % 24 else: hour = ((hour - 2) % 12) + 1 self.hour_display.setText(f"{hour:02d}") self.updateTime() def incrementMinute(self): minute = int(self.minute_display.text()) minute = (minute + 1) % 60 self.minute_display.setText(f"{minute:02d}") self.updateTime() def decrementMinute(self): minute = int(self.minute_display.text()) minute = (minute - 1) % 60 self.minute_display.setText(f"{minute:02d}") self.updateTime() def updateTime(self): hour = int(self.hour_display.text()) minute = int(self.minute_display.text()) if not self.use_24h and self.pm_button.isChecked() and hour < 12: hour += 12 elif not self.use_24h and self.am_button.isChecked() and hour == 12: hour = 0 self.temp_time = QTime(hour, minute) self.timeChanged.emit(self.temp_time) def setPeriod(self, period): if period == "AM": self.am_button.setChecked(True) self.pm_button.setChecked(False) else: self.am_button.setChecked(False) self.pm_button.setChecked(True) self.updateTime() def toggleTimeFormat(self): self.use_24h = not self.use_24h self.format_toggle.setText(f"Format {24 if self.use_24h else 12}h") self.ampm_widget.setVisible(not self.use_24h) self.updateDisplays() def updateDisplays(self): if self.use_24h: self.hour_display.setText(f"{self.temp_time.hour():02d}") else: hour = self.temp_time.hour() if hour == 0: hour = 12 elif hour > 12: hour -= 12 self.hour_display.setText(f"{hour:02d}") self.minute_display.setText(f"{self.temp_time.minute():02d}") # Update AM/PM if not self.use_24h: if self.temp_time.hour() >= 12: self.setPeriod("PM") else: self.setPeriod("AM") def getTime(self): """Retourne l'heure au format QTime""" hour = int(self.hour_display.text()) minute = int(self.minute_display.text()) # Conversion 12h -> 24h si nécessaire if not self.use_24h and self.pm_button.isChecked(): if hour != 12: hour += 12 elif not self.use_24h and self.am_button.isChecked() and hour == 12: hour = 0 return f"{hour:02d}:{minute:02d}:00" # Format HH:MM:SS def setTime(self, time): self.temp_time = time self.updateDisplays() class TaskManager(QThread): task_started = pyqtSignal(dict) task_completed = pyqtSignal(dict) task_error = pyqtSignal(dict, str) load_profile_requested = pyqtSignal(str) profile_load_requested = pyqtSignal(str) def __init__(self, main_window): super().__init__() self.main_window = main_window self.running = True self.last_check = {} # Pour éviter la double exécution def run(self): while self.running: try: settings = QSettings("Snoown2k", "MyApp") enabled = settings.value("scheduler/enable_verification", True, type=bool) interval = settings.value("scheduler/interval_seconds", 30, type=int) if enabled: self.check_scheduled_tasks() time.sleep(interval) except Exception as e: logger.error(f"Erreur dans la boucle principale du TaskManager: {str(e)}") continue def check_scheduled_tasks(self): """Vérifie et exécute les tâches planifiées, gère l'historique""" try: schedules_file = os.path.join(r"C:\autolook", "schedules.json") history_file = os.path.join(r"C:\autolook", "completed_tasks.json") # Vérifier existence du fichier des tâches if not os.path.exists(schedules_file): logger.info("Aucune tâche planifiée trouvée") return # Chargement des tâches planifiées with open(schedules_file, 'r') as f: schedules = json.load(f) current_time = datetime.now().replace(second=0, microsecond=0) updated_schedules = [] # Pour les tâches à conserver tasks_to_execute = [] # Pour les tâches à exécuter maintenant logger.info(f"Vérification des tâches planifiées à {current_time}") # Chargement de l'historique existant completed_tasks = [] if os.path.exists(history_file): with open(history_file, 'r') as f: completed_tasks = json.load(f) # Analyse de chaque tâche for schedule in schedules: try: schedule_date = datetime.strptime(schedule['date'], '%Y-%m-%d').date() schedule_time = datetime.strptime(schedule['time'], '%H:%M:%S').time().replace(second=0) scheduled_datetime = datetime.combine(schedule_date, schedule_time) # Création d'une clé unique pour cette tâche task_key = f"{schedule['profile']}_{schedule_date}_{schedule_time.strftime('%H:%M')}" logger.info(f"Vérification tâche: {task_key}") logger.info(f"Heure planifiée: {scheduled_datetime}") logger.info(f"Heure actuelle: {current_time}") if scheduled_datetime == current_time: # Vérifier si la tâche n'a pas déjà été exécutée récemment if task_key not in self.last_check: logger.info(f"Tâche à exécuter trouvée: {schedule['profile']}") self.last_check[task_key] = current_time # Vérifier l'existence du profil profile_path = os.path.join(r"C:\autolook\profiles", f"{schedule['profile']}.json") if os.path.exists(profile_path): # Ajouter la tâche dans l'historique schedule_copy = schedule.copy() schedule_copy['executed_at'] = current_time.isoformat() schedule_copy['status'] = 'completed' completed_tasks.append(schedule_copy) # Ajouter aux tâches à exécuter tasks_to_execute.append(schedule) # Pour les tâches récurrentes, créer la prochaine occurrence if schedule['recurrence'] != "Une fois": next_schedule = self.calculate_next_occurrence(schedule) if next_schedule: updated_schedules.append(next_schedule) logger.info(f"Prochaine occurrence créée pour: {task_key}") else: logger.error(f"Profil non trouvé: {profile_path}") continue elif scheduled_datetime > current_time: # Conserver les tâches futures updated_schedules.append(schedule) logger.info(f"Tâche conservée pour plus tard: {task_key}") else: # Tâche passée non exécutée, l'ajouter à l'historique comme manquée schedule_copy = schedule.copy() schedule_copy['executed_at'] = current_time.isoformat() schedule_copy['status'] = 'missed' completed_tasks.append(schedule_copy) logger.info(f"Tâche passée marquée comme manquée: {task_key}") except Exception as e: logger.error(f"Erreur lors du traitement de la tâche : {str(e)}") continue # Nettoyer les anciennes entrées du last_check self.clean_last_check() # Sauvegarder les tâches mises à jour with open(schedules_file, 'w') as f: json.dump(updated_schedules, f, indent=4) # Sauvegarder l'historique mis à jour with open(history_file, 'w') as f: json.dump(completed_tasks, f, indent=4) # Exécuter les tâches trouvées for task in tasks_to_execute: logger.info(f"Exécution de la tâche: {task['profile']}") self.execute_task(task) except Exception as e: logger.error(f"Erreur lors de la vérification des tâches planifiées : {str(e)}") def get_schedules(self): """Récupère toutes les tâches planifiées""" try: schedules_file = os.path.join(r"C:\autolook", "schedules.json") if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: return json.load(f) return [] except Exception as e: logger.error(f"Erreur lors de la récupération des tâches planifiées : {str(e)}") return [] def clean_last_check(self): """Nettoie les anciennes entrées de last_check""" current_time = datetime.now() obsolete_keys = [] for key, check_time in self.last_check.items(): if (current_time - check_time).total_seconds() > 3600: # 1 heure obsolete_keys.append(key) for key in obsolete_keys: del self.last_check[key] def calculate_next_occurrence(self, schedule): """Calcule la prochaine occurrence d'une tâche récurrente""" try: current_date = datetime.strptime(schedule['date'], '%Y-%m-%d').date() if schedule['recurrence'] == "Quotidien": next_date = current_date + timedelta(days=1) elif schedule['recurrence'] == "Hebdomadaire": next_date = current_date + timedelta(weeks=1) elif schedule['recurrence'] == "Mensuel": if current_date.month == 12: next_date = current_date.replace(year=current_date.year + 1, month=1) else: next_date = current_date.replace(month=current_date.month + 1) else: return None next_schedule = schedule.copy() next_schedule['date'] = next_date.strftime('%Y-%m-%d') return next_schedule except Exception as e: logger.error(f"Erreur lors du calcul de la prochaine occurrence : {str(e)}") return None def execute_task(self, schedule): try: self.task_started.emit(schedule) profile = schedule['profile'] mail_count = schedule.get('mail_count', 1) # NOUVEAU: Stocker le nombre d'emails dans un fichier temporaire task_params_file = os.path.join(r"C:\autolook", "current_task_params.json") with open(task_params_file, 'w') as f: json.dump({ 'mail_count': mail_count, 'profile': profile, 'generate_report': schedule.get('generate_report', False) }, f) logger.info(f"Paramètres de la tâche sauvegardés: {mail_count} emails pour le profil {profile}") # Charger le profil self.load_profile_requested.emit(profile) # Simulez l'exécution de la tâche (par exemple, un traitement) time.sleep(5) # Simulation du temps de traitement self.task_completed.emit(schedule) except Exception as e: self.task_error.emit(schedule, str(e)) @pyqtSlot(str) def handle_scheduled_profile_load(self, profile_name): """Slot pour charger un profil depuis le planificateur""" try: # Sélectionner le profil dans la liste index = self.profile_list.findText(profile_name) if index >= 0: self.profile_list.setCurrentIndex(index) # Charger le profil self.load_profile() except Exception as e: logger.error(f"Erreur lors du chargement planifié du profil : {str(e)}") def select_folder(self, outlook_widget, outlook_settings): """Sélectionne le dossier Outlook après un délai""" try: if 'folder' in outlook_settings: folder_name = outlook_settings['folder'].strip() for i in range(outlook_widget.folder_listbox.count()): item = outlook_widget.folder_listbox.item(i) if item.text().strip() == folder_name: outlook_widget.folder_listbox.setCurrentItem(item) if hasattr(outlook_widget, 'on_folder_selected'): outlook_widget.on_folder_selected() break except Exception as e: logger.error(f"Erreur lors de la sélection du dossier : {str(e)}") def stop(self): """Arrête le gestionnaire de tâches""" self.running = False self.wait() class AuthDialog(QDialog): def __init__(self, parent=None, checkbox=None): super().__init__(parent) self.checkbox = checkbox self.setWindowTitle("Authentification") self.setFixedSize(400, 300) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Champ URL layout.addWidget(QLabel("Lien URL :")) self.url_field = QLineEdit() self.url_field.setPlaceholderText("Entrez le lien URL") layout.addWidget(self.url_field) # Champ Identifiant layout.addWidget(QLabel("Identifiant :")) self.login_field = QLineEdit() self.login_field.setPlaceholderText("Entrez l'identifiant") layout.addWidget(self.login_field) # Champ Mot de Passe layout.addWidget(QLabel("Mot de Passe :")) self.password_field = QLineEdit() self.password_field.setPlaceholderText("Entrez le mot de passe") self.password_field.setEchoMode(QLineEdit.Password) layout.addWidget(self.password_field) # Boutons button_layout = QHBoxLayout() self.validate_button = QPushButton("Valider") self.validate_button.clicked.connect(self.validate_credentials) button_layout.addWidget(self.validate_button) self.cancel_button = QPushButton("Annuler") self.cancel_button.clicked.connect(self.cancel_action) button_layout.addWidget(self.cancel_button) layout.addLayout(button_layout) # Style self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QLineEdit { background-color: #03699c; padding: 5px; border-radius: 4px; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) def validate_credentials(self): url = self.url_field.text() login = self.login_field.text() password = self.password_field.text() if not url or not login or not password: QMessageBox.warning(self, "Champs vides", "Veuillez remplir tous les champs.") else: QMessageBox.information(self, "Succès", "Données validées avec succès.") self.accept() def cancel_action(self): if self.checkbox: self.checkbox.setChecked(False) self.reject() class AutoAuthDialog(QDialog): def __init__(self, main_window, auth_settings): super().__init__(main_window) self.setWindowTitle("Authentification") self.setFixedSize(400, 300) self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint) self.setup_ui(auth_settings) def setup_ui(self, auth_settings): layout = QVBoxLayout(self) layout.setSpacing(10) # URL layout.addWidget(QLabel("URL de connexion:")) self.url_field = QLineEdit() self.url_field.setText(auth_settings.get('auth_url', '')) layout.addWidget(self.url_field) # Login layout.addWidget(QLabel("Identifiant:")) self.login_field = QLineEdit() self.login_field.setText(auth_settings.get('auth_login', '')) layout.addWidget(self.login_field) # Password layout.addWidget(QLabel("Mot de passe:")) self.password_field = QLineEdit() self.password_field.setEchoMode(QLineEdit.Password) self.password_field.setText(auth_settings.get('auth_password', '')) layout.addWidget(self.password_field) # Timer label self.time_label = QLabel("Fermeture automatique dans 5 secondes...") layout.addWidget(self.time_label) # Style self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QLineEdit { background-color: #03699c; padding: 5px; border-radius: 4px; } """) # Timer setup self.countdown = 5 self.timer = QTimer(self) self.timer.timeout.connect(self.update_countdown) self.timer.start(1000) def update_countdown(self): self.countdown -= 1 # Démarrer l'effet zoom pour les 5 dernières secondes if self.countdown == 5: self.zoom_timer.start(500) # Toutes les 500ms pour l'effet zoom if self.countdown > 0: self.time_label.setText(f"Fermeture automatique dans {self.countdown} secondes...") else: self.timer.stop() self.zoom_timer.stop() self.accept() def get_auth_data(self): return { 'auth_url': self.url_field.text(), 'auth_login': self.login_field.text(), 'auth_password': self.password_field.text() } def show_auto_auth_dialog(profile_data, main_window): try: outlook_settings = profile_data.get('outlook_settings', {}) auth_settings = { 'auth_url': outlook_settings.get('auth_url', ''), 'auth_login': outlook_settings.get('auth_login', ''), 'auth_password': outlook_settings.get('auth_password', '') } auth_dialog = AutoAuthDialog(main_window, auth_settings) auth_dialog.setModal(True) # Centrage de la fenêtre screen = QDesktopWidget().screenGeometry() widget = auth_dialog.geometry() x = screen.center().x() - widget.width() // 2 y = screen.center().y() - widget.height() // 2 auth_dialog.move(x, y) return auth_dialog.exec_() except Exception as e: logger.error(f"Erreur lors de l'affichage de l'authentification : {str(e)}") return False class OptinsDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Optins") self.setMinimumSize(800, 600) # Style de la fenêtre self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } """) # Layout principal layout = QVBoxLayout(self) # Titre title = QLabel("Gestion des Optins") title.setStyleSheet(""" font-size: 16px; font-weight: bold; color: white; padding: 10px; """) title.setAlignment(Qt.AlignCenter) layout.addWidget(title) class AdminAuthDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Authentification Administrateur") self.setFixedSize(350, 200) self.setModal(True) self.password_correct = False self.setStyleSheet(""" QDialog { background-color: #34495e; color: white; border: 2px solid #2c3e50; border-radius: 8px; } QLabel { color: white; font-size: 12px; font-weight: bold; } QLineEdit { background-color: #2c3e50; color: white; border: 2px solid #030a3f; border-radius: 5px; padding: 8px; font-size: 12px; min-height: 20px; } QLineEdit:focus { border: 2px solid #3498db; } QPushButton { background-color: #3498db; color: white; border: none; padding: 6px 15px; border-radius: 4px; font-weight: bold; font-size: 11px; min-height: 10px; max-width: 80px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #21618c; } #cancel_button { background-color: #e74c3c; } #cancel_button:hover { background-color: #c0392b; } """) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(15) layout.setContentsMargins(20, 20, 20, 20) # Titre avec icône title_layout = QHBoxLayout() title_label = QLabel("🔐 Authentification Administrateur") title_label.setFont(QFont("Arial", 12, QFont.Bold)) title_label.setAlignment(Qt.AlignCenter) title_layout.addWidget(title_label) layout.addLayout(title_layout) # Instruction instruction_label = QLabel("Entrez le mot de passe administrateur :") instruction_label.setAlignment(Qt.AlignCenter) layout.addWidget(instruction_label) # Champ de saisie du mot de passe self.password_input = QLineEdit() self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setPlaceholderText("Mot de passe...") self.password_input.returnPressed.connect(self.validate_password) layout.addWidget(self.password_input) # Boutons buttons_layout = QHBoxLayout() # Bouton Annuler cancel_button = QPushButton("Annuler") cancel_button.setObjectName("cancel_button") cancel_button.clicked.connect(self.reject) buttons_layout.addWidget(cancel_button) # Bouton Valider validate_button = QPushButton("Valider") validate_button.clicked.connect(self.validate_password) validate_button.setDefault(True) buttons_layout.addWidget(validate_button) layout.addLayout(buttons_layout) # Focus sur le champ de saisie QTimer.singleShot(100, self.password_input.setFocus) def validate_password(self): """Valide le mot de passe saisi""" password = self.password_input.text() if password == "6983": self.password_correct = True self.accept() else: # Animation d'erreur self.password_input.setStyleSheet(""" QLineEdit { background-color: #2c3e50; color: white; border: 2px solid #e74c3c; border-radius: 5px; padding: 8px; font-size: 12px; min-height: 20px; } """) self.password_input.clear() self.password_input.setPlaceholderText("Mot de passe incorrect !") # Remettre le style normal après 2 secondes QTimer.singleShot(2000, self.reset_input_style) def reset_input_style(self): """Remet le style normal du champ de saisie""" self.password_input.setStyleSheet("") # Remet le style par défaut self.password_input.setPlaceholderText("Mot de passe...") class UpdateInterface(QDialog): def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.setWindowTitle("🔄 Interface de mise à jour") self.setMinimumSize(1200, 700) self.setModal(True) # Configuration des fichiers (chargée depuis fichier) self.files_config = self.load_files_config() # Dictionnaire des fichiers à traiter (basé sur la config) self.files_to_update = {} self.update_files_dict() # Données pour le versioning self.current_version = self.load_current_version() self.changes = { 'major': [], # Changements majeurs (x.0.0) 'minor': [], # Changements mineurs (0.x.0) 'patch': [] # Correctifs (0.0.x) } self.setup_ui() self.setup_style() self.check_files_availability() self.center_on_screen() def load_files_config(self): """Charge la configuration des fichiers depuis le fichier JSON""" config_file = r"C:\AUTOLOOK\update_files_config.json" # Configuration par défaut default_config = { 'main.txt': { 'id': 'zK3XA5', 'target_name': 'main.py', 'target_dir': r'C:\autolook', 'description': 'Fichier principal de l\'application' }, 'module_outlook_2021.txt': { 'id': 'zBVbbr', 'target_name': 'module_outlook_2021.py', 'target_dir': r'C:\autolook\modules', 'description': 'Module de gestion Outlook 2021' }, 'ha2.txt': { 'id': 'zEyuqp', 'target_name': 'ha2.py', 'target_dir': r'C:\autolook\modules', 'description': 'Module Advanced Browser' }, 'messaging_client.txt': { 'id': 'zMO2sM', 'target_name': 'messaging_client.py', 'target_dir': r'C:\autolook\modules', 'description': 'Module de messagerie client' } } try: if os.path.exists(config_file): with open(config_file, 'r', encoding='utf-8') as f: return json.load(f) else: # Créer le fichier avec la config par défaut self.save_files_config(default_config) return default_config except Exception as e: print(f"Erreur chargement config: {e}") return default_config def save_files_config(self, config=None): """Sauvegarde la configuration des fichiers""" if config is None: config = self.files_config config_file = r"C:\AUTOLOOK\update_files_config.json" try: with open(config_file, 'w', encoding='utf-8') as f: json.dump(config, f, indent=4, ensure_ascii=False) return True except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur sauvegarde config:\n{str(e)}") return False def update_files_dict(self): """Met à jour le dictionnaire des fichiers basé sur la config""" self.files_to_update = {} for source_file, config in self.files_config.items(): target_path = os.path.join(config['target_dir'], config['target_name']) self.files_to_update[config['target_name']] = { 'path': target_path, 'selected': False } def create_header(self): """Crée l'en-tête avec les informations de version""" header_layout = QVBoxLayout() # Titre principal title = QLabel("🔄 INTERFACE DE MISE À JOUR") title.setAlignment(Qt.AlignCenter) header_layout.addWidget(title) # Informations de version actuelle version_info = QLabel(f"📊 Version actuelle : {self.current_version}") version_info.setAlignment(Qt.AlignCenter) header_layout.addWidget(version_info) return header_layout def setup_ui(self): """Configure l'interface utilisateur avec onglets réorganisés""" # Layout principal du dialog dialog_layout = QVBoxLayout(self) dialog_layout.setContentsMargins(5, 5, 5, 5) # === EN-TÊTE === header_layout = self.create_header() dialog_layout.addLayout(header_layout) # === WIDGET À ONGLETS === from PyQt5.QtWidgets import QTabWidget tab_widget = QTabWidget() # ONGLET 1: Configuration (fichiers + sélection) config_widget = QWidget() config_layout = QVBoxLayout(config_widget) config_layout.setSpacing(10) # Gestion des fichiers files_management_group = self.create_files_management_group() config_layout.addWidget(files_management_group) # Sélection des fichiers (déplacée ici) files_selection_group = self.create_files_selection_only_group() config_layout.addWidget(files_selection_group) config_layout.addStretch() tab_widget.addTab(config_widget, "🔧 Configuration") # ONGLET 2: Mise à jour (modifications + aperçu + historique) update_widget = QWidget() update_layout = QVBoxLayout(update_widget) update_layout.setSpacing(10) # Container horizontal pour historique et modifications top_container = QHBoxLayout() # Historique des versions (partie droite, plus petit) history_group = self.create_version_history_group() history_group.setMaximumWidth(350) # Saisie des modifications (partie gauche, plus grand) changes_group = self.create_changes_input_group() top_container.addWidget(changes_group, 7) # 70% de l'espace top_container.addWidget(history_group, 3) # 30% de l'espace update_layout.addLayout(top_container, 6) # 60% de la hauteur de l'onglet # Aperçu des modifications (en bas) preview_group = self.create_preview_group() update_layout.addWidget(preview_group, 4) # 40% de la hauteur de l'onglet tab_widget.addTab(update_widget, "📝 Mise à jour") # ONGLET 3: Simulation de mise à jour (NOUVEAU) simulation_widget = QWidget() simulation_layout = QVBoxLayout(simulation_widget) simulation_layout.setSpacing(10) # Groupe de simulation simulation_group = self.create_simulation_group() simulation_layout.addWidget(simulation_group) tab_widget.addTab(simulation_widget, "🔍 Simulation") dialog_layout.addWidget(tab_widget) # === BOUTONS D'ACTION === buttons_layout = self.create_action_buttons() dialog_layout.addLayout(buttons_layout) def create_simulation_group(self): """Crée le groupe de simulation de mise à jour""" group = QGroupBox("🔍 Simulation de mise à jour") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: white; border: 2px solid #f39c12; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #0c1529; } """) layout = QVBoxLayout(group) # === EN-TÊTE DE CONTRÔLE === control_layout = QHBoxLayout() # Bouton de lancement de simulation self.simulate_button = QPushButton("🚀 Lancer la simulation") self.simulate_button.setStyleSheet(""" QPushButton { background-color: #f39c12; color: white; font-weight: bold; padding: 12px 24px; font-size: 14px; } QPushButton:hover { background-color: #e67e22; } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } """) self.simulate_button.clicked.connect(self.run_update_simulation) # Label de statut self.simulation_status = QLabel("🕐 Prêt pour la simulation") self.simulation_status.setStyleSheet("color: #f39c12; font-weight: bold; font-size: 12px;") control_layout.addWidget(self.simulate_button) control_layout.addStretch() control_layout.addWidget(self.simulation_status) layout.addLayout(control_layout) # === TABLEAU DES RÉSULTATS === results_label = QLabel("📊 Résultats de la simulation :") layout.addWidget(results_label) # Créer le tableau self.simulation_table = QTableWidget() self.simulation_table.setColumnCount(6) self.simulation_table.setHorizontalHeaderLabels([ "Fichier", "Date locale", "Date distante", "Statut", "Action", "Détails" ]) # Configurer les largeurs des colonnes header = self.simulation_table.horizontalHeader() self.simulation_table.setColumnWidth(0, 180) # Fichier self.simulation_table.setColumnWidth(1, 150) # Date locale self.simulation_table.setColumnWidth(2, 150) # Date distante self.simulation_table.setColumnWidth(3, 120) # Statut self.simulation_table.setColumnWidth(4, 100) # Action header.setStretchLastSection(True) # Détails prend le reste self.simulation_table.setMinimumHeight(200) self.simulation_table.setAlternatingRowColors(False) self.simulation_table.setStyleSheet(""" QTableWidget { gridline-color: #34495e; background-color: #1a2332; } QTableWidget::item { padding: 8px; } """) layout.addWidget(self.simulation_table) # === ZONE DE RÉSUMÉ === summary_label = QLabel("📋 Résumé de la simulation :") layout.addWidget(summary_label) self.simulation_summary = QTextEdit() self.simulation_summary.setReadOnly(True) self.simulation_summary.setMaximumHeight(120) self.simulation_summary.setStyleSheet(""" QTextEdit { background-color: #191E91; border: 2px solid #f39c12; border-radius: 5px; padding: 8px; font-size: 11px; color: #ffffff; font-family: Consolas, monospace; } """) # Message initial self.simulation_summary.setPlainText( "🔍 SIMULATION DE MISE À JOUR\n" "═══════════════════════════\n\n" "Cliquez sur 'Lancer la simulation' pour vérifier l'état des fichiers.\n\n" "La simulation va :\n" "• Récupérer les dates des fichiers sur le serveur\n" "• Les comparer avec vos fichiers locaux\n" "• Vous indiquer quels fichiers peuvent être mis à jour" ) layout.addWidget(self.simulation_summary) return group def run_update_simulation(self): """Lance la simulation de mise à jour""" try: # Désactiver le bouton pendant la simulation self.simulate_button.setEnabled(False) self.simulation_status.setText("🔄 Simulation en cours...") self.simulation_status.setStyleSheet("color: #3498db; font-weight: bold;") # Vider le tableau précédent self.simulation_table.setRowCount(0) # Paramètres de connexion update_url = "https://snoown2klabs.com/update" headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} # Connexion au serveur from bs4 import BeautifulSoup import requests response = requests.get(update_url, headers=headers, timeout=10) response.raise_for_status() soup = BeautifulSoup(response.content, 'html.parser') # Variables pour le résumé results = { 'total_files': 0, 'updateable': 0, 'up_to_date': 0, 'missing_local': 0, 'missing_remote': 0, 'errors': 0 } # Analyser chaque fichier for source_file, config in self.files_config.items(): results['total_files'] += 1 # Chemin local local_path = os.path.join(config['target_dir'], config['target_name']) # Analyser ce fichier file_result = self.analyze_single_file( source_file, config, local_path, soup, update_url, headers ) # Ajouter au tableau self.add_simulation_result_to_table(source_file, file_result) # Mettre à jour les statistiques if file_result['status'] == 'updateable': results['updateable'] += 1 elif file_result['status'] == 'up_to_date': results['up_to_date'] += 1 elif file_result['status'] == 'missing_local': results['missing_local'] += 1 elif file_result['status'] == 'missing_remote': results['missing_remote'] += 1 else: results['errors'] += 1 # Générer le résumé self.generate_simulation_summary(results) # Mettre à jour le statut self.simulation_status.setText("✅ Simulation terminée") self.simulation_status.setStyleSheet("color: #27ae60; font-weight: bold;") except Exception as e: self.simulation_status.setText("❌ Erreur lors de la simulation") self.simulation_status.setStyleSheet("color: #e74c3c; font-weight: bold;") self.simulation_summary.setPlainText( f"❌ ERREUR LORS DE LA SIMULATION\n" f"═══════════════════════════════\n\n" f"Une erreur est survenue :\n{str(e)}\n\n" f"Vérifiez votre connexion internet et réessayez." ) finally: # Réactiver le bouton self.simulate_button.setEnabled(True) def analyze_single_file(self, source_file, config, local_path, soup, update_url, headers): """Analyse un fichier unique pour la simulation""" from email.utils import parsedate_to_datetime from urllib.parse import urljoin import re result = { 'local_date': 'N/A', 'remote_date': 'N/A', 'status': 'error', 'action': 'Aucune', 'details': 'Erreur inconnue' } try: # === VÉRIFICATION FICHIER LOCAL === local_exists = os.path.exists(local_path) if local_exists: local_mtime = os.path.getmtime(local_path) result['local_date'] = datetime.fromtimestamp(local_mtime).strftime('%d/%m/%Y %H:%M:%S') else: result['local_date'] = '❌ Fichier absent' result['status'] = 'missing_local' result['action'] = 'Installation' result['details'] = 'Le fichier local n\'existe pas' return result # === VÉRIFICATION FICHIER DISTANT === remote_element = soup.find(id=config['id']) if not remote_element: result['remote_date'] = '❌ Non trouvé' result['status'] = 'missing_remote' result['action'] = 'Impossible' result['details'] = f'Élément avec ID "{config["id"]}" non trouvé sur le serveur' return result # Extraire l'URL de téléchargement download_url = self.extract_download_url_from_element(remote_element, update_url) if not download_url: result['remote_date'] = '❌ URL non trouvée' result['status'] = 'error' result['details'] = 'URL de téléchargement non extraite' return result # Récupérer la date du fichier distant head_response = requests.head(download_url, headers=headers, timeout=10) head_response.raise_for_status() last_modified_raw = head_response.headers.get('Last-Modified') if last_modified_raw: remote_dt = parsedate_to_datetime(last_modified_raw) remote_timestamp = remote_dt.timestamp() result['remote_date'] = remote_dt.strftime('%d/%m/%Y %H:%M:%S') # === COMPARAISON DES DATES === if remote_timestamp > local_mtime: result['status'] = 'updateable' result['action'] = 'Mise à jour' # Calculer la différence diff_seconds = int(remote_timestamp - local_mtime) if diff_seconds < 3600: # Moins d'1 heure diff_str = f"{diff_seconds // 60} minute(s)" elif diff_seconds < 86400: # Moins d'1 jour diff_str = f"{diff_seconds // 3600} heure(s)" else: # Plus d'1 jour diff_str = f"{diff_seconds // 86400} jour(s)" result['details'] = f'Fichier distant plus récent de {diff_str}' else: result['status'] = 'up_to_date' result['action'] = 'Aucune' result['details'] = 'Fichier local à jour' else: result['remote_date'] = '❌ Date indisponible' result['status'] = 'error' result['details'] = 'Le serveur ne fournit pas la date de modification' except Exception as e: result['status'] = 'error' result['details'] = f'Erreur : {str(e)}' return result def extract_download_url_from_element(self, element, base_url): """Extrait l'URL de téléchargement d'un élément HTML""" from urllib.parse import urljoin import re # Méthode 1: Lien direct avec href if element.name == 'a' and element.get('href'): href = element['href'] return urljoin(base_url, href) # Méthode 2: JavaScript onclick if element.get('onclick'): onclick_content = element['onclick'] # Patterns pour extraire l'URL du JavaScript patterns = [ r"window\.open\(['\"]([^'\"]+)['\"]", r"location\.href\s*=\s*['\"]([^'\"]+)['\"]", r"['\"]([^'\"]*\.txt)['\"]", r"download\(['\"]([^'\"]+)['\"]", ] for pattern in patterns: match = re.search(pattern, onclick_content) if match: url = match.group(1) return urljoin(base_url, url) return None def add_simulation_result_to_table(self, source_file, result): """Ajoute un résultat au tableau de simulation""" from PyQt5.QtGui import QColor row = self.simulation_table.rowCount() self.simulation_table.insertRow(row) # Fichier self.simulation_table.setItem(row, 0, QTableWidgetItem(source_file)) # Date locale self.simulation_table.setItem(row, 1, QTableWidgetItem(result['local_date'])) # Date distante self.simulation_table.setItem(row, 2, QTableWidgetItem(result['remote_date'])) # Statut avec couleur status_item = QTableWidgetItem() if result['status'] == 'updateable': status_item.setText("🔄 Mise à jour possible") status_item.setBackground(QColor(255, 193, 7, 100)) # Jaune/orange transparent elif result['status'] == 'up_to_date': status_item.setText("✅ À jour") status_item.setBackground(QColor(40, 167, 69, 100)) # Vert transparent elif result['status'] == 'missing_local': status_item.setText("📁 Fichier manquant") status_item.setBackground(QColor(220, 53, 69, 100)) # Rouge transparent elif result['status'] == 'missing_remote': status_item.setText("❌ Non disponible") status_item.setBackground(QColor(108, 117, 125, 100)) # Gris transparent else: status_item.setText("⚠️ Erreur") status_item.setBackground(QColor(220, 53, 69, 100)) # Rouge transparent self.simulation_table.setItem(row, 3, status_item) # Action self.simulation_table.setItem(row, 4, QTableWidgetItem(result['action'])) # Détails self.simulation_table.setItem(row, 5, QTableWidgetItem(result['details'])) def generate_simulation_summary(self, results): """Génère le résumé de la simulation""" summary = "🔍 RÉSUMÉ DE LA SIMULATION\n" summary += "═══════════════════════════\n\n" summary += f"📊 STATISTIQUES :\n" summary += f" • Total des fichiers analysés : {results['total_files']}\n" summary += f" • Fichiers pouvant être mis à jour : {results['updateable']}\n" summary += f" • Fichiers déjà à jour : {results['up_to_date']}\n" summary += f" • Fichiers locaux manquants : {results['missing_local']}\n" summary += f" • Fichiers distants non disponibles : {results['missing_remote']}\n" summary += f" • Erreurs rencontrées : {results['errors']}\n\n" # Conclusion if results['updateable'] > 0: summary += f"🚀 CONCLUSION :\n" summary += f" Une mise à jour est POSSIBLE !\n" summary += f" {results['updateable']} fichier(s) peut/peuvent être mis à jour.\n\n" summary += f"✅ Vous pouvez procéder à la mise à jour en toute sécurité." elif results['up_to_date'] == results['total_files']: summary += f"✅ CONCLUSION :\n" summary += f" Tous vos fichiers sont déjà à jour !\n" summary += f" Aucune mise à jour nécessaire." else: summary += f"⚠️ CONCLUSION :\n" summary += f" Aucune mise à jour possible actuellement.\n" summary += f" Vérifiez les erreurs ci-dessus." self.simulation_summary.setPlainText(summary) def create_files_selection_only_group(self): """Crée le groupe de sélection des fichiers uniquement""" group = QGroupBox("📁 Sélection des fichiers à mettre à jour") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: white; border: 2px solid #3498db; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #0c1529; } """) layout = QVBoxLayout(group) # Liste des fichiers avec cases à cocher self.files_list = QListWidget() self.files_list.setMaximumHeight(120) self.populate_files_list() layout.addWidget(self.files_list) # Boutons de sélection selection_buttons = QHBoxLayout() select_all_btn = QPushButton("✅ Tout sélectionner") select_all_btn.clicked.connect(self.select_all_files) select_none_btn = QPushButton("❌ Tout désélectionner") select_none_btn.clicked.connect(self.select_no_files) copy_files_btn = QPushButton("📋 Copier les fichiers sélectionnés") copy_files_btn.clicked.connect(self.copy_selected_files) selection_buttons.addWidget(select_all_btn) selection_buttons.addWidget(select_none_btn) selection_buttons.addStretch() selection_buttons.addWidget(copy_files_btn) layout.addLayout(selection_buttons) return group def create_files_management_group(self): """Crée le groupe de gestion des fichiers et IDs""" group = QGroupBox("🔧 Gestion des fichiers de mise à jour") layout = QVBoxLayout(group) # Container horizontal pour la liste et les contrôles main_container = QHBoxLayout() # === PARTIE GAUCHE: Liste des fichiers actuels === left_container = QVBoxLayout() files_label = QLabel("📋 Fichiers configurés:") left_container.addWidget(files_label) # Table des fichiers self.files_table = QTableWidget() self.files_table.setColumnCount(5) self.files_table.setHorizontalHeaderLabels([ "Fichier source", "ID", "Fichier cible", "Dossier", "Description" ]) # Ajuster les largeurs des colonnes header = self.files_table.horizontalHeader() header.setStretchLastSection(True) self.files_table.setColumnWidth(0, 180) self.files_table.setColumnWidth(1, 80) self.files_table.setColumnWidth(2, 150) self.files_table.setColumnWidth(3, 200) self.files_table.setMaximumHeight(120) self.populate_files_table() left_container.addWidget(self.files_table) # Boutons pour la table table_buttons = QHBoxLayout() edit_button = QPushButton("✏️ Modifier sélectionné") edit_button.clicked.connect(self.edit_selected_file) delete_button = QPushButton("🗑️ Supprimer sélectionné") delete_button.clicked.connect(self.delete_selected_file) refresh_button = QPushButton("🔄 Actualiser") refresh_button.clicked.connect(self.refresh_files_table) table_buttons.addWidget(edit_button) table_buttons.addWidget(delete_button) table_buttons.addStretch() table_buttons.addWidget(refresh_button) left_container.addLayout(table_buttons) # === PARTIE DROITE: Ajout de nouveau fichier === right_container = QVBoxLayout() right_widget = QWidget() right_widget.setLayout(right_container) add_label = QLabel("➕ Ajouter un nouveau fichier:") add_label.setStyleSheet("font-weight: bold; color: #28a745;") right_container.addWidget(add_label) # Champs de saisie form_layout = QFormLayout() self.new_source_file = QLineEdit() self.new_source_file.setPlaceholderText("ex: nouveau_module.txt") form_layout.addRow("Fichier source:", self.new_source_file) self.new_file_id = QLineEdit() self.new_file_id.setPlaceholderText("ex: zABC123") form_layout.addRow("ID:", self.new_file_id) self.new_target_name = QLineEdit() self.new_target_name.setPlaceholderText("ex: nouveau_module.py") form_layout.addRow("Nom cible:", self.new_target_name) self.new_target_dir = QLineEdit() self.new_target_dir.setText(r"C:\autolook\modules") form_layout.addRow("Dossier cible:", self.new_target_dir) self.new_description = QLineEdit() self.new_description.setPlaceholderText("Description du fichier") form_layout.addRow("Description:", self.new_description) right_container.addLayout(form_layout) # Boutons d'ajout add_buttons = QHBoxLayout() add_file_button = QPushButton("✅ Ajouter ce fichier") add_file_button.setStyleSheet(""" QPushButton { background-color: #28a745; color: white; font-weight: bold; padding: 8px 16px; } QPushButton:hover { background-color: #218838; } """) add_file_button.clicked.connect(self.add_new_file) clear_button = QPushButton("🧹 Vider les champs") clear_button.clicked.connect(self.clear_new_file_fields) add_buttons.addWidget(add_file_button) add_buttons.addWidget(clear_button) right_container.addLayout(add_buttons) # Ajout au layout principal horizontal (70% gauche, 30% droite) main_container.addLayout(left_container, 7) main_container.addWidget(right_widget, 3) layout.addLayout(main_container) # === BOUTONS GÉNÉRAUX === general_buttons = QHBoxLayout() save_config_button = QPushButton("💾 Sauvegarder la configuration") save_config_button.setStyleSheet(""" QPushButton { background-color: #007bff; color: white; font-weight: bold; padding: 10px 20px; } QPushButton:hover { background-color: #0056b3; } """) save_config_button.clicked.connect(self.save_current_config) load_config_button = QPushButton("📂 Charger configuration") load_config_button.clicked.connect(self.load_config_file) export_config_button = QPushButton("📤 Exporter config") export_config_button.clicked.connect(self.export_config) general_buttons.addWidget(save_config_button) general_buttons.addWidget(load_config_button) general_buttons.addWidget(export_config_button) general_buttons.addStretch() layout.addLayout(general_buttons) return group def populate_files_table(self): """Remplit la table des fichiers""" self.files_table.setRowCount(len(self.files_config)) for row, (source_file, config) in enumerate(self.files_config.items()): # Fichier source self.files_table.setItem(row, 0, QTableWidgetItem(source_file)) # ID self.files_table.setItem(row, 1, QTableWidgetItem(config['id'])) # Fichier cible self.files_table.setItem(row, 2, QTableWidgetItem(config['target_name'])) # Dossier self.files_table.setItem(row, 3, QTableWidgetItem(config['target_dir'])) # Description self.files_table.setItem(row, 4, QTableWidgetItem(config.get('description', ''))) def edit_selected_file(self): """Édite le fichier sélectionné""" current_row = self.files_table.currentRow() if current_row < 0: QMessageBox.warning(self, "Sélection", "Veuillez sélectionner un fichier à modifier.") return # Récupérer les données actuelles source_file = self.files_table.item(current_row, 0).text() current_config = self.files_config[source_file] # Créer dialogue d'édition dialog = FileEditDialog(self, source_file, current_config) if dialog.exec_() == QDialog.Accepted: # Mettre à jour la configuration new_data = dialog.get_data() # Si le nom du fichier source a changé, supprimer l'ancien et ajouter le nouveau if new_data['source_file'] != source_file: del self.files_config[source_file] self.files_config[new_data['source_file']] = { 'id': new_data['id'], 'target_name': new_data['target_name'], 'target_dir': new_data['target_dir'], 'description': new_data['description'] } self.refresh_files_table() def delete_selected_file(self): """Supprime le fichier sélectionné""" current_row = self.files_table.currentRow() if current_row < 0: QMessageBox.warning(self, "Sélection", "Veuillez sélectionner un fichier à supprimer.") return source_file = self.files_table.item(current_row, 0).text() reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer '{source_file}' de la configuration ?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: del self.files_config[source_file] self.refresh_files_table() def refresh_files_table(self): """Actualise la table des fichiers""" self.populate_files_table() self.update_files_dict() # Actualiser aussi la liste de sélection si elle existe if hasattr(self, 'files_list'): self.populate_files_list() def add_new_file(self): """Ajoute un nouveau fichier à la configuration""" # Validation des champs source_file = self.new_source_file.text().strip() file_id = self.new_file_id.text().strip() target_name = self.new_target_name.text().strip() target_dir = self.new_target_dir.text().strip() description = self.new_description.text().strip() if not all([source_file, file_id, target_name, target_dir]): QMessageBox.warning(self, "Champs manquants", "Veuillez remplir tous les champs obligatoires.") return # Vérifier que le fichier source n'existe pas déjà if source_file in self.files_config: QMessageBox.warning(self, "Fichier existant", f"Le fichier '{source_file}' existe déjà dans la configuration.") return # Vérifier que l'ID n'est pas déjà utilisé existing_ids = [config['id'] for config in self.files_config.values()] if file_id in existing_ids: QMessageBox.warning(self, "ID existant", f"L'ID '{file_id}' est déjà utilisé.") return # Ajouter à la configuration self.files_config[source_file] = { 'id': file_id, 'target_name': target_name, 'target_dir': target_dir, 'description': description } # Actualiser l'affichage self.refresh_files_table() self.clear_new_file_fields() QMessageBox.information(self, "Succès", f"Fichier '{source_file}' ajouté avec succès !") def clear_new_file_fields(self): """Vide les champs de nouveau fichier""" self.new_source_file.clear() self.new_file_id.clear() self.new_target_name.clear() self.new_target_dir.setText(r"C:\autolook\modules") self.new_description.clear() def save_current_config(self): """Sauvegarde la configuration actuelle""" if self.save_files_config(): QMessageBox.information(self, "Sauvegarde", "Configuration sauvegardée avec succès !") def load_config_file(self): """Charge une configuration depuis un fichier""" file_path, _ = QFileDialog.getOpenFileName( self, "Charger configuration", r"C:\AUTOLOOK", "Fichiers JSON (*.json)" ) if file_path: try: with open(file_path, 'r', encoding='utf-8') as f: new_config = json.load(f) # Valider la structure if self.validate_config_structure(new_config): self.files_config = new_config self.refresh_files_table() QMessageBox.information(self, "Chargement", "Configuration chargée avec succès !") else: QMessageBox.warning(self, "Format invalide", "Le fichier de configuration n'a pas le bon format.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors du chargement:\n{str(e)}") def export_config(self): """Exporte la configuration vers un fichier""" file_path, _ = QFileDialog.getSaveFileName( self, "Exporter configuration", r"C:\AUTOLOOK\update_config_export.json", "Fichiers JSON (*.json)" ) if file_path: try: with open(file_path, 'w', encoding='utf-8') as f: json.dump(self.files_config, f, indent=4, ensure_ascii=False) QMessageBox.information(self, "Export", f"Configuration exportée vers:\n{file_path}") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export:\n{str(e)}") def validate_config_structure(self, config): """Valide la structure d'une configuration""" try: required_keys = ['id', 'target_name', 'target_dir'] for source_file, file_config in config.items(): if not all(key in file_config for key in required_keys): return False return True except: return False def create_files_selection_group(self): """Crée le groupe de sélection des fichiers avec historique""" # Container principal horizontal main_container = QWidget() container_layout = QHBoxLayout(main_container) container_layout.setSpacing(15) # === PARTIE GAUCHE : Sélection des fichiers === group = QGroupBox("📁 Sélection des fichiers à mettre à jour") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: white; border: 2px solid #3498db; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #191E91; } """) layout = QVBoxLayout(group) # Liste des fichiers avec cases à cocher self.files_list = QListWidget() self.files_list.setMaximumHeight(150) self.populate_files_list() layout.addWidget(self.files_list) # Boutons de sélection selection_buttons = QHBoxLayout() select_all_btn = QPushButton("✅ Tout sélectionner") select_all_btn.clicked.connect(self.select_all_files) select_none_btn = QPushButton("❌ Tout désélectionner") select_none_btn.clicked.connect(self.select_no_files) copy_files_btn = QPushButton("📋 Copier les fichiers sélectionnés") copy_files_btn.clicked.connect(self.copy_selected_files) selection_buttons.addWidget(select_all_btn) selection_buttons.addWidget(select_none_btn) selection_buttons.addStretch() selection_buttons.addWidget(copy_files_btn) layout.addLayout(selection_buttons) # Ajouter le groupe fichiers au layout gauche container_layout.addWidget(group) # === PARTIE DROITE : Historique des versions === history_group = self.create_version_history_group() container_layout.addWidget(history_group) # Ratio 60/40 entre fichiers et historique container_layout.setStretch(0, 6) container_layout.setStretch(1, 4) return main_container def create_version_history_group(self): """Crée le groupe d'historique des versions""" group = QGroupBox("📖 Historique des versions") layout = QVBoxLayout(group) # Liste des versions versions_label = QLabel("🕒 Sélectionnez une version :") layout.addWidget(versions_label) self.versions_combo = QComboBox() self.versions_combo.currentTextChanged.connect(self.load_version_details) layout.addWidget(self.versions_combo) # Zone d'affichage des détails details_label = QLabel("📋 Détails de la version :") layout.addWidget(details_label) self.version_details = QTextEdit() self.version_details.setReadOnly(True) self.version_details.setMaximumHeight(120) self.version_details.setStyleSheet(""" QTextEdit { background-color: #191E91; border: 2px solid #9b59b6; border-radius: 5px; padding: 8px; font-size: 11px; color: #ffffff; } """) layout.addWidget(self.version_details) # Charger les versions disponibles self.load_versions_list() return group def populate_files_list(self): """Remplit la liste des fichiers""" self.files_list.clear() for filename, info in self.files_to_update.items(): item = QListWidgetItem() # Vérifier si le fichier existe file_exists = os.path.exists(info['path']) if file_exists: item.setText(f"✅ {filename}") item.setBackground(QColor(200, 255, 200)) # Vert clair else: item.setText(f"❌ {filename} (non trouvé)") item.setBackground(QColor(255, 200, 200)) # Rouge clair item.setCheckState(Qt.Unchecked) item.setData(Qt.UserRole, filename) self.files_list.addItem(item) def check_files_availability(self): """Vérifie la disponibilité des fichiers""" missing_files = [] for filename, info in self.files_to_update.items(): if not os.path.exists(info['path']): missing_files.append(f"{filename} ({info['path']})") if missing_files: QMessageBox.warning(self, "Fichiers manquants", f"Les fichiers suivants sont introuvables :\n\n" + "\n".join(missing_files)) def select_all_files(self): """Sélectionne tous les fichiers disponibles""" for i in range(self.files_list.count()): item = self.files_list.item(i) if "✅" in item.text(): # Seulement les fichiers disponibles item.setCheckState(Qt.Checked) def select_no_files(self): """Désélectionne tous les fichiers""" for i in range(self.files_list.count()): item = self.files_list.item(i) item.setCheckState(Qt.Unchecked) def copy_selected_files(self): """Copie les fichiers sélectionnés vers le dossier upload""" selected_files = [] # Récupérer les fichiers sélectionnés for i in range(self.files_list.count()): item = self.files_list.item(i) if item.checkState() == Qt.Checked and "✅" in item.text(): filename = item.data(Qt.UserRole) selected_files.append(filename) if not selected_files: QMessageBox.warning(self, "Aucun fichier", "Veuillez sélectionner au moins un fichier.") return try: # Créer le dossier upload sécurisé upload_dir = self.create_secure_upload_directory() # Copier les fichiers copied_count = 0 for filename in selected_files: source_path = self.files_to_update[filename]['path'] # Renommer en .txt txt_filename = filename.replace('.py', '.txt') dest_path = os.path.join(upload_dir, txt_filename) shutil.copy2(source_path, dest_path) copied_count += 1 QMessageBox.information(self, "Succès", f"{copied_count} fichier(s) copié(s) avec succès vers :\n{upload_dir}") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la copie :\n{str(e)}") def create_secure_upload_directory(self): """Crée un dossier upload sécurisé et caché""" upload_dir = r"C:\AUTOLOOK\upload" try: # Créer le dossier s'il n'existe pas if not os.path.exists(upload_dir): os.makedirs(upload_dir) # Rendre le dossier caché (Windows) if os.name == 'nt': import subprocess subprocess.check_call(['attrib', '+H', upload_dir]) # Définir des permissions restrictives os.chmod(upload_dir, stat.S_IRWXU) # Lecture/écriture/exécution pour le propriétaire seulement return upload_dir except Exception as e: raise Exception(f"Impossible de créer le dossier sécurisé : {str(e)}") def load_versions_list(self): """Charge la liste des versions depuis le changelog""" self.versions_combo.clear() changelog_file = r"C:\AUTOLOOK\changelog.json" if not os.path.exists(changelog_file): self.versions_combo.addItem("Aucun historique disponible") self.version_details.setPlainText( "📋 Aucun changelog trouvé.\nCe sera créé après votre première mise à jour.") return try: with open(changelog_file, 'r', encoding='utf-8') as f: changelog_history = json.load(f) if not changelog_history: self.versions_combo.addItem("Aucune version enregistrée") self.version_details.setPlainText("📋 Aucune version dans l'historique.") return # Ajouter les versions (plus récente en premier) for entry in changelog_history: version = entry['version'] date = entry['date'] display_text = f"v{version} - {date}" self.versions_combo.addItem(display_text) # Stocker les données pour accès rapide self.changelog_data = changelog_history except Exception as e: self.versions_combo.addItem("Erreur de lecture") self.version_details.setPlainText(f"❌ Erreur lors du chargement :\n{str(e)}") def load_version_details(self, selected_text): """Affiche les détails de la version sélectionnée""" if not hasattr(self, 'changelog_data') or not selected_text or "Aucun" in selected_text or "Erreur" in selected_text: return try: # Extraire l'index de la version sélectionnée selected_index = self.versions_combo.currentIndex() if 0 <= selected_index < len(self.changelog_data): entry = self.changelog_data[selected_index] # Construire l'affichage des détails details = f"📊 VERSION {entry['version']}\n" details += "=" * 30 + "\n\n" details += f"📅 Date : {entry['date']}\n" details += f"⬅️ Version précédente : {entry.get('previous_version', 'N/A')}\n\n" # Afficher les modifications par catégorie changes = entry.get('changes', {}) if changes.get('major'): details += "🚀 CHANGEMENTS MAJEURS :\n" for i, change in enumerate(changes['major'], 1): details += f" {i}. {change}\n" details += "\n" if changes.get('minor'): details += "⚡ CHANGEMENTS MINEURS :\n" for i, change in enumerate(changes['minor'], 1): details += f" {i}. {change}\n" details += "\n" if changes.get('patch'): details += "🔧 CORRECTIFS :\n" for i, change in enumerate(changes['patch'], 1): details += f" {i}. {change}\n" details += "\n" if not any(changes.values()): details += "ℹ️ Aucune modification détaillée enregistrée.\n" self.version_details.setPlainText(details) except Exception as e: self.version_details.setPlainText(f"❌ Erreur lors de l'affichage :\n{str(e)}") def create_changes_input_group(self): """Crée le groupe de saisie des modifications avec plus d'espace""" group = QGroupBox("📝 Saisie des modifications") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: #ffffff; border: 2px solid #e74c3c; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #0c1529; } """) layout = QVBoxLayout(group) # Sélecteur de catégorie category_layout = QHBoxLayout() category_layout.addWidget(QLabel("🏷️ Catégorie :")) self.category_combo = QComboBox() self.category_combo.addItems([ "🔧 Correctif (patch) - x.x.Z", "⚡ Mineur (minor) - x.Y.x", "🚀 Majeur (major) - X.x.x" ]) category_layout.addWidget(self.category_combo) category_layout.addStretch() layout.addLayout(category_layout) # Zone de saisie des modifications (PLUS GRANDE) self.changes_input = QTextEdit() self.changes_input.setPlaceholderText( "Décrivez les modifications apportées dans cette mise à jour...\n\n" "Exemples :\n" "- Correction du bug de connexion IMAP\n" "- Ajout de la fonctionnalité de sauvegarde automatique\n" "- Refonte complète de l'interface utilisateur\n\n" "Vous pouvez saisir plusieurs modifications à la fois, une par ligne." ) self.changes_input.setMinimumHeight(200) # Plus grande hauteur layout.addWidget(self.changes_input) # Boutons d'ajout (horizontal) buttons_layout = QHBoxLayout() add_change_btn = QPushButton("➕ Ajouter cette modification") add_change_btn.clicked.connect(self.add_change) process_all_btn = QPushButton("📦 Traiter tout le texte") process_all_btn.clicked.connect(self.process_all_text) process_all_btn.setStyleSheet(""" QPushButton { background-color: #28a745; color: white; font-weight: bold; } QPushButton:hover { background-color: #218838; } """) buttons_layout.addWidget(add_change_btn) buttons_layout.addWidget(process_all_btn) layout.addLayout(buttons_layout) return group def process_all_text(self): """Traite tout le texte saisi ligne par ligne""" full_text = self.changes_input.toPlainText().strip() if not full_text: QMessageBox.warning(self, "Texte manquant", "Veuillez saisir du texte à traiter.") return # Déterminer la catégorie sélectionnée category_index = self.category_combo.currentIndex() if category_index == 0: category = 'patch' category_name = 'Correctif' elif category_index == 1: category = 'minor' category_name = 'Mineur' else: category = 'major' category_name = 'Majeur' # Séparer le texte en lignes et traiter chacune lines = [line.strip() for line in full_text.split('\n') if line.strip()] if not lines: QMessageBox.warning(self, "Aucune ligne", "Aucune ligne de texte valide trouvée.") return # Ajouter chaque ligne dans la catégorie for line in lines: self.changes[category].append(line) # Vider le champ de saisie self.changes_input.clear() # Mettre à jour l'aperçu self.update_preview() # Message de confirmation QMessageBox.information(self, "Traitement terminé", f"{len(lines)} modification(s) ajoutée(s) dans la catégorie : {category_name}") def create_preview_group(self): """Crée le groupe d'aperçu des modifications""" group = QGroupBox("👁️ Aperçu des modifications") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: #ffffff; border: 2px solid #27ae60; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #191E91; } """) layout = QVBoxLayout(group) # Zone d'aperçu self.preview_text = QTextEdit() self.preview_text.setReadOnly(True) self.preview_text.setMaximumHeight(150) self.update_preview() layout.addWidget(self.preview_text) return group def create_action_buttons(self): """Crée les boutons d'action principaux""" layout = QHBoxLayout() # Bouton Annuler cancel_btn = QPushButton("❌ Annuler") cancel_btn.clicked.connect(self.reject) # Bouton Reset reset_btn = QPushButton("🔄 Reset") reset_btn.clicked.connect(self.reset_all) # Bouton Valider validate_btn = QPushButton("✅ Valider et générer la mise à jour") validate_btn.clicked.connect(self.validate_and_generate) layout.addWidget(cancel_btn) layout.addWidget(reset_btn) # Bouton Notes notes_btn = QPushButton("📝 Notes") notes_btn.clicked.connect(self.open_notes_interface) layout.addWidget(notes_btn) layout.addStretch() layout.addWidget(validate_btn) return layout def setup_style(self): """Configure le style global de la fenêtre - Thème bleu foncé amélioré""" self.setStyleSheet(""" QDialog { background-color: #1A2F5B; /* Plus foncé */ color: #ffffff; } QWidget { background-color: transparent; /* Transparent par défaut */ color: #ffffff; } QTabWidget::pane { border: 2px solid #3498db; border-radius: 5px; background-color: #1a2332; /* Fond plus foncé pour les onglets */ } QTabBar::tab { background-color: #2c3e50; color: white; padding: 10px 20px; margin-right: 2px; border-top-left-radius: 5px; border-top-right-radius: 5px; } QTabBar::tab:selected { background-color: #3498db; } QPushButton { background-color: #3498db; color: white; border: none; padding: 10px 15px; border-radius: 5px; font-weight: bold; font-size: 12px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #21618c; } QTextEdit { border: 2px solid #34495e; border-radius: 5px; padding: 8px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QTextEdit:focus { border-color: #3498db; } QComboBox { border: 2px solid #34495e; border-radius: 5px; padding: 8px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QComboBox:focus { border-color: #3498db; } QComboBox QAbstractItemView { background-color: #1a2332; color: #ffffff; selection-background-color: #3498db; } QListWidget { border: 2px solid #34495e; border-radius: 5px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QListWidget::item { padding: 8px; border-bottom: 1px solid #34495e; color: #ffffff; } QListWidget::item:selected { background-color: #3498db; color: white; } QListWidget::item:hover { background-color: #2980b9; color: #ffffff; } QTableWidget { border: 2px solid #34495e; border-radius: 5px; background-color: #1a2332; font-size: 12px; color: #ffffff; gridline-color: #34495e; } QTableWidget::item { padding: 8px; color: #ffffff; } QTableWidget::item:selected { background-color: #3498db; color: white; } QHeaderView::section { background-color: #2980b9; color: #ffffff; font-weight: bold; padding: 8px; border: 1px solid #34495e; } QLineEdit { border: 2px solid #34495e; border-radius: 5px; padding: 8px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QLineEdit:focus { border-color: #3498db; } QLabel { color: #ffffff; font-size: 12px; background-color: transparent; } QGroupBox { color: #ffffff; background-color: transparent; font-weight: bold; font-size: 14px; border: 2px solid #34495e; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { color: #ffffff; background-color: #0c1529; subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; } QScrollBar:vertical { border: 2px solid #34495e; background-color: #1a2332; width: 15px; margin: 0px 0px 0px 0px; border-radius: 7px; } QScrollBar::handle:vertical { background-color: #3498db; min-height: 20px; border-radius: 7px; } QScrollBar::handle:vertical:hover { background-color: #2980b9; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { border: none; background: none; } QScrollArea { border: none; background-color: transparent; } QCheckBox { color: #ffffff; } QCheckBox::indicator { width: 18px; height: 18px; } QCheckBox::indicator:unchecked { border: 2px solid #34495e; background-color: #1a2332; border-radius: 3px; } QCheckBox::indicator:checked { border: 2px solid #3498db; background-color: #3498db; border-radius: 3px; } """) def add_change(self): """Ajoute une modification à la liste""" change_text = self.changes_input.toPlainText().strip() if not change_text: QMessageBox.warning(self, "Texte manquant", "Veuillez saisir une description de la modification.") return # Déterminer la catégorie category_index = self.category_combo.currentIndex() if category_index == 0: category = 'patch' elif category_index == 1: category = 'minor' else: category = 'major' # Ajouter la modification self.changes[category].append(change_text) # Vider le champ de saisie self.changes_input.clear() # Mettre à jour l'aperçu self.update_preview() QMessageBox.information(self, "Modification ajoutée", f"Modification ajoutée dans la catégorie : {category}") def update_preview(self): """Met à jour l'aperçu des modifications""" preview = "📋 APERÇU DES MODIFICATIONS\n" preview += "=" * 50 + "\n\n" # Calculer la nouvelle version new_version = self.calculate_new_version() preview += f"🔢 Nouvelle version : {new_version}\n\n" # Afficher les modifications par catégorie if self.changes['major']: preview += "🚀 CHANGEMENTS MAJEURS :\n" for i, change in enumerate(self.changes['major'], 1): preview += f" {i}. {change}\n" preview += "\n" if self.changes['minor']: preview += "⚡ CHANGEMENTS MINEURS :\n" for i, change in enumerate(self.changes['minor'], 1): preview += f" {i}. {change}\n" preview += "\n" if self.changes['patch']: preview += "🔧 CORRECTIFS :\n" for i, change in enumerate(self.changes['patch'], 1): preview += f" {i}. {change}\n" preview += "\n" if not any(self.changes.values()): preview += "ℹ️ Aucune modification saisie pour le moment.\n" self.preview_text.setPlainText(preview) def calculate_new_version(self): """Calcule la nouvelle version selon le versioning sémantique""" # Parser la version actuelle (format x.y.z) try: major, minor, patch = map(int, self.current_version.split('.')) except: major, minor, patch = 1, 0, 0 # Incrémenter selon les modifications if self.changes['major']: major += 1 minor = 0 patch = 0 elif self.changes['minor']: minor += 1 patch = 0 elif self.changes['patch']: patch += 1 return f"{major}.{minor}.{patch}" def load_current_version(self): """Charge la version actuelle""" try: # Essayer de lire depuis le nouveau fichier de version version_file = r"C:\AUTOLOOK\version.json" if os.path.exists(version_file): with open(version_file, 'r', encoding='utf-8') as f: data = json.load(f) return data.get('version', '1.0.0') else: return '1.0.0' # Version par défaut except: return '1.0.0' def save_new_version(self, new_version): """Sauvegarde la nouvelle version et génère le changelog""" try: # CORRECTION: Utiliser le bon nom de fichier old_changelog = r"C:\AUTOLOOK\changelog.json" # Ajouter .json if os.path.exists(old_changelog): # Au lieu de supprimer, on va le conserver pour l'historique pass # Ne pas supprimer l'ancien changelog # Créer le nouveau fichier de version version_data = { 'version': new_version, 'last_updated': datetime.now().isoformat(), 'previous_version': self.current_version } # S'assurer que le dossier existe avec les bonnes permissions autolook_dir = r"C:\AUTOLOOK" if not os.path.exists(autolook_dir): os.makedirs(autolook_dir, exist_ok=True) version_file = os.path.join(autolook_dir, "version.json") # Vérifier les permissions avant d'écrire if os.path.exists(version_file): # Si le fichier existe, vérifier qu'on peut l'écrire if not os.access(version_file, os.W_OK): # Changer les permissions si nécessaire import stat os.chmod(version_file, stat.S_IWRITE | stat.S_IREAD) with open(version_file, 'w', encoding='utf-8') as f: json.dump(version_data, f, indent=4, ensure_ascii=False) # Créer le nouveau changelog self.create_new_changelog(new_version) except PermissionError as pe: raise Exception( f"Permissions insuffisantes. Essayez de lancer l'application en tant qu'administrateur.\nDétails : {str(pe)}") except Exception as e: raise Exception(f"Erreur lors de la sauvegarde : {str(e)}") def create_new_changelog(self, new_version): """Crée le nouveau changelog avec gestion des erreurs améliorée""" try: autolook_dir = r"C:\AUTOLOOK" changelog_file = os.path.join(autolook_dir, "changelog.json") # Charger l'historique existant changelog_history = [] if os.path.exists(changelog_file): try: # Vérifier les permissions de lecture if os.access(changelog_file, os.R_OK): with open(changelog_file, 'r', encoding='utf-8') as f: changelog_history = json.load(f) else: print(f"Permissions de lecture insuffisantes pour {changelog_file}") except (json.JSONDecodeError, Exception) as e: print(f"Erreur lors de la lecture du changelog existant: {e}") # Continuer avec une liste vide changelog_history = [] # Créer la nouvelle entrée new_entry = { 'version': new_version, 'date': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), 'previous_version': self.current_version, 'changes': { 'major': self.changes['major'].copy(), 'minor': self.changes['minor'].copy(), 'patch': self.changes['patch'].copy() } } # Ajouter en tête de liste (plus récent en premier) changelog_history.insert(0, new_entry) # Limiter l'historique à 50 entrées maximum pour éviter des fichiers trop gros if len(changelog_history) > 50: changelog_history = changelog_history[:50] # Vérifier les permissions d'écriture if os.path.exists(changelog_file) and not os.access(changelog_file, os.W_OK): import stat os.chmod(changelog_file, stat.S_IWRITE | stat.S_IREAD) # Sauvegarder avec gestion d'erreur with open(changelog_file, 'w', encoding='utf-8') as f: json.dump(changelog_history, f, indent=4, ensure_ascii=False) print(f"✅ Changelog sauvegardé avec succès dans {changelog_file}") except PermissionError as pe: raise Exception( f"Permissions insuffisantes pour écrire le changelog.\nEssayez de lancer l'application en tant qu'administrateur.\nDétails : {str(pe)}") except Exception as e: raise Exception(f"Erreur lors de la création du changelog : {str(e)}") def reset_all(self): """Remet à zéro toutes les modifications""" reply = QMessageBox.question(self, "Confirmation", "Voulez-vous vraiment effacer toutes les modifications saisies ?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.changes = {'major': [], 'minor': [], 'patch': []} self.changes_input.clear() self.update_preview() self.select_no_files() def validate_and_generate(self): """Valide et génère la mise à jour""" # Vérifier qu'il y a des modifications if not any(self.changes.values()): QMessageBox.warning(self, "Aucune modification", "Veuillez saisir au moins une modification.") return # Calculer la nouvelle version new_version = self.calculate_new_version() # Confirmation finale reply = QMessageBox.question(self, "Confirmation finale", f"Voulez-vous générer la mise à jour vers la version {new_version} ?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: try: # Sauvegarder la nouvelle version self.save_new_version(new_version) # Créer une messagebox personnalisée avec police plus grande msg_box = QMessageBox(self) msg_box.setWindowTitle("Mise à jour terminée") msg_box.setIcon(QMessageBox.Information) msg_box.setText(f"🎉 Mise à jour générée avec succès !\n\n" f"📊 Nouvelle version : {new_version}\n" f"📝 Changelog mis à jour.") # Appliquer un style avec police plus grande msg_box.setStyleSheet(""" QMessageBox { font-size: 14px; font-weight: bold; color: #000000; background-color: #ffffff; min-width: 300px; } QMessageBox QPushButton { font-size: 12px; padding: 10px 20px; min-width: 80px; background-color: #28a745; color: white; border: none; border-radius: 5px; } QMessageBox QPushButton:hover { background-color: #218838; } """) msg_box.exec_() self.accept() # Fermer la fenêtre except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la génération :\n{str(e)}") def center_on_screen(self): """Centre la fenêtre sur l'écran""" from PyQt5.QtWidgets import QDesktopWidget # Obtenir la géométrie de l'écran principal screen = QDesktopWidget().screenGeometry() # Obtenir la géométrie de la fenêtre size = self.geometry() # Calculer la position pour centrer x = (screen.width() - size.width()) // 2 y = (screen.height() - size.height()) // 2 # Éviter que la fenêtre sorte de l'écran if y < 0: y = 0 if x < 0: x = 0 self.move(x, y) def open_notes_interface(self): """Ouvre l'interface de gestion des notes""" notes_dialog = NotesInterface(self) notes_dialog.exec_() class NotesInterface(QDialog): def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.setWindowTitle("📝 Gestionnaire de Notes") self.setMinimumSize(1000, 600) self.setModal(True) # Données des notes self.notes_data = self.load_notes() self.current_editing_id = None # ID de la note en cours d'édition self.setup_ui() self.setup_style() self.refresh_notes_list() self.center_on_screen() def setup_ui(self): """Configure l'interface utilisateur""" main_layout = QVBoxLayout(self) main_layout.setContentsMargins(10, 10, 10, 10) # Titre principal title_label = QLabel("📝 GESTIONNAIRE DE NOTES") title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #3498db; margin-bottom: 15px;") main_layout.addWidget(title_label) # Layout horizontal pour les 3 colonnes columns_layout = QHBoxLayout() # === COLONNE 1: SAISIE (40%) === input_group = self.create_input_column() columns_layout.addWidget(input_group, 4) # 40% # === COLONNE 2: LISTE DES NOTES (30%) === list_group = self.create_list_column() columns_layout.addWidget(list_group, 3) # 30% # === COLONNE 3: APERÇU (30%) === preview_group = self.create_preview_column() columns_layout.addWidget(preview_group, 3) # 30% main_layout.addLayout(columns_layout) # === BOUTONS GÉNÉRAUX === buttons_layout = self.create_general_buttons() main_layout.addLayout(buttons_layout) def create_input_column(self): """Crée la colonne de saisie""" group = QGroupBox("✏️ Saisie de note") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: white; border: 2px solid #27ae60; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #1a2332; } """) layout = QVBoxLayout(group) # Champ titre title_label = QLabel("📝 Titre :") layout.addWidget(title_label) self.title_input = QLineEdit() self.title_input.setPlaceholderText("Titre généré automatiquement...") layout.addWidget(self.title_input) # Champ contenu content_label = QLabel("📄 Contenu :") layout.addWidget(content_label) self.content_input = QTextEdit() self.content_input.setPlaceholderText("Saisissez le contenu de votre note ici...") self.content_input.setMinimumHeight(300) layout.addWidget(self.content_input) # Boutons de la colonne saisie input_buttons = QHBoxLayout() save_btn = QPushButton("💾 Sauvegarder") save_btn.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; font-weight: bold; padding: 10px 15px; } QPushButton:hover { background-color: #229954; } """) save_btn.clicked.connect(self.save_note) clear_btn = QPushButton("🧹 Vider") clear_btn.clicked.connect(self.clear_input_fields) input_buttons.addWidget(save_btn) input_buttons.addWidget(clear_btn) layout.addLayout(input_buttons) return group def create_list_column(self): """Crée la colonne de liste des notes""" group = QGroupBox("📋 Liste des notes") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: white; border: 2px solid #3498db; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #1a2332; } """) layout = QVBoxLayout(group) # Champ de recherche search_label = QLabel("🔍 Rechercher :") layout.addWidget(search_label) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Rechercher dans titre et contenu...") self.search_input.textChanged.connect(self.filter_notes) layout.addWidget(self.search_input) # Liste des notes self.notes_list = QListWidget() self.notes_list.setMinimumHeight(300) self.notes_list.itemSelectionChanged.connect(self.on_note_selected) layout.addWidget(self.notes_list) # Bouton supprimer delete_btn = QPushButton("🗑️ Supprimer sélectionnée") delete_btn.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; font-weight: bold; padding: 10px 15px; } QPushButton:hover { background-color: #c0392b; } """) delete_btn.clicked.connect(self.delete_selected_note) layout.addWidget(delete_btn) return group def create_preview_column(self): """Crée la colonne d'aperçu""" group = QGroupBox("👁️ Aperçu") group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: white; border: 2px solid #f39c12; border-radius: 8px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #1a2332; } """) layout = QVBoxLayout(group) # Titre de la note sélectionnée self.preview_title = QLabel("Aucune note sélectionnée") self.preview_title.setStyleSheet("font-weight: bold; font-size: 14px; color: #f39c12;") self.preview_title.setWordWrap(True) layout.addWidget(self.preview_title) # Contenu de la note sélectionnée self.preview_content = QTextEdit() self.preview_content.setReadOnly(True) self.preview_content.setMinimumHeight(250) self.preview_content.setStyleSheet(""" QTextEdit { background-color: #1a2332; border: 2px solid #f39c12; border-radius: 5px; padding: 8px; color: #ffffff; } """) layout.addWidget(self.preview_content) # Informations sur la note self.preview_info = QLabel("") self.preview_info.setStyleSheet("color: #bdc3c7; font-size: 11px;") self.preview_info.setWordWrap(True) layout.addWidget(self.preview_info) # Bouton modifier edit_btn = QPushButton("✏️ Modifier cette note") edit_btn.setStyleSheet(""" QPushButton { background-color: #f39c12; color: white; font-weight: bold; padding: 10px 15px; } QPushButton:hover { background-color: #e67e22; } """) edit_btn.clicked.connect(self.edit_selected_note) layout.addWidget(edit_btn) return group def create_general_buttons(self): """Crée les boutons généraux""" layout = QHBoxLayout() # Export TXT export_txt_btn = QPushButton("📤 Export TXT") export_txt_btn.setStyleSheet(""" QPushButton { background-color: #8e44ad; color: white; font-weight: bold; padding: 12px 20px; } QPushButton:hover { background-color: #7d3c98; } """) export_txt_btn.clicked.connect(self.export_txt) # Export JSON export_json_btn = QPushButton("📤 Export JSON") export_json_btn.setStyleSheet(""" QPushButton { background-color: #8e44ad; color: white; font-weight: bold; padding: 12px 20px; } QPushButton:hover { background-color: #7d3c98; } """) export_json_btn.clicked.connect(self.export_json) # Import import_btn = QPushButton("📥 Import") import_btn.clicked.connect(self.import_notes) # Fermer close_btn = QPushButton("❌ Fermer") close_btn.clicked.connect(self.close) layout.addWidget(export_txt_btn) layout.addWidget(export_json_btn) layout.addWidget(import_btn) layout.addStretch() layout.addWidget(close_btn) return layout def setup_style(self): """Configure le style de l'interface""" self.setStyleSheet(""" QDialog { background-color: #1A2F5B; color: #ffffff; } QLineEdit { border: 2px solid #34495e; border-radius: 5px; padding: 8px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QLineEdit:focus { border-color: #3498db; } QTextEdit { border: 2px solid #34495e; border-radius: 5px; padding: 8px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QTextEdit:focus { border-color: #3498db; } QListWidget { border: 2px solid #34495e; border-radius: 5px; background-color: #1a2332; font-size: 12px; color: #ffffff; } QListWidget::item { padding: 8px; border-bottom: 1px solid #34495e; color: #ffffff; } QListWidget::item:selected { background-color: #3498db; color: white; } QListWidget::item:hover { background-color: #2980b9; color: #ffffff; } QPushButton { background-color: #3498db; color: white; border: none; padding: 10px 15px; border-radius: 5px; font-weight: bold; font-size: 12px; } QPushButton:hover { background-color: #2980b9; } QLabel { color: #ffffff; font-size: 12px; } """) def generate_auto_title(self): """Génère un titre automatique basé sur la date et l'heure""" now = datetime.now() return f"Note du {now.strftime('%d/%m/%Y')} à {now.strftime('%H:%M')}" def load_notes(self): """Charge les notes depuis le fichier JSON""" notes_file = r"C:\AUTOLOOK\notes.json" try: if os.path.exists(notes_file): with open(notes_file, 'r', encoding='utf-8') as f: data = json.load(f) return data.get('notes', []) else: return [] except Exception as e: QMessageBox.warning(self, "Erreur", f"Erreur lors du chargement des notes:\n{str(e)}") return [] def save_notes(self): """Sauvegarde les notes dans le fichier JSON""" notes_file = r"C:\AUTOLOOK\notes.json" try: # S'assurer que le dossier existe os.makedirs(os.path.dirname(notes_file), exist_ok=True) data = {'notes': self.notes_data} with open(notes_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) return True except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde:\n{str(e)}") return False def save_note(self): """Sauvegarde une note""" title = self.title_input.text().strip() content = self.content_input.toPlainText().strip() # Si pas de titre, générer automatiquement if not title: title = self.generate_auto_title() self.title_input.setText(title) # Vérifier qu'il y a du contenu if not content: QMessageBox.warning(self, "Contenu manquant", "Veuillez saisir du contenu pour la note.") return now = datetime.now().strftime('%d/%m/%Y %H:%M:%S') if self.current_editing_id: # Modification d'une note existante for note in self.notes_data: if note['id'] == self.current_editing_id: note['title'] = title note['content'] = content note['modified'] = now break self.current_editing_id = None else: # Nouvelle note new_note = { 'id': str(uuid.uuid4()), 'title': title, 'content': content, 'created': now, 'modified': now } self.notes_data.append(new_note) # Sauvegarder et rafraîchir if self.save_notes(): self.refresh_notes_list() self.clear_input_fields() QMessageBox.information(self, "Succès", "Note sauvegardée avec succès !") def clear_input_fields(self): """Vide les champs de saisie""" self.title_input.clear() self.content_input.clear() self.current_editing_id = None def refresh_notes_list(self): """Rafraîchit la liste des notes""" self.notes_list.clear() # Trier par date de modification (plus récent en premier) sorted_notes = sorted(self.notes_data, key=lambda x: x['modified'], reverse=True) for note in sorted_notes: # Affichage: titre + date display_text = f"{note['title']}\n📅 {note['modified']}" item = QListWidgetItem(display_text) item.setData(Qt.UserRole, note['id']) self.notes_list.addItem(item) def filter_notes(self): """Filtre les notes selon la recherche""" search_text = self.search_input.text().lower() if not search_text: self.refresh_notes_list() return self.notes_list.clear() # Filtrer les notes filtered_notes = [] for note in self.notes_data: if (search_text in note['title'].lower() or search_text in note['content'].lower()): filtered_notes.append(note) # Trier par date de modification filtered_notes.sort(key=lambda x: x['modified'], reverse=True) for note in filtered_notes: display_text = f"{note['title']}\n📅 {note['modified']}" item = QListWidgetItem(display_text) item.setData(Qt.UserRole, note['id']) self.notes_list.addItem(item) def on_note_selected(self): """Gère la sélection d'une note""" current_item = self.notes_list.currentItem() if not current_item: return note_id = current_item.data(Qt.UserRole) note = next((n for n in self.notes_data if n['id'] == note_id), None) if note: # Mettre à jour l'aperçu self.preview_title.setText(note['title']) self.preview_content.setPlainText(note['content']) info_text = f"Créé : {note['created']}\nModifié : {note['modified']}" self.preview_info.setText(info_text) def edit_selected_note(self): """Charge la note sélectionnée pour modification""" current_item = self.notes_list.currentItem() if not current_item: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner une note à modifier.") return note_id = current_item.data(Qt.UserRole) note = next((n for n in self.notes_data if n['id'] == note_id), None) if note: self.title_input.setText(note['title']) self.content_input.setPlainText(note['content']) self.current_editing_id = note_id def delete_selected_note(self): """Supprime la note sélectionnée""" current_item = self.notes_list.currentItem() if not current_item: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner une note à supprimer.") return note_id = current_item.data(Qt.UserRole) note = next((n for n in self.notes_data if n['id'] == note_id), None) if note: reply = QMessageBox.question( self, "Confirmation de suppression", f"Voulez-vous vraiment supprimer la note :\n\n'{note['title']}' ?\n\nCette action est irréversible.", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.notes_data.remove(note) if self.save_notes(): self.refresh_notes_list() # Vider l'aperçu self.preview_title.setText("Aucune note sélectionnée") self.preview_content.clear() self.preview_info.clear() QMessageBox.information(self, "Suppression", "Note supprimée avec succès !") def export_txt(self): """Exporte les notes en format TXT""" if not self.notes_data: QMessageBox.warning(self, "Aucune note", "Il n'y a aucune note à exporter.") return file_path, _ = QFileDialog.getSaveFileName( self, "Exporter les notes (TXT)", f"C:\\AUTOLOOK\\notes_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", "Fichiers texte (*.txt)" ) if file_path: try: with open(file_path, 'w', encoding='utf-8') as f: f.write("📝 EXPORT DES NOTES\n") f.write("=" * 50 + "\n\n") for i, note in enumerate(self.notes_data, 1): f.write(f"NOTE {i}: {note['title']}\n") f.write("-" * 30 + "\n") f.write(f"Créé : {note['created']}\n") f.write(f"Modifié : {note['modified']}\n\n") f.write("CONTENU :\n") f.write(note['content']) f.write("\n\n" + "=" * 50 + "\n\n") QMessageBox.information(self, "Export réussi", f"Notes exportées vers :\n{file_path}") except Exception as e: QMessageBox.critical(self, "Erreur d'export", f"Erreur lors de l'export :\n{str(e)}") def export_json(self): """Exporte les notes en format JSON""" if not self.notes_data: QMessageBox.warning(self, "Aucune note", "Il n'y a aucune note à exporter.") return file_path, _ = QFileDialog.getSaveFileName( self, "Exporter les notes (JSON)", f"C:\\AUTOLOOK\\notes_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", "Fichiers JSON (*.json)" ) if file_path: try: export_data = { 'export_date': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), 'total_notes': len(self.notes_data), 'notes': self.notes_data } with open(file_path, 'w', encoding='utf-8') as f: json.dump(export_data, f, indent=4, ensure_ascii=False) QMessageBox.information(self, "Export réussi", f"Notes exportées vers :\n{file_path}") except Exception as e: QMessageBox.critical(self, "Erreur d'export", f"Erreur lors de l'export :\n{str(e)}") def import_notes(self): """Importe des notes depuis un fichier JSON""" file_path, _ = QFileDialog.getOpenFileName( self, "Importer des notes", "C:\\AUTOLOOK", "Fichiers JSON (*.json)" ) if file_path: try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) # Vérifier la structure imported_notes = data.get('notes', []) if not imported_notes: QMessageBox.warning(self, "Import", "Aucune note trouvée dans le fichier.") return # Compter les notes importées imported_count = 0 for note in imported_notes: # Vérifier que la note a les champs requis if all(key in note for key in ['title', 'content']): # Générer un nouvel ID si nécessaire if 'id' not in note: note['id'] = str(uuid.uuid4()) # Ajouter les dates si manquantes now = datetime.now().strftime('%d/%m/%Y %H:%M:%S') if 'created' not in note: note['created'] = now if 'modified' not in note: note['modified'] = now self.notes_data.append(note) imported_count += 1 if imported_count > 0: if self.save_notes(): self.refresh_notes_list() QMessageBox.information(self, "Import réussi", f"{imported_count} note(s) importée(s) avec succès !") else: QMessageBox.warning(self, "Import", "Aucune note valide trouvée dans le fichier.") except Exception as e: QMessageBox.critical(self, "Erreur d'import", f"Erreur lors de l'import :\n{str(e)}") def center_on_screen(self): """Centre la fenêtre sur l'écran""" from PyQt5.QtWidgets import QDesktopWidget screen = QDesktopWidget().screenGeometry() size = self.geometry() x = (screen.width() - size.width()) // 2 y = (screen.height() - size.height()) // 2 if y < 0: y = 0 if x < 0: x = 0 self.move(x, y) class FileEditDialog(QDialog): """Dialogue pour éditer un fichier de configuration""" def __init__(self, parent, source_file, config): super().__init__(parent) self.setWindowTitle(f"✏️ Éditer {source_file}") self.setFixedSize(500, 300) self.setModal(True) self.setup_ui(source_file, config) def setup_ui(self, source_file, config): layout = QVBoxLayout(self) # Titre title = QLabel(f"Édition de '{source_file}'") title.setStyleSheet("font-size: 14px; font-weight: bold; margin-bottom: 10px;") layout.addWidget(title) # Formulaire form_layout = QFormLayout() self.source_file_edit = QLineEdit(source_file) form_layout.addRow("Fichier source:", self.source_file_edit) self.id_edit = QLineEdit(config['id']) form_layout.addRow("ID:", self.id_edit) self.target_name_edit = QLineEdit(config['target_name']) form_layout.addRow("Nom cible:", self.target_name_edit) self.target_dir_edit = QLineEdit(config['target_dir']) form_layout.addRow("Dossier cible:", self.target_dir_edit) self.description_edit = QLineEdit(config.get('description', '')) form_layout.addRow("Description:", self.description_edit) layout.addLayout(form_layout) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) def get_data(self): """Retourne les données éditées""" return { 'source_file': self.source_file_edit.text().strip(), 'id': self.id_edit.text().strip(), 'target_name': self.target_name_edit.text().strip(), 'target_dir': self.target_dir_edit.text().strip(), 'description': self.description_edit.text().strip() } class AdminInterface(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("🔐 Interface Administrateur") self.setFixedSize(800, 550) # Largeur / Hauteur self.setStyleSheet(""" QDialog { background-color: #2c3e50; color: white; } QLabel { color: white; font-size: 12px; } QPushButton { background-color: #3498db; color: white; border: none; padding: 12px; border-radius: 6px; font-weight: bold; font-size: 13px; margin: 5px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #21618c; } QGroupBox { color: white; border: 2px solid #34495e; border-radius: 8px; margin-top: 15px; padding-top: 10px; font-weight: bold; font-size: 14px; } QGroupBox::title { subcontrol-origin: margin; left: 15px; padding: 0 10px 0 10px; background-color: #2c3e50; } """) self.setup_ui() # Initialiser l'affichage IP self.setup_ip_display() def setup_ui(self): main_layout = QVBoxLayout(self) main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # === EN-TÊTE === title = QLabel("🔐 INTERFACE ADMINISTRATEUR") title.setFont(QFont("Arial", 16, QFont.Bold)) title.setAlignment(Qt.AlignCenter) title.setStyleSheet(""" QLabel { color: #ecf0f1; background-color: #34495e; padding: 12px; border-radius: 6px; margin-bottom: 8px; } """) main_layout.addWidget(title) # === LAYOUT PRINCIPAL EN COLONNES === columns_layout = QHBoxLayout() columns_layout.setSpacing(12) # === COLONNE 1: MISE À JOUR === col1_layout = QVBoxLayout() col1_layout.setSpacing(8) # Groupe Mise à jour update_group = QGroupBox("🔄 Mises à jour") update_group.setStyleSheet(self.get_group_style()) update_layout = QVBoxLayout() update_layout.setSpacing(6) # Version actuelle (plus compacte) current_version = self.get_current_version() version_info = QLabel(f"Version : {current_version}") version_info.setAlignment(Qt.AlignCenter) version_info.setStyleSheet(""" QLabel { color: #3498db; background-color: rgba(52, 152, 219, 0.2); padding: 6px; border-radius: 4px; font-weight: bold; font-size: 11px; } """) update_layout.addWidget(version_info) # Boutons mise à jour (plus petits) self.update_interface_button = QPushButton("🚀 Interface de mise à jour") self.update_interface_button.setStyleSheet(self.get_button_style("#e74c3c")) self.update_interface_button.clicked.connect(self.open_update_interface) update_layout.addWidget(self.update_interface_button) # Boutons secondaires sur une ligne secondary_buttons_layout = QHBoxLayout() secondary_buttons_layout.setSpacing(4) changelog_button = QPushButton("📋") changelog_button.setToolTip("Voir le changelog") changelog_button.setStyleSheet(self.get_small_button_style()) changelog_button.clicked.connect(self.show_changelog) version_button = QPushButton("ℹ️") version_button.setToolTip("Infos version") version_button.setStyleSheet(self.get_small_button_style()) version_button.clicked.connect(self.show_version_info) secondary_buttons_layout.addWidget(changelog_button) secondary_buttons_layout.addWidget(version_button) secondary_buttons_layout.addStretch() update_layout.addLayout(secondary_buttons_layout) update_group.setLayout(update_layout) col1_layout.addWidget(update_group) # Groupe Informations système system_info_group = self.create_compact_system_info_section() col1_layout.addWidget(system_info_group) col1_layout.addStretch() columns_layout.addLayout(col1_layout) # === COLONNE 2: OUTILS SYSTÈME === col2_layout = QVBoxLayout() col2_layout.setSpacing(8) # Groupe Outils système system_group = QGroupBox("🛠️ Outils système") system_group.setStyleSheet(self.get_group_style()) system_layout = QVBoxLayout() system_layout.setSpacing(6) # Première ligne d'outils tools_row1 = QHBoxLayout() tools_row1.setSpacing(6) clean_temp_button = QPushButton("🧹 Nettoyer temp") clean_temp_button.setStyleSheet(self.get_button_style("#3498db")) clean_temp_button.clicked.connect(self.clean_temp_files) logs_button = QPushButton("📜 Logs") logs_button.setStyleSheet(self.get_button_style("#3498db")) logs_button.clicked.connect(self.show_logs) tools_row1.addWidget(clean_temp_button) tools_row1.addWidget(logs_button) system_layout.addLayout(tools_row1) # Deuxième ligne d'outils info_button = QPushButton("💻 Informations système") info_button.setStyleSheet(self.get_button_style("#9b59b6")) info_button.clicked.connect(self.show_system_info) system_layout.addWidget(info_button) system_group.setLayout(system_layout) col2_layout.addWidget(system_group) # Groupe Maintenance maintenance_group = QGroupBox("⚙️ Maintenance") maintenance_group.setStyleSheet(self.get_group_style()) maintenance_layout = QVBoxLayout() maintenance_layout.setSpacing(6) # Boutons de maintenance backup_button = QPushButton("💾 Sauvegarde") backup_button.setStyleSheet(self.get_button_style("#27ae60")) backup_button.clicked.connect(self.create_backup) restore_button = QPushButton("📁 Restauration") restore_button.setStyleSheet(self.get_button_style("#f39c12")) restore_button.clicked.connect(self.restore_backup) maintenance_layout.addWidget(backup_button) maintenance_layout.addWidget(restore_button) maintenance_group.setLayout(maintenance_layout) col2_layout.addWidget(maintenance_group) col2_layout.addStretch() columns_layout.addLayout(col2_layout) # === COLONNE 3: MONITORING === col3_layout = QVBoxLayout() col3_layout.setSpacing(8) # Groupe Monitoring monitoring_group = QGroupBox("📊 Monitoring") monitoring_group.setStyleSheet(self.get_group_style()) monitoring_layout = QVBoxLayout() monitoring_layout.setSpacing(6) # Indicateurs système (compacts) self.cpu_indicator = self.create_indicator("CPU", "#e74c3c") self.ram_indicator = self.create_indicator("RAM", "#f39c12") self.disk_indicator = self.create_indicator("Disque", "#3498db") monitoring_layout.addWidget(self.cpu_indicator) monitoring_layout.addWidget(self.ram_indicator) monitoring_layout.addWidget(self.disk_indicator) # Bouton monitoring détaillé detailed_monitoring_button = QPushButton("📈 Monitoring détaillé") detailed_monitoring_button.setStyleSheet(self.get_button_style("#8e44ad")) detailed_monitoring_button.clicked.connect(self.show_detailed_monitoring) monitoring_layout.addWidget(detailed_monitoring_button) monitoring_group.setLayout(monitoring_layout) col3_layout.addWidget(monitoring_group) # Groupe Actions rapides quick_actions_group = QGroupBox("⚡ Actions rapides") quick_actions_group.setStyleSheet(self.get_group_style()) quick_actions_layout = QVBoxLayout() quick_actions_layout.setSpacing(6) restart_app_button = QPushButton("🔄 Redémarrer app") restart_app_button.setStyleSheet(self.get_button_style("#e67e22")) restart_app_button.clicked.connect(self.restart_application) emergency_stop_button = QPushButton("🛑 Arrêt d'urgence") emergency_stop_button.setStyleSheet(self.get_button_style("#c0392b")) emergency_stop_button.clicked.connect(self.emergency_stop) quick_actions_layout.addWidget(restart_app_button) quick_actions_layout.addWidget(emergency_stop_button) quick_actions_group.setLayout(quick_actions_layout) col3_layout.addWidget(quick_actions_group) col3_layout.addStretch() columns_layout.addLayout(col3_layout) main_layout.addLayout(columns_layout) # === BOUTONS DE CONTRÔLE (EN BAS) === control_layout = QHBoxLayout() control_layout.setSpacing(10) # Bouton Fermer close_button = QPushButton("❌ Fermer") close_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; border: none; padding: 10px 20px; border-radius: 5px; font-weight: bold; font-size: 12px; min-width: 100px; } QPushButton:hover { background-color: #c0392b; } """) close_button.clicked.connect(self.close) control_layout.addStretch() control_layout.addWidget(close_button) main_layout.addLayout(control_layout) # Démarrer les timers de monitoring self.start_monitoring_timers() def get_group_style(self): """Style pour les groupes""" return """ QGroupBox { color: white; border: 1px solid #34495e; border-radius: 6px; margin-top: 12px; padding-top: 8px; font-weight: bold; font-size: 12px; background-color: rgba(52, 73, 94, 0.3); } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 8px 0 8px; background-color: #2c3e50; border-radius: 3px; } """ def get_button_style(self, color): """Style pour les boutons normaux""" return f""" QPushButton {{ background-color: {color}; color: white; border: none; padding: 8px 10px; border-radius: 4px; font-weight: bold; font-size: 10px; min-height: 28px; }} QPushButton:hover {{ background-color: {self.darken_color(color)}; }} QPushButton:pressed {{ background-color: {self.darken_color(color, 0.8)}; }} """ def get_small_button_style(self): """Style pour les petits boutons""" return """ QPushButton { background-color: #34495e; color: white; border: none; padding: 6px; border-radius: 3px; font-weight: bold; font-size: 12px; min-width: 30px; max-width: 30px; } QPushButton:hover { background-color: #4a6278; } """ def darken_color(self, color, factor=0.85): """Assombrit une couleur hexadécimale""" try: # Supprimer le # si présent color = color.lstrip('#') # Convertir en RGB r = int(color[0:2], 16) g = int(color[2:4], 16) b = int(color[4:6], 16) # Assombrir r = int(r * factor) g = int(g * factor) b = int(b * factor) # Reconvertir en hex return f"#{r:02x}{g:02x}{b:02x}" except: return "#2c3e50" # Couleur par défaut def create_compact_system_info_section(self): """Crée la section d'informations système compacte""" system_info_group = QGroupBox("🖥️ Système") system_info_group.setStyleSheet(self.get_group_style()) system_info_layout = QVBoxLayout() system_info_layout.setSpacing(4) # IP (compact) ip_widget = QWidget() ip_layout = QHBoxLayout(ip_widget) ip_layout.setContentsMargins(4, 2, 4, 2) ip_layout.setSpacing(6) ip_label = QLabel("🌐") ip_label.setStyleSheet("color: #3498db; font-size: 12px;") self.ip_display_label = QLabel("Chargement...") self.ip_display_label.setStyleSheet("color: #30f472; font-size: 10px; font-weight: bold;") ip_layout.addWidget(ip_label) ip_layout.addWidget(self.ip_display_label) ip_layout.addStretch() system_info_layout.addWidget(ip_widget) # Résolution (compact) resolution_widget = QWidget() resolution_layout = QHBoxLayout(resolution_widget) resolution_layout.setContentsMargins(4, 2, 4, 2) resolution_layout.setSpacing(6) resolution_label = QLabel("🖥️") resolution_label.setStyleSheet("color: #f39c12; font-size: 12px;") self.resolution_display_label = QLabel("Chargement...") self.resolution_display_label.setStyleSheet("color: #f39c12; font-size: 10px; font-weight: bold;") resolution_layout.addWidget(resolution_label) resolution_layout.addWidget(self.resolution_display_label) resolution_layout.addStretch() system_info_layout.addWidget(resolution_widget) # Bouton détails details_button = QPushButton("📊 Détails") details_button.setStyleSheet(self.get_button_style("#9b59b6")) details_button.clicked.connect(self.show_screen_details) system_info_layout.addWidget(details_button) system_info_group.setLayout(system_info_layout) return system_info_group def create_indicator(self, name, color): """Crée un indicateur système compact""" widget = QWidget() widget.setStyleSheet(f""" QWidget {{ background-color: rgba(44, 62, 80, 0.3); border-radius: 4px; padding: 4px; }} """) layout = QHBoxLayout(widget) layout.setContentsMargins(6, 4, 6, 4) layout.setSpacing(8) # Label du nom name_label = QLabel(name) name_label.setStyleSheet("color: white; font-size: 10px; font-weight: bold;") name_label.setFixedWidth(40) # Barre de progression progress = QProgressBar() progress.setRange(0, 100) progress.setValue(0) progress.setFixedHeight(12) progress.setStyleSheet(f""" QProgressBar {{ background-color: #34495e; border-radius: 6px; text-align: center; }} QProgressBar::chunk {{ background-color: {color}; border-radius: 6px; }} """) # Label de valeur value_label = QLabel("0%") value_label.setStyleSheet("color: white; font-size: 10px;") value_label.setFixedWidth(30) value_label.setAlignment(Qt.AlignRight) layout.addWidget(name_label) layout.addWidget(progress) layout.addWidget(value_label) # Stocker les références pour la mise à jour setattr(self, f"{name.lower()}_progress", progress) setattr(self, f"{name.lower()}_value_label", value_label) return widget def start_monitoring_timers(self): """Démarre les timers de surveillance""" # Timer pour les informations système self.system_info_timer = QTimer(self) self.system_info_timer.timeout.connect(self.update_system_info) self.system_info_timer.start(30000) # 30 secondes # Timer pour le monitoring self.monitoring_timer = QTimer(self) self.monitoring_timer.timeout.connect(self.update_monitoring) self.monitoring_timer.start(2000) # 2 secondes # Mise à jour initiale self.update_system_info() self.update_monitoring() def update_monitoring(self): """Met à jour les indicateurs de monitoring""" try: import psutil # CPU cpu_percent = psutil.cpu_percent() if hasattr(self, 'cpu_progress'): self.cpu_progress.setValue(int(cpu_percent)) self.cpu_value_label.setText(f"{cpu_percent:.0f}%") # RAM ram_percent = psutil.virtual_memory().percent if hasattr(self, 'ram_progress'): self.ram_progress.setValue(int(ram_percent)) self.ram_value_label.setText(f"{ram_percent:.0f}%") # Disque disk_percent = psutil.disk_usage('C:').percent if hasattr(self, 'disk_progress'): self.disk_progress.setValue(int(disk_percent)) self.disk_value_label.setText(f"{disk_percent:.0f}%") except Exception as e: print(f"Erreur mise à jour monitoring: {e}") # Nouvelles méthodes pour les fonctionnalités def create_backup(self): """Crée une sauvegarde complète de l'application""" try: # Dialogue de configuration de sauvegarde dialog = BackupConfigDialog(self) if dialog.exec_() != QDialog.Accepted: return backup_config = dialog.get_config() # Créer la sauvegarde backup_manager = AdminBackupManager() success, backup_path = backup_manager.create_backup(backup_config) if success: QMessageBox.information(self, "Sauvegarde réussie", f"Sauvegarde créée avec succès:\n{backup_path}") else: QMessageBox.critical(self, "Erreur", "Erreur lors de la création de la sauvegarde") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde: {str(e)}") def restore_backup(self): """Restaure une sauvegarde""" try: # Sélectionner le fichier de sauvegarde backup_file, _ = QFileDialog.getOpenFileName( self, "Sélectionner une sauvegarde", "", "Fichiers de sauvegarde (*.zip);;Tous les fichiers (*)" ) if not backup_file: return # Vérifier la sauvegarde backup_manager = AdminBackupManager() is_valid, backup_info = backup_manager.validate_backup(backup_file) if not is_valid: QMessageBox.critical(self, "Erreur", "Fichier de sauvegarde invalide") return # Dialogue de confirmation avec détails dialog = RestoreConfirmDialog(self, backup_info) if dialog.exec_() != QDialog.Accepted: return # Effectuer la restauration success = backup_manager.restore_backup(backup_file, dialog.get_restore_options()) if success: QMessageBox.information(self, "Restauration réussie", "Restauration terminée avec succès.\n" "Redémarrez l'application pour prendre en compte les changements.") else: QMessageBox.critical(self, "Erreur", "Erreur lors de la restauration") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la restauration: {str(e)}") def show_detailed_monitoring(self): """Affiche une interface de monitoring détaillé avec graphiques temps réel""" try: # Créer la fenêtre de monitoring self.monitoring_dialog = DetailedMonitoringDialog(self) self.monitoring_dialog.show() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture du monitoring: {str(e)}") def restart_application(self): """Redémarre l'application""" try: reply = QMessageBox.question(self, "Redémarrage", "Voulez-vous vraiment redémarrer l'application ?\n\n" "⚠️ Toutes les opérations en cours seront interrompues.", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: # Sauvegarder l'état actuel QMessageBox.information(self, "Redémarrage", "Redémarrage de l'application...\n" "L'application va se fermer et redémarrer automatiquement.") # Fermer proprement et redémarrer import sys import subprocess # Obtenir le chemin du script principal if hasattr(sys, '_MEIPASS'): # Si exécutable PyInstaller executable = sys.executable subprocess.Popen([executable]) else: # Si script Python python = sys.executable script = sys.argv[0] subprocess.Popen([python, script]) # Fermer l'application actuelle self.parent().close() if self.parent() else sys.exit() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur redémarrage: {str(e)}") def emergency_stop(self): """Arrêt d'urgence""" try: reply = QMessageBox.question(self, "🛑 ARRÊT D'URGENCE", "⚠️ ATTENTION: ARRÊT D'URGENCE ⚠️\n\n" "Cette action va :\n" "• Interrompre TOUS les processus en cours\n" "• Fermer toutes les connexions\n" "• Arrêter l'application immédiatement\n\n" "❗ Les données non sauvegardées seront perdues ❗\n\n" "Confirmez-vous l'arrêt d'urgence ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: try: # Arrêter tous les timers for obj in self.findChildren(QTimer): obj.stop() # Fermer toutes les fenêtres enfants for window in QApplication.topLevelWidgets(): if window != self and window.isVisible(): window.close() # Message final QMessageBox.critical(self, "Arrêt d'urgence", "🛑 ARRÊT D'URGENCE ACTIVÉ\n\n" "L'application va se fermer immédiatement.") # Fermeture forcée import sys sys.exit(1) except: # En cas d'erreur même dans l'arrêt d'urgence import os os._exit(1) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur arrêt d'urgence: {str(e)}") # Arrêt forcé même en cas d'erreur import os os._exit(1) def create_system_info_section(self): """Crée la section d'affichage des informations système""" system_info_group = QGroupBox("🖥️ Informations système") system_info_layout = QVBoxLayout() # === FRAME IP === ip_frame = QFrame() ip_frame.setStyleSheet(""" QFrame { background-color: #062a3c; border-radius: 6px; padding: 8px; margin: 3px 0; } QLabel { color: white; font-size: 12px; } """) ip_frame_layout = QHBoxLayout(ip_frame) ip_frame_layout.setSpacing(10) ip_frame_layout.setContentsMargins(10, 8, 10, 8) # Titre et valeur IP ip_title = QLabel("🌐 Adresse IP locale :") ip_title.setStyleSheet("color: #bdc3c7; font-size: 12px; font-weight: bold;") self.ip_display_label = QLabel("Chargement...") self.ip_display_label.setStyleSheet("color: #30f472; font-size: 12px; font-weight: bold;") self.ip_display_label.setAlignment(Qt.AlignRight) ip_frame_layout.addWidget(ip_title) ip_frame_layout.addStretch() ip_frame_layout.addWidget(self.ip_display_label) system_info_layout.addWidget(ip_frame) # === FRAME RÉSOLUTION === resolution_frame = QFrame() resolution_frame.setStyleSheet(""" QFrame { background-color: #2c3e50; border-radius: 6px; padding: 8px; margin: 3px 0; } QLabel { color: white; font-size: 12px; } """) resolution_frame_layout = QHBoxLayout(resolution_frame) resolution_frame_layout.setSpacing(10) resolution_frame_layout.setContentsMargins(10, 8, 10, 8) # Titre et valeur résolution resolution_title = QLabel("🖥️ Résolution d'écran :") resolution_title.setStyleSheet("color: #bdc3c7; font-size: 12px; font-weight: bold;") self.resolution_display_label = QLabel("Chargement...") self.resolution_display_label.setStyleSheet("color: #f39c12; font-size: 12px; font-weight: bold;") self.resolution_display_label.setAlignment(Qt.AlignRight) resolution_frame_layout.addWidget(resolution_title) resolution_frame_layout.addStretch() resolution_frame_layout.addWidget(self.resolution_display_label) system_info_layout.addWidget(resolution_frame) # === FRAME INFORMATIONS SUPPLÉMENTAIRES === extra_info_frame = QFrame() extra_info_frame.setStyleSheet(""" QFrame { background-color: #8e44ad; border-radius: 6px; padding: 8px; margin: 3px 0; } QLabel { color: white; font-size: 11px; } """) extra_info_layout = QVBoxLayout(extra_info_frame) extra_info_layout.setSpacing(4) extra_info_layout.setContentsMargins(10, 6, 10, 6) # Nombre d'écrans self.screen_count_label = QLabel("📺 Nombre d'écrans : Chargement...") self.screen_count_label.setStyleSheet("color: white; font-size: 11px;") extra_info_layout.addWidget(self.screen_count_label) # Écran principal self.primary_screen_label = QLabel("⭐ Écran principal : Chargement...") self.primary_screen_label.setStyleSheet("color: white; font-size: 11px;") extra_info_layout.addWidget(self.primary_screen_label) # DPI self.dpi_label = QLabel("🔍 DPI : Chargement...") self.dpi_label.setStyleSheet("color: white; font-size: 11px;") extra_info_layout.addWidget(self.dpi_label) system_info_layout.addWidget(extra_info_frame) # === BOUTONS D'ACTION === buttons_layout = QHBoxLayout() buttons_layout.setSpacing(8) # Bouton actualiser refresh_button = QPushButton("🔄 Actualiser") refresh_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #2980b9; } """) refresh_button.clicked.connect(self.update_system_info) # Bouton détails écrans details_button = QPushButton("📊 Détails écrans") details_button.setStyleSheet(""" QPushButton { background-color: #e67e22; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #d35400; } """) details_button.clicked.connect(self.show_screen_details) buttons_layout.addWidget(refresh_button) buttons_layout.addWidget(details_button) buttons_layout.addStretch() system_info_layout.addLayout(buttons_layout) system_info_group.setLayout(system_info_layout) return system_info_group def create_update_section(self): """Crée la section de gestion des mises à jour""" update_group = QGroupBox("🔄 Gestion des mises à jour") update_layout = QVBoxLayout() # Informations sur la version actuelle current_version = self.get_current_version() version_info = QLabel(f"📊 Version actuelle : {current_version}") version_info.setAlignment(Qt.AlignCenter) version_info.setStyleSheet(""" QLabel { color: #3498db; background-color: rgba(52, 152, 219, 0.2); padding: 10px; border-radius: 5px; font-weight: bold; font-size: 14px; margin: 5px; } """) update_layout.addWidget(version_info) # Bouton principal pour l'interface de mise à jour self.update_interface_button = QPushButton("🚀 Ouvrir l'interface de mise à jour") self.update_interface_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #e74c3c, stop:1 #c0392b); color: white; border: none; padding: 15px 25px; border-radius: 8px; font-weight: bold; font-size: 15px; margin: 10px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #c0392b, stop:1 #a93226); transform: translateY(-2px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a93226, stop:1 #8b2b1f); transform: translateY(1px); } """) self.update_interface_button.clicked.connect(self.open_update_interface) update_layout.addWidget(self.update_interface_button) # Boutons secondaires secondary_buttons = QHBoxLayout() # Bouton voir le changelog changelog_button = QPushButton("📋 Voir le changelog") changelog_button.clicked.connect(self.show_changelog) # Bouton infos version version_button = QPushButton("ℹ️ Infos version") version_button.clicked.connect(self.show_version_info) secondary_buttons.addWidget(changelog_button) secondary_buttons.addWidget(version_button) update_layout.addLayout(secondary_buttons) update_group.setLayout(update_layout) return update_group def create_system_section(self): """Crée la section des outils système""" system_group = QGroupBox("🛠️ Outils système") system_layout = QVBoxLayout() # Boutons d'outils système tools_layout = QHBoxLayout() # Nettoyer les fichiers temporaires clean_temp_button = QPushButton("🧹 Nettoyer fichiers temp") clean_temp_button.clicked.connect(self.clean_temp_files) # Voir les logs logs_button = QPushButton("📜 Voir les logs") logs_button.clicked.connect(self.show_logs) tools_layout.addWidget(clean_temp_button) tools_layout.addWidget(logs_button) system_layout.addLayout(tools_layout) # Informations système info_button = QPushButton("💻 Informations système") info_button.clicked.connect(self.show_system_info) system_layout.addWidget(info_button) system_group.setLayout(system_layout) return system_group def get_current_version(self): """Récupère la version actuelle du système""" try: # Essayer de lire depuis le fichier de version version_file = r"C:\AUTOLOOK\version.json" if os.path.exists(version_file): with open(version_file, 'r', encoding='utf-8') as f: data = json.load(f) return data.get('version', '1.0.0') else: return '1.0.0' # Version par défaut except: return '1.0.0' def setup_ip_display(self): """Configure l'affichage des informations système dans l'interface admin""" try: # Initialiser l'affichage des informations système self.update_system_info() # Timer pour mettre à jour les informations périodiquement self.system_info_timer = QTimer(self) self.system_info_timer.timeout.connect(self.update_system_info) self.system_info_timer.start(30000) # Mise à jour toutes les 30 secondes except Exception as e: print(f"Erreur lors de l'initialisation de l'affichage système: {e}") def update_system_info(self): """Met à jour toutes les informations système""" self.update_local_ip() self.update_screen_resolution() def update_local_ip(self): """Met à jour l'affichage de l'IP locale""" try: import socket hostname = socket.gethostname() ip = socket.gethostbyname(hostname) # Mettre à jour le label IP s'il existe if hasattr(self, 'ip_display_label'): self.ip_display_label.setText(ip) except Exception as e: if hasattr(self, 'ip_display_label'): self.ip_display_label.setText("Non disponible") print(f"Erreur lors de la mise à jour de l'IP: {e}") def update_screen_resolution(self): """Met à jour l'affichage de la résolution d'écran""" try: from PyQt5.QtWidgets import QApplication, QDesktopWidget # Obtenir l'application et le desktop widget app = QApplication.instance() desktop = app.desktop() # Écran principal primary_screen = app.primaryScreen() primary_geometry = primary_screen.geometry() primary_resolution = f"{primary_geometry.width()}x{primary_geometry.height()}" # Nombre total d'écrans screen_count = desktop.screenCount() # DPI de l'écran principal dpi = primary_screen.logicalDotsPerInch() # Mettre à jour les labels if hasattr(self, 'resolution_display_label'): self.resolution_display_label.setText(primary_resolution) if hasattr(self, 'screen_count_label'): self.screen_count_label.setText(f"📺 Nombre d'écrans : {screen_count}") if hasattr(self, 'primary_screen_label'): screen_name = primary_screen.name() if hasattr(primary_screen, 'name') else "Écran principal" self.primary_screen_label.setText(f"⭐ Écran principal : {screen_name}") if hasattr(self, 'dpi_label'): self.dpi_label.setText(f"🔍 DPI : {dpi:.0f}") except Exception as e: if hasattr(self, 'resolution_display_label'): self.resolution_display_label.setText("Non disponible") if hasattr(self, 'screen_count_label'): self.screen_count_label.setText("📺 Nombre d'écrans : Erreur") if hasattr(self, 'primary_screen_label'): self.primary_screen_label.setText("⭐ Écran principal : Erreur") if hasattr(self, 'dpi_label'): self.dpi_label.setText("🔍 DPI : Erreur") print(f"Erreur lors de la mise à jour de la résolution: {e}") def show_screen_details(self): """Affiche les détails de tous les écrans dans une fenêtre popup""" try: from PyQt5.QtWidgets import QApplication, QDesktopWidget, QDialog, QVBoxLayout, QTextEdit, QPushButton # Créer la fenêtre de détails dialog = QDialog(self) dialog.setWindowTitle("📊 Détails des écrans") dialog.setMinimumSize(500, 400) dialog.setStyleSheet(self.styleSheet()) layout = QVBoxLayout(dialog) # Zone de texte pour afficher les détails details_text = QTextEdit() details_text.setReadOnly(True) details_text.setStyleSheet(""" QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 10px; font-family: Consolas, monospace; font-size: 11px; } """) # Récupérer les informations détaillées app = QApplication.instance() desktop = app.desktop() details = "🖥️ INFORMATIONS DÉTAILLÉES DES ÉCRANS\n" details += "=" * 50 + "\n\n" # Écran principal primary_screen = app.primaryScreen() primary_geometry = primary_screen.geometry() details += f"📺 ÉCRAN PRINCIPAL\n" details += f" Nom : {primary_screen.name() if hasattr(primary_screen, 'name') else 'Écran principal'}\n" details += f" Résolution : {primary_geometry.width()}x{primary_geometry.height()}\n" details += f" Position : ({primary_geometry.x()}, {primary_geometry.y()})\n" details += f" DPI logique : {primary_screen.logicalDotsPerInch():.1f}\n" details += f" DPI physique : {primary_screen.physicalDotsPerInch():.1f}\n" details += f" Facteur d'échelle : {primary_screen.devicePixelRatio():.2f}\n" details += f" Profondeur de couleur : {primary_screen.depth()} bits\n\n" # Tous les écrans screen_count = desktop.screenCount() details += f"📊 RÉSUMÉ ({screen_count} écran{'s' if screen_count > 1 else ''})\n" details += "-" * 30 + "\n" total_width = 0 total_height = 0 for i in range(screen_count): screen_geometry = desktop.screenGeometry(i) details += f" Écran {i + 1} : {screen_geometry.width()}x{screen_geometry.height()}" if i == desktop.primaryScreen(): details += " (Principal)" details += "\n" # Calculer la résolution totale (bureau étendu) total_width = max(total_width, screen_geometry.x() + screen_geometry.width()) total_height = max(total_height, screen_geometry.y() + screen_geometry.height()) details += f"\n🖼️ BUREAU VIRTUEL TOTAL\n" details += f" Résolution totale : {total_width}x{total_height}\n" details += f" Surface totale : {(total_width * total_height) / 1000000:.1f} Mpx\n\n" # Informations système additionnelles details += f"ℹ️ INFORMATIONS SUPPLÉMENTAIRES\n" details += f" Mise à jour automatique : Toutes les 30 secondes\n" details += f" Support multi-écrans : {'Oui' if screen_count > 1 else 'Non'}\n" details_text.setPlainText(details) layout.addWidget(details_text) # Bouton fermer close_button = QPushButton("Fermer") close_button.clicked.connect(dialog.close) layout.addWidget(close_button) dialog.exec_() except Exception as e: print(f"Erreur lors de l'affichage des détails d'écran: {e}") from PyQt5.QtWidgets import QMessageBox QMessageBox.critical(self, "Erreur", f"Impossible d'afficher les détails d'écran:\n{str(e)}") def open_update_interface(self): """Ouvre l'interface de mise à jour""" try: update_dialog = UpdateInterface(self) result = update_dialog.exec_() # Si la mise à jour a été effectuée, actualiser l'affichage if result == QDialog.Accepted: # Actualiser l'affichage de la version self.setup_ui() QMessageBox.information(self, "Mise à jour", "L'interface a été actualisée avec la nouvelle version.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture de l'interface de mise à jour :\n{str(e)}") def show_changelog(self): """Affiche le changelog""" try: changelog_file = r"C:\AUTOLOOK\changelog.json" if os.path.exists(changelog_file): with open(changelog_file, 'r', encoding='utf-8') as f: changelog_data = json.load(f) # Créer une fenêtre pour afficher le changelog self.show_changelog_dialog(changelog_data) else: QMessageBox.information(self, "Changelog", "Aucun changelog disponible.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la lecture du changelog :\n{str(e)}") def show_changelog_dialog(self, changelog_data): """Affiche le changelog dans une fenêtre dédiée""" dialog = QDialog(self) dialog.setWindowTitle("📋 Changelog") dialog.setMinimumSize(600, 400) dialog.setStyleSheet(self.styleSheet()) layout = QVBoxLayout(dialog) # Zone de texte pour le changelog changelog_text = QTextEdit() changelog_text.setReadOnly(True) # Formatter le changelog content = "📋 HISTORIQUE DES VERSIONS\n" content += "=" * 50 + "\n\n" for entry in changelog_data: content += f"🔢 Version {entry['version']} - {entry['date']}\n" content += f" (depuis la version {entry['previous_version']})\n\n" if entry['changes']['major']: content += " 🚀 CHANGEMENTS MAJEURS :\n" for change in entry['changes']['major']: content += f" • {change}\n" content += "\n" if entry['changes']['minor']: content += " ⚡ CHANGEMENTS MINEURS :\n" for change in entry['changes']['minor']: content += f" • {change}\n" content += "\n" if entry['changes']['patch']: content += " 🔧 CORRECTIFS :\n" for change in entry['changes']['patch']: content += f" • {change}\n" content += "\n" content += "-" * 50 + "\n\n" changelog_text.setPlainText(content) layout.addWidget(changelog_text) # Bouton fermer close_btn = QPushButton("Fermer") close_btn.clicked.connect(dialog.close) layout.addWidget(close_btn) dialog.exec_() def show_version_info(self): """Affiche les informations détaillées de la version""" try: version_file = r"C:\AUTOLOOK\version.json" if os.path.exists(version_file): with open(version_file, 'r', encoding='utf-8') as f: data = json.load(f) info = f"🔢 Version actuelle : {data.get('version', 'Inconnue')}\n" info += f"📅 Dernière mise à jour : {data.get('last_updated', 'Inconnue')}\n" info += f"🔄 Version précédente : {data.get('previous_version', 'Inconnue')}" QMessageBox.information(self, "Informations de version", info) else: QMessageBox.information(self, "Informations de version", "Aucune information de version disponible.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la lecture des informations :\n{str(e)}") def clean_temp_files(self): """Nettoie les fichiers temporaires""" try: temp_dirs = [ r"C:\AUTOLOOK\temp", r"C:\AUTOLOOK\cache", r"C:\AUTOLOOK\logs\old" ] cleaned_count = 0 for temp_dir in temp_dirs: if os.path.exists(temp_dir): for file in os.listdir(temp_dir): file_path = os.path.join(temp_dir, file) try: if os.path.isfile(file_path): os.remove(file_path) cleaned_count += 1 except: continue QMessageBox.information(self, "Nettoyage", f"Nettoyage terminé : {cleaned_count} fichier(s) supprimé(s).") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors du nettoyage :\n{str(e)}") def show_logs(self): """Affiche les logs récents""" QMessageBox.information(self, "Logs", "Fonctionnalité de visualisation des logs à implémenter.") def show_system_info(self): """Affiche les informations système""" import platform import psutil try: info = f"💻 Système : {platform.system()} {platform.release()}\n" info += f"🖥️ Processeur : {platform.processor()}\n" info += f"💾 RAM : {psutil.virtual_memory().total // (1024 ** 3)} GB\n" info += f"📊 CPU : {psutil.cpu_percent()}% utilisé\n" info += f"💿 Disque : {psutil.disk_usage('C:').free // (1024 ** 3)} GB libre" QMessageBox.information(self, "Informations système", info) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la récupération des infos :\n{str(e)}") class DetailedMonitoringDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("📈 Monitoring Détaillé du Système") self.setMinimumSize(1000, 700) self.setStyleSheet(""" QDialog { background-color: #1a1a1a; color: white; } QLabel { color: white; font-size: 12px; } QGroupBox { color: white; border: 1px solid #404040; border-radius: 6px; margin-top: 12px; padding-top: 8px; font-weight: bold; background-color: rgba(64, 64, 64, 0.3); } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 8px 0 8px; background-color: #1a1a1a; border-radius: 3px; } QTableWidget { background-color: #2a2a2a; alternate-background-color: #333333; selection-background-color: #0078d4; gridline-color: #404040; color: white; border: 1px solid #404040; } QHeaderView::section { background-color: #404040; color: white; border: 1px solid #606060; padding: 4px; } """) # Historique des données pour les graphiques self.cpu_history = [] self.ram_history = [] self.disk_history = [] self.network_history = [] self.timestamps = [] # Variables pour gérer les interactions utilisateur self.user_is_filtering = False self.user_selected_process = None self.last_filter_text = "" self.setup_monitoring_ui() # Timer pour la mise à jour self.update_timer = QTimer() self.update_timer.timeout.connect(self.update_data) self.update_timer.start(5000) # Mise à jour chaque seconde # Première mise à jour self.update_data() QTimer.singleShot(1000, self.check_admin_privileges) def setup_monitoring_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) # Onglets tabs = QTabWidget() # Onglet Graphiques graphs_tab = self.create_graphs_tab() tabs.addTab(graphs_tab, "📈 Graphiques") # Onglet Processus processes_tab = self.create_processes_tab() tabs.addTab(processes_tab, "⚙️ Processus") # Onglet Système system_tab = self.create_system_tab() tabs.addTab(system_tab, "💻 Système") # Onglet Réseau network_tab = self.create_network_tab() tabs.addTab(network_tab, "🌐 Réseau") layout.addWidget(tabs) # Boutons de contrôle control_layout = QHBoxLayout() export_button = QPushButton("📊 Exporter données") export_button.clicked.connect(self.export_monitoring_data) refresh_button = QPushButton("🔄 Actualiser") refresh_button.clicked.connect(self.update_data) close_button = QPushButton("❌ Fermer") close_button.clicked.connect(self.close) control_layout.addWidget(export_button) control_layout.addWidget(refresh_button) control_layout.addStretch() control_layout.addWidget(close_button) layout.addLayout(control_layout) def create_graphs_tab(self): """Créer l'onglet avec les graphiques""" widget = QWidget() layout = QVBoxLayout(widget) # Créer la figure matplotlib self.figure = Figure(figsize=(12, 8), facecolor='#1a1a1a') self.canvas = FigureCanvas(self.figure) layout.addWidget(self.canvas) return widget def create_processes_tab(self): """Créer l'onglet des processus - VERSION AMÉLIORÉE""" widget = QWidget() layout = QVBoxLayout(widget) # Contrôles controls_layout = QHBoxLayout() sort_combo = QComboBox() sort_combo.addItems(["CPU %", "Mémoire %", "Nom", "PID"]) sort_combo.currentTextChanged.connect(self.sort_processes) filter_edit = QLineEdit() filter_edit.setPlaceholderText("Filtrer les processus...") filter_edit.textChanged.connect(self.on_filter_text_changed) self.filter_edit = filter_edit # Garder la référence # Boutons d'action kill_button = QPushButton("🛑 Terminer") kill_button.clicked.connect(self.kill_selected_process) kill_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #c0392b; } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } """) kill_button.setEnabled(False) # Désactivé par défaut self.kill_button = kill_button # Référence pour l'activer/désactiver priority_button = QPushButton("⚖️ Priorité") priority_button.clicked.connect(self.change_process_priority) priority_button.setStyleSheet(""" QPushButton { background-color: #f39c12; color: white; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #e67e22; } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } """) priority_button.setEnabled(False) # Désactivé par défaut self.priority_button = priority_button # Référence pour l'activer/désactiver refresh_button = QPushButton("🔄 Actualiser") refresh_button.clicked.connect(self.refresh_processes) refresh_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #2980b9; } """) resume_button = QPushButton("▶️ Reprendre") resume_button.clicked.connect(self.resume_auto_updates) resume_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #2ecc71; } """) controls_layout.addWidget(resume_button) controls_layout.addWidget(QLabel("Trier par:")) controls_layout.addWidget(sort_combo) controls_layout.addWidget(QLabel("Filtrer:")) controls_layout.addWidget(filter_edit) controls_layout.addStretch() controls_layout.addWidget(refresh_button) controls_layout.addWidget(priority_button) controls_layout.addWidget(kill_button) layout.addLayout(controls_layout) # Table des processus AMÉLIORÉE self.processes_table = QTableWidget() self.processes_table.setColumnCount(6) self.processes_table.setHorizontalHeaderLabels(["PID", "Nom", "CPU %", "Mémoire", "Priorité", "État"]) # Configuration avancée de la table self.processes_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.processes_table.setSelectionMode(QAbstractItemView.SingleSelection) # Désactiver le défilement automatique vers la sélection self.processes_table.setAutoScroll(False) # Garder la sélection même lors des mises à jour self.processes_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.processes_table.setAlternatingRowColors(True) self.processes_table.setSortingEnabled(True) self.processes_table.verticalHeader().setVisible(False) # Ajuster les largeurs des colonnes header = self.processes_table.horizontalHeader() header.setStretchLastSection(True) header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # PID header.setSectionResizeMode(1, QHeaderView.Stretch) # Nom header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # CPU header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Mémoire header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Priorité # Style amélioré de la table self.processes_table.setStyleSheet(""" QTableWidget { background-color: #2a2a2a; alternate-background-color: #333333; selection-background-color: #0078d4; selection-color: white; gridline-color: #404040; color: white; border: 1px solid #404040; border-radius: 4px; } QTableWidget::item { padding: 8px; border: none; } QTableWidget::item:selected { background-color: #0078d4; color: white; } QHeaderView::section { background-color: #404040; color: white; border: 1px solid #606060; padding: 8px; font-weight: bold; } QScrollBar:vertical { background-color: #2a2a2a; width: 12px; border-radius: 6px; } QScrollBar::handle:vertical { background-color: #0078d4; border-radius: 6px; min-height: 20px; } QScrollBar::handle:vertical:hover { background-color: #106ebe; } """) # Connecter la sélection pour activer/désactiver les boutons self.processes_table.itemSelectionChanged.connect(self.on_process_selection_changed) # Menu contextuel self.processes_table.setContextMenuPolicy(Qt.CustomContextMenu) self.processes_table.customContextMenuRequested.connect(self.show_process_context_menu) layout.addWidget(self.processes_table) return widget def create_system_tab(self): """Créer l'onglet système""" widget = QWidget() layout = QVBoxLayout(widget) # Informations système system_group = QGroupBox("Informations Système") system_layout = QVBoxLayout() self.system_info_label = QLabel() self.system_info_label.setWordWrap(True) system_layout.addWidget(self.system_info_label) system_group.setLayout(system_layout) layout.addWidget(system_group) # Disques disks_group = QGroupBox("Utilisation des Disques") disks_layout = QVBoxLayout() self.disks_table = QTableWidget() self.disks_table.setColumnCount(5) self.disks_table.setHorizontalHeaderLabels(["Disque", "Total", "Utilisé", "Libre", "% Utilisé"]) self.disks_table.horizontalHeader().setStretchLastSection(True) disks_layout.addWidget(self.disks_table) disks_group.setLayout(disks_layout) layout.addWidget(disks_group) return widget def create_network_tab(self): """Créer l'onglet réseau""" widget = QWidget() layout = QVBoxLayout(widget) # Interfaces réseau interfaces_group = QGroupBox("Interfaces Réseau") interfaces_layout = QVBoxLayout() self.network_table = QTableWidget() self.network_table.setColumnCount(6) self.network_table.setHorizontalHeaderLabels([ "Interface", "Adresse IP", "État", "Bytes Envoyés", "Bytes Reçus", "Erreurs" ]) self.network_table.horizontalHeader().setStretchLastSection(True) interfaces_layout.addWidget(self.network_table) interfaces_group.setLayout(interfaces_layout) layout.addWidget(interfaces_group) return widget def update_data(self): """Met à jour toutes les données""" try: current_time = datetime.now() # Collecter les données cpu_percent = psutil.cpu_percent() memory = psutil.virtual_memory() disk = psutil.disk_usage('/') # Ajouter aux historiques self.cpu_history.append(cpu_percent) self.ram_history.append(memory.percent) self.disk_history.append(disk.percent) self.timestamps.append(current_time) # Garder seulement les 60 dernières valeurs (1 minute) if len(self.cpu_history) > 60: self.cpu_history.pop(0) self.ram_history.pop(0) self.disk_history.pop(0) self.timestamps.pop(0) # Mettre à jour les graphiques self.update_graphs() # Mettre à jour les processus self.update_processes() # Mettre à jour les infos système self.update_system_info() # Mettre à jour le réseau self.update_network_info() except Exception as e: print(f"Erreur mise à jour monitoring: {e}") def update_graphs(self): """Met à jour les graphiques""" try: self.figure.clear() # Créer 3 sous-graphiques ax1 = self.figure.add_subplot(3, 1, 1) ax2 = self.figure.add_subplot(3, 1, 2) ax3 = self.figure.add_subplot(3, 1, 3) if len(self.timestamps) > 1: # Graphique CPU ax1.plot(self.timestamps, self.cpu_history, 'r-', linewidth=2, label='CPU %') ax1.set_ylabel('CPU %', color='white') ax1.set_ylim(0, 100) ax1.grid(True, alpha=0.3) ax1.set_facecolor('#2a2a2a') # Graphique RAM ax2.plot(self.timestamps, self.ram_history, 'g-', linewidth=2, label='RAM %') ax2.set_ylabel('RAM %', color='white') ax2.set_ylim(0, 100) ax2.grid(True, alpha=0.3) ax2.set_facecolor('#2a2a2a') # Graphique Disque ax3.plot(self.timestamps, self.disk_history, 'b-', linewidth=2, label='Disque %') ax3.set_ylabel('Disque %', color='white') ax3.set_ylim(0, 100) ax3.grid(True, alpha=0.3) ax3.set_facecolor('#2a2a2a') # Style sombre for ax in [ax1, ax2, ax3]: ax.tick_params(colors='white') ax.spines['bottom'].set_color('white') ax.spines['top'].set_color('white') ax.spines['right'].set_color('white') ax.spines['left'].set_color('white') self.figure.tight_layout() self.canvas.draw() except Exception as e: print(f"Erreur mise à jour graphiques: {e}") def update_system_info(self): """Met à jour les informations système""" try: info = f""" 🖥️ Système: {platform.system()} {platform.release()} 🔧 Processeur: {platform.processor()} 💾 RAM Totale: {psutil.virtual_memory().total // (1024 ** 3)} GB 📊 CPU Cœurs: {psutil.cpu_count()} physiques, {psutil.cpu_count(logical=True)} logiques ⏱️ Temps de fonctionnement: {datetime.now() - datetime.fromtimestamp(psutil.boot_time())} 🌡️ Température CPU: {"Non disponible" if not hasattr(psutil, "sensors_temperatures") else "À implémenter"} """ self.system_info_label.setText(info.strip()) # Mettre à jour les disques partitions = psutil.disk_partitions() self.disks_table.setRowCount(len(partitions)) for i, partition in enumerate(partitions): try: usage = psutil.disk_usage(partition.mountpoint) total_gb = usage.total // (1024 ** 3) used_gb = usage.used // (1024 ** 3) free_gb = usage.free // (1024 ** 3) percent = (usage.used / usage.total) * 100 self.disks_table.setItem(i, 0, QTableWidgetItem(partition.device)) self.disks_table.setItem(i, 1, QTableWidgetItem(f"{total_gb} GB")) self.disks_table.setItem(i, 2, QTableWidgetItem(f"{used_gb} GB")) self.disks_table.setItem(i, 3, QTableWidgetItem(f"{free_gb} GB")) self.disks_table.setItem(i, 4, QTableWidgetItem(f"{percent:.1f}%")) except PermissionError: continue except Exception as e: print(f"Erreur mise à jour système: {e}") def update_network_info(self): """Met à jour les informations réseau""" try: interfaces = psutil.net_if_addrs() stats = psutil.net_if_stats() io_counters = psutil.net_io_counters(pernic=True) self.network_table.setRowCount(len(interfaces)) for i, (interface, addresses) in enumerate(interfaces.items()): ip_address = "N/A" for addr in addresses: if addr.family == 2: # IPv4 ip_address = addr.address break status = "Actif" if stats.get(interface, {}).isup else "Inactif" io_counter = io_counters.get(interface, {}) bytes_sent = io_counter.bytes_sent if hasattr(io_counter, 'bytes_sent') else 0 bytes_recv = io_counter.bytes_recv if hasattr(io_counter, 'bytes_recv') else 0 errors = io_counter.errin + io_counter.errout if hasattr(io_counter, 'errin') else 0 self.network_table.setItem(i, 0, QTableWidgetItem(interface)) self.network_table.setItem(i, 1, QTableWidgetItem(ip_address)) self.network_table.setItem(i, 2, QTableWidgetItem(status)) self.network_table.setItem(i, 3, QTableWidgetItem(f"{bytes_sent // 1024} KB")) self.network_table.setItem(i, 4, QTableWidgetItem(f"{bytes_recv // 1024} KB")) self.network_table.setItem(i, 5, QTableWidgetItem(str(errors))) except Exception as e: print(f"Erreur mise à jour réseau: {e}") def export_monitoring_data(self): """Exporte les données de monitoring""" try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename, _ = QFileDialog.getSaveFileName( self, "Exporter les données de monitoring", f"monitoring_data_{timestamp}.json", "Fichiers JSON (*.json)" ) if filename: data = { 'timestamp': timestamp, 'cpu_history': self.cpu_history, 'ram_history': self.ram_history, 'disk_history': self.disk_history, 'system_info': { 'platform': platform.system(), 'processor': platform.processor(), 'ram_total': psutil.virtual_memory().total, 'cpu_count': psutil.cpu_count() } } with open(filename, 'w') as f: json.dump(data, f, indent=2, default=str) QMessageBox.information(self, "Export réussi", f"Données exportées vers:\n{filename}") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export: {str(e)}") def sort_processes(self, sort_by): """Trie les processus selon le critère choisi""" try: processes = [] for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status']): try: processes.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): continue # Trier selon le critère if sort_by == "CPU %": processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True) elif sort_by == "Mémoire %": processes.sort(key=lambda x: x.get('memory_info', {}).get('rss', 0), reverse=True) elif sort_by == "Nom": processes.sort(key=lambda x: x.get('name', '').lower()) elif sort_by == "PID": processes.sort(key=lambda x: x.get('pid', 0)) # Mettre à jour la table self.update_processes_table(processes) except Exception as e: print(f"Erreur tri processus: {e}") def filter_processes(self, filter_text): """Filtre les processus selon le texte""" try: processes = [] for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status']): try: proc_info = proc.info # Filtrer par nom de processus if filter_text.lower() in proc_info.get('name', '').lower(): processes.append(proc_info) except (psutil.NoSuchProcess, psutil.AccessDenied): continue # Trier par CPU par défaut processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True) # Mettre à jour la table self.update_processes_table(processes) except Exception as e: print(f"Erreur filtre processus: {e}") def update_processes_table(self, processes): """Met à jour la table des processus avec une liste donnée - VERSION COMPLÈTE AVEC PRIORITÉ""" try: # Sauvegarder la sélection actuelle current_row = self.processes_table.currentRow() selected_pid = None if current_row >= 0: pid_item = self.processes_table.item(current_row, 0) if pid_item: selected_pid = pid_item.data(Qt.UserRole) # Limiter à 100 processus pour éviter les lags processes = processes[:100] self.processes_table.setRowCount(len(processes)) for i, proc in enumerate(processes): # PID pid_item = QTableWidgetItem(str(proc.get('pid', 'N/A'))) pid_item.setData(Qt.UserRole, proc.get('pid')) # Stocker le PID pid_item.setTextAlignment(Qt.AlignCenter) self.processes_table.setItem(i, 0, pid_item) # Nom name = proc.get('name', 'N/A') name_item = QTableWidgetItem(name) name_item.setToolTip(f"Processus: {name}\nPID: {proc.get('pid', 'N/A')}") self.processes_table.setItem(i, 1, name_item) # CPU % cpu_percent = proc.get('cpu_percent', 0) cpu_item = QTableWidgetItem(f"{cpu_percent:.1f}%") cpu_item.setTextAlignment(Qt.AlignCenter) # Colorier selon l'utilisation CPU if cpu_percent > 80: cpu_item.setForeground(QColor("#e74c3c")) # Rouge elif cpu_percent > 50: cpu_item.setForeground(QColor("#f39c12")) # Orange elif cpu_percent > 20: cpu_item.setForeground(QColor("#f1c40f")) # Jaune else: cpu_item.setForeground(QColor("#2ecc71")) # Vert self.processes_table.setItem(i, 2, cpu_item) # Mémoire memory_info = proc.get('memory_info', {}) if hasattr(memory_info, 'rss'): memory_mb = memory_info.rss / 1024 / 1024 else: memory_mb = 0 memory_item = QTableWidgetItem(f"{memory_mb:.1f} MB") memory_item.setTextAlignment(Qt.AlignCenter) # Colorier selon l'utilisation mémoire if memory_mb > 1000: # > 1GB memory_item.setForeground(QColor("#e74c3c")) # Rouge elif memory_mb > 500: # > 500MB memory_item.setForeground(QColor("#f39c12")) # Orange elif memory_mb > 100: # > 100MB memory_item.setForeground(QColor("#f1c40f")) # Jaune else: memory_item.setForeground(QColor("#2ecc71")) # Vert self.processes_table.setItem(i, 3, memory_item) # Priorité (NOUVELLE COLONNE) try: pid = proc.get('pid') if pid: priority_text = self.get_process_priority_text(pid) else: priority_text = "N/A" except: priority_text = "N/A" priority_item = QTableWidgetItem(priority_text) priority_item.setTextAlignment(Qt.AlignCenter) # Colorier selon la priorité if "Temps réel" in priority_text: priority_item.setForeground(QColor("#e74c3c")) # Rouge elif "Haute" in priority_text: priority_item.setForeground(QColor("#f39c12")) # Orange elif "> Normale" in priority_text: priority_item.setForeground(QColor("#f1c40f")) # Jaune elif "Normale" in priority_text: priority_item.setForeground(QColor("#2ecc71")) # Vert elif "< Normale" in priority_text: priority_item.setForeground(QColor("#3498db")) # Bleu elif "Basse" in priority_text: priority_item.setForeground(QColor("#9b59b6")) # Violet self.processes_table.setItem(i, 4, priority_item) # État status = proc.get('status', 'N/A') status_item = QTableWidgetItem(status) status_item.setTextAlignment(Qt.AlignCenter) # Colorier selon l'état status_colors = { 'running': QColor("#2ecc71"), # Vert 'sleeping': QColor("#3498db"), # Bleu 'disk-sleep': QColor("#9b59b6"), # Violet 'stopped': QColor("#e74c3c"), # Rouge 'zombie': QColor("#95a5a6"), # Gris } if status.lower() in status_colors: status_item.setForeground(status_colors[status.lower()]) self.processes_table.setItem(i, 5, status_item) # Restaurer la sélection si possible if selected_pid is not None: for row in range(self.processes_table.rowCount()): pid_item = self.processes_table.item(row, 0) if pid_item and pid_item.data(Qt.UserRole) == selected_pid: self.processes_table.selectRow(row) break except Exception as e: print(f"Erreur mise à jour table processus: {e}") def kill_selected_process(self): """Termine le processus sélectionné""" try: current_row = self.processes_table.currentRow() if current_row < 0: QMessageBox.warning(self, "Attention", "Veuillez sélectionner un processus à terminer.") return # Récupérer le PID pid_item = self.processes_table.item(current_row, 0) if not pid_item: return pid = pid_item.data(Qt.UserRole) process_name = self.processes_table.item(current_row, 1).text() if not pid: QMessageBox.warning(self, "Erreur", "Impossible de récupérer le PID du processus.") return # Confirmation reply = QMessageBox.question( self, "Confirmer la suppression", f"Voulez-vous vraiment terminer le processus ?\n\n" f"Nom: {process_name}\n" f"PID: {pid}\n\n" f"⚠️ Cette action peut causer une perte de données si le processus " f"n'a pas sauvegardé son travail.", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: try: # Tenter de terminer le processus proc = psutil.Process(pid) proc.terminate() # Attendre un peu pour voir si le processus se termine try: proc.wait(timeout=3) QMessageBox.information(self, "Succès", f"Processus {process_name} (PID: {pid}) terminé avec succès.") except psutil.TimeoutExpired: # Si le processus ne se termine pas, le forcer reply_force = QMessageBox.question( self, "Processus résistant", f"Le processus {process_name} ne répond pas.\n" f"Voulez-vous le forcer à se terminer ?", QMessageBox.Yes | QMessageBox.No ) if reply_force == QMessageBox.Yes: proc.kill() QMessageBox.information(self, "Succès", f"Processus {process_name} (PID: {pid}) forcé à se terminer.") # Actualiser la liste self.update_data() except psutil.NoSuchProcess: QMessageBox.information(self, "Information", "Le processus n'existe plus.") self.update_data() except psutil.AccessDenied: QMessageBox.critical(self, "Erreur", f"Accès refusé pour terminer le processus {process_name}.\n" f"Vous n'avez pas les privilèges nécessaires.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression du processus: {str(e)}") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression: {str(e)}") def closeEvent(self, event): """Gère la fermeture de la fenêtre de monitoring""" try: # Arrêter le timer de mise à jour if hasattr(self, 'update_timer'): self.update_timer.stop() # Nettoyer les ressources matplotlib if hasattr(self, 'figure'): self.figure.clear() event.accept() except Exception as e: print(f"Erreur fermeture monitoring: {e}") event.accept() def update_processes(self): """Met à jour la liste des processus - VERSION CORRIGÉE""" try: processes = [] for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status']): try: processes.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): continue # Trier par CPU par défaut processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True) # Utiliser la nouvelle méthode self.update_processes_table(processes) except Exception as e: print(f"Erreur mise à jour processus: {e}") def update_graphs_safe(self): """Version sécurisée de update_graphs qui évite les erreurs matplotlib""" try: if not hasattr(self, 'figure') or not hasattr(self, 'canvas'): return self.figure.clear() if len(self.timestamps) < 2: # Pas assez de données pour tracer self.canvas.draw() return # Créer 3 sous-graphiques ax1 = self.figure.add_subplot(3, 1, 1) ax2 = self.figure.add_subplot(3, 1, 2) ax3 = self.figure.add_subplot(3, 1, 3) # Convertir les timestamps en nombres pour matplotlib time_nums = [(t - self.timestamps[0]).total_seconds() for t in self.timestamps] # Graphique CPU ax1.plot(time_nums, self.cpu_history, 'r-', linewidth=2, label='CPU %') ax1.set_ylabel('CPU %', color='white') ax1.set_ylim(0, 100) ax1.grid(True, alpha=0.3) ax1.set_facecolor('#2a2a2a') ax1.set_title('Utilisation CPU', color='white') # Graphique RAM ax2.plot(time_nums, self.ram_history, 'g-', linewidth=2, label='RAM %') ax2.set_ylabel('RAM %', color='white') ax2.set_ylim(0, 100) ax2.grid(True, alpha=0.3) ax2.set_facecolor('#2a2a2a') ax2.set_title('Utilisation RAM', color='white') # Graphique Disque ax3.plot(time_nums, self.disk_history, 'b-', linewidth=2, label='Disque %') ax3.set_ylabel('Disque %', color='white') ax3.set_xlabel('Temps (secondes)', color='white') ax3.set_ylim(0, 100) ax3.grid(True, alpha=0.3) ax3.set_facecolor('#2a2a2a') ax3.set_title('Utilisation Disque', color='white') # Style sombre pour tous les axes for ax in [ax1, ax2, ax3]: ax.tick_params(colors='white') for spine in ax.spines.values(): spine.set_color('white') self.figure.tight_layout() self.canvas.draw() except Exception as e: print(f"Erreur mise à jour graphiques: {e}") # En cas d'erreur, au moins effacer et redessiner try: self.figure.clear() self.canvas.draw() except: pass def change_process_priority(self): """Ouvre un dialogue pour changer la priorité du processus sélectionné""" try: current_row = self.processes_table.currentRow() if current_row < 0: QMessageBox.warning(self, "Attention", "Veuillez sélectionner un processus.") return pid_item = self.processes_table.item(current_row, 0) name_item = self.processes_table.item(current_row, 1) if not pid_item or not name_item: return pid = pid_item.data(Qt.UserRole) name = name_item.text() # Dialogue de sélection de priorité dialog = ProcessPriorityDialog(self, pid, name) if dialog.exec_() == QDialog.Accepted: new_priority = dialog.get_selected_priority() self.set_process_priority(pid, name, new_priority) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors du changement de priorité: {str(e)}") def set_process_priority(self, pid, name, priority): """Définit la priorité d'un processus - Version optimisée avec vos imports""" try: import psutil # Mapping des priorités Windows (utilise vos imports existants) priority_map = { "realtime": win32process.REALTIME_PRIORITY_CLASS, "high": win32process.HIGH_PRIORITY_CLASS, "above_normal": win32process.ABOVE_NORMAL_PRIORITY_CLASS, "normal": win32process.NORMAL_PRIORITY_CLASS, "below_normal": win32process.BELOW_NORMAL_PRIORITY_CLASS, "idle": win32process.IDLE_PRIORITY_CLASS } try: # Vérifier que le processus existe encore proc = psutil.Process(pid) # Ouvrir le processus avec les droits nécessaires handle = win32api.OpenProcess( win32con.PROCESS_SET_INFORMATION | win32con.PROCESS_QUERY_INFORMATION, False, pid ) if handle: # Définir la nouvelle priorité win32process.SetPriorityClass(handle, priority_map[priority]) # Fermer le handle win32api.CloseHandle(handle) # Confirmer le succès priority_names = { "realtime": "Temps réel", "high": "Haute", "above_normal": "Au-dessus de la normale", "normal": "Normale", "below_normal": "En-dessous de la normale", "idle": "Basse" } QMessageBox.information(self, "Succès", f"Priorité du processus '{name}' (PID: {pid})\n" f"changée vers: {priority_names.get(priority, priority)}") # Actualiser la table self.refresh_processes() else: raise Exception("Impossible d'ouvrir le processus") except psutil.NoSuchProcess: QMessageBox.information(self, "Information", f"Le processus '{name}' (PID: {pid}) n'existe plus.") self.refresh_processes() except psutil.AccessDenied: QMessageBox.critical(self, "Accès refusé", f"Impossible de changer la priorité du processus '{name}'.\n\n" f"Raisons possibles:\n" f"• Processus système protégé\n" f"• Privilèges administrateur requis\n" f"• Processus appartenant à un autre utilisateur\n\n" f"💡 Essayez de lancer l'application en tant qu'administrateur.") except Exception as e: error_msg = str(e) if "Access is denied" in error_msg: QMessageBox.critical(self, "Accès refusé", f"Privilèges insuffisants pour modifier '{name}'.\n\n" f"💡 Lancez l'application en tant qu'administrateur\n" f"pour modifier les priorités des processus système.") else: QMessageBox.critical(self, "Erreur", f"Erreur lors du changement de priorité:\n{error_msg}") except Exception as e: QMessageBox.critical(self, "Erreur critique", f"Erreur inattendue lors du changement de priorité:\n{str(e)}") def get_process_priority_text(self, pid): """Récupère le texte de priorité d'un processus""" try: # Ouvrir le processus pour lire sa priorité handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, pid) if handle: priority_class = win32process.GetPriorityClass(handle) win32api.CloseHandle(handle) # Mapper les valeurs vers du texte lisible priority_names = { win32process.REALTIME_PRIORITY_CLASS: "Temps réel", win32process.HIGH_PRIORITY_CLASS: "Haute", win32process.ABOVE_NORMAL_PRIORITY_CLASS: "> Normale", win32process.NORMAL_PRIORITY_CLASS: "Normale", win32process.BELOW_NORMAL_PRIORITY_CLASS: "< Normale", win32process.IDLE_PRIORITY_CLASS: "Basse" } return priority_names.get(priority_class, f"Inconnue ({priority_class})") else: return "N/A" except Exception: return "N/A" def on_process_selection_changed(self): """Appelé quand la sélection change dans la table des processus""" try: current_row = self.processes_table.currentRow() has_selection = current_row >= 0 # Sauvegarder le processus sélectionné if has_selection: pid_item = self.processes_table.item(current_row, 0) if pid_item: self.user_selected_process = pid_item.data(Qt.UserRole) # Arrêter les mises à jour automatiques quand un processus est sélectionné self.update_timer.stop() else: self.user_selected_process = None # Reprendre les mises à jour si pas de sélection et pas de filtre if not self.user_is_filtering: self.update_timer.start(5000) # Activer/désactiver les boutons selon la sélection if hasattr(self, 'kill_button'): self.kill_button.setEnabled(has_selection) if hasattr(self, 'priority_button'): self.priority_button.setEnabled(has_selection) except Exception as e: print(f"Erreur sélection processus: {e}") def refresh_processes(self): """Actualise manuellement la liste des processus""" try: self.update_processes() except Exception as e: print(f"Erreur actualisation processus: {e}") def show_process_context_menu(self, position): """Affiche le menu contextuel pour un processus""" try: item = self.processes_table.itemAt(position) if item is None: return current_row = item.row() pid_item = self.processes_table.item(current_row, 0) name_item = self.processes_table.item(current_row, 1) if not pid_item or not name_item: return pid = pid_item.data(Qt.UserRole) name = name_item.text() # Créer le menu contextuel menu = QMenu(self) menu.setStyleSheet(""" QMenu { background-color: #2a2a2a; color: white; border: 1px solid #404040; border-radius: 4px; padding: 4px; } QMenu::item { padding: 8px 16px; border-radius: 4px; } QMenu::item:selected { background-color: #0078d4; } """) # Actions du menu terminate_action = menu.addAction("🛑 Terminer le processus") terminate_action.triggered.connect(self.kill_selected_process) priority_action = menu.addAction("⚖️ Changer la priorité") priority_action.triggered.connect(self.change_process_priority) # Afficher le menu menu.exec_(self.processes_table.mapToGlobal(position)) except Exception as e: print(f"Erreur menu contextuel: {e}") def is_admin(self): """Vérifie si l'application s'exécute avec des privilèges administrateur""" try: return win32security.GetTokenInformation( win32security.GetCurrentProcessToken(), win32security.TokenElevation )[0] != 0 except: return False def check_admin_privileges(self): """Affiche un avertissement si pas d'admin rights""" if not self.is_admin(): QMessageBox.warning(self, "Privilèges limités", "⚠️ L'application ne s'exécute pas avec des privilèges administrateur.\n\n" "Certaines fonctionnalités peuvent être limitées:\n" "• Modification de priorités de processus système\n" "• Terminer des processus protégés\n\n" "💡 Pour toutes les fonctionnalités, relancez en tant qu'administrateur.") def on_filter_text_changed(self, text): """Appelé quand le texte de filtre change""" self.last_filter_text = text self.user_is_filtering = len(text.strip()) > 0 # Arrêter les mises à jour automatiques pendant la recherche if self.user_is_filtering: self.update_timer.stop() else: # Reprendre les mises à jour si pas de filtre self.update_timer.start(5000) # Appliquer le filtre self.filter_processes(text) def resume_auto_updates(self): """Reprend les mises à jour automatiques""" self.user_is_filtering = False self.user_selected_process = None self.last_filter_text = "" if hasattr(self, 'filter_edit'): self.filter_edit.clear() self.update_timer.start(5000) self.update_data() class ProcessPriorityDialog(QDialog): def __init__(self, parent, pid, name): super().__init__(parent) self.pid = pid self.name = name self.setWindowTitle(f"Changer la priorité - {name}") self.setMinimumSize(350, 200) self.setStyleSheet(""" QDialog { background-color: #1a1a1a; color: white; } QLabel { color: white; font-size: 12px; } QGroupBox { color: white; border: 1px solid #404040; border-radius: 6px; margin-top: 12px; padding-top: 8px; font-weight: bold; background-color: rgba(64, 64, 64, 0.3); } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 8px 0 8px; background-color: #1a1a1a; border-radius: 3px; } QRadioButton { color: white; spacing: 8px; } QRadioButton::indicator { width: 15px; height: 15px; } QRadioButton::indicator:unchecked { border: 2px solid #606060; border-radius: 8px; background-color: #2a2a2a; } QRadioButton::indicator:checked { border: 2px solid #0078d4; border-radius: 8px; background-color: #0078d4; } """) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Information sur le processus info_label = QLabel(f"Processus: {self.name}\nPID: {self.pid}") info_label.setStyleSheet("font-weight: bold; padding: 10px; color: white;") layout.addWidget(info_label) # Sélection de priorité priority_group = QGroupBox("Sélectionnez la nouvelle priorité:") priority_layout = QVBoxLayout() self.priority_buttons = QButtonGroup() priorities = [ ("🔴 Temps réel (Critique)", "realtime"), ("🟠 Haute", "high"), ("🟡 Au-dessus de la normale", "above_normal"), ("🟢 Normale (Recommandée)", "normal"), ("🔵 En-dessous de la normale", "below_normal"), ("⚫ Basse", "idle") ] for i, (text, value) in enumerate(priorities): radio = QRadioButton(text) radio.setProperty("priority_value", value) if value == "normal": radio.setChecked(True) # Normale par défaut self.priority_buttons.addButton(radio, i) priority_layout.addWidget(radio) priority_group.setLayout(priority_layout) layout.addWidget(priority_group) # Avertissement warning = QLabel("⚠️ Attention: Changer la priorité peut affecter les performances du système.") warning.setStyleSheet("color: orange; font-style: italic; padding: 5px;") warning.setWordWrap(True) layout.addWidget(warning) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) def get_selected_priority(self): checked_button = self.priority_buttons.checkedButton() if checked_button: return checked_button.property("priority_value") return "normal" class BackupConfigDialog(QDialog): """Dialogue de configuration de sauvegarde""" def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Configuration de la Sauvegarde") self.setMinimumSize(400, 300) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Options de sauvegarde options_group = QGroupBox("Éléments à sauvegarder") options_layout = QVBoxLayout() self.profiles_cb = QCheckBox("Profils utilisateur") self.profiles_cb.setChecked(True) self.settings_cb = QCheckBox("Paramètres de configuration") self.settings_cb.setChecked(True) self.logs_cb = QCheckBox("Fichiers de logs") self.logs_cb.setChecked(False) self.keywords_cb = QCheckBox("Mots-clés personnalisés") self.keywords_cb.setChecked(True) self.schedules_cb = QCheckBox("Tâches planifiées") self.schedules_cb.setChecked(True) options_layout.addWidget(self.profiles_cb) options_layout.addWidget(self.settings_cb) options_layout.addWidget(self.logs_cb) options_layout.addWidget(self.keywords_cb) options_layout.addWidget(self.schedules_cb) options_group.setLayout(options_layout) layout.addWidget(options_group) # Destination dest_group = QGroupBox("Destination") dest_layout = QVBoxLayout() self.dest_edit = QLineEdit() browse_button = QPushButton("Parcourir...") browse_button.clicked.connect(self.browse_destination) dest_row = QHBoxLayout() dest_row.addWidget(self.dest_edit) dest_row.addWidget(browse_button) dest_layout.addLayout(dest_row) dest_group.setLayout(dest_layout) layout.addWidget(dest_group) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) def browse_destination(self): folder = QFileDialog.getExistingDirectory(self, "Choisir le dossier de destination") if folder: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_name = f"MA2C_Backup_{timestamp}.zip" self.dest_edit.setText(os.path.join(folder, backup_name)) def get_config(self): return { 'profiles': self.profiles_cb.isChecked(), 'settings': self.settings_cb.isChecked(), 'logs': self.logs_cb.isChecked(), 'keywords': self.keywords_cb.isChecked(), 'schedules': self.schedules_cb.isChecked(), 'destination': self.dest_edit.text() } class RestoreConfirmDialog(QDialog): """Dialogue de confirmation de restauration""" def __init__(self, parent, backup_info): super().__init__(parent) self.backup_info = backup_info self.setWindowTitle("Confirmer la Restauration") self.setMinimumSize(500, 400) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Informations sur la sauvegarde info_group = QGroupBox("Informations sur la sauvegarde") info_layout = QVBoxLayout() info_text = f""" Date de création: {self.backup_info.get('created', 'Inconnue')} Version de l'application: {self.backup_info.get('app_version', 'Inconnue')} Nombre de profils: {self.backup_info.get('profiles_count', 0)} Taille: {self.backup_info.get('size', 'Inconnue')} """ info_label = QLabel(info_text.strip()) info_layout.addWidget(info_label) info_group.setLayout(info_layout) layout.addWidget(info_group) # Options de restauration options_group = QGroupBox("Options de restauration") options_layout = QVBoxLayout() self.overwrite_cb = QCheckBox("Écraser les fichiers existants") self.overwrite_cb.setChecked(True) self.backup_current_cb = QCheckBox("Sauvegarder la configuration actuelle avant restauration") self.backup_current_cb.setChecked(True) options_layout.addWidget(self.overwrite_cb) options_layout.addWidget(self.backup_current_cb) options_group.setLayout(options_layout) layout.addWidget(options_group) # Avertissement warning = QLabel("⚠️ ATTENTION: Cette opération va modifier votre configuration actuelle.\n" "Assurez-vous d'avoir sauvegardé vos données importantes.") warning.setStyleSheet("color: orange; font-weight: bold;") warning.setWordWrap(True) layout.addWidget(warning) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) def get_restore_options(self): return { 'overwrite': self.overwrite_cb.isChecked(), 'backup_current': self.backup_current_cb.isChecked() } class AdminBackupManager: """Gestionnaire de sauvegarde et restauration""" def __init__(self): self.base_dir = r"C:\AUTOLOOK" def create_backup(self, config): """Crée une sauvegarde selon la configuration""" try: backup_path = config['destination'] if not backup_path: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_path = os.path.join(self.base_dir, f"backup_{timestamp}.zip") with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: # Métadonnées de sauvegarde backup_info = { 'created': datetime.now().isoformat(), 'app_version': '1.0.0', # À récupérer dynamiquement 'config': config } zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2)) # Sauvegarder selon la configuration if config['profiles']: self._backup_folder(zipf, 'profiles', 'profiles/') if config['settings']: self._backup_file(zipf, 'Fontsize.json') self._backup_file(zipf, 'settings.json') if config['logs']: self._backup_folder(zipf, 'logs', 'logs/') if config['keywords']: self._backup_folder(zipf, 'Keywords', 'Keywords/') if config['schedules']: self._backup_file(zipf, 'schedules.json') return True, backup_path except Exception as e: print(f"Erreur création sauvegarde: {e}") return False, None def _backup_folder(self, zipf, folder_name, archive_path): """Sauvegarde un dossier complet""" folder_path = os.path.join(self.base_dir, folder_name) if os.path.exists(folder_path): for root, dirs, files in os.walk(folder_path): for file in files: file_path = os.path.join(root, file) arcname = os.path.join(archive_path, os.path.relpath(file_path, folder_path)) zipf.write(file_path, arcname) def _backup_file(self, zipf, filename): """Sauvegarde un fichier spécifique""" file_path = os.path.join(self.base_dir, filename) if os.path.exists(file_path): zipf.write(file_path, filename) def validate_backup(self, backup_path): """Valide un fichier de sauvegarde""" try: with zipfile.ZipFile(backup_path, 'r') as zipf: # Vérifier la présence des métadonnées if 'backup_info.json' not in zipf.namelist(): return False, None # Lire les métadonnées backup_info_data = zipf.read('backup_info.json') backup_info = json.loads(backup_info_data.decode('utf-8')) # Ajouter des informations sur la taille backup_info['size'] = f"{os.path.getsize(backup_path) / 1024 / 1024:.1f} MB" return True, backup_info except Exception as e: print(f"Erreur validation sauvegarde: {e}") return False, None def restore_backup(self, backup_path, options): """Restaure une sauvegarde""" try: # Sauvegarde de sécurité si demandée if options['backup_current']: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safety_backup_path = os.path.join(self.base_dir, f"safety_backup_{timestamp}.zip") self.create_backup({ 'profiles': True, 'settings': True, 'logs': False, 'keywords': True, 'schedules': True, 'destination': safety_backup_path }) # Restaurer les fichiers with zipfile.ZipFile(backup_path, 'r') as zipf: for file_info in zipf.filelist: if file_info.filename == 'backup_info.json': continue extract_path = os.path.join(self.base_dir, file_info.filename) # Vérifier si le fichier existe et si on doit l'écraser if os.path.exists(extract_path) and not options['overwrite']: continue # Créer les dossiers parent si nécessaires os.makedirs(os.path.dirname(extract_path), exist_ok=True) # Extraire le fichier zipf.extract(file_info.filename, self.base_dir) return True except Exception as e: print(f"Erreur restauration: {e}") return False class UpdateDialog(QDialog): def __init__(self, parent, update_details): super().__init__(parent) self.setWindowTitle("Mise à jour disponible") self.setFixedSize(400, 300) self.setStyleSheet(""" QDialog { background-color: #2b3e50; color: white; border: 2px solid #3498db; } QLabel { color: white; font-size: 12px; } QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 12px; } QPushButton:hover { background-color: #2980b9; } """) layout = QVBoxLayout() # Titre title_label = QLabel("🎉 Mise à jour disponible !") title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px;") layout.addWidget(title_label) # Message message_label = QLabel("Fichiers à mettre à jour :") layout.addWidget(message_label) # Liste des fichiers files_label = QLabel("\n".join(update_details)) files_label.setStyleSheet("margin: 10px; padding: 10px; background-color: #34495e; border-radius: 4px;") layout.addWidget(files_label) # Instructions instruction_label = QLabel("Cliquez sur le bouton 'Update' pour installer les mises à jour.") instruction_label.setStyleSheet("margin-top: 10px;") layout.addWidget(instruction_label) # Bouton OK ok_button = QPushButton("OK") ok_button.clicked.connect(self.accept) layout.addWidget(ok_button) self.setLayout(layout) class UpdateManager: def __init__(self, main_window): self.main_window = main_window # CORRECTION : Fixer le fichier version au démarrage QTimer.singleShot(500, self.fix_version_file) def download_and_install_updates(self): """ Méthode de téléchargement et installation automatique avec fenêtre de progression et sauvegarde de sécurité """ import requests from bs4 import BeautifulSoup import tempfile import os import shutil from datetime import datetime from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton, QTextEdit, QMessageBox from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QFont # Configuration des fichiers attendus expected_files = { 'main.txt': { 'target_name': 'main.py', 'target_dir': r'C:\autolook' }, 'module_outlook_2021.txt': { 'target_name': 'module_outlook_2021.py', 'target_dir': r'C:\autolook\modules' }, 'ha2.txt': { 'target_name': 'ha2.py', 'target_dir': r'C:\autolook\modules' }, 'messaging_client.txt': { 'target_name': 'messaging_client.py', 'target_dir': r'C:\autolook\modules' } } # Créer la fenêtre de progression progress_dialog = QDialog(self.main_window) progress_dialog.setWindowTitle("🔄 Mise à jour en cours") progress_dialog.setFixedSize(600, 450) progress_dialog.setWindowFlags(progress_dialog.windowFlags() | Qt.WindowStaysOnTopHint) progress_dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; font-size: 12px; } QProgressBar { border: 2px solid #2c3e50; border-radius: 8px; text-align: center; background-color: #34495e; color: white; height: 25px; font-weight: bold; } QProgressBar::chunk { background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3498db, stop: 0.5 #2980b9, stop: 1 #1abc9c); border-radius: 6px; margin: 1px; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 8px; font-family: Consolas, monospace; font-size: 11px; } QPushButton { background-color: #e74c3c; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; font-size: 12px; } QPushButton:hover { background-color: #c0392b; } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } """) # Layout de la fenêtre de progression layout = QVBoxLayout(progress_dialog) layout.setSpacing(15) layout.setContentsMargins(20, 20, 20, 20) # Titre title_label = QLabel("🔄 Mise à jour des modules MA2C") title_font = QFont() title_font.setPointSize(14) title_font.setBold(True) title_label.setFont(title_font) title_label.setAlignment(Qt.AlignCenter) layout.addWidget(title_label) # Statut actuel status_label = QLabel("Initialisation...") status_label.setStyleSheet("color: #3498db; font-weight: bold;") layout.addWidget(status_label) # Barre de progression principale main_progress = QProgressBar() main_progress.setRange(0, 100) main_progress.setValue(0) main_progress.setFormat("Progression globale: %p%") layout.addWidget(main_progress) # Barre de progression de l'étape courante step_progress = QProgressBar() step_progress.setRange(0, 100) step_progress.setValue(0) step_progress.setFormat("Étape courante: %p%") layout.addWidget(step_progress) # Zone de log détaillé log_text = QTextEdit() log_text.setMaximumHeight(200) log_text.setReadOnly(True) layout.addWidget(log_text) # Bouton d'annulation/fermeture cancel_button = QPushButton("Annuler") cancel_button.setEnabled(True) layout.addWidget(cancel_button) # Variables de contrôle cancelled = False backup_dir = None def update_log(message): """Ajoute un message au log avec timestamp""" timestamp = datetime.now().strftime("%H:%M:%S") log_text.append(f"[{timestamp}] {message}") log_text.ensureCursorVisible() def cancel_update(): nonlocal cancelled cancelled = True cancel_button.setText("Annulation...") cancel_button.setEnabled(False) cancel_button.clicked.connect(cancel_update) # Afficher la fenêtre progress_dialog.show() QApplication.processEvents() try: # === ÉTAPE 1: SAUVEGARDE DE SÉCURITÉ === update_log("🛡️ Création de la sauvegarde de sécurité...") status_label.setText("🛡️ Sauvegarde de sécurité en cours...") main_progress.setValue(5) step_progress.setValue(0) QApplication.processEvents() if cancelled: return # Créer le dossier de sauvegarde avec timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_dir = os.path.join(r'C:\autolook', f'backup_{timestamp}') os.makedirs(backup_dir, exist_ok=True) update_log(f"📁 Dossier de sauvegarde créé: {backup_dir}") self.cleanup_old_backups(keep_count=2) # Sauvegarder les fichiers existants files_to_backup = [] for file_info in expected_files.values(): target_path = os.path.join(file_info['target_dir'], file_info['target_name']) if os.path.exists(target_path): files_to_backup.append(target_path) if files_to_backup: for i, file_path in enumerate(files_to_backup): if cancelled: return try: # Calculer le chemin de sauvegarde rel_path = os.path.relpath(file_path, r'C:\autolook') backup_path = os.path.join(backup_dir, rel_path) backup_subdir = os.path.dirname(backup_path) # Créer les sous-dossiers si nécessaire os.makedirs(backup_subdir, exist_ok=True) # Copier le fichier shutil.copy2(file_path, backup_path) update_log(f"💾 Sauvegardé: {os.path.basename(file_path)}") # Mettre à jour la progression step_progress.setValue(int((i + 1) / len(files_to_backup) * 100)) QApplication.processEvents() except Exception as e: update_log(f"⚠️ Erreur sauvegarde {os.path.basename(file_path)}: {str(e)}") else: update_log("ℹ️ Aucun fichier existant à sauvegarder") step_progress.setValue(100) main_progress.setValue(15) QApplication.processEvents() # === ÉTAPE 2: CONNEXION AU SERVEUR === update_log("🌐 Connexion au serveur de mise à jour...") status_label.setText("🌐 Connexion au serveur...") step_progress.setValue(0) QApplication.processEvents() if cancelled: return update_url = "https://snoown2klabs.com/update" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(update_url, headers=headers, timeout=10) response.raise_for_status() update_log("✅ Connexion établie avec succès") step_progress.setValue(100) main_progress.setValue(25) QApplication.processEvents() # === ÉTAPE 3: ANALYSE DE LA PAGE === update_log("🔍 Analyse de la page de mise à jour...") status_label.setText("🔍 Analyse des mises à jour disponibles...") step_progress.setValue(0) QApplication.processEvents() if cancelled: return soup = BeautifulSoup(response.content, 'html.parser') update_log("✅ Page analysée avec succès") step_progress.setValue(100) main_progress.setValue(35) QApplication.processEvents() # === ÉTAPE 4: CRÉATION DES DOSSIERS === update_log("📁 Vérification des dossiers de destination...") status_label.setText("📁 Préparation des dossiers...") step_progress.setValue(0) if cancelled: return for i, file_info in enumerate(expected_files.values()): target_dir = file_info['target_dir'] os.makedirs(target_dir, exist_ok=True) update_log(f"📂 Dossier vérifié: {target_dir}") step_progress.setValue(int((i + 1) / len(expected_files) * 100)) QApplication.processEvents() main_progress.setValue(45) QApplication.processEvents() # === ÉTAPE 5: TÉLÉCHARGEMENT DES FICHIERS === update_log("⬇️ Début du téléchargement des fichiers...") status_label.setText("⬇️ Téléchargement en cours...") step_progress.setValue(0) if cancelled: return downloaded_files = [] total_files = len(expected_files) for i, (source_filename, file_info) in enumerate(expected_files.items()): if cancelled: break update_log(f"🔍 Recherche de {source_filename}...") # Recherche du lien de téléchargement download_url = self._find_download_url(soup, source_filename, update_url) if not download_url: update_log(f"❌ Lien non trouvé pour {source_filename}") continue update_log(f"🔗 URL trouvée: {source_filename}") try: update_log(f"⬇️ Téléchargement de {source_filename}...") download_response = requests.get(download_url, headers=headers, timeout=30) download_response.raise_for_status() # Sauvegarde temporaire puis déplacement with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as temp_file: temp_file.write(download_response.content) temp_path = temp_file.name target_path = os.path.join(file_info['target_dir'], file_info['target_name']) shutil.move(temp_path, target_path) downloaded_files.append(target_path) update_log(f"✅ {source_filename} → {file_info['target_name']}") except Exception as e: update_log(f"❌ Erreur téléchargement {source_filename}: {str(e)}") # Mettre à jour la progression du téléchargement step_progress.setValue(int((i + 1) / total_files * 100)) main_progress.setValue(45 + int((i + 1) / total_files * 45)) # 45% à 90% QApplication.processEvents() # === ÉTAPE 6: FINALISATION === if not cancelled: update_log("🎉 Finalisation de la mise à jour...") status_label.setText("🎉 Finalisation...") step_progress.setValue(100) main_progress.setValue(95) QApplication.processEvents() # Finalisation sans compilation self.main_window.reset_after_update() self.main_window.save_current_journal() self.main_window.last_scan_type = "Installation modules" # Désactiver le bouton de mise à jour self.main_window.update_button.setEnabled(False) self.main_window.update_button.setText("Update") self.main_window.update_button.setStyleSheet(""" background-color: #28a745; color: white; font-size: 12px; padding: 5px; border-radius: 5px; QPushButton:disabled { background-color: #6c757d; color: #adb5bd; } """) # Bilan final if len(downloaded_files) == len(expected_files): update_log( f"🎉 Installation réussie ! {len(downloaded_files)}/{len(expected_files)} fichiers installés") status_label.setText("✅ Mise à jour terminée avec succès !") elif len(downloaded_files) > 0: update_log( f"⚠️ Installation partielle : {len(downloaded_files)}/{len(expected_files)} fichiers installés") status_label.setText("⚠️ Mise à jour partiellement réussie") else: update_log("❌ Aucun fichier n'a pu être téléchargé") status_label.setText("❌ Échec de la mise à jour") if len(downloaded_files) > 0: try: self.update_version_after_download(downloaded_files) # AJOUT : Forcer la mise à jour de l'affichage de version updated_version = self.update_application_version_display() if updated_version: logger.info(f"Version de l'application mise à jour vers {updated_version}") self.show_update_completion_dialog(downloaded_files) except Exception as e: update_log(f"⚠️ Erreur lors de la mise à jour du versioning: {str(e)}") main_progress.setValue(100) cancel_button.setText("Fermer") cancel_button.setEnabled(True) cancel_button.clicked.disconnect() cancel_button.clicked.connect(progress_dialog.accept) else: # Processus annulé update_log("🚫 Mise à jour annulée par l'utilisateur") status_label.setText("🚫 Mise à jour annulée") # Proposer la restauration de la sauvegarde if backup_dir and os.path.exists(backup_dir): reply = QMessageBox.question( progress_dialog, "Restauration", "Souhaitez-vous restaurer la sauvegarde de sécurité ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: self.restore_backup(backup_dir, update_log) cancel_button.setText("Fermer") cancel_button.clicked.disconnect() cancel_button.clicked.connect(progress_dialog.accept) except requests.exceptions.RequestException as e: update_log(f"🌐 Erreur de connexion: {str(e)}") status_label.setText("❌ Erreur de connexion") main_progress.setValue(0) # Proposer la restauration en cas d'erreur if backup_dir and os.path.exists(backup_dir): reply = QMessageBox.question( progress_dialog, "Erreur - Restauration", "Une erreur est survenue. Souhaitez-vous restaurer la sauvegarde de sécurité ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: self.restore_backup(backup_dir, update_log) cancel_button.setText("Fermer") cancel_button.clicked.disconnect() cancel_button.clicked.connect(progress_dialog.accept) except Exception as e: update_log(f"⚠️ Erreur générale: {str(e)}") status_label.setText("❌ Erreur durant l'installation") main_progress.setValue(0) # Proposer la restauration en cas d'erreur if backup_dir and os.path.exists(backup_dir): reply = QMessageBox.question( progress_dialog, "Erreur - Restauration", "Une erreur est survenue. Souhaitez-vous restaurer la sauvegarde de sécurité ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: self.restore_backup(backup_dir, update_log) cancel_button.setText("Fermer") cancel_button.clicked.disconnect() cancel_button.clicked.connect(progress_dialog.accept) def update_version_after_download(self, downloaded_files): """Met à jour la version après téléchargement automatique""" try: current_version = self.load_current_version() logger.info(f"Version actuelle détectée: {current_version}") # CORRECTION : Vérifier s'il existe déjà une entrée manuelle pour la version actuelle if self._has_manual_changelog_entry(current_version): logger.info(f"Entrée manuelle trouvée pour v{current_version}, pas de création automatique") # Juste mettre à jour le fichier version.json sans changer la version self._update_version_file_only(current_version) return # Si on arrive ici, c'est qu'il faut vraiment incrémenter major, minor, patch = map(int, current_version.split('.')) # CORRECTION : Toujours incrémenter, jamais régresser has_significant_changes = len(downloaded_files) > 2 if has_significant_changes: minor += 1 patch = 0 else: patch += 1 new_version = f"{major}.{minor}.{patch}" logger.info(f"Nouvelle version calculée: {new_version}") # Préparer les changements automatiques (SANS les noms de fichiers) auto_changes = { 'major': [], 'minor': [] if not has_significant_changes else [ f"Mise à jour de {len(downloaded_files)} composants système"], 'patch': [] if has_significant_changes else [ f"Corrections et optimisations ({len(downloaded_files)} fichiers)"] } # Sauvegarder la nouvelle version self.save_version_and_changelog(new_version, current_version, auto_changes, auto_updated=True) except Exception as e: logger.error(f"Erreur mise à jour version: {str(e)}") def _update_version_file_only(self, current_version): """Met à jour uniquement le fichier version.json sans changer la version""" try: version_data = { 'version': current_version, 'last_updated': datetime.now().isoformat(), 'previous_version': current_version, # Pas de changement 'auto_updated': False # Car on ne change pas la version } with open(r"C:\autolook\version.json", 'w', encoding='utf-8') as f: json.dump(version_data, f, indent=4, ensure_ascii=False) logger.info(f"Fichier version.json mis à jour pour v{current_version}") except Exception as e: logger.error(f"Erreur mise à jour fichier version: {str(e)}") def _has_manual_changelog_entry(self, version): """Vérifie s'il existe déjà une entrée manuelle pour cette version""" try: changelog_file = r"C:\autolook\changelog.json" if os.path.exists(changelog_file): with open(changelog_file, 'r', encoding='utf-8') as f: changelog_data = json.load(f) # Chercher une entrée non auto-updated pour cette version for entry in changelog_data: if (entry.get('version') == version and not entry.get('auto_updated', False)): return True return False except Exception as e: logger.error(f"Erreur vérification entrée manuelle: {str(e)}") return False def save_version_and_changelog(self, new_version, previous_version, changes, auto_updated=False): """Sauvegarde version et changelog dans les bons emplacements""" try: # CORRECTION 1 : Sauvegarder version.json dans la RACINE version_data = { 'version': new_version, 'last_updated': datetime.now().isoformat(), 'previous_version': previous_version, 'auto_updated': auto_updated } with open(r"C:\autolook\version.json", 'w', encoding='utf-8') as f: json.dump(version_data, f, indent=4, ensure_ascii=False) # CORRECTION 2 : Sauvegarder changelog.json dans la RACINE (pas dans le dossier) changelog_file = r"C:\autolook\changelog.json" # ← RACINE # Charger historique existant changelog_history = [] if os.path.exists(changelog_file): try: with open(changelog_file, 'r', encoding='utf-8') as f: changelog_history = json.load(f) except: changelog_history = [] # Créer nouvelle entrée new_entry = { 'version': new_version, 'date': datetime.now().strftime('%d/%m/%Y %H:%M:%S'), 'previous_version': previous_version, 'changes': changes, 'auto_updated': auto_updated } # Ajouter en premier (plus récent en premier) changelog_history.insert(0, new_entry) # Limiter à 50 entrées if len(changelog_history) > 50: changelog_history = changelog_history[:50] # Sauvegarder dans la RACINE with open(changelog_file, 'w', encoding='utf-8') as f: json.dump(changelog_history, f, indent=4, ensure_ascii=False) logger.info(f"Version {new_version} et changelog sauvegardés avec succès") except Exception as e: logger.error(f"Erreur sauvegarde version et changelog: {str(e)}") def cleanup_old_backups(self, keep_count=2): """Supprime les anciennes sauvegardes, ne garde que les plus récentes""" try: autolook_dir = r'C:\autolook' # Trouver tous les dossiers backup_* backup_folders = [] for item in os.listdir(autolook_dir): item_path = os.path.join(autolook_dir, item) if os.path.isdir(item_path) and item.startswith('backup_'): # Extraire le timestamp du nom du dossier try: timestamp_str = item.replace('backup_', '') # Vérifier que c'est bien un timestamp valide (format YYYYMMDD_HHMMSS) datetime.strptime(timestamp_str, '%Y%m%d_%H%M%S') backup_folders.append((item_path, timestamp_str)) except ValueError: # Ignorer les dossiers qui ne correspondent pas au format continue # Trier par timestamp (plus récent en premier) backup_folders.sort(key=lambda x: x[1], reverse=True) # Supprimer les anciens (garder seulement keep_count) folders_to_delete = backup_folders[keep_count:] for folder_path, timestamp in folders_to_delete: try: shutil.rmtree(folder_path) logger.info(f"Sauvegarde supprimée: {folder_path}") print(f"🗑️ Ancienne sauvegarde supprimée: backup_{timestamp}") except Exception as e: logger.error(f"Erreur suppression sauvegarde {folder_path}: {str(e)}") print(f"⚠️ Erreur suppression {timestamp}: {str(e)}") if folders_to_delete: logger.info(f"Nettoyage terminé. {len(folders_to_delete)} anciennes sauvegardes supprimées") print(f"✅ Nettoyage terminé. {len(folders_to_delete)} anciennes sauvegardes supprimées") except Exception as e: logger.error(f"Erreur lors du nettoyage des sauvegardes: {str(e)}") print(f"❌ Erreur lors du nettoyage des sauvegardes: {str(e)}") def update_application_version_display(self): """Met à jour l'affichage de la version de l'application""" try: # Lire la version depuis version.json version_file = r"C:\autolook\version.json" if os.path.exists(version_file): with open(version_file, 'r', encoding='utf-8') as f: version_data = json.load(f) new_version = version_data.get('version', '1.0.0') # Mettre à jour le titre de la fenêtre principale if hasattr(self, 'main_window'): self.main_window.setWindowTitle(f"M.A.2C v{new_version}") logger.info(f"Titre de l'application mis à jour vers v{new_version}") # Mettre à jour d'autres affichages de version si nécessaire return new_version except Exception as e: logger.error(f"Erreur mise à jour affichage version: {str(e)}") return None def show_update_completion_dialog(self, downloaded_files): """Affiche le popup de fin de mise à jour avec détails du changelog""" try: dialog = QDialog(self.main_window) dialog.setWindowTitle("✅ Mise à jour terminée") dialog.setMinimumSize(600, 500) # Augmenté pour afficher les changements dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; font-size: 12px; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 10px; font-family: Consolas, monospace; font-size: 11px; } QPushButton { background-color: #27ae60; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 12px; } QPushButton:hover { background-color: #229954; } """) layout = QVBoxLayout(dialog) # Titre title = QLabel("🎉 Mise à jour terminée avec succès !") title.setStyleSheet("font-size: 16px; font-weight: bold; color: #27ae60;") title.setAlignment(Qt.AlignCenter) layout.addWidget(title) # Détails avec changelog details_text = QTextEdit() details_text.setReadOnly(True) details_text.setMinimumHeight(300) # CORRECTION : Charger et afficher les détails du changelog content = self.generate_update_summary(downloaded_files) details_text.setPlainText(content) layout.addWidget(details_text) # Bouton OK ok_button = QPushButton("OK") ok_button.clicked.connect(dialog.accept) layout.addWidget(ok_button) dialog.exec_() except Exception as e: logger.error(f"Erreur affichage popup: {str(e)}") def generate_update_summary(self, downloaded_files): """Génère le résumé de mise à jour avec les VRAIS changements""" try: # Charger la version actuelle current_version = self.load_current_version() logger.info(f"Génération résumé pour version: {current_version}") # Lire le changelog de la RACINE changelog_file = r"C:\autolook\changelog.json" if os.path.exists(changelog_file): with open(changelog_file, 'r', encoding='utf-8') as f: changelog_data = json.load(f) # CORRECTION : Chercher l'entrée MANUELLE (non auto_updated) pour cette version manual_entry = None for entry in changelog_data: if (entry.get('version') == current_version and not entry.get('auto_updated', False)): manual_entry = entry logger.info(f"Entrée manuelle trouvée pour v{current_version}") break # Si pas d'entrée manuelle, prendre la plus récente if not manual_entry and changelog_data: manual_entry = changelog_data[0] logger.info("Utilisation de l'entrée la plus récente") if manual_entry: content = f"📊 VERSION {manual_entry.get('version')}\n" content += "=" * 30 + "\n\n" content += f"📅 Date : {manual_entry.get('date', 'N/A')}\n" content += f"⬅️ Version précédente : {manual_entry.get('previous_version', 'N/A')}\n\n" changes = manual_entry.get('changes', {}) if changes.get('major'): content += "🚀 CHANGEMENTS MAJEURS :\n" for i, change in enumerate(changes['major'], 1): content += f"{i}. {change}\n" content += "\n" if changes.get('minor'): content += "⚡ CHANGEMENTS MINEURS :\n" for i, change in enumerate(changes['minor'], 1): content += f"{i}. {change}\n" content += "\n" if changes.get('patch'): content += "🔧 CORRECTIONS :\n" for i, change in enumerate(changes['patch'], 1): content += f"{i}. {change}\n" content += "\n" logger.info("Résumé généré avec succès") return content # Fallback si pas de changelog logger.warning("Aucun changelog trouvé, utilisation du fallback") content = f"📊 VERSION {current_version}\n" content += "=" * 30 + "\n\n" content += f"📂 Mise à jour système réussie\n" content += f"🕒 Date : {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n" return content except Exception as e: logger.error(f"Erreur génération résumé: {str(e)}") return f"VERSION {current_version}\nMise à jour réussie" def fix_version_file(self): """Corrige le fichier version.json s'il est incorrect""" try: # Lire le changelog pour trouver la vraie version actuelle changelog_file = r"C:\autolook\changelog.json" if os.path.exists(changelog_file): with open(changelog_file, 'r', encoding='utf-8') as f: changelog_data = json.load(f) # Trouver la version la plus élevée dans le changelog if changelog_data: # Trier les versions pour trouver la plus élevée versions = [] for entry in changelog_data: try: version = entry.get('version', '0.0.0') major, minor, patch = map(int, version.split('.')) versions.append((major, minor, patch, entry)) except: continue if versions: # Prendre la version la plus élevée versions.sort(reverse=True) highest_version_tuple, highest_entry = versions[0][0:3], versions[0][3] highest_version = f"{highest_version_tuple[0]}.{highest_version_tuple[1]}.{highest_version_tuple[2]}" # Corriger version.json version_data = { 'version': highest_version, 'last_updated': datetime.now().isoformat(), 'previous_version': highest_entry.get('previous_version', '1.0.0'), 'auto_updated': highest_entry.get('auto_updated', False) } with open(r"C:\autolook\version.json", 'w', encoding='utf-8') as f: json.dump(version_data, f, indent=4, ensure_ascii=False) logger.info(f"Version.json corrigé vers v{highest_version}") return highest_version return None except Exception as e: logger.error(f"Erreur correction version.json: {str(e)}") return None def _find_best_changelog_entry(self, changelog_data, target_version): """Trouve la meilleure entrée de changelog pour une version donnée""" if not changelog_data: return None # Chercher toutes les entrées pour cette version version_entries = [entry for entry in changelog_data if entry.get('version') == target_version] if not version_entries: return None # Si une seule entrée, la retourner if len(version_entries) == 1: return version_entries[0] # S'il y a plusieurs entrées, prioriser celle avec les meilleurs changements best_entry = None best_score = -1 for entry in version_entries: score = self._score_changelog_entry(entry) if score > best_score: best_score = score best_entry = entry return best_entry def _score_changelog_entry(self, entry): """Calcule un score pour déterminer la qualité d'une entrée de changelog""" score = 0 changes = entry.get('changes', {}) is_auto_updated = entry.get('auto_updated', False) # Pénalité pour les mises à jour automatiques if is_auto_updated: score -= 10 # Points pour chaque type de changement for category in ['major', 'minor', 'patch']: category_changes = changes.get(category, []) for change in category_changes: # Points pour les changements détaillés (non génériques) if not self._is_generic_change(change): if category == 'major': score += 10 # Changements majeurs = plus de points elif category == 'minor': score += 5 # Changements mineurs else: score += 2 # Corrections # Bonus pour les changements longs et détaillés if len(change) > 30: score += 2 return score def _is_generic_change(self, change): """Vérifie si un changement est générique/automatique""" generic_patterns = [ "Mise à jour significative de", "Fichier mis à jour:", "Mise à jour automatique de", "Auto-update", "Automatic update", "Migration vers le nouveau système" ] return any(pattern in change for pattern in generic_patterns) def load_current_version(self): # ← Ajouter cette méthode qui manque """Charge la version actuelle de manière générique""" try: version_file = r"C:\autolook\version.json" if os.path.exists(version_file): with open(version_file, 'r', encoding='utf-8') as f: data = json.load(f) return data.get('version', '1.0.0') return '1.0.0' except: return '1.0.0' def restore_backup(self, backup_dir, update_log): """Restaure la sauvegarde de sécurité""" try: update_log("🔄 Restauration de la sauvegarde en cours...") # Parcourir tous les fichiers de sauvegarde for root, dirs, files in os.walk(backup_dir): for file in files: backup_file_path = os.path.join(root, file) # Calculer le chemin de destination rel_path = os.path.relpath(backup_file_path, backup_dir) target_path = os.path.join(r'C:\autolook', rel_path) # Créer les dossiers si nécessaire target_dir = os.path.dirname(target_path) os.makedirs(target_dir, exist_ok=True) # Restaurer le fichier shutil.copy2(backup_file_path, target_path) update_log(f"🔄 Restauré: {file}") update_log("✅ Sauvegarde restaurée avec succès") except Exception as e: update_log(f"❌ Erreur lors de la restauration: {str(e)}") def _find_download_url(self, soup, filename, base_url): """ Recherche l'URL de téléchargement pour un fichier spécifique dans le HTML """ from urllib.parse import urljoin # Dictionnaire de correspondance entre les fichiers et leurs IDs file_id_mapping = { 'main.txt': 'zK3XA5', 'module_outlook_2021.txt': 'zBVbbr', 'ha2.txt': 'zEyuqp', 'messaging_client.txt': 'zMO2sM' } # Méthode 1 : Recherche par ID spécifique if filename in file_id_mapping: target_id = file_id_mapping[filename] download_element = soup.find(id=target_id) if download_element: return self._extract_url_from_element(download_element, base_url) # Méthode 2 : Recherche par nom de fichier dans les liens all_links = soup.find_all(['a', 'button'], href=True) for link in all_links: href = link.get('href', '') if filename in href: return urljoin(base_url, href) # Méthode 3 : Recherche par texte du lien for link in soup.find_all('a'): if link.text and filename in link.text: href = link.get('href', '') if href: return urljoin(base_url, href) # Méthode 4 : Recherche dans les attributs onclick onclick_elements = soup.find_all(attrs={"onclick": True}) for element in onclick_elements: onclick = element.get('onclick', '') if filename in onclick: import re url_match = re.search(r'["\']([^"\']*' + re.escape(filename) + r')["\']', onclick) if url_match: return urljoin(base_url, url_match.group(1)) return None def _extract_url_from_element(self, element, base_url): """ Extrait l'URL de téléchargement d'un élément HTML """ from urllib.parse import urljoin # Si c'est un lien direct if element.name == 'a' and element.get('href'): return urljoin(base_url, element['href']) # Si c'est un bouton avec onclick elif element.get('onclick'): onclick_content = element['onclick'] import re url_match = re.search(r'["\']([^"\']*\.txt)["\']', onclick_content) if url_match: return urljoin(base_url, url_match.group(1)) # Si c'est dans un formulaire elif element.name == 'button': form = element.find_parent('form') if form and form.get('action'): return urljoin(base_url, form['action']) return None def check_for_updates(self): """ Vérifie si des mises à jour sont disponibles en comparant les dates des fichiers via les en-têtes HTTP Last-Modified """ import requests from bs4 import BeautifulSoup from datetime import datetime from email.utils import parsedate_to_datetime import os # Configuration des fichiers à vérifier files_to_check = { 'main.txt': { 'local_path': r'C:\autolook\main.py', 'id': 'zK3XA5' }, 'module_outlook_2021.txt': { 'local_path': r'C:\autolook\modules\module_outlook_2021.py', 'id': 'zBVbbr' }, 'ha2.txt': { 'local_path': r'C:\autolook\modules\ha2.py', 'id': 'zEyuqp' }, 'messaging_client.txt': { 'local_path': r'C:\autolook\modules\messaging_client.py', 'id': 'zMO2sM' } } try: self.main_window.add_log_message("[INFO] 🔍 Vérification des mises à jour...", "info") # Connexion au serveur update_url = "https://snoown2klabs.com/update" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(update_url, headers=headers, timeout=10) response.raise_for_status() soup = BeautifulSoup(response.content, 'html.parser') updates_available = False update_details = [] # Vérifier chaque fichier for filename, file_info in files_to_check.items(): local_path = file_info['local_path'] file_id = file_info['id'] # Chercher l'élément avec l'ID spécifique remote_element = soup.find(id=file_id) if remote_element: # Extraire l'URL de téléchargement download_url = self._find_download_url(soup, filename, update_url) if download_url: if os.path.exists(local_path): # NOUVELLE LOGIQUE: Comparer les dates local_mtime = os.path.getmtime(local_path) remote_mtime = self._get_remote_file_timestamp(download_url, headers) if remote_mtime and remote_mtime > local_mtime: # Fichier distant plus récent local_date = datetime.fromtimestamp(local_mtime).strftime('%d/%m/%Y %H:%M:%S') remote_date = datetime.fromtimestamp(remote_mtime).strftime('%d/%m/%Y %H:%M:%S') updates_available = True update_details.append(f"• {filename} (Local: {local_date} → Distant: {remote_date})") self.main_window.add_log_message(f"[UPDATE] 🔄 {filename} obsolète", "warning") else: # Fichier local à jour self.main_window.add_log_message(f"[INFO] ✅ {filename} à jour", "info") else: # Fichier local manquant updates_available = True update_details.append(f"• {filename} (nouveau fichier)") self.main_window.add_log_message(f"[INSTALL] 📁 Fichier local manquant: {filename}", "warning") else: self.main_window.add_log_message(f"[ERROR] ❌ URL non trouvée pour {filename}", "error") else: # Aucun élément trouvé sur le serveur pour ce fichier self.main_window.add_log_message(f"[INFO] ❌ Aucun fichier distant trouvé pour {filename}", "warning") # Résultat final if updates_available: self.main_window.add_log_message("[INFO] 🎉 Mises à jour disponibles !", "success") return True, update_details else: self.main_window.add_log_message("[INFO] ✅ Tous les fichiers sont à jour", "info") return False, [] except Exception as e: self.main_window.add_log_message(f"[ERREUR] ⚠️ Erreur lors de la vérification: {str(e)}", "error") return False, [] def _get_remote_file_timestamp(self, download_url, headers): """ Récupère le timestamp de modification d'un fichier distant via les en-têtes HTTP Args: download_url (str): URL du fichier distant headers (dict): En-têtes HTTP à utiliser Returns: float: Timestamp Unix du fichier distant, ou None si non disponible """ try: from email.utils import parsedate_to_datetime # Faire une requête HEAD pour récupérer les en-têtes sans télécharger le fichier head_response = requests.head(download_url, headers=headers, timeout=10) head_response.raise_for_status() # Récupérer l'en-tête Last-Modified last_modified_raw = head_response.headers.get('Last-Modified') if last_modified_raw: # Convertir la date RFC 2822 en timestamp Unix dt = parsedate_to_datetime(last_modified_raw) timestamp = dt.timestamp() # Log pour débogage formatted_date = dt.strftime('%d/%m/%Y %H:%M:%S') logger.info(f"Date distante récupérée: {formatted_date} (timestamp: {timestamp})") return timestamp else: logger.warning(f"Pas d'en-tête Last-Modified pour {download_url}") return None except Exception as e: logger.error(f"Erreur lors de la récupération de la date distante pour {download_url}: {str(e)}") return None def _extract_download_url_robust(self, element, base_url): """ Version renforcée d'extraction d'URL basée sur votre test réussi """ from urllib.parse import urljoin import re # Méthode 1: Lien direct avec href if element.name == 'a' and element.get('href'): href = element['href'] return urljoin(base_url, href) # Méthode 2: JavaScript onclick (le plus probable selon votre test) if element.get('onclick'): onclick_content = element['onclick'] # Patterns pour extraire l'URL du JavaScript patterns = [ r"window\.open\(['\"]([^'\"]+)['\"]", # window.open('url') r"location\.href\s*=\s*['\"]([^'\"]+)['\"]", # location.href = 'url' r"['\"]([^'\"]*\.txt)['\"]", # Général pour .txt r"download\(['\"]([^'\"]+)['\"]", # download('url') ] for pattern in patterns: match = re.search(pattern, onclick_content) if match: url = match.group(1) return urljoin(base_url, url) # Méthode 3: Utiliser la méthode existante en fallback return self._find_download_url(None, element.get('data-filename', ''), base_url) def show_enhanced_update_dialog(self, update_details): """ Dialogue de mise à jour amélioré avec distinction entre nouveaux fichiers et mises à jour """ dialog = QDialog(self.main_window) dialog.setWindowTitle("🔄 Mises à jour disponibles") dialog.setMinimumSize(600, 400) dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; font-size: 12px; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 10px; font-family: Consolas, monospace; font-size: 11px; } QPushButton { background-color: #3498db; color: white; border: none; padding: 12px 20px; border-radius: 5px; font-weight: bold; font-size: 12px; } QPushButton:hover { background-color: #2980b9; } QPushButton#cancel { background-color: #95a5a6; } QPushButton#cancel:hover { background-color: #7f8c8d; } """) layout = QVBoxLayout(dialog) # Titre title_label = QLabel("🔄 Des mises à jour sont disponibles pour votre application") title_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #3498db; margin-bottom: 10px;") layout.addWidget(title_label) # Liste des mises à jour info_label = QLabel("📋 Fichiers à mettre à jour :") layout.addWidget(info_label) details_text = QTextEdit() details_text.setReadOnly(True) details_text.setMaximumHeight(200) # Formatage du contenu content = "DÉTAILS DES MISES À JOUR DISPONIBLES\n" content += "=" * 50 + "\n\n" new_files = [] updated_files = [] for detail in update_details: if "(nouveau fichier)" in detail: new_files.append(detail) else: updated_files.append(detail) if updated_files: content += "🔄 FICHIERS À METTRE À JOUR :\n" content += "-" * 30 + "\n" for detail in updated_files: content += f"{detail}\n" content += "\n" if new_files: content += "📁 NOUVEAUX FICHIERS À INSTALLER :\n" content += "-" * 30 + "\n" for detail in new_files: content += f"{detail}\n" content += "\n" content += "⚠️ ATTENTION : Une sauvegarde sera créée automatiquement avant la mise à jour.\n" content += "✅ Vous pourrez restaurer en cas de problème." details_text.setPlainText(content) layout.addWidget(details_text) # Boutons button_layout = QHBoxLayout() update_button = QPushButton("🚀 Installer les mises à jour") cancel_button = QPushButton("❌ Annuler") cancel_button.setObjectName("cancel") button_layout.addWidget(update_button) button_layout.addWidget(cancel_button) layout.addLayout(button_layout) # Connexions update_button.clicked.connect(dialog.accept) cancel_button.clicked.connect(dialog.reject) return dialog.exec_() == QDialog.Accepted class MainWindow(QMainWindow): def __init__(self, selected_version="COM"): super().__init__() settings = QSettings("Snoown2k", "MyApp") settings.setValue("outlook/connection_method", selected_version) mode = selected_version.upper() # === MODE IMAP CORRIGÉ === if mode == "IMAP": self.mail_client_type = "imap" from modules.module_outlook_2021 import ImapHandler # CORRECTION: Gestion simplifiée de l'attribution des comptes IMAP email = "" password = "" provider = "" # Priorité à l'attribut injecté (par main()) if hasattr(self, "selected_imap_account"): email, password, provider = self.selected_imap_account print(f"🔧 Utilisation compte injecté: {email}") else: # Fallback sur les paramètres sauvegardés email = settings.value("imap/email", "") password = settings.value("imap/password", "") provider = settings.value("imap/provider", "") print(f"🔧 Utilisation paramètres sauvegardés: {email}") # Vérifier qu'on a au moins les informations de base if email and password and provider: try: # Créer l'instance ImapHandler self.mail_handler = ImapHandler( email=email, password=password, provider=provider, log_callback=self.log_action ) # Tentative de connexion IMAP print(f"🔌 Tentative de connexion IMAP...") if self.mail_handler.connect(): # Créer OutlookWidget avec handler IMAP self.outlook_widget = OutlookWidget( parent=self, outlook_handler=None, imap_handler=self.mail_handler ) add_debug_log("MainWindow", f"✅ Connexion IMAP réussie ({email})") print(f"✅ Connexion IMAP établie pour {email}") else: # Connexion échouée - créer widget sans handler self.outlook_widget = OutlookWidget( parent=self, outlook_handler=None, imap_handler=None ) add_debug_log("MainWindow", "❌ Échec de la connexion IMAP") print("❌ Échec de la connexion IMAP") except Exception as imap_error: print(f"❌ Erreur lors de l'initialisation IMAP: {imap_error}") # En cas d'erreur, créer widget sans handler self.outlook_widget = OutlookWidget( parent=self, outlook_handler=None, imap_handler=None ) add_debug_log("MainWindow", f"❌ Erreur IMAP: {str(imap_error)}") else: print("⚠️ Informations IMAP manquantes, création d'un widget vide") # Pas d'informations IMAP - créer widget vide self.outlook_widget = OutlookWidget( parent=self, outlook_handler=None, imap_handler=None ) add_debug_log("MainWindow", "⚠️ Informations IMAP manquantes") # Neutraliser OutlookHandler en mode IMAP self.outlook_handler = None # === MODE COM / MAPI (inchangé) === else: self.mail_client_type = "com" if mode == "COM" else "mapi" self.mail_handler = OutlookHandler(main_window=self) self.outlook_widget = OutlookWidget(parent=self, outlook_handler=self.mail_handler) add_debug_log("MainWindow", f"Initialisation en mode {self.mail_client_type.upper()}") # === BLOC GÉNÉRAL (communs à tous les modes) === self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) add_debug_log("MainWindow.__init__", "Initialisation de la fenêtre principale") self.audio_player = AudioPlayer() self.setWindowTitle(f"M.A.2C v{VERSION}") QTimer.singleShot(3000, self.check_expired_profiles_at_startup) self.center() self.font_size = self.load_settings() self.setup_ui() self.apply_font_size() self.modules = load_modules() self.initialize_modules() self.font_size_changed_signal = pyqtSignal(int) self.permanent_delete_enabled = False self.apply_to_scan = True self.apply_to_count = True QTimer.singleShot(100, self.initialize_profiles) if self.mail_client_type == 'office': self.mail_handler = OutlookHandler() logger.info("Handler Outlook initialisé") elif self.mail_client_type == 'thunderbird': self.mail_handler = ThunderbirdHandler() logger.info("Handler Thunderbird initialisé") self.task_manager = TaskManager(self) self.task_manager.task_started.connect(self.on_task_started) self.task_manager.task_completed.connect(self.on_task_completed) self.task_manager.task_error.connect(self.on_task_error) self.task_manager.load_profile_requested.connect(self.load_profile_from_scheduler) self.task_manager.start() self.task_scheduler_dialog = TaskSchedulerDialog(self) self.mail_count_spinbox = QSpinBox() self.mail_count_spinbox.setMinimum(1) self.mail_count_spinbox.setMaximum(9999) self.mail_count_spinbox.setValue(1) self.audio_player.play_startup_sound() audio_path = os.path.join(r"C:\autolook\voice", "EN_ElevenLabs_Text_to_Speech_audio.mp3") if os.path.exists(audio_path): logger.info(f"Fichier audio trouvé: {audio_path}") logger.info(f"Taille du fichier: {os.path.getsize(audio_path)} bytes") logger.info(f"Permissions: {oct(os.stat(audio_path).st_mode)[-3:]}") else: logger.error(f"Fichier audio non trouvé: {audio_path}") self.priority_update_timer = QTimer(self) self.priority_update_timer.timeout.connect(self.update_priority_display) self.priority_update_timer.start(2000) self.setMinimumSize(1250, 700) self.min_emails_per_cycle = 2 self.max_emails_per_cycle = 100 self.min_pause_between_cycles = 5 self.max_pause_between_cycles = 600 self.panel = FloatingPanel(self) self.panel.show() button_style_base = """ color: white; font-size: 12px; padding: 5px; border-radius: 5px; """ self.messaging_button = QPushButton("Messagerie", self) self.messaging_button.setStyleSheet(f""" background-color: #8c44ad; {button_style_base} """) self.messaging_button.setFixedSize(100, 25) self.messaging_button.clicked.connect(self.open_messaging_window) self.toggle_button = QPushButton("Toggle Volet", self) self.toggle_button.setStyleSheet(f""" background-color: #0078D7; {button_style_base} """) self.toggle_button.setFixedSize(100, 25) self.toggle_button.clicked.connect(self.panel.toggle_panel) self.update_manager = UpdateManager(self) QTimer.singleShot(2000, self.check_updates_at_startup) self.update_button = QPushButton("Update", self) self.update_button.setEnabled(False) self.update_button.setStyleSheet(f""" background-color: #28a745; {button_style_base} """ + """ QPushButton:disabled { background-color: #6c757d; color: #adb5bd; } """) self.update_button.setFixedSize(100, 25) self.update_button.clicked.connect(self.update_manager.download_and_install_updates) self.save_profile_button = QPushButton("Sauvegarde profil", self) self.save_profile_button.setStyleSheet(f""" background-color: #4b44ad; {button_style_base} """) self.save_profile_button.setFixedSize(110, 25) self.save_profile_button.clicked.connect(self.save_profile) self.param_button = QPushButton("Paramètres", self) self.param_button.setStyleSheet(f""" background-color: #5382a9; {button_style_base} """) self.param_button.setFixedSize(100, 20) self.param_button.clicked.connect(self.toggle_sidebar) button_spacing = 10 self.messaging_button.move(self.width() - 590, 10) self.toggle_button.move(self.width() - 480, 10) self.update_button.move(self.width() - 360, 10) self.save_profile_button.move(self.width() - 240, 10) self.param_button.move(self.width() - 110, 10) self.windowMovedTimer = QTimer(self) self.windowMovedTimer.timeout.connect(self.update_panel_position) self.windowMovedTimer.start(100) self.center_on_screen() QTimer.singleShot(1000, self.check_crash_reports_at_startup) def _format_update_interval(self, minutes): """Formate l'intervalle de mise à jour de manière lisible""" if minutes < 60: return f"{minutes}mn" else: hours = minutes // 60 remaining_minutes = minutes % 60 if remaining_minutes == 0: return f"{hours}h" else: return f"{hours}h{remaining_minutes:02d}mn" def on_update_notifications_toggled(self, state): """Gère l'activation/désactivation des notifications périodiques de mise à jour""" enabled = state == Qt.Checked settings = QSettings("Snoown2k", "MyApp") settings.setValue("updates/enable_periodic_check", enabled) # Afficher/masquer le widget du slider if hasattr(self, 'update_interval_widget'): self.update_interval_widget.setVisible(enabled) # Redémarrer ou arrêter le timer self.setup_update_check_timer() logger.info(f"Notifications périodiques de mise à jour {'activées' if enabled else 'désactivées'}") def on_update_interval_changed(self, value): """Gère le changement d'intervalle avec contrainte par tranche de 15 minutes""" # Forcer la valeur à être un multiple de 15 corrected_value = int(round(value / 15)) * 15 # Si la valeur a été corrigée, mettre à jour le slider sans déclencher le signal if corrected_value != value: self.update_interval_slider.blockSignals(True) self.update_interval_slider.setValue(corrected_value) self.update_interval_slider.blockSignals(False) # Sauvegarder la valeur corrigée settings = QSettings("Snoown2k", "MyApp") settings.setValue("updates/interval_minutes", corrected_value) # Mettre à jour le label if hasattr(self, 'update_interval_label'): self.update_interval_label.setText( f"Délai entre vérifications : {self._format_update_interval(corrected_value)}") # Redémarrer le timer avec le nouvel intervalle self.setup_update_check_timer() logger.info( f"Délai de vérification des mises à jour mis à jour : {self._format_update_interval(corrected_value)}") def setup_update_check_timer(self): """Configure le timer de vérification des mises à jour selon les paramètres""" try: # Arrêter le timer existant s'il existe if hasattr(self, 'update_check_timer'): self.update_check_timer.stop() settings = QSettings("Snoown2k", "MyApp") enabled = settings.value("updates/enable_periodic_check", True, type=bool) if enabled: interval_minutes = settings.value("updates/interval_minutes", 240, type=int) interval_ms = interval_minutes * 60 * 1000 # Convertir en millisecondes # Créer et démarrer le timer if not hasattr(self, 'update_check_timer'): self.update_check_timer = QTimer(self) self.update_check_timer.timeout.connect(self.check_updates_periodically) self.update_check_timer.start(interval_ms) logger.info( f"Timer de vérification des mises à jour configuré : {self._format_update_interval(interval_minutes)}") else: logger.info("Vérification périodique des mises à jour désactivée") except Exception as e: logger.error(f"Erreur lors de la configuration du timer de mise à jour: {str(e)}") def check_updates_periodically(self): """Vérifie les mises à jour de façon périodique pendant l'utilisation""" try: # Vérifier si les notifications périodiques sont activées settings = QSettings("Snoown2k", "MyApp") enabled = settings.value("updates/enable_periodic_check", True, type=bool) if not enabled: logger.info("Vérification périodique désactivée, arrêt du timer") if hasattr(self, 'update_check_timer'): self.update_check_timer.stop() return updates_available, update_details = self.update_manager.check_for_updates() if updates_available and not self.update_button.isEnabled(): # Nouvelle mise à jour détectée self.update_button.setEnabled(True) self.update_button.setText("Update !") self.update_button.setStyleSheet(""" background-color: #e74c3c; color: white; font-weight: bold; """) # Notification système avec le délai configuré interval_minutes = settings.value("updates/interval_minutes", 240, type=int) self.log_action( f"🔄 Nouvelle mise à jour disponible ! (Prochaine vérification dans {self._format_update_interval(interval_minutes)})") else: # Pas de nouvelles mises à jour interval_minutes = settings.value("updates/interval_minutes", 240, type=int) logger.info( f"Aucune nouvelle mise à jour. Prochaine vérification dans {self._format_update_interval(interval_minutes)}") except Exception as e: logger.error(f"Erreur vérification périodique des mises à jour: {str(e)}") def check_updates_at_startup(self): """ Vérifie les mises à jour au démarrage et active le bouton si nécessaire """ updates_available, update_details = self.update_manager.check_for_updates() if updates_available: # Activer et changer l'apparence du bouton de mise à jour self.update_button.setEnabled(True) self.update_button.setStyleSheet(""" background-color: #e74c3c; color: white; font-size: 12px; padding: 5px; border-radius: 5px; font-weight: bold; """) self.update_button.setText("Update !") # Afficher le dialogue amélioré if self.update_manager.show_enhanced_update_dialog(update_details): self.update_manager.download_and_install_updates() else: # Aucune mise à jour, garder le bouton désactivé self.update_button.setEnabled(False) # Log d'information sur la configuration settings = QSettings("Snoown2k", "MyApp") periodic_enabled = settings.value("updates/enable_periodic_check", True, type=bool) if periodic_enabled: interval_minutes = settings.value("updates/interval_minutes", 240, type=int) # Protection au cas où la méthode n'existe pas encore if hasattr(self, '_format_update_interval'): interval_text = self._format_update_interval(interval_minutes) else: interval_text = f"{interval_minutes} minutes" self.log_action(f"Aucune mise à jour disponible. Vérification périodique activée ({interval_text})") else: self.log_action("Aucune mise à jour disponible. Vérification uniquement au démarrage.") def force_sync_all_panels(self): """Force la synchronisation de tous les panneaux et widgets""" try: # Récupérer les valeurs actuelles des sliders if hasattr(self, 'pause_slider'): try: min_pause, max_pause = self.pause_slider.values() self.update_pause_values(min_pause, max_pause) except: pass if hasattr(self, 'between_pages_slider'): try: min_between, max_between = self.between_pages_slider.values() self.update_between_pages_values(min_between, max_between) except: pass if hasattr(self, 'links_slider'): try: links_count = self.links_slider.value() self.update_links_count(links_count) except: pass # Forcer la mise à jour du panneau flottant if hasattr(self, 'panel'): QTimer.singleShot(100, self.panel.update_info_display) print("✅ Synchronisation forcée de tous les panneaux effectuée") except Exception as e: print(f"❌ Erreur lors de la synchronisation forcée: {e}") def open_messaging_window(self): from modules.messaging_client import MessagingClientWidget self.messaging_window = MessagingClientWidget() self.messaging_window.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) self.messaging_window.setWindowTitle("💬 MA2C - Messagerie") self.messaging_window.resize(400, 300) self.messaging_window.show() def handle_multiple_imap_accounts(self, accounts_data): """ Gère les données de comptes IMAP multiples reçues depuis ImapAccountDialog Args: accounts_data: Liste des comptes avec leurs dossiers """ try: print(f"📨 Réception de {len(accounts_data)} comptes IMAP dans MainWindow") # Si on a déjà un handler IMAP, l'utiliser pour traiter les nouveaux comptes if hasattr(self, 'mail_handler') and isinstance(self.mail_handler, ImapHandler): results = self.mail_handler.process_multiple_accounts(accounts_data) # Mettre à jour l'interface avec les résultats if hasattr(self.outlook_widget, 'update_imap_accounts'): self.outlook_widget.update_imap_accounts(results) # Log des résultats self.log_action(f"📊 Comptes IMAP traités: {results['success']}/{results['total']}") return results['success'] > 0 else: print("⚠️ Aucun handler IMAP disponible") return False except Exception as e: print(f"❌ Erreur dans handle_multiple_imap_accounts: {e}") self.log_action(f"❌ Erreur traitement comptes IMAP: {str(e)}") return False # NOUVELLE MÉTHODE pour ouvrir le dialogue IMAP def open_imap_account_dialog(self): """ Ouvre le dialogue de sélection de comptes IMAP """ try: dialog = ImapAccountDialog(self) # Connecter le signal de données reçues if hasattr(dialog, 'accounts_data_ready'): dialog.accounts_data_ready.connect(self.handle_multiple_imap_accounts) # Afficher le dialogue result = dialog.exec_() if result == QDialog.Accepted: print("✅ Dialogue IMAP fermé avec succès") return True else: print("❌ Dialogue IMAP annulé") return False except Exception as e: print(f"❌ Erreur ouverture dialogue IMAP: {e}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture du dialogue IMAP:\n{str(e)}") return False def check_crash_reports_at_startup(self): """Vérifie s'il y a des crash reports au démarrage et les affiche""" try: crash_dir = r"C:\autolook\crash_report" # Vérifier si le dossier existe if not os.path.exists(crash_dir): logger.info("Aucun dossier de crash report trouvé") return # Chercher tous les fichiers de crash report crash_files = glob.glob(os.path.join(crash_dir, "*.txt")) crash_files.extend(glob.glob(os.path.join(crash_dir, "*.log"))) if not crash_files: logger.info("Aucun fichier de crash report trouvé") return # Trier par date de modification (le plus récent en premier) crash_files.sort(key=os.path.getmtime, reverse=True) latest_crash = crash_files[0] logger.warning(f"Crash report détecté: {latest_crash}") # Lire le contenu du fichier try: with open(latest_crash, 'r', encoding='utf-8') as f: crash_content = f.read() except UnicodeDecodeError: # Essayer avec un autre encodage si UTF-8 échoue with open(latest_crash, 'r', encoding='latin-1') as f: crash_content = f.read() # Afficher la fenêtre de crash report self.show_crash_report_dialog(latest_crash, crash_content, len(crash_files)) except Exception as e: logger.error(f"Erreur lors de la vérification des crash reports: {str(e)}") def show_crash_report_dialog(self, crash_file_path, crash_content, total_crashes): """Affiche le dialogue du crash report avec possibilité de voir tous les crashes""" try: crash_dir = r"C:\autolook\crash_report" # Récupérer tous les fichiers de crash all_crash_files = glob.glob(os.path.join(crash_dir, "*.txt")) all_crash_files.extend(glob.glob(os.path.join(crash_dir, "*.log"))) all_crash_files.sort(key=os.path.getmtime, reverse=True) # Plus récent en premier if total_crashes == 1: # S'il n'y a qu'un seul crash, utiliser l'interface simple self.show_single_crash_dialog(crash_file_path, crash_content) else: # S'il y a plusieurs crashes, utiliser l'interface multiple self.show_multiple_crash_dialog(all_crash_files) except Exception as e: logger.error(f"Erreur lors de l'affichage du crash report: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'affichage du crash report: {str(e)}") def show_single_crash_dialog(self, crash_file_path, crash_content): """Affiche le dialogue pour un seul crash report""" try: dialog = QDialog(self) dialog.setWindowTitle("⚠️ Crash Report Détecté") dialog.setMinimumSize(600, 500) dialog.setStyleSheet(self.get_crash_dialog_style()) # Layout principal layout = QVBoxLayout(dialog) layout.setSpacing(15) # En-tête title_label = QLabel("🚨 L'application a détecté un crash précédent") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #e74c3c; margin-bottom: 10px;") layout.addWidget(title_label) # Informations sur le fichier file_name = os.path.basename(crash_file_path) file_date = datetime.fromtimestamp(os.path.getmtime(crash_file_path)).strftime("%d/%m/%Y à %H:%M:%S") info_label = QLabel(f"📄 Fichier: {file_name}\n📅 Date: {file_date}") info_label.setStyleSheet("font-size: 12px; color: #bdc3c7; margin-bottom: 15px;") layout.addWidget(info_label) # Zone de texte content_label = QLabel("📋 Contenu du crash report:") content_label.setStyleSheet("font-weight: bold; margin-top: 10px;") layout.addWidget(content_label) crash_text = QTextEdit() crash_text.setPlainText(crash_content) crash_text.setReadOnly(True) layout.addWidget(crash_text) # Boutons button_layout = self.create_crash_buttons(crash_file_path, crash_content, dialog) layout.addLayout(button_layout) dialog.exec_() except Exception as e: logger.error(f"Erreur lors de l'affichage du crash unique: {str(e)}") def show_multiple_crash_dialog(self, crash_files): """Affiche le dialogue pour plusieurs crash reports""" try: dialog = QDialog(self) dialog.setWindowTitle(f"⚠️ {len(crash_files)} Crash Reports Détectés") dialog.setMinimumSize(900, 500) # Fenêtre plus grande dialog.setStyleSheet(self.get_crash_dialog_style()) # Layout principal vertical main_layout = QVBoxLayout(dialog) main_layout.setSpacing(15) # Layout horizontal pour les 2 panneaux panels_layout = QHBoxLayout() panels_layout.setSpacing(15) # Panel gauche - Liste des crashes (plus petit) left_panel = QWidget() left_panel.setMinimumWidth(250) # Réduit de 300 à 250 left_panel.setMaximumWidth(280) # Réduit de 350 à 280 left_layout = QVBoxLayout(left_panel) # Titre du panel gauche list_title = QLabel(f"📋 Liste des Crash Reports ({len(crash_files)})") list_title.setStyleSheet("font-size: 14px; font-weight: bold; color: #e74c3c; margin-bottom: 10px;") left_layout.addWidget(list_title) # Liste des fichiers self.crash_list = QListWidget() self.crash_list.setStyleSheet(""" QListWidget { background-color: #2c3e50; border: 1px solid #34495e; border-radius: 5px; color: white; font-size: 11px; selection-background-color: #3498db; } QListWidget::item { padding: 8px; border-bottom: 1px solid #34495e; } QListWidget::item:hover { background-color: #34495e; } """) # Remplir la liste avec un format plus compact for i, crash_file in enumerate(crash_files): file_name = os.path.basename(crash_file) file_date = datetime.fromtimestamp(os.path.getmtime(crash_file)).strftime("%d/%m %H:%M") file_size = os.path.getsize(crash_file) size_str = f"{file_size // 1024}KB" if file_size >= 1024 else f"{file_size}B" # Format plus compact pour la liste item_text = f"{'🔴' if i == 0 else '🟡'} {file_name}\n📅 {file_date} | 📦 {size_str}" if i == 0: item_text += "\n⭐ Plus récent" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, crash_file) # Stocker le chemin complet self.crash_list.addItem(item) # Sélectionner le premier élément self.crash_list.setCurrentRow(0) left_layout.addWidget(self.crash_list) # Boutons compacts pour la liste list_buttons_layout = QVBoxLayout() list_buttons_layout.setSpacing(5) select_all_button = QPushButton("☑️ Tout sélectionner") select_all_button.setMaximumHeight(30) select_all_button.clicked.connect(lambda: self.crash_list.selectAll()) delete_selected_button = QPushButton("🗑️ Suppr. sélectionnés") delete_selected_button.setMaximumHeight(30) delete_selected_button.clicked.connect(lambda: self.delete_selected_crashes(dialog)) list_buttons_layout.addWidget(select_all_button) list_buttons_layout.addWidget(delete_selected_button) list_buttons_layout.addStretch() left_layout.addLayout(list_buttons_layout) panels_layout.addWidget(left_panel) # Panel droit - Contenu du crash sélectionné (plus grand) right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(0, 0, 0, 0) # Titre du panel droit self.content_title = QLabel("📄 Contenu du crash report") self.content_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #3498db; margin-bottom: 5px;") right_layout.addWidget(self.content_title) # Informations détaillées du fichier sélectionné self.file_info_label = QLabel("") self.file_info_label.setStyleSheet( "font-size: 12px; color: #bdc3c7; margin-bottom: 10px; padding: 5px; background-color: rgba(52, 73, 94, 0.3); border-radius: 3px;") right_layout.addWidget(self.file_info_label) # Zone de texte pour le contenu (beaucoup plus grande) self.crash_content_text = QTextEdit() self.crash_content_text.setReadOnly(True) self.crash_content_text.setStyleSheet(""" QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 15px; font-family: Consolas, monospace; font-size: 11px; line-height: 1.4; } """) right_layout.addWidget(self.crash_content_text) panels_layout.addWidget(right_panel) main_layout.addLayout(panels_layout) # Connecter la sélection de la liste self.crash_list.currentItemChanged.connect(self.on_crash_selection_changed) # Charger le premier crash if crash_files: self.load_crash_content(crash_files[0]) # === BOUTONS EN BAS DE LA FENÊTRE === bottom_buttons_layout = QHBoxLayout() bottom_buttons_layout.setSpacing(10) # Boutons d'action pour le fichier actuel save_current_button = QPushButton("💾 Sauvegarder ce fichier") save_current_button.setObjectName("save_button") self.save_current_button = save_current_button # Garder référence pour mise à jour delete_current_button = QPushButton("🗑️ Supprimer ce fichier") self.delete_current_button = delete_current_button # Garder référence pour mise à jour # Séparateur separator = QFrame() separator.setFrameShape(QFrame.VLine) separator.setStyleSheet("color: #34495e;") # Boutons généraux export_all_button = QPushButton("📤 Exporter tous les crashes") export_all_button.setObjectName("save_button") export_all_button.clicked.connect(lambda: self.export_all_crashes(crash_files)) delete_all_button = QPushButton("🧹 Supprimer tous les crash reports") delete_all_button.clicked.connect(lambda: self.delete_all_crash_reports(dialog)) close_button = QPushButton("✅ Fermer") close_button.setObjectName("ignore_button") close_button.clicked.connect(dialog.accept) # Organiser les boutons bottom_buttons_layout.addWidget(save_current_button) bottom_buttons_layout.addWidget(delete_current_button) bottom_buttons_layout.addWidget(separator) bottom_buttons_layout.addWidget(export_all_button) bottom_buttons_layout.addWidget(delete_all_button) bottom_buttons_layout.addStretch() bottom_buttons_layout.addWidget(close_button) main_layout.addLayout(bottom_buttons_layout) # Stocker la référence du dialog pour les callbacks self.current_crash_dialog = dialog dialog.exec_() except Exception as e: logger.error(f"Erreur lors de l'affichage des crashes multiples: {str(e)}") def on_crash_selection_changed(self, current, previous): """Appelé quand la sélection change dans la liste des crashes""" if current: crash_file_path = current.data(Qt.UserRole) self.load_crash_content(crash_file_path) def load_crash_content(self, crash_file_path): """Charge le contenu d'un crash report dans le panel droit""" try: # Lire le contenu try: with open(crash_file_path, 'r', encoding='utf-8') as f: content = f.read() except UnicodeDecodeError: with open(crash_file_path, 'r', encoding='latin-1') as f: content = f.read() # Mettre à jour l'affichage self.crash_content_text.setPlainText(content) # Mettre à jour les informations du fichier file_name = os.path.basename(crash_file_path) file_date = datetime.fromtimestamp(os.path.getmtime(crash_file_path)).strftime("%d/%m/%Y à %H:%M:%S") file_size = os.path.getsize(crash_file_path) size_str = f"{file_size} bytes" if file_size < 1024 else f"{file_size // 1024} KB" self.file_info_label.setText(f"📄 {file_name} | 📅 {file_date} | 📦 {size_str}") self.content_title.setText(f"📄 Contenu: {file_name}") # Mettre à jour les boutons en bas pour ce fichier if hasattr(self, 'save_current_button') and hasattr(self, 'delete_current_button'): # Déconnecter les anciens signaux try: self.save_current_button.clicked.disconnect() self.delete_current_button.clicked.disconnect() except: pass # Connecter les nouveaux signaux self.save_current_button.clicked.connect(lambda: self.save_crash_report(crash_file_path, content)) self.delete_current_button.clicked.connect(lambda: self.delete_single_crash_from_list(crash_file_path)) # Stocker le fichier actuel self.current_crash_file = crash_file_path self.current_crash_content = content except Exception as e: logger.error(f"Erreur lors du chargement du crash: {str(e)}") self.crash_content_text.setPlainText(f"Erreur lors du chargement du fichier:\n{str(e)}") self.file_info_label.setText("❌ Erreur lors du chargement") self.content_title.setText("📄 Erreur") def clear_layout(self, layout): """Vide un layout de tous ses widgets""" while layout.count(): child = layout.takeAt(0) if child.widget(): child.widget().deleteLater() def delete_single_crash_from_list(self, crash_file_path): """Supprime un crash depuis la liste multiple""" try: reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer ce crash report ?\n\n{os.path.basename(crash_file_path)}", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: os.remove(crash_file_path) # Retirer de la liste for i in range(self.crash_list.count()): item = self.crash_list.item(i) if item.data(Qt.UserRole) == crash_file_path: self.crash_list.takeItem(i) break # Si la liste est vide, fermer la fenêtre if self.crash_list.count() == 0: QMessageBox.information(self, "Information", "Tous les crash reports ont été supprimés") if hasattr(self, 'current_crash_dialog'): self.current_crash_dialog.accept() # Fermer la fenêtre else: # Sélectionner le premier élément restant if self.crash_list.count() > 0: self.crash_list.setCurrentRow(0) logger.info(f"Crash report supprimé: {crash_file_path}") except Exception as e: logger.error(f"Erreur lors de la suppression: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression: {str(e)}") def delete_selected_crashes(self, dialog): """Supprime les crashes sélectionnés dans la liste""" try: selected_items = self.crash_list.selectedItems() if not selected_items: QMessageBox.information(self, "Information", "Aucun fichier sélectionné") return reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer {len(selected_items)} crash report(s) sélectionné(s) ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: deleted_count = 0 for item in selected_items: crash_file_path = item.data(Qt.UserRole) try: os.remove(crash_file_path) row = self.crash_list.row(item) self.crash_list.takeItem(row) deleted_count += 1 except Exception as e: logger.error(f"Erreur lors de la suppression de {crash_file_path}: {str(e)}") QMessageBox.information(self, "Suppression", f"{deleted_count} crash report(s) supprimé(s)") # Si la liste est vide, fermer la fenêtre if self.crash_list.count() == 0: QMessageBox.information(self, "Information", "Tous les crash reports ont été supprimés") dialog.accept() else: # Sélectionner le premier élément restant if self.crash_list.count() > 0: self.crash_list.setCurrentRow(0) except Exception as e: logger.error(f"Erreur lors de la suppression des sélectionnés: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression: {str(e)}") def export_all_crashes(self, crash_files): """Exporte tous les crashes dans un seul fichier""" try: default_name = f"all_crash_reports_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" file_path, _ = QFileDialog.getSaveFileName( self, "Exporter tous les crash reports", default_name, "Fichiers texte (*.txt);;Tous les fichiers (*)" ) if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(f"COMPILATION DE TOUS LES CRASH REPORTS\n") f.write(f"Date d'export: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n") f.write(f"Nombre total de crashes: {len(crash_files)}\n") f.write("=" * 80 + "\n\n") for i, crash_file in enumerate(crash_files, 1): f.write(f"\n{'=' * 80}\n") f.write(f"CRASH REPORT #{i}\n") f.write(f"Fichier: {os.path.basename(crash_file)}\n") f.write( f"Date: {datetime.fromtimestamp(os.path.getmtime(crash_file)).strftime('%d/%m/%Y %H:%M:%S')}\n") f.write(f"{'=' * 80}\n\n") try: with open(crash_file, 'r', encoding='utf-8') as crash_f: content = crash_f.read() except UnicodeDecodeError: with open(crash_file, 'r', encoding='latin-1') as crash_f: content = crash_f.read() f.write(content) f.write("\n\n") QMessageBox.information(self, "Succès", f"Tous les crash reports exportés vers:\n{file_path}") logger.info(f"Tous les crash reports exportés: {file_path}") except Exception as e: logger.error(f"Erreur lors de l'export de tous les crashes: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export: {str(e)}") def get_crash_dialog_style(self): """Retourne le style CSS pour les dialogues de crash""" return """ QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; font-size: 12px; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 10px; font-family: Consolas, monospace; font-size: 11px; } QPushButton { background-color: #e74c3c; color: white; border: none; padding: 12px 20px; border-radius: 5px; font-weight: bold; font-size: 12px; min-width: 140px; min-height: 35px; } QPushButton:hover { background-color: #c0392b; } QPushButton#save_button { background-color: #3498db; min-width: 160px; } QPushButton#save_button:hover { background-color: #2980b9; } QPushButton#ignore_button { background-color: #95a5a6; min-width: 100px; } QPushButton#ignore_button:hover { background-color: #7f8c8d; } QListWidget { background-color: #2c3e50; border: 1px solid #34495e; border-radius: 5px; color: white; font-size: 11px; } QFrame[frameShape="5"] { color: #34495e; max-width: 2px; margin: 5px; } """ def create_crash_buttons(self, crash_file_path, crash_content, dialog): """Crée les boutons d'action pour un crash report""" button_layout = QHBoxLayout() save_button = QPushButton("💾 Sauvegarder le rapport") save_button.setObjectName("save_button") save_button.clicked.connect(lambda: self.save_crash_report(crash_file_path, crash_content)) delete_button = QPushButton("🗑️ Supprimer le crash report") delete_button.clicked.connect(lambda: self.delete_crash_report(crash_file_path, dialog)) ignore_button = QPushButton("⏭️ Ignorer") ignore_button.setObjectName("ignore_button") ignore_button.clicked.connect(dialog.accept) delete_all_button = QPushButton("🧹 Supprimer tous les crash reports") delete_all_button.clicked.connect(lambda: self.delete_all_crash_reports(dialog)) button_layout.addWidget(save_button) button_layout.addWidget(delete_button) button_layout.addWidget(delete_all_button) button_layout.addStretch() button_layout.addWidget(ignore_button) return button_layout def save_crash_report(self, crash_file_path, crash_content): """Sauvegarde le crash report dans un fichier choisi par l'utilisateur""" try: # Proposer un nom de fichier par défaut default_name = f"crash_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" file_path, _ = QFileDialog.getSaveFileName( self, "Sauvegarder le crash report", default_name, "Fichiers texte (*.txt);;Tous les fichiers (*)" ) if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(f"CRASH REPORT SAUVEGARDÉ\n") f.write(f"Date de sauvegarde: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n") f.write(f"Fichier original: {crash_file_path}\n") f.write("=" * 80 + "\n\n") f.write(crash_content) QMessageBox.information(self, "Succès", f"Crash report sauvegardé dans:\n{file_path}") logger.info(f"Crash report sauvegardé: {file_path}") except Exception as e: logger.error(f"Erreur lors de la sauvegarde du crash report: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde: {str(e)}") def delete_crash_report(self, crash_file_path, dialog): """Supprime le crash report actuel""" try: reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer ce crash report ?\n\n{os.path.basename(crash_file_path)}", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: os.remove(crash_file_path) QMessageBox.information(self, "Suppression", "Crash report supprimé avec succès") logger.info(f"Crash report supprimé: {crash_file_path}") dialog.accept() # Fermer la fenêtre except Exception as e: logger.error(f"Erreur lors de la suppression du crash report: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression: {str(e)}") def delete_all_crash_reports(self, dialog): """Supprime tous les crash reports""" try: crash_dir = r"C:\autolook\crash_report" crash_files = glob.glob(os.path.join(crash_dir, "*.txt")) crash_files.extend(glob.glob(os.path.join(crash_dir, "*.log"))) if not crash_files: QMessageBox.information(self, "Information", "Aucun crash report à supprimer") return reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer TOUS les crash reports ?\n\n{len(crash_files)} fichier(s) seront supprimés", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: deleted_count = 0 for crash_file in crash_files: try: os.remove(crash_file) deleted_count += 1 except Exception as e: logger.error(f"Erreur lors de la suppression de {crash_file}: {str(e)}") QMessageBox.information(self, "Suppression", f"{deleted_count} crash report(s) supprimé(s) avec succès") logger.info(f"{deleted_count} crash reports supprimés") dialog.accept() # Fermer la fenêtre except Exception as e: logger.error(f"Erreur lors de la suppression des crash reports: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression: {str(e)}") def update_mail_handler(self): settings = QSettings("Snoown2k", "MyApp") method = settings.value("outlook/connection_method", "COM") if method == "COM": self.mail_handler = self.outlook_handler elif method == "IMAP": self.mail_handler = self.imap_handler else: self.mail_handler = None def setup_ui(self): self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.main_layout = QHBoxLayout(self.central_widget) self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.setSpacing(0) self.main_interface = QWidget() self.main_interface.setStyleSheet("background-color: #1a2a3a; color: white;") self.main_layout.addWidget(self.main_interface) self.main_interface_layout = QVBoxLayout(self.main_interface) # Les boutons ont été déplacés dans __init__ self.setup_sidebar() QTimer.singleShot(1000, self.initialize_default_banner) # AJOUT: Vérification périodique de la synchronisation self.sync_timer = QTimer(self) self.sync_timer.timeout.connect(self.verify_banner_sync) self.sync_timer.start(30000) def center_on_screen(self): screen = QDesktopWidget().screenGeometry() size = self.geometry() self.move( (screen.width() - size.width()) // 2, (screen.height() - size.height()) // 2 ) def setup_system_tools(self): """ Configure et initialise les outils système avancés: - Moniteur de tâches système - Système d'autodiagnostic et réparation """ try: # Intégrer le moniteur de tâches système self.system_task_monitor = integrate_system_monitor(self) # Intégrer le système d'autodiagnostic self.system_autotest = integrate_system_autotest(self) # Créer un groupe pour les boutons de système dans l'interface if hasattr(self, 'priority_group') and isinstance(self.priority_group, QGroupBox): # Les boutons sont déjà ajoutés dans les fonctions d'intégration pass else: # Créer un groupe si celui existant n'est pas accessible system_group = QGroupBox("Outils Système") system_layout = QVBoxLayout(system_group) if hasattr(self, 'system_task_monitor') and hasattr(self.system_task_monitor, 'dashboard_button'): system_layout.addWidget(self.system_task_monitor.dashboard_button) if hasattr(self, 'system_autotest') and hasattr(self.system_autotest, 'test_button'): system_layout.addWidget(self.system_autotest.test_button) # Ajouter le groupe au layout principal if hasattr(self, 'main_interface_layout'): self.main_interface_layout.addWidget(system_group) logger.info("Outils système configurés avec succès") return True except Exception as e: logger.error(f"Erreur lors de la configuration des outils système: {str(e)}") return False def toggle_cycle_processing(self, enabled): """Active ou désactive le traitement par cycle""" # Rendre visible ou masquer le groupe de paramètres de cycle self.cycle_group.setVisible(enabled) # Mettre à jour le paramètre dans le gestionnaire Outlook if hasattr(self, 'outlook_handler') and self.outlook_handler: self.outlook_handler.set_cycle_processing(enabled) # Log pour confirmer l'action self.log_action(f"Traitement par cycle {'activé' if enabled else 'désactivé'}") def update_emails_per_cycle_range(self, min_val, max_val): """Met à jour les champs d'entrée avec les valeurs du slider pour le nombre d'emails par cycle""" self.emails_per_cycle_min_input.setText(str(min_val)) self.emails_per_cycle_max_input.setText(str(max_val)) # Mettre à jour les valeurs utilisées pour le traitement self.min_emails_per_cycle = min_val self.max_emails_per_cycle = max_val # Mise à jour dans le handler if hasattr(self, 'outlook_handler') and self.outlook_handler: self.outlook_handler.set_emails_per_cycle((min_val, max_val)) logger.info(f"Plage d'emails par cycle mise à jour: {min_val}-{max_val}") def update_emails_slider_from_min_input(self): """Met à jour le slider minimum à partir du champ d'entrée pour le nombre d'emails""" try: value = int(self.emails_per_cycle_min_input.text()) # Vérifier que la valeur est dans les limites if value < 1: value = 1 if value > 100: value = 100 # S'assurer que min ne dépasse pas max max_val = self.emails_per_cycle_slider.max_slider.value() if value > max_val: value = max_val self.emails_per_cycle_min_input.setText(str(value)) self.emails_per_cycle_slider.min_slider.setValue(value) self.min_emails_per_cycle = value except ValueError: # Rétablir la valeur précédente en cas d'erreur self.emails_per_cycle_min_input.setText(str(self.emails_per_cycle_slider.min_slider.value())) def update_emails_slider_from_max_input(self): """Met à jour le slider maximum à partir du champ d'entrée pour le nombre d'emails""" try: value = int(self.emails_per_cycle_max_input.text()) # Vérifier que la valeur est dans les limites if value < 1: value = 1 if value > 100: value = 100 # S'assurer que max n'est pas inférieur à min min_val = self.emails_per_cycle_slider.min_slider.value() if value < min_val: value = min_val self.emails_per_cycle_max_input.setText(str(value)) self.emails_per_cycle_slider.max_slider.setValue(value) self.max_emails_per_cycle = value except ValueError: # Rétablir la valeur précédente en cas d'erreur self.emails_per_cycle_max_input.setText(str(self.emails_per_cycle_slider.max_slider.value())) def update_pause_between_cycles_range(self, min_val, max_val): """Met à jour les champs d'entrée avec les valeurs du slider pour le temps de pause entre cycles""" # Mettre à jour les champs texte self.pause_between_cycles_min_input.setText(str(min_val)) self.pause_between_cycles_max_input.setText(str(max_val)) # Mettre à jour les valeurs locales self.min_pause_between_cycles = min_val self.max_pause_between_cycles = max_val # Transmettre les valeurs à l'OutlookHandler if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'outlook_handler'): handler = self.outlook_widget.outlook_handler if hasattr(handler, 'set_pause_between_cycles'): handler.set_pause_between_cycles((min_val, max_val)) self.log_action(f"Plage de pause entre cycles mise à jour: {min_val}-{max_val} secondes") else: # Fallback si la méthode spécifique n'existe pas if hasattr(handler, 'min_pause_between_cycles') and hasattr(handler, 'max_pause_between_cycles'): handler.min_pause_between_cycles = min_val handler.max_pause_between_cycles = max_val self.log_action( f"Plage de pause entre cycles mise à jour via attributs: {min_val}-{max_val} secondes") # Mettre à jour l'affichage dans le panneau d'informations si disponible if hasattr(self, 'info_panel') and hasattr(self.info_panel, 'cycle_params_label'): self.info_panel.cycle_params_label.setText( f"{self.emails_per_cycle_min_input.text()}-{self.emails_per_cycle_max_input.text()} emails / {min_val}-{max_val}s pause") logger.info(f"Plage de pause entre cycles mise à jour: {min_val}-{max_val} secondes") def update_pause_slider_from_min_input(self): """Met à jour le slider minimum à partir du champ d'entrée pour le temps de pause""" try: value = int(self.pause_between_cycles_min_input.text()) # Vérifier que la valeur est dans les limites if value < 5: value = 5 if value > 600: value = 600 # S'assurer que min ne dépasse pas max max_val = self.pause_between_cycles_slider.max_slider.value() if value > max_val: value = max_val self.pause_between_cycles_min_input.setText(str(value)) self.pause_between_cycles_slider.min_slider.setValue(value) self.min_pause_between_cycles = value except ValueError: # Rétablir la valeur précédente en cas d'erreur self.pause_between_cycles_min_input.setText(str(self.pause_between_cycles_slider.min_slider.value())) def update_pause_slider_from_max_input(self): """Met à jour le slider maximum à partir du champ d'entrée pour le temps de pause""" try: value = int(self.pause_between_cycles_max_input.text()) # Vérifier que la valeur est dans les limites if value < 5: value = 5 if value > 2700: value = 2700 # S'assurer que max n'est pas inférieur à min min_val = self.pause_between_cycles_slider.min_slider.value() if value < min_val: value = min_val self.pause_between_cycles_max_input.setText(str(value)) self.pause_between_cycles_slider.max_slider.setValue(value) self.max_pause_between_cycles = value except ValueError: # Rétablir la valeur précédente en cas d'erreur self.pause_between_cycles_max_input.setText(str(self.pause_between_cycles_slider.max_slider.value())) def update_cycle_parameters(self): """Met à jour les paramètres de cycle dans le handler""" if hasattr(self, 'outlook_handler') and self.outlook_handler: # Récupérer les valeurs actuelles enabled = self.cycle_processing_checkbox.isChecked() emails_per_cycle = self.emails_per_cycle_slider.value() pause_between_cycles = self.pause_between_cycles_slider.value() # Mettre à jour OutlookHandler si les méthodes existent if hasattr(self.outlook_handler, 'set_cycle_processing'): self.outlook_handler.set_cycle_processing(enabled) if hasattr(self.outlook_handler, 'set_emails_per_cycle'): self.outlook_handler.set_emails_per_cycle(emails_per_cycle) if hasattr(self.outlook_handler, 'set_pause_between_cycles'): self.outlook_handler.set_pause_between_cycles(pause_between_cycles) logger.info(f"Paramètres de cycle mis à jour: activé={enabled}, " f"emails={emails_per_cycle}, pause={pause_between_cycles}s") def show_empty_trash_menu(self): """Affiche un menu avec les options pour vider la corbeille Outlook""" try: menu = QMenu(self) menu.setStyleSheet(""" QMenu { background-color: #1a2a3a; color: white; border: 1px solid #34495e; padding: 5px; } QMenu::item { padding: 5px 20px; border-radius: 3px; } QMenu::item:selected { background-color: #008CBA; } """) action1 = QAction("Vider la corbeille du compte courant", self) action1.triggered.connect(self.empty_current_account_trash) action2 = QAction("Vider la corbeille de tous les comptes", self) action2.triggered.connect(self.empty_all_accounts_trash) action3 = QAction("Configuration de la suppression des mails", self) action3.triggered.connect(self.empty_scan_trash) menu.addAction(action1) menu.addAction(action2) menu.addAction(action3) # Afficher le menu sous le bouton pos = self.empty_trash_button.mapToGlobal(self.empty_trash_button.rect().bottomLeft()) menu.exec_(pos) except Exception as e: logger.error(f"Erreur lors de l'affichage du menu de corbeille: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'affichage du menu: {str(e)}") def empty_current_account_trash(self): """Vide la corbeille du compte Outlook actuellement sélectionné avec exploration récursive et décompte en temps réel""" try: # Vérifier qu'Outlook est disponible if not hasattr(self, 'outlook_widget') or not self.outlook_widget: QMessageBox.warning(self, "Attention", "Widget Outlook non disponible") return # Récupérer le compte courant current_mailbox_item = self.outlook_widget.mailbox_listbox.currentItem() if not current_mailbox_item: QMessageBox.warning(self, "Attention", "Aucun compte mail sélectionné") return mailbox_name = current_mailbox_item.text() # Confirmation reply = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment vider la corbeille du compte '{mailbox_name}' ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # Initialiser COM si nécessaire pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") # Liste des noms possibles de dossiers de corbeille trash_folder_names = ["Éléments supprimés", "Deleted Items", "Corbeille", "Bin", "Trash", "Recycled Items"] # Fonction récursive pour trouver le dossier de corbeille def find_trash_folder(parent_folder): for i in range(1, parent_folder.Folders.Count + 1): folder = parent_folder.Folders.Item(i) if folder.Name in trash_folder_names: return folder, folder.Name # Recherche récursive dans les sous-dossiers result = find_trash_folder(folder) if result[0]: return result return None, None # Trouver le compte mailbox = None for i in range(1, outlook.Folders.Count + 1): current_mailbox = outlook.Folders.Item(i) if current_mailbox.Name == mailbox_name: mailbox = current_mailbox break if not mailbox: QMessageBox.warning(self, "Attention", f"Compte '{mailbox_name}' non trouvé") pythoncom.CoUninitialize() return # Rechercher le dossier de corbeille de manière récursive deleted_items_folder, found_trash_name = find_trash_folder(mailbox) if deleted_items_folder: # Récupérer le nombre d'éléments avant la suppression items_count = deleted_items_folder.Items.Count # Créer une boîte de dialogue de progression progress_dialog = QDialog(self) progress_dialog.setWindowTitle(f"Vidage de la corbeille - {mailbox_name}") progress_dialog.setFixedSize(400, 150) progress_dialog.setWindowFlags(progress_dialog.windowFlags() | Qt.WindowStaysOnTopHint) # Style de la boîte de dialogue - en appliquant directement le setStyleSheet progress_dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; font-size: 12px; } QProgressBar { border: 2px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #34495e; color: white; height: 25px; } QProgressBar::chunk { background-color: #3498db; width: 10px; margin: 0.5px; } """) # Layout layout = QVBoxLayout(progress_dialog) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(10) # Labels d'information - styles appliqués individuellement également info_label = QLabel(f"Vidage de la corbeille '{found_trash_name}' du compte '{mailbox_name}'") info_label.setStyleSheet("color: white; font-size: 12px;") info_label.setAlignment(Qt.AlignCenter) count_label = QLabel(f"0/{items_count} éléments supprimés") count_label.setStyleSheet("color: white; font-size: 12px;") count_label.setAlignment(Qt.AlignCenter) # Barre de progression progress_bar = QProgressBar() progress_bar.setRange(0, items_count) progress_bar.setValue(0) progress_bar.setStyleSheet(""" QProgressBar { border: 2px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #34495e; color: white; height: 25px; } QProgressBar::chunk { background-color: #3498db; width: 10px; margin: 0.5px; } """) # Ajouter les widgets au layout layout.addWidget(info_label) layout.addWidget(progress_bar) layout.addWidget(count_label) # Afficher la boîte de dialogue sans bloquer progress_dialog.show() QApplication.processEvents() # Supprimer tous les éléments if items_count > 0: items_deleted = 0 errors = 0 for item in reversed(list(range(1, items_count + 1))): try: deleted_items_folder.Items.Item(item).Delete() items_deleted += 1 # Mettre à jour la progression tous les 5 éléments ou lors du dernier élément if items_deleted % 5 == 0 or items_deleted == items_count: progress_bar.setValue(items_deleted) count_label.setText( f"{items_deleted}/{items_count} éléments supprimés ({errors} erreurs)") QApplication.processEvents() except Exception as e: errors += 1 logger.error(f"Erreur lors de la suppression d'un élément: {str(e)}") continue # Fermer la boîte de dialogue progress_dialog.accept() QMessageBox.information( self, "Succès", f"La corbeille ('{found_trash_name}') du compte '{mailbox_name}' a été vidée.\n" f"{items_deleted} élément(s) supprimé(s) sur {items_count} ({errors} erreurs)." ) logger.info( f"Corbeille '{found_trash_name}' vidée pour le compte {mailbox_name}: {items_deleted}/{items_count} éléments ({errors} erreurs)") else: # Fermer la boîte de dialogue progress_dialog.accept() QMessageBox.information( self, "Information", f"La corbeille ('{found_trash_name}') du compte '{mailbox_name}' est déjà vide." ) else: # Énumérer les dossiers principaux pour le diagnostic top_folders = [] for i in range(1, min(20, mailbox.Folders.Count + 1)): try: top_folders.append(mailbox.Folders.Item(i).Name) except: pass QMessageBox.warning( self, "Attention", f"Dossier de corbeille non trouvé pour le compte '{mailbox_name}'.\n" f"Noms recherchés: {', '.join(trash_folder_names)}\n\n" f"Dossiers principaux: {', '.join(top_folders)}" ) # Désinitialiser COM pythoncom.CoUninitialize() except Exception as e: logger.error(f"Erreur lors du vidage de la corbeille: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du vidage de la corbeille: {str(e)}") try: pythoncom.CoUninitialize() except: pass def empty_all_accounts_trash(self): """Vide la corbeille de tous les comptes Outlook avec exploration récursive et décompte en temps réel""" try: # Confirmation reply = QMessageBox.question( self, "Confirmation", "Voulez-vous vraiment vider la corbeille de TOUS les comptes Outlook ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # Initialiser COM si nécessaire pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") # Liste des noms possibles de dossiers de corbeille trash_folder_names = ["Éléments supprimés", "Deleted Items", "Corbeille", "Bin", "Trash", "Recycled Items"] # Fonction récursive pour trouver le dossier de corbeille def find_trash_folder(parent_folder): for i in range(1, parent_folder.Folders.Count + 1): try: folder = parent_folder.Folders.Item(i) if folder.Name in trash_folder_names: return folder, folder.Name # Recherche récursive dans les sous-dossiers result = find_trash_folder(folder) if result[0]: return result except Exception as e: logger.error(f"Erreur lors de l'accès à un sous-dossier: {str(e)}") continue return None, None # Variables pour les statistiques accounts_processed = 0 accounts_info = [] total_items_deleted = 0 total_items_count = 0 # Créer une boîte de dialogue de progression principale AGRANDIE main_progress_dialog = QDialog(self) main_progress_dialog.setWindowTitle("Vidage de toutes les corbeilles") main_progress_dialog.setMinimumSize(700, 600) # Taille augmentée main_progress_dialog.setMaximumSize(900, 800) # Taille maximale main_progress_dialog.resize(750, 650) # Taille par défaut main_progress_dialog.setWindowFlags(main_progress_dialog.windowFlags() | Qt.WindowStaysOnTopHint) # Style amélioré de la boîte de dialogue main_progress_dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; font-size: 12px; font-weight: bold; } QProgressBar { border: 2px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #34495e; color: white; height: 25px; font-weight: bold; } QProgressBar::chunk { background-color: #3498db; border-radius: 3px; margin: 0.5px; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 5px; font-family: Consolas, monospace; font-size: 11px; } QGroupBox { font-weight: bold; color: white; border: 2px solid #2c3e50; border-radius: 5px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; } QScrollArea { border: none; background-color: transparent; } QScrollBar:vertical { background: rgba(0, 0, 0, 0.1); width: 8px; margin: 0px; border: none; border-radius: 4px; } QScrollBar::handle:vertical { background: rgba(255, 255, 255, 0.3); min-height: 20px; border-radius: 4px; margin: 2px; } QScrollBar::handle:vertical:hover { background: rgba(255, 255, 255, 0.5); } QScrollBar::handle:vertical:pressed { background: rgba(255, 255, 255, 0.7); } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; border: none; background: none; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; border: none; } """) # Layout principal avec marges appropriées main_layout = QVBoxLayout(main_progress_dialog) main_layout.setContentsMargins(15, 15, 15, 15) main_layout.setSpacing(10) # En-tête avec informations générales header_group = QGroupBox("Progression générale") header_layout = QVBoxLayout(header_group) # Labels d'information main_info_label = QLabel("Vidage des corbeilles de tous les comptes") main_info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: white; padding: 5px;") header_layout.addWidget(main_info_label) # Barre de progression pour les comptes account_progress_bar = QProgressBar() account_progress_bar.setRange(0, outlook.Folders.Count) account_progress_bar.setValue(0) account_progress_bar.setFormat("Comptes traités: %v/%m") header_layout.addWidget(account_progress_bar) # Compteur global main_count_label = QLabel("0 comptes traités | 0/0 éléments supprimés") main_count_label.setStyleSheet("font-size: 12px; color: #5babfb; padding: 3px;") header_layout.addWidget(main_count_label) main_layout.addWidget(header_group) # Zone défilante pour les comptes individuels accounts_group = QGroupBox("Progression par compte") accounts_group_layout = QVBoxLayout(accounts_group) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setMinimumHeight(200) # Hauteur minimale pour la zone défilante # Widget conteneur pour les barres de progression des comptes accounts_widget = QWidget() accounts_layout = QVBoxLayout(accounts_widget) accounts_layout.setContentsMargins(5, 5, 5, 5) accounts_layout.setSpacing(5) scroll_area.setWidget(accounts_widget) accounts_group_layout.addWidget(scroll_area) main_layout.addWidget(accounts_group) # Zone de détails avec hauteur augmentée details_group = QGroupBox("Détails") details_layout = QVBoxLayout(details_group) log_text = QTextEdit() log_text.setReadOnly(True) log_text.setMinimumHeight(150) # Hauteur augmentée pour les détails details_layout.addWidget(log_text) main_layout.addWidget(details_group) # Fonction pour ajouter une barre de progression pour un compte def add_account_progress_bar(account_name, total_items): # Container pour ce compte account_container = QWidget() account_container_layout = QVBoxLayout(account_container) account_container_layout.setContentsMargins(0, 0, 0, 0) account_container_layout.setSpacing(3) # Label du compte avec info account_label = QLabel(f"{account_name}:") account_label.setStyleSheet("color: white; font-size: 11px; font-weight: bold;") account_container_layout.addWidget(account_label) # Barre de progression pour ce compte account_progress = QProgressBar() account_progress.setRange(0, total_items) account_progress.setValue(0) account_progress.setStyleSheet(""" QProgressBar { border: 1px solid #2c3e50; border-radius: 3px; text-align: center; background-color: #34495e; color: white; height: 18px; font-size: 10px; } QProgressBar::chunk { background-color: #27ae60; border-radius: 2px; } """) if total_items > 0: account_progress.setFormat(f"0/{total_items} éléments supprimés") else: account_progress.setFormat("Corbeille vide") account_container_layout.addWidget(account_progress) # Ajouter au layout principal des comptes accounts_layout.addWidget(account_container) # Faire défiler vers le bas pour voir le nouveau compte QApplication.processEvents() scroll_bar = scroll_area.verticalScrollBar() scroll_bar.setValue(scroll_bar.maximum()) return account_progress # Fonction pour ajouter du texte au log def append_to_log(text): from PyQt5.QtCore import QDateTime timestamp = QDateTime.currentDateTime().toString("HH:mm:ss") formatted_text = f"[{timestamp}] {text}" log_text.append(formatted_text) # Défiler vers le bas scroll_bar = log_text.verticalScrollBar() scroll_bar.setValue(scroll_bar.maximum()) QApplication.processEvents() # Afficher la boîte de dialogue sans bloquer main_progress_dialog.show() QApplication.processEvents() # Traiter chaque compte for i in range(1, outlook.Folders.Count + 1): try: mailbox = outlook.Folders.Item(i) mailbox_name = mailbox.Name append_to_log(f"Traitement du compte: {mailbox_name}") main_info_label.setText(f"Traitement du compte: {mailbox_name}") account_progress_bar.setValue(i) QApplication.processEvents() # Rechercher le dossier de corbeille de manière récursive deleted_items_folder, found_trash_name = find_trash_folder(mailbox) if deleted_items_folder: # Récupérer le nombre d'éléments avant la suppression items_count = deleted_items_folder.Items.Count total_items_count += items_count items_deleted = 0 errors = 0 if items_count > 0: append_to_log(f"Corbeille '{found_trash_name}' trouvée avec {items_count} éléments") # Créer une barre de progression pour ce compte account_progress = add_account_progress_bar(mailbox_name, items_count) else: append_to_log(f"Corbeille '{found_trash_name}' trouvée mais vide") account_progress = add_account_progress_bar(mailbox_name, 0) if items_count > 0: # Supprimer tous les éléments for item in reversed(list(range(1, items_count + 1))): try: deleted_items_folder.Items.Item(item).Delete() items_deleted += 1 total_items_deleted += 1 # Mettre à jour la progression tous les 5 éléments ou lors du dernier élément if items_deleted % 5 == 0 or items_deleted == items_count: account_progress.setValue(items_deleted) account_progress.setFormat( f"{items_deleted}/{items_count} éléments supprimés") main_count_label.setText( f"{i}/{outlook.Folders.Count} comptes traités | {total_items_deleted}/{total_items_count} éléments supprimés") QApplication.processEvents() except Exception as e: errors += 1 continue # Mise à jour finale pour ce compte account_progress.setValue(items_count) append_to_log( f"✓ {mailbox_name}: {items_deleted}/{items_count} éléments supprimés ({errors} erreurs)") else: append_to_log(f"✓ {mailbox_name}: corbeille '{found_trash_name}' déjà vide") accounts_processed += 1 accounts_info.append({ 'name': mailbox_name, 'trash_name': found_trash_name, 'items_deleted': items_deleted, 'items_count': items_count, 'errors': errors }) else: append_to_log(f"✗ {mailbox_name}: corbeille non trouvée") add_account_progress_bar(mailbox_name, 0) accounts_info.append({ 'name': mailbox_name, 'trash_name': 'Non trouvée', 'items_deleted': 0, 'items_count': 0, 'errors': 0 }) except Exception as e: append_to_log(f"✗ Erreur avec {mailbox_name}: {str(e)}") logger.error(f"Erreur lors du traitement du compte {mailbox_name}: {str(e)}") continue # Mettre à jour les statistiques globales main_count_label.setText( f"{i}/{outlook.Folders.Count} comptes traités | {total_items_deleted}/{total_items_count} éléments supprimés") QApplication.processEvents() # Mise à jour finale account_progress_bar.setValue(outlook.Folders.Count) main_info_label.setText("Opération terminée !") append_to_log("🎉 Opération terminée avec succès !") # Ajouter un espace flexible avant le bouton spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) main_layout.addWidget(spacer) # Bouton Fermer avec style amélioré close_button = QPushButton("Fermer") close_button.clicked.connect(main_progress_dialog.accept) close_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 12px; min-width: 100px; } QPushButton:hover { background-color: #c0392b; } QPushButton:pressed { background-color: #a93226; } """) main_layout.addWidget(close_button) # Attendre que l'utilisateur ferme la boîte de dialogue main_progress_dialog.exec_() # Créer un rapport détaillé pour l'afficher après if accounts_processed > 0: report = f"Opération terminée: {accounts_processed} corbeille(s) vidée(s)\n\n" report += f"Total: {total_items_deleted} élément(s) supprimé(s) sur {total_items_count}\n\n" report += "Détails par compte:\n" for account in accounts_info: if account['trash_name'] != 'Non trouvée': report += f"• {account['name']} ('{account['trash_name']}'): " report += f"{account['items_deleted']}/{account['items_count']} éléments supprimés" if account['errors'] > 0: report += f" ({account['errors']} erreurs)" report += "\n" else: report += f"• {account['name']}: Corbeille non trouvée\n" logger.info(report) else: report = "Aucune corbeille n'a pu être vidée.\n\nComptes analysés :\n" for account in accounts_info: report += f"• {account['name']}: Corbeille non trouvée\n" logger.warning(report) # Désinitialiser COM pythoncom.CoUninitialize() except Exception as e: logger.error(f"Erreur lors du vidage de toutes les corbeilles: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du vidage des corbeilles: {str(e)}") try: pythoncom.CoUninitialize() except: pass def empty_scan_trash(self): """Définit si les emails doivent être supprimés définitivement lors du scan ou du comptage""" try: # Dialogue de configuration pour la suppression définitive dialog = QDialog(self) dialog.setWindowTitle("Configuration de la suppression d'emails") dialog.setMinimumWidth(400) dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QCheckBox { color: white; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) layout = QVBoxLayout(dialog) # Explication info_label = QLabel( "Lorsqu'un email est marqué comme supprimé lors d'un scan ou d'un comptage, " "vous pouvez choisir s'il doit être mis à la corbeille ou supprimé définitivement." ) info_label.setWordWrap(True) layout.addWidget(info_label) # Options pour la suppression self.permanent_delete_checkbox = QCheckBox( "Supprimer définitivement les emails (sans passer par la corbeille)") # Récupérer l'état actuel s'il existe if hasattr(self, 'permanent_delete_enabled'): self.permanent_delete_checkbox.setChecked(self.permanent_delete_enabled) layout.addWidget(self.permanent_delete_checkbox) # Appliquer aux différentes opérations scan_group = QGroupBox("Appliquer à :") scan_layout = QVBoxLayout() self.apply_to_scan_checkbox = QCheckBox("Scan email") self.apply_to_scan_checkbox.setChecked(True) self.apply_to_count_checkbox = QCheckBox("Comptage email") self.apply_to_count_checkbox.setChecked(True) scan_layout.addWidget(self.apply_to_scan_checkbox) scan_layout.addWidget(self.apply_to_count_checkbox) scan_group.setLayout(scan_layout) scan_group.setStyleSheet("QGroupBox { color: white; }") layout.addWidget(scan_group) # Avertissement warning_label = QLabel( "⚠️ ATTENTION: La suppression définitive est irréversible. " "Les emails ne pourront pas être récupérés." ) warning_label.setStyleSheet("color: #ff6b6b; font-weight: bold;") warning_label.setWordWrap(True) layout.addWidget(warning_label) # Boutons button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(dialog.accept) button_box.rejected.connect(dialog.reject) layout.addWidget(button_box) # Exécution du dialogue if dialog.exec_() == QDialog.Accepted: # Sauvegarder les paramètres self.permanent_delete_enabled = self.permanent_delete_checkbox.isChecked() self.apply_to_scan = self.apply_to_scan_checkbox.isChecked() self.apply_to_count = self.apply_to_count_checkbox.isChecked() # Appliquer les modifications aux gestionnaires correspondants if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'outlook_handler'): handler = self.outlook_widget.outlook_handler handler.permanent_delete_enabled = self.permanent_delete_enabled handler.apply_to_scan = self.apply_to_scan handler.apply_to_count = self.apply_to_count # Informer l'utilisateur status_msg = "la suppression définitive" if self.permanent_delete_enabled else "la mise à la corbeille" operations = [] if self.apply_to_scan: operations.append("scan email") if self.apply_to_count: operations.append("comptage email") if operations: ops_str = " et ".join(operations) QMessageBox.information( self, "Configuration enregistrée", f"Les emails seront traités avec {status_msg} lors des opérations: {ops_str}" ) logger.info(f"Configuration de suppression des emails mise à jour: {status_msg} pour {ops_str}") else: QMessageBox.warning( self, "Aucune opération sélectionnée", "Aucune opération n'a été sélectionnée pour appliquer la configuration." ) except Exception as e: logger.error(f"Erreur lors de la configuration de la suppression: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la configuration: {str(e)}") def update_pause_values(self, min_val, max_val): """ Met à jour les valeurs de pause (temps d'affichage) dans toute l'interface """ # Mettre à jour l'étiquette dans le panneau latéral if hasattr(self, 'pause_value_label'): self.pause_value_label.setText(f"{min_val}-{max_val}") # Mettre à jour l'OutlookWidget if hasattr(self, 'outlook_widget'): if hasattr(self.outlook_widget, 'update_display_time'): self.outlook_widget.update_display_time(min_val, max_val) # Mettre à jour l'affichage principal for label in self.findChildren(QLabel): if hasattr(label, 'text') and label.text() and "Temps d'affichage" in label.text(): label.setText(f"Temps d'affichage: {min_val}-{max_val}s") break # Mettre à jour le panneau flottant if hasattr(self, 'panel'): if hasattr(self.panel, 'display_value'): self.panel.display_value.setText(f"{min_val}-{max_val}s") # Forcer la mise à jour immédiate du panneau self.panel.update_info_display() # Mettre à jour l'OutlookHandler si disponible if hasattr(self, 'outlook_handler') and self.outlook_handler: if hasattr(self.outlook_handler, 'set_pause_time_range'): self.outlook_handler.set_pause_time_range(min_val, max_val) def update_between_pages_values(self, min_val, max_val): """ Met à jour les valeurs de temps entre pages dans toute l'interface """ # Mettre à jour l'étiquette dans le panneau latéral if hasattr(self, 'between_pages_value_label'): self.between_pages_value_label.setText(f"{min_val}-{max_val}") # Mettre à jour l'OutlookWidget if hasattr(self, 'outlook_widget'): if hasattr(self.outlook_widget, 'update_pause_time'): self.outlook_widget.update_pause_time(min_val, max_val) # Mettre à jour l'affichage principal for label in self.findChildren(QLabel): if hasattr(label, 'text') and label.text() and "Temps de pause" in label.text(): label.setText(f"Temps de pause: {min_val}-{max_val}s") break # Mettre à jour le panneau flottant if hasattr(self, 'panel'): if hasattr(self.panel, 'pause_value'): self.panel.pause_value.setText(f"{min_val}-{max_val}s") # Forcer la mise à jour immédiate du panneau self.panel.update_info_display() # Mettre à jour l'OutlookHandler si disponible if hasattr(self, 'outlook_handler') and self.outlook_handler: if hasattr(self.outlook_handler, 'set_between_pages_time_range'): self.outlook_handler.set_between_pages_time_range(min_val, max_val) def update_links_count(self, value): # Mettre à jour l'étiquette dans le panneau latéral self.links_value_label.setText(str(value)) # Mettre à jour les informations dans l'interface principale if hasattr(self, 'links_count_main_label'): self.links_count_main_label.setText(f"Nombre de liens traités simultanément: {value}") # Mettre à jour OutlookHandler if self.outlook_handler: self.outlook_handler.set_links_count(value) # Mettre à jour OutlookWidget si nécessaire if hasattr(self, 'outlook_widget') and self.outlook_widget: if hasattr(self.outlook_widget, 'update_max_tabs'): self.outlook_widget.update_max_tabs(value) # Mettre à jour le panneau flottant if hasattr(self, 'panel') and hasattr(self.panel, 'links_value'): self.panel.links_value.setText(str(value)) def moveEvent(self, event): """Déplace le volet flottant lorsque la fenêtre bouge.""" self.update_panel_position() super().moveEvent(event) def update_panel_position(self): """Synchronise la position du volet avec la fenêtre principale.""" if hasattr(self, 'panel') and self.panel: # Appeler la méthode du panneau au lieu de faire le déplacement directement self.panel.update_panel_position() def initialize_profiles(self): """Initialise les profils avec un délai pour assurer que l'interface est prête""" try: logger.info("Début de l'initialisation des profils") # Charger la liste des profils self.load_profiles_list() # Vérifier s'il y a des profils chargés if self.profile_list.count() > 0: logger.info(f"{self.profile_list.count()} profils trouvés") # Charger le dernier profil utilisé last_profile_file = os.path.join(r"C:\autolook\profiles", ".last_used") if os.path.exists(last_profile_file): try: with open(last_profile_file, 'r') as f: last_profile = f.read().strip() # Trouver et sélectionner le dernier profil utilisé index = self.profile_list.findText(last_profile) if index >= 0: self.profile_list.setCurrentIndex(index) # Charger le profil avec un petit délai QTimer.singleShot(200, lambda: self.load_profile()) logger.info(f"Dernier profil '{last_profile}' chargé") except Exception as e: logger.error(f"Erreur lors du chargement du dernier profil : {str(e)}") else: logger.warning("Aucun profil trouvé") except Exception as e: logger.error(f"Erreur lors de l'initialisation des profils : {str(e)}") def show_links_dashboard(self): try: # Créer le dialogue de chargement loading_dialog = EnhancedLoadingDialog(self) # Classe worker pour le chargement class LoaderThread(QThread): finished = pyqtSignal(object) progress_updated = pyqtSignal(int) error_occurred = pyqtSignal(str) def run(self): try: # Initialisation self.progress_updated.emit(10) QThread.msleep(200) # Création du tableau de bord dashboard = LinksDashboardDialog(None) self.progress_updated.emit(30) QThread.msleep(200) # Chargement des données dashboard.load_profiles() self.progress_updated.emit(60) QThread.msleep(200) # Finalisation self.progress_updated.emit(100) self.finished.emit(dashboard) except Exception as e: self.error_occurred.emit(str(e)) # Créer et configurer le thread loader = LoaderThread() def handle_finished(dashboard): loading_dialog.accept() if dashboard: dashboard.exec_() def handle_error(error_msg): loading_dialog.accept() QMessageBox.critical(self, "Erreur", f"Erreur lors du chargement : {error_msg}") loader.finished.connect(handle_finished) loader.progress_updated.connect(loading_dialog.set_progress) loader.error_occurred.connect(handle_error) # Démarrer le chargement loader.start() loading_dialog.exec_() except Exception as e: logger.error(f"Erreur lors de l'affichage du tableau de bord : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'affichage du tableau de bord : {str(e)}") def validate_profile_data(self, profile_data): """ Valide les données de profil avant leur sauvegarde ou modification. """ # Vérification de la Date de validité si elle est cochée if profile_data.get('validity_date_checked', False): # Vérifie si la case est cochée if 'validity_days' not in profile_data or not profile_data['validity_days']: QMessageBox.warning(self, "Validation Erreur", "Veuillez saisir un nombre de jours pour la date de validité.") return False try: validity_days = int(profile_data['validity_days']) validity_date = datetime.now() + timedelta(days=validity_days) if validity_date < datetime.now(): QMessageBox.warning(self, "Validation Erreur", "La date de validité doit être dans le futur.") return False except ValueError: QMessageBox.warning(self, "Validation Erreur", "Le nombre de jours pour la validité doit être un entier.") return False # Vérifier d'autres champs si nécessaire (comme "Nombre max d'emails") if 'max_emails' in profile_data: try: max_emails = int(profile_data['max_emails']) if max_emails <= 0: QMessageBox.warning(self, "Validation Erreur", "Le nombre maximum d'emails doit être supérieur à zéro.") return False except ValueError: QMessageBox.warning(self, "Validation Erreur", "Le nombre maximum d'emails doit être un entier.") return False # Si toutes les vérifications passent return True def create_priority_display(self): # Création du groupe pour l'affichage de la priorité et des ressources priority_group = QGroupBox("État du Système") priority_layout = QVBoxLayout() priority_layout.setSpacing(8) priority_layout.setContentsMargins(10, 15, 10, 10) # LIGNE 1: Tableau de bord système + Forçage Low Priority (sur la même ligne) system_controls_layout = QHBoxLayout() system_controls_layout.setSpacing(5) # Bouton du tableau de bord système self.dashboard_button = QPushButton("🔍 Tableau de bord") self.dashboard_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 6px 8px; border-radius: 4px; font-weight: bold; margin: 1px; font-size: 11px; } QPushButton:hover { background-color: #2980b9; color: white; } """) self.dashboard_button.clicked.connect(self.open_system_dashboard) # Bouton de forçage priorité basse self.force_low_priority_button = QPushButton("Forçage Low Priority") self.force_low_priority_button.setCheckable(True) self.force_low_priority_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 6px 8px; border-radius: 4px; font-weight: bold; margin: 1px; font-size: 11px; } QPushButton:checked { background-color: #FF5252; color: white; } QPushButton:hover { background-color: #45a049; } QPushButton:checked:hover { background-color: #ff4444; } """) self.force_low_priority_button.clicked.connect(self.toggle_low_priority) system_controls_layout.addWidget(self.dashboard_button) system_controls_layout.addWidget(self.force_low_priority_button) priority_layout.addLayout(system_controls_layout) # LIGNE 2: Affichage de la priorité self.priority_label = QLabel("Priorité : En attente...") self.priority_label.setAlignment(Qt.AlignCenter) self.priority_label.setStyleSheet(""" QLabel { color: #3498db; font-size: 13px; font-weight: bold; padding: 4px; border-radius: 3px; background-color: rgba(52, 152, 219, 0.1); } """) priority_layout.addWidget(self.priority_label) # Timer pour mettre à jour l'affichage de la priorité if not hasattr(self, 'priority_update_timer'): self.priority_update_timer = QTimer() self.priority_update_timer.timeout.connect(self.update_priority_display) self.priority_update_timer.start(2000) # Mise à jour toutes les 2 secondes priority_group.setLayout(priority_layout) return priority_group def open_system_dashboard(self): """Ouvre le tableau de bord système""" try: if hasattr(self, 'system_task_monitor'): self.system_task_monitor.open_dashboard() else: # Initialiser le moniteur de tâches système si ce n'est pas déjà fait self.system_task_monitor = SystemTaskMonitor(self) self.system_task_monitor.open_dashboard() except Exception as e: logger.error(f"Erreur lors de l'ouverture du tableau de bord système: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture du tableau de bord: {str(e)}") def update_priority_display(self): """Met à jour l'affichage de l'état de priorité""" try: # Obtenir le processus courant current_process = psutil.Process() if sys.platform == 'win32': import win32process try: # Obtenir la priorité actuelle priority_class = current_process.nice() # Déterminer le texte et la couleur à afficher if hasattr(self, 'force_low_priority_button') and self.force_low_priority_button.isChecked(): priority_text = "Basse (Forcée)" color = "#18e836" # Vert pour priorité basse else: # Mapping des valeurs de priorité priority_mapping = { win32process.IDLE_PRIORITY_CLASS: ("Basse", "#18e836"), win32process.BELOW_NORMAL_PRIORITY_CLASS: ("Sous normale", "#9fdb5a"), win32process.NORMAL_PRIORITY_CLASS: ("Normale", "#FFC107"), win32process.ABOVE_NORMAL_PRIORITY_CLASS: ("Au dessus de la normale", "#FF9800"), win32process.HIGH_PRIORITY_CLASS: ("Haute", "#F44336"), win32process.REALTIME_PRIORITY_CLASS: ("Temps réel", "#9C27B0") } priority_text, color = priority_mapping.get( priority_class, ("Inconnue", "#c3c3c3")) # Texte simple sans HTML full_text = f"Priorité : {priority_text}" # Vérifier que le texte ne dépasse pas la largeur du label if len(full_text) > 25: # Ajuster cette valeur si nécessaire short_text = f"Priorité: {priority_text[:10]}..." self.priority_label.setText(short_text) else: self.priority_label.setText(full_text) # Appliquer couleur à tout le label self.priority_label.setStyleSheet(f"color: {color}; font-size: 14px; font-weight: ") except Exception as e: self.priority_label.setText("Priorité : N/A") logger.error(f"Erreur lors de la récupération de la priorité : {str(e)}") else: # Systèmes non-Windows self.priority_label.setText("Priorité : N/A") except Exception as e: logger.error(f"Erreur lors de la mise à jour de l'affichage de la priorité : {str(e)}") self.priority_label.setText("Priorité : Erreur") def toggle_low_priority(self): """Active ou désactive la priorité basse forcée sur tous les processus""" try: if sys.platform == 'win32': import win32process import win32api import win32con # Obtenir le processus actuel current_process = psutil.Process() # Déterminer la priorité à appliquer selon l'état du bouton if self.force_low_priority_button.isChecked(): # Appliquer priorité basse (IDLE_PRIORITY_CLASS) target_priority = win32process.IDLE_PRIORITY_CLASS self.force_low_priority_button.setText("Désactiver Forçage Low Priority") logger.info("Priorité basse forcée appliquée") else: # Restaurer priorité normale target_priority = win32process.NORMAL_PRIORITY_CLASS self.force_low_priority_button.setText("Activer Forçage Low Priority") logger.info("Priorité normale restaurée") # Appliquer au processus principal Python try: handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, os.getpid()) win32process.SetPriorityClass(handle, target_priority) win32api.CloseHandle(handle) logger.info(f"Priorité du processus principal mise à jour: {target_priority}") except Exception as e: logger.error(f"Erreur lors de la modification de la priorité du processus principal: {str(e)}") # Appliquer aux processus QtWebEngine for proc in psutil.process_iter(['name', 'pid']): try: if 'QtWebEngineProcess' in proc.info['name']: try: # Ouvrir le processus avec les droits nécessaires web_handle = win32api.OpenProcess( win32con.PROCESS_SET_INFORMATION | win32con.PROCESS_QUERY_INFORMATION, False, proc.info['pid'] ) # Modifier la priorité win32process.SetPriorityClass(web_handle, target_priority) # Fermer le handle win32api.CloseHandle(web_handle) logger.info(f"Priorité modifiée pour {proc.info['name']} (PID: {proc.info['pid']})") except Exception as proc_error: logger.error( f"Erreur lors de la modification de la priorité de {proc.info['name']}: {str(proc_error)}") except (psutil.NoSuchProcess, psutil.AccessDenied): continue # Configuration du timer de surveillance if not hasattr(self, 'priority_check_timer'): self.priority_check_timer = QTimer(self) self.priority_check_timer.timeout.connect(self.check_process_priorities) if self.force_low_priority_button.isChecked(): self.priority_check_timer.start(5000) # Vérifier toutes les 5 secondes else: self.priority_check_timer.stop() # Forcer la mise à jour de l'affichage self.update_priority_display() else: # Système non Windows logger.info("La fonction de priorité est uniquement disponible sous Windows") QMessageBox.information(self, "Information", "La fonction de priorité est uniquement disponible sous Windows") except Exception as e: logger.error(f"Erreur lors du changement de priorité : {str(e)}") self.priority_label.setText("Priorité : Erreur") def check_process_priorities(self): """Vérifie et maintient la priorité des processus""" try: if not hasattr(self, 'force_low_priority_button') or not self.force_low_priority_button.isChecked(): return import win32process import win32api import win32con # Vérifier uniquement les processus QtWebEngine for proc in psutil.process_iter(['name', 'pid']): try: if 'QtWebEngineProcess' in proc.info['name']: try: # Ouvrir le processus web_handle = win32api.OpenProcess( win32con.PROCESS_SET_INFORMATION | win32con.PROCESS_QUERY_INFORMATION, False, proc.info['pid'] ) # Vérifier la priorité actuelle current_priority = win32process.GetPriorityClass(web_handle) # Restaurer priorité basse si nécessaire if current_priority != win32process.IDLE_PRIORITY_CLASS: win32process.SetPriorityClass(web_handle, win32process.IDLE_PRIORITY_CLASS) logger.info( f"Priorité basse restaurée pour {proc.info['name']} (PID: {proc.info['pid']})") # Fermer le handle win32api.CloseHandle(web_handle) except Exception as e: # Ignorer les erreurs silencieusement pour éviter de spammer les logs pass except (psutil.NoSuchProcess, psutil.AccessDenied): continue except Exception as e: logger.error(f"Erreur lors de la vérification des priorités : {str(e)}") @pyqtSlot(str) def load_profile_from_scheduler(self, profile_name): """Charge un profil depuis le planificateur et lance le scan automatiquement""" self.is_loading_profile = True try: logger.info(f"Chargement du profil {profile_name} depuis le planificateur") # Récupérer les informations de la tâche planifiée scheduled_mail_count = None scheduled_report = False schedules_file = os.path.join(r"C:\autolook", "schedules.json") if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: schedules = json.load(f) for schedule in schedules: if schedule['profile'] == profile_name: scheduled_mail_count = schedule.get('mail_count', None) scheduled_report = schedule.get('generate_report', False) logger.info( f"Tâche trouvée avec {scheduled_mail_count} emails et rapport: {scheduled_report}") break # Stockage global temporaire de la valeur pour passage direct au scan self.scheduled_task_params = { 'mail_count': scheduled_mail_count, 'generate_report': scheduled_report } # PARTIE CRITIQUE - Manuellement chercher le profil sans se fier à la liste déroulante profile_files = os.listdir(r"C:\autolook\profiles") found_profile = False # Rechercher le profil exact dans le dossier des profils profile_filename = f"{profile_name}.json" if profile_filename in profile_files: # Profil trouvé, maintenant on doit le sélectionner dans la liste déroulante found_profile = True # Chercher dans la liste déroulante (avec ou sans étoile) index = -1 for i in range(self.profile_list.count()): item_text = self.profile_list.itemText(i) # Comparer avec le nom exact et avec le nom précédé de l'étoile if item_text == profile_name or item_text == f"⭐ {profile_name}": index = i logger.info(f"Profil trouvé dans la liste à l'index {index}: {item_text}") break if index >= 0: self.profile_list.setCurrentIndex(index) # Charger le profil avec le drapeau from_scheduler=True self.load_profile(from_scheduler=True) # Lancer le scan avec un délai QTimer.singleShot(2000, lambda: self.start_scheduled_scan(profile_name)) logger.info(f"Chargement et scan programmés pour le profil {profile_name}") else: # Si le profil existe mais n'est pas dans la liste, le premier charger dans la liste logger.warning(f"Profil {profile_name} trouvé sur disque mais pas dans l'interface") # On recharge la liste des profils self.load_profiles_list() # On essaie à nouveau après avoir rechargé la liste QTimer.singleShot(1000, lambda: self.load_profile_from_scheduler(profile_name)) else: # Le profil n'existe pas du tout dans le dossier logger.error(f"Profil {profile_name} introuvable dans le dossier des profils") logger.error(f"Profils disponibles: {profile_files}") self.is_loading_profile = False self.outlook_widget.mini_browser.update_page_time("Display Xs", start_timer=False) return found_profile except Exception as e: logger.error(f"Erreur lors du chargement du profil depuis le planificateur: {str(e)}") # Nettoyage en cas d'erreur if hasattr(self, 'scheduled_task_params'): delattr(self, 'scheduled_task_params') self.is_loading_profile = False return False def start_scan_timer(self, estimated_duration): """ Démarre un minuteur décomptant pour afficher la durée restante du scan Args: estimated_duration: Durée estimée en secondes """ try: # Initialiser le temps restant self.remaining_seconds = int(estimated_duration) # Créer et démarrer le timer self.elapsed_timer = QTimer() self.elapsed_timer.timeout.connect(self.update_countdown_time) self.elapsed_timer.start(1000) # Mise à jour chaque seconde # Afficher le temps initial self.update_countdown_time() logger.info(f"Décompte démarré - Durée: {estimated_duration} secondes") except Exception as e: logger.error(f"Erreur lors du démarrage du décompte: {str(e)}") def update_countdown_time(self): """Met à jour l'affichage du décompte""" try: if hasattr(self, 'outlook_widget'): if self.remaining_seconds > 0: # Calculer minutes et secondes restantes minutes = self.remaining_seconds // 60 seconds = self.remaining_seconds % 60 # Mettre à jour l'affichage du temps (format MM:SS) time_display = f"{minutes:02d}:{seconds:02d}" # Chercher le QLabel spécifique qui affiche le temps for widget in self.findChildren(QLabel): if widget.text().count(':') == 1 and len(widget.text()) == 5: widget.setText(time_display) break # Décrémenter le compteur self.remaining_seconds -= 1 else: # Arrêter le timer quand le décompte atteint zéro self.stop_scan_timer() logger.info("Décompte terminé") # Vérifier si le scan est terminé if not self.outlook_widget.is_scanning: self.stop_scan_timer() logger.info("Scan terminé avant la fin du décompte") except Exception as e: logger.error(f"Erreur lors de la mise à jour du décompte: {str(e)}") self.stop_scan_timer() def stop_scan_timer(self): """Arrête le minuteur de scan""" try: if hasattr(self, 'elapsed_timer'): self.elapsed_timer.stop() # Effacer le timer pour libérer les ressources self.elapsed_timer.deleteLater() delattr(self, 'elapsed_timer') logger.info("Minuteur de décompte arrêté") except Exception as e: logger.error(f"Erreur lors de l'arrêt du minuteur: {str(e)}") def update_scan_progress(self): """Met à jour l'affichage de la progression du scan""" try: if hasattr(self, 'outlook_widget'): elapsed_time = (datetime.now() - self.scan_start_time).total_seconds() progress = min(int((elapsed_time / self.scan_estimated_duration) * 100), 100) # Mettre à jour la barre de progression self.outlook_widget.progress_bar.setValue(progress) # Calculer le temps restant if progress < 100: remaining_seconds = max(self.scan_estimated_duration - elapsed_time, 0) minutes = int(remaining_seconds // 60) seconds = int(remaining_seconds % 60) self.outlook_widget.status_label.setText( f"Scan en cours... Temps restant estimé: {minutes}m {seconds}s" ) else: # Arrêter le timer si terminé self.scan_timer.stop() logger.info("Minuteur de scan terminé") except Exception as e: logger.error(f"Erreur lors de la mise à jour de la progression: {str(e)}") if hasattr(self, 'scan_timer'): self.scan_timer.stop() def start_scheduled_scan(self, profile_name): """Lance le scan après que l'interface soit mise à jour""" try: # NOUVEAU: Lire les paramètres de tâche directement depuis le fichier task_params_file = os.path.join(r"C:\autolook", "current_task_params.json") task_params = None max_emails = None # Initialiser max_emails ici if os.path.exists(task_params_file): try: # Utiliser un bloc try pour gérer les problèmes d'accès au fichier with open(task_params_file, 'r') as f: task_params = json.load(f) logger.info(f"Paramètres récupérés du fichier temporaire: {task_params}") # Ne tenter de supprimer le fichier que si la lecture a réussi try: # Ajout d'un délai avant de tenter de supprimer le fichier QTimer.singleShot(100, lambda: self.safely_remove_file(task_params_file)) except Exception as file_error: logger.warning(f"Impossible de supprimer le fichier: {str(file_error)}") except Exception as read_error: logger.warning(f"Impossible de lire le fichier de paramètres: {str(read_error)}") else: logger.warning("Fichier de paramètres de tâche non trouvé") if hasattr(self, 'outlook_widget'): # Vérifier que la boîte mail et le dossier sont bien sélectionnés mailbox_item = self.outlook_widget.mailbox_listbox.currentItem() folder_item = self.outlook_widget.folder_listbox.currentItem() if not mailbox_item or not folder_item: logger.error("Boîte mail ou dossier non sélectionnés après délai") return # IMPORTANT: Forcer le nombre d'emails depuis les paramètres de la tâche if task_params and 'mail_count' in task_params: max_emails = task_params['mail_count'] # Mettre à jour l'interface mais ce n'est pas ce qui sera utilisé if hasattr(self.outlook_widget, 'max_emails_entry'): self.outlook_widget.max_emails_entry.setText(str(max_emails)) logger.info(f"Utilisation forcée du nombre d'emails de la tâche: {max_emails}") # Mettre à jour le toggle de rapport if hasattr(self, 'report_toggle') and 'generate_report' in task_params: self.report_toggle.setChecked(task_params['generate_report']) else: # Si aucun paramètre trouvé, utiliser l'interface (cas normal non planifié) max_emails = int( self.outlook_widget.max_emails_entry.text()) if self.outlook_widget.max_emails_entry.text() else None logger.info(f"Aucun paramètre de tâche trouvé, utilisation de l'interface: {max_emails}") # Initialisation du scan self.outlook_widget.is_scanning = True self.outlook_widget.set_scanning_state(True) self.outlook_widget.status_label.setText("Scan en cours...") self.outlook_widget.progress_bar.setValue(0) self.outlook_widget.outlook_handler.links_clicked = 0 self.outlook_widget.outlook_handler.links_skipped = 0 # Paramètres du scan scan_params = { 'mailbox': mailbox_item.text(), 'folder': folder_item.text(), 'sender': self.outlook_widget.sender_entry.text(), 'subject_keywords': self.outlook_widget.subject_entry.text().split(), 'link_keywords': self.outlook_widget.link_entry.text().split(), 'max_emails': max_emails # Valeur forcée de la tâche } logger.info(f"Démarrage du scan avec paramètres: {scan_params}") self.outlook_widget.log_action(f"Démarrage du scan automatique avec {max_emails} emails") # NOUVEAU: Définir directement max_emails dans l'OutlookHandler avant de lancer le scan if hasattr(self.outlook_widget, 'outlook_handler') and max_emails is not None: # Stocker directement la valeur max_emails dans l'OutlookHandler self.outlook_widget.outlook_handler._forced_max_emails = max_emails logger.info(f"Valeur forcée dans outlook_handler: {max_emails}") # Connecter le signal de progression self.outlook_widget.outlook_handler.scan_progress.connect(self.outlook_widget.update_progress) # Exécution du scan clicked_links, matching_emails = self.outlook_widget.outlook_handler.scan_emails( scan_params['mailbox'], scan_params['folder'], scan_params['sender'], scan_params['subject_keywords'], scan_params['link_keywords'], scan_params['max_emails'] ) # Finalisation et nettoyage if hasattr(self.outlook_widget, 'outlook_handler') and hasattr(self.outlook_widget.outlook_handler, '_forced_max_emails'): delattr(self.outlook_widget.outlook_handler, '_forced_max_emails') self.outlook_widget.outlook_handler.scan_progress.disconnect(self.outlook_widget.update_progress) self.outlook_widget.is_scanning = False self.outlook_widget.set_scanning_state(False) self.outlook_widget.progress_bar.setValue(100) self.outlook_widget.status_label.setText(f"Scan terminé. {clicked_links} liens cliqués.") self.outlook_widget.update_reward_display() except Exception as e: logger.error(f"Erreur lors du scan automatique: {str(e)}") if hasattr(self, 'outlook_widget'): self.outlook_widget.is_scanning = False self.outlook_widget.set_scanning_state(False) self.outlook_widget.status_label.setText("Erreur lors du scan") # Ajouter cette méthode pour supprimer le fichier en toute sécurité def safely_remove_file(self, file_path): """Supprime un fichier de manière sécurisée avec plusieurs tentatives""" max_attempts = 5 for attempt in range(max_attempts): try: if os.path.exists(file_path): os.remove(file_path) logger.info(f"Fichier temporaire supprimé: {file_path}") return True return False except Exception as e: logger.warning(f"Tentative {attempt + 1}/{max_attempts} - Échec de suppression: {str(e)}") # Attendre un peu avant de réessayer time.sleep(0.2) logger.error(f"Impossible de supprimer le fichier après {max_attempts} tentatives: {file_path}") return False def verify_mail_count(self, profile_name): """Vérifie et corrige le nombre de mails si nécessaire""" try: profile_path = os.path.join(r"C:\autolook\profiles", f"{profile_name}.json") if os.path.exists(profile_path): with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) if 'settings' in profile_data and 'mail_count' in profile_data['settings']: expected_mail_count = profile_data['settings']['mail_count'] current_mail_count = self.mail_count_spinbox.value() if current_mail_count != expected_mail_count: logger.info(f"Correction du nombre de mails: {current_mail_count} -> {expected_mail_count}") self.mail_count_spinbox.setValue(expected_mail_count) except Exception as e: logger.error(f"Erreur lors de la vérification du nombre de mails: {str(e)}") def resizeEvent(self, event): """Gère le redimensionnement de la fenêtre et ajuste les éléments d'interface""" # Repositionner les boutons self.messaging_button.move(self.width() - 590, 10) self.toggle_button.move(self.width() - 480, 10) self.update_button.move(self.width() - 360, 10) self.save_profile_button.move(self.width() - 240, 10) self.param_button.move(self.width() - 110, 10) # Adapter la taille des composants internes if hasattr(self, 'outlook_widget'): # Ajuster la taille de l'interface Outlook self.outlook_widget.setFixedWidth(self.width() - 20) # Mettre à jour la position du panneau flottant self.update_panel_position() super().resizeEvent(event) def update_system_info(self): cpu_usage = psutil.cpu_percent() memory_usage = psutil.virtual_memory().percent def on_task_started(self, schedule): if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"Tâche planifiée démarrée : {schedule['profile']}") def on_task_completed(self, schedule): if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"Tâche planifiée terminée : {schedule['profile']}") def on_task_error(self, schedule, error): if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"Erreur lors de l'exécution de la tâche {schedule['profile']} : {error}") def update_modules(self): # Implémentez ici la logique pour mettre à jour les modules QMessageBox.information(self, "Mise à jour des modules", "La mise à jour des modules n'est pas encore implémentée.") def show_help(self): print("Affichage de l'aide") # Implémentez ici la logique pour afficher l'aide def setup_sidebar(self): self.sidebar = QWidget() self.sidebar.setFixedWidth(0) self.sidebar.setStyleSheet("background-color: #34495e; color: white;") self.main_layout.addWidget(self.sidebar) # Création d'un widget de défilement scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet(""" QScrollArea { background-color: #34495e; border: none; } QScrollBar:vertical { background-color: #2c3e50; width: 12px; margin: 0px; } QScrollBar::handle:vertical { background-color: #030a3f; min-height: 20px; border-radius: 6px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } """) # Créer un widget pour contenir tous les éléments sidebar_content = QWidget() self.sidebar_content = sidebar_content sidebar_layout = QVBoxLayout(sidebar_content) sidebar_layout.setSpacing(10) sidebar_title = QLabel("Paramètres") sidebar_title.setFont(QFont("Arial", 20, QFont.Bold)) sidebar_title.setAlignment(Qt.AlignCenter) sidebar_layout.addWidget(sidebar_title) # Bouton Mode Admin self.admin_button = QPushButton("🔐 Mode Admin") self.admin_button.setStyleSheet(""" QPushButton { background-color: #403ce7; color: white; border: none; padding: 5px; border-radius: 6px; font-weight: bold; font-size: 12px; margin: 5px 0px; } QPushButton:hover { background-color: #c0392b; } """) self.admin_button.clicked.connect(self.open_admin_interface) sidebar_layout.addWidget(self.admin_button) # Bouton Notes self.notes_btn = QPushButton("📝 Notes") self.notes_btn.setStyleSheet(""" QPushButton { background-color: #2D297A; color: white; border: none; padding: 5px; border-radius: 6px; font-weight: bold; font-size: 12px; margin: 5px 0px; } QPushButton:hover { background-color: #18138A; } """) self.notes_btn.clicked.connect(self.open_update_notes_interface) sidebar_layout.addWidget(self.notes_btn) # === Planificateur : activation et intervalle de vérification === self.scheduler_group = QGroupBox("") scheduler_layout = QVBoxLayout(self.scheduler_group) # Checkbox pour activer / désactiver la vérification des tâches planifiées self.verify_tasks_checkbox = QCheckBox("Activer vérification des tâches planifiées") self.verify_tasks_checkbox.setStyleSheet("color: white;") settings = QSettings("Snoown2k", "MyApp") enabled = settings.value("scheduler/enable_verification", True, type=bool) self.verify_tasks_checkbox.setChecked(enabled) self.verify_tasks_checkbox.stateChanged.connect(self.on_scheduler_verification_toggled) scheduler_layout.addWidget(self.verify_tasks_checkbox) # Widget contenant le slider + label (visible seulement si activé) self.interval_widget = QWidget() interval_layout = QVBoxLayout(self.interval_widget) interval_layout.setContentsMargins(10, 0, 0, 0) # Indentation pour montrer la hiérarchie # Récupérer la valeur initiale et s'assurer qu'elle est un multiple de 5 initial_interval = settings.value("scheduler/interval_seconds", 30, type=int) initial_interval = int(round(initial_interval / 5)) * 5 # Forcer un multiple de 5 self.interval_label = QLabel(f"Délai entre vérifications : {initial_interval}s") self.interval_label.setStyleSheet("color: white; font-size: 11px;") self.interval_slider = QSlider(Qt.Horizontal) self.interval_slider.setMinimum(30) # 30 secondes minimum self.interval_slider.setMaximum(300) # 300 secondes maximum (5 minutes) self.interval_slider.setSingleStep(5) # Pas de 5 secondes self.interval_slider.setPageStep(5) # Page step aussi de 5 self.interval_slider.setTickInterval(30) # Graduations tous les 30s self.interval_slider.setTickPosition(QSlider.TicksBelow) self.interval_slider.setValue(initial_interval) self.interval_slider.valueChanged.connect(self.on_scheduler_interval_changed) self.interval_slider.setStyleSheet(""" QSlider::groove:horizontal { height: 1px; background: #34495e; margin: 0px; border-radius: 1px; } QSlider::handle:horizontal { background: #3498db; border: 1px solid #2980b9; width: 12px; height: 12px; margin: -6px 0; border-radius: 6px; } QSlider::sub-page:horizontal { background: #3498db; height: 1px; border-radius: 1px; } QSlider::add-page:horizontal { background: #34495e; height: 1px; border-radius: 1px; } """) interval_layout.addWidget(self.interval_label) interval_layout.addWidget(self.interval_slider) # Rendre visible seulement si la vérification est activée self.interval_widget.setVisible(enabled) scheduler_layout.addWidget(self.interval_widget) sidebar_layout.addWidget(self.scheduler_group) # Séparateur visuel separator = QFrame() separator.setFrameShape(QFrame.HLine) separator.setStyleSheet("color: #7f8c8d;") sidebar_layout.addWidget(separator) # === Notifications de mise à jour : activation et intervalle de vérification === self.update_notifications_group = QGroupBox("") update_notifications_layout = QVBoxLayout(self.update_notifications_group) # Checkbox pour activer / désactiver les notifications périodiques de mise à jour self.verify_updates_checkbox = QCheckBox("Activer notifications périodiques de mise à jour") self.verify_updates_checkbox.setStyleSheet("color: white;") settings = QSettings("Snoown2k", "MyApp") enabled = settings.value("updates/enable_periodic_check", True, type=bool) self.verify_updates_checkbox.setChecked(enabled) self.verify_updates_checkbox.stateChanged.connect(self.on_update_notifications_toggled) update_notifications_layout.addWidget(self.verify_updates_checkbox) # Widget contenant le slider + label (visible seulement si activé) self.update_interval_widget = QWidget() update_interval_layout = QVBoxLayout(self.update_interval_widget) update_interval_layout.setContentsMargins(10, 0, 0, 0) # Indentation pour montrer la hiérarchie # Récupérer la valeur initiale et s'assurer qu'elle est un multiple de 15 minutes initial_interval = settings.value("updates/interval_minutes", 240, type=int) # 4 heures par défaut initial_interval = int(round(initial_interval / 15)) * 15 # Forcer un multiple de 15 self.update_interval_label = QLabel( f"Délai entre vérifications : {self._format_update_interval(initial_interval)}") self.update_interval_label.setStyleSheet("color: white; font-size: 11px;") self.update_interval_slider = QSlider(Qt.Horizontal) self.update_interval_slider.setMinimum(10) # 10 minutes minimum self.update_interval_slider.setMaximum(240) # 240 minutes maximum (4 heures) self.update_interval_slider.setSingleStep(15) # Pas de 15 minutes self.update_interval_slider.setPageStep(15) # Page step aussi de 15 self.update_interval_slider.setTickInterval(30) # Graduations toutes les 30 minutes self.update_interval_slider.setTickPosition(QSlider.TicksBelow) self.update_interval_slider.setValue(initial_interval) self.update_interval_slider.valueChanged.connect(self.on_update_interval_changed) self.update_interval_slider.setStyleSheet(""" QSlider::groove:horizontal { height: 1px; background: #34495e; margin: 0px; border-radius: 1px; } QSlider::handle:horizontal { background: #e74c3c; border: 1px solid #c0392b; width: 12px; height: 12px; margin: -6px 0; border-radius: 6px; } QSlider::sub-page:horizontal { background: #e74c3c; height: 1px; border-radius: 1px; } QSlider::add-page:horizontal { background: #34495e; height: 1px; border-radius: 1px; } """) update_interval_layout.addWidget(self.update_interval_label) update_interval_layout.addWidget(self.update_interval_slider) # Rendre visible seulement si la vérification est activée self.update_interval_widget.setVisible(enabled) update_notifications_layout.addWidget(self.update_interval_widget) sidebar_layout.addWidget(self.update_notifications_group) # Configurer le timer de mise à jour avec les paramètres sauvegardés self.setup_update_check_timer() # Séparateur visuel separator = QFrame() separator.setFrameShape(QFrame.HLine) separator.setStyleSheet("color: #7f8c8d;") sidebar_layout.addWidget(separator) # Fonction pour créer les cadres de paramètres def create_param_frame(label_text, slider, value_label): frame = QFrame() frame.setStyleSheet("background-color: #2c3e50; border-radius: 5px;") layout = QVBoxLayout(frame) label = QLabel(label_text) layout.addWidget(label) slider_layout = QHBoxLayout() slider_layout.addWidget(slider) slider_layout.addWidget(value_label) layout.addLayout(slider_layout) return frame # Période d'affichage slider self.pause_slider = DoubleSlider() self.pause_value_label = QLabel("15-25") self.pause_slider.valueChanged.connect(lambda min_val, max_val: self.update_pause_values(min_val, max_val)) pause_frame = create_param_frame( "Période d'affichage de la page (secondes):", self.pause_slider, self.pause_value_label ) sidebar_layout.addWidget(pause_frame) # Période entre pages slider self.between_pages_slider = DoubleSlider() self.between_pages_slider.min_slider.setMinimum(3) self.between_pages_slider.min_slider.setMaximum(20) self.between_pages_slider.max_slider.setMinimum(3) self.between_pages_slider.max_slider.setMaximum(20) self.between_pages_slider.setValues(5, 10) self.between_pages_value_label = QLabel("5-10") self.between_pages_slider.valueChanged.connect( lambda min_val, max_val: self.update_between_pages_values(min_val, max_val)) between_pages_frame = create_param_frame( "Période de pause entre pages (secondes):", self.between_pages_slider, self.between_pages_value_label ) sidebar_layout.addWidget(between_pages_frame) # 1. Bouton toggle pour choisir le mode de traitement self.processing_mode_button = QPushButton("⚡ Traitement Mail Mode direct") self.processing_mode_button.setCheckable(True) self.processing_mode_button.setChecked(False) # False = mode direct par défaut self.processing_mode_button.clicked.connect(self.toggle_processing_mode) # Style moderne et compact du bouton self.processing_mode_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); color: white; border: 1px solid #2c5282; padding: 6px 12px; border-radius: 12px; font-size: 10px; font-weight: 600; max-height: 24px; min-width: 120px; } QPushButton:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #5ba0f2, stop: 1 #4682cd); box-shadow: 0 2px 4px rgba(0,0,0,0.2); } QPushButton:pressed { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #357abd, stop: 1 #2c5282); } """) # 2. Variable d'état liée au bouton self.use_cached_processing = False # False = traitement direct à la volée # 3. Ajout du bouton dans ton layout (remplace layout_parametres par le nom exact chez toi) sidebar_layout.addWidget(self.processing_mode_button) # Ajouter la case à cocher pour Traitement Multilink self.multilink_checkbox = QCheckBox("Traitement Multilink") self.multilink_checkbox.setStyleSheet("color: white;") self.multilink_checkbox.toggled.connect(self.toggle_multilink_processing) sidebar_layout.addWidget(self.multilink_checkbox) # Nombre d'emails slider - avec affichage dynamique self.links_slider = QSlider(Qt.Horizontal) self.links_slider.setMinimum(2) self.links_slider.setMaximum(50) self.links_slider.setValue(2) self.links_slider.setTickPosition(QSlider.TicksBelow) self.links_slider.setTickInterval(5) # Créer le label pour afficher la valeur actuelle self.links_value_label = QLabel("2") self.links_value_label.setStyleSheet(""" background-color: #030a3f; color: white; border-radius: 3px; padding: 2px 6px; min-width: 30px; text-align: center; """) self.links_value_label.setAlignment(Qt.AlignCenter) # Connecter le signal valueChanged pour mettre à jour à la fois l'interface et le backend self.links_slider.valueChanged.connect(self.update_links_count_display) # Créer la frame avec un layout horizontal pour le slider et le label self.links_frame = QFrame() self.links_frame.setStyleSheet("background-color: #2c3e50; border-radius: 5px;") links_layout = QVBoxLayout(self.links_frame) # Ajouter l'étiquette principale links_label = QLabel("Nombre d'emails simultanés traités:") links_layout.addWidget(links_label) # Créer un layout horizontal pour le slider et la valeur slider_value_layout = QHBoxLayout() slider_value_layout.addWidget(self.links_slider) slider_value_layout.addWidget(self.links_value_label) links_layout.addLayout(slider_value_layout) # Ajouter la frame au layout de la sidebar sidebar_layout.addWidget(self.links_frame) # Masquer par défaut le contrôle du nombre d'emails self.links_frame.setVisible(False) # Case à cocher pour Traitement par cycle self.cycle_processing_checkbox = QCheckBox("Traitement par cycle") self.cycle_processing_checkbox.setStyleSheet("color: white;") self.cycle_processing_checkbox.toggled.connect(self.toggle_cycle_processing) sidebar_layout.addWidget(self.cycle_processing_checkbox) # Groupe pour les paramètres de cycle self.cycle_group = QGroupBox("") self.cycle_group.setStyleSheet("background-color: #2c3e50; color: white; border-radius: 1px;") cycle_group_layout = QVBoxLayout(self.cycle_group) cycle_group_layout.setSpacing(10) # Slider pour le nombre d'emails par série emails_per_cycle_layout = QVBoxLayout() emails_per_cycle_label = QLabel("Nombre d'emails par série (min-max):") emails_per_cycle_layout.addWidget(emails_per_cycle_label) # Créer un double slider pour le nombre d'emails par cycle self.emails_per_cycle_slider = DoubleSlider() self.emails_per_cycle_slider.min_slider.setMinimum(2) self.emails_per_cycle_slider.min_slider.setMaximum(100) self.emails_per_cycle_slider.max_slider.setMinimum(2) self.emails_per_cycle_slider.max_slider.setMaximum(100) self.emails_per_cycle_slider.setValues(2, 100) # Créer un widget pour les valeurs min et max emails_input_layout = QHBoxLayout() # Champ de saisie pour le nombre minimal d'emails par série self.emails_per_cycle_min_input = QLineEdit() self.emails_per_cycle_min_input.setFixedWidth(30) self.emails_per_cycle_min_input.setAlignment(Qt.AlignCenter) self.emails_per_cycle_min_input.setText(str(2)) self.emails_per_cycle_min_input.setStyleSheet(""" background-color: #03699c; color: white; border-radius: 2px; padding: 1px; """) # Label séparateur separator_label = QLabel("-") separator_label.setStyleSheet("color: white;") # Champ de saisie pour le nombre maximal d'emails par série self.emails_per_cycle_max_input = QLineEdit() self.emails_per_cycle_max_input.setFixedWidth(30) self.emails_per_cycle_max_input.setAlignment(Qt.AlignCenter) self.emails_per_cycle_max_input.setText(str(100)) self.emails_per_cycle_max_input.setStyleSheet(""" background-color: #03699c; color: white; border-radius: 2px; padding: 1px; """) # Ajouter les widgets au layout emails_input_layout.addWidget(self.emails_per_cycle_min_input) emails_input_layout.addWidget(separator_label) emails_input_layout.addWidget(self.emails_per_cycle_max_input) # Layout pour slider et valeurs emails_slider_layout = QHBoxLayout() emails_slider_layout.addWidget(self.emails_per_cycle_slider) emails_slider_layout.addLayout(emails_input_layout) emails_per_cycle_layout.addLayout(emails_slider_layout) cycle_group_layout.addLayout(emails_per_cycle_layout) # Connexion des signaux et slots pour le nombre d'emails self.emails_per_cycle_slider.valueChanged.connect(self.update_emails_per_cycle_range) self.emails_per_cycle_min_input.textChanged.connect(self.update_emails_slider_from_min_input) self.emails_per_cycle_max_input.textChanged.connect(self.update_emails_slider_from_max_input) # Slider pour le temps de pause entre les séries pause_between_cycles_layout = QVBoxLayout() pause_between_cycles_label = QLabel("Temps de pause entre séries (secondes):") pause_between_cycles_layout.addWidget(pause_between_cycles_label) # Créer un double slider pour le temps de pause entre cycles self.pause_between_cycles_slider = DoubleSlider() self.pause_between_cycles_slider.min_slider.setMinimum(5) self.pause_between_cycles_slider.min_slider.setMaximum(2700) self.pause_between_cycles_slider.max_slider.setMinimum(5) self.pause_between_cycles_slider.max_slider.setMaximum(2700) self.pause_between_cycles_slider.setValues(5, 2700) # Créer un widget pour les valeurs min et max pause_input_layout = QHBoxLayout() # Champ de saisie pour le temps minimal de pause self.pause_between_cycles_min_input = QLineEdit() self.pause_between_cycles_min_input.setFixedWidth(30) self.pause_between_cycles_min_input.setAlignment(Qt.AlignCenter) self.pause_between_cycles_min_input.setText(str(5)) self.pause_between_cycles_min_input.setStyleSheet(""" background-color: #03699c; color: white; border-radius: 2px; padding: 1px; """) # Label séparateur pause_separator_label = QLabel("-") pause_separator_label.setStyleSheet("color: white;") # Champ de saisie pour le temps maximal de pause self.pause_between_cycles_max_input = QLineEdit() self.pause_between_cycles_max_input.setFixedWidth(30) self.pause_between_cycles_max_input.setAlignment(Qt.AlignCenter) self.pause_between_cycles_max_input.setText(str(2700)) self.pause_between_cycles_max_input.setStyleSheet(""" background-color: #03699c; color: white; border-radius: 2px; padding: 1px; """) # Ajouter les widgets au layout pause_input_layout.addWidget(self.pause_between_cycles_min_input) pause_input_layout.addWidget(pause_separator_label) pause_input_layout.addWidget(self.pause_between_cycles_max_input) # Layout pour slider et valeurs pause_slider_layout = QHBoxLayout() pause_slider_layout.addWidget(self.pause_between_cycles_slider) pause_slider_layout.addLayout(pause_input_layout) pause_between_cycles_layout.addLayout(pause_slider_layout) cycle_group_layout.addLayout(pause_between_cycles_layout) # Connexion des signaux et slots pour le temps de pause self.pause_between_cycles_slider.valueChanged.connect(self.update_pause_between_cycles_range) self.pause_between_cycles_min_input.textChanged.connect(self.update_pause_slider_from_min_input) self.pause_between_cycles_max_input.textChanged.connect(self.update_pause_slider_from_max_input) # Ajouter le groupe de cycle au layout sidebar sidebar_layout.addWidget(self.cycle_group) # Masquer par défaut le groupe de paramètres de cycle self.cycle_group.setVisible(False) # Après l'initialisation des sliders, initialiser les paramètres dans OutlookHandler if hasattr(self, 'outlook_handler') and self.outlook_handler: # Vérifier si la méthode existe avant de l'appeler if hasattr(self.outlook_handler, 'initialize_cycle_parameters'): self.outlook_handler.initialize_cycle_parameters() else: # Alternative si la méthode n'existe pas if hasattr(self.outlook_handler, 'set_emails_per_cycle'): self.outlook_handler.set_emails_per_cycle((5, 100)) if hasattr(self.outlook_handler, 'set_pause_between_cycles'): self.outlook_handler.set_pause_between_cycles((5, 600)) # Section Options fin de traitement end_options_group = QGroupBox("Options fin de traitement") end_options_group.setStyleSheet(""" QGroupBox { background-color: #2c3e50; color: white; border-radius: 5px; margin-top: 20px; font-weight: bold; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 5px 10px; background-color: #2c3e50; } """) end_options_layout = QVBoxLayout(end_options_group) end_options_layout.setContentsMargins(10, 20, 10, 10) # LIGNE 1: Rapport traitement + Vider corbeille (sur la même ligne) report_corbeille_layout = QHBoxLayout() report_corbeille_layout.setSpacing(5) # Bouton rapport self.report_toggle = QPushButton("Rapport traitement désactivé") self.report_toggle.setCheckable(True) self.report_toggle.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; border: none; padding: 8px; border-radius: 5px; font-size: 11px; font-weight: bold; } QPushButton:checked { background-color: #2ecc71; } """) self.report_toggle.toggled.connect(self.toggle_report) # Bouton pour vider la corbeille Outlook self.empty_trash_button = QPushButton("Vider la corbeille Outlook") self.empty_trash_button.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #007B9A; } """) self.empty_trash_button.clicked.connect(self.show_empty_trash_menu) report_corbeille_layout.addWidget(self.report_toggle) report_corbeille_layout.addWidget(self.empty_trash_button) end_options_layout.addLayout(report_corbeille_layout) # LIGNE 2: Options système (sur la même ligne) system_options_layout = QHBoxLayout() system_options_layout.setSpacing(5) # Case à cocher pour verrouillage de la session Windows self.lock_session_checkbox = QCheckBox("Verrouiller la session Windows") self.lock_session_checkbox.setStyleSheet("color: white; font-size: 11px;") # Case à cocher pour éteindre le PC self.shutdown_pc_checkbox = QCheckBox("Éteindre le PC") self.shutdown_pc_checkbox.setStyleSheet("color: white; font-size: 11px;") system_options_layout.addWidget(self.lock_session_checkbox) system_options_layout.addWidget(self.shutdown_pc_checkbox) end_options_layout.addLayout(system_options_layout) # Ajouter le groupe au layout sidebar sidebar_layout.addWidget(end_options_group) # Dans la création du panneau paramètres, ajouter : duplicates_group = QGroupBox("Gestion des doublons") duplicates_layout = QVBoxLayout(duplicates_group) self.duplicates_detection_checkbox = QCheckBox("Activer la détection des doublons") self.duplicates_detection_checkbox.setToolTip( "Si activé, les emails avec des liens déjà traités seront ignorés") self.duplicates_detection_checkbox.setStyleSheet(""" QToolTip { background-color: #2e2e2e; color: white; border: 1px solid #ffffff; padding: 4px; border-radius: 5px; } """) self.duplicates_detection_checkbox.stateChanged.connect(self.on_duplicates_detection_changed) duplicates_layout.addWidget(self.duplicates_detection_checkbox) # Ajouter une info sur le nombre de doublons détectés self.duplicates_info_label = QLabel("Aucun doublon détecté") self.duplicates_info_label.setStyleSheet("color: #666; font-style: italic;") duplicates_layout.addWidget(self.duplicates_info_label) sidebar_layout.addWidget(duplicates_group) banner_label = QLabel("Position du bandeau :") banner_label.setStyleSheet("color: white; font-weight: bold;") sidebar_layout.addWidget(banner_label) self.banner_position_combo = QComboBox() self.banner_position_combo.addItems(["Bas", "Haut", "Désactivé"]) self.banner_position_combo.setToolTip("Choisir la position du bandeau d'informations dans le mini navigateur") self.banner_position_combo.setStyleSheet(""" QToolTip { background-color: #2e2e2e; color: white; border: 1px solid #ffffff; padding: 4px; border-radius: 5px; } """) self.banner_position_combo.setCurrentIndex(2) # Définir "Désactivé" par défaut self.banner_position_combo.currentIndexChanged.connect(self.on_banner_position_changed) sidebar_layout.addWidget(self.banner_position_combo) # Appliquer immédiatement la configuration par défaut QTimer.singleShot(100, lambda: self.on_banner_position_changed(2)) # Ajouter une vérification après un délai plus long QTimer.singleShot(2000, self.verify_banner_sync) # Affichage priorité priority_display = self.create_priority_display() sidebar_layout.addWidget(priority_display) # Taille de police slider self.font_size_slider = QSlider(Qt.Horizontal) self.font_size_slider.setMinimum(8) self.font_size_slider.setMaximum(16) self.font_size_slider.setValue(self.font_size) self.font_size_slider.setTickPosition(QSlider.TicksBelow) self.font_size_slider.setTickInterval(1) self.font_size_slider.valueChanged.connect(self.update_font_size) self.font_size_value_label = QLabel(str(self.font_size)) font_size_frame = create_param_frame("Taille des caractères:", self.font_size_slider, self.font_size_value_label) sidebar_layout.addWidget(font_size_frame) # Section gestion des profils profiles_group = QGroupBox("Gestion des Profils") profiles_layout = QVBoxLayout(profiles_group) # Liste des profils self.profile_list = QComboBox() logger.info("Création du widget QComboBox pour la liste des profils") # Style pour la liste déroulante self.profile_list.setStyleSheet(""" QComboBox { background-color: #2a7195; color: yellow; border: 1px solid #2c3e50; border-radius: 1px; padding: 4px; min-width: 200px; min-height: 10px; } QComboBox::drop-down { border: none; padding-right: 20px; } QComboBox::down-arrow { width: 12px; height: 12px; } QComboBox QAbstractItemView { background-color: #2a7195; selection-background-color: #2c3e50; selection-color: white; } QComboBox QAbstractItemView::item { padding: 5px; border: none; } QComboBox QAbstractItemView::item:selected { background-color: #2c3e50; color: white; } """) profiles_layout.addWidget(self.profile_list) logger.info("Widget profile_list ajouté au layout") # Chargement différé des profils QTimer.singleShot(100, self.initialize_profiles) logger.info("Chargement des profils programmé") # Description du profil self.description = QLabel() self.description.setWordWrap(True) self.description.setStyleSheet("color: #bdc3c7; font-style: italic;") profiles_layout.addWidget(self.description) # Date de dernière modification self.last_modified = QLabel() self.last_modified.setStyleSheet("color: #95a5a6; font-size: 10px;") # Barre d'outils des profils toolbar = QHBoxLayout() buttons = { 'load': ("📂", "Charger", self.load_profile), 'save': ("💾", "Sauvegarder", self.save_profile), 'delete': ("🗑️", "Supprimer", self.delete_profile), 'export': ("📤", "Exporter", self.export_profile), 'import': ("📥", "Importer", self.import_profile), 'modify': ("✏️", "Modifier", self.modify_profile), 'favorite': ("⭐", "Favori", self.toggle_favorite), 'scheduler': ("📅", "Planificateur", self.show_scheduler) } for icon, tooltip, slot in buttons.values(): btn = QPushButton(icon) btn.setToolTip(tooltip) btn.clicked.connect(slot) btn.setFixedSize(30, 30) btn.setStyleSheet(""" QPushButton { background-color: #008CBA; color: Black; border: none; border-radius: 4px; font-size: 14px; } QPushButton:hover { background-color: #006A8C; } QToolTip { background-color: #e2effd; color: black; border: 1px solid #2c3e50; border-radius: 4px; padding: 5px; font-size: 12px; font-weight: bold; } """) toolbar.addWidget(btn) profiles_layout.addLayout(toolbar) # Statistiques self.profile_stats_label = QLabel("0 profils | Dernier chargé: Jamais") self.profile_stats_label.setStyleSheet("color: #95a5a6; font-size: 10px;") profiles_layout.addWidget(self.profile_stats_label) # Case à cocher pour afficher les Emails expirés self.show_empty_window_checkbox = QCheckBox("Afficher les Emails expirés au démarrage") self.show_empty_window_checkbox.setStyleSheet(""" QCheckBox { color: white; font-size: 11px; margin-top: 5px; } QCheckBox::indicator { width: 15px; height: 15px; } QCheckBox::indicator:checked { background-color: #2ecc71; border: 1px solid #2ecc71; border-radius: 3px; } QCheckBox::indicator:unchecked { background-color: #34495e; border: 1px solid #7f8c8d; border-radius: 3px; } """) self.show_empty_window_checkbox.toggled.connect(self.toggle_expired_check) settings = QSettings("Snoown2k", "MyApp") self.show_empty_window_checkbox.setChecked(settings.value("check_expired_profiles", False, type=bool)) profiles_layout.addWidget(self.show_empty_window_checkbox) # Ajouter le groupe de profils sidebar_layout.addWidget(profiles_group) # Groupe compacté pour la gestion des mots-clés (sans titre, pour ligne personnalisée) pattern_keywords_group = QGroupBox() pattern_keywords_group.setStyleSheet(""" QGroupBox { border: 1px solid #3b82f6; border-radius: 6px; margin-top: 10px; padding-top: 8px; font-weight: bold; color: white; } QToolTip { color: #111; background-color: #fefefe; border: 1px solid #aaa; padding: 4px; } """) pattern_keywords_layout = QVBoxLayout(pattern_keywords_group) pattern_keywords_layout.setContentsMargins(8, 12, 8, 8) pattern_keywords_layout.setSpacing(6) # Ligne titre + compteur title_layout = QHBoxLayout() title_label = QLabel("🔍 Mots-clés Pattern") title_label.setStyleSheet("color: white; font-weight: bold; font-size: 11px;") self.pattern_count_label = QLabel("📊 0") self.pattern_count_label.setStyleSheet("color: #10b981; font-size: 11px; font-weight: bold;") title_layout.addWidget(title_label) title_layout.addStretch() title_layout.addWidget(self.pattern_count_label) pattern_keywords_layout.addLayout(title_layout) # Liste des mots-clés self.pattern_keywords_list = QListWidget() self.pattern_keywords_list.setFixedHeight(80) self.pattern_keywords_list.setStyleSheet(""" QListWidget { background-color: #0f172a; border: 1px solid #475569; border-radius: 4px; color: #f1f5f9; font-size: 11px; padding: 4px; } QListWidget::item { padding: 4px 8px; } QListWidget::item:selected { background-color: #3b82f6; color: white; } """) pattern_keywords_layout.addWidget(self.pattern_keywords_list) # Ligne compacte : champ + 5 boutons input_layout = QHBoxLayout() input_layout.setSpacing(6) self.pattern_keyword_input = QLineEdit() self.pattern_keyword_input.setPlaceholderText("Ajouter un mot-clé...") self.pattern_keyword_input.setMaximumWidth(180) self.pattern_keyword_input.setStyleSheet(""" QLineEdit { background-color: #1e293b; color: white; border: 1px solid #334155; border-radius: 4px; padding: 4px 8px; font-size: 11px; } """) input_layout.addWidget(self.pattern_keyword_input) # Style des boutons icon_btn_style = """ QPushButton { background-color: #334155; border: none; color: white; font-size: 13px; padding: 6px 8px; border-radius: 4px; } QPushButton:hover { background-color: #475569; } """ # Boutons btns = [ ("➕", "Ajouter", self.add_pattern_keyword), ("✏️", "Modifier", self.edit_pattern_keyword), ("🗑️", "Supprimer", self.remove_pattern_keyword), ("📥", "Importer", self.import_pattern_keywords), ("📤", "Exporter", self.export_pattern_keywords), ] for symbol, tooltip, handler in btns: btn = QPushButton(symbol) btn.setToolTip(tooltip) btn.setStyleSheet(icon_btn_style) btn.clicked.connect(handler) input_layout.addWidget(btn) pattern_keywords_layout.addLayout(input_layout) # Chargement initial self.load_pattern_keywords_immediately() self.update_pattern_keyword_count() # Appel initial du compteur sidebar_layout.addWidget(pattern_keywords_group) # Chargement initial self.load_pattern_keywords_immediately() sidebar_layout.addWidget(pattern_keywords_group) # Configurer le timer de mise à jour avec les paramètres sauvegardés (si pas déjà fait) if not hasattr(self, 'update_check_timer'): self.setup_update_check_timer() # Ajouter un espace extensible pour permettre le défilement jusqu'au bas sidebar_layout.addStretch() # Assigner le widget contenu à la scroll area scroll_area.setWidget(sidebar_content) # Créer le layout principal du sidebar qui contient uniquement la scroll area main_sidebar_layout = QVBoxLayout(self.sidebar) main_sidebar_layout.setContentsMargins(0, 0, 0, 0) main_sidebar_layout.addWidget(scroll_area) # Configurer les animations self.sidebar_animation = QPropertyAnimation(self.sidebar, b"minimumWidth") self.sidebar_animation.setEasingCurve(QEasingCurve.InOutQuad) self.sidebar_open = False # Configurer la surveillance du système if not hasattr(self, 'system_health_timer'): self.system_health_timer = QTimer(self) self.system_health_timer.timeout.connect(self.update_system_health) self.system_health_timer.start(5000) # Mise à jour toutes les 5 secondes # Timer pour mise à jour des informations système self.timer = QTimer(self) self.timer.timeout.connect(self.update_system_info) self.timer.start(1000) # Création différée de l'indicateur de performance QTimer.singleShot(500, lambda: self.create_performance_gauge() if hasattr(self, 'create_performance_gauge') else None) def on_scheduler_verification_toggled(self, state): """Gère l'activation/désactivation de la vérification des tâches planifiées""" enabled = state == Qt.Checked settings = QSettings("Snoown2k", "MyApp") settings.setValue("scheduler/enable_verification", enabled) # Afficher/masquer le widget du slider if hasattr(self, 'interval_widget'): self.interval_widget.setVisible(enabled) logger.info(f"Vérification planifiée {'activée' if enabled else 'désactivée'}") def on_scheduler_interval_changed(self, value): """Gère le changement d'intervalle avec contrainte par tranche de 5s""" # Forcer la valeur à être un multiple de 5 corrected_value = int(round(value / 5)) * 5 # Si la valeur a été corrigée, mettre à jour le slider sans déclencher le signal if corrected_value != value: self.interval_slider.blockSignals(True) self.interval_slider.setValue(corrected_value) self.interval_slider.blockSignals(False) # Sauvegarder la valeur corrigée settings = QSettings("Snoown2k", "MyApp") settings.setValue("scheduler/interval_seconds", corrected_value) # Mettre à jour le label if hasattr(self, 'interval_label'): self.interval_label.setText(f"Délai entre vérifications : {corrected_value}s") logger.info(f"Délai de vérification des tâches planifiées mis à jour : {corrected_value}s") def open_update_notes_interface(self): """Ouvre l'interface de notes via UpdateInterface""" try: # Créer une instance de UpdateInterface update_interface = UpdateInterface(self) # Appeler la méthode open_notes_interface de cette instance update_interface.open_notes_interface() except Exception as e: logger.error(f"Erreur lors de l'ouverture de l'interface des notes: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture de l'interface des notes: {str(e)}") def open_admin_interface(self): """Ouvre l'interface admin après vérification du mot de passe""" auth_dialog = AdminAuthDialog(self) if auth_dialog.exec_() == QDialog.Accepted and auth_dialog.password_correct: # Mot de passe correct, ouvrir l'interface admin admin_dialog = AdminInterface(self) admin_dialog.exec_() def apply_profile_colors(self): """Applique des couleurs spécifiques aux profils avec date de validité""" try: model = self.profile_list.model() if not model: return for i in range(self.profile_list.count()): item_text = self.profile_list.itemText(i) # Vérifier si l'item contient le symbole de validité if "⏰" in item_text: # Appliquer la couleur Vert pour les profils avec date de validité brush = QBrush(QColor("#7BF1A8")) # Vert model.setData(model.index(i, 0), brush, Qt.ForegroundRole) elif "⭐" in item_text: # Conserver la couleur jaune pour les favoris brush = QBrush(QColor("#FFD700")) # Jaune model.setData(model.index(i, 0), brush, Qt.ForegroundRole) except Exception as e: logger.error(f"Erreur lors de l'application des couleurs aux profils: {str(e)}") def toggle_processing_mode(self): if self.processing_mode_button.isChecked(): # Mode indirect - Style orange/gold self.processing_mode_button.setText("💾 Traitement Mail Mode Indirect") self.processing_mode_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f39c12, stop: 1 #e67e22); color: white; border: 1px solid #d35400; padding: 6px 12px; border-radius: 12px; font-size: 10px; font-weight: 600; max-height: 24px; min-width: 120px; } QPushButton:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f4ac22, stop: 1 #f39c12); box-shadow: 0 2px 4px rgba(0,0,0,0.2); } QPushButton:pressed { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e67e22, stop: 1 #d35400); } """) self.use_cached_processing = True if hasattr(self, 'log_message'): self.log_message("💾 Mode indirect activé") else: print("[MA2C] 💾 Mode indirect activé") else: # Mode direct - Style bleu self.processing_mode_button.setText("⚡ Traitement Mail Mode Direct") self.processing_mode_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #4a90e2, stop: 1 #357abd); color: white; border: 1px solid #2c5282; padding: 6px 12px; border-radius: 12px; font-size: 10px; font-weight: 600; max-height: 24px; min-width: 120px; } QPushButton:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #5ba0f2, stop: 1 #4682cd); box-shadow: 0 2px 4px rgba(0,0,0,0.2); } QPushButton:pressed { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #357abd, stop: 1 #2c5282); } """) self.use_cached_processing = False if hasattr(self, 'log_message'): self.log_message("⚡ Mode direct activé") else: print("[MA2C] ⚡ Mode direct activé") def update_pattern_keyword_count(self): count = self.pattern_keywords_list.count() self.pattern_count_label.setText(f"📊 {count}") def update_keywords_count_display(self): """Met à jour l'affichage du nombre de mots-clés.""" if not hasattr(self, 'keywords_count_label'): return # Si le widget n'existe pas encore count = self.pattern_keywords_list.count() if count == 0: self.keywords_count_label.setText("📊 0 mots-clés") self.keywords_count_label.setStyleSheet(""" QLabel { color: #6b7280; font-weight: bold; font-size: 12px; background: transparent; padding: 4px 8px; border-radius: 3px; } """) else: self.keywords_count_label.setText(f"📊 {count} mot{'s' if count > 1 else ''}-clé{'s' if count > 1 else ''}") self.keywords_count_label.setStyleSheet(""" QLabel { color: #fbbf24; font-weight: bold; font-size: 12px; background: transparent; padding: 4px 8px; border-radius: 3px; } """) print(f"✅ Compteur mis à jour : {count} mots-clés") # Debug def on_keyword_double_clicked(self, item): """Charge le mot-clé dans le champ de saisie lors du double-clic.""" if item: self.pattern_keyword_input.setText(item.text()) self.pattern_keyword_input.setFocus() # Changer le placeholder pour indiquer qu'on modifie self.pattern_keyword_input.setPlaceholderText("✏️ Modification en cours...") def load_pattern_keywords_immediately(self): """Charge les mots-clés immédiatement, même si outlook_handler n'est pas prêt.""" try: import os import json os.makedirs("Keywords", exist_ok=True) filepath = os.path.join("Keywords", "pattern_keywords.json") # Charger directement depuis le fichier if os.path.exists(filepath): with open(filepath, "r", encoding="utf-8") as f: keywords = json.load(f) print(f"🔄 Chargement de {len(keywords)} mots-clés depuis {filepath}") # Debug else: # Créer une liste par défaut keywords = ["view", "confirm", "click", "activate", "verify"] # Sauvegarder immédiatement with open(filepath, "w", encoding="utf-8") as f: json.dump(keywords, f, ensure_ascii=False, indent=2) print(f"🔄 Création du fichier avec {len(keywords)} mots-clés par défaut") # Debug # Afficher dans la liste self.pattern_keywords_list.clear() self.pattern_keywords_list.addItems(keywords) # IMPORTANT : Mettre à jour le compteur APRÈS avoir ajouté les éléments QTimer.singleShot(100, self.update_keywords_count_display) # Délai pour s'assurer que la liste est remplie # Programmer une synchronisation avec outlook_handler quand il sera prêt QTimer.singleShot(2000, lambda: self.sync_with_outlook_handler(keywords)) except Exception as e: print(f"❌ Erreur chargement : {e}") # En cas d'erreur, au moins afficher quelques mots-clés par défaut default_keywords = ["view", "confirm", "click", "activate", "verify"] self.pattern_keywords_list.clear() self.pattern_keywords_list.addItems(default_keywords) QTimer.singleShot(100, self.update_keywords_count_display) # Mettre à jour le compteur aussi ici def sync_with_outlook_handler(self, keywords): """Synchronise les mots-clés avec outlook_handler quand il est disponible.""" if hasattr(self, 'outlook_handler') and self.outlook_handler: if not hasattr(self.outlook_handler, 'pattern_keywords'): self.outlook_handler.pattern_keywords = [] self.outlook_handler.pattern_keywords = keywords.copy() # Sauvegarder via OutlookWidget self.outlook_handler.save_pattern_keywords() def refresh_pattern_keywords_list(self): """Met à jour dynamiquement la liste des mots-clés affichés dans le volet latéral.""" if hasattr(self, 'outlook_handler') and hasattr(self.outlook_handler, 'pattern_keywords'): self.pattern_keywords_list.clear() self.pattern_keywords_list.addItems(self.outlook_handler.pattern_keywords) print(f"🔄 Liste rafraîchie avec {len(self.outlook_handler.pattern_keywords)} mots-clés") # Debug else: # Si outlook_handler n'est pas prêt, recharger depuis le fichier self.load_pattern_keywords_immediately() return # Le compteur sera mis à jour dans load_pattern_keywords_immediately # Mettre à jour le compteur self.update_keywords_count_display() def save_keywords_to_file(self): """Sauvegarde directement les mots-clés de la liste dans le fichier.""" try: import os import json # Récupérer tous les mots-clés de la liste keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] # Sauvegarder os.makedirs("Keywords", exist_ok=True) filepath = os.path.join("Keywords", "pattern_keywords.json") with open(filepath, "w", encoding="utf-8") as f: json.dump(keywords, f, ensure_ascii=False, indent=2) except Exception as e: print(f"Erreur sauvegarde : {e}") def add_pattern_keyword(self): """Ajoute un mot-clé au mode pattern si non déjà présent.""" text = self.pattern_keyword_input.text().strip() if not text: return # Récupérer les mots-clés actuels de la liste current_keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] if text not in current_keywords: # Ajouter à la liste visuelle self.pattern_keywords_list.addItem(text) self.update_keywords_count_display() # Mettre à jour outlook_handler si disponible if hasattr(self, 'outlook_handler') and self.outlook_handler: if not hasattr(self.outlook_handler, 'pattern_keywords'): self.outlook_handler.pattern_keywords = [] self.outlook_handler.pattern_keywords.append(text) self.outlook_handler.save_pattern_keywords() else: # Sauvegarder directement dans le fichier self.save_keywords_to_file() self.pattern_keyword_input.clear() def edit_pattern_keyword(self): """Modifie le mot-clé sélectionné dans la liste.""" selected_items = self.pattern_keywords_list.selectedItems() if not selected_items: self.pattern_keyword_input.setPlaceholderText("⚠️ Veuillez d'abord sélectionner un mot-clé dans la liste") return old_text = selected_items[0].text() new_text = self.pattern_keyword_input.text().strip() if not new_text or new_text == old_text: return # Vérifier que le nouveau texte n'existe pas déjà current_keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] if new_text in current_keywords: self.pattern_keyword_input.setPlaceholderText("⚠️ Ce mot-clé existe déjà") return # Modifier dans la liste visuelle selected_items[0].setText(new_text) # Synchroniser avec outlook_handler si disponible if hasattr(self, 'outlook_handler') and self.outlook_handler: if hasattr(self.outlook_handler, 'pattern_keywords') and old_text in self.outlook_handler.pattern_keywords: index = self.outlook_handler.pattern_keywords.index(old_text) self.outlook_handler.pattern_keywords[index] = new_text self.outlook_handler.save_pattern_keywords() else: # Sauvegarder directement dans le fichier self.save_keywords_to_file() self.pattern_keyword_input.clear() self.update_keywords_count_display() def remove_pattern_keyword(self): """Supprime le(s) mot(s)-clé(s) sélectionné(s) dans la liste.""" selected_items = self.pattern_keywords_list.selectedItems() if not selected_items: return # Supprimer de la liste visuelle for item in selected_items: row = self.pattern_keywords_list.row(item) self.pattern_keywords_list.takeItem(row) # Synchroniser avec outlook_handler si disponible if hasattr(self, 'outlook_handler') and self.outlook_handler: if hasattr(self.outlook_handler, 'pattern_keywords'): # Reconstruire la liste depuis l'interface current_keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] self.outlook_handler.pattern_keywords = current_keywords.copy() self.outlook_handler.save_pattern_keywords() else: # Sauvegarder directement dans le fichier self.save_keywords_to_file() self.pattern_keyword_input.clear() self.update_keywords_count_display() def import_pattern_keywords(self): """Importe des mots-clés depuis un fichier JSON.""" from PyQt5.QtWidgets import QFileDialog, QMessageBox file_path, _ = QFileDialog.getOpenFileName( self, "Importer les mots-clés pattern", "", "Fichiers JSON (*.json);;Tous les fichiers (*)" ) if file_path: try: with open(file_path, 'r', encoding='utf-8') as f: imported_keywords = json.load(f) if not isinstance(imported_keywords, list): QMessageBox.warning(self, "Erreur", "Le fichier doit contenir une liste de mots-clés.") return # Récupérer les mots-clés actuels depuis la liste visuelle current_keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] # Ajouter les nouveaux mots-clés (éviter les doublons) added_count = 0 for keyword in imported_keywords: if keyword not in current_keywords: self.pattern_keywords_list.addItem(keyword) added_count += 1 # Synchroniser avec outlook_handler si disponible if hasattr(self, 'outlook_handler') and self.outlook_handler: # Reconstruire la liste complète all_keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] if not hasattr(self.outlook_handler, 'pattern_keywords'): self.outlook_handler.pattern_keywords = [] self.outlook_handler.pattern_keywords = all_keywords.copy() self.outlook_handler.save_pattern_keywords() else: # Sauvegarder directement dans le fichier self.save_keywords_to_file() self.update_keywords_count_display() QMessageBox.information( self, "Import réussi", f"{added_count} nouveaux mots-clés importés.\nTotal : {self.pattern_keywords_list.count()} mots-clés." ) except Exception as e: QMessageBox.critical(self, "Erreur d'import", f"Impossible d'importer le fichier :\n{e}") def export_pattern_keywords(self): """Exporte les mots-clés actuels vers un fichier JSON.""" from PyQt5.QtWidgets import QFileDialog, QMessageBox # Récupérer les mots-clés depuis la liste visuelle keywords = [self.pattern_keywords_list.item(i).text() for i in range(self.pattern_keywords_list.count())] if not keywords: QMessageBox.information(self, "Aucun mot-clé", "Aucun mot-clé à exporter.") return file_path, _ = QFileDialog.getSaveFileName( self, "Exporter les mots-clés pattern", f"pattern_keywords_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", "Fichiers JSON (*.json);;Tous les fichiers (*)" ) if file_path: try: with open(file_path, 'w', encoding='utf-8') as f: json.dump(keywords, f, ensure_ascii=False, indent=2) QMessageBox.information( self, "Export réussi", f"{len(keywords)} mots-clés exportés vers :\n{file_path}" ) except Exception as e: QMessageBox.critical(self, "Erreur d'export", f"Impossible d'exporter le fichier :\n{e}") def initialize_default_banner(self): """Initialise le bandeau avec le message par défaut""" try: if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if hasattr(browser, 'bottom_banner'): banner = browser.bottom_banner # S'assurer que le bandeau est visible en position bottom if hasattr(browser, 'centralWidget') and hasattr(browser.centralWidget(), 'layout'): layout = browser.centralWidget().layout() # Retirer d'abord le banner de son emplacement actuel layout.removeWidget(banner) # L'ajouter en bas layout.addWidget(banner) # Définir le message par défaut if hasattr(banner, 'setText'): banner.setText("En attente de traitement") elif hasattr(banner, 'show_banner'): banner.show_banner("En attente de traitement") # S'assurer qu'il est visible banner.show() # Mettre à jour le combo pour refléter l'état if hasattr(self, 'banner_position_combo'): self.banner_position_combo.setCurrentIndex(2) # "Désactivé" print("✅ Bandeau initialisé en position bottom avec message par défaut") except Exception as e: print(f"⚠️ Erreur initialisation bandeau: {e}") def on_banner_position_changed(self, index): positions = ["bottom", "top", "disabled"] selected_position = positions[index] # CORRECTION: S'assurer que OutlookHandler est synchronisé if hasattr(self, 'outlook_handler') and self.outlook_handler: if hasattr(self.outlook_handler, 'set_banner_position'): self.outlook_handler.set_banner_position(selected_position) else: # Fallback: définir directement l'attribut self.outlook_handler.banner_position = selected_position print(f"Banner position définie directement: {selected_position}") if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if hasattr(browser, 'bottom_banner'): banner = browser.bottom_banner # Obtenir le layout correctement try: if hasattr(browser, 'centralWidget') and browser.centralWidget(): layout = browser.centralWidget().layout() else: layout = browser.layout() if layout: layout.removeWidget(banner) if selected_position == "disabled": banner.hide() else: banner.show() # Définir le message par défaut quand on change de position if hasattr(banner, 'setText'): banner.setText("En attente de traitement") elif hasattr(banner, 'show_banner'): banner.show_banner("En attente de traitement") if selected_position == "top": layout.insertWidget(0, banner) else: # bottom layout.addWidget(banner) print(f"Banner repositionné en: {selected_position}") except Exception as e: print(f"Erreur repositionnement banner: {e}") def verify_banner_sync(self): """Vérifie que la position du bandeau est bien synchronisée""" try: if hasattr(self, 'outlook_handler') and hasattr(self, 'banner_position_combo'): handler_position = getattr(self.outlook_handler, 'banner_position', 'unknown') combo_index = self.banner_position_combo.currentIndex() positions = ["bottom", "top", "disabled"] combo_position = positions[combo_index] if combo_index < len(positions) else 'unknown' print(f"Sync check - Handler: {handler_position}, Combo: {combo_position}") if handler_position != combo_position: print(f"⚠️ Désynchronisation détectée! Correction...") # Forcer la synchronisation self.outlook_handler.banner_position = combo_position print(f"✅ Handler position corrigée: {combo_position}") except Exception as e: print(f"Erreur vérification sync: {e}") def on_duplicates_detection_changed(self, state): """Gère le changement d'état de la détection des doublons""" enabled = state == Qt.Checked if hasattr(self, 'outlook_handler'): self.outlook_handler.set_duplicates_detection(enabled) # Sauvegarder dans les paramètres settings = QSettings("VotreEntreprise", "AutoLook") duplicates_enabled = settings.value("duplicates_detection_enabled", False, type=bool) self.duplicates_detection_checkbox.setChecked(duplicates_enabled) # Appliquer au handler if hasattr(self, 'outlook_handler'): self.outlook_handler.set_duplicates_detection(duplicates_enabled) # Version améliorée de la méthode handle_end_of_processing def handle_end_of_processing(self): """Gère les actions à exécuter à la fin du traitement des emails.""" # Ajouter un log pour vérifier que la méthode est bien appelée logger.info("Méthode handle_end_of_processing appelée") # Vérifier si l'option de verrouillage est activée if hasattr(self, 'lock_session_checkbox') and self.lock_session_checkbox.isChecked(): logger.info("Option de verrouillage activée, tentative de verrouillage de la session...") try: # Utilisation de la méthode plus fiable avec ctypes import ctypes result = ctypes.windll.user32.LockWorkStation() logger.info(f"Appel à LockWorkStation effectué. Résultat: {result}") # Si la méthode ci-dessus échoue, essayons une alternative if result == 0: # 0 indique un échec logger.warning("Premier essai de verrouillage échoué, tentative alternative...") # Alternative: utiliser la commande rundll32 import os os.system('rundll32.exe user32.dll,LockWorkStation') logger.info("Commande alternative de verrouillage exécutée") except Exception as e: logger.error(f"Erreur lors du verrouillage de la session Windows: {str(e)}") # Tenter une méthode alternative en cas d'erreur try: import os os.system('rundll32.exe user32.dll,LockWorkStation') logger.info("Méthode alternative de verrouillage exécutée après erreur") except Exception as e2: logger.error(f"Échec de la méthode alternative: {str(e2)}") # Vérifier si l'option d'extinction est activée if hasattr(self, 'shutdown_pc_checkbox') and self.shutdown_pc_checkbox.isChecked(): logger.info("Option d'extinction activée, programmation de l'extinction...") try: import os # Extinction dans 30 secondes avec message d'avertissement os.system('shutdown /s /t 30 /c "Extinction automatique programmée par l\'application."') logger.info("Commande d'extinction exécutée") except Exception as e: logger.error(f"Erreur lors de la programmation de l'extinction: {str(e)}") def toggle_multilink_processing(self, enabled): """Active ou désactive le traitement multilink""" # Rendre visible ou masquer le groupe de paramètres de nombre d'emails self.links_frame.setVisible(enabled) # S'assurer que la valeur est initialisée if enabled and hasattr(self, 'links_slider'): current_value = self.links_slider.value() # Important: forcer la mise à jour self.update_links_count(current_value) # Mettre à jour le paramètre dans le gestionnaire Outlook if hasattr(self, 'outlook_handler') and self.outlook_handler: if hasattr(self.outlook_handler, 'set_multilink_processing'): self.outlook_handler.set_multilink_processing(enabled) # Log pour confirmer l'action self.log_action(f"Traitement multilink {'activé' if enabled else 'désactivé'}") def update_performance_impact(self, value): """ Évalue l'impact sur les performances et génère uniquement le tooltip sans afficher le cadre d'impact dans la sidebar """ import psutil # Obtenir l'utilisation actuelle du CPU et de la RAM cpu_usage = psutil.cpu_percent() ram_usage = psutil.virtual_memory().percent # Facteur d'impact basé sur le nombre d'emails (échelle logarithmique) # Plus le nombre d'emails est élevé, plus l'impact est important if value == 1: impact_factor = 1.0 # Impact minimal pour 1 email elif value <= 5: impact_factor = 1.2 # Impact faible pour 2-5 emails elif value <= 10: impact_factor = 1.5 # Impact modéré pour 6-10 emails elif value <= 20: impact_factor = 2.0 # Impact significatif pour 11-20 emails elif value <= 50: impact_factor = 3.0 # Impact important pour 21-50 emails else: impact_factor = 4.0 # Impact critique pour >50 emails # Facteur système basé sur l'utilisation actuelle des ressources # Si le système est déjà chargé, l'impact sera plus important system_load_factor = 1.0 if cpu_usage > 80 or ram_usage > 80: system_load_factor = 1.5 # Système déjà très chargé elif cpu_usage > 60 or ram_usage > 60: system_load_factor = 1.3 # Système moyennement chargé elif cpu_usage > 40 or ram_usage > 40: system_load_factor = 1.1 # Système légèrement chargé # Impact combiné: nombre d'emails × charge actuelle du système combined_factor = impact_factor * system_load_factor # Estimation de l'utilisation après traitement estimated_cpu = min(100, cpu_usage * combined_factor) estimated_ram = min(100, ram_usage * combined_factor) # Niveau de charge estimé pour déterminer le message et la couleur overall_load = max(estimated_cpu, estimated_ram) # Déterminer le message et la couleur en fonction de la charge globale estimée if overall_load < 30: warning_message = f"✅ Valeur {value} optimale. Performances stables." color = "#2ecc71" # Vert elif overall_load < 50: warning_message = f"ℹ️ Valeur {value} bonne. Impact minimal sur les performances." color = "#3498db" # Bleu elif overall_load < 70: warning_message = f"ℹ️ Valeur {value} acceptable. Impact modéré sur les performances." color = "#f39c12" # Orange elif overall_load < 85: warning_message = f"⚠️ Valeur {value} élevée. Performances réduites." color = "#e67e22" # Orange foncé elif overall_load < 95: warning_message = f"⚠️ Valeur {value} très élevée. Performances fortement réduites." color = "#e74c3c" # Rouge else: warning_message = f"⚠️ Valeur {value} critique! Risque de blocage du système." color = "#c0392b" # Rouge foncé # Générer le warning_message pour une utilisation éventuelle ailleurs # mais ne pas créer ou afficher le cadre de performance if hasattr(self, 'performance_warning'): self.performance_warning.setText(warning_message) self.performance_warning.setStyleSheet(f"color: {color}; font-style: italic;") # Mise à jour du slider pour refléter l'impact visuel self.links_slider.setStyleSheet(f""" QSlider::groove:horizontal {{ height: 8px; background: #34495e; margin: 2px 0; border-radius: 4px; }} QSlider::handle:horizontal {{ background: {color}; border: 1px solid #5c5c5c; width: 18px; margin: -2px 0; border-radius: 4px; }} QSlider::sub-page:horizontal {{ background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #2ecc71, stop: 0.4 #f39c12, stop: 0.8 #e74c3c, stop: 1 #c0392b); height: 8px; border-radius: 4px; }} """) # Créer un tooltip détaillé pour le slider tooltip = f"""

Impact sur les performances

CPU actuel: {cpu_usage:.1f}% → estimé: {estimated_cpu:.1f}%
RAM actuelle: {ram_usage:.1f}% → estimée: {estimated_ram:.1f}%

{warning_message}

""" self.links_slider.setToolTip(tooltip) self.links_value_label.setToolTip(tooltip) def create_performance_gauge(self): """ Initialise uniquement le label d'avertissement de performance sans créer de cadre visible. Ce label sera utilisé pour le tooltip et d'autres fonctionnalités. """ # Créer uniquement le label d'avertissement sans l'ajouter à l'interface self.performance_warning = QLabel("") self.performance_warning.setWordWrap(True) self.performance_warning.setStyleSheet("color: #f39c12; font-style: italic;") # Ne pas créer ou ajouter de cadre à l'interface # Les tooltips et les couleurs du slider seront toujours mis à jour def update_performance_gauge(self, value, current_cpu, current_ram, estimated_cpu, estimated_ram): """ Met à jour la jauge de performance avec les valeurs actuelles et estimées """ # Mise à jour des labels CPU et RAM self.cpu_label.setText(f"CPU: {current_cpu:.1f}% → {estimated_cpu:.1f}%") self.ram_label.setText(f"RAM: {current_ram:.1f}% → {estimated_ram:.1f}%") # Mise à jour des barres de progression self.cpu_indicator.setValue(int(estimated_cpu)) self.ram_indicator.setValue(int(estimated_ram)) # Définir la couleur des barres en fonction du niveau d'utilisation def set_progress_color(indicator, value): if value < 50: color = "#2ecc71" # Vert elif value < 75: color = "#f39c12" # Orange else: color = "#e74c3c" # Rouge indicator.setStyleSheet(f""" QProgressBar {{ background-color: #34495e; border-radius: 4px; }} QProgressBar::chunk {{ background-color: {color}; border-radius: 4px; }} """) set_progress_color(self.cpu_indicator, estimated_cpu) set_progress_color(self.ram_indicator, estimated_ram) # Mise à jour du message d'avertissement warning_message = "" if estimated_cpu > 90 or estimated_ram > 90: warning_message = f"⚠️ Valeur {value} critique! Risque de blocage du système." elif estimated_cpu > 80 or estimated_ram > 80: warning_message = f"⚠️ Valeur {value} très élevée. Performances fortement réduites." elif estimated_cpu > 70 or estimated_ram > 70: warning_message = f"⚠️ Valeur {value} élevée. Performances réduites." elif estimated_cpu > 50 or estimated_ram > 50: warning_message = f"ℹ️ Valeur {value} acceptable. Impact modéré sur les performances." else: warning_message = f"✅ Valeur {value} optimale. Performances stables." self.performance_warning.setText(warning_message) # Créer un tooltip détaillé pour le slider tooltip = f"""

Impact sur les performances

CPU: {current_cpu:.1f}% → {estimated_cpu:.1f}%
RAM: {current_ram:.1f}% → {estimated_ram:.1f}%

{warning_message}

""" self.links_slider.setToolTip(tooltip) self.links_value_label.setToolTip(tooltip) def update_links_count_display(self, value): """ Met à jour l'affichage du slider et évalue l'impact sur les performances avec confirmation pour les valeurs élevées, sans affichage des métriques CPU/RAM """ # Stocker la valeur courante pour revenir en arrière si nécessaire old_value = int(self.links_value_label.text()) if self.links_value_label.text().isdigit() else 1 # Vérifier si la valeur est élevée et nécessite une confirmation if value > 20 and old_value <= 20: # Créer une boîte de dialogue personnalisée confirm_dialog = QMessageBox(self) confirm_dialog.setWindowTitle("Attention - Risque de performances") confirm_dialog.setIcon(QMessageBox.Warning) # Message personnalisé simplifié message = f"""

Vous avez sélectionné {value} emails simultanés.

Cette valeur peut fortement impacter les performances du système et ralentir votre ordinateur.

Le traitement simultané de nombreux emails peut :

Une valeur entre 5 et 10 est généralement recommandée.

Souhaitez-vous vraiment continuer avec {value} emails simultanés ?

""" confirm_dialog.setText(message) # Boutons personnalisés confirm_dialog.addButton("Continuer", QMessageBox.YesRole) opt_button = confirm_dialog.addButton(f"Utiliser valeur optimale (10)", QMessageBox.NoRole) cancel_button = confirm_dialog.addButton("Annuler", QMessageBox.RejectRole) # Afficher la boîte de dialogue et récupérer la réponse confirm_dialog.exec_() clicked_button = confirm_dialog.clickedButton() if clicked_button == cancel_button: # Annuler et revenir à l'ancienne valeur self.links_slider.blockSignals(True) self.links_slider.setValue(old_value) self.links_slider.blockSignals(False) # Mettre à jour l'affichage avec l'ancienne valeur self.links_value_label.setText(str(old_value)) # Mettre à jour le backend avec l'ancienne valeur if hasattr(self, 'outlook_widget') and self.outlook_widget: self.outlook_widget.update_links_count(old_value) # Mettre à jour l'indicateur de performance self.update_performance_impact(old_value) return elif clicked_button == opt_button: # Utiliser la valeur optimale recommandée optimal_value = 10 self.links_slider.blockSignals(True) self.links_slider.setValue(optimal_value) self.links_slider.blockSignals(False) # Mettre à jour l'affichage avec la valeur optimale self.links_value_label.setText(str(optimal_value)) # Mettre à jour le backend avec la valeur optimale if hasattr(self, 'outlook_widget') and self.outlook_widget: self.outlook_widget.update_links_count(optimal_value) # Mettre à jour l'indicateur de performance self.update_performance_impact(optimal_value) return # Continuer avec la mise à jour normale (si pas d'annulation) self.links_value_label.setText(str(value)) # Mettre à jour le backend (outlook_widget) if hasattr(self, 'outlook_widget') and self.outlook_widget: self.outlook_widget.update_links_count(value) # Calculer l'impact sur les performances self.update_performance_impact(value) def system_health_timer_setup(self): """ Configure un timer pour surveiller et mettre à jour périodiquement l'état du système """ if not hasattr(self, 'system_health_timer'): self.system_health_timer = QTimer(self) self.system_health_timer.timeout.connect(self.update_system_health) self.system_health_timer.start(5000) # Mise à jour toutes les 5 secondes def update_system_health(self): """ Met à jour les indicateurs de santé du système en temps réel """ if hasattr(self, 'links_value_label') and self.links_value_label.text().isdigit(): current_value = int(self.links_value_label.text()) self.update_performance_impact(current_value) def toggle_expired_check(self, checked): """Enregistre la préférence pour vérifier les profils périmés au démarrage""" try: settings = QSettings("Snoown2k", "MyApp") settings.setValue("check_expired_profiles", checked) settings.sync() # Forcer la synchronisation immédiate logger.info(f"Vérification des profils périmés au démarrage {'activée' if checked else 'désactivée'}") except Exception as e: logger.error(f"Erreur lors de l'enregistrement de la préférence: {str(e)}") def load_expired_check_setting(self): """Charge la préférence pour vérifier les profils périmés au démarrage""" try: settings = QSettings("Snoown2k", "MyApp") checked = settings.value("check_expired_profiles", False, type=bool) # S'assurer que la case existe avant de modifier son état if hasattr(self, 'show_empty_window_checkbox'): self.show_empty_window_checkbox.setChecked(checked) logger.info(f"Paramètre de vérification des profils chargé: {checked}") return checked except Exception as e: logger.error(f"Erreur lors du chargement de la préférence: {str(e)}") return False def check_expired_profiles_at_startup(self): """Vérifie les Emails expirés au démarrage si l'option est activée""" try: settings = QSettings("Snoown2k", "MyApp") check_enabled = settings.value("check_expired_profiles", False, type=bool) logger.info(f"Vérification du paramètre au démarrage: {check_enabled}") if check_enabled: logger.info("Vérification des Emails expirés au démarrage...") expired_profiles = self.check_expired_profiles() if expired_profiles: logger.info(f"Trouvé {len(expired_profiles)} Emails expirés, affichage du dialogue") # Utiliser QTimer.singleShot pour éviter les problèmes de timing QTimer.singleShot(100, lambda: self.show_expired_profiles_dialog()) else: logger.info("Aucun Email expiré trouvé") except Exception as e: logger.error(f"Erreur lors de la vérification des Emails expirés au démarrage: {str(e)}") # Afficher l'erreur de manière visible pour comprendre le problème print(f"ERREUR VÉRIFICATION PROFILS: {str(e)}") def check_expired_profiles(self): """Vérifie les profils avec des dates de validité expirées""" try: expired_profiles = [] profiles_dir = r"C:\AUTOLOOK\profiles" if not os.path.exists(profiles_dir): logger.warning(f"Le dossier profiles n'existe pas: {profiles_dir}") return [] # Date actuelle pour comparaison current_date = datetime.now().date() logger.info(f"Vérification des Emails expirés - date actuelle: {current_date}") # Parcourir tous les profils for filename in os.listdir(profiles_dir): if not filename.endswith('.json') or filename in ['.last_used', '.favorites']: continue profile_path = os.path.join(profiles_dir, filename) profile_name = os.path.splitext(filename)[0] try: with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) # Vérifier si le profil a des critères de validité validity_settings = profile_data.get('settings', {}).get('validity', {}) if validity_settings.get('enabled', False) and validity_settings.get('days') is not None: # Calculer la date de validité try: # Utiliser la date de dernière modification comme référence last_mod_time = os.path.getmtime(profile_path) last_mod_date = datetime.fromtimestamp(last_mod_time).date() validity_days = int(validity_settings['days']) expiry_date = last_mod_date + timedelta(days=validity_days) logger.info( f"Profil: {profile_name}, Modifié: {last_mod_date}, Expire: {expiry_date}, Validité: {validity_days} jours") # Vérifier si la date est passée if current_date > expiry_date: days_expired = (current_date - expiry_date).days expired_profiles.append({ 'name': profile_name, 'expiry_date': expiry_date.strftime('%d/%m/%Y'), 'days_expired': days_expired, 'validity_days': validity_days }) logger.info(f"Email expiré trouvé: {profile_name}, expiré depuis {days_expired} jours") except Exception as exp_err: logger.error( f"Erreur lors du calcul de date d'expiration pour {profile_name}: {str(exp_err)}") except Exception as e: logger.error(f"Erreur lors de la lecture du profil {profile_name}: {str(e)}") continue return expired_profiles except Exception as e: logger.error(f"Erreur lors de la vérification des Emails expirés: {str(e)}") return [] def show_expired_profiles_dialog(self): """Affiche le dialogue des Emails expirés""" try: expired_profiles = self.check_expired_profiles() if expired_profiles: dialog = ExpiredProfilesDialog(self, expired_profiles) dialog.exec_() else: logger.info("Aucun Email expiré trouvé") except Exception as e: logger.error(f"Erreur lors de l'affichage du dialogue des Emails expirés: {str(e)}") def show_optins(self): """Affiche la fenêtre de gestion des Optins""" try: dialog = OptinsDialog(self) dialog.exec_() except Exception as e: logger.error(f"Erreur lors de l'ouverture de la fenêtre Optins : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture de la fenêtre Optins : {str(e)}") def show_scheduler(self): """Affiche le planificateur de tâches""" try: scheduler_dialog = TaskSchedulerDialog(self) # Configuration du style scheduler_dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; } QCalendarWidget { background-color: #03699c; } QComboBox { background-color: #03699c; color: #f2f1f1; padding: 5px; border-radius: 4px; } QTimeEdit { background-color: #03699c; color: #f2f1f1; padding: 5px; border-radius: 4px; } """) # Connexion au gestionnaire de tâches if hasattr(self, 'task_manager'): scheduler_dialog.schedules = self.task_manager.get_schedules() if hasattr(self.task_manager, 'get_schedules') else [] scheduler_dialog.exec_() # Rafraîchir l'affichage après la fermeture if hasattr(self, 'task_manager'): self.task_manager.check_scheduled_tasks() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture du planificateur : {str(e)}") logger.error(f"Erreur lors de l'ouverture du planificateur : {str(e)}") def update_between_pages_time(self, min_value, max_value): """Met à jour l'affichage et les paramètres du temps entre les pages""" self.between_pages_value_label.setText(f"{min_value}-{max_value}") self.between_pages_time_label.setText(f"Temps de pause entre pages: {min_value}-{max_value} secondes") if self.outlook_handler: self.outlook_handler.set_between_pages_time_range(min_value, max_value) def load_profiles_list(self): """Charge la liste des profils disponibles""" try: profiles_dir = r"C:\AUTOLOOK\profiles" logger.info(f"Chargement des profils depuis {profiles_dir}") # Liste des fichiers système à exclure SYSTEM_FILES = {'completed_tasks.json', 'schedules.json', '.last_used', '.favorites'} # S'assurer que le dossier existe if not os.path.exists(profiles_dir): logger.warning(f"Création du dossier profiles : {profiles_dir}") os.makedirs(profiles_dir, exist_ok=True) # Vider la liste actuelle if hasattr(self, 'profile_list'): self.profile_list.clear() logger.debug("Liste des profils vidée") # Récupérer tous les fichiers .json sauf les fichiers système profile_files = [f for f in os.listdir(profiles_dir) if f.endswith('.json') and f not in SYSTEM_FILES] logger.info(f"Nombre de profils trouvés : {len(profile_files)}") # Liste pour stocker les profils triés profiles = [] # Charger les profils for filename in profile_files: try: profile_path = os.path.join(profiles_dir, filename) profile_name = os.path.splitext(filename)[0] with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) # Vérifier si c'est un fichier de profil valide if not isinstance(profile_data, dict): logger.warning(f"Fichier ignoré car format invalide : {filename}") continue # Vérifier si le profil est un favori is_favorite = profile_data.get('favorite', False) # Vérifier si le profil a une date de validité définie has_validity = False validity_settings = profile_data.get('settings', {}).get('validity', {}) if validity_settings.get('enabled', False) and validity_settings.get('days') is not None: has_validity = True # Créer le nom d'affichage avec les marqueurs display_name = profile_name if is_favorite: display_name = f"⭐ {display_name}" if has_validity: display_name = f"{display_name} ⏰" profiles.append({ 'name': profile_name, 'is_favorite': is_favorite, 'has_validity': has_validity, 'display_name': display_name }) logger.debug(f"Profil chargé : {profile_name} (favori: {is_favorite})") except Exception as e: logger.error(f"Erreur lors du chargement du profil {filename}: {e}") continue # Trier les profils (favoris en premier, puis ordre alphabétique) profiles.sort(key=lambda x: (-x['is_favorite'], x['name'].lower())) # Ajouter les profils triés à la liste for profile in profiles: self.profile_list.addItem(profile['display_name']) # Mettre à jour les statistiques total_profiles = len(profiles) if hasattr(self, 'profile_stats_label'): last_profile = "Jamais" last_profile_file = os.path.join(profiles_dir, ".last_used") if os.path.exists(last_profile_file): try: with open(last_profile_file, 'r') as f: last_profile = f.read().strip() except Exception: pass self.profile_stats_label.setText(f"{total_profiles} profils | Dernier chargé: {last_profile}") logger.info(f"Chargement des profils terminé. {total_profiles} profils chargés.") # Appliquer les couleurs spécifiques après avoir chargé tous les profils QTimer.singleShot(100, self.apply_profile_colors) return True except Exception as e: logger.error(f"Erreur critique lors du chargement des profils : {e}") if hasattr(self, 'profile_stats_label'): self.profile_stats_label.setText("Erreur de chargement") return False def verify_profile_list_widget(self): """Vérifie que le widget profile_list est correctement initialisé""" try: if not hasattr(self, 'profile_list'): logger.error("Le widget profile_list n'existe pas !") return False logger.info(f"Type de profile_list : {type(self.profile_list)}") logger.info(f"Nombre d'items dans profile_list : {self.profile_list.count()}") return True except Exception as e: logger.error(f"Erreur lors de la vérification du widget profile_list : {e}") return False def update_profile_stats(self, loaded_profile=None): """Met à jour les statistiques des profils""" try: profiles_dir = r"C:\autolook\profiles" profile_count = len([f for f in os.listdir(profiles_dir) if f.endswith('.json')]) last_loaded = loaded_profile if loaded_profile else "Jamais" if hasattr(self, 'profile_stats_label'): self.profile_stats_label.setText(f"{profile_count} profils | Dernier chargé: {last_loaded}") logger.info(f"Statistiques mises à jour: {profile_count} profils, dernier chargé: {last_loaded}") except Exception as e: logger.error(f"Erreur lors de la mise à jour des statistiques : {str(e)}") def try_load_last_profile(self): """Essaie de charger le dernier profil utilisé""" try: last_profile_file = os.path.join(r"C:\autolook\profiles", ".last_used") if os.path.exists(last_profile_file): with open(last_profile_file, 'r') as f: last_profile = f.read().strip() # Vérifier si le profil existe toujours if os.path.exists(os.path.join(r"C:\autolook\profiles", f"{last_profile}.json")): # Sélectionner le profil dans la liste index = self.profile_list.findText(last_profile) if index >= 0: self.profile_list.setCurrentIndex(index) # Charger le profil self.load_profile() logger.info(f"Dernier profil '{last_profile}' chargé automatiquement") except Exception as e: logger.error(f"Erreur lors du chargement du dernier profil : {str(e)}") def sync_mail_count(self, count): """Synchronise le nombre de mails dans tous les composants pertinents""" try: if hasattr(self, 'mail_count_spinbox'): self.mail_count_spinbox.setValue(count) if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'max_emails_entry'): self.outlook_widget.max_emails_entry.setText(str(count)) # Mise à jour du profil courant si existant current_profile = self.profile_list.currentText() if current_profile: profile_path = os.path.join(r"C:\autolook\profiles", f"{current_profile}.json") if os.path.exists(profile_path): with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) profile_data['settings']['mail_count'] = count profile_data['outlook_settings']['max_emails'] = str(count) with open(profile_path, 'w', encoding='utf-8') as f: json.dump(profile_data, f, indent=4) logger.info(f"Nombre de mails synchronisé: {count}") except Exception as e: logger.error(f"Erreur lors de la synchronisation du nombre de mails: {str(e)}") def _select_folder_after_delay(self, outlook_settings): """Sélectionne le dossier après un délai""" try: if 'folder' in outlook_settings and hasattr(self.outlook_widget, 'folder_listbox'): folder_name = outlook_settings['folder'] logger.info(f"Tentative de sélection du dossier: {folder_name}") # Sélectionner le dossier for i in range(self.outlook_widget.folder_listbox.count()): item = self.outlook_widget.folder_listbox.item(i) if item.text().strip() == folder_name.strip(): self.outlook_widget.folder_listbox.setCurrentItem(item) logger.info(f"Dossier sélectionné: {folder_name}") break except Exception as e: logger.error(f"Erreur lors de la sélection du dossier : {str(e)}") def _select_mailbox(self, mailbox_name): """Sélectionne une boîte mail dans la liste""" if not mailbox_name: return logger.info(f"Tentative de sélection de la boîte mail: '{mailbox_name}'") # Parcourir toutes les boîtes mail for i in range(self.mailbox_listbox.count()): item = self.mailbox_listbox.item(i) current_mailbox = item.text().strip() if current_mailbox == mailbox_name: logger.info(f"Boîte mail '{mailbox_name}' trouvée") self.mailbox_listbox.setCurrentRow(i) # Forcer le rafraîchissement des dossiers self.update_folders(mailbox_name) return True logger.warning(f"Boîte mail '{mailbox_name}' non trouvée dans la liste") return False def _select_folder(self, folder_name): """Sélectionne un dossier dans la liste""" if not folder_name: return logger.info(f"Tentative de sélection du dossier: '{folder_name}'") # Parcourir tous les dossiers for i in range(self.folder_listbox.count()): item = self.folder_listbox.item(i) current_folder = item.text().strip() if current_folder == folder_name: logger.info(f"Dossier '{folder_name}' trouvé") self.folder_listbox.setCurrentRow(i) return True logger.warning(f"Dossier '{folder_name}' non trouvé dans la liste") return False def _load_general_settings(self, settings): """Charge les paramètres généraux""" try: if 'pause_time_range' in settings: pause_range = settings['pause_time_range'] self.pause_slider.setValues(pause_range['min'], pause_range['max']) if 'between_pages_time_range' in settings: between_range = settings['between_pages_time_range'] self.between_pages_slider.setValues(between_range['min'], between_range['max']) if 'links_count' in settings: self.links_slider.setValue(settings['links_count']) if 'font_size' in settings: self.font_size = settings['font_size'] self.apply_font_size() if 'report_enabled' in settings: self.report_toggle.setChecked(settings['report_enabled']) except Exception as e: logger.error(f"Erreur lors du chargement des paramètres généraux : {str(e)}") def _load_outlook_settings(self, settings): """Charge les paramètres Outlook""" try: if hasattr(self, 'sender_entry'): self.sender_entry.setText(settings.get('sender', '')) if hasattr(self, 'subject_entry'): self.subject_entry.setText(settings.get('subject_keywords', '')) if hasattr(self, 'link_entry'): self.link_entry.setText(settings.get('link_keywords', '')) if hasattr(self, 'max_emails_entry'): self.max_emails_entry.setText(str(settings.get('max_emails', ''))) if hasattr(self, 'reward_entry'): self.reward_entry.setText(str(settings.get('reward', ''))) except Exception as e: logger.error(f"Erreur lors du chargement des paramètres Outlook : {str(e)}") def update_folders(self, mailbox): """Met à jour la liste des dossiers pour une boîte mail donnée""" try: logger.info(f"Mise à jour des dossiers pour la boîte mail: {mailbox}") folders = self.outlook_handler.get_folders(mailbox) self.folder_listbox.clear() for folder_name, folder_id in folders: item = QListWidgetItem(folder_name.strip()) item.setData(Qt.UserRole, folder_id) self.folder_listbox.addItem(item) logger.info(f"{len(folders)} dossiers chargés") except Exception as e: logger.error(f"Erreur lors de la mise à jour des dossiers : {str(e)}") def _delayed_load_folder(self, outlook_settings): """Charge le dossier Outlook après un délai""" try: if 'folder' in outlook_settings and hasattr(self.outlook_widget, 'folder_listbox'): folder = outlook_settings['folder'].strip() folder_listbox = self.outlook_widget.folder_listbox folder_found = False # Parcourir les éléments de la liste des dossiers for i in range(folder_listbox.count()): if folder_listbox.item(i).text().strip() == folder: folder_listbox.setCurrentRow(i) folder_found = True # Si on a un gestionnaire de sélection de dossier, l'appeler if hasattr(self.outlook_widget, 'on_folder_selected'): self.outlook_widget.on_folder_selected() break if folder_found: logger.info(f"Dossier '{folder}' sélectionné") else: logger.warning(f"Dossier '{folder}' non trouvé") except Exception as e: logger.error(f"Erreur lors du chargement différé du dossier : {str(e)}") def _load_outlook_folder(self, outlook_settings): """Charge le dossier Outlook""" try: if 'folder' in outlook_settings and hasattr(self.outlook_widget, 'folder_listbox'): folder = outlook_settings['folder'].strip() for i in range(self.outlook_widget.folder_listbox.count()): item = self.outlook_widget.folder_listbox.item(i) if item.text().strip() == folder: self.outlook_widget.folder_listbox.setCurrentItem(item) if hasattr(self.outlook_widget, 'on_folder_selected'): self.outlook_widget.on_folder_selected() break except Exception as e: logger.error(f"Erreur lors du chargement du dossier Outlook : {str(e)}") def _delayed_folder_selection(self, outlook_settings): """Effectue la sélection du dossier après un délai""" try: if 'folder' in outlook_settings and hasattr(self.outlook_widget, 'folder_listbox'): folder = outlook_settings['folder'] items = self.outlook_widget.folder_listbox.findItems(folder, Qt.MatchExactly) if items: self.outlook_widget.folder_listbox.setCurrentItem(items[0]) if hasattr(self.outlook_widget, 'on_folder_selected'): self.outlook_widget.on_folder_selected() except Exception as e: logger.error(f"Erreur lors de la sélection différée du dossier : {str(e)}") def load_profile(self, from_scheduler=False): """Charge un profil""" self.is_loading_profile = True try: # Récupérer le nom du profil en retirant les marqueurs current = self.profile_list.currentText().replace("⭐ ", "").replace(" ⏰", "") if not current: self.is_loading_profile = False return profile_name = current profile_path = os.path.join(r"C:\autolook\profiles", f"{profile_name}.json") if not os.path.exists(profile_path): QMessageBox.warning(self, "Erreur", f"Profil introuvable: {profile_path}") self.is_loading_profile = False return with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) # Gestion du mail_count mail_count = profile_data.get('settings', {}).get('mail_count', int(profile_data.get('outlook_settings', {}).get( 'max_emails', 1))) if hasattr(self, 'mail_count_spinbox'): self.mail_count_spinbox.setValue(mail_count) if hasattr(self.outlook_widget, 'max_emails_entry'): self.outlook_widget.max_emails_entry.setText(str(mail_count)) # Réinitialiser les champs de validité if hasattr(self.outlook_widget, 'validity_checkbox'): self.outlook_widget.validity_checkbox.setChecked(False) if hasattr(self.outlook_widget, 'validity_entry'): self.outlook_widget.validity_entry.clear() self.outlook_widget.validity_entry.setVisible(False) # Paramètres de validité validity = profile_data.get('settings', {}).get('validity', {}) if isinstance(validity, dict): enabled = validity.get('enabled', False) if hasattr(self.outlook_widget, 'validity_checkbox'): self.outlook_widget.validity_checkbox.setChecked(enabled) if hasattr(self.outlook_widget, 'validity_entry'): self.outlook_widget.validity_entry.setVisible(enabled) self.outlook_widget.validity_entry.setText( str(validity.get('days', ''))) if enabled else self.outlook_widget.validity_entry.clear() # Chargement des autres paramètres (pause, cycle, sliders) settings = profile_data.get('settings', {}) if 'pause_time_range' in settings: r = settings['pause_time_range'] self.pause_slider.setValues(r['min'], r['max']) # Forcer la mise à jour immédiate self.update_pause_values(r['min'], r['max']) if 'between_pages_time_range' in settings: r = settings['between_pages_time_range'] self.between_pages_slider.setValues(r['min'], r['max']) # Forcer la mise à jour immédiate self.update_between_pages_values(r['min'], r['max']) if 'links_count' in settings: self.links_slider.setValue(settings['links_count']) # Traitement cycle_processing cycle = settings.get('cycle_processing', {}) enabled = cycle.get('enabled', False) self.cycle_processing_checkbox.setChecked(enabled) self.cycle_group.setVisible(enabled) if 'emails_per_cycle_range' in cycle: r = cycle['emails_per_cycle_range'] self.emails_per_cycle_slider.setValues(r['min'], r['max']) self.emails_per_cycle_min_input.setText(str(r['min'])) self.emails_per_cycle_max_input.setText(str(r['max'])) elif 'emails_per_cycle' in cycle: v = cycle['emails_per_cycle'] self.emails_per_cycle_slider.setValues(v, v) self.emails_per_cycle_min_input.setText(str(v)) self.emails_per_cycle_max_input.setText(str(v)) if 'pause_between_cycles_range' in cycle: r = cycle['pause_between_cycles_range'] self.pause_between_cycles_slider.setValues(r['min'], r['max']) self.pause_between_cycles_min_input.setText(str(r['min'])) self.pause_between_cycles_max_input.setText(str(r['max'])) elif 'pause_between_cycles' in cycle: v = cycle['pause_between_cycles'] self.pause_between_cycles_slider.setValues(v, v) self.pause_between_cycles_min_input.setText(str(v)) self.pause_between_cycles_max_input.setText(str(v)) # Sélection de la boîte mail outlook = profile_data.get('outlook_settings', {}) if 'mailbox' in outlook: mbox = outlook['mailbox'] for i in range(self.outlook_widget.mailbox_listbox.count()): item = self.outlook_widget.mailbox_listbox.item(i) if item.text().strip() == mbox.strip(): self.outlook_widget.mailbox_listbox.setCurrentItem(item) QApplication.processEvents() if hasattr(self.outlook_widget, 'on_mailbox_selected'): self.outlook_widget.on_mailbox_selected(item) break # Mise à jour des champs texte if hasattr(self.outlook_widget, 'sender_entry'): self.outlook_widget.sender_entry.setText(outlook.get('sender', '')) if hasattr(self.outlook_widget, 'subject_entry'): self.outlook_widget.subject_entry.setText(outlook.get('subject_keywords', '')) if hasattr(self.outlook_widget, 'link_entry'): self.outlook_widget.link_entry.setText(outlook.get('link_keywords', '')) # Paramètres de récompense if hasattr(self.outlook_widget, 'reward_entry'): reward = outlook.get('reward', '') self.outlook_widget.reward_entry.setText(str(reward)) if reward: self.outlook_widget.validate_reward() self.outlook_widget.reward_entry.setEnabled(False) self.outlook_widget.validate_reward_button.setEnabled(False) self.outlook_widget.reset_reward_button.setEnabled(True) # Sélection différée du dossier QTimer.singleShot(1000, lambda: self._select_folder_after_delay(outlook)) # Mise à jour des statistiques self.update_profile_stats(profile_name) # Évite le démarrage du timer if hasattr(self.outlook_widget, 'mini_browser'): self.outlook_widget.mini_browser.update_page_time("Display Xs", start_timer=False) self.is_loading_profile = False QTimer.singleShot(500, self.force_sync_all_panels) return True except Exception as e: logger.error(f"Erreur lors du chargement du profil : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du chargement du profil : {str(e)}") self.is_loading_profile = False return False def modify_profile(self): """Modifie un profil existant""" try: # Vérification du profil sélectionné et retrait de l'étoile si présente current = self.profile_list.currentText().replace("⭐ ", "").replace(" ⏰", "") if not current: QMessageBox.warning(self, "Erreur", "Veuillez sélectionner un profil à modifier") return # Vérifier si le fichier existe profile_path = os.path.join(r"C:\autolook\profiles", f"{current}.json") if not os.path.exists(profile_path): QMessageBox.warning(self, "Erreur", f"Le profil '{current}' n'existe pas") return # Lecture du profil existant with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) # Création de la boîte de dialogue de modification dialog = QDialog(self) dialog.setWindowTitle(f"Modifier le profil: {current}") dialog.setMinimumWidth(500) # Augmenté pour les plages dialog.setMinimumHeight(550) # Augmenté pour les contrôles supplémentaires dialog.setStyleSheet(""" QDialog, QWidget { background-color: #1a2a3a; } QLabel { color: white; } QGroupBox { color: white; border: 1px solid #34495e; border-radius: 4px; margin-top: 10px; background-color: #1a2a3a; } QGroupBox::title { color: white; } QLineEdit, QSpinBox, QComboBox { background-color: #03699c; color: white; border-radius: 4px; padding: 5px; font-size: 12px; margin: 2px 0; min-height: 25px; } QComboBox { color: #65ddfb; /* Texte en bleu ciel lorsqu'il n'est pas sélectionné */ } QComboBox QAbstractItemView { background-color: #034567; color: #65ddfb; /* Couleur des éléments dans la liste déroulante */ selection-background-color: #0078D7; /* Couleur de fond lors de la sélection */ selection-color: white; /* Couleur du texte lors de la sélection */ } QCheckBox { color: white; background-color: #1a2a3a; } QCheckBox::indicator { width: 15px; height: 15px; background-color: #03699c; } QCheckBox::indicator:checked { background-color: #2980b9; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } QScrollArea { border: none; background-color: #1a2a3a; } QScrollBar:vertical { background-color: #2c3e50; width: 12px; margin: 0px; } QScrollBar::handle:vertical { background-color: #008CBA; min-height: 20px; border-radius: 6px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } """) # Layout principal main_layout = QVBoxLayout(dialog) # Création de la ScrollArea scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Widget conteneur pour le scroll scroll_content = QWidget() layout = QVBoxLayout(scroll_content) layout.setSpacing(10) # Section nom du profil name_group = QGroupBox("") name_layout = QHBoxLayout() name_label = QLabel("Nouveau nom:") name_edit = QLineEdit(current) name_layout.addWidget(name_label) name_layout.addWidget(name_edit) name_group.setLayout(name_layout) layout.addWidget(name_group) # Section paramètres généraux settings_group = QGroupBox("") settings_layout = QVBoxLayout() settings_layout.setSpacing(8) # Temps de pause pause_label = QLabel("Temps d'affichage (secondes):") pause_min = QSpinBox() pause_max = QSpinBox() pause_min.setRange(5, 30) pause_max.setRange(5, 30) if 'settings' in profile_data and 'pause_time_range' in profile_data['settings']: pause_min.setValue(profile_data['settings']['pause_time_range']['min']) pause_max.setValue(profile_data['settings']['pause_time_range']['max']) pause_layout = QHBoxLayout() pause_layout.addWidget(pause_label) pause_layout.addWidget(pause_min) pause_layout.addWidget(QLabel("-")) pause_layout.addWidget(pause_max) settings_layout.addLayout(pause_layout) # Temps entre pages between_pages_label = QLabel("Temps entre pages (secondes):") between_pages_min = QSpinBox() between_pages_max = QSpinBox() between_pages_min.setRange(3, 20) between_pages_max.setRange(3, 20) if 'settings' in profile_data and 'between_pages_time_range' in profile_data['settings']: between_pages_min.setValue(profile_data['settings']['between_pages_time_range']['min']) between_pages_max.setValue(profile_data['settings']['between_pages_time_range']['max']) between_pages_layout = QHBoxLayout() between_pages_layout.addWidget(between_pages_label) between_pages_layout.addWidget(between_pages_min) between_pages_layout.addWidget(QLabel("-")) between_pages_layout.addWidget(between_pages_max) settings_layout.addLayout(between_pages_layout) # Zoom level pour le mini navigateur zoom_label = QLabel("Niveau de zoom mini browser (%):") zoom_browser_spinbox = QSpinBox() zoom_browser_spinbox.setRange(20, 200) zoom_browser_spinbox.setSingleStep(10) # Charger la valeur actuelle du zoom depuis le profil if 'settings' in profile_data and 'browser_zoom' in profile_data['settings']: zoom_browser_spinbox.setValue(int(profile_data['settings']['browser_zoom'] * 100)) else: zoom_browser_spinbox.setValue(40) # 40% par défaut zoom_layout = QHBoxLayout() zoom_layout.addWidget(zoom_label) zoom_layout.addWidget(zoom_browser_spinbox) settings_layout.addLayout(zoom_layout) # Nombre de mails mail_label = QLabel("Nombre de mails:") mail_count = QSpinBox() mail_count.setRange(1, 999) if 'settings' in profile_data and 'mail_count' in profile_data['settings']: mail_count.setValue(profile_data['settings']['mail_count']) mail_layout = QHBoxLayout() mail_layout.addWidget(mail_label) mail_layout.addWidget(mail_count) settings_layout.addLayout(mail_layout) # Date de validité validity_container = QWidget() validity_layout = QHBoxLayout(validity_container) validity_layout.setContentsMargins(0, 0, 0, 0) validity_label = QLabel("Date de validité:") validity_checkbox = QCheckBox() validity_entry = QLineEdit() validity_entry.setAlignment(Qt.AlignCenter) validity_entry.setFixedWidth(60) validity_entry.setMaxLength(3) # Charger les paramètres de validité existants if 'settings' in profile_data and 'validity' in profile_data['settings']: validity_settings = profile_data['settings']['validity'] validity_checkbox.setChecked(validity_settings.get('enabled', False)) if validity_settings.get('enabled'): validity_entry.setText(str(validity_settings.get('days', ''))) validity_entry.setVisible(True) else: validity_entry.setVisible(False) else: validity_entry.setVisible(False) validity_layout.addWidget(validity_label) validity_layout.addWidget(validity_checkbox) validity_layout.addWidget(validity_entry) settings_layout.addWidget(validity_container) validity_checkbox.stateChanged.connect( lambda state: validity_entry.setVisible(state == Qt.Checked) ) settings_group.setLayout(settings_layout) layout.addWidget(settings_group) # Ajout des paramètres de cycle cycle_group = QGroupBox("") cycle_layout = QVBoxLayout() cycle_layout.setSpacing(8) # Case à cocher pour activer/désactiver le traitement par cycle cycle_enabled_checkbox = QCheckBox("Activer le traitement par cycle") # Charger l'état du traitement par cycle et les valeurs cycle_enabled = False emails_min_value = 5 # valeur par défaut emails_max_value = 15 # valeur par défaut pause_min_value = 60 # valeur par défaut pause_max_value = 180 # valeur par défaut if ('settings' in profile_data and 'cycle_processing' in profile_data['settings']): cycle_settings = profile_data['settings']['cycle_processing'] cycle_enabled = cycle_settings.get('enabled', False) # Récupérer les valeurs de plage ou valeur fixe pour emails par cycle if 'emails_per_cycle_range' in cycle_settings: emails_min_value = int(cycle_settings['emails_per_cycle_range'].get('min', 5)) emails_max_value = int(cycle_settings['emails_per_cycle_range'].get('max', 15)) elif 'emails_per_cycle' in cycle_settings: email_value = int(cycle_settings.get('emails_per_cycle', 5)) emails_min_value = email_value emails_max_value = email_value # Récupérer les valeurs de plage ou valeur fixe pour pause entre cycles if 'pause_between_cycles_range' in cycle_settings: pause_min_value = int(cycle_settings['pause_between_cycles_range'].get('min', 60)) pause_max_value = int(cycle_settings['pause_between_cycles_range'].get('max', 180)) elif 'pause_between_cycles' in cycle_settings: pause_value = int(cycle_settings.get('pause_between_cycles', 60)) pause_min_value = pause_value pause_max_value = pause_value cycle_enabled_checkbox.setChecked(cycle_enabled) cycle_layout.addWidget(cycle_enabled_checkbox) # Paramètres de cycle cycle_params_container = QWidget() cycle_params_layout = QVBoxLayout(cycle_params_container) cycle_params_layout.setContentsMargins(0, 0, 0, 0) # Emails par cycle (plage) emails_per_cycle_layout = QHBoxLayout() emails_per_cycle_label = QLabel("Emails par cycle (min-max) :") emails_per_cycle_min_spinbox = QSpinBox() emails_per_cycle_min_spinbox.setRange(1, 100) emails_per_cycle_min_spinbox.setValue(emails_min_value) emails_per_cycle_max_spinbox = QSpinBox() emails_per_cycle_max_spinbox.setRange(1, 100) emails_per_cycle_max_spinbox.setValue(emails_max_value) emails_per_cycle_layout.addWidget(emails_per_cycle_label) emails_per_cycle_layout.addWidget(emails_per_cycle_min_spinbox) emails_per_cycle_layout.addWidget(QLabel("-")) emails_per_cycle_layout.addWidget(emails_per_cycle_max_spinbox) cycle_params_layout.addLayout(emails_per_cycle_layout) # Pause entre cycles (plage) pause_between_cycles_layout = QHBoxLayout() pause_between_cycles_label = QLabel("Pause entre cycles (secondes, min-max) :") pause_between_cycles_min_spinbox = QSpinBox() pause_between_cycles_min_spinbox.setRange(5, 600) pause_between_cycles_min_spinbox.setValue(pause_min_value) pause_between_cycles_max_spinbox = QSpinBox() pause_between_cycles_max_spinbox.setRange(5, 600) pause_between_cycles_max_spinbox.setValue(pause_max_value) pause_between_cycles_layout.addWidget(pause_between_cycles_label) pause_between_cycles_layout.addWidget(pause_between_cycles_min_spinbox) pause_between_cycles_layout.addWidget(QLabel("-")) pause_between_cycles_layout.addWidget(pause_between_cycles_max_spinbox) cycle_params_layout.addLayout(pause_between_cycles_layout) cycle_layout.addWidget(cycle_params_container) cycle_group.setLayout(cycle_layout) layout.addWidget(cycle_group) # Afficher/masquer les paramètres de cycle en fonction de l'état de la case à cocher cycle_params_container.setVisible(cycle_enabled) cycle_enabled_checkbox.stateChanged.connect( lambda state: cycle_params_container.setVisible(state == Qt.Checked) ) # Section paramètres Outlook if 'outlook_settings' in profile_data: outlook_group = QGroupBox("") outlook_layout = QVBoxLayout() outlook_layout.setSpacing(8) # Boîte mail et dossier mailbox_label = QLabel("Boîte mail:") mailbox_combo = QComboBox() folder_label = QLabel("Dossier:") folder_combo = QComboBox() outlook_layout.addWidget(mailbox_label) outlook_layout.addWidget(mailbox_combo) outlook_layout.addWidget(folder_label) outlook_layout.addWidget(folder_combo) # Remplir les boîtes mail et dossiers try: outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") current_mailbox = profile_data['outlook_settings'].get('mailbox', '') current_folder = profile_data['outlook_settings'].get('folder', '') # Fonction pour mettre à jour les dossiers def update_folders(mailbox_name): folder_combo.clear() for i in range(outlook.Folders.Count): if outlook.Folders.Item(i + 1).Name == mailbox_name: mailbox = outlook.Folders.Item(i + 1) def add_folders(folder, parent=""): if '@' not in folder.Name: display_name = folder.Name full_path = f"{parent}/{folder.Name}" if parent else folder.Name folder_combo.addItem(display_name) index = folder_combo.count() - 1 folder_combo.setItemData(index, full_path, Qt.UserRole) if folder.Folders.Count > 0: for j in range(folder.Folders.Count): subfolder = folder.Folders.Item(j + 1) add_folders(subfolder, parent) add_folders(mailbox) break # Remplir les boîtes mail for i in range(outlook.Folders.Count): mailbox = outlook.Folders.Item(i + 1) mailbox_combo.addItem(mailbox.Name) # Sélectionner les valeurs actuelles index = mailbox_combo.findText(current_mailbox) if index >= 0: mailbox_combo.setCurrentIndex(index) update_folders(current_mailbox) index = folder_combo.findText(current_folder) if index >= -1: folder_combo.setCurrentIndex(index) # Connecter le changement de boîte mail mailbox_combo.currentTextChanged.connect(update_folders) except Exception as e: logger.error(f"Erreur lors du chargement des boîtes mail: {str(e)}") # Autres champs Outlook fields = [ ("Expéditeur:", 'sender'), ("Mots-clés sujet:", 'subject_keywords'), ("Mots-clés lien:", 'link_keywords'), ("Récompense:", 'reward') ] for label_text, field_name in fields: field_layout = QHBoxLayout() label = QLabel(label_text) label.setFixedWidth(120) # Largeur fixe pour les labels edit = QLineEdit() edit.setText(str(profile_data['outlook_settings'].get(field_name, ''))) edit.setObjectName(field_name) field_layout.addWidget(label) field_layout.addWidget(edit) outlook_layout.addLayout(field_layout) outlook_group.setLayout(outlook_layout) layout.addWidget(outlook_group) # Finaliser le scroll area scroll_area.setWidget(scroll_content) main_layout.addWidget(scroll_area) # Boutons button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(dialog.accept) button_box.rejected.connect(dialog.reject) main_layout.addWidget(button_box) # Exécution du dialogue if dialog.exec_() == QDialog.Accepted: # Validation de la date de validité if validity_checkbox.isChecked(): try: validity_days = int(validity_entry.text()) if validity_days <= 0: QMessageBox.warning(self, "Erreur", "Le nombre de jours doit être positif") return except ValueError: QMessageBox.warning(self, "Erreur", "Le nombre de jours doit être un nombre entier") return # Récupération du nouveau nom new_name = name_edit.text().strip() new_path = os.path.join(r"C:\autolook\profiles", f"{new_name}.json") # Gestion du renommage if new_name.lower() != current.lower() and os.path.exists(new_path): QMessageBox.warning(self, "Erreur", f"Un profil nommé '{new_name}' existe déjà") return # Mise à jour des paramètres profile_data['settings'] = { 'pause_time_range': { 'min': pause_min.value(), 'max': pause_max.value() }, 'between_pages_time_range': { 'min': between_pages_min.value(), 'max': between_pages_max.value() }, 'mail_count': mail_count.value(), 'validity': { 'enabled': validity_checkbox.isChecked(), 'days': int( validity_entry.text()) if validity_checkbox.isChecked() and validity_entry.text() else None }, # Ajout du niveau de zoom du navigateur 'browser_zoom': zoom_browser_spinbox.value() / 100.0, # Convertir le pourcentage en facteur de zoom # Ajout des paramètres de cycle avec plages 'cycle_processing': { 'enabled': cycle_enabled_checkbox.isChecked(), 'emails_per_cycle_range': { 'min': emails_per_cycle_min_spinbox.value(), 'max': emails_per_cycle_max_spinbox.value() }, 'pause_between_cycles_range': { 'min': pause_between_cycles_min_spinbox.value(), 'max': pause_between_cycles_max_spinbox.value() } } } # Mise à jour des paramètres Outlook if 'outlook_settings' in profile_data: profile_data['outlook_settings'].update({ 'mailbox': mailbox_combo.currentText(), 'folder': folder_combo.currentText(), 'sender': dialog.findChild(QLineEdit, 'sender').text(), 'subject_keywords': dialog.findChild(QLineEdit, 'subject_keywords').text(), 'link_keywords': dialog.findChild(QLineEdit, 'link_keywords').text(), 'reward': dialog.findChild(QLineEdit, 'reward').text(), 'max_emails': str(mail_count.value()) }) # Gérer le renommage si nécessaire if new_name != current: if os.path.exists(profile_path): os.remove(profile_path) profile_path = new_path # Sauvegarde du profil modifié with open(profile_path, 'w', encoding='utf-8') as f: json.dump(profile_data, f, indent=4) # Rafraîchir l'interface self.load_profiles_list() index = self.profile_list.findText(new_name) if index >= 0: self.profile_list.setCurrentIndex(index) QMessageBox.information(self, "Succès", f"Profil '{new_name}' modifié avec succès") QTimer.singleShot(200, self.force_sync_all_panels) except Exception as e: logger.error(f"Erreur lors de la modification du profil : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la modification : {str(e)}") def save_profile(self): """Sauvegarde un profil""" try: # Demander le nom du profil name, ok = QInputDialog.getText(self, "Sauvegarder le profil", "Nom du profil:") if not ok or not name: return # Récupérer les valeurs des sliders min_pause, max_pause = self.pause_slider.values() min_between, max_between = self.between_pages_slider.values() # Récupérer le niveau de zoom actuel du mini navigateur browser_zoom = 0.4 # Valeur par défaut 40% if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): current_tab = self.outlook_widget.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): browser_zoom = current_tab.web_view.zoomFactor() # Récupérer les valeurs de plage pour les cycles # Vérifier d'abord si les attributs sont disponibles emails_min = 5 # Valeur par défaut emails_max = 15 # Valeur par défaut pause_cycles_min = 60 # Valeur par défaut pause_cycles_max = 180 # Valeur par défaut # Récupérer les valeurs réelles si les attributs existent if hasattr(self, 'emails_per_cycle_min_input') and hasattr(self, 'emails_per_cycle_max_input'): try: emails_min = int(self.emails_per_cycle_min_input.text()) emails_max = int(self.emails_per_cycle_max_input.text()) except (ValueError, AttributeError): # Fallback sur les valeurs par défaut déjà définies pass if hasattr(self, 'pause_between_cycles_min_input') and hasattr(self, 'pause_between_cycles_max_input'): try: pause_cycles_min = int(self.pause_between_cycles_min_input.text()) pause_cycles_max = int(self.pause_between_cycles_max_input.text()) except (ValueError, AttributeError): # Fallback sur les valeurs par défaut déjà définies pass # Initialiser la structure de données du profil avec plages de valeurs profile_data = { 'settings': { 'pause_time_range': {'min': min_pause, 'max': max_pause}, 'between_pages_time_range': {'min': min_between, 'max': max_between}, 'links_count': self.links_slider.value(), 'font_size': self.font_size, 'report_enabled': self.report_toggle.isChecked(), 'mail_count': self.mail_count_spinbox.value(), 'browser_zoom': browser_zoom, # Paramètres de cycle avec plages 'cycle_processing': { 'enabled': self.cycle_processing_checkbox.isChecked(), 'emails_per_cycle_range': { 'min': emails_min, 'max': emails_max }, 'pause_between_cycles_range': { 'min': pause_cycles_min, 'max': pause_cycles_max } } }, 'outlook_settings': {} } # Ajouter les paramètres Outlook si le widget existe if hasattr(self, 'outlook_widget'): mailbox_item = self.outlook_widget.mailbox_listbox.currentItem() folder_item = self.outlook_widget.folder_listbox.currentItem() if hasattr(self, 'outlook_widget'): mailbox_item = self.outlook_widget.mailbox_listbox.currentItem() folder_item = self.outlook_widget.folder_listbox.currentItem() mail_count = self.mail_count_spinbox.value() # Récupérer la valeur profile_data['outlook_settings'] = { 'mailbox': mailbox_item.text() if mailbox_item else "", 'folder': folder_item.text() if folder_item else "", 'sender': self.outlook_widget.sender_entry.text().strip(), 'subject_keywords': self.outlook_widget.subject_entry.text().strip(), 'link_keywords': self.outlook_widget.link_entry.text().strip(), 'max_emails': str(mail_count), # Utiliser la même valeur que mail_count 'reward': self.outlook_widget.reward_entry.text().strip() if hasattr(self.outlook_widget, 'reward_entry') else "", # Ajout des paramètres d'authentification 'auth_enabled': self.outlook_widget.auth_checkbox.isChecked() if hasattr(self.outlook_widget, 'auth_checkbox') else False, 'auth_url': self.outlook_widget.url_field if hasattr(self.outlook_widget, 'url_field') else "", 'auth_login': self.outlook_widget.login_field if hasattr(self.outlook_widget, 'login_field') else "", 'auth_password': self.outlook_widget.password_field if hasattr(self.outlook_widget, 'password_field') else "" } logger.info( f"Sauvegarde du profil - Mailbox: {profile_data['outlook_settings']['mailbox']}, Folder: {profile_data['outlook_settings']['folder']}") # Créer le dossier profiles s'il n'existe pas os.makedirs(r"C:\autolook\profiles", exist_ok=True) profile_path = os.path.join(r"C:\autolook\profiles", f"{name}.json") # Sauvegarder le profil with open(profile_path, 'w', encoding='utf-8') as f: json.dump(profile_data, f, indent=4) # Mettre à jour l'interface self.load_profiles_list() index = self.profile_list.findText(name) if index >= 0: self.profile_list.setCurrentIndex(index) # Afficher confirmation QMessageBox.information(self, "Succès", f"Profil '{name}' sauvegardé avec succès") except Exception as e: logger.error(f"Erreur lors de la sauvegarde du profil : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde : {str(e)}") def export_profile(self): """Exporte un profil""" try: current = self.profile_list.currentText() if not current: return path, _ = QFileDialog.getSaveFileName( self, "Exporter le profil", f"{current}.json", "Fichiers JSON (*.json)" ) if path: profile_path = os.path.join(r"C:\autolook\profiles", f"{current}.json") shutil.copy2(profile_path, path) QMessageBox.information(self, "Succès", f"Profil '{current}' exporté avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export : {str(e)}") def import_profile(self): """Importe un profil""" try: path, _ = QFileDialog.getOpenFileName( self, "Importer un profil", "", "Fichiers JSON (*.json)" ) if path: name = os.path.splitext(os.path.basename(path))[0] dest = os.path.join(r"C:\autolook\profiles", f"{name}.json") shutil.copy2(path, dest) self.refresh_profile_list() QMessageBox.information(self, "Succès", f"Profil '{name}' importé avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'import : {str(e)}") def delete_profile(self): """Supprime un profil""" try: current = self.profile_list.currentText() if not current: return reply = QMessageBox.question(self, "Confirmation", f"Voulez-vous vraiment supprimer le profil '{current}' ?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: profile_path = os.path.join(r"C:\autolook\profiles", f"{current}.json") if os.path.exists(profile_path): os.remove(profile_path) self.refresh_profile_list() QMessageBox.information(self, "Succès", f"Profil '{current}' supprimé avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression : {str(e)}") def add_log_message(self, message, message_type="info"): """ Ajoute un message de log avec support pour différents types de messages Args: message (str): Le message à logger message_type (str): Le type de message ("info", "success", "error", "warning") """ try: # Logger le message selon son type if message_type == "error": logger.error(message) elif message_type == "warning": logger.warning(message) elif message_type == "success": logger.info(message) # Success messages en tant qu'info else: # "info" ou autre logger.info(message) # Rediriger vers OutlookWidget si disponible pour l'affichage dans l'interface if hasattr(self, 'outlook_widget') and self.outlook_widget: if hasattr(self.outlook_widget, 'log_action'): self.outlook_widget.log_action(message) elif hasattr(self.outlook_widget, 'add_log_message'): self.outlook_widget.add_log_message(message, message_type) # Alternativement, on peut utiliser la méthode log_action existante self.log_action(message) except Exception as e: # En cas d'erreur, au moins logger dans le système logger.error(f"Erreur lors de l'ajout du message de log: {str(e)}") print(f"LOG: {message}") # Fallback vers print def log_action(self, message): """ Log un message au niveau de l'application et redirige vers OutlookWidget si disponible. """ logger.info(message) # Rediriger vers OutlookWidget si disponible if hasattr(self, 'outlook_widget') and self.outlook_widget: if hasattr(self.outlook_widget, 'log_action'): self.outlook_widget.log_action(message) def toggle_favorite(self): """Change le statut favori du profil sélectionné. Un seul profil peut être favori à la fois.""" try: # Récupérer le nom du profil sans l'étoile current = self.profile_list.currentText().replace("⭐ ", "").replace(" ⏰", "") if not current: logger.warning("Aucun profil sélectionné") return profile_dir = r"C:\AUTOLOOK\profiles" current_path = os.path.join(profile_dir, f"{current}.json") if not os.path.exists(current_path): logger.error(f"Fichier profil non trouvé: {current_path}") return # Lire le profil actuel with open(current_path, 'r', encoding='utf-8') as f: current_profile_data = json.load(f) # Récupérer le statut favori actuel is_currently_favorite = current_profile_data.get('favorite', False) # Si on veut mettre en favori, il faut d'abord retirer le statut favori des autres profils if not is_currently_favorite: # Parcourir tous les profils et retirer leur statut favori for filename in os.listdir(profile_dir): if filename.endswith('.json'): profile_path = os.path.join(profile_dir, filename) try: with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) if profile_data.get('favorite', False): profile_data['favorite'] = False with open(profile_path, 'w', encoding='utf-8') as f: json.dump(profile_data, f, indent=4) logger.info(f"Statut favori retiré de {filename}") except Exception as e: logger.error(f"Erreur lors du traitement du profil {filename}: {e}") continue # Mettre à jour le statut du profil sélectionné current_profile_data['favorite'] = not is_currently_favorite with open(current_path, 'w', encoding='utf-8') as f: json.dump(current_profile_data, f, indent=4) logger.info(f"Statut favori modifié pour {current}: {current_profile_data['favorite']}") # Rafraîchir l'affichage current_text = self.profile_list.currentText() self.load_profiles_list() # Resélectionner le profil new_name = f"⭐ {current}" if current_profile_data['favorite'] else current index = self.profile_list.findText(new_name) if index >= 0: self.profile_list.setCurrentIndex(index) except Exception as e: logger.error(f"Erreur lors du changement de favori : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du changement de favori : {str(e)}") def refresh_profile_list(self): try: self.profile_list.clear() profile_dir = r"C:\autolook\profiles" if not os.path.exists(profile_dir): logger.warning("Dossier des profils inexistant") os.makedirs(profile_dir) return favorites_file = os.path.join(profile_dir, ".favorites") favorites = set() # Charger l'état des favoris depuis le fichier .favorites if os.path.exists(favorites_file): try: with open(favorites_file, 'r', encoding='utf-8') as f: favorites = set(line.strip() for line in f.readlines()) except Exception as e: logger.error(f"Erreur lors de la lecture des favoris : {e}") profiles = [] # Parcourir tous les profils for filename in os.listdir(profile_dir): if not filename.endswith('.json'): continue profile_name = filename[:-5] profile_path = os.path.join(profile_dir, filename) try: with open(profile_path, 'r', encoding='utf-8') as f: data = json.load(f) # Vérifier si le profil est dans le fichier .favorites is_favorite = profile_name in favorites # Mettre à jour le statut favori dans les données du profil data['favorite'] = is_favorite # Sauvegarder les modifications with open(profile_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4) profiles.append({ 'name': profile_name, 'is_favorite': is_favorite, 'path': profile_path, 'data': data }) except Exception as e: logger.error(f"Erreur lors du traitement du profil {profile_name}: {e}") profiles.append({ 'name': profile_name, 'is_favorite': False, 'path': profile_path, 'data': {} }) # Trier les profils (favoris en premier, puis ordre alphabétique) profiles.sort(key=lambda x: (-x['is_favorite'], x['name'].lower())) # Mettre à jour la liste d'affichage for profile in profiles: display_name = f"⭐ {profile['name']}" if profile['is_favorite'] else profile['name'] self.profile_list.addItem(display_name) logger.info(f"Liste des profils rafraîchie avec succès. {len(profiles)} profils chargés.") except Exception as e: logger.error(f"Erreur lors de l'actualisation de la liste des profils : {e}") QMessageBox.warning(self, "Erreur", "Impossible de rafraîchir la liste des profils") def profile_exists(self, name): """Vérifie si un profil existe""" return os.path.exists(os.path.join(r"C:\autolook\profiles", f"{name}.json")) def update_last_used(self, profile_name): """Met à jour le dernier profil utilisé""" try: with open(os.path.join(r"C:\autolook\profiles", ".last_used"), 'w') as f: f.write(profile_name) except Exception as e: print(f"Erreur lors de la mise à jour du dernier profil : {e}") # def on_font_size_changed(self, value): # self.font_size = value # self.font_size_value_label.setText(str(value)) # if hasattr(self, 'outlook_widget'): # self.outlook_widget.update_font_size(value) # self.save_settings() def update_font_size(self, value): """Met à jour la taille de police""" try: self.font_size = value self.font_size_value_label.setText(str(value)) # Appliquer la nouvelle taille self.apply_font_size() # Sauvegarder immédiatement self.save_settings() logger.info(f"Taille de police mise à jour: {value}") except Exception as e: logger.error(f"Erreur lors de la mise à jour de la taille de police: {str(e)}") def load_settings(self): """Charge les paramètres depuis le fichier Fontsize.json""" try: font_size_file = os.path.join(r"C:\autolook", "Fontsize.json") if os.path.exists(font_size_file): with open(font_size_file, 'r') as f: settings = json.load(f) font_size = settings.get("font_size", 9) # Valeur par défaut: 9 logger.info(f"Taille de police chargée: {font_size}") return font_size else: logger.info("Fichier de paramètres non trouvé, utilisation de la taille par défaut") return 9 # Taille par défaut except Exception as e: logger.error(f"Erreur lors du chargement des paramètres de police: {str(e)}") return 9 # Taille par défaut en cas d'erreur def save_settings(self): """Sauvegarde les paramètres dans le fichier Fontsize.json""" try: font_size_file = os.path.join(r"C:\autolook", "Fontsize.json") settings = {"font_size": self.font_size} with open(font_size_file, 'w') as f: json.dump(settings, f, indent=4) logger.info(f"Taille de police sauvegardée: {self.font_size}") except Exception as e: logger.error(f"Erreur lors de la sauvegarde des paramètres de police: {str(e)}") def apply_font_size(self): """ Applique la taille de police à toute l'application de manière fluide, en évitant les lags dus à une récursion trop profonde sur tous les widgets. """ try: QApplication.setOverrideCursor(Qt.WaitCursor) self.setUpdatesEnabled(False) # Créer une police de base avec la taille spécifiée base_font = QFont() base_font.setPointSize(self.font_size) # Sauvegarder la police actuelle du titre s'il existe title_font = None if hasattr(self, 'title_label'): title_font = self.title_label.font() title_size = title_font.pointSize() # Conserver la taille du titre # Appliquer à l'application entière QApplication.setFont(base_font) # Appliquer la police à tous les widgets enfants (non récursif pour éviter les freezes) for child in self.findChildren(QWidget): # Ne pas modifier le titre if child == getattr(self, 'title_label', None): continue # Éviter les composants lourds if child.metaObject().className() in ["QWebEngineView", "QPlainTextEdit"]: continue current_font = child.font() current_font.setPointSize(self.font_size) child.setFont(current_font) # Restaurer la police du titre si besoin if hasattr(self, 'title_label') and title_font: self.title_label.setFont(title_font) # Appliquer aussi à OutlookWidget s’il existe if hasattr(self, 'outlook_widget') and self.outlook_widget: self.outlook_widget.update_font_size(self.font_size) # Mettre à jour le slider de taille if hasattr(self, 'font_size_slider'): self.font_size_slider.setValue(self.font_size) if hasattr(self, 'font_size_value_label'): self.font_size_value_label.setText(str(self.font_size)) logger.info(f"Taille de police appliquée dynamiquement: {self.font_size}") QApplication.processEvents() if hasattr(self, 'font_size_changed_signal'): self.font_size_changed_signal.emit(self.font_size) except Exception as e: logger.error(f"Erreur lors de l'application dynamique de la taille de police: {str(e)}") finally: self.setUpdatesEnabled(True) QApplication.restoreOverrideCursor() def toggle_report(self, checked): if checked: self.report_toggle.setText("Rapport traitement activé") self.report_data = [] else: self.report_toggle.setText("Rapport traitement désactivé") self.display_report() def display_report(self): print("Début de la méthode display_report") try: if hasattr(self, 'report_toggle'): print(f"État de report_toggle: {self.report_toggle.isChecked()}") else: print("Attention: report_toggle n'existe pas") return if self.report_toggle.isChecked(): print("Génération du rapport...") report_content = self.generate_report() print(f"Contenu du rapport: {report_content[:100]}...") # Affiche les 100 premiers caractères print("Création de la boîte de dialogue du rapport...") dialog = ReportDialog(report_content, self) dialog.setStyleSheet(self.styleSheet()) print("Affichage de la boîte de dialogue...") dialog.exec_() else: print("Le rapport est désactivé") except Exception as e: print(f"Erreur dans display_report: {e}") finally: print("Fin de la méthode display_report") def generate_and_display_report(self): """Génère et affiche le rapport de traitement""" try: report_content = self.generate_report() dialog = ReportDialog(report_content, parent=None) dialog.exec_() except Exception as e: self.log_action(f"Erreur lors de la génération du rapport : {str(e)}") def record_action(self, source, link, reward): print(f"record_action appelée avec: source={source}, link={link}, reward={reward}") if self.report_toggle.isChecked(): now = QDateTime.currentDateTime() cpu_usage = psutil.cpu_percent() ram_usage = psutil.virtual_memory().percent action_data = { 'source': source, 'link': link, 'datetime': now.toString("yyyy-MM-dd HH:mm:ss"), 'reward': reward, 'cpu_usage': cpu_usage, 'ram_usage': ram_usage } self.report_data.append(action_data) print(f"Action enregistrée: {action_data}") # Forcer la mise à jour de l'interface utilisateur QApplication.processEvents() else: print("L'enregistrement des actions est désactivé") def generate_report(self): try: # Format texte pour l'impression avec plus d'espacement #report = "RAPPORT DE TRAITEMENT\n" #report += "=" * 50 + "\n\n\n" # Triple saut de ligne après le titre if not hasattr(self, 'report_data') or not self.report_data: report += "Aucune donnée de traitement n'a été enregistrée.\n" else: total_reward = 0 email_count = len(self.report_data) report += "RÉSUMÉ DU TRAITEMENT\n" report += "-" * 30 + "\n\n" # Double saut de ligne report += f"Nombre total d'emails traités: {email_count}\n\n\n" # Triple saut de ligne for idx, data in enumerate(self.report_data, 1): report += f"Email #{idx}\n" report += "-" * 20 + "\n\n" # Double saut de ligne report += f"Source: {data.get('source', 'N/A')}\n\n" # Saut de ligne après chaque entrée report += f"Date/Heure: {data.get('datetime', 'N/A')}\n\n" report += f"Lien: {data.get('link', 'N/A')}\n\n" reward = data.get('reward', 0) total_reward += float(reward) if reward != 'N/A' else 0 report += f"Récompense: {reward}\n\n" report += f"CPU: {data.get('cpu_usage', 'N/A')}%\n\n" report += f"RAM: {data.get('ram_usage', 'N/A')}%\n\n" # Ajout des nouvelles informations report += f"Temps d'affichage: {data.get('display_time', 'N/A')} secondes\n\n" report += f"Temps de pause: {data.get('pause_time', 'N/A')} secondes\n\n" report += "=" * 50 + "\n\n\n\n\n" # Triple saut de ligne entre emails report += "RÉCAPITULATIF\n" report += "-" * 30 + "\n\n" report += f"Total des récompenses: {total_reward:.2f}\n" report += f"Moyenne par email: {(total_reward / email_count):.2f}\n\n" report += "=" * 50 + "\n" return report except Exception as e: print(f"Erreur dans generate_report: {e}") return "Erreur lors de la génération du rapport." def update_pause_time(self, min_value, max_value): self.pause_value_label.setText(f"{min_value}-{max_value}") self.pause_time_main_label.setText(f"Temps d'affichage de la page: {min_value}-{max_value} secondes") # Transmettre à OutlookHandler if self.outlook_handler: self.outlook_handler.set_pause_time_range(min_value, max_value) def initialize_modules(self): add_debug_log("initialize_modules", "Début de l'initialisation des modules") if not self.modules: logger.warning("Aucun module n'a été chargé") self.handle_missing_modules() return config = load_config() successful_modules = [] failed_modules = [] # Initialisation d'Advanced Browser if 'ha2' in self.modules: try: browser_module = self.modules['ha2'] self.ha2 = browser_module.BrowserWindow() if hasattr(browser_module, 'setup_interface'): browser_module.setup_interface(self) logger.info("Module ha2 initialisé avec succès") successful_modules.append("ha2") except Exception as e: logger.error(f"Erreur lors de l'initialisation du module ha2: {str(e)}") failed_modules.append(f"ha2: {str(e)}") # Initialisation des autres modules for module_name, module in self.modules.items(): if module_name != 'ha2': try: if hasattr(module, 'setup_interface'): module.setup_interface(self) logger.info(f"Module {module_name} initialisé avec succès") successful_modules.append(module_name) else: logger.warning(f"Le module {module_name} n'a pas de méthode setup_interface") except Exception as e: logger.error(f"Erreur lors de l'initialisation du module {module_name}: {str(e)}") failed_modules.append(f"{module_name}: {str(e)}") # Création et enregistrement du résumé dans les logs summary = "Résumé de l'initialisation des modules:\n" if successful_modules: summary += "Modules initialisés avec succès:\n- " + "\n- ".join(successful_modules) + "\n" if failed_modules: summary += "Modules non initialisés ou avec erreurs:\n- " + "\n- ".join(failed_modules) + "\n" logger.info(summary) add_debug_log("initialize_modules", summary) add_debug_log("initialize_modules", "Fin de l'initialisation des modules") def setup_ha2(self): # Initialisation spécifique pour ha2 logger.info("Initialisation spécifique d'ha2") if hasattr(self, 'ha2'): # Exemple d'initialisation : # self.ha2.set_main_window(self) # self.ha2.initialize_settings(self.config.get('ha2', {})) # Vous pouvez ajouter ici toute logique d'initialisation nécessaire # par exemple, configurer des options, connecter des signaux, etc. pass else: logger.warning("ha2 n'est pas correctement initialisé") def handle_missing_modules(self): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("Aucun module n'est chargé") msg.setWindowTitle("Avertissement") msg.setStandardButtons(QMessageBox.Ok) QTimer.singleShot(5000, msg.close) msg.exec_() self.redirect_to_module_integrator() def redirect_to_module_integrator(self): logger.info("Redirection vers l'intégrateur de module...") self.hide() from module_integrator import ModuleIntegrator integrator = ModuleIntegrator(os.path.join(AUTOLOOK_DIR, "main.py")) integrator.show() QApplication.instance().exec_() self.show() self.reload_modules() def reload_modules(self): self.modules = load_modules() self.initialize_modules() def closeEvent(self, event): if self.isActiveWindow(): # Ne s'applique qu'à la fenêtre principale add_debug_log("MainWindow.closeEvent", "Fermeture de l'application") if hasattr(self, 'audio_player'): self.audio_player.stop() self.save_settings() write_debug_file() event.accept() def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def toggle_sidebar(self): width = 420 if not self.sidebar_open else 0 # ← AUGMENTÉ DE 350 À 420 self.sidebar_animation.setDuration(350) self.sidebar_animation.setStartValue(self.sidebar.width()) self.sidebar_animation.setEndValue(width) self.sidebar_animation.start() self.sidebar_open = not self.sidebar_open def show_error_message(self, message): QMessageBox.critical(self, "Erreur", message) def reset_after_update(self): """ Remet à zéro l'interface après une mise à jour """ try: # Réinitialiser les compteurs de liens if hasattr(self, 'outlook_widget') and self.outlook_widget: if hasattr(self.outlook_widget, 'outlook_handler') and self.outlook_widget.outlook_handler: self.outlook_widget.outlook_handler.links_clicked = 0 self.outlook_widget.outlook_handler.links_skipped = 0 # Mettre à jour le statut if hasattr(self, 'outlook_widget') and self.outlook_widget: self.outlook_widget.status_label.setText("Prêt après mise à jour") self.outlook_widget.progress_bar.setValue(0) # Logger l'action self.log_action("Interface réinitialisée après mise à jour") except Exception as e: logger.error(f"Erreur lors de la réinitialisation après mise à jour: {str(e)}") def save_current_journal(self): """ Sauvegarde le journal actuel des actions """ try: from datetime import datetime import os import json # Créer le dossier de journaux s'il n'existe pas journal_dir = os.path.join(r"C:\autolook", "journals") os.makedirs(journal_dir, exist_ok=True) # Nom du fichier journal avec timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") journal_file = os.path.join(journal_dir, f"journal_{timestamp}.json") # Données du journal journal_data = { "timestamp": datetime.now().isoformat(), "type": "post_update", "status": "Interface mise à jour avec succès", "actions": [] } # Ajouter les actions du rapport si disponibles if hasattr(self, 'report_data') and self.report_data: journal_data["actions"] = self.report_data # Sauvegarder le journal with open(journal_file, 'w', encoding='utf-8') as f: json.dump(journal_data, f, indent=2, ensure_ascii=False) # Logger l'action self.log_action(f"Journal sauvegardé: {journal_file}") except Exception as e: logger.error(f"Erreur lors de la sauvegarde du journal: {str(e)}") class ImapAccountDialog(QDialog): def __init__(self, parent=None, accounts_file="imap_accounts.json", password_manager=None): super().__init__(parent) self.setWindowTitle("Connexion IMAP - Sélection ou ajout de compte") self.setFixedSize(800, 500) self.accounts_file = accounts_file self.setup_modern_style() self.selected_account = None self.password_manager = password_manager self.selected_accounts = [] # Pour les comptes multiples self.button_state = "select" self.layout = QVBoxLayout(self) # === Interface en deux parties === # === PARTIE HAUTE : 3 colonnes === # Layout horizontal pour les trois colonnes fields_layout = QHBoxLayout() self.layout.addLayout(fields_layout) # === COLONNE DE GAUCHE : Champs de saisie === left_column = QVBoxLayout() fields_layout.addLayout(left_column) # Champs de saisie self.email_input = QLineEdit() self.email_input.setPlaceholderText("Adresse email") self.password_input = QLineEdit() self.password_input.setPlaceholderText("Mot de passe") self.password_input.setEchoMode(QLineEdit.Password) self.provider_combo = QComboBox() self.provider_combo.addItems(["imap.gmail.com", "imap-mail.outlook.com", "imap.mail.yahoo.com", "Autre..."]) # Ajout des champs à la colonne de gauche avec plus d'espace left_column.addWidget(QLabel("📧 Adresse email :")) left_column.addWidget(self.email_input) left_column.addSpacing(10) left_column.addWidget(QLabel("🔐 Mot de passe :")) left_column.addWidget(self.password_input) left_column.addSpacing(10) left_column.addWidget(QLabel("🌐 Fournisseur IMAP :")) left_column.addWidget(self.provider_combo) left_column.addSpacing(15) # Ajout d'un bouton de validation rapide self.quick_validate_btn = QPushButton("⚡ Validation rapide") self.quick_validate_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #16a085, stop:1 #1abc9c); border: none; border-radius: 6px; color: white; font-size: 12px; font-weight: bold; padding: 8px 16px; min-height: 12px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1abc9c, stop:1 #16a085); } """) left_column.addWidget(self.quick_validate_btn) left_column.addStretch() # === COLONNE DU MILIEU : Aide mot de passe d'application === middle_column = QVBoxLayout() fields_layout.addLayout(middle_column) # Fenêtre d'aide pour les mots de passe d'application self.help_widget = QWidget() self.help_widget.setStyleSheet(""" QWidget { background-color: rgba(52, 152, 219, 0.15); border: 2px solid #3498db; border-radius: 8px; padding: 10px; margin: 5px; } """) self.help_layout = QVBoxLayout(self.help_widget) self.help_title = QLabel("🔐 Mot de passe d'application") self.help_title.setStyleSheet("font-weight: bold; color: #3498db; font-size: 14px;") self.help_layout.addWidget(self.help_title) self.help_text = QLabel() self.help_text.setWordWrap(True) self.help_text.setOpenExternalLinks(True) self.help_layout.addWidget(self.help_text) self.help_layout.addStretch() middle_column.addWidget(self.help_widget) middle_column.addStretch() # === COLONNE DE DROITE : Comptes enregistrés === right_column = QVBoxLayout() fields_layout.addLayout(right_column) # Titre des comptes accounts_title = QLabel("📋 Comptes IMAP enregistrés") accounts_title.setStyleSheet("font-weight: bold; color: #ecf0f1; font-size: 14px; padding: 5px;") right_column.addWidget(accounts_title) # Liste des comptes self.account_list = QListWidget() self.account_list.setMinimumHeight(120) self.account_list.setMaximumHeight(180) right_column.addWidget(self.account_list) # Boutons d'action pour les comptes (2x2) accounts_btn_container = QVBoxLayout() # === PREMIÈRE LIGNE : Utiliser + Synchroniser === first_row = QHBoxLayout() self.use_btn = QPushButton("✅ Utiliser") self.use_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #27ae60, stop:1 #2ecc71); font-size: 10px; padding: 5px 8px; min-height: 8px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2ecc71, stop:1 #27ae60); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #229954, stop:1 #27ae60); } """) self.sync_btn = QPushButton("🔄 Sync") self.sync_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #f39c12, stop:1 #e67e22); font-size: 10px; padding: 5px 8px; min-height: 8px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #e67e22, stop:1 #f39c12); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #d68910, stop:1 #f39c12); } """) first_row.addWidget(self.use_btn) first_row.addWidget(self.sync_btn) accounts_btn_container.addLayout(first_row) # === DEUXIÈME LIGNE : Ajouter + Supprimer === second_row = QHBoxLayout() self.add_btn = QPushButton("➕ Ajouter") self.add_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #8e44ad, stop:1 #9b59b6); font-size: 10px; padding: 5px 8px; min-height: 8px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #9b59b6, stop:1 #8e44ad); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #7d3c98, stop:1 #8e44ad); } """) self.delete_btn = QPushButton("🗑️ Suppr") self.delete_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #e74c3c, stop:1 #c0392b); font-size: 10px; padding: 5px 8px; min-height: 8px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #c0392b, stop:1 #e74c3c); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a93226, stop:1 #c0392b); } """) second_row.addWidget(self.add_btn) second_row.addWidget(self.delete_btn) accounts_btn_container.addLayout(second_row) right_column.addLayout(accounts_btn_container) # === COMPTEUR DE BOÎTES MAIL === self.accounts_counter = QLabel("📊 0 boîte mail") self.accounts_counter.setStyleSheet(""" QLabel { background-color: rgba(44, 62, 80, 0.8); border: 2px solid #34495e; border-radius: 6px; padding: 8px 12px; color: #ecf0f1; font-size: 11px; font-weight: bold; text-align: center; margin: 5px 0px; } """) self.accounts_counter.setAlignment(Qt.AlignCenter) right_column.addWidget(self.accounts_counter) # === BOUTON SÉLECTIONNER ET CHARGER === self.select_load_btn = QPushButton("📬 Sélectionner et charger") self.select_load_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3498db, stop:1 #2980b9); border: none; border-radius: 6px; color: white; font-size: 12px; font-weight: bold; padding: 10px 15px; min-height: 14px; margin: 8px 2px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5dade2, stop:1 #3498db); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2980b9, stop:1 #21618c); } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } """) # Variable pour gérer l'état du bouton self.selected_accounts = [] self.button_state = "select" # "select" ou "load" right_column.addWidget(self.select_load_btn) right_column.addStretch() # Ajuster les proportions des colonnes (35% gauche, 40% milieu, 25% droite) fields_layout.setStretchFactor(left_column, 25) fields_layout.setStretchFactor(middle_column, 35) fields_layout.setStretchFactor(right_column, 40) # === PARTIE BASSE : Section test de connexion sur toute la largeur === self.layout.addSpacing(15) # Espace entre les deux parties # Widget de test de connexion test_widget = QWidget() test_widget.setStyleSheet(""" QWidget { background-color: rgba(52, 152, 219, 0.15); border: 2px solid #3498db; border-radius: 8px; padding: 15px; margin: 5px; } """) test_layout = QVBoxLayout(test_widget) # Titre et bouton sur la même ligne test_header_layout = QHBoxLayout() test_title = QLabel("🔧 Test de connexion") test_title.setStyleSheet("font-weight: bold; color: #3498db; font-size: 12px;") test_header_layout.addWidget(test_title) test_header_layout.addStretch() # Pousser le bouton à droite self.test_btn = QPushButton("📡 Tester la connexion") self.test_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #27ae60, stop:1 #2ecc71); border: none; border-radius: 6px; color: white; font-size: 14px; font-weight: bold; padding: 12px 24px; min-height: 16px; min-width: 200px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2ecc71, stop:1 #27ae60); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #229954, stop:1 #27ae60); } """) test_header_layout.addWidget(self.test_btn) test_layout.addLayout(test_header_layout) # Champ de résultat du test (grand et bien visible) self.test_result = QLabel( "Remplissez les champs ci-dessus puis cliquez sur 'Tester la connexion' pour vérifier la configuration IMAP") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(44, 62, 80, 0.9); border: 2px solid #34495e; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) self.test_result.setWordWrap(True) self.test_result.setAlignment(Qt.AlignTop) test_layout.addWidget(self.test_result) self.layout.addWidget(test_widget) # === Connexions des signaux === self.use_btn.clicked.connect(self.use_account) self.sync_btn.clicked.connect(self.load_accounts) self.add_btn.clicked.connect(self.add_account) self.account_list.itemClicked.connect(self.populate_fields) self.provider_combo.currentTextChanged.connect(self.update_help_content) self.test_btn.clicked.connect(self.test_connection) self.quick_validate_btn.clicked.connect(self.quick_validate) self.email_input.textChanged.connect(self.auto_detect_provider) self.delete_btn.clicked.connect(self.delete_account) self.select_load_btn.clicked.connect(self.handle_select_load) # === Initialisation === self.update_help_content() self.load_accounts() def delete_account(self): """Supprime le compte sélectionné de la liste""" # Vérifier qu'un compte est sélectionné current_item = self.account_list.currentItem() if not current_item: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner un compte à supprimer.") return # Récupérer les informations du compte account = current_item.data(Qt.UserRole) email = account.get('email', 'Compte inconnu') # Demander confirmation reply = QMessageBox.question(self, "Confirmer la suppression", f"Êtes-vous sûr de vouloir supprimer le compte :\n\n📧 {email}\n\n" "Cette action est irréversible !", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply != QMessageBox.Yes: return try: # Déterminer la source du compte source = account.get('source', 'json') if source == 'password_manager': # Compte du gestionnaire de mots de passe - Avertissement spécial reply = QMessageBox.question(self, "⚠️ ATTENTION - Suppression du gestionnaire", f"VOUS ALLEZ SUPPRIMER UN COMPTE DU GESTIONNAIRE DE MOTS DE PASSE !\n\n" f"📧 Compte : {email}\n" f"🔐 Source : Gestionnaire de mots de passe\n\n" f"⚠️ AVERTISSEMENTS IMPORTANTS :\n" f"• Cette suppression sera DÉFINITIVE\n" f"• SEUL ce compte sera retiré du gestionnaire\n" f"• Les autres comptes restent intacts\n" f"• Cette action est IRRÉVERSIBLE\n\n" f"Êtes-vous absolument certain de vouloir continuer ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply != QMessageBox.Yes: return # Supprimer du gestionnaire de mots de passe try: success = self.delete_from_password_manager(email) if success: # Recharger la liste self.load_accounts() # Le compteur est mis à jour automatiquement par load_accounts() # Message de confirmation self.test_result.setText(f"✅ Compte supprimé du gestionnaire !\n\n📧 {email}\n\nLe compte a été retiré du gestionnaire de mots de passe.\nLes autres comptes du gestionnaire restent intacts.\n\n⚠️ Suppression irréversible effectuée.") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 2px solid #2e7d32; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) # Vider les champs si c'était le compte affiché if self.email_input.text().strip() == email: self.email_input.clear() self.password_input.clear() self.provider_combo.setCurrentIndex(0) else: QMessageBox.critical(self, "Erreur de suppression", f"Impossible de supprimer le compte '{email}' du gestionnaire.\n\n" "Le compte pourrait être protégé ou le fichier est inaccessible.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression du compte du gestionnaire :\n\n{str(e)}") print(f"Erreur suppression gestionnaire: {e}") return elif source == 'json': # Compte du fichier JSON - on peut le supprimer row = self.account_list.row(current_item) # Reconstruire la liste sans le compte supprimé updated_accounts = [] for i, acc in enumerate(self.accounts): if acc.get('source') == 'json' and i != row: # Garder tous les comptes JSON sauf celui à supprimer updated_accounts.append({ 'email': acc['email'], 'password': acc['password'], 'provider': acc['provider'] }) # Sauvegarder le fichier JSON mis à jour with open(self.accounts_file, "w", encoding="utf-8") as f: json.dump(updated_accounts, f, indent=2, ensure_ascii=False) # Recharger la liste self.load_accounts() # Message de confirmation self.test_result.setText( f"✅ Compte supprimé avec succès !\n\n📧 {email}\n\nLe compte a été retiré de la liste des comptes enregistrés.") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 2px solid #2e7d32; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) # Vider les champs si c'était le compte affiché if self.email_input.text().strip() == email: self.email_input.clear() self.password_input.clear() self.provider_combo.setCurrentIndex(0) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression du compte :\n\n{str(e)}") print(f"Erreur suppression compte: {e}") def delete_from_password_manager(self, email_to_delete): """Supprime un compte spécifique du gestionnaire de mots de passe""" try: import os import json import base64 password_file = "passwords.enc" if not os.path.exists(password_file): print(f"📂 Fichier {password_file} introuvable") return False # Lire le fichier with open(password_file, 'r', encoding='utf-8') as f: encrypted_data = f.read() if not encrypted_data.strip(): print("📂 Fichier vide") return False # Déchiffrer selon les différents niveaux de sécurité decrypted_data = self.decrypt_password_file(encrypted_data) if not decrypted_data: print("❌ Impossible de déchiffrer le fichier pour suppression") return False print(f"✅ Données déchiffrées pour suppression: {len(decrypted_data)} entrées") # Filtrer pour supprimer le compte spécifique original_count = len(decrypted_data) updated_data = [] found_and_removed = False for entry in decrypted_data: if (entry.get('category') == '📧 IMAP/Email' and entry.get('email', '').lower() == email_to_delete.lower()): print(f"✅ Compte {email_to_delete} supprimé avec succès du gestionnaire") found_and_removed = True # Ne pas ajouter cette entrée à updated_data (= suppression) else: # Garder toutes les autres entrées updated_data.append(entry) if not found_and_removed: print(f"❌ Compte {email_to_delete} non trouvé dans le gestionnaire") return False print(f"📊 Entrées avant: {original_count}, après: {len(updated_data)}") # Sauvegarder les données mises à jour success = self.save_to_password_manager(updated_data, encrypted_data) if success: print(f"✅ Compte {email_to_delete} supprimé avec succès du gestionnaire") return True else: print(f"❌ Échec de la sauvegarde après suppression") return False except Exception as e: print(f"❌ Erreur suppression gestionnaire: {e}") import traceback traceback.print_exc() return False def save_to_password_manager(self, data, original_encrypted_data=""): """Sauvegarde les données dans le gestionnaire de mots de passe""" try: import json import base64 import os # Déterminer le niveau de chiffrement encryption_level = 2 # Valeur par défaut if original_encrypted_data: encryption_level = self.detect_encryption_level(original_encrypted_data) else: # Détecter depuis le fichier existant password_file = "passwords.enc" if os.path.exists(password_file): try: with open(password_file, 'r', encoding='utf-8') as f: existing_data = f.read() encryption_level = self.detect_encryption_level(existing_data) except Exception as e: print(f"⚠️ Erreur lecture fichier existant: {e}") encryption_level = 2 # Fallback else: encryption_level = 2 # Niveau 2 par défaut pour nouveaux fichiers print(f"🔐 Niveau de chiffrement utilisé: {encryption_level}") # Générer le contenu chiffré selon le niveau if encryption_level == 1: # Niveau 1: JSON clair encrypted_content = json.dumps(data, indent=2, ensure_ascii=False) elif encryption_level == 2: # Niveau 2: Obfuscation Base64 + ROT13 json_data = json.dumps(data, indent=2, ensure_ascii=False) # Rotation (ROT13 modifié) obfuscated = "" for char in json_data: obfuscated += chr((ord(char) + 13) % 256) # Encoder en Base64 encrypted_content = base64.b64encode(obfuscated.encode('latin-1')).decode() elif encryption_level == 3: # Niveau 3: Chiffrement avec clé machine try: from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # Générer la même clé machine machine_key = self.generate_machine_key() salt = b'machine_key_salt_2024_password_manager' kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=50000, ) key = base64.urlsafe_b64encode(kdf.derive(machine_key.encode())) # Chiffrer f = Fernet(key) json_bytes = json.dumps(data, indent=2, ensure_ascii=False).encode() encrypted_bytes = f.encrypt(json_bytes) encrypted_content = base64.b64encode(encrypted_bytes).decode() except Exception as e: print(f"❌ Erreur chiffrement niveau 3: {e}") print("🔄 Retour au niveau 2") # Fallback vers niveau 2 json_data = json.dumps(data, indent=2, ensure_ascii=False) obfuscated = "" for char in json_data: obfuscated += chr((ord(char) + 13) % 256) encrypted_content = base64.b64encode(obfuscated.encode('latin-1')).decode() else: print(f"⚠️ Niveau de chiffrement {encryption_level} non supporté, utilisation niveau 2") # Fallback vers niveau 2 json_data = json.dumps(data, indent=2, ensure_ascii=False) obfuscated = "" for char in json_data: obfuscated += chr((ord(char) + 13) % 256) encrypted_content = base64.b64encode(obfuscated.encode('latin-1')).decode() # Sauvegarder le fichier try: with open("passwords.enc", 'w', encoding='utf-8') as f: f.write(encrypted_content) print("✅ Fichier passwords.enc sauvegardé avec succès") return True except Exception as e: print(f"❌ Erreur écriture fichier: {e}") return False except Exception as e: print(f"❌ Erreur sauvegarde générale: {e}") import traceback traceback.print_exc() return False def detect_encryption_level(self, encrypted_data): """Détecte le niveau de chiffrement du fichier""" try: if not encrypted_data or not encrypted_data.strip(): return 2 # Défaut pour fichier vide import json import base64 # Test niveau 1 (JSON clair) try: json.loads(encrypted_data) print("🔓 Détecté: Niveau 1 (JSON clair)") return 1 except: pass # Test niveau 2 (Base64 + obfuscation) try: decoded = base64.b64decode(encrypted_data.encode()).decode('latin-1') deobfuscated = "" for char in decoded: deobfuscated += chr((ord(char) - 13) % 256) json.loads(deobfuscated) print("🔓 Détecté: Niveau 2 (obfuscation)") return 2 except: pass # Test niveau 3 (chiffrement machine) try: # Si c'est du Base64 valide mais pas niveau 2, probablement niveau 3 base64.b64decode(encrypted_data.encode()) print("🔓 Détecté: Niveau 3 (chiffrement machine)") return 3 except: pass # Niveau inconnu - utiliser niveau 2 par défaut print("⚠️ Niveau de chiffrement inconnu, utilisation niveau 2 par défaut") return 2 except Exception as e: print(f"❌ Erreur détection niveau: {e}") return 2 # Niveau par défaut en cas d'erreur def load_password_manager_full_data(self): """Charge toutes les données du gestionnaire (pas seulement IMAP)""" try: import os password_file = "passwords.enc" if not os.path.exists(password_file): print(f"📂 Fichier {password_file} introuvable") return [] # Lire le fichier with open(password_file, 'r', encoding='utf-8') as f: encrypted_data = f.read() if not encrypted_data.strip(): print("📂 Fichier vide") return [] # Déchiffrer decrypted_data = self.decrypt_password_file(encrypted_data) if not decrypted_data: print("❌ Impossible de déchiffrer le fichier") return [] print(f"✅ Toutes les données chargées: {len(decrypted_data)} entrées") return decrypted_data except Exception as e: print(f"❌ Erreur lecture complète passwords.enc: {e}") return [] def save_new_account_to_password_manager(self, all_data): """Sauvegarde toutes les données (y compris le nouveau compte) dans passwords.enc""" try: import os # Déterminer le niveau de chiffrement existant password_file = "passwords.enc" encryption_level = 2 # Par défaut niveau 2 if os.path.exists(password_file): with open(password_file, 'r', encoding='utf-8') as f: original_data = f.read() encryption_level = self.detect_encryption_level(original_data) # Sauvegarder avec le même niveau de chiffrement success = self.save_to_password_manager(all_data, "") return success except Exception as e: print(f"❌ Erreur sauvegarde nouveau compte: {e}") return False def update_accounts_counter(self): """Met à jour le compteur de boîtes mail""" try: count = len(self.accounts) if count == 0: text = "📊 Aucune boîte mail" color = "#e74c3c" # Rouge elif count == 1: text = "📊 1 boîte mail" color = "#f39c12" # Orange else: text = f"📊 {count} boîtes mail" color = "#27ae60" # Vert self.accounts_counter.setText(text) # Changer la couleur selon le nombre self.accounts_counter.setStyleSheet(f""" QLabel {{ background-color: rgba(44, 62, 80, 0.8); border: 2px solid {color}; border-radius: 6px; padding: 8px 12px; color: #ecf0f1; font-size: 11px; font-weight: bold; text-align: center; margin: 5px 0px; }} """) print(f"📊 Compteur mis à jour: {count} compte(s)") except Exception as e: print(f"❌ Erreur mise à jour compteur: {e}") self.accounts_counter.setText("📊 Erreur comptage") def handle_select_load(self): """Gère le bouton sélectionner/charger selon son état""" if self.button_state == "select": # Mode sélection self.select_mailboxes() else: # Mode chargement self.load_selected_mailboxes() def select_mailboxes(self): """Permet de sélectionner une ou plusieurs boîtes mail avec interface améliorée""" try: # Vérifier qu'il y a des comptes disponibles if not self.accounts: QMessageBox.warning(self, "Aucun compte", "Aucun compte IMAP disponible.\nVeuillez d'abord ajouter des comptes.") return print(f"🔍 Ouverture sélection avec {len(self.accounts)} comptes disponibles") # Créer une fenêtre de sélection dialog = QDialog(self) dialog.setWindowTitle("Sélectionner les boîtes mail à charger") dialog.setFixedSize(500, 400) dialog.setModal(True) # Fenêtre modale # === EFFET DE FLOU SUR LA FENÊTRE PRINCIPALE === blur_effect = QGraphicsBlurEffect() blur_effect.setBlurRadius(8) # Intensité du flou self.setGraphicsEffect(blur_effect) # Fonction pour supprimer l'effet de flou à la fermeture def remove_blur(): try: self.setGraphicsEffect(None) print("🔄 Effet de flou supprimé") except Exception as e: print(f"⚠️ Erreur suppression flou: {e}") # Connecter la suppression du flou à la fermeture du dialog dialog.finished.connect(remove_blur) print("✨ Effet de flou appliqué sur la fenêtre principale") # Style de la fenêtre dialog.setStyleSheet(""" QDialog { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #2c3e50, stop:1 #34495e); color: white; border: 3px solid #3498db; border-radius: 15px; } QLabel { color: #ecf0f1; } """) # Layout principal main_layout = QVBoxLayout(dialog) main_layout.setSpacing(20) main_layout.setContentsMargins(25, 25, 25, 25) # Titre avec compteur et icône title = QLabel(f"📬 Sélectionnez les boîtes mail à charger ({len(self.accounts)} disponibles)") title.setStyleSheet(""" QLabel { font-weight: bold; font-size: 16px; color: #3498db; padding: 10px; background: rgba(52, 152, 219, 0.1); border: 2px solid rgba(52, 152, 219, 0.3); border-radius: 8px; text-align: center; } """) title.setAlignment(Qt.AlignCenter) main_layout.addWidget(title) # Zone de scroll pour les checkboxes from PyQt5.QtWidgets import QCheckBox, QScrollArea scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet(""" QScrollArea { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(44, 62, 80, 0.9), stop:1 rgba(52, 73, 94, 0.9)); border: 2px solid #34495e; border-radius: 10px; padding: 8px; } QScrollBar:vertical { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2c3e50, stop:1 #34495e); width: 14px; border-radius: 7px; margin: 2px; } QScrollBar::handle:vertical { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3498db, stop:1 #5dade2); border-radius: 7px; min-height: 25px; margin: 1px; } QScrollBar::handle:vertical:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5dade2, stop:1 #85c1e9); } """) # Widget conteneur pour les checkboxes scroll_widget = QWidget() scroll_layout = QVBoxLayout(scroll_widget) scroll_layout.setSpacing(10) scroll_layout.setContentsMargins(15, 15, 15, 15) # Créer les checkboxes avec le nouveau style amélioré self.checkboxes = [] for i, account in enumerate(self.accounts): try: email = account.get('email', 'Email inconnu') # Extraire le nom du fournisseur provider_name = self.extract_provider_from_site(account.get('server', '')) if not provider_name or provider_name == "Configuration manuelle": provider_name = account.get('provider', 'Manuel') # Nettoyer le nom du provider si c'est un serveur IMAP if provider_name and provider_name.startswith('imap.'): provider_name = provider_name.replace('imap.', '').replace('.com', '').title() # Créer la checkbox avec un texte informatif checkbox_text = f"🔐 {email} ({provider_name})" checkbox = QCheckBox(checkbox_text) # Style des checkboxes AMÉLIORÉ pour une meilleure lisibilité checkbox.setStyleSheet(""" QCheckBox { color: #ecf0f1; font-size: 13px; font-weight: 500; padding: 12px 15px; spacing: 10px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(52, 73, 94, 0.8), stop:1 rgba(44, 62, 80, 0.8)); border: 2px solid rgba(52, 152, 219, 0.3); border-radius: 8px; margin: 3px; min-height: 20px; } QCheckBox:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(52, 152, 219, 0.4), stop:1 rgba(41, 128, 185, 0.4)); border: 2px solid #3498db; color: white; transform: translateY(-1px); } QCheckBox:checked { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(39, 174, 96, 0.6), stop:1 rgba(46, 125, 50, 0.6)); border: 2px solid #27ae60; color: white; font-weight: bold; } QCheckBox:checked:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(46, 204, 113, 0.7), stop:1 rgba(39, 174, 96, 0.7)); border: 2px solid #2ecc71; } QCheckBox::indicator { width: 22px; height: 22px; border-radius: 5px; margin-right: 8px; } QCheckBox::indicator:unchecked { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #34495e, stop:1 #2c3e50); border: 2px solid #7f8c8d; } QCheckBox::indicator:unchecked:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5d6d7e, stop:1 #34495e); border: 2px solid #95a5a6; } QCheckBox::indicator:checked { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #27ae60, stop:1 #2ecc71); border: 2px solid #2ecc71; image: none; } QCheckBox::indicator:checked:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2ecc71, stop:1 #27ae60); border: 2px solid #58d68d; } """) # Associer les données du compte à la checkbox checkbox.setProperty("account_data", account) checkbox.setProperty("account_index", i) # Ajouter un tooltip informatif tooltip = f"Email: {email}\nFournisseur: {provider_name}\nServeur: {account.get('server', 'N/A')}" checkbox.setToolTip(tooltip) # Ajouter à la liste et au layout self.checkboxes.append(checkbox) scroll_layout.addWidget(checkbox) print(f"✅ Checkbox créée pour: {email}") except Exception as e: print(f"❌ Erreur création checkbox {i}: {e}") continue # Ajouter un espaceur flexible scroll_layout.addStretch() # Configurer le scroll area scroll_area.setWidget(scroll_widget) scroll_area.setMinimumHeight(180) main_layout.addWidget(scroll_area) # Boutons d'action en deux lignes # Première ligne : sélection selection_layout = QHBoxLayout() select_all_btn = QPushButton("✅ Tout sélectionner") select_all_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #27ae60, stop:1 #2ecc71); color: white; border: none; border-radius: 8px; padding: 12px 18px; font-weight: bold; font-size: 12px; min-height: 18px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2ecc71, stop:1 #58d68d); transform: translateY(-2px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1e8449, stop:1 #27ae60); transform: translateY(1px); } """) clear_all_btn = QPushButton("❌ Tout désélectionner") clear_all_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #e74c3c, stop:1 #c0392b); color: white; border: none; border-radius: 8px; padding: 12px 18px; font-weight: bold; font-size: 12px; min-height: 18px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #c0392b, stop:1 #e74c3c); transform: translateY(-2px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #a93226, stop:1 #c0392b); transform: translateY(1px); } """) # Fonctions de sélection avec messages de debug def select_all(): print("🔄 Sélection de toutes les checkboxes...") for cb in self.checkboxes: cb.setChecked(True) print(f"✅ {len(self.checkboxes)} checkboxes sélectionnées") def clear_all(): print("🔄 Désélection de toutes les checkboxes...") for cb in self.checkboxes: cb.setChecked(False) print("✅ Toutes les checkboxes désélectionnées") select_all_btn.clicked.connect(select_all) clear_all_btn.clicked.connect(clear_all) selection_layout.addWidget(select_all_btn) selection_layout.addWidget(clear_all_btn) main_layout.addLayout(selection_layout) # Deuxième ligne : validation validation_layout = QHBoxLayout() ok_btn = QPushButton("🚀 Confirmer sélection") ok_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3498db, stop:1 #2980b9); color: white; border: none; border-radius: 8px; padding: 15px 25px; font-weight: bold; font-size: 14px; min-height: 22px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5dade2, stop:1 #3498db); transform: translateY(-2px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2980b9, stop:1 #21618c); transform: translateY(1px); } """) cancel_btn = QPushButton("❌ Annuler") cancel_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #95a5a6, stop:1 #7f8c8d); color: white; border: none; border-radius: 8px; padding: 15px 25px; font-weight: bold; font-size: 14px; min-height: 22px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #7f8c8d, stop:1 #5d6d7e); transform: translateY(-2px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5d6d7e, stop:1 #4a5568); transform: translateY(1px); } """) ok_btn.clicked.connect(dialog.accept) cancel_btn.clicked.connect(dialog.reject) validation_layout.addWidget(cancel_btn) validation_layout.addWidget(ok_btn) main_layout.addLayout(validation_layout) # Afficher la fenêtre et traiter le résultat print("🚀 Affichage de la fenêtre de sélection...") result = dialog.exec_() if result == QDialog.Accepted: print("✅ Utilisateur a confirmé la sélection") # Récupérer les comptes sélectionnés self.selected_accounts = [] selected_count = 0 for i, checkbox in enumerate(self.checkboxes): if checkbox.isChecked(): account_data = checkbox.property("account_data") if account_data: self.selected_accounts.append(account_data) selected_count += 1 email = account_data.get('email', 'N/A') print(f"📧 Compte sélectionné {selected_count}: {email}") print(f"📊 Total sélectionné: {len(self.selected_accounts)} comptes") if self.selected_accounts: # Changer l'état du bouton principal self.button_state = "load" count = len(self.selected_accounts) # Mettre à jour le texte du bouton new_text = f"🚀 Charger ({count} sélectionné{'s' if count > 1 else ''})" self.select_load_btn.setText(new_text) # Changer le style du bouton pour l'état "charger" self.select_load_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #27ae60, stop:1 #2ecc71); border: none; border-radius: 6px; color: white; font-size: 12px; font-weight: bold; padding: 10px 15px; min-height: 14px; margin: 8px 2px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2ecc71, stop:1 #27ae60); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #229954, stop:1 #2ecc71); } """) # Créer le message informatif emails = [acc.get('email', 'N/A') for acc in self.selected_accounts] emails_list = "\n".join(f"• {email}" for email in emails) message_text = (f"✅ {count} boîte{'s' if count > 1 else ''} mail " f"sélectionnée{'s' if count > 1 else ''} !\n\n" f"📧 Comptes :\n{emails_list}\n\n" f"🚀 Cliquez sur 'Charger' pour les charger") self.test_result.setText(message_text) self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 2px solid #2e7d32; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) print(f"🎯 Bouton mis à jour: {new_text}") print(f"🔄 État du bouton: {self.button_state}") else: print("⚠️ Aucune sélection effectuée") QMessageBox.information(self, "Aucune sélection", "Aucune boîte mail sélectionnée.") else: print("❌ Utilisateur a annulé la sélection") except Exception as e: print(f"❌ Erreur dans select_mailboxes: {e}") import traceback traceback.print_exc() QMessageBox.critical(self, "Erreur", f"Erreur lors de la sélection des boîtes mail :\n\n{str(e)}") def load_selected_mailboxes(self): """Charge les boîtes mail sélectionnées""" if not self.selected_accounts: QMessageBox.warning(self, "Aucune sélection", "Aucune boîte mail sélectionnée.") return # Message de chargement count = len(self.selected_accounts) self.test_result.setText( f"⏳ Chargement de {count} boîte{'s' if count > 1 else ''} mail en cours...\n\nVeuillez patienter...") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(52, 152, 219, 0.2); border: 2px solid #3498db; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) # Forcer le rafraîchissement self.repaint() # Appeler la méthode test self.load_imap_accounts_data() # PRÉPARER LE COMPTE POUR L'INTERFACE PRINCIPALE if self.selected_accounts: first_account = self.selected_accounts[0] self.selected_account = ( first_account.get('email', ''), first_account.get('password', ''), first_account.get('server', first_account.get('provider', '')) ) # ============================================ # ⭐ AJOUTEZ CETTE LIGNE ICI - TRÈS IMPORTANT ⭐ # ============================================ self.accept() # Remettre le bouton en mode sélection self.reset_button_state() def reset_button_state(self): """Remet le bouton en mode sélection""" self.button_state = "select" self.selected_accounts = [] self.select_load_btn.setText("📬 Sélectionner et charger") self.select_load_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3498db, stop:1 #2980b9); border: none; border-radius: 6px; color: white; font-size: 12px; font-weight: bold; padding: 10px 15px; min-height: 14px; margin: 8px 2px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5dade2, stop:1 #3498db); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2980b9, stop:1 #21618c); } """) def load_imap_accounts_data(self): """ MÉTHODE CORRIGÉE - Récupère les boîtes mail sélectionnées et leurs dossiers """ try: if not self.selected_accounts: self.test_result.setText( "❌ Aucun compte sélectionné !\n\nVeuillez d'abord sélectionner des comptes à charger.") self._set_error_style() return False print(f"🚀 Début du chargement de {len(self.selected_accounts)} comptes") # Données préparées pour ImapHandler accounts_data = [] success_count = 0 error_count = 0 # Traiter chaque compte sélectionné for i, account in enumerate(self.selected_accounts): try: email = account.get('email', '') password = account.get('password', '') server = account.get('server', account.get('provider', '')) print(f"📧 Traitement du compte {i + 1}/{len(self.selected_accounts)}: {email}") # Message de progression self._update_progress_message(i + 1, len(self.selected_accounts), email) # Récupérer les dossiers de ce compte folders = self.get_imap_folders(email, password, server) if folders: account_data = { 'email': email, 'password': password, 'server': server, 'port': account.get('port', 993), 'ssl': account.get('ssl', True), 'folders': folders, 'name': account.get('name', f'Compte {email}'), 'source': account.get('source', 'password_manager') } accounts_data.append(account_data) success_count += 1 print(f"✅ Compte {email}: {len(folders)} dossiers trouvés") else: error_count += 1 print(f"❌ Compte {email}: Impossible de récupérer les dossiers") except Exception as account_error: error_count += 1 print(f"❌ Erreur pour le compte {email}: {account_error}") continue # Appeler ImapHandler CORRIGÉ if accounts_data: success = self._call_imap_handler_corrected(accounts_data) if success: self._set_success_message(success_count, error_count) return True else: self._set_error_message("Échec de la transmission à ImapHandler") return False else: self._set_error_message(f"Aucun compte chargé ({error_count} erreur(s))") return False except Exception as e: print(f"❌ Erreur générale dans load_imap_accounts_data: {e}") self._set_error_message(f"Erreur générale: {str(e)}") return False def _call_imap_handler_corrected(self, accounts_data): """ NOUVELLE MÉTHODE CORRIGÉE - Appel simplifié à ImapHandler """ try: print("🔄 Appel d'ImapHandler (version corrigée)...") print(f"📊 Nombre de comptes à traiter: {len(accounts_data)}") # Importer SEULEMENT la classe ImapHandler from modules.module_outlook_2021 import ImapHandler # APPROCHE CORRIGÉE: Une seule instance pour traiter tous les comptes # Utiliser le premier compte pour l'instance principale if accounts_data: first_account = accounts_data[0] handler = ImapHandler( email=first_account['email'], password=first_account['password'], provider=first_account['server'] ) # Utiliser la nouvelle méthode pour traiter tous les comptes results = handler.process_multiple_accounts(accounts_data) # Analyser les résultats success_rate = results['success'] / results['total'] if results['total'] > 0 else 0 print(f"📊 Résultats: {results['success']}/{results['total']} succès ({success_rate:.1%})") # Considérer comme succès si au moins 50% des comptes sont traités return success_rate >= 0.5 return False except ImportError as import_error: print(f"❌ Erreur d'importation: {import_error}") raise Exception(f"Impossible d'importer ImapHandler: {import_error}") except Exception as e: print(f"❌ Erreur dans _call_imap_handler_corrected: {e}") import traceback traceback.print_exc() raise Exception(f"Erreur ImapHandler: {e}") def _update_progress_message(self, current, total, email): """Met à jour le message de progression""" self.test_result.setText( f"⏳ Chargement en cours... ({current}/{total})\n\n" f"📧 Compte actuel: {email}\n" f"🔄 Récupération des dossiers..." ) self.repaint() def _set_success_message(self, success_count, error_count): """Affiche le message de succès""" self.test_result.setText( f"✅ Chargement terminé avec succès !\n\n" f"📊 Résultats:\n" f"• {success_count} compte(s) chargé(s)\n" f"• {error_count} erreur(s)\n\n" f"🚀 Données transmises à ImapHandler" ) self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 2px solid #2e7d32; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) def _set_error_message(self, message): """Affiche un message d'erreur""" self.test_result.setText(f"❌ {message}") self._set_error_style() def _set_error_style(self): """Applique le style d'erreur""" self.test_result.setStyleSheet(""" QLabel { background-color: rgba(231, 76, 60, 0.2); border: 2px solid #e74c3c; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) def get_imap_folders(self, email, password, server): """Récupère la liste des dossiers IMAP pour un compte donné""" try: import imaplib import ssl print(f"🔍 Connexion IMAP pour {email}...") # Connexion sécurisée IMAP mail = imaplib.IMAP4_SSL(server, 993) mail.login(email, password) # Récupérer la liste des dossiers status, folder_list = mail.list() if status == 'OK': folders = [] for folder_info in folder_list: # Décoder les informations du dossier folder_string = folder_info.decode('utf-8') # Extraire le nom du dossier (après le dernier espace) folder_name = folder_string.split('"')[-2] if '"' in folder_string else folder_string.split()[-1] # Nettoyer le nom du dossier if folder_name and folder_name != '': folders.append({ 'name': folder_name, 'full_info': folder_string, 'display_name': folder_name }) # Fermer la connexion mail.logout() print(f"✅ {len(folders)} dossiers trouvés pour {email}") return folders else: print(f"❌ Impossible de lister les dossiers pour {email}") return [] except Exception as e: print(f"❌ Erreur récupération dossiers pour {email}: {e}") return [] def quick_validate(self): """Validation rapide des champs avant test""" email = self.email_input.text().strip() password = self.password_input.text().strip() provider = self.provider_combo.currentText().strip() if not email: self.test_result.setText("❌ Veuillez saisir une adresse email") return if not password: self.test_result.setText("❌ Veuillez saisir un mot de passe") return if not provider or provider == "Autre...": self.test_result.setText("❌ Veuillez sélectionner un fournisseur IMAP") return # Validation format email if "@" not in email or "." not in email: self.test_result.setText("❌ Format d'email invalide") return self.test_result.setText( "✅ Validation réussie !\nTous les champs sont correctement remplis.\nVous pouvez tester la connexion.") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 2px solid #2e7d32; border-radius: 8px; padding: 12px; color: #ecf0f1; font-size: 12px; min-height: 120px; font-family: 'Courier New', monospace; } """) def auto_detect_provider(self): """Détecte automatiquement le fournisseur IMAP basé sur l'adresse email""" email = self.email_input.text().strip().lower() if not email or "@" not in email: return # Ne rien faire si l'email est vide ou invalide # Extraire le domaine try: domain = email.split("@")[1] except IndexError: return # Mapping des domaines vers les fournisseurs IMAP domain_to_provider = { # Gmail "gmail.com": "imap.gmail.com", # Microsoft (Outlook/Hotmail/Live) "outlook.com": "imap-mail.outlook.com", "hotmail.com": "imap-mail.outlook.com", "hotmail.fr": "imap-mail.outlook.com", "live.com": "imap-mail.outlook.com", "live.fr": "imap-mail.outlook.com", "msn.com": "imap-mail.outlook.com", # Yahoo "yahoo.com": "imap.mail.yahoo.com", "yahoo.fr": "imap.mail.yahoo.com", "yahoo.co.uk": "imap.mail.yahoo.com", "ymail.com": "imap.mail.yahoo.com", # Autres fournisseurs courants français "orange.fr": "Autre...", "wanadoo.fr": "Autre...", "free.fr": "Autre...", "sfr.fr": "Autre...", "bbox.fr": "Autre...", "laposte.net": "Autre...", "numericable.fr": "Autre...", "neuf.fr": "Autre...", } # Chercher le fournisseur correspondant provider = domain_to_provider.get(domain, "Autre...") # Mettre à jour le combo box seulement si c'est différent current_provider = self.provider_combo.currentText() if current_provider != provider: self.provider_combo.setCurrentText(provider) # Afficher un message informatif dans le test_result if provider != "Autre...": self.test_result.setText( f"✨ Fournisseur détecté automatiquement !\n\n📧 Email: {email}\n🌐 Fournisseur: {provider}\n\n💡 Vous pouvez maintenant tester la connexion") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(52, 152, 219, 0.2); border: 2px solid #3498db; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) else: self.test_result.setText( f"⚠️ Domaine non reconnu : {domain}\n\nVeuillez configurer manuellement le serveur IMAP.\n\n💡 Consultez la documentation de votre fournisseur d'email pour les paramètres IMAP") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(241, 196, 15, 0.2); border: 2px solid #f1c40f; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) def update_help_content(self): """Met à jour le contenu de l'aide selon le fournisseur sélectionné""" provider = self.provider_combo.currentText() if provider == "imap.gmail.com": self.help_text.setText("""

📧 Gmail :
• Activez l'authentification à 2 facteurs
• Créez un mot de passe d'application
• Serveur : imap.gmail.com
• Port : 993 (SSL)

🔗 Lien utile :
Créer un mot de passe Gmail

⚠️ Important :
Utilisez le mot de passe d'application,
JAMAIS votre mot de passe habituel !

🔧 Étapes rapides :
1. Activez la 2FA sur votre compte
2. Allez sur le lien ci-dessus
3. Générez un mot de passe d'application
4. Utilisez ce mot de passe ici

""") elif provider == "imap-mail.outlook.com": self.help_text.setText("""

📧 Outlook/Hotmail/Live :
• Activez l'authentification à 2 facteurs
• Créez un mot de passe d'application
• Serveur : imap-mail.outlook.com
• Port : 993 (SSL)

🔗 Lien utile :
Créer un mot de passe Outlook

⚠️ Important :
Utilisez le mot de passe d'application
généré par Microsoft !

🔧 Étapes rapides :
1. Connectez-vous à votre compte Microsoft
2. Activez la vérification en 2 étapes
3. Créez un mot de passe d'application
4. Nommez-le "IMAP Client"

""") elif provider == "imap.mail.yahoo.com": self.help_text.setText("""

📧 Yahoo :
• Activez l'authentification à 2 facteurs
• Créez un mot de passe d'application
• Serveur : imap.mail.yahoo.com
• Port : 993 (SSL)

🔗 Lien utile :
Créer un mot de passe Yahoo

⚠️ Important :
Sélectionnez "Mail" comme application
lors de la création !

🔧 Étapes rapides :
1. Activez la vérification en 2 étapes
2. Allez dans Sécurité > Mots de passe d'app
3. Générez un nouveau mot de passe
4. Choisissez "Mail" comme type

""") else: # Autre... - Détection intelligente du domaine # Récupérer le domaine de l'email pour donner des conseils spécifiques email = self.email_input.text().strip().lower() domain = "" if "@" in email: try: domain = email.split("@")[1] except: pass if domain in ["orange.fr", "wanadoo.fr"]: self.help_text.setText("""

📧 Orange/Wanadoo :
• Serveur : imap.orange.fr
• Port : 993 (SSL/TLS)
• Authentification : Normale
• Pas besoin de mot de passe d'application

🔗 Lien utile :
Configuration Orange

💡 Conseils :
• Utilisez votre mot de passe habituel
• Vérifiez que IMAP est activé
• Identifiant : votre adresse complète

📞 Support : 3900 (gratuit)

""") elif domain in ["free.fr"]: self.help_text.setText("""

📧 Free :
• Serveur : imap.free.fr
• Port : 993 (SSL/TLS)
• Authentification : Normale
• Activation requise dans l'espace abonné

🔗 Lien utile :
Configuration Free

⚠️ Important :
• Activez IMAP dans votre espace abonné
• Mot de passe = mot de passe Free
• Format : prenom.nom@free.fr

🌐 Espace abonné :
Mon compte Free

""") elif domain in ["sfr.fr"]: self.help_text.setText("""

📧 SFR :
• Serveur : imap.sfr.fr
• Port : 993 (SSL/TLS)
• Authentification : Normale
• Identifiant complet requis

🔗 Lien utile :
Configuration SFR

💡 Conseils :
• Utilisez votre identifiant SFR complet
• Format : ab123456@sfr.fr
• Mot de passe = mot de passe SFR

📞 Support : 1023 (gratuit)

""") elif domain in ["bbox.fr", "bouygues-telecom.fr"]: self.help_text.setText("""

📧 Bouygues Telecom :
• Serveur : imap.bbox.fr
• Port : 993 (SSL/TLS)
• Authentification : Normale

🔗 Lien utile :
Assistance Bouygues

💡 Conseils :
• Adresse complète comme identifiant
• Mot de passe de votre compte Bbox
• Activez IMAP si nécessaire

📞 Support : 1064 (gratuit)

""") elif domain in ["laposte.net"]: self.help_text.setText("""

📧 La Poste :
• Serveur : imap.laposte.net
• Port : 993 (SSL/TLS)
• Authentification : Normale

🔗 Lien utile :
Aide La Poste

💡 Conseils :
• Utilisez votre adresse complète
• Mot de passe de votre compte LaPoste
• Service gratuit et fiable

""") elif domain: # Domaine détecté mais non reconnu self.help_text.setText(f"""

📧 Domaine détecté : {domain}

🔧 Configuration manuelle :
• Consultez la documentation de {domain}
• Recherchez "paramètres IMAP {domain}"
• Vérifiez les paramètres serveur

⚙️ Paramètres courants :
• Serveur : imap.{domain}
• Port SSL : 993
• Port STARTTLS : 143
• Authentification : Normale

💡 Astuce :
Contactez le support de votre
fournisseur pour les paramètres exacts

🔍 Recherche suggérée :
Chercher les paramètres IMAP

""") else: # Aucun domaine détecté self.help_text.setText("""

📧 Configuration manuelle :
• Saisissez d'abord votre adresse email
• Le système détectera automatiquement
le fournisseur si possible

🔧 Paramètres IMAP courants :
• Port SSL/TLS : 993
• Port STARTTLS : 143
• Authentification : selon fournisseur

🌐 Fournisseurs supportés :
• Gmail, Outlook, Yahoo (auto-détectés)
• Orange, Free, SFR, Bouygues
• La Poste et autres

💡 Conseil :
Tapez votre email pour une
détection automatique !

""") def test_connection(self): """Teste la connexion IMAP avec les paramètres saisis""" import imaplib import ssl email = self.email_input.text().strip() password = self.password_input.text().strip() provider = self.provider_combo.currentText().strip() if not email or not password or not provider: self.test_result.setText("❌ Veuillez remplir tous les champs avant de tester") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(231, 76, 60, 0.2); border: 1px solid #e74c3c; border-radius: 6px; padding: 8px; color: #ecf0f1; font-size: 11px; min-height: 60px; } """) return self.test_result.setText("⏳ Test en cours...") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(52, 152, 219, 0.2); border: 1px solid #3498db; border-radius: 6px; padding: 8px; color: #ecf0f1; font-size: 11px; min-height: 60px; } """) # Forcer le rafraîchissement de l'interface self.repaint() try: # Connexion IMAP mail = imaplib.IMAP4_SSL(provider, 993) mail.login(email, password) mail.select('inbox') # Compter les messages status, messages = mail.search(None, 'ALL') message_count = len(messages[0].split()) if messages[0] else 0 mail.close() mail.logout() self.test_result.setText( f"✅ Connexion réussie !\n\n📧 Email: {email}\n🌐 Serveur: {provider}\n📬 Messages: {message_count}") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 1px solid #2e7d32; border-radius: 6px; padding: 8px; color: #ecf0f1; font-size: 11px; min-height: 60px; } """) except Exception as e: error_msg = str(e) if "authentication failed" in error_msg.lower(): self.test_result.setText( "❌ Échec de l'authentification\n\nVérifiez votre email et votre mot de passe d'application") elif "connection" in error_msg.lower(): self.test_result.setText( "❌ Erreur de connexion\n\nVérifiez votre connexion internet et le serveur IMAP") else: self.test_result.setText(f"❌ Erreur: {error_msg[:100]}...") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(231, 76, 60, 0.2); border: 1px solid #e74c3c; border-radius: 6px; padding: 8px; color: #ecf0f1; font-size: 11px; min-height: 60px; } """) def load_accounts(self): """Charge les comptes UNIQUEMENT depuis le fichier passwords.enc""" try: print("🔍 === CHARGEMENT DEPUIS passwords.enc UNIQUEMENT ===") self.account_list.clear() self.accounts = [] # === CHARGER DIRECTEMENT DEPUIS passwords.enc === try: password_manager_accounts = self.load_password_manager_accounts() if password_manager_accounts: for account in password_manager_accounts: account['source'] = 'password_manager' self.accounts.append(account) print(f"🔐 Comptes chargés: {len(password_manager_accounts)}") except Exception as e: print(f"❌ Erreur lecture gestionnaire: {e}") # === AFFICHAGE UNIFORME AVEC CADENAS === print(f"📋 Total comptes à afficher: {len(self.accounts)}") for i, account in enumerate(self.accounts): try: item = QListWidgetItem() # Format uniforme : 🔐 email (fournisseur) provider_name = self.extract_provider_from_site(account.get('server', '')) if not provider_name or provider_name == "Configuration manuelle": provider_name = account.get('provider', 'Manuel') if provider_name.startswith('imap.'): provider_name = provider_name.replace('imap.', '').replace('.com', '').title() display_text = f"🔐 {account['email']} ({provider_name})" tooltip_text = f"Email: {account['email']}\nFournisseur: {provider_name}\nServeur: {account.get('server', 'N/A')}\nModifié: {account.get('modified', 'N/A')}" item.setText(display_text) item.setToolTip(tooltip_text) item.setData(Qt.UserRole, account) # Couleur uniforme pour tous les comptes item.setBackground(QColor(46, 125, 50, 60)) # Vert léger uniforme self.account_list.addItem(item) except Exception as item_error: print(f"❌ Erreur ajout élément {i + 1}: {item_error}") print(f"🎯 RÉSULTAT: {len(self.accounts)} comptes affichés depuis passwords.enc") # Mettre à jour le compteur self.update_accounts_counter() except Exception as e: print(f"❌ ERREUR dans load_accounts: {e}") import traceback traceback.print_exc() self.accounts = [] def load_password_manager_accounts(self): """Charge les comptes IMAP directement depuis le fichier passwords.enc""" try: import os password_file = "passwords.enc" if not os.path.exists(password_file): print(f"📂 Fichier {password_file} introuvable") return [] print(f"📂 Fichier {password_file} trouvé") # Lire le fichier with open(password_file, 'r', encoding='utf-8') as f: encrypted_data = f.read() if not encrypted_data.strip(): print("📂 Fichier vide") return [] # Déchiffrer selon les différents niveaux de sécurité decrypted_data = self.decrypt_password_file(encrypted_data) if not decrypted_data: print("❌ Impossible de déchiffrer le fichier") return [] print(f"✅ Données déchiffrées: {len(decrypted_data)} entrées") # Filtrer les comptes IMAP imap_accounts = [] for entry in decrypted_data: if entry.get('category') == '📧 IMAP/Email': # Convertir au format attendu imap_account = { 'email': entry.get('email', ''), 'password': entry.get('password', ''), 'provider': self.extract_provider_from_site(entry.get('site', '')), 'server': self.extract_server_from_site(entry.get('site', '')), 'port': entry.get('imap_port', 993), 'ssl': entry.get('imap_ssl', True), 'name': entry.get('name', 'Compte IMAP'), 'notes': entry.get('notes', ''), 'modified': entry.get('modified', '') } imap_accounts.append(imap_account) print(f"📧 Compte IMAP trouvé: {imap_account['email']}") return imap_accounts except Exception as e: print(f"❌ Erreur lecture passwords.enc: {e}") import traceback traceback.print_exc() return [] def decrypt_password_file(self, encrypted_data): """Tente de déchiffrer le fichier passwords.enc selon différents niveaux""" import json # === ESSAYER NIVEAU 1 (JSON CLAIR) === try: data = json.loads(encrypted_data) print("🔓 Niveau 1: Données en clair détectées") return data except: pass # === ESSAYER NIVEAU 2 (OBFUSCATION BASE64) === try: import base64 # Décoder Base64 decoded = base64.b64decode(encrypted_data.encode()).decode('latin-1') # Rotation inverse (ROT13 modifié) deobfuscated = "" for char in decoded: deobfuscated += chr((ord(char) - 13) % 256) data = json.loads(deobfuscated) print("🔓 Niveau 2: Données obfusquées déchiffrées") return data except: pass # === ESSAYER NIVEAU 3 (CLEF MACHINE) === try: import base64 from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # Générer la clé machine machine_key = self.generate_machine_key() salt = b'machine_key_salt_2024_password_manager' kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=50000, ) key = base64.urlsafe_b64encode(kdf.derive(machine_key.encode())) # Déchiffrer f = Fernet(key) encrypted_bytes = base64.b64decode(encrypted_data.encode()) decrypted_bytes = f.decrypt(encrypted_bytes) data = json.loads(decrypted_bytes.decode()) print("🔓 Niveau 3: Données chiffrées avec clé machine déchiffrées") return data except Exception as e: print(f"⚠️ Niveau 3 échoué: {e}") pass # === NIVEAU 4 (MOT DE PASSE MAÎTRE) === # On ne peut pas le déchiffrer sans le mot de passe maître print("🔒 Niveau 4 détecté - Mot de passe maître requis") print("💡 Veuillez utiliser le gestionnaire de mots de passe pour accéder aux comptes") return [] def generate_machine_key(self): """Génère la même clé machine que le gestionnaire""" try: import hashlib import platform import os machine_info = ( platform.node() + platform.system() + platform.processor() + str(os.environ.get('USERNAME', '')) + str(os.environ.get('COMPUTERNAME', '')) ) machine_hash = hashlib.sha256(machine_info.encode()).hexdigest() return machine_hash[:32] + "_password_manager_2024" except Exception: return "fallback_machine_key_2024_default" def extract_provider_from_site(self, site): """Extrait le nom du fournisseur depuis le champ site""" try: if not site: return "Configuration manuelle" site_lower = site.lower() if 'gmail' in site_lower: return "Gmail" elif 'outlook' in site_lower or 'office365' in site_lower: return "Outlook/Hotmail" elif 'yahoo' in site_lower: return "Yahoo" elif 'orange' in site_lower: return "Orange" elif 'free' in site_lower: return "Free" elif 'sfr' in site_lower: return "SFR" elif 'bbox' in site_lower: return "Bouygues" elif 'laposte' in site_lower: return "La Poste" else: return "Configuration manuelle" except Exception: return "Configuration manuelle" def extract_server_from_site(self, site): """Extrait le serveur depuis le champ site (format serveur:port)""" try: if not site: return "" # Si le format est "serveur:port", extraire juste le serveur if ':' in site: return site.split(':')[0] else: return site except Exception: return "" def populate_fields(self, item): account = item.data(Qt.UserRole) self.email_input.setText(account["email"]) self.password_input.setText(account.get("password", "")) self.provider_combo.setCurrentText(account.get("provider", "")) def use_account(self): email = self.email_input.text().strip() password = self.password_input.text().strip() provider = self.provider_combo.currentText().strip() if not email or not password or not provider: QMessageBox.warning(self, "Champs manquants", "Veuillez remplir tous les champs.") return self.selected_account = (email, password, provider) self.accept() def add_account(self): """Ajoute un nouveau compte UNIQUEMENT dans passwords.enc""" email = self.email_input.text().strip() password = self.password_input.text().strip() provider = self.provider_combo.currentText().strip() if not email or not password or not provider: QMessageBox.warning(self, "Champs manquants", "Veuillez remplir tous les champs.") return try: # Vérifier que le compte n'existe pas déjà for account in self.accounts: if account.get('email', '').lower() == email.lower(): QMessageBox.warning(self, "Compte existant", f"Le compte '{email}' existe déjà dans le gestionnaire.") return # Créer la nouvelle entrée pour passwords.enc import datetime new_entry = { 'name': f"Compte IMAP - {email}", 'email': email, 'password': password, 'site': provider, # Serveur IMAP 'category': '📧 IMAP/Email', 'notes': f"Ajouté depuis l'interface IMAP le {datetime.datetime.now().strftime('%d/%m/%Y à %H:%M')}", 'imap_port': 993, 'imap_ssl': True, 'modified': datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S'), 'created': datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') } # Charger les données existantes existing_data = self.load_password_manager_full_data() if existing_data is None: existing_data = [] # Ajouter la nouvelle entrée existing_data.append(new_entry) # Sauvegarder dans passwords.enc success = self.save_new_account_to_password_manager(existing_data) if success: # Recharger la liste self.load_accounts() # Le compteur est mis à jour automatiquement par load_accounts() # Message de confirmation self.test_result.setText( f"✅ Compte ajouté avec succès !\n\n🔐 {email}\n\nLe compte a été enregistré dans le gestionnaire de mots de passe.\n\n💾 Sauvegarde effectuée.") self.test_result.setStyleSheet(""" QLabel { background-color: rgba(46, 125, 50, 0.2); border: 2px solid #2e7d32; border-radius: 8px; padding: 15px; color: #ecf0f1; font-size: 13px; min-height: 100px; font-family: 'Courier New', monospace; } """) # Vider les champs self.email_input.clear() self.password_input.clear() self.provider_combo.setCurrentIndex(0) else: QMessageBox.critical(self, "Erreur", "Impossible d'ajouter le compte au gestionnaire de mots de passe.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ajout du compte :\n\n{str(e)}") print(f"Erreur ajout compte: {e}") def get_selected_account(self): return self.selected_account def setup_modern_style(self): """Configure un style moderne pour la fenêtre de connexion IMAP""" try: # Style principal de la fenêtre self.setStyleSheet(""" QDialog { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1a2332, stop:1 #2c3e50); color: white; font-family: 'Segoe UI', Arial, sans-serif; border-radius: 12px; } QLabel { color: #ecf0f1; font-size: 12px; font-weight: 500; padding: 4px 0px; } QLineEdit { background-color: rgba(52, 73, 94, 0.8); border: 2px solid #34495e; border-radius: 8px; padding: 12px 15px; color: white; font-size: 13px; font-weight: 500; min-height: 20px; } QLineEdit:focus { border: 2px solid #3498db; background-color: rgba(52, 152, 219, 0.1); box-shadow: 0 0 10px rgba(52, 152, 219, 0.3); } QLineEdit::placeholder { color: #95a5a6; font-style: italic; } QComboBox { background-color: rgba(52, 73, 94, 0.8); border: 2px solid #34495e; border-radius: 8px; padding: 12px 15px; color: white; font-size: 13px; font-weight: 500; min-height: 20px; min-width: 200px; } QComboBox:focus { border: 2px solid #3498db; background-color: rgba(52, 152, 219, 0.1); } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 25px; border-left: 1px solid #34495e; border-top-right-radius: 8px; border-bottom-right-radius: 8px; background-color: #34495e; } QComboBox::down-arrow { image: none; border: 3px solid transparent; border-top: 6px solid #ecf0f1; width: 0px; height: 0px; margin: 4px; } QComboBox QAbstractItemView { background-color: #2c3e50; border: 1px solid #34495e; border-radius: 6px; color: white; selection-background-color: #3498db; padding: 4px; } QListWidget { background-color: rgba(44, 62, 80, 0.9); border: 2px solid #34495e; border-radius: 10px; color: white; font-size: 12px; padding: 8px; alternate-background-color: rgba(52, 73, 94, 0.5); } QListWidget::item { padding: 12px; border-radius: 6px; margin: 2px; background-color: rgba(52, 73, 94, 0.3); border: 1px solid transparent; } QListWidget::item:selected { background-color: rgba(52, 152, 219, 0.8); border: 1px solid #3498db; color: white; font-weight: bold; } QListWidget::item:hover { background-color: rgba(52, 152, 219, 0.4); border: 1px solid #5dade2; } QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3498db, stop:1 #2980b9); border: none; border-radius: 8px; color: white; font-size: 13px; font-weight: bold; padding: 12px 20px; min-height: 16px; min-width: 100px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5dade2, stop:1 #3498db); transform: translateY(-2px); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2980b9, stop:1 #21618c); transform: translateY(1px); } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } QListWidget::item[source="password_manager"] { border-left: 4px solid #27ae60; } QListWidget::item[source="json"] { border-left: 4px solid #3498db; } """) # Titre de la fenêtre avec icône self.setWindowTitle("🔐 Connexion IMAP - Configuration avancée") # Taille optimisée pour le nouveau design self.setFixedSize(900, 700) except Exception as e: print(f"Erreur lors de l'application du style: {e}") class SystemTaskDashboard(QDialog): """Tableau de bord pour la gestion des tâches système""" closed = pyqtSignal() def __init__(self, parent=None, system_monitor=None): super().__init__(parent) self.system_monitor = system_monitor self.setWindowTitle("Tableau de bord système") self.setMinimumSize(900, 800) self.issues_found = [] # Liste pour stocker les problèmes détectés lors du diagnostic self.current_selected_pid = None # Initialisation de OutlookHandler self.outlook_handler = None self.imap_handler = None # Log pour déboguer print("Initialisation du SystemTaskDashboard") if system_monitor: print(f"Méthodes disponibles dans system_monitor: {dir(system_monitor)}") # Style général optimisé self.setStyleSheet(""" QDialog, QTabWidget, QWidget { background-color: #1a2a3a; color: white; font-family: 'Segoe UI', Arial, sans-serif; } QTableWidget { background-color: #f4f8f9; alternate-background-color: #e7f1f5; gridline-color: #dfe6e9; color: #2d3436; font-size: 12px; border-radius: 4px; } QTableWidget::item:selected { background-color: #3498db; color: white; } QHeaderView::section { background-color: #2c3e50; color: white; padding: 6px; border: 1px solid #34495e; font-weight: bold; } QTabWidget::pane { border: 1px solid #34495e; background-color: #1a2a3a; border-radius: 4px; } QTabBar::tab { background-color: #2c3e50; color: white; padding: 8px 12px; margin-right: 2px; border-top-left-radius: 4px; border-top-right-radius: 4px; min-width: 100px; } QTabBar::tab:selected { background-color: #3498db; } QTabBar::tab:hover:!selected { background-color: #34495e; } QPushButton { background-color: #2980b9; color: white; border: none; padding: 6px 10px; border-radius: 4px; font-weight: bold; min-height: 22px; } QPushButton:hover { background-color: #3498db; transform: translateY(-1px); } QPushButton:pressed { background-color: #21618c; transform: translateY(1px); } QPushButton:disabled { background-color: #95a5a6; color: #ecf0f1; } QLabel { color: white; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 4px; padding: 4px; } QComboBox { background-color: #2c3e50; color: white; padding: 4px; border: 1px solid #34495e; border-radius: 4px; min-height: 20px; } QComboBox::drop-down { border: none; background-color: #34495e; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } QComboBox::down-arrow { image: none; border: 2px solid white; border-top: none; border-right: none; width: 6px; height: 6px; transform: rotate(-45deg); } QListWidget, QTreeWidget { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 4px; padding: 2px; } QProgressBar { border: 1px solid #2c3e50; border-radius: 4px; text-align: center; color: white; background-color: #34495e; font-weight: bold; } QProgressBar::chunk { background-color: #3498db; border-radius: 3px; } QCheckBox { color: white; spacing: 6px; } QCheckBox::indicator { width: 16px; height: 16px; border-radius: 3px; border: 1px solid #34495e; background-color: #2c3e50; } QCheckBox::indicator:checked { background-color: #3498db; border-color: #2980b9; } QCheckBox::indicator:checked::before { content: "✓"; color: white; font-weight: bold; } QGroupBox { color: white; border: 1px solid #34495e; border-radius: 6px; margin-top: 12px; font-weight: bold; padding-top: 6px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 0 8px; background-color: #1a2a3a; } QScrollBar:vertical { background-color: #2c3e50; width: 12px; border-radius: 6px; } QScrollBar::handle:vertical { background-color: #34495e; border-radius: 6px; min-height: 20px; } QScrollBar::handle:vertical:hover { background-color: #4a6278; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { border: none; background: none; } """) # Initialisation de l'interface self.setup_ui() # Timer pour mettre à jour les informations système self.timer = QTimer(self) self.timer.timeout.connect(self.update_tasks) self.timer.timeout.connect(self.update_system_resources) self.timer.start(2000) # Mise à jour toutes les 2 secondes # Initialisation différée d'OutlookHandler (après que l'interface soit prête) QTimer.singleShot(500, self.initialize_outlook_handler) # Mise à jour initiale QTimer.singleShot(100, self.initial_update) # Log de fin d'initialisation print("✅ SystemTaskDashboard initialisé avec succès") def initialize_outlook_handler(self): """Initialise OutlookHandler après l'interface""" try: # Éviter les doubles initialisations if hasattr(self, 'outlook_handler') and self.outlook_handler is not None: print("⚠️ OutlookHandler déjà initialisé, abandon") return print("🔄 Initialisation d'OutlookHandler...") # Vérifier que l'interface COM est prête if not hasattr(self, 'com_mode_label'): print("⚠️ Interface COM pas encore prête, report de l'initialisation...") QTimer.singleShot(500, self.initialize_outlook_handler) return # MockOutlookHandler amélioré avec gestion d'état cohérente class MockOutlookHandler: def __init__(self): # Charger le mode de connexion sauvegardé settings = QSettings("Snoown2k", "MyApp") self.permanent_connection = settings.value("outlook/permanent_connection", False, type=bool) self.outlook_connected = False self.connection_attempts = 0 def log_action(self, message): print(f"OutlookHandler: {message}") # Afficher dans le journal COM si disponible if hasattr(self, 'dashboard') and hasattr(self.dashboard, 'display_outlook_log'): self.dashboard.display_outlook_log(message) def check_connection(self): """Simulation d'une vérification de connexion avec logique réaliste""" try: # Simuler une tentative de connexion import random # Si déjà connecté, 90% de chance que ça marche encore if self.outlook_connected: success = random.random() > 0.1 else: # Si pas connecté, 70% de chance de se connecter success = random.random() > 0.3 self.outlook_connected = success return success except Exception: self.outlook_connected = False return False def connect(self): """ Connexion IMAP générique pour tous fournisseurs """ try: import imaplib import socket print(f"🔌 Connexion à {self.server}:{self.port} (SSL: {self.use_ssl})") # Timeout de connexion socket.setdefaulttimeout(30) # Connexion selon le type if self.use_ssl: self.connection = imaplib.IMAP4_SSL(self.server, self.port) print("🔒 Connexion SSL établie") else: self.connection = imaplib.IMAP4(self.server, self.port) print("🔓 Connexion établie") # Login print(f"👤 Login avec: {self.email}") self.connection.login(self.email, self.password) print("✅ Login réussi") return True except imaplib.IMAP4.error as e: print(f"❌ Erreur IMAP: {e}") return False except socket.gaierror as e: print(f"❌ Erreur DNS/serveur: {e}") return False except socket.timeout: print("❌ Timeout de connexion") return False except Exception as e: print(f"❌ Erreur connexion: {e}") return False def disconnect(self): """Simulation d'une déconnexion""" self.outlook_connected = False self.log_action("🔌 Déconnexion simulée") def set_dashboard_reference(self, dashboard): self.dashboard = dashboard def notify_connection_change(self): """Notifie le changement d'état au dashboard""" if hasattr(self, 'dashboard') and hasattr(self.dashboard, 'update_com_mode_label'): self.dashboard.update_com_mode_label() self.log_action(f"📡 État mis à jour: {'Connecté' if self.outlook_connected else 'Déconnecté'}") # Créer l'instance mock self.outlook_handler = MockOutlookHandler() # Établir la référence bidirectionnelle self.outlook_handler.set_dashboard_reference(self) print("✅ OutlookHandler simulé initialisé") # Mettre à jour l'affichage de l'état COM self.update_com_mode_label() # Afficher un message d'information dans le log COM (une seule fois) self.display_outlook_log("🔄 OutlookHandler simulé initialisé - Fonctions de test disponibles") except Exception as e: print(f"❌ Erreur initialisation OutlookHandler: {e}") self.outlook_handler = None if hasattr(self, 'outlook_log_display'): self.display_outlook_log(f"❌ Erreur initialisation: {e}") # ─────────────────────────────────────────────── # NOUVELLE MÉTHODE : INITIALISATION IMAP # ─────────────────────────────────────────────── def test_imap_connection(self): """Teste la connexion IMAP avec les paramètres sauvegardés""" try: # Récupérer les paramètres IMAP sauvegardés settings = QSettings("Snoown2k", "MyApp") email = settings.value("imap/email", "").strip() password = settings.value("imap/password", "") provider = settings.value("imap/provider", "Gmail") # Vérifier que les paramètres sont configurés if not email or not password: self.display_outlook_log("⚠️ Configuration IMAP incomplète") self.display_outlook_log("💡 Allez dans 'Config IMAP' pour configurer vos paramètres") return self.display_outlook_log(f"🔍 Test de connexion IMAP pour {email}") self.display_outlook_log(f"📧 Fournisseur: {provider}") # Créer le gestionnaire IMAP avec callback de log def log_callback(message): self.display_outlook_log(message) imap_handler = ImapHandler(email, password, provider, log_callback) # Tester la connexion if imap_handler.connect(): self.display_outlook_log("✅ Connexion IMAP réussie !") # Récupérer les dossiers folders = imap_handler.list_folders() if folders: self.display_outlook_log(f"📁 {len(folders)} dossier(s) trouvé(s) :") for folder in folders[:10]: # Limiter à 10 dossiers pour l'affichage self.display_outlook_log(f" {folder}") if len(folders) > 10: self.display_outlook_log(f" ... et {len(folders) - 10} autres dossiers") else: self.display_outlook_log("📂 Connexion réussie mais aucun dossier trouvé") # Déconnecter imap_handler.disconnect() else: self.display_outlook_log("❌ Échec de la connexion IMAP") self.display_outlook_log("💡 Vérifiez vos paramètres dans 'Config IMAP'") except Exception as e: self.display_outlook_log(f"❌ Erreur lors du test IMAP: {e}") import traceback traceback.print_exc() def display_outlook_log(self, message): """Affiche un message de log dans le journal Outlook COM""" try: # CORRECTION: Vérifier que le widget existe if not hasattr(self, 'outlook_log_display'): print(f"Log COM (widget pas prêt): {message}") return # Ajouter timestamp from datetime import datetime timestamp = datetime.now().strftime("%H:%M:%S") formatted_message = f"[{timestamp}] {message}" # Ajouter au journal self.outlook_log_display.append(formatted_message) # Auto-scroll vers le bas scrollbar = self.outlook_log_display.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) # Auto-clear si l'option est activée if hasattr(self, 'auto_clear_log_checkbox') and self.auto_clear_log_checkbox.isChecked(): QTimer.singleShot(20000, self.clear_old_logs) # Effacer après 20 secondes except Exception as e: print(f"Erreur affichage log Outlook: {e}") def clear_old_logs(self): """Efface les anciens logs du journal COM""" try: if hasattr(self, 'outlook_log_display'): self.outlook_log_display.clear() self.display_outlook_log("🧹 Journal effacé automatiquement") except Exception as e: print(f"Erreur effacement logs: {e}") def show_error_message(self, title, message): """Affiche un message d'erreur""" try: error_dialog = QMessageBox(self) error_dialog.setWindowTitle(title) error_dialog.setText(message) error_dialog.setIcon(QMessageBox.Critical) error_dialog.setStandardButtons(QMessageBox.Ok) # Journal de l'erreur pour débogage print(f"ERREUR - {title}: {message}") return error_dialog.exec_() except Exception as e: # Fallback si la boîte de dialogue échoue print(f"ERREUR CRITIQUE - Impossible d'afficher le message d'erreur: {str(e)}") print(f"Message original: {title} - {message}") def initial_update(self): """Effectue la mise à jour initiale des données après l'initialisation complète""" try: self.update_tasks() self.update_system_resources() except Exception as e: self.show_error_message("Erreur d'initialisation", str(e)) def closeEvent(self, event): """Gérer la fermeture de la fenêtre""" try: self.timer.stop() # Arrêter le timer avant de fermer self.closed.emit() # Émettre le signal pour informer le parent except Exception as e: print(f"Erreur lors de la fermeture: {str(e)}") finally: super().closeEvent(event) def setup_ui(self): """Initialise l'interface utilisateur principale""" try: layout = QVBoxLayout(self) # Onglets self.tab_widget = QTabWidget() # Onglet 1: Gestionnaire de tâches self.task_tab = QWidget() self.setup_task_tab() self.tab_widget.addTab(self.task_tab, "📊 Gestionnaire de tâches") # Onglet 2: Diagnostic & Réparation #self.diagnostic_tab = QWidget() #self.setup_diagnostic_tab() #self.tab_widget.addTab(self.diagnostic_tab, "🔧 Diagnostic & Réparation") # Onglet 3: Sauvegardes self.backup_tab = QWidget() self.setup_backup_tab() self.tab_widget.addTab(self.backup_tab, "💾 Sauvegardes") # Onglet 4: COM / IMAP self.setup_outlook_tab() self.tab_widget.addTab(self.outlook_tab, "📬 Connexion Emails") # Onglet 5: Autotest Système #self.autotest_tab = QWidget() #self.setup_autotest_tab() #self.tab_widget.addTab(self.autotest_tab, "🔬 Autotest MA2C_TA V1") # Onglet 6: Gestionnaire de mots de passe self.password_tab = QWidget() self.setup_password_tab() self.tab_widget.addTab(self.password_tab, "🔐 Gestionnaire de mots de passe") layout.addWidget(self.tab_widget) # Barre de statut en bas status_layout = QHBoxLayout() self.cpu_label = QLabel("CPU: 0%") self.ram_label = QLabel("RAM: 0%") self.disk_label = QLabel("Disque: 0%") self.last_update_label = QLabel("Dernière mise à jour: Jamais") for label in [self.cpu_label, self.ram_label, self.disk_label]: label.setStyleSheet("color: #3498db; font-weight: bold;") status_layout.addWidget(label) status_layout.addStretch() status_layout.addWidget(self.last_update_label) layout.addLayout(status_layout) except Exception as e: print(f"Erreur dans setup_ui: {str(e)}") self.show_error_message("Erreur d'interface", f"Impossible d'initialiser l'interface: {str(e)}") def setup_outlook_tab(self): """Configure l'onglet de connexion Outlook avec interface optimisée""" try: self.outlook_tab = QWidget() layout = QVBoxLayout(self.outlook_tab) layout.setSpacing(15) layout.setContentsMargins(15, 15, 15, 15) # === AFFICHEUR DE MODE ACTIF === self.connection_status_label = QLabel() self.connection_status_label.setAlignment(Qt.AlignCenter) self.connection_status_label.setStyleSheet( "font-weight: bold; font-size: 14px; color: #2ecc71; padding: 6px;") layout.addWidget(self.connection_status_label) # === GROUPE PARAMÈTRES OUTLOOK === options_group = QGroupBox("⚙️ Paramètres Outlook") options_group.setStyleSheet(""" QGroupBox { margin-top: 8px; padding: 10px; border: 2px solid #2980b9; border-radius: 6px; color: white; font-weight: bold; font-size: 13px; } """) options_layout = QVBoxLayout() # Sélecteur de méthode de connexion method_layout = QHBoxLayout() method_label = QLabel("Connexion Outlook :") method_label.setStyleSheet("color: #ecf0f1; font-size: 11px;") self.connection_method_combo = QComboBox() self.connection_method_combo.addItems(["COM", "IMAP"]) self.connection_method_combo.setStyleSheet("font-size: 11px; padding: 2px;") self.connection_method_combo.setToolTip("Choisissez la méthode de connexion Outlook") # Charger la méthode sauvegardée settings = QSettings("Snoown2k", "MyApp") saved_method = settings.value("outlook/connection_method", "COM") if isinstance(saved_method, str): index = self.connection_method_combo.findText(saved_method) if index != -1: self.connection_method_combo.setCurrentIndex(index) self.connection_method_combo.currentIndexChanged.connect(self.toggle_connection_panels) method_layout.addWidget(method_label) method_layout.addWidget(self.connection_method_combo) method_layout.addStretch() options_layout.addLayout(method_layout) options_group.setLayout(options_layout) layout.addWidget(options_group) # === GROUPE PARAMÈTRES COM === self.com_panel = QGroupBox("🔧 Paramètres COM Outlook") self.com_panel.setStyleSheet(options_group.styleSheet()) com_layout = QVBoxLayout() # Checkboxes de configuration COM self.permanent_connection_checkbox = QCheckBox("🔒 Connexion COM permanente") self.permanent_connection_checkbox.setToolTip("Maintient ou Désactive la connexion hors actions (Redémarrage MA2C nécessaire)") saved_value = settings.value("outlook/permanent_connection", False, type=bool) self.permanent_connection_checkbox.setChecked(saved_value) self.permanent_connection_checkbox.setStyleSheet("color: #ecf0f1; font-size: 11px; padding: 2px;") # Style pour le tooltip de la checkbox permanente self.permanent_connection_checkbox.setStyleSheet(""" QCheckBox { color: #ecf0f1; font-size: 11px; padding: 2px; } QToolTip { background-color: #2c3e50; color: #ecf0f1; border: 1px solid #34495e; border-radius: 4px; padding: 6px; font-size: 11px; font-weight: bold; } """) self.permanent_connection_checkbox.stateChanged.connect(self.toggle_connection_mode) com_layout.addWidget(self.permanent_connection_checkbox) self.auto_clear_log_checkbox = QCheckBox("🧹 Effacer automatiquement le journal après 20 secondes") self.auto_clear_log_checkbox.setChecked(False) self.auto_clear_log_checkbox.setStyleSheet("color: #ecf0f1; font-size: 11px; padding: 2px;") com_layout.addWidget(self.auto_clear_log_checkbox) # Label d'état de connexion self.com_mode_label = QLabel() self.com_mode_label.setAlignment(Qt.AlignCenter) self.com_mode_label.setMinimumHeight(35) self.com_mode_label.setMaximumHeight(35) com_layout.addWidget(self.com_mode_label) # === GROUPE ACTIONS (BOUTONS) === actions_group = QGroupBox("🔧 Actions COM & IMAP") actions_group.setStyleSheet(options_group.styleSheet()) actions_layout = QHBoxLayout() actions_layout.setSpacing(8) actions_layout.setContentsMargins(10, 15, 10, 10) # Création des boutons d'action self.test_com_button = QPushButton("🔍 Tester COM") self.test_com_button.setToolTip("Tester la connexion COM Outlook") self.test_com_button.setMinimumWidth(100) self.test_com_button.setMaximumWidth(120) self.test_imap_button = QPushButton("🔌 Tester Connexion IMAP") self.test_imap_button.clicked.connect(self.test_imap_connection) self.test_imap_button.setToolTip("Tester la connexion IMAP") self.test_imap_button.setMinimumWidth(100) self.test_imap_button.setMaximumWidth(120) self.config_imap_button = QPushButton("⚙️ Config IMAP") self.config_imap_button.setToolTip("Configurer les paramètres IMAP") self.config_imap_button.setMinimumWidth(100) self.config_imap_button.setMaximumWidth(120) self.sync_imap_button = QPushButton("🔄 Sync") self.sync_imap_button.setToolTip("Synchroniser avec le gestionnaire de mots de passe") self.sync_imap_button.setMinimumWidth(80) self.sync_imap_button.setMaximumWidth(100) # Style uniforme pour tous les boutons uniform_button_style = """ QPushButton { background-color: #3498db; color: white; font-size: 10px; font-weight: bold; padding: 6px 8px; border: none; border-radius: 4px; min-height: 24px; max-height: 28px; } QPushButton:hover { background-color: #2980b9; transform: translateY(-1px); } QPushButton:pressed { background-color: #21618c; transform: translateY(1px); } QPushButton:disabled { background-color: #95a5a6; color: #ecf0f1; } """ # Appliquer le style aux boutons for btn in [self.test_com_button, self.test_imap_button, self.config_imap_button, self.sync_imap_button]: btn.setStyleSheet(uniform_button_style) # Ajouter les boutons à la ligne horizontale actions_layout.addWidget(self.test_com_button) actions_layout.addWidget(self.test_imap_button) actions_layout.addWidget(self.config_imap_button) actions_layout.addWidget(self.sync_imap_button) actions_layout.addStretch() # Pousser les boutons vers la gauche actions_group.setLayout(actions_layout) com_layout.addWidget(actions_group) # === ACTIONS RAPIDES === quick_actions_layout = QHBoxLayout() quick_actions_layout.setSpacing(8) quick_label = QLabel("Actions rapides:") quick_label.setStyleSheet("color: #bdc3c7; font-size: 11px; font-weight: bold;") self.clear_log_button = QPushButton("🗑️") self.clear_log_button.setToolTip("Effacer le journal") self.clear_log_button.setStyleSheet(""" QPushButton { background-color: #34495e; color: white; border: none; border-radius: 15px; font-size: 12px; font-weight: bold; min-width: 30px; max-width: 30px; min-height: 30px; max-height: 30px; } QPushButton:hover { background-color: #4a6278; } """) quick_actions_layout.addWidget(quick_label) quick_actions_layout.addWidget(self.clear_log_button) quick_actions_layout.addStretch() com_layout.addLayout(quick_actions_layout) # === JOURNAL DES ÉVÉNEMENTS === log_label = QLabel("📄 Journal des événements COM") log_label.setStyleSheet("color: #bdc3c7; font-size: 12px; font-weight: bold;") com_layout.addWidget(log_label) self.outlook_log_display = QTextEdit() self.outlook_log_display.setReadOnly(True) self.outlook_log_display.setMinimumHeight(180) self.outlook_log_display.setMaximumHeight(300) self.outlook_log_display.setStyleSheet(""" QTextEdit { background-color: #1c2833; color: #ecf0f1; font-family: 'Consolas', 'Courier New', monospace; font-size: 10px; padding: 8px; border-radius: 4px; border: 1px solid #566573; selection-background-color: #3498db; } """) com_layout.addWidget(self.outlook_log_display) # Finaliser le panel COM self.com_panel.setLayout(com_layout) layout.addWidget(self.com_panel) # === CONNEXIONS DES ÉVÉNEMENTS === self.test_com_button.clicked.connect(self.test_com_connection) self.test_imap_button.clicked.connect(self.test_imap_connection) self.config_imap_button.clicked.connect(self.configure_imap_settings) self.sync_imap_button.clicked.connect(self.show_imap_sync_menu) # === BOUTON STRESS TEST COM === self.stress_test_com_button = QPushButton("⚡ Stress Test COM") self.stress_test_com_button.setToolTip("Stress test pour reproduire les crashs du module COM") self.stress_test_com_button.setMinimumWidth(120) self.stress_test_com_button.setMaximumWidth(140) # Style spécial pour le stress test (rouge/orange pour indiquer que c'est un test) stress_test_style = """ QPushButton { background-color: #e74c3c; color: white; border: none; padding: 6px 8px; border-radius: 4px; font-weight: bold; font-size: 10px; min-height: 24px; max-height: 28px; } QPushButton:hover { background-color: #c0392b; transform: translateY(-1px); } QPushButton:pressed { background-color: #a93226; transform: translateY(1px); } QPushButton:disabled { background-color: #95a5a6; color: #ecf0f1; } """ self.stress_test_com_button.setStyleSheet(stress_test_style) self.stress_test_com_button.clicked.connect(self.start_com_stress_test) # Ajouter le bouton stress test à la ligne des boutons d'action actions_layout.addWidget(self.stress_test_com_button) self.clear_log_button.clicked.connect(self.clear_outlook_log) # === TIMER DE MISE À JOUR === self.com_status_timer = QTimer() self.com_status_timer.timeout.connect(self.update_com_mode_label) self.com_status_timer.start(2000) # === FINALISATION === layout.addStretch() # Initialisation différée de l'OutlookHandler if not hasattr(self, 'outlook_handler') or self.outlook_handler is None: QTimer.singleShot(100, self.initialize_outlook_handler) # Appliquer la configuration initiale des panels QTimer.singleShot(200, self.toggle_connection_panels) print("✅ Onglet Outlook configuré avec succès") except Exception as e: print(f"❌ Erreur dans setup_outlook_tab: {str(e)}") self.show_error_message("Erreur", f"Impossible d'initialiser l'onglet Outlook: {str(e)}") def clear_outlook_log(self): """Efface le contenu du journal COM/IMAP""" try: if hasattr(self, 'outlook_log_display'): self.outlook_log_display.clear() self.display_outlook_log("🗑️ Journal effacé manuellement") print("✅ Journal Outlook effacé") except Exception as e: print(f"❌ Erreur lors de l'effacement du journal: {e}") def configure_imap_settings(self): """Ouvre une boîte de dialogue pour configurer les paramètres IMAP avec support multi-fournisseurs""" try: # === ÉTAPE 1: ESSAYER D'ABORD LE DIALOGUE DE SÉLECTION DE COMPTES === # Créer la référence au gestionnaire de mots de passe password_manager_ref = None if hasattr(self, 'password_data'): class TempPasswordManager: def __init__(self, password_data): self.password_data = password_data password_manager_ref = TempPasswordManager(self.password_data) self.display_outlook_log("🔗 Gestionnaire de mots de passe détecté") # Ouvrir le dialogue de sélection de comptes imap_dialog = ImapAccountDialog(self, password_manager=password_manager_ref) if imap_dialog.exec_() == QDialog.Accepted: selected = imap_dialog.get_selected_account() if selected: email, password, provider = selected # === SAUVEGARDE DU COMPTE SÉLECTIONNÉ === self.save_imap_configuration(email, password, provider, imap_dialog.get_selected_account_data()) self.display_outlook_log(f"✅ Compte IMAP sélectionné: {email}") return # Sortir ici si un compte a été sélectionné # === ÉTAPE 2: CONFIGURATION MANUELLE SI AUCUN COMPTE SÉLECTIONNÉ === self.display_outlook_log("📝 Ouverture de la configuration manuelle IMAP") self.show_manual_imap_configuration() except Exception as e: self.display_outlook_log(f"❌ Erreur configuration IMAP: {e}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la configuration IMAP:\n{str(e)}") def save_imap_configuration(self, email, password, provider, account_data=None): """Sauvegarde la configuration IMAP avec gestion intelligente des paramètres serveur""" try: settings = QSettings("Snoown2k", "MyApp") # Sauvegarder les paramètres de base settings.setValue("imap/email", email) settings.setValue("imap/password", password) settings.setValue("imap/provider", provider) # === DÉTERMINATION INTELLIGENTE DES PARAMÈTRES SERVEUR === server_configs = { "Gmail": {"server": "imap.gmail.com", "port": 993, "ssl": True}, "Outlook/Hotmail": {"server": "outlook.office365.com", "port": 993, "ssl": True}, "Yahoo": {"server": "imap.mail.yahoo.com", "port": 993, "ssl": True}, "Orange": {"server": "imap.orange.fr", "port": 993, "ssl": True}, "Free": {"server": "imap.free.fr", "port": 993, "ssl": True}, "SFR": {"server": "imap.sfr.fr", "port": 993, "ssl": True}, "Bouygues": {"server": "imap.bbox.fr", "port": 993, "ssl": True}, "La Poste": {"server": "imap.laposte.net", "port": 993, "ssl": True} } # Si on a des données de compte détaillées (du gestionnaire), les utiliser if account_data and account_data.get('source') == 'password_manager': settings.setValue("imap/server", account_data.get('server', 'imap.gmail.com')) settings.setValue("imap/port", account_data.get('port', 993)) settings.setValue("imap/use_ssl", account_data.get('ssl', True)) self.display_outlook_log(f"📡 Serveur: {account_data.get('server')} (depuis gestionnaire)") # Sinon utiliser la configuration prédéfinie du fournisseur elif provider in server_configs: config = server_configs[provider] settings.setValue("imap/server", config["server"]) settings.setValue("imap/port", config["port"]) settings.setValue("imap/use_ssl", config["ssl"]) self.display_outlook_log(f"📡 Serveur: {config['server']} (configuration {provider})") # Configuration par défaut si rien d'autre else: settings.setValue("imap/server", "imap.gmail.com") settings.setValue("imap/port", 993) settings.setValue("imap/use_ssl", True) self.display_outlook_log("📡 Serveur: Configuration par défaut (Gmail)") settings.sync() # Réinitialiser l'ImapHandler self.imap_handler = None # === PROPOSITION DE SYNCHRONISATION === if hasattr(self, 'password_data'): sync_reply = QMessageBox.question( self, "Synchronisation avec le gestionnaire", f"✅ Configuration IMAP sauvegardée pour {provider}\n\n" f"Email: {email}\n" f"Serveur: {settings.value('imap/server')}\n\n" f"Voulez-vous synchroniser ces paramètres avec le gestionnaire de mots de passe ?", QMessageBox.Yes | QMessageBox.No ) if sync_reply == QMessageBox.Yes: QTimer.singleShot(100, self.sync_imap_to_password_manager) else: QMessageBox.information(self, "Configuration sauvée", f"✅ Configuration IMAP sauvegardée\n\n" f"Email: {email}\n" f"Fournisseur: {provider}") self.display_outlook_log("✅ Configuration IMAP sauvegardée avec succès") except Exception as e: self.display_outlook_log(f"❌ Erreur sauvegarde: {e}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde: {str(e)}") def show_manual_imap_configuration(self): """Affiche le dialogue de configuration manuelle IMAP""" try: from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QLineEdit, QSpinBox, QCheckBox, \ QPushButton, QDialogButtonBox, QComboBox, QHBoxLayout, QLabel, QGroupBox dialog = QDialog(self) dialog.setWindowTitle("🔧 Configuration manuelle IMAP") dialog.setModal(True) dialog.setMinimumWidth(500) dialog.setStyleSheet(""" QDialog { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1a2332, stop:1 #2c3e50); color: white; font-family: 'Segoe UI', Arial, sans-serif; } QGroupBox { font-weight: bold; border: 2px solid #34495e; border-radius: 8px; margin-top: 10px; padding-top: 10px; color: white; } QLineEdit, QSpinBox, QComboBox { background-color: rgba(52, 73, 94, 0.8); border: 2px solid #34495e; border-radius: 6px; padding: 8px; color: white; font-size: 12px; } QLineEdit:focus, QSpinBox:focus, QComboBox:focus { border: 2px solid #3498db; background-color: rgba(52, 152, 219, 0.1); } QPushButton { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3498db, stop:1 #2980b9); border: none; border-radius: 6px; color: white; font-weight: bold; padding: 10px 16px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5dade2, stop:1 #3498db); } """) layout = QVBoxLayout(dialog) # === TITRE ET DESCRIPTION === title_label = QLabel("🔧 Configuration manuelle des paramètres IMAP") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #3498db; margin-bottom: 10px;") layout.addWidget(title_label) desc_label = QLabel("Configurez manuellement vos paramètres de connexion IMAP") desc_label.setStyleSheet("color: #bdc3c7; margin-bottom: 15px;") layout.addWidget(desc_label) # === GROUPE SÉLECTION FOURNISSEUR === provider_group = QGroupBox("🌐 Sélection du fournisseur") provider_layout = QVBoxLayout(provider_group) provider_combo = QComboBox() providers = { "Gmail": {"server": "imap.gmail.com", "port": 993, "ssl": True}, "Outlook/Hotmail": {"server": "outlook.office365.com", "port": 993, "ssl": True}, "Yahoo": {"server": "imap.mail.yahoo.com", "port": 993, "ssl": True}, "Orange": {"server": "imap.orange.fr", "port": 993, "ssl": True}, "Free": {"server": "imap.free.fr", "port": 993, "ssl": True}, "SFR": {"server": "imap.sfr.fr", "port": 993, "ssl": True}, "Bouygues": {"server": "imap.bbox.fr", "port": 993, "ssl": True}, "La Poste": {"server": "imap.laposte.net", "port": 993, "ssl": True}, "Configuration manuelle": {"server": "", "port": 993, "ssl": True} } provider_combo.addItems(list(providers.keys())) provider_layout.addWidget(QLabel("Choisissez votre fournisseur de messagerie :")) provider_layout.addWidget(provider_combo) layout.addWidget(provider_group) # === GROUPE PARAMÈTRES === params_group = QGroupBox("⚙️ Paramètres de connexion") form_layout = QFormLayout(params_group) # Créer les champs email_edit = QLineEdit() password_edit = QLineEdit() password_edit.setEchoMode(QLineEdit.Password) server_edit = QLineEdit() port_spin = QSpinBox() port_spin.setRange(1, 65535) port_spin.setValue(993) ssl_checkbox = QCheckBox() ssl_checkbox.setChecked(True) # Charger les valeurs existantes settings = QSettings("Snoown2k", "MyApp") email_edit.setText(settings.value("imap/email", "")) current_server = settings.value("imap/server", "imap.gmail.com") server_edit.setText(current_server) port_spin.setValue(settings.value("imap/port", 993, type=int)) ssl_checkbox.setChecked(settings.value("imap/use_ssl", True, type=bool)) # Déterminer le fournisseur actuel current_provider = "Configuration manuelle" for provider_name, config in providers.items(): if config["server"] == current_server: current_provider = provider_name break provider_combo.setCurrentText(current_provider) # === FONCTION DE MISE À JOUR AUTOMATIQUE === def update_provider_settings(): selected_provider = provider_combo.currentText() if selected_provider != "Configuration manuelle" and selected_provider in providers: # Configuration automatique config = providers[selected_provider] server_edit.setText(config["server"]) port_spin.setValue(config["port"]) ssl_checkbox.setChecked(config["ssl"]) # Désactiver les champs serveur server_edit.setEnabled(False) port_spin.setEnabled(False) ssl_checkbox.setEnabled(False) self.display_outlook_log(f"📧 Configuration automatique: {selected_provider}") else: # Configuration manuelle server_edit.setEnabled(True) port_spin.setEnabled(True) ssl_checkbox.setEnabled(True) self.display_outlook_log("⚙️ Configuration manuelle activée") provider_combo.currentTextChanged.connect(update_provider_settings) # Ajouter les champs au formulaire form_layout.addRow("📧 Email:", email_edit) form_layout.addRow("🔐 Mot de passe:", password_edit) form_layout.addRow("🌐 Serveur IMAP:", server_edit) form_layout.addRow("🔌 Port:", port_spin) form_layout.addRow("🔒 Utiliser SSL/TLS:", ssl_checkbox) layout.addWidget(params_group) # === BOUTON DE TEST === test_layout = QHBoxLayout() test_button = QPushButton("🧪 Tester la connexion") def test_connection(): email = email_edit.text().strip() password = password_edit.text().strip() provider = provider_combo.currentText() if not email or not password: QMessageBox.warning(dialog, "Erreur", "Veuillez remplir l'email et le mot de passe") return try: # Créer un gestionnaire IMAP pour le test def log_callback(message): self.display_outlook_log(message) imap_handler = ImapHandler(email, password, provider, log_callback) self.display_outlook_log(f"🔍 Test de connexion: {email} avec {provider}") if imap_handler.connect(): folders = imap_handler.list_folders() total_folders = len(folders) if folders else 0 self.display_outlook_log(f"✅ Test réussi! {total_folders} dossier(s) trouvé(s)") QMessageBox.information(dialog, "Test réussi", f"Connexion réussie !\n" f"Fournisseur: {provider}\n" f"Dossiers trouvés: {total_folders}") imap_handler.disconnect() else: self.display_outlook_log("❌ Test de connexion échoué") QMessageBox.warning(dialog, "Test échoué", "Impossible de se connecter au serveur IMAP.\n" "Vérifiez vos paramètres.") except Exception as e: self.display_outlook_log(f"❌ Erreur lors du test: {e}") QMessageBox.critical(dialog, "Erreur", f"Erreur lors du test: {str(e)}") test_button.clicked.connect(test_connection) test_layout.addWidget(test_button) test_layout.addStretch() layout.addLayout(test_layout) # === BOUTONS DE VALIDATION === buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) # Appliquer la configuration initiale update_provider_settings() # === EXÉCUTION DU DIALOGUE === if dialog.exec_() == QDialog.Accepted: # Sauvegarder la configuration manuelle email = email_edit.text().strip() password = password_edit.text().strip() provider = provider_combo.currentText() if email and password: # Créer des données de compte pour la sauvegarde account_data = { 'server': server_edit.text(), 'port': port_spin.value(), 'ssl': ssl_checkbox.isChecked(), 'source': 'manual' } self.save_imap_configuration(email, password, provider, account_data) else: QMessageBox.warning(dialog, "Erreur", "Email et mot de passe sont obligatoires") except Exception as e: self.display_outlook_log(f"❌ Erreur configuration manuelle: {e}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la configuration manuelle: {str(e)}") def toggle_connection_panels(self): """ Active ou désactive les sections COM selon la méthode sélectionnée. Affiche un indicateur de mode actif (IMAP ou COM). """ # Vérification que tous les widgets sont créés if not hasattr(self, 'com_mode_label') or not hasattr(self, 'test_com_button'): print("⚠️ Widgets pas encore créés, report de toggle_connection_panels") QTimer.singleShot(100, self.toggle_connection_panels) return mode = self.connection_method_combo.currentText() print(f"🔄 Basculement vers le mode: {mode}") # Debug if mode == "IMAP": print("📡 Activation du mode IMAP") # Debug # Ne PAS désactiver tout le panel COM, garder tout actif if hasattr(self, 'com_panel'): self.com_panel.setEnabled(True) print("✓ com_panel reste actif en mode IMAP") # Désactiver les éléments COM spécifiques if hasattr(self, 'permanent_connection_checkbox'): self.permanent_connection_checkbox.setEnabled(False) print("✓ permanent_connection_checkbox désactivé") # === GESTION DES BOUTONS IMAP (ACTIFS) === if hasattr(self, 'test_imap_button'): self.test_imap_button.setEnabled(True) self.test_imap_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; font-size: 11px; font-weight: bold; padding: 6px 10px; border: none; border-radius: 4px; min-width: 80px; max-width: 120px; min-height: 28px; max-height: 28px; } QPushButton:hover { background-color: #2ecc71; } QPushButton:pressed { background-color: #1e8449; } """) print("✓ test_imap_button activé et stylé") if hasattr(self, 'config_imap_button'): self.config_imap_button.setEnabled(True) self.config_imap_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; font-size: 11px; font-weight: bold; padding: 6px 10px; border: none; border-radius: 4px; min-width: 80px; max-width: 120px; min-height: 28px; max-height: 28px; } QPushButton:hover { background-color: #2ecc71; } QPushButton:pressed { background-color: #1e8449; } """) print("✓ config_imap_button activé et stylé") # === GESTION DES BOUTONS COM (DÉSACTIVÉS) === if hasattr(self, 'test_com_button'): self.test_com_button.setEnabled(False) self.test_com_button.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: #ecf0f1; font-size: 11px; font-weight: bold; padding: 6px 10px; border: none; border-radius: 4px; min-width: 80px; max-width: 120px; min-height: 28px; max-height: 28px; } """) print("✓ test_com_button désactivé et grisé") # === AFFICHAGE DE L'ÉTAT === self.connection_status_label.setText("📡 Mode IMAP activé") self.connection_status_label.setStyleSheet("color: #2ecc71; font-size: 13px; font-weight: bold;") if hasattr(self, 'com_mode_label'): self.com_mode_label.setText("📡 Connexion IMAP : Actif") self.com_mode_label.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f2c3f, stop:1 #2c5f2d); color: #00ff88; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #27ae60; """) print("✓ com_mode_label mis à jour pour affichage IMAP") # === GESTION DU BOUTON STRESS TEST COM === if hasattr(self, 'stress_test_com_button'): if mode == "IMAP": # Désactiver le stress test en mode IMAP self.stress_test_com_button.setEnabled(False) self.stress_test_com_button.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: #ecf0f1; font-size: 10px; font-weight: bold; padding: 6px 8px; border: none; border-radius: 4px; min-width: 80px; max-width: 140px; min-height: 28px; max-height: 28px; } """) print("✓ stress_test_com_button désactivé en mode IMAP") elif mode == "COM": # Activer le stress test en mode COM self.stress_test_com_button.setEnabled(True) stress_test_style = """ QPushButton { background-color: #e74c3c; color: white; border: none; padding: 6px 8px; border-radius: 4px; font-weight: bold; font-size: 10px; min-height: 24px; max-height: 28px; min-width: 80px; max-width: 140px; } QPushButton:hover { background-color: #c0392b; transform: translateY(-1px); } QPushButton:pressed { background-color: #a93226; transform: translateY(1px); } """ self.stress_test_com_button.setStyleSheet(stress_test_style) print("✓ stress_test_com_button activé en mode COM") # Journal if hasattr(self, 'outlook_log_display'): self.outlook_log_display.append("📡 Mode IMAP activé - Boutons IMAP activés, COM désactivés") elif mode == "COM": print("🔌 Activation du mode COM") # Debug # Activer la section COM if hasattr(self, 'com_panel'): self.com_panel.setEnabled(True) print("✓ com_panel activé") # Activer les éléments COM spécifiques if hasattr(self, 'permanent_connection_checkbox'): self.permanent_connection_checkbox.setEnabled(True) print("✓ permanent_connection_checkbox activé") # === GESTION DES BOUTONS COM (ACTIFS) === if hasattr(self, 'test_com_button'): self.test_com_button.setEnabled(True) self.test_com_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; font-size: 11px; font-weight: bold; padding: 6px 10px; border: none; border-radius: 4px; min-width: 80px; max-width: 120px; min-height: 28px; max-height: 28px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #21618c; } """) print("✓ test_com_button activé et stylé") # === GESTION DES BOUTONS IMAP (DÉSACTIVÉS) === if hasattr(self, 'test_imap_button'): self.test_imap_button.setEnabled(False) self.test_imap_button.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: #ecf0f1; font-size: 11px; font-weight: bold; padding: 6px 10px; border: none; border-radius: 4px; min-width: 80px; max-width: 120px; min-height: 28px; max-height: 28px; } """) print("✓ test_imap_button désactivé et grisé") if hasattr(self, 'config_imap_button'): self.config_imap_button.setEnabled(False) self.config_imap_button.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: #ecf0f1; font-size: 11px; font-weight: bold; padding: 6px 10px; border: none; border-radius: 4px; min-width: 80px; max-width: 120px; min-height: 28px; max-height: 28px; } """) print("✓ config_imap_button désactivé et grisé") # === AFFICHAGE DE L'ÉTAT === self.connection_status_label.setText("🔌 Mode COM activé") self.connection_status_label.setStyleSheet("color: #f39c12; font-size: 13px; font-weight: bold;") # Le label de mode COM sera mis à jour par le timer existant print("✓ Mode COM activé, le timer gérera l'affichage du statut") # Journal if hasattr(self, 'outlook_log_display'): self.outlook_log_display.append("🔌 Mode COM activé - Boutons COM activés, IMAP désactivés") else: # Mode inconnu self.connection_status_label.setText("❓ Mode inconnu") self.connection_status_label.setStyleSheet("color: red; font-size: 13px; font-weight: bold;") print(f"❌ Mode inconnu: {mode}") # Sauvegarde immédiate du mode choisi QSettings("Snoown2k", "MyApp").setValue("outlook/connection_method", mode) print(f"💾 Mode {mode} sauvegardé") # Debug def save_connection_method(self): selected_method = self.connection_method_combo.currentText() QSettings("Snoown2k", "MyApp").setValue("outlook/connection_method", selected_method) self.log_action(f"💾 Mode de connexion sauvegardé : {selected_method}") def setup_autotest_tab(self): """Configure l'onglet d'autotest système""" try: layout = QVBoxLayout(self.autotest_tab) layout.setSpacing(15) layout.setContentsMargins(15, 15, 15, 15) # Titre et description title_label = QLabel("Autotest MA2C_TA V1") title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px; color: white;") desc_label = QLabel( "Lancez un diagnostic complet de l'application et ses interactions pour identifier et résoudre automatiquement le(s) problème(s)." ) desc_label.setWordWrap(True) desc_label.setStyleSheet("color: #bdc3c7; margin-bottom: 15px;") # Boutons d'action buttons_layout = QHBoxLayout() self.autotest_start_button = QPushButton("🚀 Lancer l'autotest complet") self.autotest_start_button.setMinimumWidth(220) self.autotest_start_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; border: none; padding: 12px 16px; border-radius: 6px; font-weight: bold; font-size: 13px; } QPushButton:hover { background-color: #2ecc71; } QPushButton:disabled { background-color: #7f8c8d; } """) self.autotest_start_button.clicked.connect(self.start_autotest) self.autotest_repair_button = QPushButton("🔧 Réparer les problèmes") self.autotest_repair_button.setMinimumWidth(200) self.autotest_repair_button.setEnabled(False) self.autotest_repair_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; border: none; padding: 12px 16px; border-radius: 6px; font-weight: bold; font-size: 13px; } QPushButton:hover { background-color: #c0392b; } QPushButton:disabled { background-color: #7f8c8d; } """) self.autotest_repair_button.clicked.connect(self.start_autotest_repair) # NOUVEAU: Créer le bouton de nettoyage système self.cleanup_button = QPushButton("🧹 Nettoyage Système") self.cleanup_button.setMinimumWidth(180) self.cleanup_button.setStyleSheet(""" QPushButton { background-color: #f39c12; color: white; border: none; padding: 12px 16px; border-radius: 6px; font-weight: bold; font-size: 13px; } QPushButton:hover { background-color: #e67e22; } """) self.cleanup_button.clicked.connect(self.show_system_cleanup_dialog) # Ajouter les boutons au layout buttons_layout.addWidget(self.autotest_start_button) buttons_layout.addWidget(self.autotest_repair_button) buttons_layout.addWidget(self.cleanup_button) # NOUVEAU bouton ajouté ici buttons_layout.addStretch() # Zone de progression progress_group = QGroupBox("Progression des tests") progress_group.setStyleSheet(""" QGroupBox { color: white; border: 1px solid #34495e; border-radius: 6px; margin-top: 12px; font-weight: bold; padding-top: 6px; } """) progress_layout = QVBoxLayout(progress_group) self.autotest_status_label = QLabel("Statut: Prêt à lancer les tests") self.autotest_status_label.setStyleSheet("color: #3498db; font-weight: bold;") self.autotest_progress_bar = QProgressBar() self.autotest_progress_bar.setRange(0, 100) self.autotest_progress_bar.setValue(0) self.autotest_progress_bar.setStyleSheet(""" QProgressBar { border: 2px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #34495e; color: white; height: 25px; font-weight: bold; } QProgressBar::chunk { background-color: #3498db; border-radius: 3px; } """) progress_layout.addWidget(self.autotest_status_label) progress_layout.addWidget(self.autotest_progress_bar) # Zone de résultats results_group = QGroupBox("Résultats des tests") results_group.setStyleSheet(""" QGroupBox { color: white; border: 1px solid #34495e; border-radius: 6px; margin-top: 12px; font-weight: bold; padding-top: 6px; } """) results_layout = QVBoxLayout(results_group) self.autotest_output = QTextEdit() self.autotest_output.setReadOnly(True) self.autotest_output.setMinimumHeight(300) self.autotest_output.setStyleSheet(""" QTextEdit { background-color: #1c2833; color: #ecf0f1; font-family: 'Consolas', 'Courier New', monospace; font-size: 11px; padding: 8px; border-radius: 4px; border: 1px solid #566573; selection-background-color: #3498db; } """) self.autotest_output.setHtml( "➡️ Cliquez sur \"Lancer l'autotest complet\" pour analyser votre système." ) results_layout.addWidget(self.autotest_output) # Assembler le layout layout.addWidget(title_label) layout.addWidget(desc_label) layout.addLayout(buttons_layout) layout.addWidget(progress_group) layout.addWidget(results_group) # Initialiser le système d'autotest self.autotest_system = None self.autotest_results = None except Exception as e: print(f"Erreur dans setup_autotest_tab: {str(e)}") self.show_error_message("Erreur", f"Impossible d'initialiser l'onglet d'autotest: {str(e)}") def add_cleanup_button_to_autotest(self): """Ajoute le bouton de nettoyage système à l'interface d'autotest""" try: # Créer le bouton de nettoyage (à ajouter après les boutons existants) self.cleanup_button = QPushButton("🧹 Nettoyage Système") self.cleanup_button.setStyleSheet(""" QPushButton { background-color: #f39c12; color: white; border: none; padding: 12px 20px; border-radius: 6px; font-weight: bold; font-size: 12px; min-width: 150px; } QPushButton:hover { background-color: #e67e22; } """) self.cleanup_button.clicked.connect(self.show_system_cleanup_dialog) # L'ajouter au layout des boutons existants # (vous devrez adapter selon votre structure de layout) # Par exemple : self.buttons_layout.addWidget(self.cleanup_button) except Exception as e: logger.error(f"Erreur lors de l'ajout du bouton de nettoyage: {str(e)}") def show_system_cleanup_dialog(self): """Affiche la fenêtre de nettoyage système""" try: # Analyser l'état du système cleanup_data = self.analyze_system_cleanup() # Créer la fenêtre de dialogue dialog = QDialog(self) dialog.setWindowTitle("🧹 Nettoyage Système - MA2C") dialog.setMinimumSize(900, 700) dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; font-size: 12px; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 5px; padding: 10px; font-family: Consolas, monospace; font-size: 11px; } QPushButton { background-color: #3498db; color: white; border: none; padding: 10px 15px; border-radius: 5px; font-weight: bold; font-size: 11px; min-width: 120px; } QPushButton:hover { background-color: #2980b9; } QPushButton[cleanup="danger"] { background-color: #e74c3c; } QPushButton[cleanup="danger"]:hover { background-color: #c0392b; } QPushButton[cleanup="success"] { background-color: #27ae60; } QPushButton[cleanup="success"]:hover { background-color: #229954; } QGroupBox { font-weight: bold; border: 2px solid #34495e; border-radius: 5px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; } QProgressBar { border: 2px solid #34495e; border-radius: 5px; text-align: center; background-color: #2c3e50; color: white; height: 20px; } QProgressBar::chunk { background-color: #3498db; border-radius: 3px; } """) # Layout principal main_layout = QVBoxLayout(dialog) main_layout.setSpacing(15) # En-tête header_label = QLabel("🧹 Analyse et nettoyage des fichiers temporaires du système") header_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #3498db; margin-bottom: 15px;") main_layout.addWidget(header_label) # Résumé rapide summary_text = self.create_cleanup_summary(cleanup_data) summary_label = QLabel(summary_text) summary_label.setStyleSheet( "background-color: rgba(52, 73, 94, 0.3); padding: 10px; border-radius: 5px; margin-bottom: 15px;") main_layout.addWidget(summary_label) # Layout horizontal pour les sections sections_layout = QHBoxLayout() # Section gauche - Liste des dossiers left_panel = self.create_cleanup_left_panel(cleanup_data) sections_layout.addWidget(left_panel) # Section droite - Détails et actions right_panel = self.create_cleanup_right_panel(cleanup_data) sections_layout.addWidget(right_panel) main_layout.addLayout(sections_layout) # Boutons d'action en bas buttons_layout = QHBoxLayout() refresh_button = QPushButton("🔄 Actualiser l'analyse") refresh_button.clicked.connect(lambda: self.refresh_cleanup_analysis(dialog, auto_reopen=True)) clean_selected_button = QPushButton("🧹 Nettoyer la sélection") clean_selected_button.setProperty("cleanup", "danger") clean_selected_button.clicked.connect(lambda: self.clean_selected_items(dialog)) clean_all_button = QPushButton("🧹 Tout nettoyer") clean_all_button.setProperty("cleanup", "danger") clean_all_button.clicked.connect(lambda: self.clean_all_items(cleanup_data, dialog)) close_button = QPushButton("✅ Fermer") close_button.setProperty("cleanup", "success") # CORRECTION: Utiliser une lambda pour capturer la référence correcte self.cleanup_dialog_force_close = False # Flag pour forcer la fermeture close_button.clicked.connect(lambda: self.force_close_cleanup_dialog()) buttons_layout.addWidget(refresh_button) buttons_layout.addWidget(clean_selected_button) buttons_layout.addWidget(clean_all_button) buttons_layout.addStretch() buttons_layout.addWidget(close_button) main_layout.addLayout(buttons_layout) # Stocker les données pour référence self.current_cleanup_data = cleanup_data self.cleanup_dialog = dialog dialog.exec_() except Exception as e: logger.error(f"Erreur lors de l'affichage du nettoyage système: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'affichage du nettoyage: {str(e)}") def force_close_cleanup_dialog(self): """Force la fermeture de la fenêtre de nettoyage sans réouverture""" try: self.cleanup_dialog_force_close = True if hasattr(self, 'cleanup_dialog') and self.cleanup_dialog: self.cleanup_dialog.close() except Exception as e: logger.error(f"Erreur lors de la fermeture forcée: {str(e)}") # SOLUTION 2: Alternative - Créer une méthode dédiée pour fermer def close_cleanup_dialog(self): """Ferme la fenêtre de nettoyage""" try: if hasattr(self, 'cleanup_dialog') and self.cleanup_dialog: self.cleanup_dialog.close() except Exception as e: logger.error(f"Erreur lors de la fermeture: {str(e)}") def analyze_system_cleanup(self): """Analyse les dossiers et fichiers à nettoyer""" cleanup_data = { 'folders': [], 'total_size': 0, 'total_files': 0, 'analysis_time': datetime.now().strftime("%d/%m/%Y %H:%M:%S") } # Dossiers à analyser folders_to_check = [ { 'path': r'C:\AUTOLOOK\log', 'name': 'Logs système', 'description': 'Fichiers de logs de l\'application', 'type': 'logs' }, { 'path': r'C:\AUTOLOOK\debug', 'name': 'Fichiers de debug', 'description': 'Fichiers de débogage et traces d\'erreur', 'type': 'debug' }, { 'path': r'C:\AUTOLOOK\modules\__pycache__', 'name': 'Cache Python', 'description': 'Fichiers de cache Python compilés (.pyc)', 'type': 'cache' }, { 'path': r'C:\AUTOLOOK\modules\browser_cache\Cache', 'name': 'Cache navigateur', 'description': 'Cache du navigateur intégré', 'type': 'browser_cache' } ] for folder_info in folders_to_check: folder_data = self.analyze_folder(folder_info) cleanup_data['folders'].append(folder_data) cleanup_data['total_size'] += folder_data['size'] cleanup_data['total_files'] += folder_data['file_count'] return cleanup_data def analyze_folder(self, folder_info): """Analyse un dossier spécifique""" folder_data = { 'path': folder_info['path'], 'name': folder_info['name'], 'description': folder_info['description'], 'type': folder_info['type'], 'exists': False, 'size': 0, 'file_count': 0, 'files': [], 'error': None } try: folder_path = Path(folder_info['path']) if folder_path.exists(): folder_data['exists'] = True # Analyser tous les fichiers du dossier for file_path in folder_path.rglob('*'): if file_path.is_file(): try: file_size = file_path.stat().st_size file_modified = datetime.fromtimestamp(file_path.stat().st_mtime) folder_data['files'].append({ 'name': file_path.name, 'path': str(file_path), 'size': file_size, 'modified': file_modified, 'relative_path': str(file_path.relative_to(folder_path)) }) folder_data['size'] += file_size folder_data['file_count'] += 1 except Exception as e: logger.warning(f"Erreur lors de l'analyse du fichier {file_path}: {str(e)}") continue except Exception as e: folder_data['error'] = str(e) logger.error(f"Erreur lors de l'analyse du dossier {folder_info['path']}: {str(e)}") return folder_data def create_cleanup_summary(self, cleanup_data): """Crée le résumé de l'analyse""" total_size_mb = cleanup_data['total_size'] / (1024 * 1024) existing_folders = sum(1 for folder in cleanup_data['folders'] if folder['exists']) summary = f"📊 Analyse terminée le {cleanup_data['analysis_time']}\n" summary += f"📁 {existing_folders}/{len(cleanup_data['folders'])} dossiers trouvés | " summary += f"📄 {cleanup_data['total_files']} fichiers | " summary += f"💾 {total_size_mb:.1f} MB à nettoyer" return summary def create_cleanup_left_panel(self, cleanup_data): """Crée le panel gauche avec la liste des dossiers""" left_panel = QGroupBox("📁 Dossiers à analyser") left_layout = QVBoxLayout(left_panel) # Liste des dossiers avec cases à cocher self.cleanup_list = QListWidget() self.cleanup_list.setStyleSheet(""" QListWidget { background-color: #2c3e50; border: 1px solid #34495e; border-radius: 5px; font-size: 11px; } QListWidget::item { padding: 10px; border-bottom: 1px solid #34495e; } QListWidget::item:selected { background-color: #3498db; } """) for folder in cleanup_data['folders']: size_mb = folder['size'] / (1024 * 1024) if folder['size'] > 0 else 0 if folder['exists']: status_icon = "✅" if folder['file_count'] > 0 else "📂" item_text = f"{status_icon} {folder['name']}\n" item_text += f"📄 {folder['file_count']} fichiers | 💾 {size_mb:.1f} MB\n" item_text += f"📍 {folder['path']}" else: item_text = f"❌ {folder['name']}\n📍 Dossier introuvable: {folder['path']}" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, folder) # Couleur selon le statut if not folder['exists']: item.setBackground(QColor(231, 76, 60, 50)) # Rouge transparent elif folder['file_count'] == 0: item.setBackground(QColor(149, 165, 166, 50)) # Gris transparent else: item.setBackground(QColor(241, 196, 15, 50)) # Orange transparent self.cleanup_list.addItem(item) self.cleanup_list.currentItemChanged.connect(self.on_cleanup_folder_selected) left_layout.addWidget(self.cleanup_list) # Sélectionner le premier élément if self.cleanup_list.count() > 0: self.cleanup_list.setCurrentRow(0) return left_panel def create_cleanup_right_panel(self, cleanup_data): """Crée le panel droit avec les détails du dossier sélectionné""" try: right_panel = QGroupBox("📋 Détails du dossier sélectionné") right_layout = QVBoxLayout(right_panel) right_layout.setSpacing(10) right_layout.setContentsMargins(15, 15, 15, 15) # === SECTION INFORMATIONS DU DOSSIER === self.folder_info_label = QLabel("Sélectionnez un dossier pour voir les détails") self.folder_info_label.setStyleSheet(""" QLabel { background-color: rgba(52, 73, 94, 0.3); color: #ecf0f1; padding: 12px; border-radius: 6px; border: 1px solid #566573; font-size: 12px; margin-bottom: 10px; } """) self.folder_info_label.setWordWrap(True) right_layout.addWidget(self.folder_info_label) # === SECTION LISTE DES FICHIERS === files_section = QGroupBox("📄 Contenu du dossier") files_section.setStyleSheet(""" QGroupBox { font-weight: bold; border: 1px solid #566573; border-radius: 5px; margin-top: 10px; padding-top: 10px; color: #ecf0f1; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #3498db; } """) files_layout = QVBoxLayout(files_section) self.files_text = QTextEdit() self.files_text.setReadOnly(True) self.files_text.setMinimumHeight(180) self.files_text.setMaximumHeight(250) self.files_text.setStyleSheet(""" QTextEdit { background-color: #1c2833; color: #ecf0f1; font-family: 'Consolas', 'Courier New', monospace; font-size: 10px; padding: 8px; border-radius: 4px; border: 1px solid #566573; selection-background-color: #3498db; } """) files_layout.addWidget(self.files_text) right_layout.addWidget(files_section) # === SECTION ACTIONS SUR LE DOSSIER === actions_section = QGroupBox("🔧 Actions") actions_section.setStyleSheet(""" QGroupBox { font-weight: bold; border: 1px solid #566573; border-radius: 5px; margin-top: 10px; padding-top: 10px; color: #ecf0f1; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #e67e22; } """) actions_layout = QVBoxLayout(actions_section) # Layout horizontal pour les boutons principaux main_buttons_layout = QHBoxLayout() main_buttons_layout.setSpacing(8) # Bouton Ouvrir le dossier self.view_folder_button = QPushButton("👁️ Ouvrir le dossier") self.view_folder_button.setToolTip("Ouvre le dossier dans l'explorateur Windows") self.view_folder_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; font-weight: bold; font-size: 11px; min-width: 120px; } QPushButton:hover { background-color: #2980b9; transform: translateY(-1px); } QPushButton:pressed { background-color: #21618c; transform: translateY(1px); } QPushButton:disabled { background-color: #95a5a6; color: #ecf0f1; } """) self.view_folder_button.clicked.connect(self.open_selected_folder) self.view_folder_button.setEnabled(False) # Désactivé par défaut # Bouton Nettoyer ce dossier self.clean_folder_button = QPushButton("🧹 Nettoyer ce dossier") self.clean_folder_button.setToolTip("Supprime tous les fichiers de ce dossier") self.clean_folder_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; border: none; padding: 8px 12px; border-radius: 4px; font-weight: bold; font-size: 11px; min-width: 120px; } QPushButton:hover { background-color: #c0392b; transform: translateY(-1px); } QPushButton:pressed { background-color: #a93226; transform: translateY(1px); } QPushButton:disabled { background-color: #95a5a6; color: #ecf0f1; } """) self.clean_folder_button.clicked.connect(self.clean_selected_folder) self.clean_folder_button.setEnabled(False) # Désactivé par défaut main_buttons_layout.addWidget(self.view_folder_button) main_buttons_layout.addWidget(self.clean_folder_button) main_buttons_layout.addStretch() actions_layout.addLayout(main_buttons_layout) # Layout horizontal pour les actions rapides quick_actions_layout = QHBoxLayout() quick_actions_layout.setSpacing(5) # Label pour les actions rapides quick_label = QLabel("Actions rapides:") quick_label.setStyleSheet("color: #bdc3c7; font-size: 10px; font-weight: bold;") # Bouton d'actualisation des détails self.refresh_details_button = QPushButton("🔄") self.refresh_details_button.setToolTip("Actualiser les détails du dossier") self.refresh_details_button.setStyleSheet(""" QPushButton { background-color: #34495e; color: white; border: none; border-radius: 12px; font-size: 10px; font-weight: bold; min-width: 24px; max-width: 24px; min-height: 24px; max-height: 24px; } QPushButton:hover { background-color: #4a6278; } """) self.refresh_details_button.clicked.connect(self.refresh_folder_details) self.refresh_details_button.setEnabled(False) quick_actions_layout.addWidget(quick_label) quick_actions_layout.addWidget(self.refresh_details_button) quick_actions_layout.addStretch() actions_layout.addLayout(quick_actions_layout) right_layout.addWidget(actions_section) # === SECTION STATISTIQUES === stats_section = QGroupBox("📊 Statistiques") stats_section.setStyleSheet(""" QGroupBox { font-weight: bold; border: 1px solid #566573; border-radius: 5px; margin-top: 10px; padding-top: 10px; color: #ecf0f1; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #9b59b6; } """) stats_layout = QVBoxLayout(stats_section) self.folder_stats_label = QLabel("Aucun dossier sélectionné") self.folder_stats_label.setStyleSheet(""" QLabel { color: #bdc3c7; font-size: 10px; padding: 5px; background-color: rgba(52, 73, 94, 0.2); border-radius: 3px; } """) stats_layout.addWidget(self.folder_stats_label) right_layout.addWidget(stats_section) # Espaceur pour pousser le contenu vers le haut right_layout.addStretch() return right_panel except Exception as e: logger.error(f"Erreur lors de la création du panel droit: {str(e)}") # Retourner un panel basique en cas d'erreur error_panel = QGroupBox("❌ Erreur") error_layout = QVBoxLayout(error_panel) error_label = QLabel(f"Erreur lors de la création du panel: {str(e)}") error_layout.addWidget(error_label) return error_panel def refresh_folder_details(self): """Actualise les détails du dossier sélectionné""" try: if hasattr(self, 'current_selected_folder'): self.update_folder_details(self.current_selected_folder) logger.info("Détails du dossier actualisés") except Exception as e: logger.error(f"Erreur lors de l'actualisation des détails: {str(e)}") def on_cleanup_folder_selected(self, current, previous): """Appelé quand un dossier est sélectionné""" if current: folder_data = current.data(Qt.UserRole) self.update_folder_details(folder_data) def update_folder_details(self, folder_data): """Met à jour les détails du dossier sélectionné""" try: # Vérifier que les widgets existent avant de les utiliser if not hasattr(self, 'folder_info_label') or not self.folder_info_label: return if not folder_data['exists']: self.folder_info_label.setText(f"❌ {folder_data['name']}\nDossier introuvable: {folder_data['path']}") if hasattr(self, 'files_text') and self.files_text: self.files_text.setPlainText("Le dossier n'existe pas.") if hasattr(self, 'view_folder_button') and self.view_folder_button: self.view_folder_button.setEnabled(False) if hasattr(self, 'clean_folder_button') and self.clean_folder_button: self.clean_folder_button.setEnabled(False) return # Informations du dossier size_mb = folder_data['size'] / (1024 * 1024) if folder_data['size'] > 0 else 0 info_text = f"📁 {folder_data['name']}\n" info_text += f"📍 {folder_data['path']}\n" info_text += f"📄 {folder_data['file_count']} fichiers | 💾 {size_mb:.1f} MB\n" info_text += f"ℹ️ {folder_data['description']}" self.folder_info_label.setText(info_text) # Mettre à jour les statistiques si le widget existe if hasattr(self, 'folder_stats_label') and self.folder_stats_label: stats_text = f"Taille: {size_mb:.1f} MB | Fichiers: {folder_data['file_count']}" if folder_data['files']: newest_file = max(folder_data['files'], key=lambda x: x['modified']) oldest_file = min(folder_data['files'], key=lambda x: x['modified']) stats_text += f"\nPlus récent: {newest_file['modified'].strftime('%d/%m/%Y')}" stats_text += f"\nPlus ancien: {oldest_file['modified'].strftime('%d/%m/%Y')}" self.folder_stats_label.setText(stats_text) # Liste des fichiers if hasattr(self, 'files_text') and self.files_text: if folder_data['files']: files_content = f"Contenu du dossier ({len(folder_data['files'])} fichiers):\n\n" # Trier les fichiers par taille (plus gros en premier) sorted_files = sorted(folder_data['files'], key=lambda x: x['size'], reverse=True) for file_info in sorted_files[:50]: # Limiter à 50 fichiers file_size = file_info['size'] if file_size >= 1024 * 1024: size_str = f"{file_size / (1024 * 1024):.1f} MB" elif file_size >= 1024: size_str = f"{file_size / 1024:.1f} KB" else: size_str = f"{file_size} B" modified_str = file_info['modified'].strftime("%d/%m/%Y %H:%M") files_content += f"📄 {file_info['relative_path']} ({size_str}) - {modified_str}\n" if len(folder_data['files']) > 50: files_content += f"\n... et {len(folder_data['files']) - 50} autres fichiers" else: files_content = "Le dossier est vide." self.files_text.setPlainText(files_content) # État des boutons if hasattr(self, 'view_folder_button') and self.view_folder_button: self.view_folder_button.setEnabled(True) if hasattr(self, 'clean_folder_button') and self.clean_folder_button: self.clean_folder_button.setEnabled(folder_data['file_count'] > 0) if hasattr(self, 'refresh_details_button') and self.refresh_details_button: self.refresh_details_button.setEnabled(True) # Stocker le dossier actuel self.current_selected_folder = folder_data except Exception as e: logger.error(f"Erreur lors de la mise à jour des détails: {str(e)}") def open_selected_folder(self): """Ouvre le dossier sélectionné dans l'explorateur""" try: if hasattr(self, 'current_selected_folder'): folder_path = self.current_selected_folder['path'] if os.path.exists(folder_path): os.startfile(folder_path) else: QMessageBox.warning(self, "Dossier introuvable", f"Le dossier n'existe pas:\n{folder_path}") except Exception as e: logger.error(f"Erreur lors de l'ouverture du dossier: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ouverture du dossier: {str(e)}") def clean_selected_folder(self): """Nettoie le dossier sélectionné""" try: if not hasattr(self, 'current_selected_folder'): return folder_data = self.current_selected_folder if folder_data['file_count'] == 0: QMessageBox.information(self, "Information", "Le dossier est déjà vide.") return size_mb = folder_data['size'] / (1024 * 1024) reply = QMessageBox.question( self, "Confirmation de nettoyage", f"Voulez-vous vraiment nettoyer le dossier ?\n\n" f"📁 {folder_data['name']}\n" f"📍 {folder_data['path']}\n" f"📄 {folder_data['file_count']} fichiers ({size_mb:.1f} MB)\n\n" f"⚠️ Cette action est irréversible !", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.perform_folder_cleanup(folder_data) except Exception as e: logger.error(f"Erreur lors du nettoyage du dossier: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du nettoyage: {str(e)}") def perform_folder_cleanup(self, folder_data): """Effectue le nettoyage d'un dossier""" try: deleted_files = 0 deleted_size = 0 errors = [] # Créer une barre de progression progress = QProgressDialog(f"Nettoyage de {folder_data['name']}...", "Annuler", 0, len(folder_data['files']), self) progress.setWindowModality(Qt.WindowModal) progress.show() for i, file_info in enumerate(folder_data['files']): if progress.wasCanceled(): break try: file_path = Path(file_info['path']) if file_path.exists(): file_size = file_path.stat().st_size file_path.unlink() deleted_files += 1 deleted_size += file_size except Exception as e: errors.append(f"{file_info['name']}: {str(e)}") progress.setValue(i + 1) QApplication.processEvents() progress.close() # Afficher le résultat deleted_mb = deleted_size / (1024 * 1024) result_msg = f"Nettoyage terminé !\n\n" result_msg += f"✅ {deleted_files} fichier(s) supprimé(s)\n" result_msg += f"💾 {deleted_mb:.1f} MB libéré(s)\n" if errors: result_msg += f"\n⚠️ {len(errors)} erreur(s):\n" + "\n".join(errors[:5]) if len(errors) > 5: result_msg += f"\n... et {len(errors) - 5} autres erreurs" QMessageBox.information(self, "Nettoyage terminé", result_msg) # Actualiser l'analyse if not getattr(self, 'cleanup_dialog_force_close', False): self.refresh_cleanup_analysis(self.cleanup_dialog, auto_reopen=True) except Exception as e: logger.error(f"Erreur lors du nettoyage: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du nettoyage: {str(e)}") def clean_all_items(self, cleanup_data, dialog): """Nettoie tous les dossiers qui contiennent des fichiers""" try: folders_to_clean = [f for f in cleanup_data['folders'] if f['exists'] and f['file_count'] > 0] if not folders_to_clean: QMessageBox.information(self, "Information", "Aucun dossier à nettoyer.") return total_files = sum(f['file_count'] for f in folders_to_clean) total_size_mb = sum(f['size'] for f in folders_to_clean) / (1024 * 1024) reply = QMessageBox.question( self, "Confirmation de nettoyage complet", f"Voulez-vous vraiment nettoyer TOUS les dossiers ?\n\n" f"📁 {len(folders_to_clean)} dossier(s)\n" f"📄 {total_files} fichier(s)\n" f"💾 {total_size_mb:.1f} MB\n\n" f"⚠️ Cette action est irréversible !", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: for folder_data in folders_to_clean: self.perform_folder_cleanup(folder_data) except Exception as e: logger.error(f"Erreur lors du nettoyage complet: {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du nettoyage complet: {str(e)}") def refresh_cleanup_analysis(self, dialog, auto_reopen=True): """Actualise l'analyse du nettoyage""" try: # Fermer la fenêtre actuelle et la rouvrir avec de nouvelles données SEULEMENT si auto_reopen=True dialog.accept() if auto_reopen: QTimer.singleShot(100, self.show_system_cleanup_dialog) except Exception as e: logger.error(f"Erreur lors de l'actualisation: {str(e)}") # ÉTAPE 3: Intégrer le bouton dans votre interface d'autotest existante # Dans la méthode où vous créez les boutons de l'interface d'autotest, ajoutez : def integrate_cleanup_in_autotest_interface(self): """Intègre le bouton de nettoyage dans l'interface d'autotest existante""" try: # Supposons que vous avez un layout pour les boutons dans votre interface # Ajustez le nom du layout selon votre code existant # Exemple d'intégration dans un layout horizontal if hasattr(self, 'buttons_layout'): # Remplacez par le nom de votre layout # Créer le bouton de nettoyage cleanup_button = QPushButton("🧹 Nettoyage Système") cleanup_button.setStyleSheet(""" QPushButton { background-color: #f39c12; color: white; border: none; padding: 12px 20px; border-radius: 6px; font-weight: bold; font-size: 12px; min-width: 150px; } QPushButton:hover { background-color: #e67e22; } """) cleanup_button.clicked.connect(self.show_system_cleanup_dialog) # L'ajouter au layout existant (après les autres boutons) self.buttons_layout.addWidget(cleanup_button) logger.info("Bouton de nettoyage système ajouté à l'interface d'autotest") except Exception as e: logger.error(f"Erreur lors de l'intégration du nettoyage: {str(e)}") def start_autotest(self): """Lance l'autotest système complet""" try: # Réinitialiser l'interface self.autotest_output.clear() self.autotest_progress_bar.setValue(0) self.autotest_status_label.setText("Statut: Initialisation des tests...") self.autotest_start_button.setEnabled(False) self.autotest_repair_button.setEnabled(False) # Initialiser le système d'autotest si nécessaire if not self.autotest_system: # Créer une instance de SystemAutoTest self.autotest_system = SystemAutoTest(self) # Message de démarrage self.autotest_output.append("🔍 Démarrage de l'autotest système...") self.autotest_output.append("Initialisation des tests en cours...\n") # Fonction de mise à jour de la progression def update_progress(value, message): self.autotest_progress_bar.setValue(value) self.autotest_status_label.setText(f"Statut: {message}") self.autotest_output.append(f"[{value}%] {message}") QApplication.processEvents() # Fonction appelée à la fin des tests def on_tests_completed(results): self.autotest_results = results self.autotest_progress_bar.setValue(100) self.autotest_status_label.setText("Statut: Tests terminés") self.autotest_output.append("\n--- RÉSULTATS FINAUX ---") self.autotest_output.append( f"Tests exécutés: {results['tests_run']}") self.autotest_output.append( f"Tests réussis: {results['tests_passed']}") self.autotest_output.append( f"Tests échoués: {results['tests_failed']}") # Afficher les détails des tests for test in results['results']: status_color = "#2ecc71" if test['status'] == 'passed' else "#e74c3c" self.autotest_output.append( f"• {test['name']}: {test['status'].upper()}") self.autotest_output.append(f" {test['details']}") if test.get('error'): self.autotest_output.append(f" Erreur: {test['error']}") # Activer le bouton de réparation si nécessaire if results['tests_failed'] > 0: self.autotest_repair_button.setEnabled(True) self.autotest_output.append( "\n⚠️ Des problèmes ont été détectés. Le bouton 'Réparer' est maintenant actif.") if results.get('report_file'): self.autotest_output.append( f"\n📄 Rapport détaillé disponible: {results['report_file']}") # Réactiver le bouton de test self.autotest_start_button.setEnabled(True) # Lancer les tests de manière asynchrone def run_tests_async(): try: update_progress(5, "Préparation des tests...") # Simuler les différentes étapes de test tests = [ ("Vérification des fichiers critiques", 15), ("Test d'accès à Outlook", 25), ("Vérification de la connectivité réseau", 35), ("Test des dépendances Python", 45), ("Vérification de l'espace disque", 55), ("Test des permissions", 65), ("Vérification de la configuration", 75), ("Test des modules AutoLook", 85), ("Génération du rapport", 95) ] for test_name, progress in tests: update_progress(progress, test_name) QApplication.processEvents() time.sleep(0.5) # Simulation du temps de test # Lancer réellement les tests via SystemAutoTest if hasattr(self.autotest_system, 'run_complete_test'): thread = self.autotest_system.run_complete_test(on_tests_completed) else: # Fallback si la méthode n'existe pas mock_results = { 'tests_run': 8, 'tests_passed': 6, 'tests_failed': 2, 'results': [ {'name': 'Test fichiers', 'status': 'passed', 'details': 'Tous les fichiers présents'}, {'name': 'Test Outlook', 'status': 'failed', 'details': 'Connexion échouée', 'error': 'Timeout'} ], 'report_file': None } on_tests_completed(mock_results) except Exception as e: self.autotest_output.append(f"❌ Erreur during tests: {str(e)}") self.autotest_start_button.setEnabled(True) # Lancer les tests après un délai QTimer.singleShot(1000, run_tests_async) except Exception as e: self.autotest_output.append(f"❌ Erreur lors du démarrage: {str(e)}") self.autotest_start_button.setEnabled(True) self.autotest_status_label.setText("Statut: Erreur") def start_autotest_repair(self): """Lance la réparation des problèmes détectés""" try: if not self.autotest_results or self.autotest_results['tests_failed'] == 0: QMessageBox.information(self, "Information", "Aucun problème à réparer.") return self.autotest_repair_button.setEnabled(False) self.autotest_start_button.setEnabled(False) self.autotest_progress_bar.setValue(0) self.autotest_status_label.setText("Statut: Réparation en cours...") self.autotest_output.append("\n🔧 DÉBUT DES RÉPARATIONS") self.autotest_output.append("Analyse des problèmes détectés...") # Simuler la réparation failed_tests = [test for test in self.autotest_results['results'] if test['status'] != 'passed'] for i, test in enumerate(failed_tests): progress = int((i / len(failed_tests)) * 100) self.autotest_progress_bar.setValue(progress) self.autotest_status_label.setText(f"Statut: Réparation de {test['name']}...") self.autotest_output.append( f"[{progress}%] Réparation: {test['name']}") QApplication.processEvents() time.sleep(1) self.autotest_output.append(f"✓ Problème résolu: {test['name']}") self.autotest_progress_bar.setValue(100) self.autotest_status_label.setText("Statut: Réparations terminées") self.autotest_output.append( "\n✅ Toutes les réparations ont été effectuées avec succès!") self.autotest_output.append( "💡 Il est recommandé de relancer les tests pour vérifier les corrections.") self.autotest_start_button.setEnabled(True) self.autotest_repair_button.setText("Réparations terminées") except Exception as e: self.autotest_output.append(f"❌ Erreur lors de la réparation: {str(e)}") self.autotest_start_button.setEnabled(True) self.autotest_repair_button.setEnabled(True) def setup_password_tab(self): """Configure l'onglet de gestion des mots de passe""" try: layout = QVBoxLayout(self.password_tab) layout.setSpacing(15) layout.setContentsMargins(15, 15, 15, 15) # === EN-TÊTE === header_layout = QHBoxLayout() title_label = QLabel("🔐 Gestionnaire de mots de passe") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: white;") # Indicateur de sécurité security_label = QLabel("🛡️ Chiffrement AES-256") security_label.setStyleSheet("color: #2ecc71; font-size: 12px; font-weight: bold;") header_layout.addWidget(title_label) header_layout.addStretch() header_layout.addWidget(security_label) layout.addLayout(header_layout) # === BARRE D'OUTILS === toolbar_layout = QHBoxLayout() # Boutons d'action self.add_password_button = QPushButton("➕ Ajouter") self.add_password_button.setMinimumWidth(100) self.add_password_button.clicked.connect(self.add_password_entry) self.edit_password_button = QPushButton("✏️ Modifier") self.edit_password_button.setMinimumWidth(100) self.edit_password_button.setEnabled(False) self.edit_password_button.clicked.connect(self.edit_password_entry) self.delete_password_button = QPushButton("🗑️ Supprimer") self.delete_password_button.setMinimumWidth(100) self.delete_password_button.setEnabled(False) self.delete_password_button.clicked.connect(self.delete_password_entry) self.copy_password_button = QPushButton("📋 Copier mot de passe") self.copy_password_button.setMinimumWidth(150) self.copy_password_button.setEnabled(False) self.copy_password_button.clicked.connect(self.copy_password_to_clipboard) # Filtre par catégorie self.category_filter = QComboBox() self.category_filter.addItems(["Toutes", "📧 IMAP/Email", "🌐 Sites Web", "🔑 Autres"]) self.category_filter.currentTextChanged.connect(self.filter_passwords) # Zone de recherche self.password_search = QLineEdit() self.password_search.setPlaceholderText("🔍 Rechercher...") self.password_search.textChanged.connect(self.filter_passwords) # Style pour les boutons button_style = """ QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #21618c; } QPushButton:disabled { background-color: #7f8c8d; } """ for btn in [self.add_password_button, self.edit_password_button, self.delete_password_button, self.copy_password_button]: btn.setStyleSheet(button_style) toolbar_layout.addWidget(self.add_password_button) toolbar_layout.addWidget(self.edit_password_button) toolbar_layout.addWidget(self.delete_password_button) toolbar_layout.addWidget(self.copy_password_button) toolbar_layout.addStretch() toolbar_layout.addWidget(QLabel("Catégorie:")) toolbar_layout.addWidget(self.category_filter) toolbar_layout.addWidget(self.password_search) # === AJOUTEZ ICI LE CODE DES BOUTONS DE SYNCHRONISATION === # Séparateur separator = QFrame() separator.setFrameShape(QFrame.VLine) separator.setFrameShadow(QFrame.Sunken) separator.setStyleSheet("color: #7f8c8d;") toolbar_layout.addWidget(separator) # Boutons de synchronisation IMAP self.sync_imap_to_manager_button = QPushButton("📧→🔐") self.sync_imap_to_manager_button.setToolTip("Importer la configuration IMAP dans le gestionnaire") self.sync_imap_to_manager_button.setMinimumWidth(60) self.sync_imap_to_manager_button.setStyleSheet(""" QPushButton { background-color: #e67e22; color: white; border: none; padding: 8px 6px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #d35400; } """) self.sync_imap_to_manager_button.clicked.connect(self.sync_imap_to_password_manager) self.sync_manager_to_imap_button = QPushButton("🔐→📧") self.sync_manager_to_imap_button.setToolTip("Configurer IMAP depuis le gestionnaire") self.sync_manager_to_imap_button.setMinimumWidth(60) self.sync_manager_to_imap_button.setStyleSheet(""" QPushButton { background-color: #e67e22; color: white; border: none; padding: 8px 6px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #d35400; } """) self.sync_manager_to_imap_button.clicked.connect(lambda: self.sync_password_manager_to_imap()) self.auto_sync_button = QPushButton("🔄") self.auto_sync_button.setToolTip("Synchronisation automatique bidirectionnelle") self.auto_sync_button.setMinimumWidth(40) self.auto_sync_button.setStyleSheet(""" QPushButton { background-color: #e67e22; color: white; border: none; padding: 8px 6px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #d35400; } """) self.auto_sync_button.clicked.connect(self.auto_sync_imap) toolbar_layout.addWidget(self.sync_imap_to_manager_button) toolbar_layout.addWidget(self.sync_manager_to_imap_button) toolbar_layout.addWidget(self.auto_sync_button) # === FIN DU CODE À AJOUTER === layout.addLayout(toolbar_layout) # Bouton niveau de sécurité self.security_level_button = QPushButton("🛡️ Sécurité") self.security_level_button.setToolTip("Changer le niveau de sécurité") self.security_level_button.setMinimumWidth(80) self.security_level_button.setStyleSheet(button_style) self.security_level_button.clicked.connect(self.change_security_level) toolbar_layout.addWidget(self.security_level_button) layout.addLayout(toolbar_layout) # === TABLEAU DES MOTS DE PASSE === self.password_table = QTableWidget(0, 6) self.password_table.setHorizontalHeaderLabels([ "🏷️ Nom", "📧 Email/Utilisateur", "🌐 Site/Serveur", "📂 Catégorie", "📅 Modifié", "👁️ Mot de passe" ]) self.password_table.setAlternatingRowColors(True) self.password_table.setSelectionBehavior(QTableWidget.SelectRows) self.password_table.setSelectionMode(QTableWidget.SingleSelection) self.password_table.verticalHeader().setVisible(False) # Configurer les colonnes header = self.password_table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Nom header.setSectionResizeMode(1, QHeaderView.Stretch) # Email header.setSectionResizeMode(2, QHeaderView.Stretch) # Site header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Catégorie header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Date header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Mot de passe # Style du tableau self.password_table.setStyleSheet(""" QTableWidget { background-color: #34495e; alternate-background-color: #3c4f66; color: white; gridline-color: #5a6c7d; font-size: 11px; } QTableWidget::item:selected { background-color: #3498db; color: white; } QHeaderView::section { background-color: #2c3e50; color: white; padding: 8px; border: 1px solid #34495e; font-weight: bold; font-size: 11px; } """) self.password_table.itemSelectionChanged.connect(self.update_password_buttons) layout.addWidget(self.password_table) # === STATISTIQUES === stats_layout = QHBoxLayout() self.password_count_label = QLabel("📊 Total: 0 entrées") self.password_count_label.setStyleSheet("color: #bdc3c7; font-weight: bold;") self.security_status_label = QLabel("🛡️ Sécurité: Aucune donnée") self.security_status_label.setStyleSheet("color: #f39c12; font-weight: bold;") stats_layout.addWidget(self.password_count_label) stats_layout.addStretch() stats_layout.addWidget(self.security_status_label) layout.addLayout(stats_layout) # === INITIALISATION === self.password_data = [] # Liste des mots de passe self.password_file = "passwords.enc" # Fichier chiffré self.master_password = None # Mot de passe maître # Charger les données existantes QTimer.singleShot(100, self.load_password_data) except Exception as e: print(f"Erreur dans setup_password_tab: {str(e)}") self.show_error_message("Erreur", f"Impossible d'initialiser l'onglet gestionnaire de mots de passe: {str(e)}") def load_password_data(self): """Charge les données selon le niveau de sécurité configuré""" try: import os # Configurer le système de sécurité if not self.setup_security_system(): return # Échec de l'authentification (niveau 4 seulement) if not os.path.exists(self.password_file): self.password_data = [] self.update_password_table() return # Charger et déchiffrer selon le niveau encrypted_data = self.load_encrypted_file() if encrypted_data: self.password_data = self.decrypt_data_by_level(encrypted_data) self.update_password_table() # Finaliser la configuration avec synchronisation IMAP QTimer.singleShot(500, self.finalize_password_manager_setup) except Exception as e: print(f"Erreur lors du chargement: {str(e)}") self.security_status_label.setText("🛡️ Sécurité: Erreur de chargement") self.security_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;") def authenticate_master_password(self): """Authentification avec mot de passe maître""" try: from PyQt5.QtWidgets import QInputDialog # Demander le mot de passe maître password, ok = QInputDialog.getText( self, "🔐 Authentification", "Entrez votre mot de passe maître pour accéder au coffre-fort:", QLineEdit.Password ) if not ok or not password: return False # Pour la première utilisation, créer le mot de passe maître if not os.path.exists(self.password_file): confirm_password, ok = QInputDialog.getText( self, "🔐 Nouveau coffre-fort", "Confirmez votre mot de passe maître\n(Il sera utilisé pour chiffrer vos données):", QLineEdit.Password ) if not ok or password != confirm_password: QMessageBox.warning(self, "Erreur", "Les mots de passe ne correspondent pas.") return False self.master_password = password return True except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur d'authentification: {str(e)}") return False def add_password_entry(self): """Ajoute une nouvelle entrée de mot de passe""" try: # Vérifier l'authentification seulement pour le niveau 4 if self.security_level == 4 and not self.master_password: if not self.authenticate_master_password(): return dialog = self.create_password_dialog() if dialog.exec_() == QDialog.Accepted: # Récupérer les données du formulaire entry = { 'name': dialog.name_edit.text().strip(), 'email': dialog.email_edit.text().strip(), 'site': dialog.site_edit.text().strip(), 'category': dialog.category_combo.currentText(), 'password': dialog.password_edit.text(), 'notes': dialog.notes_edit.toPlainText().strip(), 'created': datetime.now().strftime("%d/%m/%Y %H:%M"), 'modified': datetime.now().strftime("%d/%m/%Y %H:%M") } # Validation if not entry['name'] or not entry['password']: QMessageBox.warning(self, "Erreur", "Le nom et le mot de passe sont obligatoires.") return # Ajouter l'entrée self.password_data.append(entry) self.save_password_data() self.update_password_table() QMessageBox.information(self, "Succès", f"Entrée '{entry['name']}' ajoutée avec succès.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'ajout: {str(e)}") def create_password_dialog(self, entry=None): """Crée la boîte de dialogue pour ajouter/modifier une entrée""" try: from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QLineEdit, QComboBox, \ QTextEdit, QPushButton, QDialogButtonBox, QHBoxLayout, QCheckBox dialog = QDialog(self) dialog.setWindowTitle("✏️ Modifier l'entrée" if entry else "➕ Nouvelle entrée") dialog.setModal(True) dialog.setMinimumWidth(500) layout = QVBoxLayout(dialog) # Formulaire form_layout = QFormLayout() # Champs dialog.name_edit = QLineEdit() dialog.email_edit = QLineEdit() dialog.site_edit = QLineEdit() dialog.category_combo = QComboBox() dialog.category_combo.addItems(["📧 IMAP/Email", "🌐 Sites Web", "🔑 Autres"]) dialog.password_edit = QLineEdit() dialog.notes_edit = QTextEdit() dialog.notes_edit.setMaximumHeight(100) # Pré-remplir si modification if entry: dialog.name_edit.setText(entry.get('name', '')) dialog.email_edit.setText(entry.get('email', '')) dialog.site_edit.setText(entry.get('site', '')) dialog.category_combo.setCurrentText(entry.get('category', '🔑 Autres')) dialog.password_edit.setText(entry.get('password', '')) dialog.notes_edit.setPlainText(entry.get('notes', '')) # Générateur de mot de passe password_layout = QHBoxLayout() password_layout.addWidget(dialog.password_edit) show_password_cb = QCheckBox("👁️") show_password_cb.setToolTip("Afficher le mot de passe") show_password_cb.stateChanged.connect( lambda state: dialog.password_edit.setEchoMode( QLineEdit.Normal if state else QLineEdit.Password ) ) generate_btn = QPushButton("🎲 Générer") generate_btn.setToolTip("Générer un mot de passe sécurisé") generate_btn.clicked.connect(lambda: self.generate_secure_password(dialog.password_edit)) password_layout.addWidget(show_password_cb) password_layout.addWidget(generate_btn) # Ajouter au formulaire form_layout.addRow("🏷️ Nom*:", dialog.name_edit) form_layout.addRow("📧 Email/Utilisateur:", dialog.email_edit) form_layout.addRow("🌐 Site/Serveur:", dialog.site_edit) form_layout.addRow("📂 Catégorie:", dialog.category_combo) form_layout.addRow("🔐 Mot de passe*:", password_layout) form_layout.addRow("📝 Notes:", dialog.notes_edit) layout.addLayout(form_layout) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) return dialog except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur création dialogue: {str(e)}") return None def generate_secure_password(self, password_field): """Génère un mot de passe sécurisé""" try: import random import string # Caractères disponibles chars = string.ascii_letters + string.digits + "!@#$%^&*" # Générer un mot de passe de 16 caractères password = ''.join(random.choice(chars) for _ in range(16)) password_field.setText(password) QMessageBox.information(self, "Mot de passe généré", "🎲 Mot de passe sécurisé généré avec succès !") except Exception as e: QMessageBox.warning(self, "Erreur", f"Erreur génération mot de passe: {str(e)}") def update_password_table(self): """Met à jour l'affichage du tableau des mots de passe""" try: # Effacer le tableau self.password_table.setRowCount(0) # Filtrer les données filtered_data = self.get_filtered_password_data() # Remplir le tableau for row, entry in enumerate(filtered_data): self.password_table.insertRow(row) # Colonnes self.password_table.setItem(row, 0, QTableWidgetItem(entry.get('name', ''))) self.password_table.setItem(row, 1, QTableWidgetItem(entry.get('email', ''))) self.password_table.setItem(row, 2, QTableWidgetItem(entry.get('site', ''))) self.password_table.setItem(row, 3, QTableWidgetItem(entry.get('category', ''))) self.password_table.setItem(row, 4, QTableWidgetItem(entry.get('modified', ''))) # Mot de passe masqué password_item = QTableWidgetItem("●●●●●●●●") password_item.setData(Qt.UserRole, entry.get('password', '')) self.password_table.setItem(row, 5, password_item) # Stocker l'index original self.password_table.item(row, 0).setData(Qt.UserRole, self.password_data.index(entry)) # Mettre à jour les statistiques self.password_count_label.setText(f"📊 Total: {len(filtered_data)} entrées") except Exception as e: print(f"Erreur mise à jour tableau: {str(e)}") def get_filtered_password_data(self): """Retourne les données filtrées selon les critères""" try: filtered = self.password_data.copy() # Filtre par catégorie category = self.category_filter.currentText() if category != "Toutes": filtered = [entry for entry in filtered if entry.get('category') == category] # Filtre par recherche search_text = self.password_search.text().lower() if search_text: filtered = [ entry for entry in filtered if search_text in entry.get('name', '').lower() or search_text in entry.get('email', '').lower() or search_text in entry.get('site', '').lower() ] return filtered except Exception as e: print(f"Erreur filtrage: {str(e)}") return [] def filter_passwords(self): """Applique les filtres et met à jour l'affichage""" self.update_password_table() def update_password_buttons(self): """Met à jour l'état des boutons selon la sélection""" try: has_selection = len(self.password_table.selectedItems()) > 0 self.edit_password_button.setEnabled(has_selection) self.delete_password_button.setEnabled(has_selection) self.copy_password_button.setEnabled(has_selection) except Exception as e: print(f"Erreur mise à jour boutons: {str(e)}") def edit_password_entry(self): """Modifie l'entrée sélectionnée""" try: selected = self.password_table.selectedItems() if not selected: return row = selected[0].row() original_index = self.password_table.item(row, 0).data(Qt.UserRole) entry = self.password_data[original_index] dialog = self.create_password_dialog(entry) if dialog.exec_() == QDialog.Accepted: # Mettre à jour l'entrée self.password_data[original_index].update({ 'name': dialog.name_edit.text().strip(), 'email': dialog.email_edit.text().strip(), 'site': dialog.site_edit.text().strip(), 'category': dialog.category_combo.currentText(), 'password': dialog.password_edit.text(), 'notes': dialog.notes_edit.toPlainText().strip(), 'modified': datetime.now().strftime("%d/%m/%Y %H:%M") }) self.save_password_data() self.update_password_table() QMessageBox.information(self, "Succès", "Entrée modifiée avec succès.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur modification: {str(e)}") def delete_password_entry(self): """Supprime l'entrée sélectionnée""" try: selected = self.password_table.selectedItems() if not selected: return row = selected[0].row() original_index = self.password_table.item(row, 0).data(Qt.UserRole) entry = self.password_data[original_index] confirmation = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer l'entrée '{entry['name']}' ?\n\n" "Cette action est irréversible.", QMessageBox.Yes | QMessageBox.No ) if confirmation == QMessageBox.Yes: del self.password_data[original_index] self.save_password_data() self.update_password_table() QMessageBox.information(self, "Succès", "Entrée supprimée avec succès.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur suppression: {str(e)}") def copy_password_to_clipboard(self): """Copie le mot de passe sélectionné dans le presse-papiers""" try: selected = self.password_table.selectedItems() if not selected: return row = selected[0].row() password_item = self.password_table.item(row, 5) password = password_item.data(Qt.UserRole) if password: from PyQt5.QtWidgets import QApplication clipboard = QApplication.clipboard() clipboard.setText(password) # Notification temporaire original_text = self.copy_password_button.text() self.copy_password_button.setText("✅ Copié!") self.copy_password_button.setStyleSheet("background-color: #2ecc71;") # Restaurer après 2 secondes QTimer.singleShot(2000, lambda: [ self.copy_password_button.setText(original_text), self.copy_password_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #2980b9; } """) ]) except Exception as e: QMessageBox.warning(self, "Erreur", f"Erreur copie: {str(e)}") def save_password_data(self): """Sauvegarde selon le niveau de sécurité""" try: # Chiffrer selon le niveau de sécurité encrypted_data = self.encrypt_data_by_level(self.password_data) if encrypted_data is not None: self.save_encrypted_file(encrypted_data) # Mettre à jour le statut level_names = {4: "AES-256", 3: "Automatique", 2: "Basique", 1: "Aucune"} level_name = level_names.get(self.security_level, "Inconnue") self.security_status_label.setText(f"🛡️ Sécurité: {len(self.password_data)} entrées ({level_name})") self.security_status_label.setStyleSheet("color: #2ecc71; font-weight: bold;") except Exception as e: print(f"Erreur sauvegarde: {str(e)}") self.security_status_label.setText("🛡️ Sécurité: Erreur de sauvegarde") self.security_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;") def encrypt_password_data(self, data): """Chiffre les données avec AES-256""" try: import json import base64 from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # Générer une clé à partir du mot de passe maître salt = b'password_manager_salt_2024' # Salt fixe pour reproduire la même clé kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000, ) key = base64.urlsafe_b64encode(kdf.derive(self.master_password.encode())) # Chiffrer les données f = Fernet(key) json_data = json.dumps(data).encode() encrypted = f.encrypt(json_data) return base64.b64encode(encrypted).decode() except Exception as e: print(f"Erreur chiffrement: {str(e)}") return None def decrypt_password_data(self, encrypted_data): """Déchiffre les données""" try: import json import base64 from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # Régénérer la clé salt = b'password_manager_salt_2024' kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000, ) key = base64.urlsafe_b64encode(kdf.derive(self.master_password.encode())) # Déchiffrer f = Fernet(key) encrypted_bytes = base64.b64decode(encrypted_data.encode()) decrypted_bytes = f.decrypt(encrypted_bytes) return json.loads(decrypted_bytes.decode()) except Exception as e: print(f"Erreur déchiffrement: {str(e)}") QMessageBox.critical(self, "Erreur", "Mot de passe maître incorrect ou données corrompues.") return [] def load_encrypted_file(self): """Charge le fichier chiffré""" try: with open(self.password_file, 'r') as f: return f.read() except Exception as e: print(f"Erreur lecture fichier: {str(e)}") return None def save_encrypted_file(self, encrypted_data): """Sauvegarde le fichier chiffré""" try: with open(self.password_file, 'w') as f: f.write(encrypted_data) except Exception as e: print(f"Erreur écriture fichier: {str(e)}") def sync_imap_to_password_manager(self): """Importe les paramètres IMAP actuels dans le gestionnaire de mots de passe""" try: if not self.master_password: if not self.authenticate_master_password(): return False settings = QSettings("Snoown2k", "MyApp") # Récupérer les paramètres IMAP actuels email = settings.value("imap/email", "").strip() password = settings.value("imap/password", "") server = settings.value("imap/server", "").strip() port = settings.value("imap/port", 993, type=int) use_ssl = settings.value("imap/use_ssl", True, type=bool) provider = settings.value("imap/provider", "Configuration manuelle") if not email or not password: self.display_outlook_log("⚠️ Aucune configuration IMAP à synchroniser") return False # Vérifier si cette entrée existe déjà existing_entry = None for i, entry in enumerate(self.password_data): if (entry.get('category') == '📧 IMAP/Email' and entry.get('email') == email and entry.get('site') == server): existing_entry = i break # Créer ou mettre à jour l'entrée imap_entry = { 'name': f"IMAP - {provider}", 'email': email, 'site': f"{server}:{port}", 'category': '📧 IMAP/Email', 'password': password, 'notes': f"Provider: {provider}\nSSL: {'Oui' if use_ssl else 'Non'}\nSynchronisé automatiquement", 'modified': datetime.now().strftime("%d/%m/%Y %H:%M"), 'imap_sync': True, # Marqueur pour la synchronisation 'imap_server': server, 'imap_port': port, 'imap_ssl': use_ssl, 'imap_provider': provider } if existing_entry is not None: # Mettre à jour l'entrée existante imap_entry['created'] = self.password_data[existing_entry].get('created', imap_entry['modified']) self.password_data[existing_entry] = imap_entry action = "mise à jour" else: # Ajouter nouvelle entrée imap_entry['created'] = imap_entry['modified'] self.password_data.append(imap_entry) action = "ajout" # Sauvegarder self.save_password_data() self.update_password_table() self.display_outlook_log(f"✅ Configuration IMAP synchronisée vers le gestionnaire ({action})") return True except Exception as e: self.display_outlook_log(f"❌ Erreur synchronisation IMAP → Gestionnaire: {e}") return False def sync_password_manager_to_imap(self, entry_index=None): """Configure IMAP depuis une entrée du gestionnaire de mots de passe""" try: # Si aucun index spécifié, chercher la première entrée IMAP if entry_index is None: imap_entries = self.get_imap_entries_from_password_manager() if not imap_entries: QMessageBox.information(self, "Aucune entrée IMAP", "Aucune entrée IMAP trouvée dans le gestionnaire de mots de passe.") return False if len(imap_entries) > 1: # Afficher un dialogue de sélection entry_index = self.select_imap_entry_dialog(imap_entries) if entry_index is None: return False else: entry_index = imap_entries[0]['index'] # Récupérer l'entrée sélectionnée if entry_index >= len(self.password_data): QMessageBox.warning(self, "Erreur", "Entrée invalide sélectionnée.") return False entry = self.password_data[entry_index] # Vérifier que c'est bien une entrée IMAP if entry.get('category') != '📧 IMAP/Email': QMessageBox.warning(self, "Erreur", "L'entrée sélectionnée n'est pas une configuration IMAP.") return False # Extraire les paramètres IMAP email = entry.get('email', '') password = entry.get('password', '') # Paramètres étendus si disponibles if entry.get('imap_sync'): server = entry.get('imap_server', '') port = entry.get('imap_port', 993) use_ssl = entry.get('imap_ssl', True) provider = entry.get('imap_provider', 'Configuration manuelle') else: # Parser depuis le champ site site = entry.get('site', '') if ':' in site: server, port_str = site.split(':', 1) try: port = int(port_str) except ValueError: port = 993 else: server = site port = 993 use_ssl = True provider = "Configuration manuelle" if not all([email, password, server]): QMessageBox.warning(self, "Données incomplètes", "L'entrée sélectionnée ne contient pas toutes les informations IMAP nécessaires.") return False # Confirmation avant application confirmation = QMessageBox.question( self, "Confirmer la synchronisation", f"Voulez-vous configurer IMAP avec ces paramètres ?\n\n" f"Email: {email}\n" f"Serveur: {server}:{port}\n" f"Provider: {provider}\n" f"SSL: {'Oui' if use_ssl else 'Non'}\n\n" f"Cela remplacera la configuration IMAP actuelle.", QMessageBox.Yes | QMessageBox.No ) if confirmation != QMessageBox.Yes: return False # Appliquer la configuration IMAP settings = QSettings("Snoown2k", "MyApp") settings.setValue("imap/email", email) settings.setValue("imap/password", password) settings.setValue("imap/server", server) settings.setValue("imap/port", port) settings.setValue("imap/use_ssl", use_ssl) settings.setValue("imap/provider", provider) settings.sync() # Réinitialiser l'ImapHandler self.imap_handler = None # Mettre à jour l'affichage de la méthode de connexion si nécessaire if hasattr(self, 'connection_method_combo'): self.connection_method_combo.setCurrentText("IMAP") self.toggle_connection_panels() self.display_outlook_log(f"✅ Configuration IMAP mise à jour depuis le gestionnaire") QMessageBox.information(self, "Synchronisation réussie", f"Configuration IMAP mise à jour avec les paramètres de '{entry.get('name')}'") return True except Exception as e: self.display_outlook_log(f"❌ Erreur synchronisation Gestionnaire → IMAP: {e}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la synchronisation: {str(e)}") return False def get_imap_entries_from_password_manager(self): """Récupère toutes les entrées IMAP du gestionnaire de mots de passe""" try: imap_entries = [] for i, entry in enumerate(self.password_data): if entry.get('category') == '📧 IMAP/Email': imap_entries.append({ 'index': i, 'name': entry.get('name', ''), 'email': entry.get('email', ''), 'site': entry.get('site', ''), 'provider': entry.get('imap_provider', 'Inconnu') }) return imap_entries except Exception as e: print(f"Erreur récupération entrées IMAP: {e}") return [] def select_imap_entry_dialog(self, imap_entries): """Affiche un dialogue pour sélectionner une entrée IMAP""" try: from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QDialogButtonBox, \ QLabel, QListWidgetItem dialog = QDialog(self) dialog.setWindowTitle("Sélectionner une configuration IMAP") dialog.setModal(True) dialog.setMinimumWidth(400) layout = QVBoxLayout(dialog) # Instructions label = QLabel("Plusieurs configurations IMAP trouvées.\nSélectionnez celle à utiliser :") layout.addWidget(label) # Liste des entrées list_widget = QListWidget() for entry in imap_entries: item_text = f"{entry['name']} ({entry['email']} - {entry['provider']})" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, entry['index']) list_widget.addItem(item) layout.addWidget(list_widget) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) # Sélectionner le premier élément par défaut if imap_entries: list_widget.setCurrentRow(0) if dialog.exec_() == QDialog.Accepted: current_item = list_widget.currentItem() if current_item: return current_item.data(Qt.UserRole) return None except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur dialogue sélection: {str(e)}") return None def auto_sync_imap(self): """Synchronisation automatique bidirectionnelle IMAP ↔ Gestionnaire""" try: if not hasattr(self, 'password_data'): return # Gestionnaire pas encore initialisé settings = QSettings("Snoown2k", "MyApp") auto_sync_enabled = settings.value("password_manager/auto_sync_imap", True, type=bool) if not auto_sync_enabled: return # Vérifier s'il y a une configuration IMAP imap_email = settings.value("imap/email", "").strip() imap_password = settings.value("imap/password", "") # Vérifier s'il y a des entrées IMAP dans le gestionnaire imap_entries = self.get_imap_entries_from_password_manager() if imap_email and imap_password and not imap_entries: # Cas 1: Configuration IMAP existe mais pas dans le gestionnaire → Importer self.display_outlook_log("🔄 Synchronisation automatique: IMAP → Gestionnaire") return self.sync_imap_to_password_manager() elif imap_entries and not imap_email: # Cas 2: Entrées gestionnaire existent mais pas de config IMAP → Exporter self.display_outlook_log("🔄 Synchronisation automatique: Gestionnaire → IMAP") return self.sync_password_manager_to_imap(imap_entries[0]['index']) elif imap_entries and imap_email: # Cas 3: Les deux existent → Vérifier la cohérence first_entry = self.password_data[imap_entries[0]['index']] if (first_entry.get('email') != imap_email or first_entry.get('password') != imap_password): # Demander à l'utilisateur quelle version garder self.display_outlook_log("⚠️ Détection de configurations IMAP différentes") return self.resolve_imap_sync_conflict(first_entry, imap_email, imap_password) return True except Exception as e: print(f"Erreur synchronisation automatique: {e}") return False def resolve_imap_sync_conflict(self, manager_entry, imap_email, imap_password): """Résout les conflits de synchronisation IMAP""" try: from PyQt5.QtWidgets import QMessageBox reply = QMessageBox.question( self, "Conflit de synchronisation IMAP", f"Deux configurations IMAP différentes détectées :\n\n" f"📱 Gestionnaire de mots de passe:\n" f" Email: {manager_entry.get('email')}\n" f" Serveur: {manager_entry.get('site')}\n\n" f"⚙️ Configuration IMAP actuelle:\n" f" Email: {imap_email}\n\n" f"Quelle configuration voulez-vous conserver ?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel ) # Personnaliser les textes des boutons yes_button = reply.button(QMessageBox.StandardButton.Yes) no_button = reply.button(QMessageBox.StandardButton.No) cancel_button = reply.button(QMessageBox.StandardButton.Cancel) yes_button.setText("Gestionnaire → IMAP") no_button.setText("IMAP → Gestionnaire") cancel_button.setText("Annuler") reply.exec_() clicked_button = reply.clickedButton() if clicked_button == yes_button: # Utiliser la version du gestionnaire return self.sync_password_manager_to_imap() elif clicked_button == no_button: # Utiliser la version IMAP return self.sync_imap_to_password_manager() else: # Annuler - ne rien faire self.display_outlook_log("🚫 Synchronisation annulée par l'utilisateur") return False except Exception as e: print(f"Erreur résolution conflit: {e}") return False def add_sync_buttons_to_password_tab(self): """Ajoute les boutons de synchronisation IMAP à l'interface""" try: # Cette méthode sera appelée après la création de l'onglet if not hasattr(self, 'password_tab'): return # Trouver la toolbar toolbar_layout = None for i in range(self.password_tab.layout().count()): item = self.password_tab.layout().itemAt(i) if isinstance(item, QHBoxLayout) and item.count() > 4: # La toolbar a plusieurs boutons toolbar_layout = item break if not toolbar_layout: return # Ajouter un séparateur separator = QFrame() separator.setFrameShape(QFrame.VLine) separator.setFrameShadow(QFrame.Sunken) separator.setStyleSheet("color: #7f8c8d;") toolbar_layout.insertWidget(toolbar_layout.count() - 3, separator) # Avant les filtres # Bouton synchronisation IMAP → Gestionnaire self.sync_imap_to_manager_button = QPushButton("📧→🔐") self.sync_imap_to_manager_button.setToolTip("Importer la configuration IMAP dans le gestionnaire") self.sync_imap_to_manager_button.setMinimumWidth(60) self.sync_imap_to_manager_button.clicked.connect(self.sync_imap_to_password_manager) # Bouton synchronisation Gestionnaire → IMAP self.sync_manager_to_imap_button = QPushButton("🔐→📧") self.sync_manager_to_imap_button.setToolTip("Configurer IMAP depuis le gestionnaire") self.sync_manager_to_imap_button.setMinimumWidth(60) self.sync_manager_to_imap_button.clicked.connect(lambda: self.sync_password_manager_to_imap()) # Bouton synchronisation automatique self.auto_sync_button = QPushButton("🔄") self.auto_sync_button.setToolTip("Synchronisation automatique bidirectionnelle") self.auto_sync_button.setMinimumWidth(40) self.auto_sync_button.clicked.connect(self.auto_sync_imap) # Bouton niveau de sécurité self.security_level_button = QPushButton("🛡️ Sécurité") self.security_level_button.setToolTip("Changer le niveau de sécurité") self.security_level_button.setMinimumWidth(80) self.security_level_button.clicked.connect(self.change_security_level) # Style pour les boutons de sync sync_button_style = """ QPushButton { background-color: #e67e22; color: white; border: none; padding: 8px 6px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #d35400; } QPushButton:pressed { background-color: #a04000; } """ for btn in [self.sync_imap_to_manager_button, self.sync_manager_to_imap_button, self.auto_sync_button]: btn.setStyleSheet(sync_button_style) # Insérer les boutons avant les filtres insert_position = toolbar_layout.count() - 3 toolbar_layout.insertWidget(insert_position, self.sync_imap_to_manager_button) toolbar_layout.insertWidget(insert_position + 1, self.sync_manager_to_imap_button) toolbar_layout.insertWidget(insert_position + 2, self.auto_sync_button) except Exception as e: print(f"Erreur ajout boutons sync: {e}") # Méthode à appeler après l'initialisation du gestionnaire de mots de passe def finalize_password_manager_setup(self): """Finalise la configuration du gestionnaire avec synchronisation IMAP""" try: # Ajouter les boutons de synchronisation self.add_sync_buttons_to_password_tab() # Effectuer une synchronisation automatique au démarrage QTimer.singleShot(1000, self.auto_sync_imap) self.display_outlook_log("🔐 Gestionnaire de mots de passe initialisé avec synchronisation IMAP") except Exception as e: print(f"Erreur finalisation gestionnaire: {e}") def setup_security_level_dialog(self): """Configure le niveau de sécurité du gestionnaire de mots de passe""" try: from PyQt5.QtWidgets import QDialog, QVBoxLayout, QRadioButton, QButtonGroup, \ QLabel, QDialogButtonBox, QGroupBox, QHBoxLayout, QTextEdit dialog = QDialog(self) dialog.setWindowTitle("🔐 Configuration de la sécurité") dialog.setModal(True) dialog.setMinimumWidth(600) dialog.setMinimumHeight(500) layout = QVBoxLayout(dialog) # En-tête header = QLabel("Choisissez votre niveau de sécurité pour le gestionnaire de mots de passe") header.setStyleSheet("font-size: 14px; font-weight: bold; color: white; margin-bottom: 15px;") layout.addWidget(header) # Groupe de boutons radio self.security_group = QButtonGroup() # === NIVEAU 4 - SÉCURITÉ MAXIMALE === level4_group = QGroupBox("🔐 Niveau 4 - Sécurité maximale") level4_layout = QVBoxLayout(level4_group) self.level4_radio = QRadioButton("Chiffrement AES-256 avec mot de passe maître") self.level4_radio.setStyleSheet("font-weight: bold; color: #e74c3c;") level4_desc = QTextEdit() level4_desc.setMaximumHeight(80) level4_desc.setHtml(""" Avantages: Sécurité maximale, protection même si le fichier est volé
Inconvénients: Mot de passe à retenir, demandé à chaque utilisation
Idéal pour: Données sensibles, environnements professionnels """) level4_desc.setStyleSheet("background-color: #2c3e50; border: 1px solid #e74c3c;") level4_layout.addWidget(self.level4_radio) level4_layout.addWidget(level4_desc) # === NIVEAU 3 - SÉCURITÉ AUTOMATIQUE === level3_group = QGroupBox("🔒 Niveau 3 - Sécurité automatique (Recommandé)") level3_layout = QVBoxLayout(level3_group) self.level3_radio = QRadioButton("Chiffrement avec clé machine (aucun mot de passe)") self.level3_radio.setStyleSheet("font-weight: bold; color: #f39c12;") self.level3_radio.setChecked(True) # Défaut recommandé level3_desc = QTextEdit() level3_desc.setMaximumHeight(80) level3_desc.setHtml(""" Avantages: Sécurité élevée, aucun mot de passe à retenir, accès automatique
Inconvénients: Données liées à cette machine uniquement
Idéal pour: Usage personnel, PC fixe, équilibre sécurité/praticité """) level3_desc.setStyleSheet("background-color: #2c3e50; border: 1px solid #f39c12;") level3_layout.addWidget(self.level3_radio) level3_layout.addWidget(level3_desc) # === NIVEAU 2 - SÉCURITÉ BASIQUE === level2_group = QGroupBox("🔓 Niveau 2 - Sécurité basique") level2_layout = QVBoxLayout(level2_group) self.level2_radio = QRadioButton("Obfuscation simple des mots de passe") self.level2_radio.setStyleSheet("font-weight: bold; color: #3498db;") level2_desc = QTextEdit() level2_desc.setMaximumHeight(80) level2_desc.setHtml(""" Avantages: Accès rapide, protection contre lecture accidentelle
Inconvénients: Facilement contournable par quelqu'un de motivé
Idéal pour: Tests, développement, environnement contrôlé """) level2_desc.setStyleSheet("background-color: #2c3e50; border: 1px solid #3498db;") level2_layout.addWidget(self.level2_radio) level2_layout.addWidget(level2_desc) # === NIVEAU 1 - AUCUNE PROTECTION === level1_group = QGroupBox("📂 Niveau 1 - Aucune protection (Non recommandé)") level1_layout = QVBoxLayout(level1_group) self.level1_radio = QRadioButton("Stockage en clair (aucune protection)") self.level1_radio.setStyleSheet("font-weight: bold; color: #95a5a6;") level1_desc = QTextEdit() level1_desc.setMaximumHeight(80) level1_desc.setHtml(""" Avantages: Accès instantané, aucune complexité
Inconvénients: Aucune sécurité, mots de passe visibles par tous
Idéal pour: Tests uniquement, données non sensibles """) level1_desc.setStyleSheet("background-color: #2c3e50; border: 1px solid #95a5a6;") level1_layout.addWidget(self.level1_radio) level1_layout.addWidget(level1_desc) # Ajouter au groupe self.security_group.addButton(self.level4_radio, 4) self.security_group.addButton(self.level3_radio, 3) self.security_group.addButton(self.level2_radio, 2) self.security_group.addButton(self.level1_radio, 1) # Ajouter au layout layout.addWidget(level4_group) layout.addWidget(level3_group) layout.addWidget(level2_group) layout.addWidget(level1_group) # Note de sécurité security_note = QLabel("💡 Vous pourrez changer ce niveau à tout moment dans les paramètres.") security_note.setStyleSheet("color: #bdc3c7; font-style: italic; margin-top: 10px;") layout.addWidget(security_note) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) return dialog except Exception as e: print(f"Erreur création dialogue sécurité: {e}") return None def get_security_level(self): """Récupère ou configure le niveau de sécurité""" try: settings = QSettings("Snoown2k", "MyApp") saved_level = settings.value("password_manager/security_level", None) if saved_level is None: # Première utilisation - demander le niveau dialog = self.setup_security_level_dialog() if dialog and dialog.exec_() == QDialog.Accepted: selected_level = self.security_group.checkedId() settings.setValue("password_manager/security_level", selected_level) settings.sync() return selected_level else: # Annulé - utiliser le niveau 3 par défaut settings.setValue("password_manager/security_level", 3) settings.sync() return 3 else: return int(saved_level) except Exception as e: print(f"Erreur récupération niveau sécurité: {e}") return 3 # Niveau par défaut def setup_security_system(self): """Configure le système de sécurité selon le niveau choisi""" try: self.security_level = self.get_security_level() # Configurer selon le niveau if self.security_level == 4: # Niveau 4 - Mot de passe maître requis self.master_password = None self.encryption_key = None self.security_status_label.setText("🔐 Sécurité: Niveau 4 - Chiffrement AES-256") self.security_status_label.setStyleSheet("color: #e74c3c; font-weight: bold;") return self.authenticate_master_password() elif self.security_level == 3: # Niveau 3 - Clé machine automatique self.master_password = None self.encryption_key = self.generate_machine_key() self.security_status_label.setText("🔒 Sécurité: Niveau 3 - Chiffrement automatique") self.security_status_label.setStyleSheet("color: #f39c12; font-weight: bold;") return True elif self.security_level == 2: # Niveau 2 - Obfuscation simple self.master_password = None self.encryption_key = "simple_obfuscation_key_2024" self.security_status_label.setText("🔓 Sécurité: Niveau 2 - Obfuscation basique") self.security_status_label.setStyleSheet("color: #3498db; font-weight: bold;") return True else: # Niveau 1 # Niveau 1 - Aucune protection self.master_password = None self.encryption_key = None self.security_status_label.setText("📂 Sécurité: Niveau 1 - Aucune protection") self.security_status_label.setStyleSheet("color: #95a5a6; font-weight: bold;") return True except Exception as e: print(f"Erreur configuration sécurité: {e}") return False def generate_machine_key(self): """Génère une clé unique à la machine""" try: import hashlib import platform import os # Créer un identifiant unique basé sur la machine machine_info = ( platform.node() + # Nom de la machine platform.system() + # OS platform.processor() + # Processeur str(os.environ.get('USERNAME', '')) + # Utilisateur str(os.environ.get('COMPUTERNAME', '')) # Nom d'ordinateur ) # Générer un hash SHA-256 machine_hash = hashlib.sha256(machine_info.encode()).hexdigest() # Utiliser les premiers 32 caractères comme clé return machine_hash[:32] + "_password_manager_2024" except Exception as e: print(f"Erreur génération clé machine: {e}") return "fallback_machine_key_2024_default" def encrypt_data_by_level(self, data): """Chiffre les données selon le niveau de sécurité""" try: if self.security_level == 4: # Niveau 4 - AES-256 avec mot de passe maître return self.encrypt_password_data(data) elif self.security_level == 3: # Niveau 3 - AES-256 avec clé machine return self.encrypt_with_machine_key(data) elif self.security_level == 2: # Niveau 2 - Obfuscation simple return self.obfuscate_data(data) else: # Niveau 1 # Niveau 1 - Stockage en clair import json return json.dumps(data, indent=2) except Exception as e: print(f"Erreur chiffrement niveau {self.security_level}: {e}") return None def decrypt_data_by_level(self, encrypted_data): """Déchiffre les données selon le niveau de sécurité""" try: if self.security_level == 4: # Niveau 4 - AES-256 avec mot de passe maître return self.decrypt_password_data(encrypted_data) elif self.security_level == 3: # Niveau 3 - AES-256 avec clé machine return self.decrypt_with_machine_key(encrypted_data) elif self.security_level == 2: # Niveau 2 - Désofuscation simple return self.deobfuscate_data(encrypted_data) else: # Niveau 1 # Niveau 1 - Lecture directe import json return json.loads(encrypted_data) except Exception as e: print(f"Erreur déchiffrement niveau {self.security_level}: {e}") return [] def encrypt_with_machine_key(self, data): """Chiffrement avec clé machine (niveau 3)""" try: import json import base64 from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # Générer une clé Fernet à partir de la clé machine salt = b'machine_key_salt_2024_password_manager' kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=50000, # Moins d'itérations que le niveau 4 ) key = base64.urlsafe_b64encode(kdf.derive(self.encryption_key.encode())) # Chiffrer f = Fernet(key) json_data = json.dumps(data).encode() encrypted = f.encrypt(json_data) return base64.b64encode(encrypted).decode() except Exception as e: print(f"Erreur chiffrement machine: {e}") return None def decrypt_with_machine_key(self, encrypted_data): """Déchiffrement avec clé machine (niveau 3)""" try: import json import base64 from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # Régénérer la clé salt = b'machine_key_salt_2024_password_manager' kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=50000, ) key = base64.urlsafe_b64encode(kdf.derive(self.encryption_key.encode())) # Déchiffrer f = Fernet(key) encrypted_bytes = base64.b64decode(encrypted_data.encode()) decrypted_bytes = f.decrypt(encrypted_bytes) return json.loads(decrypted_bytes.decode()) except Exception as e: print(f"Erreur déchiffrement machine: {e}") return [] def obfuscate_data(self, data): """Obfuscation simple (niveau 2)""" try: import json import base64 # Simple encodage Base64 avec rotation json_data = json.dumps(data) # Rotation simple des caractères obfuscated = "" for char in json_data: obfuscated += chr((ord(char) + 13) % 256) # Encoder en Base64 return base64.b64encode(obfuscated.encode('latin-1')).decode() except Exception as e: print(f"Erreur obfuscation: {e}") return None def deobfuscate_data(self, obfuscated_data): """Désofuscation simple (niveau 2)""" try: import json import base64 # Décoder Base64 decoded = base64.b64decode(obfuscated_data.encode()).decode('latin-1') # Rotation inverse deobfuscated = "" for char in decoded: deobfuscated += chr((ord(char) - 13) % 256) return json.loads(deobfuscated) except Exception as e: print(f"Erreur désofuscation: {e}") return [] def change_security_level(self): """Permet de changer le niveau de sécurité""" try: dialog = self.setup_security_level_dialog() if not dialog: return # Sélectionner le niveau actuel current_level = getattr(self, 'security_level', 3) if current_level == 4: self.level4_radio.setChecked(True) elif current_level == 3: self.level3_radio.setChecked(True) elif current_level == 2: self.level2_radio.setChecked(True) else: self.level1_radio.setChecked(True) if dialog.exec_() == QDialog.Accepted: new_level = self.security_group.checkedId() if new_level != current_level: # Confirmer le changement confirmation = QMessageBox.question( self, "Confirmer le changement", f"Voulez-vous changer du niveau {current_level} au niveau {new_level} ?\n\n" f"Vos données seront automatiquement converties au nouveau format.", QMessageBox.Yes | QMessageBox.No ) if confirmation == QMessageBox.Yes: self.convert_security_level(current_level, new_level) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur changement sécurité: {str(e)}") def convert_security_level(self, old_level, new_level): """Convertit les données d'un niveau de sécurité à un autre""" try: # Sauvegarder les données actuelles (si elles existent) current_data = self.password_data.copy() if hasattr(self, 'password_data') else [] # Mettre à jour le niveau et reconfigurer settings = QSettings("Snoown2k", "MyApp") settings.setValue("password_manager/security_level", new_level) settings.sync() self.security_level = new_level # Reconfigurer le système de sécurité if not self.setup_security_system(): # Si échec (ex: annulation mot de passe niveau 4), restaurer l'ancien niveau settings.setValue("password_manager/security_level", old_level) self.security_level = old_level return # Resauvegarder les données avec le nouveau niveau self.password_data = current_data self.save_password_data() self.update_password_table() level_names = {4: "Sécurité maximale", 3: "Sécurité automatique", 2: "Sécurité basique", 1: "Aucune protection"} QMessageBox.information(self, "Conversion réussie", f"✅ Données converties vers le niveau {new_level}\n" f"({level_names.get(new_level, 'Inconnu')})") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur conversion: {str(e)}") def show_imap_sync_menu(self): """Affiche le menu de synchronisation IMAP""" try: from PyQt5.QtWidgets import QMenu if not hasattr(self, 'password_data'): QMessageBox.information(self, "Gestionnaire non disponible", "Le gestionnaire de mots de passe n'est pas encore initialisé.\n" "Allez dans l'onglet 'Gestionnaire de mots de passe' d'abord.") return menu = QMenu(self) # Actions du menu import_action = menu.addAction("📧→🔐 Importer IMAP vers Gestionnaire") export_action = menu.addAction("🔐→📧 Configurer IMAP depuis Gestionnaire") menu.addSeparator() auto_action = menu.addAction("🔄 Synchronisation automatique") # Afficher le menu action = menu.exec_(self.sync_imap_button.mapToGlobal(self.sync_imap_button.rect().bottomLeft())) # Traiter l'action sélectionnée if action == import_action: self.sync_imap_to_password_manager() elif action == export_action: self.sync_password_manager_to_imap() elif action == auto_action: self.auto_sync_imap() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur menu synchronisation: {str(e)}") def update_com_mode_label(self): """Met à jour l'indicateur d'état de la connexion COM/IMAP en temps réel""" try: # Vérifier que le widget existe if not hasattr(self, 'com_mode_label'): print("⚠️ com_mode_label pas encore créé") return # Déterminer le mode de connexion actuel current_mode = "COM" # Valeur par défaut if hasattr(self, 'connection_method_combo'): current_mode = self.connection_method_combo.currentText() # === GESTION DU MODE IMAP === if current_mode == "IMAP": # Pour IMAP, on assume qu'il est connecté si le gestionnaire existe imap_connected = False if hasattr(self, 'mail_handler') and self.mail_handler: imap_connected = True # Simplification : si le gestionnaire existe, on considère comme connecté # Affichage selon l'état de connexion IMAP if imap_connected: self.com_mode_label.setText("📡 Connexion IMAP : Connecté") self.com_mode_label.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f2c3f, stop:1 #2c5f2d); color: #00ff88; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #27ae60; """) else: self.com_mode_label.setText("📡 Connexion IMAP : Non initialisé") self.com_mode_label.setStyleSheet(""" background-color: #34495e; color: #95a5a6; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #7f8c8d; """) return # Sortir pour ne pas traiter le mode COM # === GESTION DU MODE COM === elif current_mode == "COM": # Vérifier si outlook_handler existe et est initialisé if hasattr(self, 'outlook_handler') and self.outlook_handler: permanent = getattr(self.outlook_handler, 'permanent_connection', False) connected = getattr(self.outlook_handler, 'outlook_connected', False) # Détails sur les tentatives de connexion attempts = getattr(self.outlook_handler, 'connection_attempts', 0) status_detail = f" (T#{attempts})" if attempts > 0 else "" # === MODE PERMANENT === if permanent: if connected: self.com_mode_label.setText(f"🔒 Connexion COM permanente • 🟢 Connecté{status_detail}") self.com_mode_label.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f2c3f, stop:1 #2c5f2d); color: #00ff88; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #27ae60; """) else: self.com_mode_label.setText(f"🔒 Connexion COM permanente • 🟢 Connecté{status_detail}") self.com_mode_label.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f2c3f, stop:1 #2c5f2d); color: #00ff88; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #27ae60; """) # === MODE PONCTUEL === else: if connected: self.com_mode_label.setText(f"🔌 Connexion COM ponctuelle • 🟢 Connecté{status_detail}") self.com_mode_label.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f2c3f, stop:1 #2c5f2d); color: #00ff88; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #27ae60; """) else: self.com_mode_label.setText(f"🔌 Connexion COM ponctuelle • 🟡 En attente{status_detail}") self.com_mode_label.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f2c3f, stop:1 #5d4e37); color: #ffa500; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #e67e22; """) else: # OutlookHandler pas initialisé - afficher le mode sauvegardé settings = QSettings("Snoown2k", "MyApp") saved_mode = settings.value("outlook/permanent_connection", False, type=bool) if saved_mode: self.com_mode_label.setText("🔒 Mode permanent sauvegardé • ⚪ Non initialisé") else: self.com_mode_label.setText("🔌 Mode ponctuel sauvegardé • ⚪ Non initialisé") self.com_mode_label.setStyleSheet(""" background-color: #34495e; color: #95a5a6; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #7f8c8d; """) # === MODE INCONNU === else: self.com_mode_label.setText(f"❓ Mode inconnu: {current_mode}") self.com_mode_label.setStyleSheet(""" background-color: #34495e; color: #95a5a6; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #7f8c8d; """) except Exception as e: print(f"Erreur dans update_com_mode_label: {e}") if hasattr(self, 'com_mode_label'): self.com_mode_label.setText("❌ Erreur de mise à jour") self.com_mode_label.setStyleSheet(""" background-color: #34495e; color: #95a5a6; font-size: 11px; font-weight: bold; padding: 8px; border-radius: 4px; border: 1px solid #7f8c8d; """) def toggle_connection_mode(self, state): """Gère le changement de mode de connexion COM""" mode = state == Qt.Checked settings = QSettings("Snoown2k", "MyApp") settings.setValue("outlook/permanent_connection", mode) settings.sync() # CORRECTION: Vérifier si outlook_handler existe if hasattr(self, "outlook_handler") and self.outlook_handler: old_mode = getattr(self.outlook_handler, 'permanent_connection', False) self.outlook_handler.permanent_connection = mode # Gérer le changement de mode if mode and not old_mode: # Passage en mode permanent self.outlook_handler.log_action("🔒 Activation du mode connexion permanente") # Arrêter le timer de déconnexion s'il est actif if hasattr(self.outlook_handler, 'auto_disconnect_timer') and self.outlook_handler.auto_disconnect_timer.isActive(): self.outlook_handler.auto_disconnect_timer.stop() self.outlook_handler.log_action("⏹️ Timer de déconnexion arrêté") elif not mode and old_mode: # Passage en mode ponctuel self.outlook_handler.log_action("🔌 Activation du mode connexion ponctuelle") # Redémarrer le timer de déconnexion if hasattr(self.outlook_handler, 'reset_auto_disconnect_timer'): self.outlook_handler.reset_auto_disconnect_timer() self.outlook_handler.log_action( f"{'✅ Connexion COM permanente activée' if mode else '🔌 Connexion COM ponctuelle activée'}") # Forcer la notification du changement if hasattr(self.outlook_handler, 'notify_connection_change'): self.outlook_handler.notify_connection_change() else: # OutlookHandler pas encore initialisé print(f"⚠️ OutlookHandler non initialisé - Mode sauvegardé: {'Permanent' if mode else 'Ponctuel'}") if hasattr(self, 'outlook_log_display'): self.outlook_log_display.append( f"⚠️ Mode {'permanent' if mode else 'ponctuel'} sauvegardé (OutlookHandler non initialisé)") # Mettre à jour l'indicateur d'état immédiatement self.update_com_mode_label() def refresh_com_status(self): """Force la mise à jour de l'état COM""" try: if hasattr(self, 'outlook_handler') and self.outlook_handler: # Forcer une vérification de connexion is_connected = self.outlook_handler.check_connection() # CORRECTION: Mettre à jour l'état dans outlook_handler self.outlook_handler.outlook_connected = is_connected # Log avec le bon statut self.outlook_handler.log_action( f"🔍 Vérification de l'état COM: {'Connecté' if is_connected else 'Connecté'}") # Forcer la notification du changement if hasattr(self.outlook_handler, 'notify_connection_change'): self.outlook_handler.notify_connection_change() else: print("⚠️ OutlookHandler non initialisé lors du rafraîchissement") if hasattr(self, 'outlook_log_display'): self.display_outlook_log("⚠️ OutlookHandler non initialisé") # Mettre à jour l'affichage après avoir mis à jour l'état self.update_com_mode_label() # Effet visuel temporaire pour montrer que l'action a été effectuée if hasattr(self, 'refresh_status_button'): original_style = self.refresh_status_button.styleSheet() self.refresh_status_button.setStyleSheet(original_style + "background-color: #27ae60;") QTimer.singleShot(200, lambda: self.refresh_status_button.setStyleSheet(original_style)) except Exception as e: print(f"Erreur lors du rafraîchissement de l'état COM: {e}") if hasattr(self, 'outlook_log_display'): self.display_outlook_log(f"❌ Erreur rafraîchissement: {e}") def test_com_connection(self): """Teste la connexion COM Outlook et liste les VRAIES adresses mails""" try: # Réinitialiser le marqueur de fin de test self._test_com_completed = False self.display_outlook_log("🔍 Test de connexion COM Outlook en cours...") # Tentative de connexion COM réelle à Outlook try: import win32com.client self.display_outlook_log("📚 Module win32com importé avec succès") # Se connecter à Outlook outlook_app = win32com.client.Dispatch("Outlook.Application") self.display_outlook_log("✅ Connexion COM Outlook établie") # Récupérer le namespace MAPI namespace = outlook_app.GetNamespace("MAPI") self.display_outlook_log("📂 Namespace MAPI récupéré") # Récupérer tous les comptes accounts = outlook_app.Session.Accounts self.display_outlook_log(f"📧 {accounts.Count} compte(s) Outlook détecté(s) :") if accounts.Count == 0: self.display_outlook_log("⚠️ Aucun compte Outlook configuré") return # Parcourir chaque compte for i in range(1, accounts.Count + 1): account = accounts.Item(i) account_name = account.DisplayName account_email = getattr(account, 'SmtpAddress', 'Email non disponible') self.display_outlook_log(f"") self.display_outlook_log(f"📧 Compte {i}: {account_name}") self.display_outlook_log(f" 📮 Email: {account_email}") try: # Récupérer le store associé au compte delivery_store = account.DeliveryStore root_folder = delivery_store.GetRootFolder() self.display_outlook_log(f" 📁 Dossiers disponibles:") # Lister les dossiers principaux self.list_folders(root_folder, indent=" ") except Exception as folder_error: self.display_outlook_log(f" ⚠️ Erreur accès dossiers: {folder_error}") # Message de fin de test (une seule fois) self.display_outlook_log("") if not self._test_com_completed: self.display_outlook_log("💡 Test COM terminé avec succès - Données réelles récupérées") self._test_com_completed = True # Mettre à jour l'état de connexion if hasattr(self, 'outlook_handler') and self.outlook_handler: self.outlook_handler.outlook_connected = True self.display_outlook_log("🔄 État de connexion mis à jour") # Forcer la mise à jour du bandeau d'état self.update_com_mode_label() except ImportError: self.display_outlook_log("❌ Module win32com non disponible") self.display_outlook_log("💡 Installez: pip install pywin32") except Exception as com_error: self.display_outlook_log(f"❌ Erreur connexion COM: {com_error}") self.display_outlook_log("💡 Vérifiez qu'Outlook est ouvert et configuré") except Exception as e: self.display_outlook_log(f"❌ Erreur générale: {e}") def start_com_stress_test(self): """Lance un stress test du module COM pour reproduire les crashs""" try: # Vérifier qu'on est bien en mode COM if hasattr(self, 'connection_method_combo') and self.connection_method_combo.currentText() != "COM": self.display_outlook_log("❌ Stress test disponible uniquement en mode COM") return # Dialogue de configuration du stress test config_dialog = self.create_stress_test_config_dialog() if config_dialog.exec_() != QDialog.Accepted: return # Récupérer les paramètres max_emails = config_dialog.max_emails_spinbox.value() delay_between_emails = config_dialog.delay_spinbox.value() simulate_links = config_dialog.simulate_links_checkbox.isChecked() force_cleanup = config_dialog.force_cleanup_checkbox.isChecked() # Confirmation finale confirmation = QMessageBox.question( self, "⚡ Lancer le Stress Test COM", f"Paramètres du stress test :\n\n" f"• Nombre max d'emails : {max_emails}\n" f"• Délai entre emails : {delay_between_emails}ms\n" f"• Simuler clics sur liens : {'Oui' if simulate_links else 'Non'}\n" f"• Nettoyage forcé : {'Oui' if force_cleanup else 'Non'}\n\n" f"⚠️ Ce test peut provoquer un crash volontaire d'Outlook.\n" f"Voulez-vous continuer ?", QMessageBox.Yes | QMessageBox.No ) if confirmation != QMessageBox.Yes: return # Désactiver le bouton pendant le test self.stress_test_com_button.setEnabled(False) self.stress_test_com_button.setText("⏳ Test en cours...") # Démarrer le stress test self.display_outlook_log("⚡ DÉBUT DU STRESS TEST COM") self.display_outlook_log(f"📊 Paramètres: {max_emails} emails, {delay_between_emails}ms délai") # Lancer le test dans un timer pour éviter de bloquer l'interface self.stress_test_params = { 'max_emails': max_emails, 'delay': delay_between_emails, 'simulate_links': simulate_links, 'force_cleanup': force_cleanup, 'current_email': 0, 'start_time': time.time(), 'errors': [], 'outlook_app': None, 'namespace': None } # Timer pour exécuter le stress test par étapes self.stress_test_timer = QTimer() self.stress_test_timer.timeout.connect(self.execute_stress_test_step) self.stress_test_timer.start(100) # Vérifier toutes les 100ms except Exception as e: self.display_outlook_log(f"❌ Erreur démarrage stress test: {e}") self.stress_test_com_button.setEnabled(True) self.stress_test_com_button.setText("⚡ Stress Test COM") def create_stress_test_config_dialog(self): """Crée le dialogue de configuration du stress test""" try: from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QSpinBox, QCheckBox, \ QPushButton, QDialogButtonBox, QLabel, QGroupBox dialog = QDialog(self) dialog.setWindowTitle("⚡ Configuration Stress Test COM") dialog.setModal(True) dialog.setMinimumWidth(450) layout = QVBoxLayout(dialog) # En-tête header = QLabel("⚠️ Configuration du Stress Test pour Module COM") header.setStyleSheet("font-size: 14px; font-weight: bold; color: #e74c3c; margin-bottom: 15px;") layout.addWidget(header) # Groupe paramètres params_group = QGroupBox("📊 Paramètres du test") form_layout = QFormLayout(params_group) # Nombre maximum d'emails dialog.max_emails_spinbox = QSpinBox() dialog.max_emails_spinbox.setRange(1, 50) dialog.max_emails_spinbox.setValue(10) # Valeur par défaut dialog.max_emails_spinbox.setToolTip("Nombre d'emails à traiter avant arrêt du test") form_layout.addRow("📧 Nombre max d'emails:", dialog.max_emails_spinbox) # Délai entre les emails dialog.delay_spinbox = QSpinBox() dialog.delay_spinbox.setRange(0, 5000) dialog.delay_spinbox.setValue(500) # 500ms par défaut dialog.delay_spinbox.setSuffix(" ms") dialog.delay_spinbox.setToolTip("Délai en millisecondes entre chaque traitement d'email") form_layout.addRow("⏱️ Délai entre emails:", dialog.delay_spinbox) layout.addWidget(params_group) # Groupe options avancées advanced_group = QGroupBox("🔧 Options avancées") advanced_layout = QVBoxLayout(advanced_group) # Simuler les clics sur liens dialog.simulate_links_checkbox = QCheckBox("Simuler les clics sur liens (plus intensif)") dialog.simulate_links_checkbox.setChecked(True) dialog.simulate_links_checkbox.setToolTip( "Active la simulation des clics sur liens qui causent souvent les crashs") advanced_layout.addWidget(dialog.simulate_links_checkbox) # Nettoyage forcé des objets COM dialog.force_cleanup_checkbox = QCheckBox("Forcer le nettoyage des objets COM") dialog.force_cleanup_checkbox.setChecked(True) dialog.force_cleanup_checkbox.setToolTip("Force la libération des objets COM après chaque email") advanced_layout.addWidget(dialog.force_cleanup_checkbox) layout.addWidget(advanced_group) # Avertissement warning = QLabel("⚠️ ATTENTION: Ce test est conçu pour reproduire les crashs de manière contrôlée.\n" "Il peut provoquer l'arrêt d'Outlook et de l'application.") warning.setStyleSheet("color: #e67e22; font-weight: bold; padding: 10px; " "background-color: #2c3e50; border-radius: 4px;") warning.setWordWrap(True) layout.addWidget(warning) # Boutons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) return dialog except Exception as e: self.display_outlook_log(f"❌ Erreur création dialogue config: {e}") return None def execute_stress_test_step(self): """Exécute une étape du stress test""" try: params = self.stress_test_params # Vérifier si on a atteint la limite if params['current_email'] >= params['max_emails']: self.finish_stress_test("✅ Test terminé - Limite atteinte sans crash") return # Première étape : se connecter à Outlook if params['outlook_app'] is None: try: import win32com.client params['outlook_app'] = win32com.client.Dispatch("Outlook.Application") params['namespace'] = params['outlook_app'].GetNamespace("MAPI") self.display_outlook_log("🔌 Connexion COM Outlook établie pour le stress test") return # Sortir pour laisser le timer continuer except Exception as connect_error: self.finish_stress_test(f"❌ Échec connexion COM: {connect_error}") return # Traiter l'email actuel email_num = params['current_email'] + 1 self.display_outlook_log(f"📧 Traitement email #{email_num}/{params['max_emails']}") # Variables pour les objets COM (important pour la libération) inbox = None items = None mail_item = None try: # Simuler le traitement intensif qui cause le crash inbox = params['namespace'].GetDefaultFolder(6) # 6 = olFolderInbox # Récupérer la collection d'emails items = inbox.Items if items.Count > 0: # Prendre un email différent à chaque fois pour simuler un vrai scan # Utiliser l'index basé sur current_email pour parcourir les emails email_index = min(params['current_email'] + 1, items.Count) mail_item = items.Item(email_index) # Simuler la lecture des propriétés (opération qui peut causer des fuites mémoire) subject = getattr(mail_item, 'Subject', 'Pas de sujet') sender = getattr(mail_item, 'SenderName', 'Pas d\'expéditeur') # CRITIQUE : Accès au corps de l'email (souvent source de crash) try: body = getattr(mail_item, 'Body', 'Pas de corps') if len(body) > 100: body = body[:100] except Exception as body_error: body = f"Erreur lecture corps: {body_error}" self.display_outlook_log(f" 📄 Sujet: {subject[:50]}...") self.display_outlook_log(f" 👤 Expéditeur: {sender[:30]}...") # Simuler les clics sur liens si activé (PARTIE CRITIQUE POUR LES CRASHS) if params['simulate_links']: # Simulation de recherche de liens dans le corps links_found = body.count('http') + body.count('www.') + body.count('mailto:') self.display_outlook_log(f" 🔗 Liens détectés: {links_found}") # Simuler des opérations intensives sur les liens (cause probable du crash) for link_num in range(min(links_found, 3)): # Max 3 liens par email self.display_outlook_log(f" ⚡ Simulation clic lien #{link_num + 1}") # Simuler l'accès aux propriétés liées aux liens (très intensif en COM) try: # Accès multiple aux propriétés (crée une charge sur COM) _ = getattr(mail_item, 'HTMLBody', '')[:50] _ = getattr(mail_item, 'Size', 0) _ = getattr(mail_item, 'ReceivedTime', None) # Pause pour simuler le traitement du lien time.sleep(0.01) except Exception as link_error: self.display_outlook_log(f" ❌ Erreur simulation lien: {link_error}") # SIMULATION SUPPLÉMENTAIRE : Accès aux attachments (autre source de crash) try: attachments = getattr(mail_item, 'Attachments', None) if attachments and attachments.Count > 0: self.display_outlook_log(f" 📎 Pièces jointes: {attachments.Count}") # Accéder aux propriétés des attachments (intensif) for i in range(min(attachments.Count, 2)): # Max 2 attachments try: attachment = attachments.Item(i + 1) filename = getattr(attachment, 'FileName', 'Inconnu') size = getattr(attachment, 'Size', 0) self.display_outlook_log(f" 📎 {filename} ({size} bytes)") # Libérer l'objet attachment del attachment except Exception as attach_error: self.display_outlook_log(f" ❌ Erreur attachment: {attach_error}") # Libérer la collection attachments del attachments except Exception as attachments_error: self.display_outlook_log(f" ⚠️ Erreur accès attachments: {attachments_error}") else: self.display_outlook_log(" ⚠️ Aucun email trouvé dans la boîte de réception") except Exception as processing_error: error_msg = f"❌ Erreur email #{email_num}: {processing_error}" self.display_outlook_log(error_msg) params['errors'].append(error_msg) # Si trop d'erreurs, arrêter le test if len(params['errors']) >= 3: self.finish_stress_test(f"❌ Test arrêté - Trop d'erreurs ({len(params['errors'])})") return finally: # NETTOYAGE FORCÉ DES OBJETS COM (CRITIQUE POUR ÉVITER LES FUITES) if params['force_cleanup']: try: # Ordre important : libérer dans l'ordre inverse de création if mail_item is not None: del mail_item self.display_outlook_log(" 🧹 mail_item libéré") if items is not None: del items self.display_outlook_log(" 🧹 items libéré") if inbox is not None: del inbox self.display_outlook_log(" 🧹 inbox libéré") # Forcer le garbage collection Python import gc collected = gc.collect() if collected > 0: self.display_outlook_log(f" 🧹 Garbage collection: {collected} objets libérés") except Exception as cleanup_error: self.display_outlook_log(f" ⚠️ Erreur nettoyage: {cleanup_error}") # Incrémenter le compteur params['current_email'] += 1 # Pause configurée entre les emails if params['delay'] > 0: time.sleep(params['delay'] / 1000.0) # Convertir ms en secondes # Vérification supplémentaire : détecter si Outlook répond encore try: # Test rapide de connectivité COM _ = params['outlook_app'].Version except Exception as connectivity_error: self.finish_stress_test(f"💥 CRASH DÉTECTÉ - Outlook ne répond plus: {connectivity_error}") return except Exception as e: self.finish_stress_test(f"❌ Erreur critique dans le stress test: {e}") def finish_stress_test(self, reason): """Termine le stress test et affiche les résultats""" try: # Arrêter le timer if hasattr(self, 'stress_test_timer'): self.stress_test_timer.stop() params = self.stress_test_params # Calculer la durée duration = time.time() - params['start_time'] # Nettoyer les objets COM try: if params.get('outlook_app'): del params['outlook_app'] if params.get('namespace'): del params['namespace'] import gc gc.collect() except: pass # Afficher les résultats self.display_outlook_log("") self.display_outlook_log("📊 RÉSULTATS DU STRESS TEST COM") self.display_outlook_log(f"🔚 Raison d'arrêt: {reason}") self.display_outlook_log(f"📧 Emails traités: {params['current_email']}/{params['max_emails']}") self.display_outlook_log(f"⏱️ Durée: {duration:.2f} secondes") self.display_outlook_log(f"⚡ Vitesse: {params['current_email'] / duration:.2f} emails/sec") if params['errors']: self.display_outlook_log(f"❌ Erreurs rencontrées: {len(params['errors'])}") for error in params['errors'][:3]: # Afficher max 3 erreurs self.display_outlook_log(f" {error}") # Point de crash détecté ? if params['current_email'] < params['max_emails'] and "crash" not in reason.lower(): self.display_outlook_log(f"🎯 POINT DE CRASH POTENTIEL: Environ {params['current_email']} emails") # Réactiver le bouton self.stress_test_com_button.setEnabled(True) self.stress_test_com_button.setText("⚡ Stress Test COM") except Exception as e: self.display_outlook_log(f"❌ Erreur finalisation stress test: {e}") self.stress_test_com_button.setEnabled(True) self.stress_test_com_button.setText("⚡ Stress Test COM") def list_folders(self, folder, indent="", max_depth=3, current_depth=0): """Fonction auxiliaire pour lister les dossiers récursivement""" try: if current_depth >= max_depth: return folder_name = folder.Name folder_count = folder.Items.Count if hasattr(folder, 'Items') else 0 self.display_outlook_log(f"{indent}• {folder_name} ({folder_count} éléments)") # Lister les sous-dossiers if hasattr(folder, 'Folders') and folder.Folders.Count > 0: for i in range(1, min(folder.Folders.Count + 1, 6)): # Limiter à 5 sous-dossiers try: subfolder = folder.Folders.Item(i) self.list_folders(subfolder, indent + " ", max_depth, current_depth + 1) except: pass except Exception as e: self.display_outlook_log(f"{indent}⚠️ Erreur lecture dossier: {e}") def setup_task_tab(self): """Configure l'onglet de gestion des tâches sans charger l'interface principale immédiatement""" try: layout = QHBoxLayout(self.task_tab) self.task_interface_loaded = False # Marqueur pour chargement différé # ====== PANNEAU DE CATÉGORIES À GAUCHE ====== categories_panel = QWidget() categories_layout = QVBoxLayout(categories_panel) categories_layout.setContentsMargins(0, 0, 0, 0) # 🔘 BOUTON D’ACTIVATION self.activate_interface_button = QPushButton("🚀 Activer l’interface") self.activate_interface_button.setStyleSheet("background-color: #2ecc71; color: white; font-weight: bold;") self.activate_interface_button.clicked.connect(self.activate_task_interface) categories_layout.addWidget(self.activate_interface_button) # 📂 FILTRES filter_title = QLabel("Filtres") filter_title.setStyleSheet("font-weight: bold; font-size: 14px; color: white;") categories_layout.addWidget(filter_title) self.category_system = QCheckBox("Processus système") self.category_system.setStyleSheet("color: #e74c3c;") self.category_system.stateChanged.connect(self.filter_tasks) self.category_sensitive = QCheckBox("Processus sensibles") self.category_sensitive.setStyleSheet("color: #e67e22;") self.category_sensitive.stateChanged.connect(self.filter_tasks) self.category_applications = QCheckBox("Applications") self.category_applications.setStyleSheet("color: #3498db;") self.category_applications.setChecked(True) self.category_applications.stateChanged.connect(self.filter_tasks) self.category_normal = QCheckBox("Processus normaux") self.category_normal.setStyleSheet("color: #2ecc71;") self.category_normal.stateChanged.connect(self.filter_tasks) self.category_blocked = QCheckBox("Processus bloqués") self.category_blocked.setStyleSheet("color: #9b59b6;") self.category_blocked.stateChanged.connect(self.filter_tasks) for checkbox in [self.category_system, self.category_sensitive, self.category_applications, self.category_normal, self.category_blocked]: categories_layout.addWidget(checkbox) # Séparateur line = QFrame() line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) line.setStyleSheet("background-color: #34495e;") categories_layout.addWidget(line) # Filtres supplémentaires filter_title2 = QLabel("Filtres supplémentaires") filter_title2.setStyleSheet("font-weight: bold; font-size: 14px; color: white; margin-top: 10px;") categories_layout.addWidget(filter_title2) self.filter_high_cpu = QCheckBox("CPU élevé (>5%)") self.filter_high_cpu.stateChanged.connect(self.filter_tasks) self.filter_high_ram = QCheckBox("RAM élevée (>1%)") self.filter_high_ram.stateChanged.connect(self.filter_tasks) categories_layout.addWidget(self.filter_high_cpu) categories_layout.addWidget(self.filter_high_ram) # 🔍 Recherche self.search_box = QLineEdit() self.search_box.setPlaceholderText("🔍 Rechercher une tâche…") self.search_box.textChanged.connect(self.filter_tasks) self.search_box.returnPressed.connect(self.search_task) categories_layout.addWidget(self.search_box) categories_layout.addStretch() layout.addWidget(categories_panel, 1) # Panneau gauche seul au début except Exception as e: print(f"Erreur dans setup_task_tab: {str(e)}") self.show_error_message("Erreur", f"Impossible d'initialiser l'onglet de tâches: {str(e)}") def activate_task_interface(self): """Charge dynamiquement le panneau principal des tâches""" try: if self.task_interface_loaded: return tasks_panel = QWidget() tasks_layout = QVBoxLayout(tasks_panel) tasks_layout.setContentsMargins(0, 0, 0, 0) self.tasks_table = QTableWidget(0, 9) self.tasks_table.setHorizontalHeaderLabels([ "PID", "Nom", "Utilisateur", "CPU %", "RAM %", "État", "Priorité", "Actions", "Bloquer" ]) self.tasks_table.setAlternatingRowColors(True) self.tasks_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.tasks_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.tasks_table.verticalHeader().setVisible(False) self.tasks_table.setSelectionBehavior(QTableWidget.SelectRows) self.tasks_table.setRowCount(100) self.tasks_table.setUpdatesEnabled(False) tasks_layout.addWidget(self.tasks_table) # 🔧 Contrôles controls_layout = QHBoxLayout() self.refresh_button = QPushButton("🔄 Rafraîchir") self.refresh_button.clicked.connect(self.update_tasks) self.stop_button = QPushButton("⏹️ Arrêter") self.stop_button.setEnabled(False) self.stop_button.clicked.connect(self.stop_selected_process) self.block_button = QPushButton("🔒 Bloquer") self.block_button.setEnabled(False) self.block_button.clicked.connect(self.toggle_block_selected_process) priority_layout = QHBoxLayout() priority_layout.addWidget(QLabel("Priorité:")) self.priority_combo = QComboBox() self.priority_combo.addItems(["idle", "below_normal", "normal", "above_normal", "high"]) priority_layout.addWidget(self.priority_combo) self.priority_button = QPushButton("Appliquer") self.priority_button.setEnabled(False) self.priority_button.clicked.connect(self.set_priority_for_selected) priority_layout.addWidget(self.priority_button) self.show_all_checkbox = QCheckBox("Afficher tous les processus") self.show_all_checkbox.setChecked(False) self.show_all_checkbox.stateChanged.connect(self.update_tasks) controls_layout.addWidget(self.refresh_button) controls_layout.addWidget(self.stop_button) controls_layout.addWidget(self.block_button) controls_layout.addLayout(priority_layout) controls_layout.addStretch() controls_layout.addWidget(self.show_all_checkbox) # ✅ Légende avec étiquettes correctement stylées legend_layout = QHBoxLayout() legend_layout.addWidget(QLabel("Légende:")) system_label = QLabel("■ Système") system_label.setStyleSheet("color: #e74c3c;") legend_layout.addWidget(system_label) sensitive_label = QLabel("■ Sensible") sensitive_label.setStyleSheet("color: #e67e22;") legend_layout.addWidget(sensitive_label) normal_label = QLabel("■ Normal") normal_label.setStyleSheet("color: #2ecc71;") legend_layout.addWidget(normal_label) blocked_label = QLabel("■ Bloqué") blocked_label.setStyleSheet("color: #9b59b6;") legend_layout.addWidget(blocked_label) app_label = QLabel("■ Applications") app_label.setStyleSheet("color: #3498db;") legend_layout.addWidget(app_label) tasks_layout.addLayout(controls_layout) tasks_layout.addLayout(legend_layout) self.task_tab.layout().addWidget(tasks_panel, 4) self.tasks_table.itemSelectionChanged.connect(self.update_buttons_state) self.task_interface_loaded = True self.activate_interface_button.setEnabled(False) if hasattr(self, 'log_action'): self.log_action("✅ Interface Gestionnaire des Tâches activée") else: print("✅ Interface Gestionnaire des Tâches activée") if hasattr(self, 'timer'): self.timer.start(2000) if hasattr(self, 'tasks_table') and isinstance(self.tasks_table, QTableWidget): self.update_tasks() except Exception as e: if hasattr(self, 'log_action'): self.log_action(f"❌ Erreur activation interface: {e}") else: print(f"❌ Erreur activation interface: {e}") def log_action(self, message): print(message) # ou connecter à ton système de logs si tu veux plus def search_task(self): """Recherche et sélectionne la première tâche correspondant au texte de recherche.""" search_text = self.search_box.text().strip().lower() if not search_text: return # Parcourir toutes les lignes visibles du tableau for row in range(self.tasks_table.rowCount()): if self.tasks_table.isRowHidden(row): continue name_item = self.tasks_table.item(row, 1) if name_item and search_text in name_item.text().lower(): # Sélectionner cette ligne self.tasks_table.selectRow(row) # Faire défiler pour rendre visible self.tasks_table.scrollToItem(name_item) return def filter_tasks(self): """Filtre les tâches selon les catégories sélectionnées et le texte de recherche""" try: # Déterminer quelles catégories sont activées show_system = self.category_system.isChecked() show_sensitive = self.category_sensitive.isChecked() show_normal = self.category_normal.isChecked() show_blocked = self.category_blocked.isChecked() show_applications = self.category_applications.isChecked() show_high_cpu = self.filter_high_cpu.isChecked() show_high_ram = self.filter_high_ram.isChecked() # Récupérer le texte de recherche search_text = self.search_box.text().lower().strip() # Parcourir toutes les lignes du tableau for row in range(self.tasks_table.rowCount()): if row >= self.tasks_table.rowCount(): continue # Récupérer les informations de la ligne name_item = self.tasks_table.item(row, 1) # Colonne du nom du processus if not name_item: continue # Vérifier si le texte de recherche correspond au nom du processus process_name = name_item.text().lower() search_match = not search_text or search_text in process_name # Si le texte de recherche ne correspond pas, cacher la ligne et passer à la suivante if not search_match: self.tasks_table.setRowHidden(row, True) continue # Déterminer si la ligne doit être visible selon sa catégorie bg_color = name_item.background().color().name() # Vérifier si le processus est bloqué ou est une application block_button = self.tasks_table.cellWidget(row, 8) is_blocked = False is_application = False if block_button and hasattr(block_button, 'property'): is_blocked = block_button.property("is_blocked") is_application = block_button.property("is_application") # Vérifier le CPU et la RAM cpu_item = self.tasks_table.item(row, 3) ram_item = self.tasks_table.item(row, 4) cpu_value = float(cpu_item.text()) if cpu_item and cpu_item.text() else 0 ram_value = float(ram_item.text()) if ram_item and ram_item.text() else 0 # Définir la visibilité selon les filtres show_row = False # Par catégorie if is_blocked and show_blocked: show_row = True elif bg_color == "#fadbd8" and show_system: # Rouge léger (système) show_row = True elif bg_color == "#fae5d3" and show_sensitive: # Orange léger (sensible) show_row = True elif bg_color == "#d4e6f1" and show_applications: # Bleu clair (applications) show_row = True elif bg_color == "#d5f5e3" and show_normal: # Vert léger (normal) show_row = True # Par utilisation de ressources if show_high_cpu and cpu_value > 5: show_row = True if show_high_ram and ram_value > 1: show_row = True # Cacher ou montrer la ligne self.tasks_table.setRowHidden(row, not show_row) except Exception as e: print(f"Erreur lors du filtrage des tâches: {str(e)}") def update_tasks(self): """Met à jour la liste des tâches""" if not hasattr(self, 'tasks_table') or not isinstance(self.tasks_table, QTableWidget): return # L’interface n’a pas encore été activée ou a été supprimée try: # Sauvegarder la sélection actuelle selected_pid = None selected_rows = self.tasks_table.selectedItems() if selected_rows: row = selected_rows[0].row() pid_item = self.tasks_table.item(row, 0) if pid_item: selected_pid = pid_item.text() self.current_selected_pid = selected_pid if not self.system_monitor: self.show_error_message("Erreur", "Moniteur système non disponible") return self.tasks_table.setUpdatesEnabled(False) # Obtenir les processus show_all = self.show_all_checkbox.isChecked() if hasattr(self, 'show_all_checkbox') else False try: processes = self.system_monitor.get_system_tasks(show_all=show_all) except TypeError: processes = self.system_monitor.get_system_tasks() processes = processes or [] # S’assurer que c’est une liste current_row_count = self.tasks_table.rowCount() if len(processes) > current_row_count: self.tasks_table.setRowCount(len(processes)) if current_row_count > len(processes): for i in range(len(processes), current_row_count): for j in range(self.tasks_table.columnCount()): self.tasks_table.setItem(i, j, None) row_to_select = -1 for row_index, process in enumerate(processes): category = process.get('category', 'normal') is_blocked = process.get('is_blocked', False) process_name = process.get('name', 'N/A') app_extensions = [".exe", ".msi", ".appx", ".app"] system_processes = [ "svchost.exe", "dllhost.exe", "conhost.exe", "services.exe", "csrss.exe", "lsass.exe", "smss.exe", "winlogon.exe", "dwm.exe", "rundll32.exe", "regsvr32.exe" ] is_application = ( process_name.lower().endswith(".exe") and process_name not in system_processes and category not in ["system", "sensitive"] ) if is_blocked: row_color = "#e8daef" text_color = "#000000" elif category == "system": row_color = "#fadbd8" text_color = "#000000" elif category == "sensitive": row_color = "#fae5d3" text_color = "#000000" elif is_application: row_color = "#d4e6f1" text_color = "#000000" else: row_color = "#d5f5e3" text_color = "#000000" pid = process.get('pid', 'N/A') self.tasks_table.setItem(row_index, 0, QTableWidgetItem(str(pid))) if str(pid) == self.current_selected_pid: row_to_select = row_index self.tasks_table.setItem(row_index, 1, QTableWidgetItem(process.get('name', 'N/A'))) self.tasks_table.setItem(row_index, 2, QTableWidgetItem(process.get('username', 'N/A'))) cpu_item = QTableWidgetItem(f"{process.get('cpu_percent', 0):.1f}") self.tasks_table.setItem(row_index, 3, cpu_item) memory_item = QTableWidgetItem(f"{process.get('memory_percent', 0):.1f}") self.tasks_table.setItem(row_index, 4, memory_item) self.tasks_table.setItem(row_index, 5, QTableWidgetItem(process.get('status', 'N/A'))) self.tasks_table.setItem(row_index, 6, QTableWidgetItem(str(process.get('priority', 'N/A')))) for col in range(7): item = self.tasks_table.item(row_index, col) if item: item.setBackground(QColor(row_color)) item.setForeground(QColor(text_color)) kill_button = QPushButton("Arrêter") kill_button.setStyleSheet(f"background-color: {row_color}; color: {text_color};") kill_button.setProperty("pid", pid) kill_button.clicked.connect(lambda checked, pid=pid: self.kill_process(pid)) self.tasks_table.setCellWidget(row_index, 7, kill_button) block_button = QPushButton("Débloquer" if is_blocked else "Bloquer") block_button.setStyleSheet(f"background-color: {row_color}; color: {text_color};") block_button.setProperty("pid", pid) block_button.setProperty("is_blocked", is_blocked) block_button.setProperty("is_application", is_application) block_button.clicked.connect(lambda checked, pid=pid, blocked=is_blocked: self.toggle_block_process(pid, blocked)) self.tasks_table.setCellWidget(row_index, 8, block_button) self.tasks_table.setRowCount(len(processes)) self.tasks_table.setUpdatesEnabled(True) if row_to_select >= 0: self.tasks_table.selectRow(row_to_select) self.filter_tasks() if hasattr(self, 'last_update_label'): now = datetime.now().strftime("%H:%M:%S") self.last_update_label.setText(f"Dernière mise à jour: {now}") except Exception as e: try: self.tasks_table.setUpdatesEnabled(True) except: pass print(f"Erreur lors de la mise à jour des tâches: {e}") self.show_error_message("Erreur", str(e)) def update_system_resources(self): """Met à jour les informations sur les ressources système""" try: if not self.system_monitor: return # Récupérer les infos système depuis le moniteur if hasattr(self.system_monitor, 'get_system_info'): try: system_info = self.system_monitor.get_system_info() if not system_info or not isinstance(system_info, dict): system_info = {'cpu_percent': 0, 'memory_percent': 0, 'disk_percent': 0} # Mettre à jour les labels avec de meilleurs formatages cpu_percent = system_info.get('cpu_percent', 0) self.cpu_label.setText(f"CPU: {cpu_percent:.1f}%") # Colorer les labels selon l'utilisation (rouge si élevée) if cpu_percent > 80: self.cpu_label.setStyleSheet("color: #e74c3c; font-weight: bold;") elif cpu_percent > 50: self.cpu_label.setStyleSheet("color: #e67e22; font-weight: bold;") else: self.cpu_label.setStyleSheet("color: #3498db; font-weight: bold;") # RAM avec formatage avancé memory_percent = system_info.get('memory_percent', 0) memory_total = system_info.get('memory_total', 0) memory_available = system_info.get('memory_available', 0) # Formater les valeurs en Go memory_total_gb = memory_total / (1024 ** 3) if memory_total else 0 memory_available_gb = memory_available / (1024 ** 3) if memory_available else 0 memory_used_gb = memory_total_gb - memory_available_gb if memory_total_gb >= memory_available_gb else 0 if memory_total_gb > 0: self.ram_label.setText( f"RAM: {memory_percent:.1f}% ({memory_used_gb:.1f}/{memory_total_gb:.1f} Go)") else: self.ram_label.setText(f"RAM: {memory_percent:.1f}%") # Colorer selon l'utilisation if memory_percent > 80: self.ram_label.setStyleSheet("color: #e74c3c; font-weight: bold;") elif memory_percent > 50: self.ram_label.setStyleSheet("color: #e67e22; font-weight: bold;") else: self.ram_label.setStyleSheet("color: #3498db; font-weight: bold;") # Disque avec formatage avancé disk_percent = system_info.get('disk_percent', 0) disk_total = system_info.get('disk_total', 0) disk_free = system_info.get('disk_free', 0) # Formater les valeurs en Go disk_total_gb = disk_total / (1024 ** 3) if disk_total else 0 disk_free_gb = disk_free / (1024 ** 3) if disk_free else 0 disk_used_gb = disk_total_gb - disk_free_gb if disk_total_gb >= disk_free_gb else 0 if disk_total_gb > 0: self.disk_label.setText( f"Disque: {disk_percent:.1f}% ({disk_used_gb:.1f}/{disk_total_gb:.1f} Go)") else: self.disk_label.setText(f"Disque: {disk_percent:.1f}%") # Colorer selon l'utilisation if disk_percent > 90: self.disk_label.setStyleSheet("color: #e74c3c; font-weight: bold;") elif disk_percent > 70: self.disk_label.setStyleSheet("color: #e67e22; font-weight: bold;") else: self.disk_label.setStyleSheet("color: #3498db; font-weight: bold;") except Exception as resource_error: print(f"Erreur dans get_system_info: {str(resource_error)}") # Réinitialiser les valeurs en cas d'erreur self.cpu_label.setText("CPU: --") self.ram_label.setText("RAM: --") self.disk_label.setText("Disque: --") else: # Fallback si get_system_info n'est pas disponible self.cpu_label.setText("CPU: --") self.ram_label.setText("RAM: --") self.disk_label.setText("Disque: --") except Exception as e: error_message = f"Erreur lors de la mise à jour des ressources: {str(e)}" print(error_message) # Afficher dans la console pour le débogage # Ne pas afficher de message d'erreur à l'utilisateur pour cette opération de fond def kill_process(self, pid): """Tue un processus avec le PID spécifié""" try: if not pid: # Vérifier si le PID est valide self.show_error_message("Erreur", "PID invalide") return # Récupérer le nom du processus pour l'afficher dans la confirmation process_name = "Inconnu" for row in range(self.tasks_table.rowCount()): if self.tasks_table.item(row, 0) and str(self.tasks_table.item(row, 0).text()) == str(pid): process_name = self.tasks_table.item(row, 1).text() break confirmation = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment arrêter le processus {process_name} (PID: {pid}) ?", QMessageBox.Yes | QMessageBox.No ) if confirmation == QMessageBox.Yes: # Vérifier si c'est un processus système ou sensible is_system_process = False is_sensitive_process = False for row in range(self.tasks_table.rowCount()): if self.tasks_table.item(row, 0) and str(self.tasks_table.item(row, 0).text()) == str(pid): item = self.tasks_table.item(row, 1) if item and item.background().color().name() == "#fadbd8": # Couleur rouge des processus système is_system_process = True elif item and item.background().color().name() == "#fae5d3": # Couleur orange des processus sensibles is_sensitive_process = True break # Double confirmation pour les processus système if is_system_process: confirm_system = QMessageBox.warning( self, "⚠️ Avertissement - Processus Système ⚠️", f"ATTENTION: {process_name} est un processus SYSTÈME CRITIQUE.\n\n" f"L'arrêt de ce processus peut causer l'instabilité du système ou un redémarrage forcé.\n\n" f"Êtes-vous ABSOLUMENT CERTAIN de vouloir continuer ?", QMessageBox.Yes | QMessageBox.No ) if confirm_system != QMessageBox.Yes: return # Confirmation supplémentaire pour les processus sensibles elif is_sensitive_process: confirm_sensitive = QMessageBox.warning( self, "⚠️ Avertissement - Processus Sensible ⚠️", f"Attention: {process_name} est un processus sensible.\n\n" f"L'arrêt de ce processus peut interrompre certaines fonctionnalités.\n\n" f"Voulez-vous vraiment continuer ?", QMessageBox.Yes | QMessageBox.No ) if confirm_sensitive != QMessageBox.Yes: return # Exécuter l'arrêt du processus try: response = self.system_monitor.stop_process(pid) if response and isinstance(response, dict) and response.get("success", False): QMessageBox.information(self, "Succès", response.get("message", "Processus arrêté")) self.update_tasks() # Rafraîchir la liste après l'arrêt du processus else: error_message = response.get("error", "Erreur inconnue") if isinstance(response, dict) else "Erreur lors de l'arrêt du processus" self.show_error_message("Erreur", error_message) except Exception as stop_error: self.show_error_message("Erreur", f"Erreur lors de l'arrêt du processus: {str(stop_error)}") except Exception as e: self.show_error_message("Erreur lors de l'arrêt du processus", str(e)) def toggle_block_process(self, pid, is_currently_blocked): """Bloque ou débloque un processus selon son état actuel""" try: if not pid: # Vérifier si le PID est valide self.show_error_message("Erreur", "PID invalide") return if is_currently_blocked: # Débloquer le processus if hasattr(self.system_monitor, 'unblock_process'): response = self.system_monitor.unblock_process(pid) action_text = "déblocage" else: self.show_error_message("Erreur", "La fonction de déblocage n'est pas disponible") return else: # Bloquer le processus if hasattr(self.system_monitor, 'block_process'): response = self.system_monitor.block_process(pid) action_text = "blocage" else: self.show_error_message("Erreur", "La fonction de blocage n'est pas disponible") return if response and isinstance(response, dict) and response.get("success", False): QMessageBox.information(self, "Succès", response.get("message", f"Processus {action_text} effectué")) self.update_tasks() # Rafraîchir la liste else: error_message = response.get("error", f"Erreur lors du {action_text} du processus") if isinstance( response, dict) else f"Erreur lors du {action_text} du processus" self.show_error_message("Erreur", error_message) except Exception as e: self.show_error_message(f"Erreur lors du blocage/déblocage du processus", str(e)) def toggle_block_selected_process(self): """Bloque ou débloque le processus sélectionné""" try: selected_items = self.tasks_table.selectedItems() if not selected_items: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner un processus.") return row = selected_items[0].row() pid_item = self.tasks_table.item(row, 0) # Colonne 0 contient le PID if pid_item: try: pid = int(pid_item.text()) # Déterminer si le processus est déjà bloqué is_blocked = False block_button = self.tasks_table.cellWidget(row, 8) if block_button: is_blocked = block_button.property("is_blocked") # Basculer l'état de blocage self.toggle_block_process(pid, is_blocked) except ValueError: self.show_error_message("Erreur", "PID invalide") else: self.show_error_message("Erreur", "PID non trouvé") except Exception as e: self.show_error_message("Erreur", f"Erreur lors du blocage/déblocage: {str(e)}") def stop_selected_process(self): """Arrête le processus sélectionné dans le tableau""" try: selected_items = self.tasks_table.selectedItems() if not selected_items: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner un processus à arrêter.") return row = selected_items[0].row() pid_item = self.tasks_table.item(row, 0) # Colonne 0 contient le PID if pid_item: try: pid = int(pid_item.text()) self.kill_process(pid) except ValueError: self.show_error_message("Erreur", "PID invalide") else: self.show_error_message("Erreur", "PID non trouvé") except Exception as e: self.show_error_message("Erreur", f"Erreur lors de l'arrêt du processus: {str(e)}") def set_priority_for_selected(self): """Modifie la priorité du processus sélectionné""" try: selected_items = self.tasks_table.selectedItems() if not selected_items: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner un processus à modifier.") return row = selected_items[0].row() pid_item = self.tasks_table.item(row, 0) if not pid_item: self.show_error_message("Erreur", "PID non trouvé") return try: pid = int(pid_item.text()) name_item = self.tasks_table.item(row, 1) process_name = name_item.text() if name_item else "Inconnu" except ValueError: self.show_error_message("Erreur", "PID invalide") return # Récupérer la priorité sélectionnée priority = self.priority_combo.currentText() # Confirmation pour changer la priorité confirmation = QMessageBox.question( self, "Confirmation", f"Voulez-vous modifier la priorité du processus {process_name} (PID: {pid}) à {priority} ?", QMessageBox.Yes | QMessageBox.No ) if confirmation == QMessageBox.Yes: if not self.system_monitor or not hasattr(self.system_monitor, 'set_process_priority'): self.show_error_message("Erreur", "Fonction de modification de priorité non disponible") return try: response = self.system_monitor.set_process_priority(pid, priority) if response and isinstance(response, dict) and response.get("success", False): QMessageBox.information(self, "Succès", response.get("message", "Priorité modifiée avec succès")) self.update_tasks() # Rafraîchir la liste else: error_message = response.get("error", "Erreur inconnue") if isinstance(response, dict) else "Erreur lors de la modification de la priorité" self.show_error_message("Erreur", error_message) except Exception as priority_error: self.show_error_message("Erreur", f"Erreur lors de la modification de la priorité: {str(priority_error)}") except Exception as e: self.show_error_message("Erreur lors de la modification de la priorité", str(e)) def update_buttons_state(self): """Active ou désactive les boutons en fonction de la sélection""" try: has_selection = len(self.tasks_table.selectedItems()) > 0 self.stop_button.setEnabled(has_selection) self.priority_button.setEnabled(has_selection) self.block_button.setEnabled(has_selection) # Modifier le texte du bouton bloquer en fonction de l'état du processus if has_selection: row = self.tasks_table.selectedItems()[0].row() block_button = self.tasks_table.cellWidget(row, 8) if block_button and block_button.property("is_blocked"): self.block_button.setText("🔓 Débloquer") else: self.block_button.setText("🔒 Bloquer") except Exception as e: print(f"Erreur dans update_buttons_state: {str(e)}") # Ne pas montrer d'erreur à l'utilisateur pour cette fonction d'interface def setup_diagnostic_tab(self): """Configure l'onglet de diagnostic système""" try: layout = QVBoxLayout(self.diagnostic_tab) # Titre et description title_label = QLabel("Diagnostic et réparation du système") title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px;") desc_label = QLabel( "Lancez un diagnostic complet pour identifier et résoudre les problèmes potentiels du système.") desc_label.setWordWrap(True) # Boutons avec icônes buttons_layout = QHBoxLayout() self.diagnostic_button = QPushButton("🔍 Lancer le diagnostic") self.diagnostic_button.setMinimumWidth(200) self.diagnostic_button.clicked.connect(self.run_diagnostic) self.repair_button = QPushButton("🔧 Réparer les problèmes") self.repair_button.setMinimumWidth(200) self.repair_button.setEnabled(False) # Désactivé par défaut jusqu'à ce que le diagnostic soit terminé self.repair_button.clicked.connect(self.repair_issues) buttons_layout.addWidget(self.diagnostic_button) buttons_layout.addWidget(self.repair_button) buttons_layout.addStretch() # Zone de progression progress_layout = QHBoxLayout() self.progress_label = QLabel("Statut: Prêt") self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) progress_layout.addWidget(self.progress_label) progress_layout.addWidget(self.progress_bar) # Zone de sortie output_label = QLabel("Résultats du diagnostic:") self.diagnostic_output = QTextEdit() self.diagnostic_output.setReadOnly(True) self.diagnostic_output.setMinimumHeight(300) self.diagnostic_output.setHtml( "➡️ Cliquez sur \"Lancer le diagnostic\" pour analyser votre système.") # Assembler le layout layout.addWidget(title_label) layout.addWidget(desc_label) layout.addSpacing(10) layout.addLayout(buttons_layout) layout.addSpacing(10) layout.addLayout(progress_layout) layout.addSpacing(5) layout.addWidget(output_label) layout.addWidget(self.diagnostic_output) except Exception as e: print(f"Erreur dans setup_diagnostic_tab: {str(e)}") self.show_error_message("Erreur", f"Impossible d'initialiser l'onglet de diagnostic: {str(e)}") def run_diagnostic(self): """Lance un diagnostic système et affiche les résultats""" try: if not self.system_monitor: self.show_error_message("Erreur", "Moniteur système non disponible") return # Réinitialiser l'interface self.diagnostic_output.clear() self.progress_bar.setValue(0) self.progress_label.setText("Statut: Diagnostic en cours...") self.diagnostic_button.setEnabled(False) self.repair_button.setEnabled(False) # Afficher un message de démarrage self.diagnostic_output.append("🔍 Démarrage du diagnostic système...") # Liste des vérifications à effectuer checks = [ "Analyse des processus système...", "Vérification de l'utilisation CPU...", "Vérification de l'utilisation mémoire...", "Analyse des processus bloqués...", "Vérification de l'intégrité du système de fichiers...", "Recherche de fichiers temporaires inutilisés...", "Vérification des mises à jour du système...", "Analyse des journaux système pour erreurs..." ] # Simuler un diagnostic progressif self.issues_found = [] for i, check in enumerate(checks): # Mettre à jour la progression progress = int((i / len(checks)) * 100) self.progress_bar.setValue(progress) self.progress_label.setText(f"Statut: {check}") # Afficher l'étape actuelle self.diagnostic_output.append(f"[{progress}%] {check}") # Simuler un temps de traitement QApplication.processEvents() time.sleep(0.3) # Attente courte pour simuler le travail # Ajouter parfois un problème détecté if i in [1, 4, 7]: # Ajouter des problèmes à certaines étapes issue_text = "" if i == 1: issue_text = "Utilisation élevée du CPU détectée sur certains processus" self.issues_found.append({ "type": "cpu", "description": "Certains processus utilisent une quantité anormale de CPU", "severity": "medium" }) elif i == 4: issue_text = "Fichiers temporaires excessifs détectés" self.issues_found.append({ "type": "temp_files", "description": "Accumulation de fichiers temporaires inutilisés", "severity": "low" }) elif i == 7: issue_text = "Erreurs de journalisation système détectées" self.issues_found.append({ "type": "log_errors", "description": "Erreurs récurrentes détectées dans les journaux système", "severity": "high" }) # Afficher le problème avec un style approprié self.diagnostic_output.append(f" ⚠️ PROBLÈME: {issue_text}") # Terminer le diagnostic self.progress_bar.setValue(100) self.progress_label.setText("Statut: Diagnostic terminé") # Résumé des résultats self.diagnostic_output.append("\n✅ Diagnostic terminé") if self.issues_found: self.diagnostic_output.append( f"\n⚠️ {len(self.issues_found)} problème(s) détecté(s):") for issue in self.issues_found: severity_color = "#e74c3c" if issue["severity"] == "high" else "#e67e22" if issue[ "severity"] == "medium" else "#f1c40f" severity_text = "Critique" if issue["severity"] == "high" else "Moyen" if issue[ "severity"] == "medium" else "Faible" self.diagnostic_output.append( f" • {issue['description']} (Sévérité: {severity_text})") # Activer le bouton de réparation si des problèmes sont détectés self.repair_button.setEnabled(True) else: self.diagnostic_output.append( "\n✅ Aucun problème détecté. Le système fonctionne normalement.") # Réactiver le bouton de diagnostic self.diagnostic_button.setEnabled(True) except Exception as e: self.diagnostic_output.append(f"❌ Erreur lors du diagnostic: {str(e)}") self.diagnostic_button.setEnabled(True) self.progress_label.setText("Statut: Erreur") print(f"Erreur dans run_diagnostic: {str(e)}") def repair_issues(self): """Répare les problèmes détectés lors du diagnostic""" try: if not self.issues_found: QMessageBox.information(self, "Information", "Aucun problème à réparer.") return confirmation = QMessageBox.question( self, "Confirmation", f"Voulez-vous réparer les {len(self.issues_found)} problème(s) détecté(s) ?", QMessageBox.Yes | QMessageBox.No ) if confirmation != QMessageBox.Yes: return # Désactiver les boutons pendant la réparation self.repair_button.setEnabled(False) self.diagnostic_button.setEnabled(False) # Réinitialiser la barre de progression self.progress_bar.setValue(0) self.progress_label.setText("Statut: Réparation en cours...") # Ajouter une séparation dans la sortie self.diagnostic_output.append("\n🔧 Début des réparations...") # Réparer chaque problème for i, issue in enumerate(self.issues_found): # Mettre à jour la progression progress = int((i / len(self.issues_found)) * 100) self.progress_bar.setValue(progress) # Afficher l'action de réparation repair_action = "" if issue["type"] == "cpu": repair_action = "Optimisation des processus CPU intensifs..." elif issue["type"] == "temp_files": repair_action = "Nettoyage des fichiers temporaires..." elif issue["type"] == "log_errors": repair_action = "Résolution des erreurs de journalisation système..." else: repair_action = f"Réparation du problème de type {issue['type']}..." self.progress_label.setText(f"Statut: {repair_action}") self.diagnostic_output.append(f"[{progress}%] {repair_action}") # Simuler la réparation QApplication.processEvents() time.sleep(1) # Attente plus longue pour simuler la réparation # Afficher le résultat de la réparation self.diagnostic_output.append( f" ✓ Problème résolu: {issue['description']}") # Terminer la réparation self.progress_bar.setValue(100) self.progress_label.setText("Statut: Réparation terminée") self.diagnostic_output.append( "\n✅ Toutes les réparations ont été effectuées avec succès.") self.diagnostic_output.append( "ℹ️ Il est recommandé de redémarrer l'application pour appliquer tous les changements.") # Vider la liste des problèmes self.issues_found = [] # Réactiver le bouton de diagnostic self.diagnostic_button.setEnabled(True) except Exception as e: self.diagnostic_output.append( f"❌ Erreur lors de la réparation: {str(e)}") self.diagnostic_button.setEnabled(True) self.repair_button.setEnabled(True) self.progress_label.setText("Statut: Erreur") print(f"Erreur dans repair_issues: {str(e)}") def setup_backup_tab(self): """Configure l'onglet de sauvegarde des fichiers""" try: layout = QVBoxLayout(self.backup_tab) # Titre et description title_label = QLabel("Gestion des sauvegardes") title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 10px;") desc_label = QLabel("Créez, gérez et restaurez des sauvegardes des fichiers critiques de l'application.") desc_label.setWordWrap(True) # Boutons avec icônes buttons_layout = QHBoxLayout() self.backup_button = QPushButton("💾 Créer une sauvegarde") self.backup_button.setMinimumWidth(200) self.backup_button.clicked.connect(self.on_create_backup_clicked) self.restore_button = QPushButton("📥 Restaurer la sauvegarde") self.restore_button.setMinimumWidth(200) self.restore_button.setEnabled(False) self.restore_button.clicked.connect(self.restore_selected_backup) self.delete_backup_button = QPushButton("🗑️ Supprimer") self.delete_backup_button.setMinimumWidth(120) self.delete_backup_button.setEnabled(False) self.delete_backup_button.clicked.connect(self.delete_selected_backups) # Nouveau: Bouton pour sélectionner/désélectionner tout self.select_all_button = QPushButton("☑️ Tout sélectionner") self.select_all_button.setMinimumWidth(150) self.select_all_button.clicked.connect(self.toggle_select_all) self.is_all_selected = False # Pour suivre l'état de sélection buttons_layout.addWidget(self.backup_button) buttons_layout.addWidget(self.restore_button) buttons_layout.addWidget(self.delete_backup_button) buttons_layout.addWidget(self.select_all_button) # Ajout du nouveau bouton buttons_layout.addStretch() # Liste des sauvegardes avec en-têtes self.backup_list = QTreeWidget() self.backup_list.setHeaderLabels(["Nom", "Date", "Taille", "Description"]) self.backup_list.setAlternatingRowColors(True) self.backup_list.itemSelectionChanged.connect(self.update_restore_button) # Activer la sélection multiple self.backup_list.setSelectionMode(QAbstractItemView.MultiSelection) # Détails de la sauvegarde details_group = QGroupBox("Détails de la sauvegarde") details_layout = QVBoxLayout(details_group) self.backup_details = QTextEdit() self.backup_details.setReadOnly(True) self.backup_details.setMaximumHeight(300) details_layout.addWidget(self.backup_details) # Ajouter tous les éléments au layout principal layout.addWidget(title_label) layout.addWidget(desc_label) layout.addSpacing(10) layout.addLayout(buttons_layout) layout.addSpacing(10) layout.addWidget(self.backup_list) layout.addWidget(details_group) # Charger la liste initiale des sauvegardes QTimer.singleShot(500, self.load_backup_list) # Différer le chargement initial # Après avoir créé self.backup_list = QTreeWidget() self.backup_list.setStyleSheet(""" QTreeWidget { background-color: #2c3e50; color: white; alternate-background-color: #34495e; } QHeaderView::section { background-color: #1a2a3a; color: white; font-weight: bold; } """) except Exception as e: print(f"Erreur dans setup_backup_tab: {str(e)}") self.show_error_message("Erreur", f"Impossible d'initialiser l'onglet de sauvegarde: {str(e)}") def toggle_select_all(self): """Sélectionne ou désélectionne tous les éléments de la liste""" try: if not self.is_all_selected: # Sélectionner tous les éléments self.backup_list.selectAll() # Forcer un focus sur le widget pour améliorer la visibilité de la sélection self.backup_list.setFocus() self.select_all_button.setText("❎ Tout désélectionner") self.is_all_selected = True else: # Désélectionner tous les éléments self.backup_list.clearSelection() self.select_all_button.setText("☑️ Tout sélectionner") self.is_all_selected = False # Mettre à jour l'état des boutons self.update_restore_button() except Exception as e: print(f"Erreur lors de la sélection/désélection: {str(e)}") def load_backup_list(self): """Charge la liste des sauvegardes disponibles""" try: # Vérifier si le système de sauvegarde est disponible if not self.system_monitor: self.backup_details.setHtml("Moniteur système non disponible") return # Effacer la liste actuelle self.backup_list.clear() # Récupérer les sauvegardes try: backups = self.system_monitor.list_backups() print(f"Sauvegardes récupérées: {backups}") # Débogage except Exception as list_error: self.backup_details.setHtml( f"Erreur lors de la récupération des sauvegardes: {str(list_error)}") print(f"Erreur lors de la récupération des sauvegardes: {str(list_error)}") return # Vérifier si nous avons des sauvegardes if not backups: self.backup_details.setHtml("Aucune sauvegarde disponible") return # Traiter chaque sauvegarde for backup in backups: try: # Créer un élément pour la liste avec toutes les colonnes item = QTreeWidgetItem() # Colonne 1: Nom du fichier file_name = backup.get('file', '') item.setText(0, file_name) # Colonne 2: Date de la sauvegarde timestamp = backup.get('timestamp', 'Date inconnue') item.setText(1, timestamp) # Colonne 3: Taille avec formatage size_bytes = backup.get('size', 0) if size_bytes < 1024: size_str = f"{size_bytes} B" elif size_bytes < 1024 ** 2: size_str = f"{size_bytes / 1024:.1f} KB" elif size_bytes < 1024 ** 3: size_str = f"{size_bytes / (1024 ** 2):.2f} MB" else: size_str = f"{size_bytes / (1024 ** 3):.2f} GB" item.setText(2, size_str) # Colonne 4: Description description = "Sauvegarde standard" if "_prerestore_" in file_name: description = "Sauvegarde avant restauration" elif any(c in file_name for c in ["_", "-"]) and file_name.count(".") > 1: description = "Sauvegarde horodatée" item.setText(3, description) # Stocker le nom du fichier pour la restauration item.setData(0, Qt.UserRole, file_name) # Ajouter l'élément à la liste self.backup_list.addTopLevelItem(item) # Utiliser addTopLevelItem au lieu de addItem except Exception as item_error: print(f"Erreur lors du traitement d'un élément de sauvegarde: {str(item_error)}") # Ajuster les colonnes for i in range(4): self.backup_list.resizeColumnToContents(i) # Mettre à jour les informations self.backup_details.setHtml( f"{len(backups)} sauvegarde(s) disponible(s)") # Activer/désactiver les boutons selon qu'il y a des sauvegardes self.restore_button.setEnabled(False) # Désactivé jusqu'à sélection self.delete_backup_button.setEnabled(False) # Désactivé jusqu'à sélection except Exception as e: error_message = f"Erreur lors du chargement des sauvegardes: {str(e)}" print(error_message) self.backup_details.setHtml(f"{error_message}") def on_create_backup_clicked(self): """Gestionnaire d'événement pour le bouton de création de sauvegarde""" try: # Afficher un indicateur visuel que la sauvegarde est en cours self.backup_button.setEnabled(False) self.backup_button.setText("⏳ Sauvegarde en cours...") self.backup_details.setHtml("Création de la sauvegarde en cours...") QApplication.processEvents() # Forcer la mise à jour de l'interface # Créer la sauvegarde result = self.system_monitor.create_backup() print(f"Résultat de la sauvegarde: {result}") # Débogage # Réactiver le bouton self.backup_button.setEnabled(True) self.backup_button.setText("💾 Créer une sauvegarde") # Vérifier s'il y a des sauvegardes créées avec succès backups_created = result.get("backups", []) has_backups = len(backups_created) > 0 # Vérifier s'il y a des erreurs failed_list = result.get("failed", []) has_failures = len(failed_list) > 0 # Gérer les différents cas if has_backups: # Au moins une sauvegarde a été créée avec succès backup_list = "
".join(backups_created) self.backup_details.setHtml( f"✅ Sauvegarde créée avec succès!

{backup_list}") # Message de notification message = f"{len(backups_created)} fichier(s) sauvegardé(s) avec succès." if has_failures: message += f"\n{len(failed_list)} fichier(s) n'ont pas pu être sauvegardés." QMessageBox.information(self, "Sauvegarde créée", message) # Rafraîchir la liste des sauvegardes self.load_backup_list() elif has_failures: # Seulement des échecs failed_html = "
".join(failed_list) self.backup_details.setHtml( f"❌ Erreur lors de la sauvegarde:

{failed_html}") QMessageBox.warning(self, "Erreur de sauvegarde", f"{len(failed_list)} fichier(s) n'ont pas pu être sauvegardés. Voir les détails pour plus d'informations.") else: # Aucune sauvegarde et aucun échec - situation étrange self.backup_details.setHtml( f"⚠️ Avertissement: Aucun fichier n'a été sauvegardé ou signalé en échec.") QMessageBox.warning(self, "Résultat de sauvegarde", "Aucun fichier n'a été traité. Vérifiez les paramètres de sauvegarde.") except Exception as e: self.backup_button.setEnabled(True) self.backup_button.setText("💾 Créer une sauvegarde") self.backup_details.setHtml(f"Erreur: {str(e)}") self.show_error_message("Erreur lors de la création de la sauvegarde", str(e)) def delete_backup(self, backup_file): """Supprime une sauvegarde spécifique""" try: backup_path = os.path.join(self.backup_dir, backup_file) if not os.path.exists(backup_path): return {"success": False, "error": f"Sauvegarde introuvable: {backup_file}"} os.remove(backup_path) return {"success": True, "message": f"Sauvegarde {backup_file} supprimée avec succès"} except Exception as e: logger.error(f"Erreur lors de la suppression de {backup_file}: {str(e)}") return {"success": False, "error": str(e)} def update_restore_button(self): """Active ou désactive les boutons de restauration et suppression en fonction de la sélection""" try: selected_items = self.backup_list.selectedItems() has_selection = len(selected_items) > 0 # Activer le bouton de suppression si au moins un élément est sélectionné self.delete_backup_button.setEnabled(has_selection) # Activer le bouton de restauration seulement si UN SEUL élément est sélectionné self.restore_button.setEnabled(len(selected_items) == 1) # Afficher les détails de la sauvegarde sélectionnée if len(selected_items) == 1: item = selected_items[0] file_name = item.text(0) date = item.text(1) size = item.text(2) description = item.text(3) details = f""" Détails de la sauvegarde:
Nom: {file_name}
Date: {date}
Taille: {size}
Type: {description}
Chemin: {os.path.join(self.system_monitor.backup_manager.backup_dir, file_name)} """ self.backup_details.setHtml(details) elif len(selected_items) > 1: # Afficher un résumé pour les sélections multiples self.backup_details.setHtml( f"{len(selected_items)} sauvegardes sélectionnées
" "Sélectionnez une seule sauvegarde pour voir ses détails." ) else: self.backup_details.clear() except Exception as e: print(f"Erreur dans update_restore_button: {str(e)}") def restore_selected_backup(self): """Restaure la sauvegarde sélectionnée""" try: selected_items = self.backup_list.selectedItems() if not selected_items: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner une sauvegarde à restaurer.") return item = selected_items[0] backup_file = item.text(0) # Obtenir le nom du fichier # Confirmation avant restauration confirmation = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment restaurer la sauvegarde '{backup_file}' ?\n\n" "Attention: Cette opération remplacera les données actuelles.", QMessageBox.Yes | QMessageBox.No ) if confirmation == QMessageBox.Yes: # Afficher l'état de la restauration self.restore_button.setEnabled(False) self.restore_button.setText("⏳ Restauration...") self.backup_details.setHtml("Restauration en cours...") QApplication.processEvents() # Forcer la mise à jour de l'interface # Effectuer la restauration response = self.system_monitor.restore_backup(backup_file) # Réactiver le bouton self.restore_button.setEnabled(True) self.restore_button.setText("📥 Restaurer la sauvegarde") # Traiter la réponse if response and response.get("success", False): self.backup_details.setHtml( f"✅ {response.get('message', 'Sauvegarde restaurée avec succès')}") QMessageBox.information(self, "Succès", response.get("message", "Sauvegarde restaurée avec succès")) # Rafraîchir la liste pour inclure la sauvegarde pré-restauration self.load_backup_list() else: error_message = response.get("error", "Erreur inconnue") if isinstance(response, dict) else "Erreur lors de la restauration" self.backup_details.setHtml(f"❌ {error_message}") self.show_error_message("Erreur", error_message) except Exception as e: self.restore_button.setEnabled(True) self.restore_button.setText("📥 Restaurer la sauvegarde") self.show_error_message("Erreur lors de la restauration", str(e)) def delete_selected_backups(self): """Supprime les sauvegardes sélectionnées avec gestion améliorée des répertoires""" try: selected_items = self.backup_list.selectedItems() if not selected_items: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner une ou plusieurs sauvegardes à supprimer.") return # Collecter les noms de fichiers à supprimer backup_files = [item.text(0) for item in selected_items] # Confirmation avant suppression confirmation = QMessageBox.question( self, "Confirmation", f"Voulez-vous vraiment supprimer {len(backup_files)} sauvegarde(s) ?\n\n" "Attention: Cette opération est irréversible.", QMessageBox.Yes | QMessageBox.No ) if confirmation == QMessageBox.Yes: success_count = 0 error_count = 0 error_messages = [] # Supprimer chaque fichier sélectionné for backup_file in backup_files: try: backup_path = os.path.join(self.system_monitor.backup_manager.backup_dir, backup_file) if not os.path.exists(backup_path): error_count += 1 error_messages.append(f"Fichier introuvable: {backup_file}") continue # Traitement spécial pour les répertoires if os.path.isdir(backup_path): try: # Supprimer en forçant la suppression # Attendre un peu que tous les fichiers soient libérés time.sleep(0.2) # Changer les attributs des fichiers pour permettre la suppression for root, dirs, files in os.walk(backup_path): for file in files: file_path = os.path.join(root, file) try: # Retirer l'attribut en lecture seule if os.path.exists(file_path): os.chmod(file_path, stat.S_IWRITE) except Exception: pass # Supprimer le répertoire shutil.rmtree(backup_path, ignore_errors=True) # Vérifier si la suppression a réussi if os.path.exists(backup_path): # Essai avec une autre méthode si rmtree a échoué subprocess.run(['rmdir', '/S', '/Q', backup_path], shell=True, check=True) success_count += 1 except Exception as dir_error: error_count += 1 error_messages.append( f"Erreur lors de la suppression du répertoire {backup_file}: {str(dir_error)}") else: # Suppression standard pour les fichiers os.remove(backup_path) success_count += 1 except Exception as delete_error: error_count += 1 error_messages.append(f"Erreur lors de la suppression de {backup_file}: {str(delete_error)}") # Rafraîchir la liste après la suppression self.load_backup_list() # Afficher le résultat if success_count > 0 and error_count == 0: QMessageBox.information(self, "Succès", f"{success_count} sauvegarde(s) supprimée(s) avec succès") elif success_count > 0 and error_count > 0: error_detail = "\n".join(error_messages[:5]) # Limiter à 5 messages d'erreur if len(error_messages) > 5: error_detail += f"\n... et {len(error_messages) - 5} autres erreurs" QMessageBox.warning(self, "Résultat mitigé", f"{success_count} sauvegarde(s) supprimée(s) avec succès\n" f"{error_count} erreur(s) rencontrée(s):\n\n{error_detail}") else: error_detail = "\n".join(error_messages[:5]) if len(error_messages) > 5: error_detail += f"\n... et {len(error_messages) - 5} autres erreurs" QMessageBox.critical(self, "Échec", f"Aucune sauvegarde n'a pu être supprimée.\n" f"Erreurs rencontrées:\n\n{error_detail}") except Exception as e: self.show_error_message("Erreur lors de la suppression", str(e)) class ExpiredProfilesDialog(QDialog): def __init__(self, parent, expired_profiles): super().__init__(parent) self.expired_profiles = expired_profiles self.parent = parent self.setWindowTitle("Emails expirés") self.setMinimumSize(800, 400) # Suivre le déplacement de la fenêtre principale self.setWindowFlags(Qt.Dialog | Qt.WindowStaysOnTopHint) self.updatePosition() # Configurer le timer pour suivre les mouvements self.positionTimer = QTimer(self) self.positionTimer.timeout.connect(self.updatePosition) self.positionTimer.start(100) # Vérifier toutes les 100ms # Initialiser l'instance d'OutlookHandler et OutlookWidget si disponible dans le parent self.outlook_handler = None self.outlook_widget = None if hasattr(parent, 'outlook_handler'): self.outlook_handler = parent.outlook_handler if hasattr(parent, 'outlook_widget'): self.outlook_widget = parent.outlook_widget if hasattr(parent.outlook_widget, 'outlook_handler'): self.outlook_handler = parent.outlook_widget.outlook_handler self.setup_ui() def updatePosition(self): """Met à jour la position de la fenêtre pour suivre la principale""" if self.parent and self.isVisible(): # Positionnement centré par rapport à la fenêtre principale parentGeometry = self.parent.geometry() selfGeometry = self.geometry() newX = parentGeometry.x() + (parentGeometry.width() - selfGeometry.width()) // 2 newY = parentGeometry.y() + (parentGeometry.height() - selfGeometry.height()) // 2 self.move(newX, newY) def setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) layout.setContentsMargins(15, 15, 15, 15) # Style du dialogue self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QTableWidget { background-color: #03699c; color: white; gridline-color: #34495e; border: none; border-radius: 4px; } QTableWidget::item { padding: 5px; } QTableWidget QHeaderView::section { background-color: #2c3e50; color: white; padding: 5px; border: 1px solid #34495e; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; min-width: 100px; } QPushButton:hover { background-color: #007B9A; } """) # Titre title = QLabel("Rapport de validité des mails des Profiles") title.setStyleSheet("font-size: 14px; font-weight: bold; margin-bottom: 10px;") layout.addWidget(title) # Tableau des Emails expirés self.table = QTableWidget(len(self.expired_profiles), 4) self.table.setHorizontalHeaderLabels([ "Profil", "Jours validité", "Mails périmés", "Mails à périr" ]) # Définir des largeurs pour les colonnes self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) # Profil extensible for col in range(1, 4): self.table.setColumnWidth(col, 120) # Largeur fixe pour les autres colonnes self.table.verticalHeader().setDefaultSectionSize(40) # Hauteur des lignes # Remplir le tableau avec les données for row, profile in enumerate(self.expired_profiles): # Nom du profil name_item = QTableWidgetItem(profile['name']) name_item.setFlags(name_item.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 0, name_item) # Jours de validité configurés validity_days = QTableWidgetItem(str(profile.get('validity_days', 'N/A'))) validity_days.setTextAlignment(Qt.AlignCenter) validity_days.setFlags(validity_days.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 1, validity_days) # Compter les mails périmés et ceux qui vont périr expired_count = QTableWidgetItem("Calcul...") expired_count.setTextAlignment(Qt.AlignCenter) expired_count.setFlags(expired_count.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 2, expired_count) to_expire_count = QTableWidgetItem("Calcul...") to_expire_count.setTextAlignment(Qt.AlignCenter) to_expire_count.setFlags(to_expire_count.flags() & ~Qt.ItemIsEditable) self.table.setItem(row, 3, to_expire_count) # Lancer l'analyse des mails en arrière-plan pour ce profil QTimer.singleShot(100 * row, lambda p=profile, r=row: self.analyze_mails_for_profile(p, r)) layout.addWidget(self.table) # Bouton de fermeture button_layout = QHBoxLayout() button_layout.addStretch() self.close_button = QPushButton("Fermer") self.close_button.clicked.connect(self.accept) self.close_button.setFixedSize(120, 35) button_layout.addWidget(self.close_button) layout.addLayout(button_layout) def analyze_mails_for_profile(self, profile, row): """Analyse les mails pour un profil donné et met à jour le tableau""" try: # Charger les détails du profil profile_path = os.path.join(r"C:\AUTOLOOK\profiles", f"{profile['name']}.json") if not os.path.exists(profile_path): self.table.item(row, 2).setText("Profil non trouvé") self.table.item(row, 3).setText("Profil non trouvé") return with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) # Extraire les paramètres du profil outlook_settings = profile_data.get('outlook_settings', {}) mailbox = outlook_settings.get('mailbox', '') folder = outlook_settings.get('folder', '') sender = outlook_settings.get('sender', '') subject_keywords = outlook_settings.get('subject_keywords', '').split() if outlook_settings.get( 'subject_keywords') else [] link_keywords = outlook_settings.get('link_keywords', '').split() if outlook_settings.get( 'link_keywords') else [] validity_days = profile.get('validity_days') if not mailbox or not folder: self.table.item(row, 2).setText("Config. incomplète") self.table.item(row, 3).setText("Config. incomplète") return # Utiliser l'OutlookHandler pour compter les emails if self.outlook_handler: # Définir une fonction de progression simplifiée def progress_handler(current, total): # On n'affiche pas la progression ici pour ne pas perturber l'interface pass # Appel direct à count_emails du handler (sans count_only) try: result = self.outlook_handler.count_emails( mailbox, folder, sender, subject_keywords, link_keywords, None, validity_days, progress_callback=progress_handler ) if isinstance(result, dict): # Extraire les comptages du résultat expired_count = result.get('expired_count', 0) valid_count = result.get('valid_count', 0) # Mettre à jour le tableau avec les valeurs réelles self.table.item(row, 2).setText(str(expired_count)) # Estimation des emails qui vont expirer bientôt (dans les 7 prochains jours) # Nous simulons cette valeur à environ 10% des emails valides soon_to_expire = int(valid_count * 0.1) self.table.item(row, 3).setText(str(soon_to_expire)) else: self.table.item(row, 2).setText("0") self.table.item(row, 3).setText("0") except Exception as count_error: logger.error(f"Erreur lors du comptage des emails pour {profile['name']}: {str(count_error)}") self.table.item(row, 2).setText("Erreur comptage") self.table.item(row, 3).setText("Erreur comptage") else: # Comptage manuel si l'OutlookHandler n'est pas disponible try: pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") # Recherche de la boîte mail mailbox_found = None for i in range(outlook.Folders.Count): if outlook.Folders.Item(i + 1).Name == mailbox: mailbox_found = outlook.Folders.Item(i + 1) break if not mailbox_found: self.table.item(row, 2).setText("Boîte non trouvée") self.table.item(row, 3).setText("Boîte non trouvée") return # Recherche du dossier def find_folder(parent, name): for i in range(parent.Folders.Count): current = parent.Folders.Item(i + 1) if current.Name == name: return current # Recherche récursive result = find_folder(current, name) if result: return result return None folder_obj = find_folder(mailbox_found, folder) if not folder_obj: self.table.item(row, 2).setText("Dossier non trouvé") self.table.item(row, 3).setText("Dossier non trouvé") return # Comptage manuel des emails emails = folder_obj.Items expired_count = 0 valid_count = 0 for email in emails: if hasattr(email, 'ReceivedTime'): received_date = email.ReceivedTime # Convertir en datetime Python received_datetime = datetime( received_date.Year, received_date.Month, received_date.Day, received_date.Hour, received_date.Minute, received_date.Second ) # Calculer la date d'expiration expiry_date = received_datetime + timedelta(days=validity_days) # Vérifier si l'email est expiré if expiry_date < datetime.now(): expired_count += 1 else: valid_count += 1 # Estimation des emails qui vont expirer bientôt soon_to_expire = int(valid_count * 0.1) # Mise à jour du tableau self.table.item(row, 2).setText(str(expired_count)) self.table.item(row, 3).setText(str(soon_to_expire)) except Exception as e: logger.error(f"Erreur lors du comptage manuel des emails pour {profile['name']}: {str(e)}") self.table.item(row, 2).setText("Erreur manuelle") self.table.item(row, 3).setText("Erreur manuelle") except Exception as e: logger.error(f"Erreur générale lors de l'analyse des mails pour {profile['name']}: {str(e)}") self.table.item(row, 2).setText("Erreur") self.table.item(row, 3).setText("Erreur") class FloatingPanel(QWidget): def __init__(self, main_window): super().__init__(main_window) self.main_window = main_window self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_TranslucentBackground) # Remplacez les lignes de positionnement par : self.vertical_offset = 250 # Distance au-dessus de la fenêtre principale self.initial_y = self.main_window.y() - self.vertical_offset self.target_y = self.initial_y # Réduire la hauteur du volet pour qu'il soit moins intrusif self.setFixedSize(main_window.width(), 200) # Hauteur réduite de 180 à 120 self.is_open = False # État initial (fermé) # Effet de fondu self.opacity_effect = QGraphicsOpacityEffect(self) self.setGraphicsEffect(self.opacity_effect) self.opacity_effect.setOpacity(0.0) # Opacité initiale à 0 # Animation pour le déplacement self.move_animation = QPropertyAnimation(self, b"geometry") self.move_animation.setDuration(300) # Durée légèrement plus courte pour réduire les tremblements # Animation pour le fondu self.opacity_animation = QPropertyAnimation(self.opacity_effect, b"opacity") self.opacity_animation.setDuration(300) # Même durée que le déplacement # Layout principal layout = QVBoxLayout(self) layout.setContentsMargins(10, 10, 10, 10) # Marges réduites layout.setSpacing(8) # Espacement réduit # Remplacer le titre par les informations système sur une ligne info_header = QFrame() info_header.setStyleSheet(""" background-color: #1a2a3a; color: white; padding: 5px; border-radius: 5px; """) # Layout horizontal pour les infos système info_header_layout = QHBoxLayout(info_header) info_header_layout.setContentsMargins(5, 3, 5, 3) # Marges réduites info_header_layout.setSpacing(10) # Label pour les informations de RAM self.ram_info_label = QLabel("RAM : --") self.ram_info_label.setAlignment(Qt.AlignCenter) self.ram_info_label.setStyleSheet(""" color: #0ff6f2; font-size: 12px; font-weight: bold; border-right: 1px solid #34495e; padding-right: 10px; """) # Label pour les informations CPU self.cpu_info_label = QLabel("CPU : --") self.cpu_info_label.setAlignment(Qt.AlignCenter) self.cpu_info_label.setStyleSheet(""" color: #0ff6f2; font-size: 12px; font-weight: bold; border-right: 1px solid #34495e; padding-right: 10px; """) # Label pour l'affichage des cœurs CPU utilisés self.cpu_cores_label = QLabel("Cœurs : --") self.cpu_cores_label.setAlignment(Qt.AlignCenter) self.cpu_cores_label.setStyleSheet(""" color: #0ff6f2; font-size: 12px; font-weight: bold; """) # Ajouter les labels au layout horizontal info_header_layout.addWidget(self.ram_info_label, 1) info_header_layout.addWidget(self.cpu_info_label, 1) info_header_layout.addWidget(self.cpu_cores_label, 1) # Ajouter le cadre d'info au layout principal layout.addWidget(info_header) # Section d'informations info_panel = self.create_info_panel() layout.addWidget(info_panel) # Position initiale hors écran et masqué self.initial_y = self.main_window.y() - 120 # Ajusté à la nouvelle hauteur self.target_y = self.main_window.y() - 80 # Position plus haute (40px sous le haut de la fenêtre principale) self.move(self.main_window.x(), self.initial_y) self.hide() # Masquer complètement au départ # Désactiver les animations pendant l'initialisation self.setUpdatesEnabled(False) QTimer.singleShot(100, self.enable_updates) # Timer pour mettre à jour les informations self.info_update_timer = QTimer(self) self.info_update_timer.timeout.connect(self.update_info_display) self.info_update_timer.start(1500) # Mise à jour très fréquente pour une parfaite synchronisation def create_info_panel(self): """Crée le panneau d'informations en version plus compacte""" group = QGroupBox("Informations") group.setStyleSheet(""" QGroupBox { color: white; border: 1px solid #34495e; border-radius: 5px; margin-top: 5px; font-weight: bold; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; font-size: 11px; } """) layout = QVBoxLayout(group) layout.setSpacing(5) layout.setContentsMargins(5, 10, 5, 5) # Marges réduites # Grille pour les informations info_grid = QGridLayout() info_grid.setSpacing(3) info_grid.setContentsMargins(0, 0, 0, 0) # Frame temps d'affichage display_frame = QFrame() display_frame.setStyleSheet(""" QFrame { background-color: #1a2a3a; border-radius: 3px; padding: 2px; max-height: 45px; } QLabel { color: white; font-size: 11px; } """) display_layout = QVBoxLayout(display_frame) display_layout.setSpacing(0) display_layout.setContentsMargins(2, 2, 2, 2) display_title = QLabel("👁️ Temps d'affichage") display_title.setAlignment(Qt.AlignCenter) # Valeur initiale vide pour être mise à jour par update_info_display self.display_value = QLabel("") self.display_value.setStyleSheet("color: #94e5e1;") self.display_value.setAlignment(Qt.AlignCenter) display_layout.addWidget(display_title) # Temps de pause pause_frame = QFrame() pause_frame.setStyleSheet(""" QFrame { background-color: #1a2a3a; border-radius: 3px; padding: 2px; max-height: 45px; } QLabel { color: white; font-size: 11px; } """) pause_layout = QVBoxLayout(pause_frame) pause_layout.setSpacing(0) pause_layout.setContentsMargins(2, 2, 2, 2) pause_title = QLabel("⏲️ Temps de pause") pause_title.setAlignment(Qt.AlignCenter) # Valeur initiale vide pour être mise à jour par update_info_display self.pause_value = QLabel("") self.pause_value.setStyleSheet("color: #94e5e1;") self.pause_value.setAlignment(Qt.AlignCenter) pause_layout.addWidget(pause_title) # Liens simultanés links_frame = QFrame() links_frame.setStyleSheet(""" QFrame { background-color: #1a2a3a; border-radius: 3px; padding: 2px; max-height: 45px; } QLabel { color: white; font-size: 11px; } """) links_layout = QHBoxLayout(links_frame) links_layout.setSpacing(0) links_layout.setContentsMargins(2, 2, 2, 2) links_title = QLabel("🔗 Liens simultanés") links_title.setAlignment(Qt.AlignCenter) # Valeur initiale vide pour être mise à jour par update_info_display self.links_value = QLabel("") self.links_value.setStyleSheet("color: #94e5e1;") self.links_value.setAlignment(Qt.AlignCenter) links_layout.addWidget(links_title) links_layout.addWidget(self.links_value) # Ajouter les frames à la grille info_grid.addWidget(display_frame, 0, 0) info_grid.addWidget(pause_frame, 0, 1) info_grid.addWidget(links_frame, 0, 2) # Créer un frame pour les paramètres de cycle cycle_frame = QFrame() cycle_frame.setStyleSheet(""" QFrame { background-color: #1a2a3a; border-radius: 3px; padding: 2px; max-height: 45px; } QLabel { color: white; font-size: 11px; } """) cycle_layout = QHBoxLayout(cycle_frame) cycle_layout.setSpacing(0) cycle_layout.setContentsMargins(2, 2, 2, 2) # Label pour indiquer le statut du traitement par cycle self.cycle_status_label = QLabel("🔄 Cycle: désactivé") self.cycle_status_label.setAlignment(Qt.AlignCenter) # Label pour afficher les paramètres de cycle self.cycle_params_label = QLabel("") self.cycle_params_label.setStyleSheet("color: #94e5e1;") self.cycle_params_label.setAlignment(Qt.AlignCenter) cycle_layout.addWidget(self.cycle_status_label) cycle_layout.addWidget(self.cycle_params_label) # Ajouter le frame à la grille info_grid.addWidget(cycle_frame, 1, 0, 1, 3) # Occupe toute la ligne 1, colonnes 0-2 # Ajouter les boutons de contrôle en mode plein écran self.control_buttons_frame = QFrame() self.control_buttons_frame.setStyleSheet(""" QFrame { background-color: #1a2a3a; border-radius: 3px; padding: 2px; margin-top: 5px; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 4px 8px; border-radius: 3px; font-size: 10px; font-weight: bold; margin: 1px; } QPushButton:hover { background-color: #007B9A; } """) control_buttons_layout = QHBoxLayout(self.control_buttons_frame) control_buttons_layout.setSpacing(3) control_buttons_layout.setContentsMargins(3, 3, 3, 3) # Bouton Toggle Volet (pour fermer) self.close_panel_button = QPushButton("Fermer volet") self.close_panel_button.clicked.connect(self.toggle_panel) # Bouton Sauvegarde profil self.save_profile_button = QPushButton("Sauvegarder Profil") self.save_profile_button.clicked.connect(self.main_window.save_profile) # Bouton Paramètres self.settings_button = QPushButton("Paramètres") self.settings_button.clicked.connect(self.main_window.toggle_sidebar) control_buttons_layout.addWidget(self.close_panel_button) control_buttons_layout.addWidget(self.save_profile_button) control_buttons_layout.addWidget(self.settings_button) # Ajouter à la grille des informations info_grid.addWidget(self.control_buttons_frame, 2, 0, 1, 3) # Ligne 2, colonnes 0-2 # Masquer par défaut (seulement visible en plein écran) self.control_buttons_frame.setVisible(False) layout.addLayout(info_grid) return group def update_info_display(self): """Met à jour les informations affichées dans le panneau""" try: # ===================================================================== # SECTION 1 : INFORMATIONS SYSTÈME (RAM, CPU, CORES) # ===================================================================== try: # Informations RAM ram = psutil.virtual_memory() ram_percent = ram.percent ram_used = ram.used / (1024 * 1024 * 1024) # Conversion en GB ram_total = ram.total / (1024 * 1024 * 1024) # Conversion en GB self.ram_info_label.setText(f"RAM : {ram_used:.1f} GB / {ram_total:.1f} GB ({ram_percent:.1f}%)") # Informations CPU cpu_percent = psutil.cpu_percent() cpu_freq = psutil.cpu_freq() if cpu_freq: current_freq = cpu_freq.current / 1000 # Conversion en GHz self.cpu_info_label.setText(f"CPU : {cpu_percent:.1f}% @ {current_freq:.1f}GHz") else: self.cpu_info_label.setText(f"CPU : {cpu_percent:.1f}%") # Informations sur les cœurs CPU try: current_process = psutil.Process() cpu_count = psutil.cpu_count() cpu_count_physical = psutil.cpu_count(logical=False) affinity = len(current_process.cpu_affinity()) self.cpu_cores_label.setText(f"Cœurs : {affinity}/{cpu_count} actifs ({cpu_count_physical} phys.)") except Exception: self.cpu_cores_label.setText("Cœurs : Information non disponible") # Adapter la couleur selon l'utilisation CPU if cpu_percent > 80: cpu_color = "#FF5252" # Rouge elif cpu_percent > 50: cpu_color = "#FFC107" # Orange else: cpu_color = "#4CAF50" # Vert self.cpu_info_label.setStyleSheet(f""" color: {cpu_color}; font-size: 12px; font-weight: bold; border-right: 1px solid #34495e; padding-right: 10px; """) except Exception as e: # En cas d'erreur, afficher un message générique self.ram_info_label.setText("RAM : Information non disponible") self.cpu_info_label.setText("CPU : Information non disponible") self.cpu_cores_label.setText("Cœurs : Information non disponible") print(f"Erreur informations système: {e}") # ===================================================================== # SECTION 2 : TEMPS D'AFFICHAGE, PAUSE ET LIENS # ===================================================================== # Initialiser les valeurs par défaut display_text = "N/A" pause_text = "N/A" links_text = "N/A" # MÉTHODE 1 : Récupération directe depuis les sliders (priorité haute) if (hasattr(self.main_window, 'pause_slider') and hasattr(self.main_window, 'between_pages_slider') and hasattr(self.main_window, 'links_slider')): try: # Temps d'affichage (pause_slider) try: if hasattr(self.main_window.pause_slider, 'values'): min_display, max_display = self.main_window.pause_slider.values() display_text = f"{min_display}-{max_display}s" elif hasattr(self.main_window.pause_slider, 'min_slider') and hasattr( self.main_window.pause_slider, 'max_slider'): min_display = self.main_window.pause_slider.min_slider.value() max_display = self.main_window.pause_slider.max_slider.value() display_text = f"{min_display}-{max_display}s" else: display_text = f"{self.main_window.pause_slider.value()}s" except Exception as e: print(f"Erreur récupération temps d'affichage: {e}") # Temps de pause entre pages (between_pages_slider) try: if hasattr(self.main_window.between_pages_slider, 'values'): min_pause, max_pause = self.main_window.between_pages_slider.values() pause_text = f"{min_pause}-{max_pause}s" elif hasattr(self.main_window.between_pages_slider, 'min_slider') and hasattr( self.main_window.between_pages_slider, 'max_slider'): min_pause = self.main_window.between_pages_slider.min_slider.value() max_pause = self.main_window.between_pages_slider.max_slider.value() pause_text = f"{min_pause}-{max_pause}s" else: pause_text = f"{self.main_window.between_pages_slider.value()}s" except Exception as e: print(f"Erreur récupération temps de pause: {e}") # Nombre de liens simultanés try: links_count = self.main_window.links_slider.value() links_text = f"{links_count}" except Exception as e: print(f"Erreur récupération nombre de liens: {e}") except Exception as e: print(f"Erreur récupération générale depuis sliders: {e}") # MÉTHODE 2 : Fallback via les labels du panneau latéral if display_text == "N/A" or pause_text == "N/A" or links_text == "N/A": try: if hasattr(self.main_window, 'pause_value_label') and self.main_window.pause_value_label.text(): if display_text == "N/A": display_text = f"{self.main_window.pause_value_label.text()}s" if hasattr(self.main_window, 'between_pages_value_label') and self.main_window.between_pages_value_label.text(): if pause_text == "N/A": pause_text = f"{self.main_window.between_pages_value_label.text()}s" if hasattr(self.main_window, 'links_value_label') and self.main_window.links_value_label.text(): if links_text == "N/A": links_text = self.main_window.links_value_label.text() except Exception as e: print(f"Erreur récupération depuis labels latéraux: {e}") # MÉTHODE 3 : Fallback via OutlookHandler if (display_text == "N/A" or pause_text == "N/A" or links_text == "N/A") and hasattr(self.main_window, 'outlook_widget'): try: outlook_widget = self.main_window.outlook_widget if hasattr(outlook_widget, 'outlook_handler') and outlook_widget.outlook_handler: handler = outlook_widget.outlook_handler # Temps d'affichage depuis handler if display_text == "N/A": if hasattr(handler, 'min_pause_time') and hasattr(handler, 'max_pause_time'): display_text = f"{handler.min_pause_time}-{handler.max_pause_time}s" elif hasattr(handler, 'pause_time'): display_text = f"{handler.pause_time}s" # Temps de pause depuis handler if pause_text == "N/A": if hasattr(handler, 'min_between_pages_time') and hasattr(handler, 'max_between_pages_time'): pause_text = f"{handler.min_between_pages_time}-{handler.max_between_pages_time}s" elif hasattr(handler, 'between_pages_time'): pause_text = f"{handler.between_pages_time}s" # Nombre de liens depuis handler if links_text == "N/A": if hasattr(handler, 'links_count'): links_text = f"{handler.links_count}" elif hasattr(handler, 'max_tabs'): links_text = f"{handler.max_tabs}" except Exception as e: print(f"Erreur récupération depuis OutlookHandler: {e}") # Appliquer les valeurs récupérées aux labels try: if hasattr(self, 'display_value'): self.display_value.setText(display_text) if hasattr(self, 'pause_value'): self.pause_value.setText(pause_text) if hasattr(self, 'links_value'): self.links_value.setText(links_text) except Exception as e: print(f"Erreur mise à jour labels principaux: {e}") # ===================================================================== # SECTION 3 : INFORMATIONS DE CYCLE # ===================================================================== try: if hasattr(self.main_window, 'cycle_processing_checkbox'): cycle_enabled = self.main_window.cycle_processing_checkbox.isChecked() if cycle_enabled: emails_text = "N/A" cycle_pause_text = "N/A" # Récupérer les paramètres d'emails par cycle if hasattr(self.main_window, 'emails_per_cycle_slider'): try: if hasattr(self.main_window.emails_per_cycle_slider, 'values'): min_emails, max_emails = self.main_window.emails_per_cycle_slider.values() emails_text = f"{min_emails}-{max_emails}" else: emails_text = str(self.main_window.emails_per_cycle_slider.value()) except Exception as e: print(f"Erreur récupération emails par cycle: {e}") # Récupérer les paramètres de pause entre cycles if hasattr(self.main_window, 'pause_between_cycles_slider'): try: if hasattr(self.main_window.pause_between_cycles_slider, 'values'): min_pause_cycle, max_pause_cycle = self.main_window.pause_between_cycles_slider.values() cycle_pause_text = f"{min_pause_cycle}-{max_pause_cycle}s" else: pause_seconds = self.main_window.pause_between_cycles_slider.value() cycle_pause_text = f"{pause_seconds}s" except Exception as e: print(f"Erreur récupération pause entre cycles: {e}") # Fallback via les champs de saisie if emails_text == "N/A": try: if (hasattr(self.main_window, 'emails_per_cycle_min_input') and hasattr(self.main_window, 'emails_per_cycle_max_input')): min_val = self.main_window.emails_per_cycle_min_input.text() max_val = self.main_window.emails_per_cycle_max_input.text() if min_val and max_val: emails_text = f"{min_val}-{max_val}" except Exception: pass if cycle_pause_text == "N/A": try: if (hasattr(self.main_window, 'pause_between_cycles_min_input') and hasattr(self.main_window, 'pause_between_cycles_max_input')): min_val = self.main_window.pause_between_cycles_min_input.text() max_val = self.main_window.pause_between_cycles_max_input.text() if min_val and max_val: cycle_pause_text = f"{min_val}-{max_val}s" except Exception: pass # Mettre à jour l'affichage du cycle actif if hasattr(self, 'cycle_status_label') and hasattr(self, 'cycle_params_label'): self.cycle_status_label.setText("🔄 Cycle: activé") self.cycle_params_label.setText(f"{emails_text} emails / {cycle_pause_text} pause") self.cycle_status_label.setStyleSheet("color: #4CAF50; font-weight: bold;") else: # Cycle désactivé if hasattr(self, 'cycle_status_label') and hasattr(self, 'cycle_params_label'): self.cycle_status_label.setText("🔄 Cycle: désactivé") self.cycle_params_label.setText("") self.cycle_status_label.setStyleSheet("color: #9E9E9E; font-weight: normal;") except Exception as e: print(f"Erreur mise à jour informations de cycle: {e}") except Exception as e: print(f"Erreur générale dans update_info_display: {e}") # En cas d'erreur critique, mettre des valeurs par défaut try: if hasattr(self, 'display_value'): self.display_value.setText("Erreur") if hasattr(self, 'pause_value'): self.pause_value.setText("Erreur") if hasattr(self, 'links_value'): self.links_value.setText("Erreur") except: pass def enable_updates(self): """Réactive les mises à jour visuelles après l'initialisation""" self.setUpdatesEnabled(True) self.update_info_display() # Force une mise à jour immédiate def toggle_panel(self): """Ouvre ou ferme le volet avec animation fluide et stable""" try: # Arrêter toute animation en cours self.move_animation.stop() self.opacity_animation.stop() # Position actuelle current_x = self.main_window.x() is_fullscreen = self.main_window.isMaximized() or self.main_window.isFullScreen() # Ajuster la largeur avant l'animation self.setFixedWidth(self.main_window.width()) if is_fullscreen: # Mode plein écran : animation verticale hidden_y = -self.height() visible_y = 10 target_x = 0 # Gérer l'affichage des boutons de contrôle if hasattr(self, 'control_buttons_frame'): self.control_buttons_frame.setVisible(not self.is_open) # Visible quand on ouvre if self.is_open: # Fermeture self.move_animation.setStartValue(QRect(target_x, visible_y, self.width(), self.height())) self.move_animation.setEndValue(QRect(target_x, hidden_y, self.width(), self.height())) self.opacity_animation.setStartValue(1.0) self.opacity_animation.setEndValue(0.0) self.opacity_animation.finished.connect(self.hide_panel) else: # Ouverture self.move(target_x, hidden_y) self.show() self.move_animation.setStartValue(QRect(target_x, hidden_y, self.width(), self.height())) self.move_animation.setEndValue(QRect(target_x, visible_y, self.width(), self.height())) self.opacity_animation.setStartValue(0.0) self.opacity_animation.setEndValue(1.0) # Déconnecter le signal hide try: self.opacity_animation.finished.disconnect(self.hide_panel) except: pass else: # Mode fenêtré : animation d'opacité seulement position_y = self.main_window.y() - self.vertical_offset target_x = self.main_window.x() # Vérifications des limites d'écran screen = QApplication.desktop().screenGeometry() if position_y < 10: position_y = 10 if target_x + self.width() > screen.width(): target_x = screen.width() - self.width() if target_x < 0: target_x = 0 if hasattr(self, 'control_buttons_frame'): self.control_buttons_frame.setVisible(False) # Masqué en mode fenêtré if self.is_open: # Animation de fermeture self.opacity_animation.setStartValue(1.0) self.opacity_animation.setEndValue(0.0) self.opacity_animation.finished.connect(self.hide_panel) else: # Positionner avant d'afficher self.move(target_x, position_y) self.show() self.opacity_animation.setStartValue(0.0) self.opacity_animation.setEndValue(1.0) # Déconnecter le signal hide try: self.opacity_animation.finished.disconnect(self.hide_panel) except: pass # Démarrer les animations if is_fullscreen: self.move_animation.start() self.opacity_animation.start() # Inverser l'état self.is_open = not self.is_open # Forcer une mise à jour des informations QTimer.singleShot(100, self.update_info_display) except Exception as e: print(f"Erreur toggle panel: {e}") def update_panel_position(self): """Synchronise la position du volet avec la fenêtre principale""" try: # Adapter la largeur à celle de la fenêtre principale self.setFixedWidth(self.main_window.width()) # Déterminer le mode d'affichage is_fullscreen = self.main_window.isMaximized() or self.main_window.isFullScreen() if is_fullscreen: # Mode plein écran : position en haut de l'écran self.initial_y = -self.height() # Caché au-dessus self.target_y = 10 # Position visible en haut target_x = 0 # Aligné à gauche de l'écran else: # Mode fenêtré : au-dessus de la fenêtre principale self.initial_y = self.main_window.y() - self.vertical_offset self.target_y = self.initial_y target_x = self.main_window.x() # Vérification pour ne pas sortir de l'écran screen = QApplication.desktop().screenGeometry() if self.target_y < 10: self.target_y = 10 if target_x + self.width() > screen.width(): target_x = screen.width() - self.width() if target_x < 0: target_x = 0 # Mettre à jour la visibilité des boutons selon le mode if hasattr(self, 'control_buttons_frame'): self.control_buttons_frame.setVisible(is_fullscreen and self.is_open) # Appliquer la position selon l'état du volet if self.is_open: if is_fullscreen: self.move(target_x, self.target_y) else: self.move(target_x, self.target_y) else: self.move(target_x, self.initial_y) except Exception as e: print(f"Erreur positionnement volet: {e}") def is_fullscreen_mode(self): """Vérifie si l'application est en mode plein écran""" return self.main_window.isMaximized() or self.main_window.isFullScreen() def hide_panel(self): """Cache le panneau une fois l'animation terminée""" self.hide() class TaskJournalDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.setWindowTitle("Journal des tâches") self.setMinimumSize(800, 600) # Style global du dialogue self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QGroupBox { color: white; border: 1px solid #34495e; border-radius: 4px; margin-top: 10px; } QGroupBox::title { color: white; } QCheckBox { color: white; } """) self.setup_ui() self.load_past_tasks() def setup_ui(self): layout = QVBoxLayout(self) # En-tête header = QLabel("Journal des tâches passées") header.setStyleSheet(""" QLabel { font-size: 16px; font-weight: bold; color: white; background-color: #2c3e50; padding: 10px; border-radius: 4px; } """) layout.addWidget(header) # Liste des tâches passées avec cases à cocher self.tasks_list = QListWidget() self.tasks_list.setSelectionMode(QListWidget.ExtendedSelection) self.tasks_list.setStyleSheet(""" QListWidget { background-color: #03699c; border-radius: 4px; padding: 5px; } QListWidget::item { background-color: blue; color: white; margin: 2px; padding: 5px; border-radius: 2px; } QListWidget::item:selected { background-color: #3498db; color: white; } """) layout.addWidget(self.tasks_list) # Boutons d'action button_layout = QHBoxLayout() # Sélection multiple select_layout = QHBoxLayout() self.select_all = QPushButton("Tout sélectionner") self.select_none = QPushButton("Tout désélectionner") for btn in [self.select_all, self.select_none]: btn.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #7f8c8d; } """) self.select_all.clicked.connect(self.select_all_items) self.select_none.clicked.connect(self.deselect_all_items) select_layout.addWidget(self.select_all) select_layout.addWidget(self.select_none) layout.addLayout(select_layout) # Bouton Supprimer self.delete_button = QPushButton("🗑️ Supprimer la sélection") self.delete_button.clicked.connect(self.delete_selected_tasks) self.delete_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #c0392b; } """) self.replan_button = QPushButton("🔄 Replanifier") self.replan_button.clicked.connect(self.replan_task) self.replan_button.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) self.export_button = QPushButton("📊 Exporter la sélection") self.export_button.clicked.connect(self.export_selected_tasks) self.export_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #219a52; } """) button_layout.addWidget(self.delete_button) button_layout.addWidget(self.replan_button) button_layout.addWidget(self.export_button) button_layout.addStretch() layout.addLayout(button_layout) # Connecter le signal de sélection pour mettre à jour l'état des boutons self.tasks_list.itemSelectionChanged.connect(self.update_button_states) def select_all_items(self): """Sélectionne tous les éléments de la liste""" for i in range(self.tasks_list.count()): self.tasks_list.item(i).setSelected(True) def deselect_all_items(self): """Désélectionne tous les éléments de la liste""" for i in range(self.tasks_list.count()): self.tasks_list.item(i).setSelected(False) def update_button_states(self): """Met à jour l'état des boutons en fonction de la sélection""" selected_items = self.tasks_list.selectedItems() has_selection = len(selected_items) > 0 self.delete_button.setEnabled(has_selection) self.export_button.setEnabled(has_selection) # Replanifier uniquement disponible pour une sélection unique self.replan_button.setEnabled(len(selected_items) == 1) def delete_selected_tasks(self): """Supprime les tâches sélectionnées de l'historique""" selected_items = self.tasks_list.selectedItems() if not selected_items: QMessageBox.warning(self, "Attention", "Veuillez sélectionner au moins une tâche à supprimer") return reply = QMessageBox.question( self, "Confirmation de suppression", f"Êtes-vous sûr de vouloir supprimer {len(selected_items)} tâche(s) de l'historique ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: try: # Récupérer l'historique actuel history_file = os.path.join(r"C:\autolook", "completed_tasks.json") with open(history_file, 'r') as f: completed_tasks = json.load(f) # Créer un ensemble des tâches à supprimer pour une recherche plus efficace tasks_to_remove = { (item.data(Qt.UserRole)['profile'], item.data(Qt.UserRole)['date'], item.data(Qt.UserRole)['time']) for item in selected_items } # Filtrer les tâches completed_tasks = [ task for task in completed_tasks if (task['profile'], task['date'], task['time']) not in tasks_to_remove ] # Sauvegarder l'historique mis à jour with open(history_file, 'w') as f: json.dump(completed_tasks, f, indent=4) # Mettre à jour l'affichage self.load_past_tasks() QMessageBox.information(self, "Succès", "Les tâches sélectionnées ont été supprimées") except Exception as e: logger.error(f"Erreur lors de la suppression des tâches : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression : {str(e)}") def export_selected_tasks(self): """Exporte les tâches sélectionnées au format CSV""" selected_items = self.tasks_list.selectedItems() if not selected_items: QMessageBox.warning(self, "Attention", "Veuillez sélectionner au moins une tâche à exporter") return try: file_path, _ = QFileDialog.getSaveFileName( self, "Exporter les tâches sélectionnées", f"journal_taches_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", "Fichiers CSV (*.csv)" ) if file_path: with open(file_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow( ['Profil', 'Date', 'Heure', 'Récurrence', 'Nombre de mails', 'Statut', 'Temps estimé']) for item in selected_items: task = item.data(Qt.UserRole) writer.writerow([ task['profile'], task['date'], task['time'], task['recurrence'], task.get('mail_count', 1), task.get('status', 'completed'), task.get('estimated_time', 'N/A') ]) QMessageBox.information(self, "Succès", "Les tâches sélectionnées ont été exportées avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export : {str(e)}") def delete_task(self): """Supprime la tâche sélectionnée de l'historique""" try: current_item = self.tasks_list.currentItem() if not current_item: QMessageBox.warning(self, "Attention", "Veuillez sélectionner une tâche à supprimer") return reply = QMessageBox.question( self, "Confirmation de suppression", "Êtes-vous sûr de vouloir supprimer cette tâche de l'historique ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # Récupérer l'historique actuel history_file = os.path.join(r"C:\autolook", "completed_tasks.json") with open(history_file, 'r') as f: completed_tasks = json.load(f) # Récupérer les données de la tâche à supprimer task_data = current_item.data(Qt.UserRole) # Supprimer la tâche de la liste completed_tasks = [task for task in completed_tasks if not (task['profile'] == task_data['profile'] and task['date'] == task_data['date'] and task['time'] == task_data['time'])] # Sauvegarder l'historique mis à jour with open(history_file, 'w') as f: json.dump(completed_tasks, f, indent=4) # Mettre à jour l'affichage self.load_past_tasks() QMessageBox.information(self, "Succès", "La tâche a été supprimée de l'historique") # Mettre à jour l'interface parent si nécessaire if hasattr(self.parent, 'update_tasks_display'): self.parent.update_tasks_display() except Exception as e: logger.error(f"Erreur lors de la suppression de la tâche : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors de la suppression : {str(e)}") def load_past_tasks(self): """Charge les tâches passées depuis le fichier de l'historique""" try: self.tasks_list.clear() history_file = os.path.join(r"C:\autolook", "completed_tasks.json") if os.path.exists(history_file): with open(history_file, 'r') as f: completed_tasks = json.load(f) for task in completed_tasks: # Charger les paramètres du profil pour obtenir les temps de pause profile_path = os.path.join(r"C:\autolook\profiles", f"{task['profile']}.json") display_time_min = 15 # Valeurs par défaut display_time_max = 25 between_pages_min = 5 between_pages_max = 10 if os.path.exists(profile_path): with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) if 'settings' in profile_data: settings = profile_data['settings'] if 'pause_time_range' in settings: display_time_min = settings['pause_time_range']['min'] display_time_max = settings['pause_time_range']['max'] if 'between_pages_time_range' in settings: between_pages_min = settings['between_pages_time_range']['min'] between_pages_max = settings['between_pages_time_range']['max'] # Calculer le temps estimé mail_count = task.get('mail_count', 1) avg_display_time = (display_time_min + display_time_max) / 2 avg_between_time = (between_pages_min + between_pages_max) / 2 total_time_sec = mail_count * (avg_display_time + avg_between_time) # Convertir en minutes et secondes est_minutes = int(total_time_sec // 60) est_seconds = int(total_time_sec % 60) estimated_time = f"{est_minutes}m {est_seconds}s" # Date d'exécution execution_date = datetime.fromisoformat( task['executed_at']) if 'executed_at' in task else 'Date inconnue' task_status = task.get('status', 'completed') # Créer le texte de l'item avec le temps estimé task_info = (f"{task['profile']} - " f"Planifié: {task['date']} {task['time']} - " f"Exécuté: {execution_date if isinstance(execution_date, str) else execution_date.strftime('%d/%m/%Y %H:%M')} - " f"Statut: {task_status} - " f"Mails: {mail_count} - " f"Temps estimé: {estimated_time}") item = QListWidgetItem(task_info) item.setData(Qt.UserRole, task) # Définir la couleur de fond en fonction du statut if task_status == 'missed': item.setBackground(QColor('#ffebee')) # Rouge clair pour les tâches manquées else: item.setBackground(QColor('#e3f2fd')) # Bleu clair pour les tâches complétées self.tasks_list.addItem(item) except Exception as e: logger.error(f"Erreur lors du chargement de l'historique : {str(e)}") QMessageBox.critical(self, "Erreur", f"Erreur lors du chargement de l'historique : {str(e)}") def save_completed_task(self, task): try: history_file = os.path.join(r"C:\autolook", "completed_tasks.json") completed_tasks = [] if os.path.exists(history_file): with open(history_file, 'r') as f: completed_tasks = json.load(f) task['completed_at'] = datetime.now().isoformat() completed_tasks.append(task) with open(history_file, 'w') as f: json.dump(completed_tasks, f, indent=4) except Exception as e: logger.error(f"Erreur lors de la sauvegarde de la tâche complétée : {str(e)}") def replan_task(self): """Replanifie la tâche sélectionnée""" try: current_item = self.tasks_list.currentItem() if not current_item: QMessageBox.warning(self, "Attention", "Veuillez sélectionner une tâche à replanifier") return task = current_item.data(Qt.UserRole) # Créer le dialogue de replanification replan_dialog = ReplanTaskDialog(self, task) if replan_dialog.exec_() == QDialog.Accepted: new_schedule = replan_dialog.get_new_schedule() self.add_replanned_task(new_schedule) QMessageBox.information(self, "Succès", "Tâche replanifiée avec succès") # Actualiser les affichages if hasattr(self.parent, 'update_tasks_display'): self.parent.update_tasks_display() if hasattr(self.parent, 'update_tasks_combo'): self.parent.update_tasks_combo() except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la replanification : {str(e)}") def add_replanned_task(self, new_schedule): """Ajoute une tâche replanifiée au fichier des tâches""" try: schedules_file = os.path.join(r"C:\autolook", "schedules.json") schedules = [] if os.path.exists(schedules_file): with open(schedules_file, 'r') as f: schedules = json.load(f) schedules.append(new_schedule) with open(schedules_file, 'w') as f: json.dump(schedules, f, indent=4) except Exception as e: raise Exception(f"Erreur lors de l'ajout de la tâche replanifiée : {str(e)}") def export_journal(self): """Exporte le journal des tâches au format CSV""" try: file_path, _ = QFileDialog.getSaveFileName( self, "Exporter le journal", f"journal_taches_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", "Fichiers CSV (*.csv)" ) if file_path: with open(file_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['Profil', 'Date', 'Heure', 'Récurrence', 'Nombre de mails']) for i in range(self.tasks_list.count()): task = self.tasks_list.item(i).data(Qt.UserRole) writer.writerow([ task['profile'], task['date'], task['time'], task['recurrence'], task.get('mail_count', 1) ]) QMessageBox.information(self, "Succès", "Journal exporté avec succès") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export : {str(e)}") class SystemDiagnostic(QThread): """Thread pour exécuter les diagnostics système en arrière-plan""" progress_updated = pyqtSignal(int, str) diagnostic_complete = pyqtSignal(dict) def __init__(self, critical_files=None): super().__init__() self.critical_files = critical_files or [ r"C:\AUTOLOOK\main.py", r"C:\AUTOLOOK\modules\module_outlook_2021.py", r"C:\AUTOLOOK\modules\ha2.py" ] self.backup_dir = r"C:\AUTOLOOK\backup" os.makedirs(self.backup_dir, exist_ok=True) def run(self): self.progress_updated.emit(0, "Démarrage du diagnostic système...") results = { "status": "ok", "issues": [], "checksums": {}, "repairs": [] } # Étape 1: Vérifier les fichiers critiques self.progress_updated.emit(10, "Vérification des fichiers critiques...") for i, file_path in enumerate(self.critical_files): progress = 10 + (i * 20 // len(self.critical_files)) self.progress_updated.emit(progress, f"Vérification de {os.path.basename(file_path)}...") if not os.path.exists(file_path): results["status"] = "critical" results["issues"].append(f"Fichier manquant: {file_path}") continue # Vérifier les checksums par rapport aux sauvegardes checksum = self.calculate_checksum(file_path) results["checksums"][file_path] = checksum # Vérifier si on a une sauvegarde pour ce fichier backup_path = os.path.join(self.backup_dir, os.path.basename(file_path) + ".bak") if os.path.exists(backup_path): backup_checksum = self.calculate_checksum(backup_path) if checksum != backup_checksum: results["status"] = "warning" results["issues"].append(f"Le fichier {file_path} a été modifié depuis la dernière sauvegarde") # Étape 2: Vérifier les ressources système self.progress_updated.emit(40, "Vérification des ressources système...") # Vérifier la RAM try: memory = psutil.virtual_memory() if memory.percent > 90: results["status"] = "warning" results["issues"].append(f"Mémoire RAM critique: {memory.percent}% utilisée") except Exception as e: results["issues"].append(f"Erreur lors de la vérification de la RAM: {str(e)}") # Vérifier le CPU try: cpu_percent = psutil.cpu_percent(interval=1) if cpu_percent > 90: results["status"] = "warning" results["issues"].append(f"Utilisation CPU critique: {cpu_percent}%") except Exception as e: results["issues"].append(f"Erreur lors de la vérification du CPU: {str(e)}") # Étape 3: Vérifier l'espace disque self.progress_updated.emit(60, "Vérification de l'espace disque...") try: disk = psutil.disk_usage('C:/') if disk.percent > 95: results["status"] = "critical" results["issues"].append(f"Espace disque critique: {disk.percent}% utilisé") elif disk.percent > 85: results["status"] = "warning" results["issues"].append(f"Espace disque faible: {disk.percent}% utilisé") except Exception as e: results["issues"].append(f"Erreur lors de la vérification de l'espace disque: {str(e)}") # Étape 4: Vérifier l'intégrité des fichiers de configuration self.progress_updated.emit(80, "Vérification des fichiers de configuration...") config_file = r"C:\AUTOLOOK\config.json" if os.path.exists(config_file): try: with open(config_file, 'r', encoding='utf-8', errors='ignore') as f: json.load(f) # Tester si le JSON est valide except json.JSONDecodeError: results["status"] = "warning" results["issues"].append(f"Le fichier de configuration est corrompu") # Tentative de réparation backup_config = os.path.join(self.backup_dir, "config.json.bak") if os.path.exists(backup_config): try: shutil.copy2(backup_config, config_file) results["repairs"].append(f"Configuration restaurée depuis la sauvegarde") except Exception as e: results["issues"].append(f"Échec de restauration de la configuration: {str(e)}") # Finalisation self.progress_updated.emit(100, "Diagnostic terminé.") self.diagnostic_complete.emit(results) def calculate_checksum(self, file_path): """Calcule un checksum SHA256 pour un fichier""" try: sha256_hash = hashlib.sha256() with open(file_path, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() except Exception as e: logger.error(f"Erreur lors du calcul du checksum pour {file_path}: {str(e)}") return None class SystemRepair(QObject): """ Classe pour réparer automatiquement les problèmes détectés par l'autotest. """ progress_updated = pyqtSignal(int, str) repair_complete = pyqtSignal(dict) def __init__(self, issues, autolook_dir=r"C:\AUTOLOOK"): super().__init__() self.issues = issues self.autolook_dir = autolook_dir self.repair_results = { "success": True, "repaired": [], "failed": [], "details": {} } def start(self): """Lance les réparations dans un thread séparé.""" repair_thread = threading.Thread(target=self._run_repairs) repair_thread.daemon = True repair_thread.start() def _run_repairs(self): """Exécute toutes les réparations nécessaires.""" try: self.progress_updated.emit(10, "Analyse des problèmes...") time.sleep(0.5) total_issues = len(self.issues) if total_issues == 0: self.progress_updated.emit(100, "Aucune réparation nécessaire") self.repair_complete.emit(self.repair_results) return # Analyser et catégoriser les problèmes repairs_needed = self._analyze_issues() self.progress_updated.emit(20, f"Réparations identifiées: {len(repairs_needed)}") time.sleep(0.5) # Exécuter chaque réparation progress_step = 70 // max(len(repairs_needed), 1) current_progress = 20 for i, repair_action in enumerate(repairs_needed): current_progress += progress_step self.progress_updated.emit(current_progress, f"Réparation: {repair_action['description']}") try: success = self._execute_repair(repair_action) if success: self.repair_results["repaired"].append(repair_action["description"]) else: self.repair_results["failed"].append(repair_action["description"]) self.repair_results["success"] = False except Exception as e: self.repair_results["failed"].append(f"{repair_action['description']}: {str(e)}") self.repair_results["success"] = False time.sleep(0.3) # Pause pour la visibilité self.progress_updated.emit(95, "Finalisation des réparations...") time.sleep(0.5) # Résumé final repaired_count = len(self.repair_results["repaired"]) failed_count = len(self.repair_results["failed"]) if failed_count == 0: final_message = f"Toutes les réparations terminées ({repaired_count} réussies)" else: final_message = f"Réparations terminées: {repaired_count} réussies, {failed_count} échouées" self.progress_updated.emit(100, final_message) self.repair_complete.emit(self.repair_results) except Exception as e: self.repair_results["success"] = False self.repair_results["failed"].append(f"Erreur critique: {str(e)}") self.progress_updated.emit(100, f"Erreur: {str(e)}") self.repair_complete.emit(self.repair_results) def _analyze_issues(self): """Analyse les problèmes et détermine les actions de réparation.""" repairs = [] for issue in self.issues: issue_lower = issue.lower() # Problèmes de configuration if "configuration" in issue_lower or "config" in issue_lower: repairs.append({ "type": "config", "description": "Créer/réparer le fichier de configuration", "action": "fix_configuration" }) # Problèmes de permissions elif "permission" in issue_lower or "accès" in issue_lower: repairs.append({ "type": "permissions", "description": "Corriger les permissions des fichiers", "action": "fix_permissions" }) # Problèmes de fichiers manquants elif "fichier" in issue_lower and ("manquant" in issue_lower or "introuvable" in issue_lower): repairs.append({ "type": "missing_files", "description": "Créer les fichiers manquants", "action": "create_missing_files" }) # Problèmes d'encodage elif "encodage" in issue_lower or "charmap" in issue_lower or "utf" in issue_lower: repairs.append({ "type": "encoding", "description": "Corriger les problèmes d'encodage", "action": "fix_encoding" }) # Problèmes d'espace disque elif "espace" in issue_lower and "disque" in issue_lower: repairs.append({ "type": "disk_space", "description": "Nettoyer l'espace disque", "action": "clean_disk_space" }) # Problèmes de dépendances elif "dépendance" in issue_lower or "module" in issue_lower: repairs.append({ "type": "dependencies", "description": "Installer les dépendances manquantes", "action": "install_dependencies" }) # Problèmes génériques else: repairs.append({ "type": "generic", "description": f"Réparation générique: {issue[:50]}...", "action": "generic_repair" }) # Supprimer les doublons unique_repairs = [] seen_actions = set() for repair in repairs: if repair["action"] not in seen_actions: unique_repairs.append(repair) seen_actions.add(repair["action"]) return unique_repairs def _execute_repair(self, repair_action): """Exécute une action de réparation spécifique.""" action = repair_action["action"] try: if action == "fix_configuration": return self._fix_configuration() elif action == "fix_permissions": return self._fix_permissions() elif action == "create_missing_files": return self._create_missing_files() elif action == "fix_encoding": return self._fix_encoding() elif action == "clean_disk_space": return self._clean_disk_space() elif action == "install_dependencies": return self._install_dependencies() elif action == "generic_repair": return self._generic_repair() else: return False except Exception as e: logger.error(f"Erreur lors de la réparation {action}: {str(e)}") return False def _fix_configuration(self): """Répare ou crée le fichier de configuration.""" try: config_file = os.path.join(self.autolook_dir, "config.json") # Configuration par défaut complète default_config = { "app_info": { "name": "AutoLook", "version": "1.0.0", "created": datetime.now().isoformat(), "last_updated": datetime.now().isoformat() }, "outlook": { "auto_connect": True, "timeout_seconds": 30, "retry_count": 3, "security_level": "normal" }, "general": { "debug_mode": False, "auto_start": False, "language": "fr", "theme": "dark" }, "logging": { "enabled": True, "level": "INFO", "max_file_size_mb": 10, "backup_count": 5 }, "automation": { "enabled": True, "interval_minutes": 5, "retry_count": 3 }, "security": { "encrypt_passwords": True, "secure_storage": True } } # Si le fichier existe, fusionner avec la config existante if os.path.exists(config_file): try: with open(config_file, 'r', encoding='utf-8') as f: existing_config = json.load(f) # Fusionner les configurations merged_config = self._merge_configs(default_config, existing_config) config_to_save = merged_config except: config_to_save = default_config else: config_to_save = default_config # Créer le dossier si nécessaire os.makedirs(os.path.dirname(config_file), exist_ok=True) # Sauvegarder la configuration with open(config_file, 'w', encoding='utf-8') as f: json.dump(config_to_save, f, indent=4, ensure_ascii=False) return True except Exception as e: logger.error(f"Erreur lors de la réparation de la configuration: {str(e)}") return False def _merge_configs(self, default, existing): """Fusionne la configuration par défaut avec l'existante.""" merged = default.copy() for key, value in existing.items(): if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): merged[key].update(value) else: merged[key] = value return merged def _fix_permissions(self): """Corrige les permissions des fichiers critiques.""" try: critical_files = [ os.path.join(self.autolook_dir, "main.py"), os.path.join(self.autolook_dir, "config.json"), os.path.join(self.autolook_dir, "modules") ] for file_path in critical_files: if os.path.exists(file_path): try: # Tenter de corriger les permissions en recréant un fichier test if os.path.isfile(file_path): test_file = file_path + ".tmp" with open(test_file, 'w', encoding='utf-8') as f: f.write("test") os.remove(test_file) except: continue return True except Exception as e: logger.error(f"Erreur lors de la correction des permissions: {str(e)}") return False def _create_missing_files(self): """Crée les fichiers manquants essentiels.""" try: # Créer les dossiers nécessaires essential_dirs = [ os.path.join(self.autolook_dir, "logs"), os.path.join(self.autolook_dir, "system_tests"), os.path.join(self.autolook_dir, "system_tests", "reports"), os.path.join(self.autolook_dir, "system_tests", "backups") ] for dir_path in essential_dirs: os.makedirs(dir_path, exist_ok=True) # Créer un fichier de log vide si nécessaire log_file = os.path.join(self.autolook_dir, "logs", "autolook.log") if not os.path.exists(log_file): with open(log_file, 'w', encoding='utf-8') as f: f.write(f"# AutoLook Log File - Created {datetime.now().isoformat()}\n") return True except Exception as e: logger.error(f"Erreur lors de la création des fichiers: {str(e)}") return False def _fix_encoding(self): """Corrige les problèmes d'encodage des fichiers.""" try: critical_files = [ os.path.join(self.autolook_dir, "main.py"), os.path.join(self.autolook_dir, "config.json") ] for file_path in critical_files: if os.path.exists(file_path): try: # Lire avec différents encodages et convertir en UTF-8 content = None for encoding in ['utf-8', 'cp1252', 'latin1']: try: with open(file_path, 'r', encoding=encoding) as f: content = f.read() break except: continue if content: # Créer une sauvegarde backup_path = file_path + f".backup_{int(time.time())}" with open(backup_path, 'w', encoding='utf-8') as f: f.write(content) except: continue return True except Exception as e: logger.error(f"Erreur lors de la correction d'encodage: {str(e)}") return False def _clean_disk_space(self): """Nettoie l'espace disque en supprimant les fichiers temporaires.""" try: temp_dirs = [ os.path.join(self.autolook_dir, "temp"), os.path.join(self.autolook_dir, "tmp"), os.path.join(self.autolook_dir, "system_tests", "temp") ] cleaned_files = 0 for temp_dir in temp_dirs: if os.path.exists(temp_dir): for file in os.listdir(temp_dir): try: file_path = os.path.join(temp_dir, file) if os.path.isfile(file_path): os.remove(file_path) cleaned_files += 1 except: continue # Nettoyer les anciens rapports (garder seulement les 10 derniers) reports_dir = os.path.join(self.autolook_dir, "system_tests", "reports") if os.path.exists(reports_dir): reports = [f for f in os.listdir(reports_dir) if f.endswith('.html')] reports.sort() for old_report in reports[:-10]: # Garder les 10 derniers try: os.remove(os.path.join(reports_dir, old_report)) cleaned_files += 1 except: continue return cleaned_files > 0 except Exception as e: logger.error(f"Erreur lors du nettoyage: {str(e)}") return False def _install_dependencies(self): """Simule l'installation des dépendances manquantes.""" # Note: Dans un vrai environnement, vous pourriez utiliser subprocess pour pip install try: # Pour le moment, on vérifie juste que les dépendances principales sont présentes required_modules = ['win32com', 'PyQt5', 'psutil', 'requests'] missing = [] for module in required_modules: try: __import__(module) except ImportError: missing.append(module) # Dans un environnement réel, on pourrait installer avec: # subprocess.run([sys.executable, '-m', 'pip', 'install', module]) return len(missing) == 0 except Exception as e: logger.error(f"Erreur lors de la vérification des dépendances: {str(e)}") return False def _generic_repair(self): """Réparation générique pour les problèmes non spécifiques.""" try: # Vérifier l'intégrité générale success = True # Vérifier que les dossiers principaux existent if not os.path.exists(self.autolook_dir): os.makedirs(self.autolook_dir, exist_ok=True) # Créer un rapport de l'état du système status_file = os.path.join(self.autolook_dir, "system_status.txt") with open(status_file, 'w', encoding='utf-8') as f: f.write(f"Réparation générique effectuée: {datetime.now().isoformat()}\n") f.write(f"Dossier AutoLook: {self.autolook_dir}\n") f.write(f"Statut: OK\n") return success except Exception as e: logger.error(f"Erreur lors de la réparation générique: {str(e)}") return False class BackupManager: """Gère les sauvegardes des fichiers critiques""" def __init__(self, critical_files=None, backup_dir=None): self.critical_files = critical_files or [ r"C:\AUTOLOOK\main.py", r"C:\AUTOLOOK\modules\module_outlook_2021.py", r"C:\AUTOLOOK\modules\ha2.py", r"C:\AUTOLOOK\config.json", r"C:\AUTOLOOK\miniview.json" # Ajout du fichier miniview.json ] self.backup_dir = backup_dir or r"C:\AUTOLOOK\backup" self.backup_directories = [ r"C:\AUTOLOOK\profiles" # Ajout du répertoire profiles ] os.makedirs(self.backup_dir, exist_ok=True) def create_backup(self): """Crée une sauvegarde de tous les fichiers critiques et répertoires""" results = { "success": True, "backups": [], "failed": [] } # Sauvegarde des fichiers individuels for file_path in self.critical_files: if os.path.exists(file_path): try: backup_path = os.path.join(self.backup_dir, os.path.basename(file_path) + ".bak") shutil.copy2(file_path, backup_path) # Créer également une version horodatée timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") dated_backup = os.path.join(self.backup_dir, f"{os.path.basename(file_path)}_{timestamp}.bak") shutil.copy2(file_path, dated_backup) results["backups"].append(f"Sauvegarde créée: {backup_path}") except Exception as e: results["failed"].append(f"Échec de sauvegarde de {file_path}: {str(e)}") results["success"] = False else: results["failed"].append(f"Fichier introuvable: {file_path}") results["success"] = False # Sauvegarde des répertoires complets for dir_path in self.backup_directories: if os.path.exists(dir_path) and os.path.isdir(dir_path): try: dir_name = os.path.basename(dir_path) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_dir_name = f"{dir_name}_{timestamp}" backup_dir_path = os.path.join(self.backup_dir, backup_dir_name) # Utiliser shutil.copytree pour copier tout le répertoire shutil.copytree(dir_path, backup_dir_path) results["backups"].append(f"Répertoire sauvegardé: {dir_path} -> {backup_dir_path}") except Exception as e: results["failed"].append(f"Échec de sauvegarde du répertoire {dir_path}: {str(e)}") results["success"] = False else: results["failed"].append(f"Répertoire introuvable: {dir_path}") results["success"] = False # Créer un fichier manifest try: manifest = { "timestamp": datetime.now().isoformat(), "files": [{"path": f, "backup": os.path.join(self.backup_dir, os.path.basename(f) + ".bak")} for f in self.critical_files if os.path.exists(f)], "directories": [{"path": d, "backup": os.path.join(self.backup_dir, f"{os.path.basename(d)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}")} for d in self.backup_directories if os.path.exists(d)] } with open(os.path.join(self.backup_dir, "manifest.json"), 'w') as f: json.dump(manifest, f, indent=4) except Exception as e: logger.error(f"Erreur lors de la création du manifeste: {str(e)}") return results def list_backups(self): """Liste toutes les sauvegardes disponibles (fichiers et répertoires)""" backups = [] if os.path.exists(self.backup_dir): # Lister les fichiers de sauvegarde for file in os.listdir(self.backup_dir): file_path = os.path.join(self.backup_dir, file) # Traiter les fichiers .bak if file.endswith(".bak") and os.path.isfile(file_path): timestamp = datetime.fromtimestamp(os.path.getmtime(file_path)) backups.append({ "file": file, "path": file_path, "timestamp": timestamp.isoformat(), "size": os.path.getsize(file_path), "type": "file" }) # Traiter les répertoires de sauvegarde (répertoires contenant "profiles_") elif os.path.isdir(file_path) and "profiles_" in file: timestamp = datetime.fromtimestamp(os.path.getmtime(file_path)) # Calculer la taille totale du répertoire total_size = 0 for dirpath, dirnames, filenames in os.walk(file_path): for f in filenames: fp = os.path.join(dirpath, f) total_size += os.path.getsize(fp) backups.append({ "file": file, "path": file_path, "timestamp": timestamp.isoformat(), "size": total_size, "type": "directory" }) return sorted(backups, key=lambda x: x["timestamp"], reverse=True) def restore_backup(self, backup_file): """Restaure un fichier ou un répertoire spécifique depuis sa sauvegarde""" try: backup_path = os.path.join(self.backup_dir, backup_file) if not os.path.exists(backup_path): return {"success": False, "error": f"Sauvegarde introuvable: {backup_file}"} # Vérifier si c'est un répertoire (sauvegarde de profiles) if os.path.isdir(backup_path) and "profiles_" in backup_file: target_dir = r"C:\AUTOLOOK\profiles" # Faire une sauvegarde de l'état actuel avant la restauration if os.path.exists(target_dir): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") prerestore_backup = os.path.join(self.backup_dir, f"profiles_prerestore_{timestamp}") shutil.copytree(target_dir, prerestore_backup) # Vider le répertoire cible shutil.rmtree(target_dir) os.makedirs(target_dir, exist_ok=True) # Copier tous les fichiers du répertoire de sauvegarde vers le répertoire cible for item in os.listdir(backup_path): s = os.path.join(backup_path, item) d = os.path.join(target_dir, item) if os.path.isdir(s): shutil.copytree(s, d) else: shutil.copy2(s, d) return {"success": True, "message": f"Répertoire profiles restauré depuis {backup_file}"} # Cas d'un fichier else: # Déterminer le fichier de destination for critical_file in self.critical_files: if backup_file.startswith(os.path.basename(critical_file)): # Faire une sauvegarde de l'état actuel avant la restauration current_backup = os.path.join(self.backup_dir, f"{os.path.basename(critical_file)}_prerestore_{datetime.now().strftime('%Y%m%d_%H%M%S')}.bak") if os.path.exists(critical_file): shutil.copy2(critical_file, current_backup) # Restaurer depuis la sauvegarde shutil.copy2(backup_path, critical_file) return {"success": True, "message": f"Fichier restauré: {critical_file}"} return {"success": False, "error": "Fichier ou répertoire source introuvable pour cette sauvegarde"} except Exception as e: logger.error(f"Erreur lors de la restauration depuis {backup_file}: {str(e)}") return {"success": False, "error": str(e)} class SystemTaskMonitor: """ Classe pour surveiller et gérer les tâches système et la santé de l'application """ def __init__(self, main_window): self.main_window = main_window self.backup_manager = BackupManager() # Catégorisation des processus avec codage couleur self.system_processes = ["winlogon.exe", "services.exe", "svchost.exe", "csrss.exe"] # Rouge self.sensitive_processes = ["Outlook.exe", "QtWebEngineProcess.exe", "python.exe"] # Orange self.normal_processes = [] # Vert (déterminé dynamiquement) self.blocked_processes = [] # Liste des processus bloqués self.dashboard_open = False self.dashboard_dialog = None # Créer le bouton de tableau de bord dans l'interface self.dashboard_button = QPushButton("🔍 Tableau de bord système") self.dashboard_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; margin: 2px; font-size: 12px; } QPushButton:hover { background-color: #2980b9; color: white; } """) self.dashboard_button.clicked.connect(self.open_dashboard) # Ajouter le bouton à l'interface principale if hasattr(main_window, 'priority_group') and isinstance(main_window.priority_group, QGroupBox): # Trouver le layout du groupe de priorité priority_layout = main_window.priority_group.layout() if priority_layout: # Insérer le bouton avant le bouton de forçage de priorité index = 0 for i in range(priority_layout.count()): if priority_layout.itemAt(i).widget() == main_window.force_low_priority_button: index = i break priority_layout.insertWidget(index, self.dashboard_button) def open_dashboard(self): """Ouvre le tableau de bord système""" if self.dashboard_open and self.dashboard_dialog: self.dashboard_dialog.activateWindow() return self.dashboard_dialog = SystemTaskDashboard(self.main_window, self) self.dashboard_dialog.closed.connect(self.on_dashboard_closed) self.dashboard_open = True self.dashboard_dialog.show() def on_dashboard_closed(self): """Appelé lorsque le tableau de bord est fermé""" self.dashboard_open = False self.dashboard_dialog = None def get_system_tasks(self, show_all=False): """ Récupère la liste des tâches système avec des optimisations de performance """ tasks = [] try: # Utiliser une méthode plus efficace pour itérer sur les processus # Récupérer les infos en une seule fois pour tous les processus all_processes = list( psutil.process_iter(['pid', 'name', 'username', 'cpu_percent', 'memory_percent', 'status'])) # Pré-calculer la CPU pour tous les processus en une seule fois (plus efficace) psutil.cpu_percent(interval=0.1, percpu=True) for proc in all_processes: if proc.pid == 0: continue # ignorer PID 0 explicitement try: process_info = proc.info # Déterminer la catégorie du processus (pour le code couleur) category = "normal" # Vert par défaut if process_info['name'] in self.system_processes: category = "system" # Rouge elif process_info['name'] in self.sensitive_processes: category = "sensitive" # Orange # Vérifier si c'est un processus AutoLook is_autolook = False if process_info['name'] == "python.exe": try: cmd_line = proc.cmdline() is_autolook = any("autolook" in arg.lower() for arg in cmd_line) if is_autolook: category = "sensitive" # Autolook en orange except: pass # Vérifier si le processus est bloqué is_blocked = proc.pid in self.blocked_processes # Ajouter le processus à la liste si: # - show_all est activé, ou # - la catégorie n'est pas "normal", ou # - c'est un processus AutoLook, ou # - il utilise > 2% de CPU (seuil réduit pour plus de visibilité) # - il est bloqué if (show_all or category != "normal" or is_autolook or process_info['cpu_percent'] > 2 or is_blocked): # Récupérer la priorité du processus try: priority = proc.nice() except: priority = "N/A" # Ajouter aux tâches tasks.append({ 'pid': process_info['pid'], 'name': process_info['name'], 'username': process_info['username'] or "N/A", 'cpu_percent': process_info['cpu_percent'], 'memory_percent': process_info['memory_percent'], 'status': process_info['status'], 'priority': priority, 'category': category, 'is_autolook': is_autolook, 'is_blocked': is_blocked }) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass # Ignorer silencieusement les processus inaccessibles except Exception as e: logger.error(f"Erreur globale lors de la récupération des tâches: {str(e)}") # Tri optimisé return sorted(tasks, key=lambda x: x['cpu_percent'], reverse=True) def block_process(self, pid): """Bloque un processus en l'ajoutant à la liste des processus bloqués""" try: if pid not in self.blocked_processes: self.blocked_processes.append(pid) return {"success": True, "message": f"Processus {pid} bloqué"} else: return {"success": False, "error": "Processus déjà bloqué"} except Exception as e: logger.error(f"Erreur lors du blocage du processus {pid}: {str(e)}") return {"success": False, "error": str(e)} def unblock_process(self, pid): """Débloque un processus en le retirant de la liste des processus bloqués""" try: if pid in self.blocked_processes: self.blocked_processes.remove(pid) return {"success": True, "message": f"Processus {pid} débloqué"} else: return {"success": False, "error": "Processus non bloqué"} except Exception as e: logger.error(f"Erreur lors du déblocage du processus {pid}: {str(e)}") return {"success": False, "error": str(e)} def get_system_info(self): """Récupère les informations système actuelles""" try: # Récupération optimisée des ressources système cpu_percent = psutil.cpu_percent(interval=0.1) memory = psutil.virtual_memory() disk = psutil.disk_usage('/') return { 'cpu_percent': cpu_percent, 'memory_percent': memory.percent, 'disk_percent': disk.percent, 'memory_total': memory.total, 'memory_available': memory.available, 'disk_total': disk.total, 'disk_free': disk.free } except Exception as e: logger.error(f"Erreur lors de la récupération des infos système: {str(e)}") return { 'cpu_percent': 0, 'memory_percent': 0, 'disk_percent': 0 } def run_diagnostic(self): """Lance un diagnostic système complet""" diagnostic_thread = SystemDiagnostic() return diagnostic_thread def repair_system(self, issues): """Répare les problèmes identifiés lors du diagnostic""" repair_thread = SystemRepair(issues) return repair_thread def create_backup(self): """Crée une sauvegarde des fichiers critiques""" return self.backup_manager.create_backup() def list_backups(self): """Liste les sauvegardes disponibles""" return self.backup_manager.list_backups() def restore_backup(self, backup_file): """Restaure une sauvegarde spécifique""" return self.backup_manager.restore_backup(backup_file) def change_process_priority(self): """Ouvre un dialogue pour changer la priorité du processus sélectionné""" try: current_row = self.processes_table.currentRow() if current_row < 0: return pid_item = self.processes_table.item(current_row, 0) name_item = self.processes_table.item(current_row, 1) if not pid_item or not name_item: return pid = pid_item.data(Qt.UserRole) name = name_item.text() # Dialogue de sélection de priorité dialog = ProcessPriorityDialog(self, pid, name) if dialog.exec_() == QDialog.Accepted: new_priority = dialog.get_selected_priority() self.set_process_priority(pid, name, new_priority) except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors du changement de priorité: {str(e)}") def set_process_priority(self, pid, name, priority): """Définit la priorité d'un processus""" try: import psutil # Mapping des priorités priority_map = { "realtime": psutil.REALTIME_PRIORITY_CLASS if hasattr(psutil, 'REALTIME_PRIORITY_CLASS') else psutil.HIGH_PRIORITY_CLASS, "high": psutil.HIGH_PRIORITY_CLASS if hasattr(psutil, 'HIGH_PRIORITY_CLASS') else 'high', "above_normal": psutil.ABOVE_NORMAL_PRIORITY_CLASS if hasattr(psutil, 'ABOVE_NORMAL_PRIORITY_CLASS') else 'above_normal', "normal": psutil.NORMAL_PRIORITY_CLASS if hasattr(psutil, 'NORMAL_PRIORITY_CLASS') else 'normal', "below_normal": psutil.BELOW_NORMAL_PRIORITY_CLASS if hasattr(psutil, 'BELOW_NORMAL_PRIORITY_CLASS') else 'below_normal', "idle": psutil.IDLE_PRIORITY_CLASS if hasattr(psutil, 'IDLE_PRIORITY_CLASS') else 'idle' } try: proc = psutil.Process(pid) # Sur Windows, utiliser les classes de priorité if hasattr(proc, 'nice'): if priority in priority_map: proc.nice(priority_map[priority]) QMessageBox.information(self, "Succès", f"Priorité du processus {name} (PID: {pid}) changée vers: {priority}") # Actualiser la table self.refresh_processes() else: QMessageBox.warning(self, "Erreur", f"Priorité '{priority}' non supportée sur ce système") else: QMessageBox.warning(self, "Erreur", "Changement de priorité non supporté sur ce système") except psutil.NoSuchProcess: QMessageBox.information(self, "Information", "Le processus n'existe plus.") self.refresh_processes() except psutil.AccessDenied: QMessageBox.critical(self, "Erreur",f"Accès refusé pour changer la priorité du processus {name}.\n" f"Vous n'avez pas les privilèges nécessaires.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors du changement de priorité: {str(e)}") def show_process_info(self, pid, name): """Affiche les informations détaillées d'un processus""" try: proc = psutil.Process(pid) # Collecter les informations info = f""" 🔍 INFORMATIONS DÉTAILLÉES DU PROCESSUS 📋 Nom: {name} 🆔 PID: {pid} 👤 Utilisateur: {proc.username() if hasattr(proc, 'username') else 'N/A'} 📊 État: {proc.status()} 💾 Mémoire utilisée: {proc.memory_info().rss / 1024 / 1024:.1f} MB 💾 Mémoire virtuelle: {proc.memory_info().vms / 1024 / 1024:.1f} MB 🖥️ Utilisation CPU: {proc.cpu_percent()}% ⏱️ Temps de création: {datetime.fromtimestamp(proc.create_time()).strftime('%Y-%m-%d %H:%M:%S')} 📁 Répertoire de travail: {proc.cwd() if hasattr(proc, 'cwd') else 'N/A'} 💻 Ligne de commande: {' '.join(proc.cmdline()) if proc.cmdline() else 'N/A'} """ # Ajouter les informations de priorité si disponibles try: if hasattr(proc, 'nice'): priority_value = proc.nice() priority_names = { 32: "Basse (Idle)", 16384: "En-dessous de la normale", 32768: "Normale", 32832: "Au-dessus de la normale", 128: "Haute", 256: "Temps réel" } priority_name = priority_names.get(priority_value, f"Personnalisée ({priority_value})") info += f"⚖️ Priorité: {priority_name}\n" except: pass # Créer le dialogue d'information dialog = QDialog(self) dialog.setWindowTitle(f"Informations - {name}") dialog.setMinimumSize(600, 400) dialog.setStyleSheet(self.parent().styleSheet() if self.parent() else "") layout = QVBoxLayout(dialog) text_edit = QTextEdit() text_edit.setPlainText(info.strip()) text_edit.setReadOnly(True) text_edit.setStyleSheet(""" QTextEdit { background-color: #2a2a2a; color: white; border: 1px solid #404040; border-radius: 4px; padding: 10px; font-family: Consolas, monospace; font-size: 11px; } """) layout.addWidget(text_edit) close_button = QPushButton("Fermer") close_button.clicked.connect(dialog.close) layout.addWidget(close_button) dialog.exec_() except psutil.NoSuchProcess: QMessageBox.information(self, "Information", "Le processus n'existe plus.") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la récupération des informations: {str(e)}") def refresh_processes(self): """Actualise manuellement la liste des processus""" try: self.update_processes() # Feedback visuel self.processes_table.setStyleSheet(self.processes_table.styleSheet() + """ QTableWidget { border: 2px solid #27ae60; } """) QTimer.singleShot(500, lambda: self.processes_table.setStyleSheet(""" QTableWidget { background-color: #2a2a2a; alternate-background-color: #333333; selection-background-color: #0078d4; selection-color: white; gridline-color: #404040; color: white; border: 1px solid #404040; border-radius: 4px; } QTableWidget::item { padding: 8px; border: none; } QTableWidget::item:selected { background-color: #0078d4; color: white; } QHeaderView::section { background-color: #404040; color: white; border: 1px solid #606060; padding: 8px; font-weight: bold; } """)) except Exception as e: print(f"Erreur actualisation processus: {e}") def update_processes_table(self, processes): """Met à jour la table des processus avec une liste donnée - VERSION AMÉLIORÉE""" try: # Limiter à 100 processus pour éviter les lags processes = processes[:100] self.processes_table.setRowCount(len(processes)) for i, proc in enumerate(processes): # PID pid_item = QTableWidgetItem(str(proc.get('pid', 'N/A'))) pid_item.setData(Qt.UserRole, proc.get('pid')) # Stocker le PID pid_item.setTextAlignment(Qt.AlignCenter) self.processes_table.setItem(i, 0, pid_item) # Nom name = proc.get('name', 'N/A') name_item = QTableWidgetItem(name) name_item.setToolTip(f"Processus: {name}\nPID: {proc.get('pid', 'N/A')}") self.processes_table.setItem(i, 1, name_item) # CPU % cpu_percent = proc.get('cpu_percent', 0) cpu_item = QTableWidgetItem(f"{cpu_percent:.1f}%") cpu_item.setTextAlignment(Qt.AlignCenter) # Colorier selon l'utilisation CPU if cpu_percent > 80: cpu_item.setForeground(QColor("#e74c3c")) # Rouge elif cpu_percent > 50: cpu_item.setForeground(QColor("#f39c12")) # Orange elif cpu_percent > 20: cpu_item.setForeground(QColor("#f1c40f")) # Jaune else: cpu_item.setForeground(QColor("#2ecc71")) # Vert self.processes_table.setItem(i, 2, cpu_item) # Mémoire memory_info = proc.get('memory_info', {}) if hasattr(memory_info, 'rss'): memory_mb = memory_info.rss / 1024 / 1024 else: memory_mb = 0 memory_item = QTableWidgetItem(f"{memory_mb:.1f} MB") memory_item.setTextAlignment(Qt.AlignCenter) # Colorier selon l'utilisation mémoire if memory_mb > 1000: # > 1GB memory_item.setForeground(QColor("#e74c3c")) # Rouge elif memory_mb > 500: # > 500MB memory_item.setForeground(QColor("#f39c12")) # Orange elif memory_mb > 100: # > 100MB memory_item.setForeground(QColor("#f1c40f")) # Jaune else: memory_item.setForeground(QColor("#2ecc71")) # Vert self.processes_table.setItem(i, 3, memory_item) # Priorité try: pid = proc.get('pid') if pid: process = psutil.Process(pid) if hasattr(process, 'nice'): priority_value = process.nice() priority_names = { 32: "Basse", 16384: "< Normale", 32768: "Normale", 32832: "> Normale", 128: "Haute", 256: "Temps réel" } priority_text = priority_names.get(priority_value, "Personnalisée") else: priority_text = "N/A" else: priority_text = "N/A" except: priority_text = "N/A" priority_item = QTableWidgetItem(priority_text) priority_item.setTextAlignment(Qt.AlignCenter) self.processes_table.setItem(i, 4, priority_item) # État status = proc.get('status', 'N/A') status_item = QTableWidgetItem(status) status_item.setTextAlignment(Qt.AlignCenter) # Colorier selon l'état status_colors = { 'running': QColor("#2ecc71"), # Vert 'sleeping': QColor("#3498db"), # Bleu 'disk-sleep': QColor("#9b59b6"), # Violet 'stopped': QColor("#e74c3c"), # Rouge 'zombie': QColor("#95a5a6"), # Gris } if status.lower() in status_colors: status_item.setForeground(status_colors[status.lower()]) self.processes_table.setItem(i, 5, status_item) except Exception as e: print(f"Erreur mise à jour table processus: {e}") # GRAPHIQUES FLUIDES - OPTIMISATION def setup_monitoring_ui(self): """Configuration de l'interface - VERSION OPTIMISÉE POUR GRAPHIQUES FLUIDES""" layout = QVBoxLayout(self) layout.setSpacing(10) # Onglets tabs = QTabWidget() # Onglet Graphiques graphs_tab = self.create_graphs_tab() tabs.addTab(graphs_tab, "📈 Graphiques") # Onglet Processus processes_tab = self.create_processes_tab() tabs.addTab(processes_tab, "⚙️ Processus") # Onglet Système system_tab = self.create_system_tab() tabs.addTab(system_tab, "💻 Système") # Onglet Réseau network_tab = self.create_network_tab() tabs.addTab(network_tab, "🌐 Réseau") layout.addWidget(tabs) # Boutons de contrôle control_layout = QHBoxLayout() export_button = QPushButton("📊 Exporter données") export_button.clicked.connect(self.export_monitoring_data) refresh_button = QPushButton("🔄 Actualiser") refresh_button.clicked.connect(self.update_data) close_button = QPushButton("❌ Fermer") close_button.clicked.connect(self.close) control_layout.addWidget(export_button) control_layout.addWidget(refresh_button) control_layout.addStretch() control_layout.addWidget(close_button) layout.addLayout(control_layout) def update_data(self): """Met à jour toutes les données - VERSION OPTIMISÉE""" try: # Ne pas mettre à jour les processus si l'utilisateur interagit if self.user_is_filtering or self.user_selected_process: # Mettre à jour seulement les graphiques et infos système current_time = datetime.now() cpu_percent = psutil.cpu_percent() memory = psutil.virtual_memory() disk = psutil.disk_usage('/') self.cpu_history.append(cpu_percent) self.ram_history.append(memory.percent) self.disk_history.append(disk.percent) self.timestamps.append(current_time) if len(self.cpu_history) > 60: self.cpu_history.pop(0) self.ram_history.pop(0) self.disk_history.pop(0) self.timestamps.pop(0) self.update_graphs() self.update_system_info() self.update_network_info() return current_time = datetime.now() # Collecter les données cpu_percent = psutil.cpu_percent(interval=None) # Non-bloquant memory = psutil.virtual_memory() # Pour le disque, utiliser le cache pour éviter les blocages if not hasattr(self, '_disk_cache') or ( current_time - getattr(self, '_disk_cache_time', datetime.min)).seconds > 5: try: disk = psutil.disk_usage('/') self._disk_cache = disk.percent self._disk_cache_time = current_time except: if not hasattr(self, '_disk_cache'): self._disk_cache = 0 # Ajouter aux historiques self.cpu_history.append(cpu_percent) self.ram_history.append(memory.percent) self.disk_history.append(self._disk_cache) self.timestamps.append(current_time) # Garder seulement les 120 dernières valeurs (2 minutes) max_points = 120 if len(self.cpu_history) > max_points: self.cpu_history = self.cpu_history[-max_points:] self.ram_history = self.ram_history[-max_points:] self.disk_history = self.disk_history[-max_points:] self.timestamps = self.timestamps[-max_points:] # Mettre à jour les graphiques (seulement si l'onglet graphiques est visible) current_tab = self.parent().findChild(QTabWidget) if current_tab and current_tab.currentIndex() == 0: # Onglet graphiques self.update_graphs_optimized() # Mettre à jour les processus (seulement si l'onglet processus est visible) if current_tab and current_tab.currentIndex() == 1: # Onglet processus self.update_processes() # Mettre à jour les infos système moins fréquemment if not hasattr(self, '_system_update_counter'): self._system_update_counter = 0 self._system_update_counter += 1 if self._system_update_counter % 5 == 0: # Toutes les 5 secondes if current_tab and current_tab.currentIndex() == 2: # Onglet système self.update_system_info() if current_tab and current_tab.currentIndex() == 3: # Onglet réseau self.update_network_info() except Exception as e: print(f"Erreur mise à jour monitoring: {e}") def update_graphs_optimized(self): """Met à jour les graphiques de manière optimisée pour la fluidité""" try: if not hasattr(self, 'figure') or not hasattr(self, 'canvas'): return # Ne redessiner que si on a suffisamment de données if len(self.timestamps) < 2: return # Utiliser la mise en cache de matplotlib pour de meilleures performances if not hasattr(self, '_axes_initialized'): self.figure.clear() # Créer les axes une seule fois self.ax1 = self.figure.add_subplot(3, 1, 1) self.ax2 = self.figure.add_subplot(3, 1, 2) self.ax3 = self.figure.add_subplot(3, 1, 3) # Configuration initiale des axes for ax, title, color in zip([self.ax1, self.ax2, self.ax3], ['CPU %', 'RAM %', 'Disque %'], ['red', 'green', 'blue']): ax.set_ylim(0, 100) ax.set_facecolor('#2a2a2a') ax.grid(True, alpha=0.3, color='gray') ax.set_ylabel(title, color='white') ax.tick_params(colors='white') # Style des bordures for spine in ax.spines.values(): spine.set_color('white') spine.set_linewidth(0.5) # Créer les lignes vides line, = ax.plot([], [], color=color, linewidth=2, animated=True) setattr(self, f'line_{title.lower().replace(" ", "_").replace("%", "")}', line) self.figure.tight_layout() self._axes_initialized = True # Configuration pour l'animation self.canvas.draw() self.backgrounds = [self.canvas.copy_from_bbox(ax.bbox) for ax in [self.ax1, self.ax2, self.ax3]] # Conversion des timestamps pour matplotlib (plus efficace) if len(self.timestamps) > 1: time_nums = np.array([(t - self.timestamps[0]).total_seconds() for t in self.timestamps]) # Mise à jour des données des lignes self.line_cpu.set_data(time_nums, np.array(self.cpu_history)) self.line_ram.set_data(time_nums, np.array(self.ram_history)) self.line_disque.set_data(time_nums, np.array(self.disk_history)) # Ajuster les limites X dynamiquement if len(time_nums) > 0: x_min, x_max = time_nums[0], time_nums[-1] x_range = max(x_max - x_min, 10) # Minimum 10 secondes for ax in [self.ax1, self.ax2, self.ax3]: ax.set_xlim(x_min, x_min + x_range) # Utiliser l'animation blitting pour de meilleures performances try: for i, (ax, bg) in enumerate(zip([self.ax1, self.ax2, self.ax3], self.backgrounds)): self.canvas.restore_region(bg) ax.draw_artist(getattr(self, f'line_{"cpu" if i == 0 else "ram" if i == 1 else "disque"}')) self.canvas.blit(ax.bbox) except: # Fallback vers le rendu complet si l'animation échoue self.canvas.draw_idle() except Exception as e: print(f"Erreur mise à jour graphiques optimisés: {e}") # Fallback vers le rendu basique try: self.canvas.draw_idle() except: pass def stop_process(self, pid, force=False): """Arrête un processus""" try: process = psutil.Process(pid) process_name = process.name() # Vérification de double confirmation pour les processus sensibles if process_name in self.system_processes or process_name in self.sensitive_processes: # La confirmation sera gérée par l'interface pass if force: process.kill() else: process.terminate() return {"success": True, "message": f"Processus {process_name} (PID: {pid}) arrêté"} except Exception as e: logger.error(f"Erreur lors de l'arrêt du processus {pid}: {str(e)}") return {"success": False, "error": str(e)} class SystemAutoTest: """ Module d'autodiagnostic et de test pour l'application AutoLook. Permet d'exécuter des tests automatisés et de générer des rapports. """ def __init__(self, main_window=None): self.main_window = main_window self.autolook_dir = r"C:\AUTOLOOK" self.test_results = { "tests_run": 0, "tests_passed": 0, "tests_failed": 0, "start_time": datetime.now().isoformat(), "end_time": None, "results": [], "report_file": None } self.critical_files = [ r"C:\AUTOLOOK\main.py", r"C:\AUTOLOOK\modules\module_outlook_2021.py", r"C:\AUTOLOOK\modules\ha2.py" ] self.backup_dir = os.path.join(self.autolook_dir, "system_tests", "backups") os.makedirs(self.backup_dir, exist_ok=True) # Création du bouton de test self.test_button = QPushButton("🔬 Autotest système") self.test_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; margin: 2px; font-size: 12px; } QPushButton:hover { background-color: #2ecc71; color: white; } """) self.test_button.clicked.connect(self.show_test_dialog) def show_test_dialog(self): """Affiche le dialogue d'autotest - Version simplifiée et fonctionnelle""" try: dialog = QDialog(self.main_window) dialog.setWindowTitle("Autotest Système") dialog.setMinimumSize(700, 500) dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; color: white; } QLabel { color: white; font-size: 12px; } QProgressBar { border: 2px solid #2c3e50; border-radius: 5px; text-align: center; color: white; background-color: #34495e; font-weight: bold; height: 25px; } QProgressBar::chunk { background-color: #3498db; border-radius: 3px; } QPushButton { background-color: #2980b9; color: white; border: none; padding: 10px 16px; border-radius: 4px; font-weight: bold; font-size: 11px; min-width: 120px; } QPushButton:hover { background-color: #3498db; } QPushButton:disabled { background-color: #7f8c8d; color: #bdc3c7; } QTextEdit { background-color: #2c3e50; color: white; border: 1px solid #34495e; border-radius: 4px; font-family: 'Consolas', 'Courier New', monospace; font-size: 10px; } """) layout = QVBoxLayout(dialog) layout.setSpacing(10) # === EN-TÊTE === header_label = QLabel("🔬 Autotest Système MA2C_TA V1") header_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #3498db; margin-bottom: 10px;") layout.addWidget(header_label) desc_label = QLabel("Diagnostic complet de l'application et de ses interactions") desc_label.setStyleSheet("color: #bdc3c7; margin-bottom: 15px;") layout.addWidget(desc_label) # === STATUT ET PROGRESSION === self.status_label = QLabel("Prêt à lancer les tests") self.status_label.setStyleSheet("color: #ecf0f1; font-weight: bold;") layout.addWidget(self.status_label) self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) layout.addWidget(self.progress_bar) # === ZONE DE LOG === self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setMinimumHeight(300) layout.addWidget(self.log_area) # === BOUTONS === button_layout = QHBoxLayout() self.start_button = QPushButton("🚀 Lancer l'autotest complet") self.start_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; font-size: 12px; font-weight: bold; padding: 12px 20px; } QPushButton:hover { background-color: #2ecc71; } QPushButton:disabled { background-color: #7f8c8d; } """) self.repair_button = QPushButton("🔧 Réparer les problèmes") self.repair_button.setEnabled(False) self.repair_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; font-size: 12px; font-weight: bold; padding: 12px 20px; } QPushButton:hover { background-color: #c0392b; } QPushButton:disabled { background-color: #7f8c8d; } """) close_button = QPushButton("✅ Fermer") close_button.setStyleSheet(""" QPushButton { background-color: #34495e; color: white; font-size: 12px; font-weight: bold; padding: 12px 20px; } QPushButton:hover { background-color: #5d6d7e; } """) button_layout.addWidget(self.start_button) button_layout.addWidget(self.repair_button) button_layout.addStretch() button_layout.addWidget(close_button) layout.addLayout(button_layout) # === CONNEXIONS === self.start_button.clicked.connect(self.execute_autotest) self.repair_button.clicked.connect(self.execute_repair) close_button.clicked.connect(dialog.accept) # === VARIABLES D'INSTANCE === self.dialog = dialog self.is_testing = False # Afficher le dialogue dialog.exec_() except Exception as e: logger.error(f"Erreur dialogue autotest: {str(e)}") QMessageBox.critical(self.main_window, "Erreur", f"Erreur lors de l'affichage: {str(e)}") def execute_autotest(self): """Exécute l'autotest complet - Version simplifiée""" if self.is_testing: return try: self.is_testing = True self.start_button.setEnabled(False) self.repair_button.setEnabled(False) # Réinitialiser l'interface self.progress_bar.setValue(0) self.log_area.clear() self.status_label.setText("Initialisation...") self.log_area.append("🔬 Démarrage de l'autotest système...") self.log_area.append("=" * 60) # Initialiser les résultats self.test_results = { "start_time": datetime.now().isoformat(), "end_time": None, "tests_run": 0, "tests_passed": 0, "tests_failed": 0, "results": [], "report_file": None } # Liste des tests à exécuter tests_list = [ ("file_integrity", "Vérification de l'intégrité des fichiers", self.test_file_integrity), ("autolook_critical_files", "Fichiers critiques AutoLook", self.test_autolook_critical_files), ("outlook_access", "Vérification de l'accès à Outlook", self.test_outlook_access), ("web_access", "Vérification de l'accès au web", self.test_web_access), ("python_dependencies", "Vérification des dépendances Python", self.test_python_dependencies), ("disk_space", "Vérification de l'espace disque", self.test_disk_space), ("permissions", "Vérification des permissions", self.test_permissions), ("configuration", "Vérification de la configuration", self.test_configuration), ("autolook_config", "Configuration AutoLook spécifique", self.test_autolook_configuration_specific), ("autolook_startup", "Préparation au démarrage", self.test_autolook_startup_readiness), ("autolook_modules", "Intégrité modules AutoLook", self.test_autolook_modules), ("autolook_logs", "Analyse des logs", self.test_autolook_logs), ("outlook_version", "Compatibilité version Outlook", self.test_outlook_version_compatibility), ("outlook_security", "Paramètres sécurité Outlook", self.test_outlook_security_settings), ("outlook_performance", "Performance accès Outlook", self.test_outlook_performance), ("memory_usage", "Utilisation mémoire", self.test_process_memory_usage) ] total_tests = len(tests_list) # Démarrer les tests avec un timer self.current_test_index = 0 self.tests_list = tests_list self.total_tests = total_tests # Utiliser un QTimer pour exécuter les tests un par un self.test_timer = QTimer() self.test_timer.timeout.connect(self.execute_next_test) self.test_timer.start(100) # Démarrer dans 100ms except Exception as e: self.finalize_autotest_with_error(f"Erreur d'initialisation: {str(e)}") def execute_next_test(self): """Exécute le test suivant dans la liste""" try: if self.current_test_index >= len(self.tests_list): # Tous les tests sont terminés self.test_timer.stop() self.finalize_autotest() return # Récupérer le test actuel test_id, test_name, test_function = self.tests_list[self.current_test_index] # Calculer la progression (5% à 85% pour les tests) progress = 5 + int((self.current_test_index / self.total_tests) * 80) self.progress_bar.setValue(progress) self.status_label.setText(f"Test {self.current_test_index + 1}/{self.total_tests}: {test_name}") self.log_area.append(f"🔍 [{self.current_test_index + 1}/{self.total_tests}] {test_name}") QApplication.processEvents() # Exécuter le test try: self._run_test(test_id, test_name, test_function) # Afficher le résultat if self.test_results["results"]: last_result = self.test_results["results"][-1] status = last_result["status"].upper() if status == "PASSED": self.log_area.append(f" ✅ {status}") else: self.log_area.append(f" ❌ {status}") self.log_area.append(f" 📝 {last_result['details']}") except Exception as test_error: self.log_area.append(f" 💥 ERREUR: {str(test_error)}") self.log_area.append("") # Ligne vide # Passer au test suivant self.current_test_index += 1 except Exception as e: self.test_timer.stop() self.finalize_autotest_with_error(f"Erreur pendant les tests: {str(e)}") def finalize_autotest(self): """Finalise l'autotest avec génération du rapport""" try: # Générer le rapport self.progress_bar.setValue(90) self.status_label.setText("Génération du rapport...") self.log_area.append("📄 Génération du rapport en cours...") QApplication.processEvents() self.test_results["end_time"] = datetime.now().isoformat() try: report_file = self.generate_report() self.test_results["report_file"] = report_file self.log_area.append(f"✅ Rapport généré: {os.path.basename(report_file)}") except Exception as report_error: self.log_area.append(f"⚠️ Erreur génération rapport: {str(report_error)}") self.test_results["report_file"] = None # === FINALISATION À 100% GARANTIE === self.progress_bar.setValue(100) self.status_label.setText("Tests terminés avec succès") QApplication.processEvents() # Affichage des résultats finaux self.log_area.append("\n" + "=" * 60) self.log_area.append("✅ AUTOTEST TERMINÉ AVEC SUCCÈS") self.log_area.append("=" * 60) self.log_area.append(f"📊 Tests exécutés: {self.test_results['tests_run']}") self.log_area.append(f"✅ Tests réussis: {self.test_results['tests_passed']}") self.log_area.append(f"❌ Tests échoués: {self.test_results['tests_failed']}") if self.test_results.get('report_file'): self.log_area.append(f"📄 Rapport: {os.path.basename(self.test_results['report_file'])}") # Gérer les boutons if self.test_results['tests_failed'] > 0: self.repair_button.setEnabled(True) self.log_area.append("\n⚠️ Bouton 'Réparer' activé pour résoudre les problèmes détectés.") self.log_area.append("\n🏁 Autotest finalisé avec succès.") # Réactiver le bouton de démarrage self.start_button.setEnabled(True) self.is_testing = False except Exception as e: self.finalize_autotest_with_error(f"Erreur de finalisation: {str(e)}") def finalize_autotest_with_error(self, error_message): """Finalise l'autotest en cas d'erreur""" try: self.progress_bar.setValue(100) self.status_label.setText("Tests terminés avec erreurs") self.log_area.append("\n" + "=" * 60) self.log_area.append("❌ AUTOTEST TERMINÉ AVEC ERREURS") self.log_area.append("=" * 60) self.log_area.append(f"💥 Erreur: {error_message}") if hasattr(self, 'test_results') and self.test_results: self.log_area.append(f"📊 Tests exécutés: {self.test_results.get('tests_run', 0)}") self.log_area.append(f"✅ Tests réussis: {self.test_results.get('tests_passed', 0)}") self.log_area.append(f"❌ Tests échoués: {self.test_results.get('tests_failed', 0)}") self.log_area.append("\n🏁 Autotest interrompu.") # Réactiver les boutons self.start_button.setEnabled(True) self.is_testing = False # Arrêter le timer s'il est actif if hasattr(self, 'test_timer') and self.test_timer.isActive(): self.test_timer.stop() except Exception as final_error: logger.error(f"Erreur critique de finalisation: {str(final_error)}") # Fallback absolu self.progress_bar.setValue(100) self.status_label.setText("Erreur critique") self.start_button.setEnabled(True) self.is_testing = False def execute_repair(self): """Exécute les vraies réparations basées sur les problèmes détectés""" try: if not hasattr(self, 'test_results') or not self.test_results.get('results'): QMessageBox.warning(self.dialog, "Aucun test", "Veuillez d'abord exécuter les tests.") return self.repair_button.setEnabled(False) self.start_button.setEnabled(False) self.progress_bar.setValue(0) self.log_area.append("\n" + "=" * 60) self.log_area.append("🔧 DÉMARRAGE DES RÉPARATIONS RÉELLES") self.log_area.append("=" * 60) # Collecter les problèmes réels failed_tests = [test for test in self.test_results['results'] if test['status'] != 'passed'] if not failed_tests: self.log_area.append("ℹ️ Aucun problème à réparer détecté.") self.progress_bar.setValue(100) self.status_label.setText("Aucune réparation nécessaire") self.start_button.setEnabled(True) return self.log_area.append(f"📋 {len(failed_tests)} problème(s) détecté(s) - Réparations en cours...") repairs_successful = 0 repairs_failed = 0 # Effectuer les vraies réparations for i, test in enumerate(failed_tests): progress = int((i / len(failed_tests)) * 90) self.progress_bar.setValue(progress) self.status_label.setText(f"Réparation {i + 1}/{len(failed_tests)}: {test['name']}") self.log_area.append(f"🔧 [{i + 1}/{len(failed_tests)}] {test['name']}") QApplication.processEvents() # === VRAIES RÉPARATIONS SELON LE TYPE DE PROBLÈME === repair_success = False try: if test['id'] == 'file_integrity': repair_success = self.repair_file_integrity(test) elif test['id'] == 'configuration': repair_success = self.repair_configuration(test) elif test['id'] == 'permissions': repair_success = self.repair_permissions(test) elif test['id'] == 'disk_space': repair_success = self.repair_disk_space(test) elif test['id'] == 'python_dependencies': repair_success = self.repair_python_dependencies(test) elif test['id'] == 'autolook_config': repair_success = self.repair_autolook_config(test) elif test['id'] == 'autolook_critical_files': repair_success = self.repair_critical_files(test) else: # Réparation générique repair_success = self.repair_generic_issue(test) if repair_success: self.log_area.append(f" ✅ Réparation réussie") repairs_successful += 1 else: self.log_area.append(f" ❌ Réparation échouée") repairs_failed += 1 except Exception as repair_error: self.log_area.append(f" 💥 Erreur réparation: {str(repair_error)}") repairs_failed += 1 self.log_area.append("") # Finalisation self.progress_bar.setValue(100) self.status_label.setText("Réparations terminées") self.log_area.append("=" * 60) self.log_area.append("📊 RÉSULTATS DES RÉPARATIONS") self.log_area.append("=" * 60) self.log_area.append(f"✅ Réparations réussies: {repairs_successful}") self.log_area.append(f"❌ Réparations échouées: {repairs_failed}") if repairs_successful > 0: self.log_area.append("\n💡 RECOMMANDATIONS:") self.log_area.append("1. Relancez les tests pour vérifier les corrections") self.log_area.append("2. Redémarrez l'application si nécessaire") if repairs_failed > 0: self.log_area.append("3. Vérifiez manuellement les problèmes non résolus") # Réactiver les boutons self.start_button.setEnabled(True) if repairs_failed > 0: self.repair_button.setText("Relancer les réparations") self.repair_button.setEnabled(True) else: self.repair_button.setText("Réparations terminées") except Exception as e: logger.error(f"Erreur réparations: {str(e)}") self.log_area.append(f"❌ Erreur pendant les réparations: {str(e)}") self.start_button.setEnabled(True) self.repair_button.setEnabled(True) # === MÉTHODES DE RÉPARATION RÉELLES === def repair_file_integrity(self, test): """Répare les problèmes d'intégrité des fichiers""" try: self.log_area.append(" 🔍 Vérification et réparation des fichiers critiques...") # Recréer les fichiers manquants ou corrompus critical_files_templates = { "config.json": { "app_name": "AutoLook", "version": "1.0.0", "created": datetime.now().isoformat(), "settings": { "auto_start": False, "debug_mode": False } } } for filename, template in critical_files_templates.items(): file_path = os.path.join(self.autolook_dir, filename) if not os.path.exists(file_path): os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w', encoding='utf-8') as f: json.dump(template, f, indent=4, ensure_ascii=False) self.log_area.append(f" 📄 Fichier {filename} recréé") return True except Exception as e: self.log_area.append(f" ❌ Erreur réparation fichiers: {str(e)}") return False def repair_configuration(self, test): """Répare les problèmes de configuration""" try: self.log_area.append(" ⚙️ Réparation de la configuration...") config_file = os.path.join(self.autolook_dir, "config.json") # Créer une configuration par défaut si nécessaire if not os.path.exists(config_file): default_config = { "app_info": { "name": "AutoLook", "version": "1.0.0", "created": datetime.now().isoformat() }, "outlook": { "auto_connect": True, "timeout_seconds": 30, "retry_count": 3 }, "general": { "debug_mode": False, "auto_start": False, "language": "fr" } } os.makedirs(os.path.dirname(config_file), exist_ok=True) with open(config_file, 'w', encoding='utf-8') as f: json.dump(default_config, f, indent=4, ensure_ascii=False) self.log_area.append(" 📝 Configuration par défaut créée") return True # Réparer la configuration existante try: with open(config_file, 'r', encoding='utf-8') as f: config = json.load(f) # Vérifier et ajouter les sections manquantes if "app_info" not in config: config["app_info"] = {"name": "AutoLook", "version": "1.0.0"} if "outlook" not in config: config["outlook"] = {"auto_connect": True, "timeout_seconds": 30} # Sauvegarder la configuration réparée with open(config_file, 'w', encoding='utf-8') as f: json.dump(config, f, indent=4, ensure_ascii=False) self.log_area.append(" 🔧 Configuration réparée") return True except json.JSONDecodeError: # Recréer si le JSON est corrompu os.remove(config_file) return self.repair_configuration(test) # Récursion pour recréer except Exception as e: self.log_area.append(f" ❌ Erreur réparation config: {str(e)}") return False def repair_permissions(self, test): """Répare les problèmes de permissions""" try: self.log_area.append(" 🔐 Réparation des permissions...") # Réparer les permissions sur les dossiers critiques critical_dirs = [ self.autolook_dir, os.path.join(self.autolook_dir, "logs"), os.path.join(self.autolook_dir, "modules"), os.path.join(self.autolook_dir, "system_tests") ] for dir_path in critical_dirs: try: os.makedirs(dir_path, exist_ok=True) # Tester l'écriture test_file = os.path.join(dir_path, f"test_write_{int(time.time())}.tmp") with open(test_file, 'w', encoding='utf-8') as f: f.write("test") os.remove(test_file) self.log_area.append(f" ✅ Permissions OK: {os.path.basename(dir_path)}") except Exception: self.log_area.append(f" ⚠️ Permissions limitées: {os.path.basename(dir_path)}") return True except Exception as e: self.log_area.append(f" ❌ Erreur réparation permissions: {str(e)}") return False def repair_disk_space(self, test): """Nettoie l'espace disque""" try: self.log_area.append(" 🧹 Nettoyage de l'espace disque...") # Nettoyer les fichiers temporaires temp_dirs = [ os.path.join(self.autolook_dir, "logs"), os.path.join(self.autolook_dir, "debug"), os.path.join(self.autolook_dir, "modules", "__pycache__") ] total_cleaned = 0 for temp_dir in temp_dirs: if os.path.exists(temp_dir): try: for file in os.listdir(temp_dir): file_path = os.path.join(temp_dir, file) if os.path.isfile(file_path): # Supprimer les anciens fichiers (plus de 7 jours) if time.time() - os.path.getmtime(file_path) > 7 * 24 * 3600: size = os.path.getsize(file_path) os.remove(file_path) total_cleaned += size except Exception: continue if total_cleaned > 0: cleaned_mb = total_cleaned / (1024 * 1024) self.log_area.append(f" 🗑️ {cleaned_mb:.1f} MB nettoyés") else: self.log_area.append(" ℹ️ Aucun fichier à nettoyer") return True except Exception as e: self.log_area.append(f" ❌ Erreur nettoyage: {str(e)}") return False def repair_python_dependencies(self, test): """Tente de résoudre les problèmes de dépendances Python""" try: self.log_area.append(" 🐍 Vérification des dépendances Python...") # Obtenir la liste des modules manquants depuis les détails du test missing_deps = [] if 'missing_deps' in test.get('details', ''): # Extraire les noms des modules manquants (simplification) details = test['details'] if 'win32com' in details: missing_deps.append('pywin32') if 'PyQt5' in details: missing_deps.append('PyQt5') if missing_deps: self.log_area.append(f" 📦 Modules manquants détectés: {', '.join(missing_deps)}") self.log_area.append(" 💡 Recommandation: Installez manuellement avec 'pip install'") return False # Ne peut pas installer automatiquement else: self.log_area.append(" ✅ Toutes les dépendances sont disponibles") return True except Exception as e: self.log_area.append(f" ❌ Erreur vérification dépendances: {str(e)}") return False def repair_autolook_config(self, test): """Répare la configuration spécifique AutoLook""" try: self.log_area.append(" 🔧 Réparation configuration AutoLook...") # Utiliser la même logique que repair_configuration mais plus spécifique return self.repair_configuration(test) except Exception as e: self.log_area.append(f" ❌ Erreur config AutoLook: {str(e)}") return False def repair_critical_files(self, test): """Répare les fichiers critiques manquants""" try: self.log_area.append(" 📄 Réparation des fichiers critiques...") # Créer les dossiers et fichiers essentiels manquants essential_dirs = [ os.path.join(self.autolook_dir, "modules"), os.path.join(self.autolook_dir, "logs"), os.path.join(self.autolook_dir, "system_tests"), os.path.join(self.autolook_dir, "system_tests", "reports") ] for dir_path in essential_dirs: os.makedirs(dir_path, exist_ok=True) self.log_area.append(f" 📁 Dossier vérifié: {os.path.basename(dir_path)}") return True except Exception as e: self.log_area.append(f" ❌ Erreur fichiers critiques: {str(e)}") return False def repair_generic_issue(self, test): """Réparation générique pour les autres problèmes""" try: self.log_area.append(f" 🔧 Tentative de réparation générique...") self.log_area.append(f" 📝 Problème: {test['details']}") # Pour les problèmes non spécifiques, on ne peut que recommander self.log_area.append(" 💡 Intervention manuelle recommandée") return False except Exception as e: self.log_area.append(f" ❌ Erreur réparation générique: {str(e)}") return False def test_file_encodings(self): """ Teste les encodages de tous les fichiers critiques. """ results = { "success": True, "details": "Tous les fichiers ont des encodages compatibles", "encoding_issues": [], "file_encodings": {} } for file_path in self.critical_files: if os.path.exists(file_path): try: encoding = self._detect_file_encoding(file_path) results["file_encodings"][os.path.basename(file_path)] = encoding # Tenter de lire le fichier avec l'encodage détecté content, detected_encoding = self._safe_read_file(file_path) if content is None: results["success"] = False results["encoding_issues"].append({ "file": file_path, "issue": "Impossible de lire le fichier avec aucun encodage" }) elif encoding != 'utf-8': # Recommander la conversion en UTF-8 results["encoding_issues"].append({ "file": file_path, "issue": f"Fichier en {encoding}, recommandé: UTF-8" }) except Exception as e: results["success"] = False results["encoding_issues"].append({ "file": file_path, "issue": f"Erreur lors du test: {str(e)}" }) # Mettre à jour les détails if results["encoding_issues"]: if any("Impossible de lire" in issue["issue"] for issue in results["encoding_issues"]): results["success"] = False details = "Problèmes d'encodage détectés:\n" for issue in results["encoding_issues"]: details += f"- {os.path.basename(issue['file'])}: {issue['issue']}\n" results["details"] = details.strip() return results def test_system_performance(self): """ Teste les performances du système. """ results = { "success": True, "details": "Performances du système correctes", "performance_metrics": {}, "warnings": [] } try: import psutil import time # Test de la CPU cpu_percent = psutil.cpu_percent(interval=1) results["performance_metrics"]["cpu_usage"] = cpu_percent # Test de la mémoire memory = psutil.virtual_memory() results["performance_metrics"]["memory_percent"] = memory.percent results["performance_metrics"]["memory_available_gb"] = memory.available / (1024 ** 3) # Test de performance d'I/O simple start_time = time.time() test_file = os.path.join(self.autolook_dir, "temp_perf_test.tmp") # Écrire un fichier de test with open(test_file, 'w', encoding='utf-8') as f: f.write("Test de performance I/O\n" * 1000) # Lire le fichier de test with open(test_file, 'r', encoding='utf-8') as f: content = f.read() # Supprimer le fichier de test os.remove(test_file) io_time = time.time() - start_time results["performance_metrics"]["io_test_seconds"] = io_time # Vérifier les seuils de performance if cpu_percent > 80: results["warnings"].append(f"Utilisation CPU élevée: {cpu_percent}%") if memory.percent > 85: results["warnings"].append(f"Utilisation mémoire élevée: {memory.percent}%") if io_time > 2.0: results["warnings"].append(f"I/O lent: {io_time:.2f}s") # Définir le succès if cpu_percent > 90 or memory.percent > 95 or io_time > 5.0: results["success"] = False results["details"] = "Performances système dégradées" else: results["details"] = f"CPU: {cpu_percent}%, RAM: {memory.percent}%, I/O: {io_time:.2f}s" if results["warnings"]: results["details"] += f" - Avertissements: {len(results['warnings'])}" except Exception as e: results["success"] = False results["details"] = f"Erreur lors du test de performance: {str(e)}" return results def test_network_advanced(self): """ Teste la connectivité réseau de manière avancée. """ results = { "success": True, "details": "Connectivité réseau correcte", "network_tests": {}, "dns_tests": {}, "latency_tests": {} } try: import requests import socket import time # Test DNS dns_servers = ["8.8.8.8", "1.1.1.1"] for dns in dns_servers: try: start = time.time() socket.gethostbyname("www.google.com") latency = (time.time() - start) * 1000 results["dns_tests"][dns] = {"status": "OK", "latency_ms": latency} except Exception as e: results["dns_tests"][dns] = {"status": "Erreur", "error": str(e)} # Test de latence vers des serveurs test_hosts = [ ("www.google.com", 80), ("www.microsoft.com", 80), ("outlook.office365.com", 443) ] for host, port in test_hosts: try: start = time.time() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) result = sock.connect_ex((host, port)) latency = (time.time() - start) * 1000 sock.close() if result == 0: results["latency_tests"][host] = {"status": "OK", "latency_ms": latency} else: results["latency_tests"][host] = {"status": "Échec", "latency_ms": latency} except Exception as e: results["latency_tests"][host] = {"status": "Erreur", "error": str(e)} # Test de bande passante simple try: start = time.time() response = requests.get("https://httpbin.org/bytes/1024", timeout=10) download_time = time.time() - start if response.status_code == 200: speed_kbps = (1024 / download_time) / 1024 results["network_tests"]["bandwidth_test"] = { "status": "OK", "speed_kbps": speed_kbps, "time_seconds": download_time } else: results["network_tests"]["bandwidth_test"] = { "status": "Échec", "http_code": response.status_code } except Exception as e: results["network_tests"]["bandwidth_test"] = { "status": "Erreur", "error": str(e) } # Analyser les résultats failed_tests = 0 total_tests = len(results["dns_tests"]) + len(results["latency_tests"]) + len(results["network_tests"]) for test_group in [results["dns_tests"], results["latency_tests"], results["network_tests"]]: for test_name, test_result in test_group.items(): if test_result.get("status") != "OK": failed_tests += 1 if failed_tests > total_tests * 0.3: # Plus de 30% d'échecs results["success"] = False results["details"] = f"Problèmes réseau détectés: {failed_tests}/{total_tests} tests échoués" else: results["details"] = f"Réseau fonctionnel: {total_tests - failed_tests}/{total_tests} tests réussis" except Exception as e: results["success"] = False results["details"] = f"Erreur lors des tests réseau: {str(e)}" return results def _run_test(self, test_id, test_name, test_function): """ Exécute un test spécifique et enregistre le résultat. Version sécurisée avec gestion d'erreurs renforcée. """ logger.info(f"Exécution du test: {test_name}") self.test_results["tests_run"] += 1 start_time = datetime.now() try: # Exécuter le test dans un try-catch sécurisé result = test_function() # Vérifier que le résultat est valide if not isinstance(result, dict): raise ValueError("Le test doit retourner un dictionnaire") success = result.get("success", False) if success: self.test_results["tests_passed"] += 1 status = "passed" else: self.test_results["tests_failed"] += 1 status = "failed" duration = (datetime.now() - start_time).total_seconds() self.test_results["results"].append({ "id": test_id, "name": test_name, "status": status, "details": result.get("details", "Aucun détail disponible"), "error": result.get("error", None), "duration": duration, "timestamp": datetime.now().isoformat() }) logger.info(f"Test {test_name} terminé. Statut: {status}, Durée: {duration:.2f}s") except Exception as e: self.test_results["tests_failed"] += 1 duration = (datetime.now() - start_time).total_seconds() error_msg = str(e) # Gestion spéciale pour les erreurs courantes if "charmap" in error_msg: error_msg = "Problème d'encodage de caractères - fichier non UTF-8" elif "Permission" in error_msg: error_msg = "Problème d'autorisation d'accès au fichier" elif "FileNotFoundError" in error_msg: error_msg = "Fichier requis introuvable" self.test_results["results"].append({ "id": test_id, "name": test_name, "status": "error", "details": f"Une exception s'est produite: {error_msg}", "error": str(e), "duration": duration, "timestamp": datetime.now().isoformat() }) logger.error(f"Erreur lors du test {test_name}: {str(e)}") # Ne pas propager l'erreur pour éviter le plantage de l'application print(f"ERREUR NON CRITIQUE dans {test_name}: {error_msg}") except KeyboardInterrupt: # Gérer l'interruption utilisateur self.test_results["tests_failed"] += 1 duration = (datetime.now() - start_time).total_seconds() self.test_results["results"].append({ "id": test_id, "name": test_name, "status": "interrupted", "details": "Test interrompu par l'utilisateur", "error": "KeyboardInterrupt", "duration": duration, "timestamp": datetime.now().isoformat() }) logger.warning(f"Test {test_name} interrompu par l'utilisateur") except: # Catch-all pour les erreurs inattendues self.test_results["tests_failed"] += 1 duration = (datetime.now() - start_time).total_seconds() self.test_results["results"].append({ "id": test_id, "name": test_name, "status": "critical_error", "details": "Erreur critique non gérée", "error": "Erreur système inattendue", "duration": duration, "timestamp": datetime.now().isoformat() }) logger.critical(f"Erreur critique lors du test {test_name}") print(f"ERREUR CRITIQUE dans {test_name} - Test ignoré pour éviter le plantage") def test_file_integrity(self): """ Vérifie l'intégrité des fichiers critiques. """ results = { "success": True, "details": "Tous les fichiers sont intacts", "missing_files": [], "modified_files": [] } # Charger les checksums de référence si disponibles reference_checksums = {} checksums_file = os.path.join(self.autolook_dir, "system_tests", "checksums.json") if os.path.exists(checksums_file): try: with open(checksums_file, 'r') as f: reference_checksums = json.load(f) except Exception as e: logger.error(f"Erreur lors du chargement des checksums de référence: {str(e)}") # Vérifier chaque fichier critique for file_path in self.critical_files: if not os.path.exists(file_path): results["success"] = False results["missing_files"].append(file_path) continue # Calculer le checksum actuel checksum = self._calculate_checksum(file_path) # Comparer avec la référence si disponible if file_path in reference_checksums and checksum != reference_checksums[file_path]: results["success"] = False results["modified_files"].append(file_path) # Mettre à jour les détails if results["missing_files"] or results["modified_files"]: details = [] if results["missing_files"]: details.append( f"Fichiers manquants: {', '.join([os.path.basename(f) for f in results['missing_files']])}") if results["modified_files"]: details.append( f"Fichiers modifiés: {', '.join([os.path.basename(f) for f in results['modified_files']])}") results["details"] = ". ".join(details) return results def _calculate_checksum(self, file_path): """Calcule un checksum SHA256 pour un fichier""" try: import hashlib sha256_hash = hashlib.sha256() with open(file_path, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() except Exception as e: logger.error(f"Erreur lors du calcul du checksum pour {file_path}: {str(e)}") return None def test_outlook_access(self): """ Teste l'accès à Outlook. """ results = { "success": False, "details": "Échec de l'accès à Outlook" } try: # Tester l'accès à l'API Outlook import win32com.client import pythoncom pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") # Vérifier si on peut accéder aux dossiers folders_count = namespace.Folders.Count if folders_count > 0: results["success"] = True results["details"] = f"Accès réussi. {folders_count} boîtes de messagerie trouvées." else: results["details"] = "Outlook est accessible mais aucune boîte de messagerie n'a été trouvée." pythoncom.CoUninitialize() except Exception as e: results["error"] = str(e) return results def test_web_access(self): """ Teste l'accès au web. """ results = { "success": False, "details": "Échec de l'accès web", "sites_tested": {} } # Sites à tester test_sites = [ "https://www.google.com", "https://www.microsoft.com", "https://www.python.org" ] success_count = 0 for site in test_sites: try: import requests response = requests.get(site, timeout=5) if response.status_code == 200: results["sites_tested"][site] = "OK" success_count += 1 else: results["sites_tested"][site] = f"Erreur HTTP {response.status_code}" except Exception as e: results["sites_tested"][site] = f"Erreur: {str(e)}" # Définir le succès si au moins un site est accessible if success_count > 0: results["success"] = True results["details"] = f"{success_count}/{len(test_sites)} sites accessibles" return results def test_python_dependencies(self): """ Teste les dépendances Python requises. """ results = { "success": True, "details": "Toutes les dépendances sont installées", "missing_deps": [], "installed_deps": [] } # Liste des dépendances requises required_deps = [ "win32com", "PyQt5", "psutil", "requests", "pandas", "matplotlib", "pythoncom" ] for dep in required_deps: try: importlib.import_module(dep) results["installed_deps"].append(dep) except ImportError: results["missing_deps"].append(dep) results["success"] = False if results["missing_deps"]: results["details"] = f"Dépendances manquantes: {', '.join(results['missing_deps'])}" return results def test_disk_space(self): """ Vérifie l'espace disque disponible. """ results = { "success": True, "details": "Espace disque suffisant", "disk_stats": {} } try: import psutil # Vérifier l'espace disque sur le lecteur C: c_usage = psutil.disk_usage('C:/') results["disk_stats"] = { "total_gb": c_usage.total / (1024 ** 3), "used_gb": c_usage.used / (1024 ** 3), "free_gb": c_usage.free / (1024 ** 3), "percent": c_usage.percent } # Vérifier s'il y a moins de 1GB d'espace libre ou plus de 95% d'utilisation if c_usage.free < 1 * (1024 ** 3) or c_usage.percent > 95: results["success"] = False results[ "details"] = f"Espace disque critique: {results['disk_stats']['free_gb']:.2f} GB libre, {c_usage.percent}% utilisé" else: results[ "details"] = f"Espace disque suffisant: {results['disk_stats']['free_gb']:.2f} GB libre, {c_usage.percent}% utilisé" except Exception as e: results["success"] = False results["error"] = str(e) results["details"] = "Erreur lors de la vérification de l'espace disque" return results def test_permissions(self): """ Vérifie les permissions d'accès pour les fichiers critiques. Version améliorée avec gestion des encodages. """ results = { "success": True, "details": "Toutes les permissions sont correctes", "permission_issues": [], "file_encodings": {} } for file_path in self.critical_files: if os.path.exists(file_path): try: # Détecter l'encodage du fichier encoding = self._detect_file_encoding(file_path) results["file_encodings"][file_path] = encoding # Vérifier si on peut lire le fichier avec l'encodage détecté with open(file_path, 'r', encoding=encoding, errors='ignore') as f: # Lire seulement les premiers caractères pour tester test_content = f.read(100) # Vérifier si on peut écrire dans le dossier dir_path = os.path.dirname(file_path) test_file = os.path.join(dir_path, f"test_write_{int(time.time())}.tmp") with open(test_file, 'w', encoding='utf-8') as f: f.write("test d'écriture avec accents éèà") # Supprimer le fichier de test os.remove(test_file) logger.info(f"Permissions OK pour {file_path} (encodage: {encoding})") except PermissionError as e: results["success"] = False results["permission_issues"].append({ "file": file_path, "error": f"Problème de permissions: {str(e)}", "type": "permission" }) logger.error(f"Erreur de permission pour {file_path}: {str(e)}") except UnicodeDecodeError as e: results["success"] = False results["permission_issues"].append({ "file": file_path, "error": f"Problème d'encodage: {str(e)}", "type": "encoding" }) logger.error(f"Erreur d'encodage pour {file_path}: {str(e)}") except Exception as e: results["success"] = False results["permission_issues"].append({ "file": file_path, "error": f"Erreur inattendue: {str(e)}", "type": "unknown" }) logger.error(f"Erreur inattendue pour {file_path}: {str(e)}") else: results["success"] = False results["permission_issues"].append({ "file": file_path, "error": "Fichier inexistant", "type": "missing" }) # Mettre à jour les détails if results["permission_issues"]: details = "Problèmes détectés :\n" for issue in results["permission_issues"]: details += f"- {os.path.basename(issue['file'])}: {issue['error']}\n" results["details"] = details.strip() return results def _detect_file_encoding(self, file_path): """ Détecte l'encodage d'un fichier en testant plusieurs possibilités. """ encodings_to_try = ['utf-8', 'utf-8-sig', 'cp1252', 'latin1', 'ascii'] for encoding in encodings_to_try: try: with open(file_path, 'r', encoding=encoding) as f: # Essayer de lire tout le fichier f.read() return encoding except UnicodeDecodeError: continue except Exception as e: logger.warning(f"Erreur lors du test d'encodage {encoding} pour {file_path}: {str(e)}") continue # Si aucun encodage ne fonctionne, retourner utf-8 avec gestion d'erreurs logger.warning(f"Aucun encodage détecté pour {file_path}, utilisation d'utf-8 par défaut") return 'utf-8' def _safe_read_file(self, file_path, max_size=1024 * 1024): """ Lit un fichier de manière sécurisée en gérant les problèmes d'encodage. """ encoding = self._detect_file_encoding(file_path) try: with open(file_path, 'r', encoding=encoding, errors='replace') as f: # Limiter la taille de lecture pour éviter les problèmes de mémoire content = f.read(max_size) return content, encoding except Exception as e: logger.error(f"Erreur lors de la lecture de {file_path}: {str(e)}") return None, None def test_configuration(self): """ Vérifie la configuration d'AutoLook. Version sécurisée qui s'adapte à la structure existante. """ results = { "success": True, "details": "Configuration valide", "config_issues": [], "config_info": {} } config_file = os.path.join(self.autolook_dir, "config.json") # Vérifier l'existence du fichier de configuration if not os.path.exists(config_file): # Créer un fichier de configuration basique s'il n'existe pas try: default_config = { "app_name": "AutoLook", "version": "1.0.0", "created": datetime.now().isoformat(), "settings": { "auto_start": False, "debug_mode": False } } os.makedirs(os.path.dirname(config_file), exist_ok=True) with open(config_file, 'w', encoding='utf-8') as f: json.dump(default_config, f, indent=4, ensure_ascii=False) results["details"] = "Fichier de configuration créé avec des valeurs par défaut" results["config_info"]["action"] = "created_default" return results except Exception as e: results["success"] = False results["config_issues"].append(f"Impossible de créer le fichier de configuration: {str(e)}") results["details"] = "Fichier de configuration manquant et impossible à créer" return results # Tenter de lire le fichier avec gestion d'encodage try: # Essayer plusieurs encodages content = None encoding_used = None for encoding in ['utf-8', 'utf-8-sig', 'cp1252', 'latin1']: try: with open(config_file, 'r', encoding=encoding) as f: content = f.read() encoding_used = encoding break except UnicodeDecodeError: continue if content is None: results["success"] = False results["config_issues"].append("Impossible de lire le fichier avec aucun encodage") results["details"] = "Problème d'encodage du fichier de configuration" return results results["config_info"]["encoding"] = encoding_used results["config_info"]["size"] = len(content) except Exception as e: results["success"] = False results["config_issues"].append(f"Erreur lors de la lecture: {str(e)}") results["details"] = "Impossible de lire le fichier de configuration" return results # Vérifier le format JSON try: config = json.loads(content) # Vérifier que c'est un dictionnaire if not isinstance(config, dict): results["success"] = False results["config_issues"].append("Le fichier ne contient pas un objet JSON valide") else: # Analyser la structure existante (sans forcer des clés spécifiques) results["config_info"]["keys_found"] = list(config.keys()) results["config_info"]["total_keys"] = len(config.keys()) # Vérifier s'il y a au moins quelques données if len(config.keys()) == 0: results["config_issues"].append("Fichier de configuration vide") else: # Analyser le contenu de manière flexible has_app_info = any(key in config for key in ['app_name', 'name', 'title']) has_settings = any(key in config for key in ['settings', 'config', 'preferences']) results["config_info"]["has_app_info"] = has_app_info results["config_info"]["has_settings"] = has_settings if not has_app_info and not has_settings: results["config_issues"].append("Structure de configuration non standard mais valide") except json.JSONDecodeError as e: results["success"] = False results["config_issues"].append(f"JSON invalide: {str(e)}") except Exception as e: results["success"] = False results["config_issues"].append(f"Erreur lors de l'analyse: {str(e)}") # Mettre à jour les détails if results["config_issues"]: results["details"] = "Problèmes détectés: " + ", ".join(results["config_issues"]) # Ne pas marquer comme échec si c'est juste une structure non standard if not any( "invalide" in issue.lower() or "impossible" in issue.lower() for issue in results["config_issues"]): results["success"] = True # Structure différente mais fichier lisible else: total_keys = results["config_info"].get("total_keys", 0) encoding = results["config_info"].get("encoding", "inconnu") results["details"] = f"Configuration valide ({total_keys} clés, encodage: {encoding})" return results def generate_report(self): """ Génère un rapport HTML des tests exécutés. """ try: # Créer le dossier des rapports s'il n'existe pas reports_dir = os.path.join(self.autolook_dir, "system_tests", "reports") os.makedirs(reports_dir, exist_ok=True) # Nom du fichier de rapport timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") report_file = os.path.join(reports_dir, f"system_test_report_{timestamp}.html") # Calculer les statistiques start_time = datetime.fromisoformat(self.test_results["start_time"]) end_time = datetime.fromisoformat(self.test_results["end_time"]) if self.test_results[ "end_time"] else datetime.now() duration = (end_time - start_time).total_seconds() success_rate = 0 if self.test_results["tests_run"] > 0: success_rate = (self.test_results["tests_passed"] / self.test_results["tests_run"]) * 100 # Générer le HTML html = f""" Rapport d'autodiagnostic AutoLook

Rapport d'autodiagnostic AutoLook

Généré le {end_time.strftime("%d/%m/%Y à %H:%M:%S")}

Tests réussis

{self.test_results["tests_passed"]}

Tests échoués

{self.test_results["tests_failed"]}

Total

{self.test_results["tests_run"]}

Durée

{duration:.2f} secondes

= 90 else "summary-failed" if success_rate < 70 else "summary-total"}">

Taux de succès

{success_rate:.1f}%

""" # Ajouter chaque résultat de test for test in self.test_results["results"]: status_class = "" if test["status"] == "passed": status_class = "status-passed" elif test["status"] == "failed": status_class = "status-failed" else: status_class = "status-error" # Préparer les détails details_html = f"
{test['details']}
" if test.get("error"): details_html += f"
Erreur: {test['error']}
" html += f""" """ # Fermeture du tableau et du HTML html += """
Test Statut Durée Détails
{test['name']} {test['status'].upper()} {test['duration']:.2f}s {details_html}
""" # Écrire le rapport dans un fichier with open(report_file, 'w', encoding='utf-8') as f: f.write(html) logger.info(f"Rapport généré avec succès: {report_file}") return report_file except Exception as e: logger.error(f"Erreur lors de la génération du rapport: {str(e)}") return None def test_outlook_version_compatibility(self): """ Teste la compatibilité avec la version d'Outlook installée. """ results = { "success": False, "details": "Version Outlook non compatible", "outlook_info": {} } try: import win32com.client import pythoncom pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application") # Récupérer les informations sur Outlook version = outlook.Version results["outlook_info"]["version"] = version results["outlook_info"]["build"] = outlook.Build if hasattr(outlook, 'Build') else "Non disponible" # Vérifier la compatibilité (adaptatez selon vos besoins) major_version = int(version.split('.')[0]) if major_version >= 15: # Office 2013+ results["success"] = True results["details"] = f"Outlook {version} compatible" else: results["details"] = f"Outlook {version} trop ancien (minimum: Office 2013)" pythoncom.CoUninitialize() except Exception as e: results["details"] = f"Impossible de détecter Outlook: {str(e)}" return results def test_outlook_security_settings(self): """ Teste les paramètres de sécurité d'Outlook qui peuvent bloquer l'automatisation. """ results = { "success": True, "details": "Paramètres de sécurité acceptables", "security_warnings": [] } try: import win32com.client import pythoncom pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") # Tester l'accès aux dossiers principaux try: inbox = namespace.GetDefaultFolder(6) # olFolderInbox sent_items = namespace.GetDefaultFolder(5) # olFolderSentMail calendar = namespace.GetDefaultFolder(9) # olFolderCalendar results["security_warnings"].append("Accès aux dossiers principaux: OK") except Exception as e: results["success"] = False results["security_warnings"].append(f"Accès aux dossiers bloqué: {str(e)}") # Tester la création d'un élément (sans l'envoyer) try: mail_item = outlook.CreateItem(0) # olMailItem mail_item.Subject = "Test AutoLook" mail_item.Body = "Test de connectivité" # Ne pas envoyer, juste tester la création results["security_warnings"].append("Création d'emails: OK") except Exception as e: results["success"] = False results["security_warnings"].append(f"Création d'emails bloquée: {str(e)}") pythoncom.CoUninitialize() except Exception as e: results["success"] = False results["details"] = f"Erreur lors du test de sécurité: {str(e)}" if results["security_warnings"]: results["details"] = "; ".join(results["security_warnings"]) return results def test_outlook_performance(self): """ Teste les performances d'accès à Outlook. """ results = { "success": True, "details": "Performances Outlook acceptables", "performance_metrics": {} } try: import win32com.client import pythoncom import time pythoncom.CoInitialize() # Test de connexion start_time = time.time() outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") connection_time = time.time() - start_time results["performance_metrics"]["connection_time"] = connection_time # Test d'accès aux dossiers start_time = time.time() inbox = namespace.GetDefaultFolder(6) folder_count = inbox.Items.Count folder_access_time = time.time() - start_time results["performance_metrics"]["folder_access_time"] = folder_access_time results["performance_metrics"]["inbox_item_count"] = folder_count # Évaluer les performances if connection_time > 5.0: results["success"] = False results["details"] = f"Connexion lente: {connection_time:.2f}s" elif folder_access_time > 3.0: results["success"] = False results["details"] = f"Accès aux dossiers lent: {folder_access_time:.2f}s" else: results["details"] = f"Connexion: {connection_time:.2f}s, Accès dossier: {folder_access_time:.2f}s" pythoncom.CoUninitialize() except Exception as e: results["success"] = False results["details"] = f"Erreur de performance: {str(e)}" return results def test_autolook_modules(self): """ Teste l'intégrité et le fonctionnement des modules AutoLook. """ results = { "success": True, "details": "Tous les modules fonctionnent correctement", "module_tests": {}, "import_errors": [] } # Modules à tester (adaptez selon vos modules) modules_to_test = [ ("modules.module_outlook_2021", "Module Outlook 2021"), ("modules.ha2", "Module HA2"), # Ajoutez vos autres modules ici ] for module_name, module_description in modules_to_test: try: # Tenter l'import du module import importlib module = importlib.import_module(module_name) # Vérifier s'il y a des fonctions principales functions = [name for name in dir(module) if not name.startswith('_')] results["module_tests"][module_name] = { "status": "OK", "functions_count": len(functions), "has_main": hasattr(module, 'main') or hasattr(module, 'run') } except ImportError as e: results["success"] = False results["import_errors"].append(f"{module_description}: {str(e)}") results["module_tests"][module_name] = { "status": "IMPORT_ERROR", "error": str(e) } except Exception as e: results["success"] = False results["import_errors"].append(f"{module_description}: Erreur inattendue - {str(e)}") results["module_tests"][module_name] = { "status": "ERROR", "error": str(e) } # Mettre à jour les détails if results["import_errors"]: results["details"] = f"Problèmes détectés: {', '.join(results['import_errors'])}" else: loaded_modules = len([m for m in results["module_tests"] if results["module_tests"][m]["status"] == "OK"]) results["details"] = f"{loaded_modules} modules chargés avec succès" return results def test_autolook_configuration_specific(self): """ Teste la configuration spécifique à AutoLook - Version adaptative. """ results = { "success": True, "details": "Configuration AutoLook valide", "config_analysis": {}, "suggestions": [] } config_file = os.path.join(self.autolook_dir, "config.json") try: # Lire le fichier de configuration content, encoding = self._safe_read_file(config_file) if content is None: # Créer une configuration par défaut si elle n'existe pas try: default_config = { "app_info": { "name": "AutoLook", "version": "1.0.0", "created": datetime.now().isoformat() }, "outlook": { "auto_connect": True, "timeout_seconds": 30, "retry_count": 3 }, "general": { "debug_mode": False, "auto_start": False, "language": "fr" }, "logging": { "enabled": True, "level": "INFO", "max_file_size_mb": 10 } } os.makedirs(os.path.dirname(config_file), exist_ok=True) with open(config_file, 'w', encoding='utf-8') as f: json.dump(default_config, f, indent=4, ensure_ascii=False) results["details"] = "Configuration par défaut créée avec succès" results["config_analysis"]["action"] = "default_config_created" return results except Exception as e: results["success"] = False results["details"] = f"Impossible de créer la configuration: {str(e)}" return results config = json.loads(content) results["config_analysis"]["encoding"] = encoding results["config_analysis"]["total_keys"] = len(config.keys()) results["config_analysis"]["structure"] = list(config.keys()) # Analyse adaptative de la structure existante recommendations = [] # Vérifier les sections importantes (sans forcer leur existence) recommended_sections = { "app_info": "Informations de l'application", "outlook": "Paramètres Outlook", "general": "Paramètres généraux", "logging": "Configuration des logs" } existing_sections = 0 for section, description in recommended_sections.items(): if section in config: existing_sections += 1 results["config_analysis"][f"has_{section}"] = True else: recommendations.append(f"Ajouter section '{section}' pour {description}") results["config_analysis"][f"has_{section}"] = False # Vérifier s'il y a au moins des paramètres de base has_basic_info = any(key in config for key in ['name', 'app_name', 'title', 'version']) has_outlook_info = any(key in config for key in ['outlook', 'mail', 'email']) has_settings = any(key in config for key in ['settings', 'config', 'general', 'options']) results["config_analysis"]["has_basic_info"] = has_basic_info results["config_analysis"]["has_outlook_info"] = has_outlook_info results["config_analysis"]["has_settings"] = has_settings # Vérifier des paramètres critiques spécifiques critical_checks = [] # Si la section outlook existe, vérifier ses paramètres if "outlook" in config: outlook_config = config["outlook"] if "timeout" not in outlook_config and "timeout_seconds" not in outlook_config: recommendations.append("Ajouter timeout pour Outlook") if "retry_count" not in outlook_config and "retries" not in outlook_config: recommendations.append("Ajouter nombre de tentatives pour Outlook") # Si la section logging existe, vérifier ses paramètres if "logging" in config: logging_config = config["logging"] if "level" not in logging_config: recommendations.append("Ajouter niveau de log") # Évaluation finale score = 0 max_score = 4 if has_basic_info: score += 1 if has_outlook_info: score += 1 if has_settings: score += 1 if existing_sections >= 2: score += 1 if score >= 3: results["success"] = True results["details"] = f"Configuration satisfaisante ({score}/{max_score} critères remplis)" elif score >= 2: results["success"] = True results["details"] = f"Configuration basique valide ({score}/{max_score} critères remplis)" if recommendations: results["details"] += f" - {len(recommendations)} améliorations suggérées" else: results["success"] = False results["details"] = f"Configuration insuffisante ({score}/{max_score} critères remplis)" results["suggestions"] = recommendations[:5] # Max 5 suggestions except json.JSONDecodeError as e: results["success"] = False results["details"] = f"JSON invalide: {str(e)}" except Exception as e: results["success"] = False results["details"] = f"Erreur lors de l'analyse: {str(e)}" return results def test_autolook_logs(self): """ Teste l'état des logs et détecte les erreurs récentes. """ results = { "success": True, "details": "Logs en bon état", "log_analysis": {}, "recent_errors": [] } # Dossiers de logs potentiels log_paths = [ os.path.join(self.autolook_dir, "logs"), os.path.join(self.autolook_dir, "log"), self.autolook_dir # logs dans le dossier principal ] log_files_found = [] for log_path in log_paths: if os.path.exists(log_path) and os.path.isdir(log_path): # Chercher les fichiers de log for file in os.listdir(log_path): if file.endswith(('.log', '.txt')) or 'log' in file.lower(): log_files_found.append(os.path.join(log_path, file)) results["log_analysis"]["files_found"] = len(log_files_found) if not log_files_found: results["details"] = "Aucun fichier de log trouvé" return results # Analyser les logs récents from datetime import datetime, timedelta recent_date = datetime.now() - timedelta(days=7) # 7 derniers jours for log_file in log_files_found[:5]: # Analyser max 5 fichiers try: content, encoding = self._safe_read_file(log_file, max_size=50000) # Max 50KB if content: # Chercher des erreurs error_keywords = ['ERROR', 'CRITICAL', 'EXCEPTION', 'FAILED', 'CRASH'] lines = content.split('\n') for line in lines[-100:]: # 100 dernières lignes line_upper = line.upper() if any(keyword in line_upper for keyword in error_keywords): results["recent_errors"].append({ "file": os.path.basename(log_file), "content": line.strip()[:200] # Max 200 caractères }) except Exception as e: continue # Évaluer les résultats if len(results["recent_errors"]) > 10: results["success"] = False results["details"] = f"Nombreuses erreurs détectées: {len(results['recent_errors'])} erreurs récentes" elif len(results["recent_errors"]) > 0: results["details"] = f"{len(results['recent_errors'])} erreurs récentes trouvées" else: results["details"] = f"{len(log_files_found)} fichiers de log analysés, aucune erreur récente" return results def test_windows_security_context(self): """ Teste le contexte de sécurité Windows et les privilèges nécessaires. """ results = { "success": True, "details": "Contexte de sécurité approprié", "security_info": {}, "warnings": [] } try: import ctypes import os # Vérifier si on a les privilèges administrateur is_admin = ctypes.windll.shell32.IsUserAnAdmin() results["security_info"]["is_admin"] = is_admin # Vérifier l'utilisateur actuel current_user = os.getlogin() results["security_info"]["current_user"] = current_user # Tester l'accès aux répertoires système (sans modifier) system_paths = [ r"C:\Program Files\Microsoft Office", r"C:\Windows\System32", os.environ.get('APPDATA', '') ] accessible_paths = 0 for path in system_paths: if os.path.exists(path) and os.access(path, os.R_OK): accessible_paths += 1 results["security_info"]["accessible_system_paths"] = accessible_paths # Évaluer la sécurité if not is_admin and "automation" in self.autolook_dir.lower(): results["warnings"].append("Application d'automatisation sans privilèges admin") if accessible_paths < len(system_paths) // 2: results["warnings"].append("Accès limité aux répertoires système") if results["warnings"]: results["details"] = f"Avertissements: {'; '.join(results['warnings'])}" except Exception as e: results["success"] = False results["details"] = f"Erreur lors du test de sécurité: {str(e)}" return results def test_process_memory_usage(self): """ Teste l'utilisation mémoire du processus AutoLook. """ results = { "success": True, "details": "Utilisation mémoire normale", "memory_info": {} } try: import psutil import os # Obtenir les informations du processus actuel current_process = psutil.Process(os.getpid()) memory_info = current_process.memory_info() memory_percent = current_process.memory_percent() results["memory_info"]["rss_mb"] = memory_info.rss / (1024 * 1024) # MB results["memory_info"]["vms_mb"] = memory_info.vms / (1024 * 1024) # MB results["memory_info"]["percent"] = memory_percent # Vérifier les seuils if memory_percent > 10: # Plus de 10% de la RAM système results["success"] = False results["details"] = f"Utilisation mémoire élevée: {memory_percent:.1f}%" elif memory_info.rss > 500 * 1024 * 1024: # Plus de 500MB results["warnings"] = ["Utilisation mémoire importante"] results["details"] = f"Mémoire: {results['memory_info']['rss_mb']:.1f} MB ({memory_percent:.1f}%)" else: results["details"] = f"Mémoire: {results['memory_info']['rss_mb']:.1f} MB ({memory_percent:.1f}%)" except Exception as e: results["success"] = False results["details"] = f"Erreur lors du test mémoire: {str(e)}" return results def test_concurrent_outlook_access(self): """ Teste si d'autres applications accèdent à Outlook simultanément. """ results = { "success": True, "details": "Aucun conflit détecté", "concurrent_processes": [] } try: import psutil # Chercher des processus qui pourraient interférer avec Outlook outlook_related_processes = [] automation_processes = [] for proc in psutil.process_iter(['pid', 'name', 'cmdline']): try: name = proc.info['name'].lower() cmdline = ' '.join(proc.info['cmdline'] or []).lower() if 'outlook' in name or 'outlook' in cmdline: outlook_related_processes.append({ "name": proc.info['name'], "pid": proc.info['pid'] }) # Chercher d'autres outils d'automatisation automation_keywords = ['automation', 'macro', 'script', 'bot'] if any(keyword in name or keyword in cmdline for keyword in automation_keywords): if proc.info['pid'] != os.getpid(): # Exclure le processus actuel automation_processes.append({ "name": proc.info['name'], "pid": proc.info['pid'] }) except (psutil.NoSuchProcess, psutil.AccessDenied): continue results["concurrent_processes"] = { "outlook_processes": len(outlook_related_processes), "automation_processes": len(automation_processes) } # Évaluer les conflits potentiels if len(automation_processes) > 3: results["success"] = False results["details"] = f"Trop d'outils d'automatisation actifs: {len(automation_processes)}" elif len(outlook_related_processes) > 2: results["details"] = f"Plusieurs processus Outlook détectés: {len(outlook_related_processes)}" else: results["details"] = f"Environnement propre: {len(outlook_related_processes)} processus Outlook" except Exception as e: results["success"] = False results["details"] = f"Erreur lors du test de concurrence: {str(e)}" return results def test_autolook_critical_files(self): """ Teste spécifiquement les fichiers critiques d'AutoLook. """ results = { "success": True, "details": "Tous les fichiers critiques sont présents", "file_status": {}, "missing_files": [], "corrupted_files": [] } # Fichiers vraiment critiques pour AutoLook critical_files = { "main.py": "Fichier principal de l'application", "config.json": "Configuration de l'application", "modules/module_outlook_2021.py": "Module Outlook principal", "modules/ha2.py": "Module HA2" } for filename, description in critical_files.items(): file_path = os.path.join(self.autolook_dir, filename) if not os.path.exists(file_path): results["success"] = False results["missing_files"].append(filename) results["file_status"][filename] = "MANQUANT" else: try: # Tester la lisibilité file_size = os.path.getsize(file_path) if file_size == 0: results["corrupted_files"].append(filename) results["file_status"][filename] = "VIDE" results["success"] = False else: # Tester l'ouverture encoding = self._detect_file_encoding(file_path) with open(file_path, 'r', encoding=encoding, errors='ignore') as f: f.read(100) # Lire les premiers caractères results["file_status"][filename] = f"OK ({file_size} octets)" except Exception as e: results["corrupted_files"].append(filename) results["file_status"][filename] = f"ERREUR: {str(e)[:50]}" results["success"] = False # Construire le message de détail if results["missing_files"] or results["corrupted_files"]: details = [] if results["missing_files"]: details.append(f"Fichiers manquants: {', '.join(results['missing_files'])}") if results["corrupted_files"]: details.append(f"Fichiers corrompus: {', '.join(results['corrupted_files'])}") results["details"] = ". ".join(details) else: results["details"] = f"Tous les fichiers critiques sont OK ({len(critical_files)} vérifiés)" return results def test_autolook_startup_readiness(self): """ Teste si AutoLook est prêt à démarrer correctement. """ results = { "success": True, "details": "Application prête au démarrage", "readiness_checks": {}, "blocking_issues": [] } # Tests de préparation au démarrage checks = { "config_readable": lambda: self._check_config_readable(), "outlook_available": lambda: self._check_outlook_available(), "modules_importable": lambda: self._check_modules_importable(), "logs_writable": lambda: self._check_logs_writable(), "temp_space": lambda: self._check_temp_space() } for check_name, check_function in checks.items(): try: check_result = check_function() results["readiness_checks"][check_name] = check_result if not check_result["success"]: results["blocking_issues"].append(f"{check_name}: {check_result['message']}") if check_result.get("critical", False): results["success"] = False except Exception as e: results["blocking_issues"].append(f"{check_name}: Erreur - {str(e)}") results["readiness_checks"][check_name] = {"success": False, "message": str(e)} # Évaluation finale if results["blocking_issues"]: if results["success"]: results["details"] = f"Prêt avec avertissements: {len(results['blocking_issues'])} problèmes mineurs" else: results["details"] = f"Non prêt: {len(results['blocking_issues'])} problèmes bloquants" else: results["details"] = "Application entièrement prête au démarrage" return results def _check_config_readable(self): """Vérifie si la configuration est lisible.""" config_file = os.path.join(self.autolook_dir, "config.json") if not os.path.exists(config_file): return {"success": False, "message": "Fichier config.json manquant", "critical": True} try: content, encoding = self._safe_read_file(config_file) if content is None: return {"success": False, "message": "Config illisible", "critical": True} json.loads(content) return {"success": True, "message": f"Config OK ({encoding})"} except json.JSONDecodeError: return {"success": False, "message": "JSON invalide", "critical": True} except Exception as e: return {"success": False, "message": f"Erreur: {str(e)}", "critical": True} def _check_outlook_available(self): """Vérifie rapidement si Outlook est disponible.""" try: import win32com.client import pythoncom pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") folder_count = namespace.Folders.Count pythoncom.CoUninitialize() return {"success": True, "message": f"Outlook OK ({folder_count} boîtes)"} except Exception as e: return {"success": False, "message": f"Outlook inaccessible: {str(e)[:50]}", "critical": True} def _check_modules_importable(self): """Vérifie si les modules peuvent être importés.""" try: # Tester l'import des modules critiques sys.path.insert(0, self.autolook_dir) import importlib.util modules_to_check = ["modules.module_outlook_2021", "modules.ha2"] for module_name in modules_to_check: try: module = importlib.import_module(module_name) except ImportError as e: return {"success": False, "message": f"Module {module_name} non importable", "critical": True} return {"success": True, "message": f"{len(modules_to_check)} modules OK"} except Exception as e: return {"success": False, "message": f"Erreur import: {str(e)[:50]}", "critical": False} def _check_logs_writable(self): """Vérifie si on peut écrire dans les logs.""" try: logs_dir = os.path.join(self.autolook_dir, "logs") os.makedirs(logs_dir, exist_ok=True) test_log = os.path.join(logs_dir, f"test_{int(time.time())}.tmp") with open(test_log, 'w', encoding='utf-8') as f: f.write("Test d'écriture") os.remove(test_log) return {"success": True, "message": "Logs accessibles"} except Exception as e: return {"success": False, "message": f"Logs non accessibles: {str(e)[:50]}", "critical": False} def _check_temp_space(self): """Vérifie l'espace pour les fichiers temporaires.""" try: import psutil disk_usage = psutil.disk_usage(self.autolook_dir) free_gb = disk_usage.free / (1024 ** 3) if free_gb < 0.1: # Moins de 100MB return {"success": False, "message": f"Espace insuffisant: {free_gb:.1f}GB", "critical": True} elif free_gb < 1: # Moins de 1GB return {"success": False, "message": f"Espace limité: {free_gb:.1f}GB", "critical": False} else: return {"success": True, "message": f"Espace OK: {free_gb:.1f}GB"} except Exception as e: return {"success": False, "message": f"Vérification espace: {str(e)[:50]}", "critical": False} def integrate_system_monitor(main_window): """ Intègre le moniteur de tâches système dans l'application principale. Ajoute un bouton d'accès au tableau de bord des tâches dans le volet des paramètres. Args: main_window: La fenêtre principale de l'application Returns: SystemTaskMonitor: L'instance du moniteur de tâches """ try: # Créer l'instance du moniteur system_monitor = SystemTaskMonitor(main_window) # Créer le bouton dashboard avec le bon style dashboard_button = QPushButton("🔍 Tableau de bord système") dashboard_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; margin: 2px; font-size: 12px; } QPushButton:hover { background-color: #2980b9; color: white; } """) dashboard_button.clicked.connect(system_monitor.open_dashboard) # S'assurer que le bouton est accessible via l'instance monitor system_monitor.dashboard_button = dashboard_button # Ajouter le bouton au volet des paramètres, avant le bouton de forçage de priorité basse if hasattr(main_window, 'priority_group') and hasattr(main_window, 'force_low_priority_button'): priority_layout = main_window.priority_group.layout() # Trouver l'index du bouton force_low_priority_button dans le layout button_index = -1 for i in range(priority_layout.count()): if priority_layout.itemAt(i).widget() == main_window.force_low_priority_button: button_index = i break # Insérer le bouton avant le bouton de forçage de priorité if button_index >= 0: priority_layout.insertWidget(button_index, dashboard_button) logger.info("Bouton de tableau de bord système ajouté au-dessus du bouton Low Priority") else: # Si on n'a pas trouvé le bouton, l'ajouter simplement au layout priority_layout.addWidget(dashboard_button) logger.info("Bouton de tableau de bord système ajouté au groupe de priorité") else: logger.warning("Impossible de trouver le groupe de priorité ou le bouton Low Priority") logger.info("Moniteur de tâches système intégré avec succès") return system_monitor except Exception as e: logger.error(f"Erreur lors de l'intégration du moniteur de tâches : {str(e)}") return None def integrate_system_autotest(main_window): """ Intègre le système d'autodiagnostic dans l'application principale. Ajoute un bouton pour exécuter les tests système dans le volet des paramètres. Args: main_window: La fenêtre principale de l'application Returns: SystemAutoTest: L'instance du système d'autotest """ try: # Créer l'instance du système d'autotest system_autotest = SystemAutoTest(main_window) # Créer le bouton de test avec le bon style test_button = QPushButton("🔬 Autotest système") test_button.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; margin: 2px; font-size: 12px; } QPushButton:hover { background-color: #2ecc71; color: white; } """) test_button.clicked.connect(system_autotest.show_test_dialog) # S'assurer que le bouton est accessible via l'instance autotest system_autotest.test_button = test_button # Ajouter le bouton au volet des paramètres, avant le bouton de forçage de priorité basse if hasattr(main_window, 'priority_group') and hasattr(main_window, 'force_low_priority_button'): priority_layout = main_window.priority_group.layout() # Ajouter le bouton avant force_low_priority_button # Le bouton de tableau de bord est déjà ajouté, donc celui-ci doit être ajouté juste après dashboard_index = -1 for i in range(priority_layout.count()): widget = priority_layout.itemAt(i).widget() if hasattr(widget, 'text') and '🔍 Tableau de bord système' in widget.text(): dashboard_index = i break # Si on a trouvé le bouton du tableau de bord, insérer juste après if dashboard_index >= 0: priority_layout.insertWidget(dashboard_index + 1, test_button) logger.info("Bouton d'autotest système ajouté après le bouton tableau de bord") else: # Si on n'a pas trouvé le bouton du tableau de bord, insérer avant force_low_priority_button for i in range(priority_layout.count()): if priority_layout.itemAt(i).widget() == main_window.force_low_priority_button: priority_layout.insertWidget(i, test_button) logger.info("Bouton d'autotest système ajouté au-dessus du bouton Low Priority") break else: # Si on n'a toujours pas trouvé, ajouter simplement au layout priority_layout.addWidget(test_button) logger.info("Bouton d'autotest système ajouté au groupe de priorité") else: logger.warning("Impossible de trouver le groupe de priorité ou le bouton Low Priority") logger.info("Système d'autodiagnostic intégré avec succès") return system_autotest except Exception as e: logger.error(f"Erreur lors de l'intégration du système d'autodiagnostic : {str(e)}") return None class ReplanTaskDialog(QDialog): def __init__(self, parent, original_task): super().__init__(parent) self.original_task = original_task self.setWindowTitle("Replanifier la tâche") self.setMinimumWidth(400) # Style du dialogue self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QGroupBox { color: blue; border: 1px solid #34495e; border-radius: 4px; margin-top: 10px; } QGroupBox::title { color: blue; } QCalendarWidget { background-color: #03699c; } QTimeEdit { background-color: #03699c; padding: 5px; border-radius: 4px; } """) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Informations de la tâche originale info_group = QGroupBox("Tâche d'origine") info_layout = QVBoxLayout() info_layout.addWidget(QLabel(f"Profil: {self.original_task['profile']}")) info_layout.addWidget(QLabel(f"Récurrence: {self.original_task['recurrence']}")) info_layout.addWidget(QLabel(f"Nombre de mails: {self.original_task.get('mail_count', 1)}")) info_group.setLayout(info_layout) layout.addWidget(info_group) # Nouvelle date/heure datetime_group = QGroupBox("Nouvelle planification") datetime_layout = QVBoxLayout() self.calendar = QCalendarWidget() self.calendar.setMinimumDate(QDate.currentDate()) datetime_layout.addWidget(self.calendar) time_layout = QHBoxLayout() time_layout.addWidget(QLabel("Heure:")) self.time_edit = QTimeEdit() self.time_edit.setTime(QTime.currentTime()) time_layout.addWidget(self.time_edit) datetime_layout.addLayout(time_layout) datetime_group.setLayout(datetime_layout) layout.addWidget(datetime_group) # Boutons button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) button_box.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; padding: 8px 16px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) layout.addWidget(button_box) def get_new_schedule(self): """Retourne la nouvelle planification""" new_schedule = self.original_task.copy() new_schedule['date'] = self.calendar.selectedDate().toPyDate().isoformat() new_schedule['time'] = self.time_edit.time().toString("HH:mm:00") return new_schedule def clean_old_logs(folder): import os import time # Supprime les fichiers de log de plus de 7 jours par exemple now = time.time() for filename in os.listdir(folder): file_path = os.path.join(folder, filename) if os.path.isfile(file_path): if now - os.path.getmtime(file_path) > 7 * 86400: # 7 jours os.remove(file_path) print(f"Supprimé : {file_path}") def clean_old_debug_files(): """ Supprime les fichiers de debug plus vieux que 2 jours """ try: now = datetime.now() two_hours_ago = now - timedelta(days=2) # Vérifier si le répertoire existe if not os.path.exists(DEBUG_DIR): logger.warning(f"Le répertoire {DEBUG_DIR} n'existe pas") return cleaned_count = 0 for filename in os.listdir(DEBUG_DIR): file_path = os.path.join(DEBUG_DIR, filename) try: creation_time = datetime.fromtimestamp(os.path.getctime(file_path)) if creation_time < two_hours_ago: os.remove(file_path) cleaned_count += 1 logger.info(f"Fichier de debug supprimé : {filename}") except Exception as e: logger.error(f"Erreur lors de la suppression du fichier {filename}: {e}") continue logger.info(f"{cleaned_count} fichiers de debug ont été supprimés") except Exception as e: logger.error(f"Erreur lors du nettoyage des fichiers de debug : {e}") def schedule_cleanup(): """ Configure le nettoyage périodique des fichiers et du cache Python """ try: # Nettoyage initial clean_old_logs() clean_old_debug_files() clear_pycache() # Ajout du nettoyage initial du pycache # Programmer le nettoyage toutes les 24h timer = QTimer() timer.timeout.connect(clean_old_logs) timer.timeout.connect(clean_old_debug_files) timer.timeout.connect(clear_pycache) # Ajout du nettoyage périodique du pycache timer.start(24 * 60 * 60 * 1000) # 24h en millisecondes logger.info("Nettoyage automatique programmé avec succès") # Ajouter un log spécifique pour le pycache logger.info("Nettoyage automatique du __pycache__ programmé avec succès") except Exception as e: logger.error(f"Erreur lors de la configuration du nettoyage automatique : {e}") stop_trace() def main(): try: app = QApplication(sys.argv) minimize_console_to_taskbar() if sys.platform == 'win32': if sys.getwindowsversion().major >= 6: ctypes.windll.user32.SetProcessDPIAware() app_id = 'votre_entreprise.mailer_auto_click.version' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) startup_dialog = StartupDialog() screen = QDesktopWidget().screenGeometry() dialog_geometry = startup_dialog.geometry() x = (screen.width() - dialog_geometry.width()) // 2 y = (screen.height() - dialog_geometry.height()) // 2 startup_dialog.move(x, y) result = startup_dialog.exec_() if result == QDialog.Accepted: selected_version = startup_dialog.get_selected_version() logger.info(f"Démarrage de l'application en version {selected_version}") # === Initialisation de la fenêtre principale === main_window = MainWindow(selected_version=selected_version) main_window.mail_client_type = selected_version if selected_version == 'office': main_window.mail_handler = OutlookHandler(main_window) logger.info("Handler Outlook initialisé") elif selected_version == 'thunderbird': main_window.mail_handler = ThunderbirdHandler(main_window) logger.info("Handler Thunderbird initialisé") elif selected_version == 'imap': imap_dialog = ImapAccountDialog() if imap_dialog.exec_() == QDialog.Accepted: email, password, provider = imap_dialog.get_selected_account() # Sauvegarde temporaire dans les paramètres settings = QSettings("Snoown2k", "MyApp") settings.setValue("imap/email", email) settings.setValue("imap/password", password) settings.setValue("imap/provider", provider) # Injection des données dans la fenêtre principale main_window.selected_imap_account = (email, password, provider) logger.info(f"Connexion IMAP : {email} via {provider}") else: logger.warning("Connexion IMAP annulée par l'utilisateur") return # Ne pas démarrer l'application main_window.show() logger.info("Fenêtre principale affichée") return app.exec_() except Exception as e: logger.error(f"Une erreur critique s'est produite : {e}", exc_info=True) write_debug_file() QMessageBox.critical(None, "Erreur critique", f"Une erreur inattendue s'est produite : {e}\n\nL'application va se fermer.") sys.exit(1) if __name__ == "__main__": main()