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 :
- Ralentir considérablement votre ordinateur
- Provoquer des erreurs dans le traitement
- Dans des cas extrêmes, bloquer l'application
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
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}%
Test |
Statut |
Durée |
Détails |
"""
# 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"""
{test['name']} |
{test['status'].upper()} |
{test['duration']:.2f}s |
{details_html} |
"""
# Fermeture du tableau et du HTML
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()