{log_entry}"
self.log_text.append(formatted_entry)
# Scroll automatique
scrollbar = self.log_text.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
# Force la mise à jour immédiate
QApplication.processEvents()
except Exception as e:
print(f"Erreur lors de l'affichage du log: {e}")
def toggle_logging(self):
"""Gère l'activation/désactivation du journal avec transition"""
try:
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer
is_logging_disabled = self.toggle_log_button.isChecked()
# Trouver le layout parent qui contient log_text
log_container = self.log_text.parent()
log_layout = log_container.layout()
if is_logging_disabled:
# Masquer le journal
self.log_text.hide()
# Créer et afficher la bannière si elle n'existe pas
if not hasattr(self, 'banner_container'):
# Créer une bannière simple à la place
from PyQt5.QtWidgets import QLabel
self.banner_container = QLabel(
"Journal désactivé - Cliquez sur 'Activer le journal' pour réactiver")
self.banner_container.setStyleSheet("""
QLabel {
background-color: #2c3e50;
color: #42effb;
padding: 20px;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
text-align: center;
}
""")
log_layout.addWidget(self.banner_container)
else:
self.banner_container.show()
# Changer le texte du bouton
self.toggle_log_button.setText("Activer le journal")
else:
# Réafficher le journal
if hasattr(self, 'banner_container'):
self.banner_container.hide()
if hasattr(self.banner_container, 'deleteLater'):
self.banner_container.deleteLater()
delattr(self, 'banner_container')
self.log_text.show()
self.log_text.clear()
self.toggle_log_button.setText("Désactiver le journal")
self.log_action("✅ Journal réactivé")
# Forcer la mise à jour de l'interface
QApplication.processEvents()
except Exception as e:
print(f"Erreur toggle logging: {e}")
def _show_banner(self):
"""Affiche la bannière avec un effet de fondu"""
try:
self.log_text.hide()
if not hasattr(self, 'banner_container'):
self.banner_container = FlipBanner()
# Ajouter la bannière au même parent et au même index que log_text
parent_layout = self.log_text.parent().layout()
log_text_index = parent_layout.indexOf(self.log_text)
parent_layout.insertWidget(log_text_index, self.banner_container)
self.banner_container.show()
self.banner_container.setWindowOpacity(0.0)
fade_in = QPropertyAnimation(self.banner_container, b"windowOpacity")
fade_in.setDuration(300)
fade_in.setStartValue(0.0)
fade_in.setEndValue(1.0)
fade_in.start()
except Exception as e:
print(f"Erreur dans _show_banner: {e}")
self.log_action(f"Erreur lors de l'affichage de la bannière: {str(e)}")
def _show_log(self):
"""Réaffiche le journal avec un effet de fondu"""
try:
if hasattr(self, 'banner_container'):
self.banner_container.gif.stop()
self.banner_container.animation.stop()
self.banner_container.hide()
self.banner_container.deleteLater()
delattr(self, 'banner_container')
self.toggle_log_button.setText("Désactiver le journal")
self.log_text.show()
self.log_text.setWindowOpacity(0.0)
fade_in = QPropertyAnimation(self.log_text, b"windowOpacity")
fade_in.setDuration(300)
fade_in.setStartValue(0.0)
fade_in.setEndValue(1.0)
fade_in.start()
self.log_text.clear()
self.log_action("✅ Journal réactivé")
except Exception as e:
print(f"Erreur dans _show_log: {e}")
def get_current_state(self):
"""Récupère l'état actuel des paramètres pour la sauvegarde"""
state = {
"search_criteria": {
"sender": self.sender_entry.text(),
"subject_keywords": self.subject_entry.text(),
"link_keywords": self.link_entry.text(),
"max_emails": self.max_emails_entry.text()
},
"settings": {
"pause_time": self.outlook_handler.pause_time,
"links_count": self.outlook_handler.links_count,
"reward_value": self.outlook_handler.reward_value,
"validity": {
"enabled": self.validity_checkbox.isChecked(),
"days": int(
self.validity_entry.text()) if self.validity_checkbox.isChecked() and self.validity_entry.text() else None
}
}
}
if self.mailbox_listbox.currentItem():
state["mailbox"] = self.mailbox_listbox.currentItem().text()
if self.folder_listbox.currentItem():
state["folder"] = self.folder_listbox.currentItem().text()
return state
def reset_search_criteria(self):
"""Réinitialise tous les champs de critères de recherche"""
self.sender_entry.clear()
self.subject_entry.clear()
self.link_entry.clear()
self.max_emails_entry.clear()
self.reward_entry.clear()
# Réinitialiser aussi les cases à cocher
if hasattr(self, 'validity_checkbox'):
self.validity_checkbox.setChecked(False)
if hasattr(self, 'validity_entry'):
self.validity_entry.clear()
self.validity_entry.setVisible(False)
if hasattr(self, 'auth_checkbox'):
self.auth_checkbox.setChecked(False)
# Indication visuelle
self.status_label.setText("Critères de recherche réinitialisés")
self.log_action("✨ Tous les critères de recherche ont été réinitialisés")
def apply_profile(self, profile_data):
"""Applique un profil chargé"""
try:
if "search_criteria" in profile_data:
self.apply_search_criteria(profile_data["search_criteria"])
if "settings" in profile_data:
self.apply_settings(profile_data["settings"])
# Appliquer les paramètres de validité
if "validity" in profile_data["settings"]:
validity_settings = profile_data["settings"]["validity"]
self.validity_checkbox.setChecked(validity_settings.get("enabled", False))
if validity_settings.get("enabled"):
self.validity_entry.setText(str(validity_settings.get("days", "")))
self.validity_entry.setVisible(True)
else:
self.validity_entry.clear()
self.validity_entry.setVisible(False)
if "mailbox" in profile_data:
self.select_mailbox(profile_data["mailbox"])
if "folder" in profile_data:
self.select_folder(profile_data["folder"])
self.fields_empty = False
self.reset_reload_button.setText("Réinitialiser")
self.last_profile_data = profile_data # Sauvegarder ce profil comme dernier utilisé
self.log_action("Profil appliqué avec succès")
return True
except Exception as e:
self.log_action(f"Erreur lors de l'application du profil: {str(e)}")
return False
def load_profile(self, item):
try:
profile_name = self.profile_list.currentText()
if not profile_name:
return
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Impossible de charger le profil : {str(e)}")
def update_links_clicked(self, increment=1):
self.links_clicked += increment
if self.reward_value is not None:
self.total_reward = self.reward_value * self.links_clicked
self.links_clicked_updated.emit(self.links_clicked)
def update_links_clicked_display(self, count):
self.status_label.setText(f"Liens traités : {count}")
self.update_reward_display()
QApplication.processEvents()
def on_start_button_clicked(self):
"""
Gère le clic sur 'Démarrer le scan' en fonction du toggle
(direct → start_scan, indirect → start_scan_cached).
"""
# On récupère le flag depuis MainWindow (populé dans setup_interface)
if getattr(self, 'main_window', None) and self.main_window.use_cached_processing:
# Mode indirect : appelle start_scan_cached()
self.start_scan_cached()
else:
# Mode direct : appelle start_scan() existant
self.start_scan()
def start_scan(self):
"""Démarre le scan avec garantie de sauvegarde complète des logs"""
# === VÉRIFICATIONS PRÉLIMINAIRES ===
if self.link_mode != "url":
self.log_action("🔁 Redirection vers le scan pattern (mode activé)")
self.start_scan_pattern()
return
if not self.validate_inputs_for_scan():
self.log_action("Validation des entrées pour le scan échouée")
return
link_keywords = self.link_entry.text().split() if hasattr(self, 'link_entry') and self.link_entry.text() else []
# === INITIALISATION DU SCAN ET CAPTURE DES LOGS ===
print("=== DÉBUT INITIALISATION SCAN ===")
# Marquer le début du scan et initialiser l'historique
self.current_scan_active = True
self.scan_log_history = []
print(f"INIT: scan_log_history initialisé, taille: {len(self.scan_log_history)}")
# Définir le type de journal
self.set_log_type("Traitement Mail")
# Créer immédiatement le nom du journal avec horodatage
from datetime import datetime
current_time = datetime.now().strftime("%d/%m/%Y %H:%M")
self.current_log_name = f"Traitement Mail - {current_time}"
print(f"INIT: Journal nommé: {self.current_log_name}")
# === RÉINITIALISATION DES DONNÉES ===
self.report_data = []
self.deleted_emails = []
self.outlook_handler.links_clicked = 0
self.outlook_handler.links_skipped = 0
# Effacer le contenu du QTextEdit
self.log_text.clear()
self.log_action("🚀 Nouveau traitement démarré", 'email')
# Nettoyage initial avant le scan
self.cleanup_browser_resources()
QTimer.singleShot(500, lambda: None) # Pause pour laisser le nettoyage se faire
# === CONFIGURATION DU SYSTÈME DE LOG ===
self.log_categories = {
'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True},
'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True},
'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True},
'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True},
'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True}
}
# === INITIALISATION DE L'INTERFACE ===
self.is_scanning = True
self.set_scanning_state(True)
self.status_label.setText("Scan en cours...")
self.progress_bar.setValue(0)
try:
# === RÉCUPÉRATION DES PARAMÈTRES ===
mailbox = self.mailbox_listbox.currentItem().text()
folder = self.folder_listbox.currentItem().text()
sender = self.sender_entry.text()
subject_keywords = self.subject_entry.text().split()
# Vérification du mode URL sans mots-clés
if self.link_mode == "url" and not link_keywords:
self.log_action("⚠️ Aucun mot-clé de lien saisi — bascule en mode Keyword Automatique")
# Bascule vers mode pattern
self.link_mode = "pattern"
# Bascule visuelle du bouton toggle
if hasattr(self, 'toggle_click_mode_button'):
self.toggle_click_mode_button.setChecked(True)
self.toggle_click_mode_button.setText("Mode : Pattern")
# Masquer le champ de lien
if hasattr(self, 'link_entry'):
self.link_entry.setVisible(False)
# Sauvegarder le journal actuel avant de basculer
self.current_scan_active = False
self.auto_save_complete_scan_log()
# Lancer la méthode pattern et quitter
self.start_scan_pattern()
return
max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None
# === CALCUL DE LA DURÉE ESTIMÉE ===
avg_pause_time = (self.outlook_handler.min_pause_time + self.outlook_handler.max_pause_time) / 2
avg_between_time = (
self.outlook_handler.min_between_pages_time + self.outlook_handler.max_between_pages_time) / 2
estimated_duration = max_emails * (avg_pause_time + avg_between_time) if max_emails else 0
# Initialiser et démarrer le timer
self.remaining_time = int(estimated_duration)
self.scan_timer.start(1000)
# === CONNEXION DES SIGNAUX ===
self.outlook_handler.scan_progress.connect(self.update_progress)
if hasattr(self.outlook_handler, 'cycle_status_update'):
self.outlook_handler.cycle_status_update.connect(self.update_cycle_status)
# === RÉINITIALISATION DES COMPTEURS ===
self.outlook_handler.links_clicked = 0
self.outlook_handler.links_skipped = 0
# === AFFICHAGE DES PARAMÈTRES DE CYCLE ===
if hasattr(self.outlook_handler,
'cycle_processing_enabled') and self.outlook_handler.cycle_processing_enabled:
self.log_action(
f"🔄 Configuration cycles: {self.outlook_handler.min_emails_per_cycle}-{self.outlook_handler.max_emails_per_cycle} emails par cycle")
self.log_action(
f"⏱️ Temps de pause: {self.outlook_handler.min_pause_between_cycles}-{self.outlook_handler.max_pause_between_cycles} secondes")
# === PARTAGE CRITIQUE DE L'HISTORIQUE AVEC OUTLOOK_HANDLER ===
print(f"PARTAGE: Avant partage, scan_log_history taille: {len(self.scan_log_history)}")
self.outlook_handler.scan_log_history = self.scan_log_history
self.outlook_handler.current_scan_active = True
print("PARTAGE: Historique partagé avec outlook_handler")
print(
f"PARTAGE: outlook_handler.current_scan_active = {getattr(self.outlook_handler, 'current_scan_active', 'NON_DÉFINI')}")
# === LANCEMENT DU SCAN ===
self.log_action(f"🎯 Début du scan : {max_emails} emails maximum", 'system')
print("SCAN: Appel de outlook_handler.scan_emails")
clicked_links, matching_emails = self.outlook_handler.scan_emails(
mailbox, folder, sender, subject_keywords, link_keywords, max_emails
)
print(f"SCAN: Terminé - clicked_links={clicked_links}, matching_emails={matching_emails}")
# === TRAITEMENT IMMÉDIAT DE FIN DE SCAN ===
if self.is_scanning:
self.log_action("🏁 Scan terminé avec succès", 'success')
self.status_label.setText("Scan terminé.")
self.count_label.setText(f"Nombre d'emails correspondants: {matching_emails}")
self.update_reward_display()
# *** POINT CRITIQUE : ARRÊTER LA CAPTURE ET SAUVEGARDER IMMÉDIATEMENT ***
print("SAUVEGARDE: Arrêt de la capture des logs")
self.current_scan_active = False
# Récupérer les logs depuis outlook_handler si nécessaire
if hasattr(self.outlook_handler, 'scan_log_history') and len(
self.outlook_handler.scan_log_history) > len(self.scan_log_history):
print(
f"RÉCUPÉRATION: Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
print(f"SAUVEGARDE: scan_log_history final contient {len(self.scan_log_history)} entrées")
# Sauvegarder immédiatement AVANT tout reset
try:
print("SAUVEGARDE: Début sauvegarde immédiate")
result = self.auto_save_complete_scan_log()
print(f"SAUVEGARDE: Résultat = {result}")
if result:
print("SAUVEGARDE: ✅ SUCCÈS - Journal sauvegardé avec horodatage")
else:
print("SAUVEGARDE: ❌ ÉCHEC - Aucun fichier créé")
except Exception as save_error:
print(f"SAUVEGARDE: ❌ ERREUR: {save_error}")
import traceback
traceback.print_exc()
else:
self.log_action("⛔ Scan arrêté par l'utilisateur", 'system')
self.status_label.setText("Scan arrêté.")
# Même traitement pour scan arrêté
self.current_scan_active = False
# Récupérer les logs depuis outlook_handler
if hasattr(self.outlook_handler, 'scan_log_history') and len(
self.outlook_handler.scan_log_history) > len(self.scan_log_history):
print(
f"RÉCUPÉRATION (arrêt): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
# Sauvegarder même en cas d'arrêt
try:
print("SAUVEGARDE (arrêt): Début sauvegarde")
result = self.auto_save_complete_scan_log()
print(f"SAUVEGARDE (arrêt): Résultat = {result}")
except Exception as save_error:
print(f"SAUVEGARDE (arrêt): ❌ ERREUR: {save_error}")
except Exception as e:
# === GESTION DES ERREURS ===
self.log_action(f"❌ Erreur lors du scan : {str(e)}", 'error')
self.status_label.setText("Erreur lors du scan")
print(f"ERREUR SCAN: {str(e)}")
import traceback
traceback.print_exc()
# Sauvegarder même en cas d'erreur
self.current_scan_active = False
# Récupérer les logs depuis outlook_handler
if hasattr(self.outlook_handler, 'scan_log_history') and len(self.outlook_handler.scan_log_history) > len(
self.scan_log_history):
print(
f"RÉCUPÉRATION (erreur): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
try:
print("SAUVEGARDE (erreur): Début sauvegarde d'urgence")
result = self.auto_save_complete_scan_log()
print(f"SAUVEGARDE (erreur): Résultat = {result}")
except Exception as save_error:
print(f"SAUVEGARDE (erreur): ❌ ERREUR CRITIQUE: {save_error}")
finally:
# === NETTOYAGE ET RÉINITIALISATION ===
print("FINALLY: Début du nettoyage")
# Arrêter le timer
if hasattr(self.main_window, 'stop_scan_timer'):
self.main_window.stop_scan_timer()
# Déconnecter les signaux
try:
self.outlook_handler.scan_progress.disconnect(self.update_progress)
except:
pass
if hasattr(self.outlook_handler, 'cycle_status_update'):
try:
self.outlook_handler.cycle_status_update.disconnect(self.update_cycle_status)
except:
pass
# Réinitialiser l'état de scanning
self.is_scanning = False
self.set_scanning_state(False)
self.progress_bar.setValue(100)
# *** IMPORTANT : LA SAUVEGARDE A DÉJÀ ÉTÉ FAITE AVANT CE POINT ***
print("FINALLY: Nettoyage terminé - Journal déjà sauvegardé")
print("=== FIN MÉTHODE START_SCAN ===")
def _collect_links(self, mailbox_name, folder_names, sender, subject_keywords, link_keywords, max_emails):
"""
Récupère en différé tous les liens URL d'après les mêmes critères que start_scan.
Version améliorée avec recherche récursive dans les dossiers Gmail.
"""
import re
try:
# Vérifier la connexion Outlook
if not self.outlook_handler.outlook:
# Forcer une reconnexion si nécessaire
self.log_action("🔄 Vérification de la connexion Outlook...")
# Tentative de reconnexion avec retry
for attempt in range(3):
if self.outlook_handler.connect():
break
self.log_action(f"⚠️ Tentative de reconnexion {attempt + 1}/3...")
import time
time.sleep(2)
else:
self.log_action("❌ Impossible de se connecter à Outlook")
return []
# 1) Connexion et accès aux dossiers
self.log_action("📡 Accès à la boîte mail et aux dossiers...")
mapi = self.outlook_handler.safe_rpc_call(lambda: self.outlook_handler.outlook)
self.log_action("🔌 Namespace MAPI récupéré avec succès")
if not mapi:
self.log_action("❌ Impossible d'accéder au namespace MAPI")
return []
self.log_action(f"📮 Tentative d'accès à la boîte mail : {mailbox_name}")
root = self.outlook_handler.safe_rpc_call(lambda: mapi.Folders[mailbox_name])
if not root:
self.log_action(f"❌ Impossible d'accéder à la boîte mail: {mailbox_name}")
return []
# === DIAGNOSTIC APPROFONDI DES DOSSIERS ===
self.log_action("🔍 Diagnostic complet des dossiers disponibles...")
all_folders_map = {} # Dictionnaire pour stocker tous les dossiers trouvés
try:
# Parcourir les dossiers principaux
for i, folder_item in enumerate(root.Folders):
folder_name = folder_item.Name
all_folders_map[folder_name] = folder_item
if i < 5: # Afficher seulement les 5 premiers dossiers principaux
self.log_action(f"📁 Dossier principal : '{folder_name}'")
# Explorer les sous-dossiers de tous les dossiers principaux
try:
sub_folders = list(folder_item.Folders)
if len(sub_folders) > 0:
self.log_action(f"📂 Exploration des sous-dossiers de '{folder_name}'...")
for j, sub_folder in enumerate(sub_folders):
sub_name = sub_folder.Name
# Ajouter les sous-dossiers avec différentes stratégies de nommage
full_path = f"{folder_name}/{sub_name}"
all_folders_map[sub_name] = sub_folder # Nom simple
all_folders_map[full_path] = sub_folder # Nom avec chemin complet
if j < 6: # Afficher les 6 premiers sous-dossiers
self.log_action(f" 📁 Sous-dossier : '{sub_name}'")
if len(sub_folders) > 6:
self.log_action(f" 📁 ... et {len(sub_folders) - 6} autres sous-dossiers")
except Exception as subfolder_error:
# Pas d'erreur critique si pas de sous-dossiers
pass
total_main = len(list(root.Folders))
if total_main > 5:
self.log_action(f"📁 ... et {total_main - 5} autres dossiers principaux")
except Exception as diag_error:
self.log_action(f"❌ Erreur diagnostic général : {diag_error}")
# === RECHERCHE ET ACCÈS AUX DOSSIERS DEMANDÉS ===
items = []
self.log_action(f"🎯 Recherche du dossier : '{folder_names[0]}'")
for name in folder_names:
clean_name = name.strip()
found_folder = None
# Stratégie de recherche multiple pour tous les fournisseurs
search_attempts = [
clean_name, # Nom exact nettoyé
name # Nom original (avec espaces éventuels)
]
# Ajouter des tentatives avec les chemins des dossiers principaux
for main_folder in ["[Gmail]", "INBOX", "Inbox", "Boîte de réception"]:
if main_folder in all_folders_map:
search_attempts.append(f"{main_folder}/{clean_name}")
self.log_action(f"🔍 Recherche multi-fournisseur pour : '{clean_name}'")
for attempt_name in search_attempts:
if attempt_name in all_folders_map:
found_folder = all_folders_map[attempt_name]
self.log_action(f"✅ Dossier trouvé via : '{attempt_name}'")
break
# Si pas trouvé dans le mapping, essayer l'accès direct
if not found_folder:
try:
self.log_action(f"🔄 Tentative accès direct : '{clean_name}'")
found_folder = self.outlook_handler.safe_rpc_call(lambda: root.Folders[clean_name])
if found_folder:
self.log_action(f"✅ Dossier trouvé par accès direct")
except Exception as direct_error:
self.log_action(f"❌ Échec accès direct : {direct_error}")
# Traitement du dossier trouvé
if found_folder:
try:
self.log_action(f"📂 Ouverture du dossier...")
folder_items = self.outlook_handler.safe_rpc_call(lambda: found_folder.Items)
if folder_items:
folder_items_list = list(folder_items)
items.extend(folder_items_list)
self.log_action(f"📧 Collecté {len(folder_items_list)} emails du dossier")
else:
self.log_action(f"📭 Dossier vide")
except Exception as folder_error:
self.log_action(f"❌ Erreur lecture dossier : {folder_error}")
continue
else:
# Affichage des dossiers disponibles pour aide
self.log_action(f"❌ Dossier '{clean_name}' introuvable")
self.log_action("💡 Dossiers disponibles contenant des mots-clés similaires :")
# Recherche fuzzy pour suggestions
suggestions = []
search_lower = clean_name.lower()
for available_name in all_folders_map.keys():
if any(word in available_name.lower() for word in search_lower.split() if len(word) > 2):
suggestions.append(available_name)
if suggestions:
for suggestion in suggestions[:3]: # Max 3 suggestions
self.log_action(f" 💡 Suggestion : '{suggestion}'")
else:
self.log_action(" 💡 Aucune suggestion trouvée")
if not items:
self.log_action("❌ Aucun email trouvé dans les dossiers spécifiés")
return []
self.log_action(f"📊 Total emails collectés pour analyse : {len(items)}")
# === EXTRACTION DES LIENS AVEC FILTRAGE ===
self.log_action("🔗 Début de l'extraction des liens...")
collected = []
count = 0
emails_processed = 0
emails_with_links = 0
for msg in items:
if max_emails and emails_processed >= max_emails:
self.log_action(f"⏹️ Limite de {max_emails} emails atteinte")
break
emails_processed += 1
try:
# Vérifier les critères de l'email avant d'extraire les liens
if not self.outlook_handler.should_process_message(msg, sender, subject_keywords):
continue
# Affichage de progression tous les 10 emails
if emails_processed % 10 == 0:
self.log_action(f"📈 Progression : {emails_processed}/{len(items)} emails analysés")
# Extraire le corps du message de manière sécurisée
body = ''
try:
if hasattr(msg, 'Body') and msg.Body:
body = str(msg.Body)
elif hasattr(msg, 'HTMLBody') and msg.HTMLBody:
body = str(msg.HTMLBody)
except Exception as e:
self.log_action(f"⚠️ Erreur lecture corps email {emails_processed}: {str(e)}")
continue
if not body:
continue
# Nettoyer le corps et limiter la taille
body = body[:10000] # Limiter à 10000 caractères
# Compter les liens trouvés dans cet email
links_in_this_email = 0
# Extraire tous les liens du corps
for match in re.finditer(r'http[s]?://[^\s<>"\']+', body):
try:
url = match.group().strip().rstrip('<>"\'')
# Filtrage par mots-clés si spécifiés
if link_keywords:
if not any(kw.lower() in url.lower() for kw in link_keywords):
continue
# Éviter les liens suspects
exclude_keywords = ['delete', 'unsubscribe', 'remove', 'opt-out']
if any(ex_kw.lower() in url.lower() for ex_kw in exclude_keywords):
continue
# Éviter les doublons
if url not in collected:
collected.append(url)
count += 1
links_in_this_email += 1
except Exception as e:
self.log_action(f"⚠️ Erreur traitement lien: {str(e)}")
continue
# Compter les emails contenant au moins un lien
if links_in_this_email > 0:
emails_with_links += 1
except Exception as e:
self.log_action(f"❌ Erreur traitement email {emails_processed}: {str(e)}")
continue
# === RAPPORT FINAL ===
self.log_action(f"✅ Extraction terminée :")
self.log_action(f" 📊 {emails_processed} emails analysés")
self.log_action(f" 📧 {emails_with_links} emails contenant des liens")
self.log_action(f" 🔗 {count} liens uniques collectés")
return collected
except Exception as e:
self.log_action(f"❌ Erreur globale dans _collect_links: {str(e)}")
import traceback
self.log_action(f"❌ Détails : {traceback.format_exc()[-200:]}") # Derniers 200 caractères
return []
def start_scan_cached(self):
"""
Traitement différé des liens avec vérification HTTP+JS et garantie de sauvegarde complète des logs.
Reprend la logique de start_scan avec validation 100% fiable des liens.
"""
import requests
import psutil
import os
import json
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl, QEventLoop, QTimer
# === VÉRIFICATIONS PRÉLIMINAIRES ===
if not self.validate_inputs_for_scan():
self.log_action("❌ Validation des entrées pour le scan échouée")
return
link_keywords = self.link_entry.text().split() if hasattr(self, 'link_entry') and self.link_entry.text() else []
# === INITIALISATION DU SCAN ET CAPTURE DES LOGS ===
print("=== DÉBUT INITIALISATION SCAN CACHED ===")
# Marquer le début du scan et initialiser l'historique
self.current_scan_active = True
self.scan_log_history = []
print(f"INIT: scan_log_history initialisé, taille: {len(self.scan_log_history)}")
# Définir le type de journal
self.set_log_type("Traitement Mail Cached")
# Créer immédiatement le nom du journal avec horodatage
from datetime import datetime
current_time = datetime.now().strftime("%d/%m/%Y %H:%M")
self.current_log_name = f"Traitement Mail Cached - {current_time}"
print(f"INIT: Journal nommé: {self.current_log_name}")
# === RÉINITIALISATION DES DONNÉES ===
self.report_data = []
self.deleted_emails = []
self.outlook_handler.links_clicked = 0
self.outlook_handler.links_skipped = 0
# Effacer le contenu du QTextEdit
self.log_text.clear()
self.log_action("💾 Nouveau traitement différé démarré", 'email')
# === CONFIGURATION DU SYSTÈME DE LOG ===
self.log_categories = {
'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True},
'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True},
'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True},
'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True},
'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True}
}
# === INITIALISATION DE L'INTERFACE ===
self.is_scanning = True
self.set_scanning_state(True)
self.status_label.setText("Scan cached en cours...")
self.progress_bar.setValue(0)
try:
# === RÉCUPÉRATION DES PARAMÈTRES ===
mailbox = self.mailbox_listbox.currentItem().text()
folder = self.folder_listbox.currentItem().text()
sender = self.sender_entry.text()
subject_keywords = self.subject_entry.text().split()
max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None
# === PARTAGE CRITIQUE DE L'HISTORIQUE AVEC OUTLOOK_HANDLER ===
print(f"PARTAGE: Avant partage, scan_log_history taille: {len(self.scan_log_history)}")
self.outlook_handler.scan_log_history = self.scan_log_history
self.outlook_handler.current_scan_active = True
print("PARTAGE: Historique partagé avec outlook_handler")
# === PHASE 1 : COLLECTE DES LIENS ===
self.log_action("🗂️ Phase 1 : Collecte des liens depuis les emails...", 'system')
# CORRECTION: Utiliser self._collect_links au lieu de self.outlook_handler._collect_links
all_links = self._collect_links(
mailbox, [folder], sender, subject_keywords, link_keywords, max_emails
)
self.log_action(f"📊 Collecté {len(all_links)} liens au total", 'count')
if not all_links:
self.log_action("⚠️ Aucun lien trouvé dans les emails", 'error')
self.current_scan_active = False
self.auto_save_complete_scan_log()
return
# === PHASE 2 : VÉRIFICATION DES LIENS ===
self.log_action("🔍 Phase 2 : Vérification HTTP+JS des liens...", 'system')
# Configuration mémoire (10% max)
memory_info = psutil.virtual_memory()
max_memory_bytes = memory_info.total * 0.1 # 10% de la RAM
process = psutil.Process(os.getpid())
# Préparation du dossier fallback
fallback_dir = r"C:\AUTOLOOK\Fallbackmemory"
fallback_file = os.path.join(fallback_dir, "validated_links.json")
try:
os.makedirs(fallback_dir, exist_ok=True)
self.log_action(f"📁 Dossier fallback créé/vérifié : {fallback_dir}", 'system')
except Exception as e:
self.log_action(f"❌ Erreur création dossier fallback : {e}", 'error')
fallback_file = None
# Initialisation d'une seule WebView
web_view = QWebEngineView()
web_view.setAttribute(Qt.WA_DontShowOnScreen, True)
validated_links = []
risky_links = []
self.log_action("🚦 Début de la vérification individuelle des liens...", 'system')
for i, link in enumerate(all_links):
if not self.is_scanning: # Vérifier si l'utilisateur a arrêté
break
# Mise à jour du progrès
progress = int((i / len(all_links)) * 50) # 50% pour la vérification
self.progress_bar.setValue(progress)
self.log_action(f"🔍 Vérification {i + 1}/{len(all_links)} : {link[:50]}...", 'system')
# Vérifier la mémoire
current_memory = process.memory_info().rss
if current_memory > max_memory_bytes:
self.log_action("⚠️ Seuil mémoire atteint, sauvegarde intermédiaire...", 'system')
# Sauvegarder les liens validés dans le fichier JSON
if fallback_file and validated_links:
try:
with open(fallback_file, 'w', encoding='utf-8') as f:
json.dump(validated_links, f, indent=2, ensure_ascii=False)
self.log_action(f"💾 {len(validated_links)} liens sauvegardés en JSON", 'success')
validated_links.clear() # Vider la mémoire
except Exception as e:
self.log_action(f"❌ Erreur sauvegarde JSON : {e}", 'error')
# === VÉRIFICATION HTTP ===
try:
response = requests.head(link, timeout=10, allow_redirects=True)
if response.status_code != 200:
risky_links.append(f"{link} (HTTP {response.status_code})")
self.log_action(f"⚠️ Lien HTTP non fiable : {response.status_code}", 'error')
continue
except Exception as e:
risky_links.append(f"{link} (HTTP Error: {str(e)})")
self.log_action(f"❌ Erreur HTTP : {str(e)[:30]}...", 'error')
continue
# === VÉRIFICATION JS ===
try:
loop = QEventLoop()
timeout_timer = QTimer()
timeout_timer.timeout.connect(loop.quit)
timeout_timer.setSingleShot(True)
timeout_timer.start(15000) # 15 secondes max
web_view.page().loadFinished.connect(loop.quit)
web_view.page().load(QUrl(link))
loop.exec_()
timeout_timer.stop()
# Vérifier les redirections vraiment dangereuses uniquement
final_url = web_view.page().url().toString()
# Liste des domaines vraiment suspects (à adapter selon vos besoins)
dangerous_domains = ['phishing', 'malware', 'virus', 'scam']
really_suspicious = any(danger in final_url.lower() for danger in dangerous_domains)
if really_suspicious:
risky_links.append(f"{link} (Redirection vers domaine suspect: {final_url})")
self.log_action("⚠️ Redirection vers domaine dangereux détectée", 'error')
continue
# Log informatif de la redirection (sans bloquer)
if final_url != link:
self.log_action(f"🔄 Redirection: {link[:30]}... → {final_url[:30]}...", 'system')
except Exception as e:
risky_links.append(f"{link} (JS Error: {str(e)})")
self.log_action(f"❌ Erreur JS : {str(e)[:30]}...", 'error')
continue
# Si on arrive ici, le lien est 100% fiable
validated_links.append(link)
self.log_action(f"✅ Lien validé avec succès", 'success')
# Nettoyer la WebView
web_view.deleteLater()
# === RÉCUPÉRATION DES LIENS DEPUIS LE FICHIER JSON SI NÉCESSAIRE ===
if fallback_file and os.path.exists(fallback_file):
try:
with open(fallback_file, 'r', encoding='utf-8') as f:
json_links = json.load(f)
validated_links.extend(json_links)
os.remove(fallback_file) # Nettoyer le fichier temporaire
self.log_action(f"🔄 {len(json_links)} liens récupérés depuis JSON", 'success')
except Exception as e:
self.log_action(f"❌ Erreur lecture JSON : {e}", 'error')
# === PHASE 3 : TRAITEMENT DES LIENS VALIDÉS ===
total_validated = len(validated_links)
total_risky = len(risky_links)
self.log_action(f"📊 Bilan vérification : {total_validated} fiables, {total_risky} à risque", 'count')
if risky_links:
self.log_action("⚠️ Liens écartés pour sécurité :", 'error')
for risky in risky_links[:5]: # Afficher seulement les 5 premiers
self.log_action(f" • {risky[:80]}...", 'error')
if len(risky_links) > 5:
self.log_action(f" • ... et {len(risky_links) - 5} autres", 'error')
if not validated_links:
self.log_action("❌ Aucun lien fiable à traiter", 'error')
else:
self.log_action(f"🚀 Phase 3 : Traitement de {total_validated} liens fiables...", 'system')
# Traitement des liens validés
for i, link in enumerate(validated_links):
if not self.is_scanning: # Vérifier si l'utilisateur a arrêté
break
# Mise à jour du progrès (50-100%)
progress = 50 + int((i / total_validated) * 50)
self.progress_bar.setValue(progress)
try:
# CORRECTION: Utiliser une méthode de traitement des liens plus robuste
if hasattr(self, '_process_validated_link'):
self._process_validated_link(link, i + 1, total_validated)
else:
self.log_action(f"⚠️ Méthode _process_validated_link non trouvée", 'error')
self.outlook_handler.links_skipped += 1
continue
self.outlook_handler.links_clicked += 1
self.log_action(f"🖱️ Lien traité : {link[:50]}...", 'success')
except Exception as e:
self.outlook_handler.links_skipped += 1
self.log_action(f"⚠️ Échec traitement : {str(e)[:50]}...", 'error')
# === TRAITEMENT IMMÉDIAT DE FIN DE SCAN ===
if self.is_scanning:
self.log_action("🏁 Scan cached terminé avec succès", 'success')
self.status_label.setText("Scan cached terminé.")
self.count_label.setText(f"Liens traités: {self.outlook_handler.links_clicked}")
self.update_reward_display()
# *** POINT CRITIQUE : ARRÊTER LA CAPTURE ET SAUVEGARDER IMMÉDIATEMENT ***
print("SAUVEGARDE: Arrêt de la capture des logs")
self.current_scan_active = False
# Récupérer les logs depuis outlook_handler si nécessaire
if hasattr(self.outlook_handler, 'scan_log_history') and len(
self.outlook_handler.scan_log_history) > len(self.scan_log_history):
print(
f"RÉCUPÉRATION: Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
print(f"SAUVEGARDE: scan_log_history final contient {len(self.scan_log_history)} entrées")
# Sauvegarder immédiatement AVANT tout reset
try:
print("SAUVEGARDE: Début sauvegarde immédiate")
result = self.auto_save_complete_scan_log()
print(f"SAUVEGARDE: Résultat = {result}")
if result:
print("SAUVEGARDE: ✅ SUCCÈS - Journal sauvegardé avec horodatage")
else:
print("SAUVEGARDE: ❌ ÉCHEC - Aucun fichier créé")
except Exception as save_error:
print(f"SAUVEGARDE: ❌ ERREUR: {save_error}")
import traceback
traceback.print_exc()
else:
self.log_action("⛔ Scan cached arrêté par l'utilisateur", 'system')
self.status_label.setText("Scan cached arrêté.")
# Même traitement pour scan arrêté
self.current_scan_active = False
# Récupérer les logs depuis outlook_handler
if hasattr(self.outlook_handler, 'scan_log_history') and len(
self.outlook_handler.scan_log_history) > len(self.scan_log_history):
print(
f"RÉCUPÉRATION (arrêt): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
# Sauvegarder même en cas d'arrêt
try:
print("SAUVEGARDE (arrêt): Début sauvegarde")
result = self.auto_save_complete_scan_log()
print(f"SAUVEGARDE (arrêt): Résultat = {result}")
except Exception as save_error:
print(f"SAUVEGARDE (arrêt): ❌ ERREUR: {save_error}")
except Exception as e:
# === GESTION DES ERREURS ===
self.log_action(f"❌ Erreur lors du scan cached : {str(e)}", 'error')
self.status_label.setText("Erreur lors du scan cached")
print(f"ERREUR SCAN CACHED: {str(e)}")
import traceback
traceback.print_exc()
# Sauvegarder même en cas d'erreur
self.current_scan_active = False
# Récupérer les logs depuis outlook_handler
if hasattr(self.outlook_handler, 'scan_log_history') and len(self.outlook_handler.scan_log_history) > len(
self.scan_log_history):
print(
f"RÉCUPÉRATION (erreur): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
try:
print("SAUVEGARDE (erreur): Début sauvegarde d'urgence")
result = self.auto_save_complete_scan_log()
print(f"SAUVEGARDE (erreur): Résultat = {result}")
except Exception as save_error:
print(f"SAUVEGARDE (erreur): ❌ ERREUR CRITIQUE: {save_error}")
finally:
# === NETTOYAGE ET RÉINITIALISATION ===
print("FINALLY: Début du nettoyage cached")
# Arrêter le timer
if hasattr(self.main_window, 'stop_scan_timer'):
self.main_window.stop_scan_timer()
# Réinitialiser l'état de scanning
self.is_scanning = False
self.set_scanning_state(False)
self.progress_bar.setValue(100)
print("FINALLY: Nettoyage cached terminé - Journal déjà sauvegardé")
print("=== FIN MÉTHODE START_SCAN_CACHED ===")
def _process_validated_link(self, link, current_index, total_links):
"""
Version optimisée qui injecte du contenu au lieu de charger des URLs
"""
try:
from datetime import datetime
# Utiliser update_browser qui injecte maintenant du contenu
self.update_browser(link)
# Mise à jour de la console
if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'console_header'):
console = self.mini_browser.console_header
console.show()
console.update_page_time(f"Email {current_index}/{total_links}")
# Enregistrer comme traité
if hasattr(self.outlook_handler,
'duplicates_detection_enabled') and self.outlook_handler.duplicates_detection_enabled:
if hasattr(self.outlook_handler, 'links_handler'):
self.outlook_handler.links_handler.add_processed_link(
link,
metadata={
'processed_date': datetime.now().isoformat(),
'method': 'content_injection'
}
)
self.outlook_handler.update_links_clicked()
self.log_action(f"✅ Email traité: {link[:30]}...")
except Exception as e:
self.log_action(f"❌ Erreur traitement: {str(e)}")
raise
def start_scan_pattern(self):
"""Démarre un scan d'e-mails en mode pattern uniquement (analyse du corps du mail)."""
# Validation des entrées pour le scan pattern
if not self.mailbox_listbox.currentItem():
self.log_action("❌ Aucune boîte mail sélectionnée")
return
if not self.folder_listbox.selectedItems():
self.log_action("❌ Aucun dossier sélectionné")
return
# Réinitialiser les données
self.report_data = []
self.deleted_emails = []
self.outlook_handler.links_clicked = 0
self.outlook_handler.links_skipped = 0
# Effacer le contenu des logs
self.log_text.clear()
self.log_action("🚀 Nouveau traitement démarré (mode PATTERN)")
# Préparer l'interface
self.is_scanning = True
self.set_scanning_state(True)
self.status_label.setText("Scan en cours...")
self.progress_bar.setValue(0)
try:
# Récupérer les paramètres
selected_mailbox = self.mailbox_listbox.currentItem()
selected_folders = [item.text() for item in self.folder_listbox.selectedItems()]
sender = self.sender_entry.text()
subject_keywords = self.subject_entry.text().split()
max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None
self.log_action(f"📂 Boîte sélectionnée : {selected_mailbox.text()}")
self.log_action(f"📁 Dossiers sélectionnés : {selected_folders}")
# En mode pattern, utiliser les mots-clés prédéfinis depuis OutlookHandler
link_keywords = self.outlook_handler.pattern_keywords if hasattr(self.outlook_handler,
'pattern_keywords') else []
self.log_action(f"🔍 Mots-clés (pattern) depuis fichier : {link_keywords}")
# Si aucun mot-clé pattern n'est chargé, essayer de les recharger
if not link_keywords:
self.log_action("⚠️ Aucun mot-clé pattern trouvé, rechargement...")
self.outlook_handler.load_pattern_keywords()
link_keywords = self.outlook_handler.pattern_keywords
self.log_action(f"🔄 Mots-clés rechargés : {link_keywords}")
# Calcul de la durée estimée
avg_pause_time = (self.outlook_handler.min_pause_time + self.outlook_handler.max_pause_time) / 2
avg_between_time = (
self.outlook_handler.min_between_pages_time + self.outlook_handler.max_between_pages_time) / 2
estimated_duration = max_emails * (avg_pause_time + avg_between_time) if max_emails else 0
self.remaining_time = int(estimated_duration)
self.scan_timer.start(1000)
# Connecter les signaux nécessaires
self.outlook_handler.scan_progress.connect(self.update_progress)
# Vérifier la connexion Outlook
if not self.outlook_handler.ensure_connected():
self.log_action("❌ Connexion Outlook non établie")
return
# Initialiser le compteur
self.outlook_handler.reset_scan_stats()
# Lancer le scan en mode pattern
clicked_links, matching_emails = self.outlook_handler.scan_emails_pattern(
mailbox_name=selected_mailbox.text(),
folder_names=selected_folders,
sender=sender,
subject_keywords=subject_keywords,
link_keywords=link_keywords,
max_emails=max_emails
)
# Mise à jour de l'interface après scan
if self.is_scanning:
self.status_label.setText("Scan terminé.")
self.count_label.setText(f"Nombre d'emails correspondants: {matching_emails}")
self.update_reward_display()
else:
self.status_label.setText("Scan arrêté.")
self.log_action("✅ Scan pattern terminé")
except Exception as e:
self.log_action(f"❌ Erreur lors du scan : {str(e)}")
self.status_label.setText("Erreur lors du scan")
finally:
# Nettoyage final
if hasattr(self.main_window, 'stop_scan_timer'):
self.main_window.stop_scan_timer()
try:
self.outlook_handler.scan_progress.disconnect(self.update_progress)
except:
pass
self.is_scanning = False
self.set_scanning_state(False)
self.progress_bar.setValue(100)
def lock_windows_session(self):
"""Verrouille la session Windows en utilisant plusieurs méthodes."""
self.log_action("Tentative de verrouillage de la session Windows...")
# Méthode 1: Utilisation directe de la commande Windows
try:
import subprocess
self.log_action("Méthode 1: Utilisation de rundll32.exe directement")
subprocess.call('rundll32.exe user32.dll,LockWorkStation')
return # Si cette méthode fonctionne, on s'arrête là
except Exception as e:
self.log_action(f"Échec méthode 1: {str(e)}")
# Méthode 2: Utilisation de ctypes
try:
import ctypes
self.log_action("Méthode 2: Utilisation de ctypes.windll.user32.LockWorkStation")
ctypes.windll.user32.LockWorkStation()
return # Si cette méthode fonctionne, on s'arrête là
except Exception as e:
self.log_action(f"Échec méthode 2: {str(e)}")
# Méthode 3: Utilisation d'un script VBS
try:
import tempfile
import os
self.log_action("Méthode 3: Utilisation d'un script VBS")
# Créer un fichier temporaire pour le script VBS
fd, path = tempfile.mkstemp(suffix='.vbs')
try:
with os.fdopen(fd, 'w') as f:
f.write('Set WshShell = CreateObject("WScript.Shell")\n')
f.write('WshShell.Run "rundll32.exe user32.dll,LockWorkStation", 0, False\n')
# Exécuter le script avec une priorité élevée
os.system(f'cscript "{path}"')
self.log_action(f"Script VBS exécuté: {path}")
finally:
# Supprimer le fichier temporaire
try:
os.unlink(path)
except Exception as e:
self.log_action(f"Impossible de supprimer le fichier temporaire: {e}")
except Exception as e:
self.log_action(f"Échec méthode 3: {str(e)}")
# Méthode 4: Utiliser une commande pour Windows 10/11
try:
import os
self.log_action("Méthode 4: Utilisation de la commande de verrouillage Windows 10/11")
os.system('powershell -command "(New-Object -ComObject Shell.Application).WindowsSecurity()"')
except Exception as e:
self.log_action(f"Échec méthode 4: {str(e)}")
def shutdown_windows(self):
"""Programme l'extinction de Windows."""
try:
import os
os.system('shutdown /s /t 30 /c "Extinction automatique programmée par l\'application."')
self.log_action("Commande d'extinction exécutée avec délai de 30 secondes")
except Exception as e:
self.log_action(f"Erreur lors de la programmation de l'extinction: {str(e)}")
def process_next_email(self):
if not self.emails_to_process:
self.finish_scan()
return
email = self.emails_to_process.pop(0)
# Traiter l'email ici
sender = email.SenderEmailAddress
subject = email.Subject
body = email.Body
# Trouver un lien valide dans l'email
link = self.outlook_handler.find_valid_link(email, self.link_keywords)
if link:
self.log_action(f"Traitement de l'email de {sender}: {subject}")
self.update_browser(link)
# Supprimer l'email après traitement
try:
if hasattr(self, 'permanent_delete_enabled') and hasattr(self,
'apply_to_scan') and self.permanent_delete_enabled and self.apply_to_scan:
# Suppression définitive
email.Delete(0) # Le paramètre 0 signifie suppression définitive
self.log_action(f"Email supprimé définitivement : {subject}")
else:
# Suppression normale (vers la corbeille)
email.Delete()
self.log_action(f"Email déplacé vers la corbeille : {subject}")
except Exception as e:
self.log_action(f"Erreur lors de la suppression de l'email : {str(e)}")
# Mettre à jour les compteurs
self.outlook_handler.update_links_clicked()
self.update_links_clicked_display(self.outlook_handler.links_clicked)
# Programmer le traitement du prochain email
QTimer.singleShot(self.outlook_handler.pause_time * 1000, self.process_next_email)
else:
self.log_action(f"Aucun lien valide trouvé dans l'email : {subject}")
self.process_next_email() # Passer directement à l'email suivant
def get_emails_to_process(self):
emails = []
mailbox = self.mailbox_listbox.currentItem().text()
folder = self.folder_listbox.currentItem().text()
sender = self.sender_entry.text()
subject_keywords = self.subject_entry.text().split()
max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None
try:
outlook = self.outlook_handler.outlook
root_folder = next((f for f in outlook.Folders if f.Name == mailbox), None)
if not root_folder:
raise Exception(f"Boîte mail '{mailbox}' non trouvée")
target_folder = self.outlook_handler.find_folder(root_folder, folder)
if not target_folder:
raise Exception(f"Dossier '{folder}' non trouvé dans '{mailbox}'")
messages = target_folder.Items
messages.Sort("[ReceivedTime]", True)
for message in list(messages):
if self.outlook_handler.should_process_message(message, sender, subject_keywords):
emails.append(message)
if max_emails and len(emails) >= max_emails:
break
self.log_action(f"{len(emails)} emails trouvés pour traitement")
return emails
except Exception as e:
self.log_action(f"Erreur lors de la récupération des emails : {str(e)}")
return []
def _display_summary(self, summary_text):
"""Affiche le résumé avec un compte à rebours"""
self.log_text.clear()
self.log_text.append("=== RÉSUMÉ DU TRAITEMENT ===\n")
self.log_text.append(summary_text)
countdown = 5 # Durée en secondes
self.summary_timer = QTimer(self)
self.summary_countdown = countdown
self.summary_timer.timeout.connect(self.update_summary_timer)
self.summary_timer.start(1000) # Mise à jour chaque seconde
def update_summary_timer(self):
"""Met à jour le compte à rebours du résumé"""
self.summary_countdown -= 1
if self.summary_countdown > 0:
# Mettre à jour uniquement la dernière ligne
current_text = self.log_text.toPlainText()
new_text = current_text.rsplit('\n', 1)[0]
# self.log_text.setText(f"{new_text}\nCe résumé s'effacera dans {self.summary_countdown} secondes...")
else:
self.summary_timer.stop()
self.log_text.clear()
self.log_action("✅ Journal réinitialisé")
def finish_scan(self):
print("Début de finish_scan")
try:
# AJOUT : Marquer la fin du scan
self.current_scan_active = False
self.log_action("🏁 Scan terminé - Fin de l'enregistrement")
# DOUBLE SAUVEGARDE pour sécurité maximale :
# 1. Sauvegarder l'historique complet du scan (priorité)
scan_file = self.auto_save_scan_with_timestamp()
# 2. Sauvegarder aussi ce qui est visible (backup)
visible_file = self.auto_save_log()
# Log de confirmation
if scan_file and visible_file:
self.log_action("💾 Double sauvegarde effectuée (historique + visible)")
elif scan_file:
self.log_action("💾 Historique complet sauvegardé")
elif visible_file:
self.log_action("💾 Journal visible sauvegardé")
# AJOUT : Marquer la fin du scan
self.current_scan_active = False
self.log_action("🏁 Scan terminé - Fin de l'enregistrement")
# AJOUT : Sauvegarder automatiquement le journal complet du scan
self.auto_save_scan_with_timestamp()
self.is_scanning = False
self.set_scanning_state(False)
# Calculer les statistiques
if hasattr(self, 'outlook_handler'):
total_processed = self.outlook_handler.links_clicked
total_skipped = self.outlook_handler.links_skipped
total_reward = self.outlook_handler.total_reward
else:
total_processed = total_skipped = total_reward = 0
# Mettre à jour l'interface
if hasattr(self, 'progress_bar'):
self.progress_bar.setValue(100)
if hasattr(self, 'status_label'):
self.status_label.setText("Scan terminé")
self.update_reward_display()
# Vérifier les doublons (utiliser l'attribut de l'outlook_handler)
doublons_count = getattr(self.outlook_handler, 'doublons_count', 0)
if doublons_count > 0:
logger.info(self._format_log_message(f"🔍 Total des doublons détectés : {doublons_count}", is_duplicate=False))
# Réinitialiser le compteur pour le prochain scan
if hasattr(self.outlook_handler, 'doublons_count'):
self.outlook_handler.doublons_count = 0
if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'bottom_banner'):
banner = self.mini_browser.bottom_banner
if hasattr(banner, 'setText'):
banner.setText("En attente de traitement")
elif hasattr(banner, 'show_banner'):
banner.show_banner("En attente de traitement")
banner.show()
# NOUVEAU CODE: Rediriger les navigateurs vers une page d'accueil
self.reset_browsers_to_homepage()
# NOUVEAU CODE: Rediriger les navigateurs vers une page d'accueil
self.reset_browsers_to_homepage()
# Créer le résumé formaté
summary = f"""
RÉSUMÉ DU TRAITEMENT
📧 Emails traités : {total_processed:<15}
⏩ Emails ignorés : {total_skipped:<15}
🔍 Doublons détectés : {doublons_count:<15}
💰 Récompense : {total_reward:.2f}
⏱️ {QDateTime.currentDateTime().toString("dd/MM/yyyy HH:mm:ss")}
"""
# === PRÉSERVATION DE L'HISTORIQUE AVANT EFFACEMENT AUTOMATIQUE ===
try:
# S'assurer que l'historique complet est sauvegardé AVANT l'effacement automatique
if hasattr(self, 'scan_log_history') and self.scan_log_history:
# Sauvegarder dans les journaux consultables
if not hasattr(self, 'saved_journals'):
self.saved_journals = {}
# Créer une copie de l'historique pour consultation
from datetime import datetime
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M")
preservation_key = f"Traitement Mail - {timestamp}"
# Convertir l'historique en HTML pour la consultation
html_content = ""
for entry in self.scan_log_history:
html_content += f"{entry}
"
html_content += ""
# Sauvegarder pour consultation via "Consulter journal"
self.saved_journals[preservation_key] = html_content
self.current_log_name = preservation_key
print(f"DEBUG: Historique préservé pour consultation - {len(self.scan_log_history)} entrées")
except Exception as e:
print(f"ERREUR: Problème lors de la préservation de l'historique: {e}")
# L'effacement automatique va avoir lieu, mais l'historique est préservé
# Forcer la mise à jour immédiate du log et afficher le résumé
# AJOUT OU MODIFICATION DE CETTE LIGNE :
QTimer.singleShot(100, lambda: self.show_summary_with_timer(summary))
# Générer et afficher le rapport si demandé
if hasattr(self.outlook_handler, 'main_window') and \
self.outlook_handler.main_window.report_toggle.isChecked():
report = self.generate_report()
dialog = ReportDialog(report, self)
dialog.exec_()
self.start_log_clear_timer()
except Exception as e:
print(f"Erreur dans finish_scan: {e}")
import traceback
traceback.print_exc()
finally:
print("Fin de finish_scan")
self.is_scanning = False
self.set_scanning_state(False)
def auto_save_scan_with_timestamp(self):
"""Sauvegarde automatique du journal de scan avec horodatage pour le menu déroulant"""
try:
from datetime import datetime
# Créer un nom avec horodatage
current_time = datetime.now()
timestamp = current_time.strftime("%d/%m/%Y %H:%M")
journal_name = f"Traitement Mail - {timestamp}"
# Récupérer le contenu du journal complet du scan
if hasattr(self, 'scan_log_history') and self.scan_log_history:
# Convertir l'historique en HTML formaté
html_content = ""
for entry in self.scan_log_history:
timestamp_str = entry.get('timestamp', '')
message = entry.get('message', '')
category = entry.get('category', 'system')
# Récupérer la couleur selon la catégorie
color = '#87CEEB' # couleur par défaut
if hasattr(self, 'log_categories') and category in self.log_categories:
color = self.log_categories[category]['color']
html_content += f"{timestamp_str} {message}
"
html_content += ""
else:
# Fallback : récupérer le contenu visible du journal
html_content = self.log_text.toHtml()
# Initialiser le stockage si nécessaire
if not hasattr(self, 'saved_journals'):
self.saved_journals = {}
# Sauvegarder dans le dictionnaire des journaux
self.saved_journals[journal_name] = html_content
# Mettre à jour le nom du journal actuel
self.current_log_name = journal_name
# Log de confirmation
self.log_action(f"💾 Journal sauvegardé : {journal_name}")
return journal_name
except Exception as e:
self.log_action(f"❌ Erreur sauvegarde horodatée: {str(e)}")
return None
def stop_operation(self):
if self.is_scanning:
self.log_action("Arrêt du scan demandé")
self.is_scanning = False
self.outlook_handler.stop_scan()
self.status_label.setText("Arrêt du scan en cours...")
# AJOUT: Remettre le message par défaut sur la bannière
if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'bottom_banner'):
banner = self.mini_browser.bottom_banner
if hasattr(banner, 'setText'):
banner.setText("En attente de traitement")
elif hasattr(banner, 'show_banner'):
banner.show_banner("En attente de traitement")
banner.show()
def reset_browsers_to_homepage(self):
"""Redirige les navigateurs vers une page d'accueil après la fin du scan"""
try:
homepage_url = "https://www.snoown2klabs.com" # Page d'accueil par défaut (vide)
# Rediriger le mini navigateur
if hasattr(self, 'mini_browser') and self.mini_browser:
mini_tab = self.mini_browser.get_current_tab()
if mini_tab and hasattr(mini_tab, 'web_view'):
mini_tab.web_view.load(QUrl(homepage_url))
self.log_action("Mini navigateur redirigé vers la page d'accueil")
# MODIFICATION: S'assurer que la bannière affiche le message par défaut
if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'bottom_banner'):
banner = self.mini_browser.bottom_banner
if hasattr(banner, 'setText'):
banner.setText("En attente de traitement")
elif hasattr(banner, 'show_banner'):
banner.show_banner("En attente de traitement")
banner.show()
# Rediriger le navigateur principal s'il est visible
if hasattr(self, 'browser_window') and self.browser_window and \
hasattr(self.browser_window, 'isVisible') and self.browser_window.isVisible():
main_tab = self.browser_window.get_current_tab()
if main_tab and hasattr(main_tab, 'web_view'):
main_tab.web_view.load(QUrl(homepage_url))
self.log_action("Navigateur principal redirigé vers la page d'accueil")
# Effacer l'URL de la console
if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'console_header'):
self.mini_browser.console_header.update_page_time("Prêt")
if hasattr(self, 'browser_window') and hasattr(self.browser_window, 'console_header'):
self.browser_window.console_header.update_page_time("Prêt")
except Exception as e:
self.log_action(f"Erreur lors de la redirection vers la page d'accueil: {str(e)}")
def show_summary_with_timer(self, summary_text, duration_seconds=15):
"""
Affiche un résumé de fin de scan qui disparaît après le délai spécifié
avec un compteur pour montrer le temps restant
IMPORTANT : Cette méthode efface SEULEMENT l'affichage principal (log_text)
et préserve tous les journaux sauvegardés accessibles via "Consulter journal"
Args:
summary_text (str): Le texte du résumé à afficher
duration_seconds (int): Durée d'affichage en secondes
"""
try:
# AVANT d'effacer l'affichage, s'assurer que l'historique est sauvegardé
if hasattr(self, 'scan_log_history') and self.scan_log_history:
print(f"DEBUG: Préservation de {len(self.scan_log_history)} entrées d'historique")
# Effacer SEULEMENT le contenu de l'affichage principal
self.log_text.clear()
# Ajouter l'entête et le résumé
self.log_text.append(f"--- RÉSUMÉ DU SCAN ---
")
self.log_text.append(summary_text)
# Initialiser le compteur
self.countdown_remaining = duration_seconds
self.log_text.append(
f"Ce résumé disparaîtra dans {self.countdown_remaining} secondes...
")
self.log_text.append(
f"💡 L'historique complet reste accessible via 'Consulter / Sauvegarder le journal'
")
# Faire défiler vers le haut pour s'assurer que tout est visible
self.log_text.verticalScrollBar().setValue(0)
# Créer un timer pour le compte à rebours
if hasattr(self, 'countdown_timer'):
self.countdown_timer.stop()
self.countdown_timer = QTimer(self)
self.countdown_timer.timeout.connect(self.update_countdown)
self.countdown_timer.start(1000) # Mise à jour chaque seconde
# Créer un timer pour la réinitialisation finale
if hasattr(self, 'summary_timer'):
self.summary_timer.stop()
self.summary_timer = QTimer(self)
self.summary_timer.setSingleShot(True)
self.summary_timer.timeout.connect(self.reset_log_after_summary)
self.summary_timer.start(duration_seconds * 1000)
# Forcer la mise à jour de l'interface
QApplication.processEvents()
except Exception as e:
print(f"Erreur lors de l'affichage du résumé temporaire: {str(e)}")
import traceback
traceback.print_exc()
# En cas d'erreur, forcer la réinitialisation du journal principal uniquement
if hasattr(self, 'log_text'):
self.log_text.clear()
self.log_action("❌ Erreur lors de l'affichage du résumé - Affichage principal réinitialisé")
def update_countdown(self):
"""Met à jour le compteur de temps restant dans le résumé SANS défilement"""
try:
# Décrémenter le compteur
self.countdown_remaining -= 1
if self.countdown_remaining > 0:
# Récupérer le contenu actuel
text = self.log_text.toPlainText()
lines = text.split('\n')
# Remplacer seulement la dernière ligne si c'est un message de décompte
if lines and "disparaîtra dans" in lines[-1]:
content = '\n'.join(lines[:-1])
self.log_text.clear()
self.log_text.append(content)
# ✅ UTILISER la nouvelle méthode log_action avec auto_scroll=False
self.log_action(f"Ce résumé disparaîtra dans {self.countdown_remaining} secondes...",
auto_scroll=False)
else:
# Si pas de ligne de décompte existante, ajouter directement
self.log_action(f"Ce résumé disparaîtra dans {self.countdown_remaining} secondes...",
auto_scroll=False)
else:
# Arrêter le timer quand le compteur atteint zéro
self.countdown_timer.stop()
# ✅ NE PAS forcer la mise à jour pour éviter le défilement
# QApplication.processEvents() # ← CETTE LIGNE EST COMMENTÉE/SUPPRIMÉE
except Exception as e:
print(f"Erreur lors de la mise à jour du compteur: {str(e)}")
self.countdown_timer.stop()
def reset_log_after_summary(self):
"""Réinitialise SEULEMENT l'affichage principal après le résumé automatique (fin de traitement)"""
try:
# S'assurer que les timers sont arrêtés
if hasattr(self, 'summary_timer'):
self.summary_timer.stop()
if hasattr(self, 'countdown_timer'):
self.countdown_timer.stop()
# Effacer UNIQUEMENT l'affichage principal (log_text)
# L'historique reste accessible via "Consulter journal"
self.log_text.clear()
# Message de confirmation que l'historique est préservé
self.log_action("✅ Résumé terminé - Journal réinitialisé")
self.log_action("💡 Historique complet disponible via 'Consulter / Sauvegarder le journal'")
# Forcer la mise à jour de l'interface
QApplication.processEvents()
except Exception as e:
print(f"Erreur lors de la réinitialisation du journal: {str(e)}")
import traceback
traceback.print_exc()
# En cas d'erreur, effacer quand même l'affichage principal
if hasattr(self, 'log_text'):
self.log_text.clear()
self.log_action(f"❌ Erreur de réinitialisation: {str(e)}")
def create_widgets(self):
"""Méthode create_widgets() simplifiée pour widget enfant"""
try:
# === CONTENEUR PRINCIPAL SIMPLE ===
content_container = QWidget()
content_layout = QVBoxLayout(content_container)
content_layout.setContentsMargins(5, 5, 5, 5)
content_layout.setSpacing(5)
# === EN-TÊTE COMPACT ===
self._create_compact_header(content_layout)
# === SPLITTER PRINCIPAL ===
content_splitter = QSplitter(Qt.Horizontal)
content_splitter.setChildrenCollapsible(False)
# === PANNEAU GAUCHE ===
left_panel = self.create_mail_and_folder_section()
left_panel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
content_splitter.addWidget(left_panel)
# === PANNEAU CENTRAL ===
central_panel = self._create_simple_central_panel()
content_splitter.addWidget(central_panel)
# === PANNEAU DROIT ===
right_panel = self._create_simple_right_panel()
content_splitter.addWidget(right_panel)
# === PROPORTIONS SIMPLES ===
content_splitter.setSizes([300, 400, 400])
content_splitter.setStretchFactor(0, 1)
content_splitter.setStretchFactor(1, 2)
content_splitter.setStretchFactor(2, 2)
# === ASSEMBLAGE ===
content_layout.addWidget(content_splitter)
self.main_interface_layout.addWidget(content_container)
# === INITIALISATION SIMPLE ===
if hasattr(self, 'connection_indicator') and hasattr(self, 'detected_connection_mode'):
if self.detected_connection_mode:
self.connection_indicator.set_mode(self.detected_connection_mode)
self.log_action("✅ Interface widget enfant créée avec succès")
except Exception as e:
self.log_action(f"❌ Erreur création interface: {e}")
def _create_compact_browser_controls(self):
"""Crée des contrôles de navigateur compacts"""
try:
controls_container = QWidget()
controls_container.setFixedHeight(35)
controls_layout = QHBoxLayout(controls_container)
controls_layout.setContentsMargins(3, 3, 3, 3)
controls_layout.setSpacing(5)
# === ZOOM COMPACT ===
zoom_container = QWidget()
zoom_layout = QHBoxLayout(zoom_container)
zoom_layout.setContentsMargins(0, 0, 0, 0)
zoom_layout.setSpacing(2)
self.zoom_out_button = QPushButton("-")
self.zoom_reset_button = QPushButton("🔄")
self.zoom_in_button = QPushButton("+")
button_style = """
QPushButton {
background-color: #008CBA;
color: white;
border-radius: 2px;
font-size: 11px;
font-weight: bold;
}
QPushButton:hover { background-color: #007B9A; }
"""
for button in [self.zoom_out_button, self.zoom_reset_button, self.zoom_in_button]:
button.setFixedSize(20, 20)
button.setStyleSheet(button_style)
zoom_layout.addWidget(button)
controls_layout.addWidget(zoom_container)
# === TIMER COMPACT ===
self.timer_widget = TimerWidget(self)
self.timer_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.timer_widget.set_text("----", "waiting")
controls_layout.addWidget(self.timer_widget)
# === BOUTON AGRANDIR COMPACT ===
self.toggle_browser_button = QPushButton("AGRANDIR")
self.toggle_browser_button.setFixedSize(70, 20)
self.toggle_browser_button.setStyleSheet(button_style)
controls_layout.addWidget(self.toggle_browser_button)
# Connecter les signaux
self.zoom_in_button.clicked.connect(self.zoom_in)
self.zoom_out_button.clicked.connect(self.zoom_out)
self.zoom_reset_button.clicked.connect(self.reset_zoom)
self.toggle_browser_button.clicked.connect(self.toggle_browser_view)
return controls_container
except Exception as e:
self.log_action(f"❌ Erreur contrôles navigateur: {e}")
return QWidget()
def _create_compact_log_buttons(self, parent_layout):
"""Crée des boutons de journal compacts"""
try:
button_container = QWidget()
button_container.setFixedHeight(30)
button_layout = QHBoxLayout(button_container)
button_layout.setContentsMargins(0, 0, 0, 0)
button_layout.setSpacing(3)
button_style = """
QPushButton {
background-color: #3c4c5c;
color: white;
border-radius: 2px;
font-size: 10px;
padding: 2px;
}
QPushButton:hover { background-color: #4c5c6c; }
"""
self.toggle_log_button = QPushButton("Désactiver")
self.toggle_log_button.setCheckable(True)
self.toggle_log_button.setStyleSheet(button_style)
self.toggle_log_button.clicked.connect(self.toggle_logging)
button_layout.addWidget(self.toggle_log_button)
self.view_log_button = QPushButton("Consulter")
self.view_log_button.setStyleSheet(button_style + "color: #00ff00;")
self.view_log_button.clicked.connect(self.show_log_dialog)
button_layout.addWidget(self.view_log_button)
self.clear_log_button = QPushButton("Effacer")
self.clear_log_button.setStyleSheet(button_style)
self.clear_log_button.clicked.connect(self.clear_log_text)
button_layout.addWidget(self.clear_log_button)
parent_layout.addWidget(button_container)
except Exception as e:
self.log_action(f"❌ Erreur boutons journal: {e}")
def _optimize_section_layouts(self):
"""Optimise les layouts internes des sections"""
try:
sections = [
(self.search_criteria_group, "Critères"),
(self.control_group, "Contrôles"),
(self.progress_group, "Progression")
]
for section, name in sections:
if section and section.layout():
layout = section.layout()
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(2)
self.log_action(f"🔧 Layout {name} optimisé")
except Exception as e:
self.log_action(f"❌ Erreur optimisation layouts: {e}")
def _calculate_scale_factor(self, screen_width, screen_height):
"""Calcule un facteur d'échelle basé sur la résolution"""
try:
# Résolution de référence : 1920x1080
base_width = 1920
base_height = 1080
# Calculer les ratios
width_ratio = screen_width / base_width
height_ratio = screen_height / base_height
# Prendre le ratio moyen, avec un minimum de 1.0
scale_factor = max(1.0, (width_ratio + height_ratio) / 2)
self.log_action(f"📏 Facteur d'échelle calculé: {scale_factor:.2f}")
return scale_factor
except Exception as e:
self.log_action(f"❌ Erreur calcul échelle: {e}")
return 1.0
def _configure_splitter(self, splitter, screen_width, scale_factor):
"""Configure les proportions du splitter selon l'écran"""
try:
# Proportions selon la largeur d'écran
if screen_width >= 3000: # Très haute résolution 4K+
sizes = [600, 900, 900]
elif screen_width >= 2560: # 2K+
sizes = [500, 750, 750]
elif screen_width >= 1920:
sizes = [350, 500, 500]
elif screen_width >= 1680:
sizes = [300, 450, 450]
elif screen_width >= 1366:
sizes = [280, 400, 400]
else:
sizes = [250, 350, 350]
splitter.setSizes(sizes)
# Stretch factors équilibrés
splitter.setStretchFactor(0, 1) # Gauche
splitter.setStretchFactor(1, 2) # Centre (plus d'espace)
splitter.setStretchFactor(2, 2) # Droite (plus d'espace)
self.log_action(f"📐 Splitter configuré: {sizes}")
except Exception as e:
self.log_action(f"❌ Erreur configuration splitter: {e}")
def _create_compact_header(self, parent_layout):
"""Crée un en-tête compact"""
try:
header_container = QWidget()
header_container.setFixedHeight(40)
header_layout = QHBoxLayout(header_container)
header_layout.setContentsMargins(5, 2, 5, 2)
header_layout.setSpacing(10)
# Voyant de connexion
self.connection_indicator = ConnectionIndicator()
header_layout.addWidget(self.connection_indicator)
# Titre centré
self.title_label = QLabel("Tableau de bord : CREDITS")
self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
header_layout.addWidget(self.title_label)
# Bouton toggle panneau
self.toggle_panel_button = QPushButton("≡")
self.toggle_panel_button.setToolTip("Afficher/Masquer le panneau de sélection")
self.toggle_panel_button.setFixedSize(25, 25)
self.toggle_panel_button.setStyleSheet("""
QPushButton {
background-color: #03699c;
color: white;
border-radius: 2px;
font-size: 12px;
font-weight: bold;
}
QPushButton:hover { background-color: #007B9A; }
""")
self.toggle_panel_button.clicked.connect(self.toggle_mail_folder_panel)
header_layout.addWidget(self.toggle_panel_button)
parent_layout.addWidget(header_container)
except Exception as e:
self.log_action(f"❌ Erreur création en-tête: {e}")
def _create_simple_central_panel(self):
"""Crée le panneau central avec des hauteurs fixes simples"""
try:
central_panel = QWidget()
central_panel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
central_layout = QVBoxLayout(central_panel)
central_layout.setContentsMargins(2, 2, 2, 2)
central_layout.setSpacing(5)
# === SECTION CRITÈRES (hauteur fixe) ===
self.search_criteria_group = self.create_search_criteria_section()
self.search_criteria_group.setFixedHeight(180)
central_layout.addWidget(self.search_criteria_group)
# === SECTION CONTRÔLES (hauteur fixe) ===
self.control_group = self.create_control_section()
self.control_group.setFixedHeight(120)
central_layout.addWidget(self.control_group)
# === SECTION PROGRESSION (extensible) ===
self.progress_group = self.create_progress_section()
self.progress_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
central_layout.addWidget(self.progress_group, 1) # Prend l'espace restant
return central_panel
except Exception as e:
self.log_action(f"❌ Erreur panneau central: {e}")
return QWidget()
def _create_simple_right_panel(self):
"""Crée le panneau droit avec des hauteurs simples"""
try:
right_panel = QWidget()
right_panel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(2, 2, 2, 2)
right_layout.setSpacing(5)
# === MINI NAVIGATEUR ===
browser_group = QGroupBox("Mini Navigateur")
browser_group.setFixedHeight(450) # Hauteur augmentée pour laisser plus de place au Timer
browser_layout = QVBoxLayout(browser_group)
browser_layout.setContentsMargins(3, 3, 3, 3)
browser_layout.setSpacing(2)
# Navigateur web
self.mini_browser = BrowserWindow(is_mini=True)
self.mini_browser.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
browser_layout.addWidget(self.mini_browser)
# Contrôles du navigateur
controls_container = self._create_compact_browser_controls()
browser_layout.addWidget(controls_container)
right_layout.addWidget(browser_group)
# === JOURNAL ===
log_group = QGroupBox("Journal")
log_group.setFixedHeight(150) # Hauteur fixe réduite pour le journal
log_layout = QVBoxLayout(log_group)
log_layout.setContentsMargins(3, 3, 3, 3)
log_layout.setSpacing(2)
# Boutons du journal
self._create_compact_log_buttons(log_layout)
# Zone de texte du journal
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
log_layout.addWidget(self.log_text)
right_layout.addWidget(log_group)
return right_panel
except Exception as e:
self.log_action(f"❌ Erreur panneau droit: {e}")
return QWidget()
def setup_responsive_system(self):
"""Initialise le système de responsivité avancé"""
try:
# Timer pour gérer les redimensionnements
self.resize_timer = QTimer()
self.resize_timer.setSingleShot(True)
self.resize_timer.timeout.connect(self.on_delayed_resize)
# Stocker la taille initiale
self.previous_size = self.size()
self.log_action("🎯 Système de responsivité avancé initialisé")
except Exception as e:
self.log_action(f"❌ Erreur init responsivité: {e}")
def on_delayed_resize(self):
"""Gère le redimensionnement avec délai"""
try:
# Vérifier à nouveau les débordements après redimensionnement
QTimer.singleShot(50, self.check_initial_overflow)
except Exception as e:
self.log_action(f"❌ Erreur redimensionnement retardé: {e}")
def handle_window_resize(self, event):
"""
Gère le redimensionnement de la fenêtre pour maintenir les proportions
et adapter l'interface selon le mode (fenêtré/plein écran)
"""
try:
# Vérifier que l'événement est valide
if not hasattr(event, 'size'):
return
from PyQt5.QtWidgets import QApplication
new_size = event.size()
is_fullscreen = new_size.width() > 1600 and new_size.height() > 900
# Reste du code inchangé...
# === AJUSTEMENT DES PROPORTIONS DU SPLITTER ===
if hasattr(self, 'content_splitter'):
current_sizes = self.content_splitter.sizes()
total_width = sum(current_sizes)
if is_fullscreen:
# Mode plein écran : donner plus d'espace au panneau droit (navigateur)
new_sizes = [
int(total_width * 0.20), # 20% pour le panneau gauche
int(total_width * 0.35), # 35% pour le panneau central
int(total_width * 0.45) # 45% pour le panneau droit
]
else:
# Mode fenêtré : proportions équilibrées
new_sizes = [
int(total_width * 0.30), # 30% pour le panneau gauche
int(total_width * 0.35), # 35% pour le panneau central
int(total_width * 0.35) # 35% pour le panneau droit
]
self.content_splitter.setSizes(new_sizes)
# Force la mise à jour de l'affichage
QApplication.processEvents()
self.log_action(f"🔄 Interface redimensionnée : {new_size.width()}x{new_size.height()}")
except Exception as e:
self.log_action(f"❌ Erreur redimensionnement: {e}")
def adapt_font_sizes_to_window(self, window_size):
"""
Adapte la taille des polices en fonction de la taille de la fenêtre
"""
try:
# Calculer un facteur d'échelle basé sur la largeur
base_width = 1200 # Largeur de référence
scale_factor = window_size.width() / base_width
scale_factor = max(0.8, min(scale_factor, 1.5)) # Limiter entre 80% et 150%
# Ajuster la taille des polices principales
if hasattr(self, 'title_label'):
font = self.title_label.font()
base_size = 16
new_size = max(12, int(base_size * scale_factor))
font.setPointSize(new_size)
self.title_label.setFont(font)
# Adapter les autres éléments de l'interface
if hasattr(self, 'dashboard_label'):
font = self.dashboard_label.font()
base_size = 12
new_size = max(10, int(base_size * scale_factor))
font.setPointSize(new_size)
self.dashboard_label.setFont(font)
except Exception as e:
self.log_action(f"❌ Erreur adaptation polices: {e}")
def toggle_fullscreen_mode(self):
"""
Bascule entre le mode fenêtré et plein écran avec adaptation complète de l'interface
"""
try:
from PyQt5.QtCore import QTimer
if hasattr(self, 'main_window'):
was_fullscreen = self.main_window.isFullScreen()
if was_fullscreen:
self.main_window.showNormal()
self.log_action("🔳 Mode fenêtré activé")
# Forcer l'adaptation immédiate au mode fenêtré
QTimer.singleShot(200, self.adapt_to_windowed_mode)
else:
self.main_window.showFullScreen()
self.log_action("🔲 Mode plein écran activé")
# Forcer l'adaptation immédiate au mode plein écran
QTimer.singleShot(200, self.adapt_to_fullscreen_mode)
except Exception as e:
self.log_action(f"❌ Erreur basculement plein écran: {e}")
def adapt_to_fullscreen_mode(self):
"""
Adapte spécifiquement l'interface au mode plein écran
"""
try:
from PyQt5.QtWidgets import QDesktopWidget
from PyQt5.QtCore import QTimer
screen = QDesktopWidget().availableGeometry()
screen_height = screen.height()
# === ADAPTATION DU MINI-NAVIGATEUR ===
if hasattr(self, 'mini_browser'):
min_height = int(screen_height * 0.35)
max_height = int(screen_height * 0.65)
self.mini_browser.setMinimumHeight(min_height)
self.mini_browser.setMaximumHeight(max_height)
# === ADAPTATION DES SECTIONS CENTRALES ===
if hasattr(self, 'search_criteria_group'):
self.search_criteria_group.setMinimumHeight(int(screen_height * 0.25))
self.search_criteria_group.setMaximumHeight(16777215)
if hasattr(self, 'control_group'):
self.control_group.setMinimumHeight(int(screen_height * 0.20))
self.control_group.setMaximumHeight(16777215)
if hasattr(self, 'progress_group'):
self.progress_group.setMinimumHeight(int(screen_height * 0.35))
self.progress_group.setMaximumHeight(16777215)
# === ADAPTATION DU JOURNAL ===
if hasattr(self, 'log_text'):
self.log_text.setMinimumHeight(int(screen_height * 0.15))
# === AJUSTEMENT DES PROPORTIONS DU SPLITTER ===
if hasattr(self, 'content_splitter'):
QTimer.singleShot(100, self.adjust_splitter_for_fullscreen)
self.log_action("✨ Interface adaptée au mode plein écran")
except Exception as e:
self.log_action(f"❌ Erreur adaptation plein écran: {e}")
def adapt_to_windowed_mode(self):
"""
Adapte spécifiquement l'interface au mode fenêtré
"""
try:
from PyQt5.QtCore import QTimer
# === RETOUR AUX TAILLES STANDARD ===
if hasattr(self, 'mini_browser'):
self.mini_browser.setMinimumHeight(200)
self.mini_browser.setMaximumHeight(400)
if hasattr(self, 'search_criteria_group'):
self.search_criteria_group.setMinimumHeight(180)
if hasattr(self, 'control_group'):
self.control_group.setMinimumHeight(160)
if hasattr(self, 'progress_group'):
self.progress_group.setMinimumHeight(350)
if hasattr(self, 'log_text'):
self.log_text.setMinimumHeight(100)
# === AJUSTEMENT DES PROPORTIONS DU SPLITTER ===
if hasattr(self, 'content_splitter'):
QTimer.singleShot(100, self.adjust_splitter_for_windowed)
self.log_action("📐 Interface adaptée au mode fenêtré")
except Exception as e:
self.log_action(f"❌ Erreur adaptation fenêtré: {e}")
def adjust_splitter_for_fullscreen(self):
"""Ajuste les proportions du splitter pour le plein écran"""
try:
if hasattr(self, 'content_splitter'):
current_sizes = self.content_splitter.sizes()
total_width = sum(current_sizes)
new_sizes = [
int(total_width * 0.20), # 20% panneau gauche
int(total_width * 0.35), # 35% panneau central
int(total_width * 0.45) # 45% panneau droit
]
self.content_splitter.setSizes(new_sizes)
except Exception as e:
self.log_action(f"❌ Erreur ajustement splitter plein écran: {e}")
def adjust_splitter_for_windowed(self):
"""Ajuste les proportions du splitter pour le mode fenêtré"""
try:
if hasattr(self, 'content_splitter'):
current_sizes = self.content_splitter.sizes()
total_width = sum(current_sizes)
new_sizes = [
int(total_width * 0.30), # 30% panneau gauche
int(total_width * 0.35), # 35% panneau central
int(total_width * 0.35) # 35% panneau droit
]
self.content_splitter.setSizes(new_sizes)
except Exception as e:
self.log_action(f"❌ Erreur ajustement splitter fenêtré: {e}")
def show_log_dialog(self):
"""Boîte de dialogue journal avec filtres dynamiques"""
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit, QTextEdit, QPushButton, \
QCheckBox, QLabel
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
dialog = QDialog(self)
dialog.setWindowTitle("Journal d'Événements - Affichage Dynamique")
dialog.setMinimumSize(900, 700)
layout = QVBoxLayout(dialog)
# === EN-TÊTE AVEC SÉLECTION DE JOURNAL ===
header = QHBoxLayout()
# Sélection du journal précédent
header.addWidget(QLabel("Journal:"))
self.journal_combo = QComboBox()
self._load_previous_journals()
# Mettre à jour le menu avec le journal actuel si il existe
if hasattr(self, 'current_log_name') and self.current_log_name:
# Chercher et sélectionner le journal actuel dans le menu
for i in range(self.journal_combo.count()):
item_text = self.journal_combo.itemText(i)
if self.current_log_name in item_text:
self.journal_combo.setCurrentIndex(i)
break
self.journal_combo.currentTextChanged.connect(self._on_journal_changed)
header.addWidget(self.journal_combo)
# Nom du journal actuel (modifiable)
header.addWidget(QLabel("Nom:"))
self.name_edit = QLineEdit(getattr(self, 'current_log_name', 'Journal actuel'))
self.name_edit.textChanged.connect(self._on_name_changed)
header.addWidget(self.name_edit)
layout.addLayout(header)
# === FILTRES DYNAMIQUES ===
filters = QHBoxLayout()
filters.addWidget(QLabel("Afficher:"))
# Initialiser les catégories de log si elles n'existent pas
if not hasattr(self, 'log_categories'):
self.log_categories = {
'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True},
'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True},
'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True},
'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True},
'info': {'color': '#87CEEB', 'icon': 'ℹ️', 'enabled': True},
'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True}
}
self.dialog_checkboxes = {}
for cat_key, cat_info in self.log_categories.items():
checkbox = QCheckBox(f"{cat_info['icon']} {cat_key.title()}")
checkbox.setChecked(cat_info['enabled'])
checkbox.setStyleSheet(f"color: {cat_info['color']}; font-weight: bold;")
# CONNEXION DYNAMIQUE
checkbox.toggled.connect(lambda checked, key=cat_key: self._filter_log_dynamically(key, checked))
self.dialog_checkboxes[cat_key] = checkbox
filters.addWidget(checkbox)
# Boutons de filtrage rapide
all_btn = QPushButton("Tout")
all_btn.clicked.connect(self._select_all_categories)
filters.addWidget(all_btn)
none_btn = QPushButton("Rien")
none_btn.clicked.connect(self._deselect_all_categories)
filters.addWidget(none_btn)
errors_btn = QPushButton("Erreurs")
errors_btn.clicked.connect(self._show_only_errors)
filters.addWidget(errors_btn)
layout.addLayout(filters)
# === ZONE DE TEXTE DU JOURNAL ===
self.dialog_log_display = QTextEdit()
self.dialog_log_display.setReadOnly(True)
self.dialog_log_display.setFont(QFont("Consolas", 9))
# Charger le contenu initial
self._refresh_dialog_display()
layout.addWidget(self.dialog_log_display)
# === STATISTIQUES ===
self.stats_label = QLabel()
self._update_stats_display()
layout.addWidget(self.stats_label)
# === BOUTONS D'ACTION ===
buttons = QHBoxLayout()
save_btn = QPushButton("💾 Sauvegarder Journal")
save_btn.clicked.connect(self._save_current_journal)
buttons.addWidget(save_btn)
export_btn = QPushButton("📤 Exporter Filtré")
export_btn.clicked.connect(self._export_filtered_log)
buttons.addWidget(export_btn)
clear_btn = QPushButton("🗑️ Vider")
clear_btn.clicked.connect(self._clear_current_log)
buttons.addWidget(clear_btn)
buttons.addStretch()
close_btn = QPushButton("❌ Fermer")
close_btn.clicked.connect(dialog.accept)
buttons.addWidget(close_btn)
layout.addLayout(buttons)
dialog.exec_()
def _load_previous_journals(self):
"""Charge la liste des journaux précédents triés par date"""
try:
# Initialiser le stockage des journaux si nécessaire
if not hasattr(self, 'saved_journals'):
self.saved_journals = {}
# Vider le combo box
self.journal_combo.clear()
# Ajouter le journal actuel en premier
self.journal_combo.addItem("📝 Journal Actuel")
# Trier les journaux par ordre chronologique décroissant (plus récent en premier)
sorted_journals = []
for journal_name in self.saved_journals.keys():
try:
# Extraire la date du nom du journal
if " - " in journal_name:
date_part = journal_name.split(" - ", 1)[1]
from datetime import datetime
date_obj = datetime.strptime(date_part, "%d/%m/%Y %H:%M")
sorted_journals.append((date_obj, journal_name))
else:
# Pour les anciens journaux sans format de date
sorted_journals.append((datetime.min, journal_name))
except:
# En cas d'erreur de parsing, mettre à la fin
from datetime import datetime
sorted_journals.append((datetime.min, journal_name))
# Trier par date décroissante
sorted_journals.sort(key=lambda x: x[0], reverse=True)
# Ajouter les journaux triés
for date_obj, journal_name in sorted_journals:
self.journal_combo.addItem(f"📋 {journal_name}")
# Log du nombre de journaux chargés
self.log_action(f"📚 {len(sorted_journals)} journaux précédents chargés")
except Exception as e:
self.log_action(f"❌ Erreur chargement journaux: {str(e)}")
def _on_journal_changed(self, selected_name):
"""Met à jour l'affichage quand l'utilisateur sélectionne un journal"""
try:
if hasattr(self, 'saved_journals') and selected_name in self.saved_journals:
html = self.saved_journals[selected_name]
self.dialog_log_display.setHtml(html)
else:
self.dialog_log_display.clear()
# Met à jour aussi la zone de saisie du nom
self.name_edit.setText(selected_name)
# Réinitialise les filtres visuels selon le nouveau contenu
self._refresh_dialog_display()
self._update_stats_display()
except Exception as e:
print(f"❌ Erreur lors du changement de journal : {e}")
def _on_name_changed(self, new_name):
"""Gère le changement de nom du journal"""
self.current_log_name = new_name
def _filter_log_dynamically(self, category, enabled):
"""VERSION ULTRA-SIMPLE - Reconstruction complète"""
try:
# Mettre à jour le filtre
self.log_categories[category]['enabled'] = enabled
# NOUVELLE APPROCHE : Reconstruire ligne par ligne
self.dialog_log_display.clear()
# Récupérer le contenu texte ligne par ligne du journal principal
original_text = self.log_text.toPlainText()
for line in original_text.split('\n'):
if line.strip(): # Ignorer les lignes vides
# Vérifier si cette ligne doit être affichée
should_show = True
# Pour chaque catégorie désactivée
for cat_key, cat_info in self.log_categories.items():
if not cat_info['enabled'] and cat_info['icon'] in line:
should_show = False
break
if should_show:
# Reconstituer le HTML avec la bonne couleur
line_category = self._detect_line_category(line)
if line_category:
cat_info = self.log_categories[line_category]
colored_line = f'{line}
'
self.dialog_log_display.append(colored_line)
else:
# Ligne sans catégorie détectée
self.dialog_log_display.append(
f'{line}
')
except Exception as e:
print(f"ERREUR: {e}")
def _detect_line_category(self, line):
"""Détecte la catégorie d'une ligne"""
for cat_key, cat_info in self.log_categories.items():
if cat_info['icon'] in line:
return cat_key
return None
def _refresh_dialog_display(self):
"""Recharge le journal avec priorité sur l'historique sauvegardé"""
try:
# Priorité à l'historique sauvegardé même si l'affichage principal est vide
if hasattr(self, 'current_log_name') and hasattr(self, 'saved_journals'):
if self.current_log_name in self.saved_journals:
self.dialog_log_display.setHtml(self.saved_journals[self.current_log_name])
return
# Sinon utiliser l'historique du scan si disponible
if hasattr(self, 'scan_log_history') and self.scan_log_history:
html_content = ""
for entry in self.scan_log_history:
html_content += f"{entry}
"
html_content += ""
self.dialog_log_display.setHtml(html_content)
return
# En dernier recours, utiliser le contenu visible
self.dialog_log_display.setHtml(self.log_text.toHtml())
except Exception as e:
print(f"Erreur refresh: {e}")
def _update_stats_display(self):
"""Met à jour l'affichage des statistiques"""
try:
if not hasattr(self, 'stats_label'):
return
# Compter les messages par catégorie
content = self.dialog_log_display.toPlainText()
stats = {}
total = 0
for cat_key, cat_info in self.log_categories.items():
count = content.count(cat_info['icon'])
stats[cat_key] = count
if cat_info['enabled']:
total += count
# Créer le texte des statistiques
stats_text = f"Total affiché: {total} | "
for cat_key, count in stats.items():
if count > 0:
icon = self.log_categories[cat_key]['icon']
stats_text += f"{icon}{count} "
self.stats_label.setText(stats_text)
self.stats_label.setStyleSheet("color: #65ddfb; font-weight: bold; padding: 5px;")
except Exception as e:
print(f"Erreur stats: {e}")
def _select_all_categories(self):
"""Sélectionne toutes les catégories"""
for checkbox in self.dialog_checkboxes.values():
checkbox.setChecked(True)
def _deselect_all_categories(self):
"""Désélectionne toutes les catégories"""
for checkbox in self.dialog_checkboxes.values():
checkbox.setChecked(False)
def _show_only_errors(self):
"""Affiche uniquement les erreurs et avertissements"""
for cat_key, checkbox in self.dialog_checkboxes.items():
should_check = cat_key in ['error', 'system']
checkbox.setChecked(should_check)
def _save_current_journal(self):
"""Sauvegarde le journal actuel avec horodatage, dans la mémoire interne et la liste déroulante"""
try:
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import QDateTime
if not hasattr(self, 'log_text') or not self.log_text:
return
# Initialiser la structure de stockage si nécessaire
if not hasattr(self, 'saved_journals'):
self.saved_journals = {}
# Obtenir le contenu HTML actuel
current_content = self.log_text.toHtml()
if not current_content.strip():
return # Ne rien sauvegarder si le journal est vide
# Générer un nom horodaté lisible
timestamp = QDateTime.currentDateTime().toString("dd/MM HH:mm")
self.current_log_name = f"Journal - {timestamp}"
# Sauvegarder dans le dictionnaire mémoire
self.saved_journals[self.current_log_name] = current_content
# Ajouter à la liste déroulante s'il n'existe pas déjà
existing_items = [self.journal_combo.itemText(i) for i in range(self.journal_combo.count())]
if self.current_log_name not in existing_items:
self.journal_combo.addItem(self.current_log_name)
# Sélectionner automatiquement ce journal
self.journal_combo.setCurrentText(self.current_log_name)
QMessageBox.information(self.parent(), "Sauvegardé",
f"Journal '{self.current_log_name}' sauvegardé avec succès")
except Exception as e:
print(f"❌ Erreur lors de la sauvegarde du journal : {e}")
def _export_filtered_log(self):
"""Exporte le journal filtré"""
try:
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QDateTime
content = self.dialog_log_display.toPlainText()
filename = f"{self.current_log_name}_{QDateTime.currentDateTime().toString('yyyyMMdd_HHmm')}.txt"
filepath, _ = QFileDialog.getSaveFileName(self.parent(), "Exporter Journal Filtré", filename,
"Fichiers texte (*.txt)")
if filepath:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(f"=== {self.current_log_name} ===\n")
f.write(f"Exporté le: {QDateTime.currentDateTime().toString('dd/MM/yyyy HH:mm')}\n\n")
# Ajouter les filtres actifs
active_filters = [cat_key for cat_key, cat_info in self.log_categories.items() if
cat_info['enabled']]
f.write(f"Filtres actifs: {', '.join(active_filters)}\n")
f.write("=" * 50 + "\n\n")
f.write(content)
QMessageBox.information(self.parent(), "Exporté", f"Journal exporté vers:\n{filepath}")
except Exception as e:
from PyQt5.QtWidgets import QMessageBox
QMessageBox.warning(self.parent(), "Erreur", f"Erreur export: {e}")
def _clear_current_log(self):
"""Vide le journal actuel"""
try:
from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question(self.parent(), "Confirmer",
"Êtes-vous sûr de vouloir vider le journal actuel ?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
self.log_text.clear()
self.dialog_log_display.clear()
self._update_stats_display()
except Exception as e:
print(f"Erreur clear log: {e}")
def clear_log_text(self):
"""Efface le contenu du journal"""
try:
self.log_text.clear()
self.log_action("✅ Journal effacé manuellement")
except Exception as e:
print(f"Erreur clear log text: {e}")
# Méthodes nécessaires pour le fonctionnement des boutons de zoom et navigateur
def zoom_in(self):
"""Augmente le zoom du mini navigateur"""
try:
if hasattr(self, 'mini_browser'):
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
current_zoom = current_tab.web_view.zoomFactor()
new_zoom = min(current_zoom + 0.1, 2.0) # Maximum zoom 200%
current_tab.web_view.setZoomFactor(new_zoom)
zoom_percentage = int(new_zoom * 100)
self.log_action(f"🔍 Zoom augmenté à {zoom_percentage}%")
except Exception as e:
self.log_action(f"❌ Erreur zoom avant: {str(e)}")
def zoom_out(self):
"""Diminue le zoom du mini navigateur"""
try:
if hasattr(self, 'mini_browser'):
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
current_zoom = current_tab.web_view.zoomFactor()
new_zoom = max(current_zoom - 0.1, 0.3) # Minimum zoom 30%
current_tab.web_view.setZoomFactor(new_zoom)
zoom_percentage = int(new_zoom * 100)
self.log_action(f"🔍 Zoom réduit à {zoom_percentage}%")
except Exception as e:
self.log_action(f"❌ Erreur zoom arrière: {str(e)}")
def reset_zoom(self):
"""Réinitialise le zoom du mini navigateur à 30%"""
try:
if hasattr(self, 'mini_browser'):
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
default_zoom = 0.3
current_tab.web_view.setZoomFactor(default_zoom)
zoom_percentage = int(default_zoom * 100)
self.log_action(f"🔄 Zoom réinitialisé à {zoom_percentage}%")
except Exception as e:
self.log_action(f"❌ Erreur réinitialisation zoom: {str(e)}")
def create_mail_and_folder_section(self):
self.mail_folder_container = QWidget()
container_layout = QVBoxLayout(self.mail_folder_container)
container_layout.setContentsMargins(0, 0, 0, 0)
group = QGroupBox("Sélection boite mails et dossiers")
group.setMinimumHeight(420)
layout = QVBoxLayout(group)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
# --- Styles ---
list_style = """QListWidget { background-color: #03699c; border: 1px solid #dcdcdc; border-radius: 4px; font-size: 14px; color: white; } QListWidget::item { padding: 1px; height: 14px; color: white; } QListWidget::item:selected { background-color: #0078D7; }"""
input_style = """QLineEdit { background-color: #03699c; color: white; border: 1px solid #dcdcdc; border-radius: 4px; padding: 5px; font-size: 12px; min-height: 25px; }"""
label_style = """QLabel { color: white; font-size: 12px; padding-bottom: 2px; }"""
button_style = """QPushButton { background-color: #3ea6a3; color: white; border: none; border-radius: 2px; padding: 1px 2px; font-size: 9px; max-width: 45px; min-height: 18px; } QPushButton:hover { background-color: #007B9A; }"""
placeholder_style = """QLineEdit { background-color: #03699c; color: white; border: 1px solid #dcdcdc; border-radius: 4px; padding: 5px; } QLineEdit::placeholder { color: rgba(255, 255, 255, 0.7); font-style: italic; }"""
layout.addSpacing(15)
# --- Boîtes mail ---
self.mailbox_listbox = QListWidget()
self.mailbox_listbox.setMinimumWidth(250)
self.mailbox_listbox.setFixedHeight(100)
self.mailbox_listbox.setStyleSheet(list_style)
self.mailbox_listbox.itemClicked.connect(self.on_mailbox_selected)
layout.addWidget(self.mailbox_listbox)
layout.addSpacing(15)
# --- Dossiers ---
self.folder_listbox = QListWidget()
self.folder_listbox.setStyleSheet(list_style)
self.folder_listbox.setFixedHeight(100)
layout.addWidget(self.folder_listbox)
# --- Barre de recherche ---
search_container = QWidget()
search_layout = QVBoxLayout(search_container)
search_layout.setContentsMargins(0, 0, 0, 0)
search_layout.setSpacing(0) # Optionnel pour réduire l’espace
self.folder_search = QLineEdit()
self.folder_search.setStyleSheet(input_style)
self.folder_search.setFixedHeight(25)
self.folder_search.setMaximumWidth(350)
self.folder_search.setPlaceholderText("🔍 Rechercher un dossier...")
self.folder_search.textChanged.connect(self.filter_folders)
search_layout.addWidget(self.folder_search)
layout.addWidget(search_container)
# --- Boutons de contrôle ---
button_container = QWidget()
button_layout = QHBoxLayout(button_container)
button_layout.setContentsMargins(0, 0, 0, 0)
button_layout.setSpacing(1)
buttons = [
("Déplier", self.expand_all_folders),
("Replier", self.collapse_all_folders),
("Actual.", lambda: self.update_folders(
self.mailbox_listbox.currentItem().text() if self.mailbox_listbox.currentItem() else None)),
("Stats", self.show_folder_stats)
]
for text, callback in buttons:
button = QPushButton(text)
button.setFixedHeight(18)
button.setStyleSheet(button_style)
button.clicked.connect(callback)
button_layout.addWidget(button)
layout.addWidget(button_container)
# --- Champs de filtrage (dans une grille) ---
self.link_mode = "url"
self.sender_entry = QLineEdit()
self.subject_entry = QLineEdit()
self.link_entry = QLineEdit()
self.reward_entry = QLineEdit()
self.sender_entry.setPlaceholderText("Expéditeur")
self.subject_entry.setPlaceholderText("Mots-clés du titre")
self.link_entry.setPlaceholderText("Mots-clés du lien")
self.reward_entry.setPlaceholderText("Récompense")
for field in [self.sender_entry, self.subject_entry, self.link_entry, self.reward_entry]:
field.setStyleSheet(placeholder_style)
# Bouton toggle mode URL / Pattern
self.link_mode_toggle = QPushButton("Keyword Mode : Manuel")
self.link_mode_toggle.setCheckable(True)
self.link_mode_toggle.setChecked(False)
self.link_mode_toggle.clicked.connect(self.toggle_link_mode)
self.link_mode_toggle.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border-radius: 4px;
padding: 5px;
font-weight: bold;
}
""")
# Grille des champs
form_grid_container = QWidget()
form_grid_layout = QGridLayout(form_grid_container)
form_grid_layout.setSpacing(5)
form_grid_layout.setContentsMargins(0, 0, 0, 0)
# Ligne 0 : champ Expéditeur en pleine largeur
form_grid_layout.addWidget(self.sender_entry, 0, 0, 1, 2)
# Ligne 1 : Mot-clé du titre et Récompense côte à côte
form_grid_layout.addWidget(self.subject_entry, 1, 0)
form_grid_layout.addWidget(self.reward_entry, 1, 1)
# Ligne 2 : Toggle URL/Pattern et champ du lien
form_grid_layout.addWidget(self.link_mode_toggle, 2, 0)
form_grid_layout.addWidget(self.link_entry, 2, 1)
# Étirement égal des colonnes
form_grid_layout.setColumnStretch(0, 1)
form_grid_layout.setColumnStretch(1, 1)
# Ajout au layout principal
layout.addWidget(form_grid_container)
# --- Boutons de récompense ---
reward_buttons_container = QWidget()
reward_buttons_layout = QHBoxLayout(reward_buttons_container)
reward_buttons_layout.setContentsMargins(5, 15, 5, 15)
reward_buttons_layout.setSpacing(5)
self.validate_reward_button = QPushButton("Valider récompense")
self.reset_reward_button = QPushButton("RAZ récompense")
reward_button_style = """
QPushButton {
background-color: #00bab5;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-weight: bold;
font-size: 12px;
min-height: 20px;
}
QPushButton:hover {
background-color: #009994;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
"""
self.validate_reward_button.setStyleSheet(reward_button_style)
self.reset_reward_button.setStyleSheet(reward_button_style)
self.validate_reward_button.clicked.connect(self.validate_reward)
self.reset_reward_button.clicked.connect(self.reset_reward)
reward_buttons_layout.addWidget(self.validate_reward_button)
reward_buttons_layout.addWidget(self.reset_reward_button)
layout.addWidget(reward_buttons_container)
container_layout.addWidget(group)
# Timer masquage panneau
self.panel_timer = QTimer(self)
self.panel_timer.setSingleShot(True)
self.panel_timer.timeout.connect(self.hide_mail_folder_panel)
return self.mail_folder_container
def create_control_section(self):
group = QGroupBox("Contrôles")
group.setMaximumHeight(180)
group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
layout = QVBoxLayout(group)
layout.setSpacing(10)
# Première ligne de boutons
top_buttons_container = QWidget()
top_buttons_layout = QHBoxLayout(top_buttons_container)
top_buttons_layout.setContentsMargins(0, 0, 0, 0)
top_buttons_layout.setSpacing(5)
self.reset_reload_button = QPushButton("Réinitialiser")
self.reset_reload_button.setToolTip("Réinitialiser tous les critères de recherche")
self.reset_reload_button.clicked.connect(self.reset_search_criteria)
self.stop_button = QPushButton("Arrêter")
self.pause_button = QPushButton("Pause")
self.skip_button = QPushButton("Passer")
buttons_first_row = [self.reset_reload_button, self.stop_button, self.pause_button, self.skip_button]
for button in buttons_first_row:
button.setStyleSheet(self.button_style)
button.setFixedHeight(30)
top_buttons_layout.addWidget(button)
layout.addWidget(top_buttons_container)
# Deuxième ligne : scan, nettoyer, planificateur
buttons_container = QWidget()
buttons_layout = QHBoxLayout(buttons_container)
buttons_layout.setContentsMargins(0, 0, 0, 0)
buttons_layout.setSpacing(5)
self.start_button = QPushButton("Gagner Crédits")
self.count_button = QPushButton("Nettoyage Mails")
self.scheduler_button = QPushButton("Planificateur")
for btn in [self.start_button, self.count_button, self.scheduler_button]:
btn.setStyleSheet(self.button_style)
btn.setFixedHeight(30)
buttons_layout.addWidget(btn)
layout.addWidget(buttons_container)
# Animation comptage
self.count_timer = QTimer(self)
self.count_timer.timeout.connect(self.update_count_button_text)
self.count_animation_step = 0
# États initiaux
self.stop_button.setEnabled(False)
self.pause_button.setEnabled(False)
self.skip_button.setEnabled(False)
# Connexions
self.start_button.clicked.connect(self.on_start_button_clicked)
self.stop_button.clicked.connect(self.stop_operation)
self.pause_button.clicked.connect(self.toggle_pause)
self.skip_button.clicked.connect(self.skip_current_email)
self.count_button.clicked.connect(self.cleanup_emails)
self.scheduler_button.clicked.connect(self.open_scheduler)
return group
def setup_fullscreen_shortcut(self):
"""
Configure le raccourci clavier pour le plein écran (F11)
"""
try:
from PyQt5.QtWidgets import QShortcut
from PyQt5.QtGui import QKeySequence
fullscreen_shortcut = QShortcut(QKeySequence("F11"), self)
fullscreen_shortcut.activated.connect(self.toggle_fullscreen_mode)
self.log_action("⌨️ Raccourci F11 configuré pour le plein écran")
except Exception as e:
self.log_action(f"❌ Erreur configuration raccourci: {e}")
def set_log_type(self, log_type):
"""Change le type de journal"""
self.current_log_type = log_type
timestamp = QDateTime.currentDateTime().toString("dd/MM HH:mm")
self.current_log_name = f"{log_type} - {timestamp}"
def _toggle_category(self, category, enabled):
"""Active/désactive une catégorie"""
self.log_categories[category]['enabled'] = enabled
def _save_simple_log(self, content):
"""Sauvegarde simple du journal"""
filename = f"{self.current_log_name}_{QDateTime.currentDateTime().toString('yyyyMMdd_HHmm')}.txt"
filepath, _ = QFileDialog.getSaveFileName(self, "Sauvegarder", filename, "Fichiers texte (*.txt)")
if filepath:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(f"=== {self.current_log_name} ===\n")
f.write(f"Généré le: {QDateTime.currentDateTime().toString('dd/MM/yyyy HH:mm')}\n\n")
f.write(content)
QMessageBox.information(self, "Sauvegardé", f"Journal sauvegardé dans:\n{filepath}")
def save_log_to_file(self, log_content, log_type="journal"):
"""
Sauvegarde le contenu du journal dans un fichier .txt
"""
try:
if not log_content.strip():
QMessageBox.warning(self, "Attention", "Le journal sélectionné est vide.")
return
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
default_filename = f"{log_type}_{timestamp}.txt"
default_path = os.path.join(LOG_DIR, default_filename)
file_path, _ = QFileDialog.getSaveFileName(
self,
f"Sauvegarder {log_type}",
default_path,
"Fichiers texte (*.txt);;Tous les fichiers (*.*)"
)
if file_path:
with open(file_path, "w", encoding="utf-8") as f:
# Ajouter un en-tête informatif
f.write(f"=== {log_type.upper().replace('_', ' ')} ===\n")
f.write(f"Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n")
f.write(f"Nombre d'entrées: {len(log_content.split(chr(10)))}\n")
f.write("=" * 50 + "\n\n")
f.write(log_content)
f.write(f"\n\n=== FIN {log_type.upper().replace('_', ' ')} ===")
QMessageBox.information(self, "Succès", f"Journal sauvegardé :\n{file_path}")
self.log_action(f"💾 {log_type} sauvegardé dans: {os.path.basename(file_path)}")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde : {e}")
self.log_action(f"❌ Erreur sauvegarde: {str(e)}")
def auto_save_complete_scan_log(self):
"""
Sauvegarde automatique de l'HISTORIQUE COMPLET du dernier scan
- Sur disque (fichier texte)
- Dans le menu déroulant (format HTML)
"""
try:
# === DEBUG INITIAL ===
print(f"DEBUG: auto_save_complete_scan_log appelée")
print(f"DEBUG: scan_log_history existe = {hasattr(self, 'scan_log_history')}")
print(f"DEBUG: scan_log_history taille = {len(getattr(self, 'scan_log_history', []))}")
# === VÉRIFICATION DE L'HISTORIQUE ===
if not hasattr(self, 'scan_log_history') or not self.scan_log_history:
print("ERREUR: scan_log_history vide ou inexistant")
# Tentative de récupération depuis outlook_handler
if hasattr(self, 'outlook_handler') and hasattr(self.outlook_handler, 'scan_log_history'):
print(f"RÉCUPÉRATION: outlook_handler a {len(self.outlook_handler.scan_log_history)} entrées")
self.scan_log_history = self.outlook_handler.scan_log_history.copy()
else:
print("ÉCHEC: Aucun historique disponible nulle part")
self.log_action("⚠️ Aucun historique de scan à sauvegarder")
return None
# === IMPORTS NÉCESSAIRES ===
from datetime import datetime
import os
# === SAUVEGARDE SUR DISQUE ===
print("SAUVEGARDE: Début sauvegarde sur disque")
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"scan_complet_{timestamp}.txt"
# Définir LOG_DIR si pas défini
if not hasattr(self, 'LOG_DIR'):
LOG_DIR = os.path.join(os.path.expanduser("~"), "Documents", "OutlookLogs")
os.makedirs(LOG_DIR, exist_ok=True)
else:
LOG_DIR = self.LOG_DIR
filepath = os.path.join(LOG_DIR, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write("=== HISTORIQUE COMPLET DU SCAN ===\n")
f.write(f"Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n")
f.write(f"Nombre d'entrées: {len(self.scan_log_history)}\n")
f.write("Note: Ceci contient TOUS les logs du scan, même ceux effacés de l'affichage\n")
f.write("=" * 60 + "\n\n")
for entry in self.scan_log_history:
f.write(f"{entry}\n")
f.write(f"\n\n=== FIN HISTORIQUE COMPLET ({len(self.scan_log_history)} entrées) ===")
print(f"SAUVEGARDE: Fichier créé: {filepath}")
# === SAUVEGARDE POUR LE MENU DÉROULANT ===
print("SAUVEGARDE: Début sauvegarde pour menu déroulant")
# Créer un nom avec horodatage pour le menu
display_timestamp = datetime.now().strftime("%d/%m/%Y %H:%M")
journal_name = f"Traitement Mail - {display_timestamp}"
print(f"SAUVEGARDE: Nom du journal: {journal_name}")
# Convertir l'historique en HTML formaté pour l'affichage
html_content = ""
for entry in self.scan_log_history:
# Analyser chaque entrée pour extraire le timestamp et le message
entry_str = str(entry)
# Déterminer la couleur selon le contenu du message
color = '#87CEEB' # couleur par défaut (system)
if '📧' in entry_str or 'Email' in entry_str or 'email' in entry_str.lower():
color = '#98FB98' # vert clair pour email
elif '🔢' in entry_str or 'Compteur' in entry_str or 'traités' in entry_str:
color = '#FFB347' # orange pour count
elif '❌' in entry_str or 'Erreur' in entry_str or 'erreur' in entry_str.lower():
color = '#FF6B6B' # rouge pour error
elif '✅' in entry_str or 'Scan terminé' in entry_str or 'succès' in entry_str.lower() or 'terminé' in entry_str.lower():
color = '#4CAF50' # vert pour success
elif '🔗' in entry_str or 'lien' in entry_str.lower() or 'cliqué' in entry_str.lower():
color = '#FFA500' # orange pour liens
elif '🏁' in entry_str or 'fin' in entry_str.lower():
color = '#32CD32' # vert lime pour fin
elif '🚀' in entry_str or 'début' in entry_str.lower():
color = '#00BFFF' # bleu ciel pour début
elif '📡' in entry_str or 'connexion' in entry_str.lower():
color = '#4169E1' # bleu royal pour connexion
elif '⚙️' in entry_str or 'Configuration' in entry_str or 'system' in entry_str.lower():
color = '#87CEEB' # bleu pour system
html_content += f"{entry_str}
"
html_content += ""
# Initialiser le stockage si nécessaire
if not hasattr(self, 'saved_journals'):
self.saved_journals = {}
print("SAUVEGARDE: Initialisation saved_journals")
# Sauvegarder dans le dictionnaire des journaux pour le menu déroulant
self.saved_journals[journal_name] = html_content
print(f"SAUVEGARDE: Journal ajouté à saved_journals, total: {len(self.saved_journals)}")
# Mettre à jour le nom du journal actuel
self.current_log_name = journal_name
print(f"SAUVEGARDE: current_log_name mis à jour: {journal_name}")
# Log de confirmation
self.log_action(f"💾 Historique complet sauvegardé: {filename}")
self.log_action(f"📋 Journal ajouté au menu: {journal_name}")
print(f"SAUVEGARDE: Succès complet, retour: {filepath}")
return filepath
except Exception as e:
print(f"ERREUR SAUVEGARDE: {str(e)}")
import traceback
traceback.print_exc()
# Essayer de log l'erreur si possible
try:
self.log_action(f"❌ Erreur sauvegarde historique complet: {str(e)}")
except:
print("Impossible de logger l'erreur")
return None
def cleanup_old_log_files(self, days_to_keep=7):
"""
Nettoie les anciens fichiers de log (garde seulement les X derniers jours)
"""
try:
if not os.path.exists(LOG_DIR):
return
current_time = time.time()
cutoff_time = current_time - (days_to_keep * 24 * 60 * 60)
cleaned_count = 0
for filename in os.listdir(LOG_DIR):
if filename.startswith(('log_auto_', 'journal_visible_', 'scan_complet_')):
filepath = os.path.join(LOG_DIR, filename)
if os.path.getctime(filepath) < cutoff_time:
os.remove(filepath)
cleaned_count += 1
if cleaned_count > 0:
self.log_action(f"🧹 {cleaned_count} anciens fichiers de log supprimés")
except Exception as e:
self.log_action(f"❌ Erreur nettoyage logs: {str(e)}")
def clear_complete_history(self):
"""
Vide l'historique complet après confirmation
"""
reply = QMessageBox.question(
self,
"Confirmer",
"Êtes-vous sûr de vouloir vider tout l'historique des logs ?\n\nCette action est irréversible.",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
if hasattr(self, 'complete_log_history'):
self.complete_log_history.clear()
if hasattr(self, 'scan_log_history'):
self.scan_log_history.clear()
if hasattr(self, 'log_text'):
self.log_text.clear()
# Effacer tous les QTextEdit de la boîte si visible
for widget in QApplication.allWidgets():
if isinstance(widget, QDialog) and widget.windowTitle() == "Journal des événements":
text_edits = widget.findChildren(QTextEdit)
for edit in text_edits:
edit.clear()
# 🔁 Mettre à jour les noms des onglets
tab_widget = widget.findChild(QTabWidget)
if tab_widget:
tab_widget.setTabText(0, "📺 Actuel (0)")
tab_widget.setTabText(1, "📚 Complet (0)")
tab_widget.setTabText(2, "🎯 Scan (0)")
self.log_action("🗑️ Historique des logs vidé")
QMessageBox.information(self, "Confirmé", "Historique des logs vidé avec succès.")
def auto_save_log(self):
"""
Sauvegarde automatique du journal VISIBLE dans un fichier daté.
Complète la sauvegarde de l'historique complet du scan.
"""
try:
if hasattr(self, 'log_text'):
content = self.log_text.toPlainText()
if content.strip(): # Ne pas sauvegarder si vide
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"journal_visible_{timestamp}.txt"
filepath = os.path.join(LOG_DIR, filename)
with open(filepath, "w", encoding="utf-8") as f:
# En-tête pour distinguer du journal complet
f.write("=== JOURNAL VISIBLE AU MOMENT DE LA SAUVEGARDE ===\n")
f.write(f"Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n")
f.write("Note: Ceci est le contenu visible à l'écran, pas l'historique complet\n")
f.write("=" * 60 + "\n\n")
f.write(content)
f.write("\n\n=== FIN DU JOURNAL VISIBLE ===")
# Message dans l'interface (plus discret)
self.log_action(f"💾 Journal visible sauvegardé: {filename}")
return filepath # Retourner le chemin pour usage externe si besoin
except Exception as e:
self.log_action(f"❌ Erreur sauvegarde journal visible: {str(e)}")
return None
def start_log_clear_timer(self):
self.log_clear_remaining = 0
self.log_clear_timer.start(1000)
self.log_action("⏳ Timer journal démarré (1 minutes)")
def update_log_timer(self):
if self.log_clear_remaining > 0:
self.log_clear_remaining -= 1
# Remplacer la dernière ligne au lieu d'en ajouter une nouvelle
current_text = self.log_text.toPlainText()
lines = current_text.split('\n')
if lines and "Effacement journal dans" in lines[-1]:
lines[
-1] = f"{QDateTime.currentDateTime().toString('HH:mm:ss')} - ⏳ Effacement journal dans {self.log_clear_remaining} s"
self.log_text.setPlainText('\n'.join(lines))
else:
self.log_action(f"⏳ Effacement journal dans {self.log_clear_remaining} s")
else:
self.log_clear_timer.stop()
self.log_text.clear()
self.log_action("✅ Journal effacé automatiquement après 2 minutes")
def resizeEvent(self, event):
"""Gère les événements de redimensionnement avec protection débordement"""
try:
super().resizeEvent(event)
# Démarrer le timer pour traiter le redimensionnement après un délai
if hasattr(self, 'resize_timer'):
self.resize_timer.start(200) # 200ms de délai
except Exception as e:
print(f"Erreur dans resizeEvent: {e}")
def set_styles(self):
self.input_style = """
QLineEdit, QListWidget {
background-color: #03699c;
color: white;
border: 1px solid #dcdcdc;
border-radius: 4px;
padding: 5px;
}
"""
self.button_style = """
QPushButton {
background-color: #008CBA;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
font-size: 10px;
}
QPushButton:hover {
background-color: #007B9A;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
"""
def toggle_link_mode(self):
"""
Bascule entre le mode URL et Pattern, avec changement de couleur et visibilité du champ.
"""
if self.link_mode_toggle.isChecked():
self.link_mode_toggle.setText("Keyword Mode : Automatique")
self.link_entry.setVisible(False)
self.link_mode = "pattern"
self.link_mode_toggle.setStyleSheet("""
QPushButton {
background-color: #6f42c1;
color: white;
border-radius: 4px;
padding: 5px;
font-weight: bold;
}
""")
else:
self.link_mode_toggle.setText("Keyword Mode : Manuel")
self.link_entry.setVisible(True)
self.link_mode = "url"
self.link_mode_toggle.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
border-radius: 4px;
padding: 5px;
font-weight: bold;
}
""")
def toggle_mail_folder_panel(self):
"""Version alternative - Redimensionnement forcé"""
try:
if self.mail_folder_container.isVisible():
# MASQUER
panel_width = self.mail_folder_container.width()
self.mail_folder_container.setVisible(False)
# Trouver la fenêtre principale par remontée
widget = self
while widget.parent():
widget = widget.parent()
main_window = widget
# Réduction forcée
current_size = main_window.size()
new_width = current_size.width() - panel_width
new_width = 850
main_window.setFixedWidth(new_width) # Forcer la largeur
# Puis remettre en redimensionnable
QTimer.singleShot(100, lambda: main_window.setMinimumWidth(0))
QTimer.singleShot(100, lambda: main_window.setMaximumWidth(16777215))
self.toggle_panel_button.setText("☰")
self.stored_panel_width = panel_width
else:
# AFFICHER
self.mail_folder_container.setVisible(True)
# Restauration forcée
widget = self
while widget.parent():
widget = widget.parent()
main_window = widget
panel_width = getattr(self, 'stored_panel_width', 400)
current_size = main_window.size()
new_width = current_size.width() + panel_width
main_window.setFixedWidth(new_width)
# Puis remettre en redimensionnable
QTimer.singleShot(100, lambda: main_window.setMinimumWidth(0))
QTimer.singleShot(100, lambda: main_window.setMaximumWidth(16777215))
self.toggle_panel_button.setText("✕")
QApplication.processEvents()
except Exception as e:
print(f"Erreur: {e}")
def show_mail_folder_panel(self):
"""Affiche le panneau de sélection et démarre le timer si spécifié"""
self.mail_folder_container.setVisible(True)
# Arrêter le timer s'il est en cours
if self.panel_timer.isActive():
self.panel_timer.stop()
def hide_mail_folder_panel(self):
"""Masque le panneau de sélection"""
self.mail_folder_container.setVisible(False)
def filter_folders(self, text):
"""Filtre les dossiers en fonction du texte de recherche"""
for i in range(self.folder_listbox.count()):
item = self.folder_listbox.item(i)
item.setHidden(text.lower() not in item.text().lower())
def update_folder_tree(self, folders_data):
"""Met à jour l'arbre des dossiers avec les nouvelles données"""
self.folder_tree.clear()
def add_folder_item(parent, folder_info):
item = QTreeWidgetItem(parent)
item.setText(0, folder_info['name'])
item.setText(1, str(folder_info['total_messages']))
item.setText(2, str(folder_info['unread_messages']))
item.setData(0, Qt.UserRole, folder_info['id'])
if folder_info.get('children'):
for child in folder_info['children']:
add_folder_item(item, child)
for folder in folders_data:
add_folder_item(self.folder_tree, folder)
self.folder_tree.expandAll()
for i in range(self.folder_tree.columnCount()):
self.folder_tree.resizeColumnToContents(i)
def create_search_criteria_section(self):
group = QGroupBox("Critères de recherche")
group.setMaximumHeight(200) # Réduire considérablement la hauteur
group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
layout = QVBoxLayout(group)
layout.setSpacing(5)
# Style commun pour les inputs
input_style = """
QLineEdit {
background-color: #03699c;
color: white;
border: 1px solid #dcdcdc;
border-radius: 4px;
padding: 5px;
font-size: 12px;
}
"""
# Container pour la ligne principale
main_row = QWidget()
main_layout = QHBoxLayout(main_row)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(5) # Espacement entre les éléments
# 1. Container pour le nombre max d'emails (avec label + champ)
max_emails_container = QWidget()
max_emails_layout = QHBoxLayout(max_emails_container)
max_emails_layout.setContentsMargins(0, 0, 0, 0)
max_emails_layout.setSpacing(5)
max_emails_label = QLabel("Nbre Emails:")
max_emails_layout.addWidget(max_emails_label)
self.max_emails_entry = QLineEdit()
self.max_emails_entry.setFixedWidth(60)
self.max_emails_entry.setFixedHeight(25)
self.max_emails_entry.setMaxLength(4)
self.max_emails_entry.setAlignment(Qt.AlignCenter)
self.max_emails_entry.setStyleSheet(input_style)
max_emails_layout.addWidget(self.max_emails_entry)
main_layout.addWidget(max_emails_container)
# 3. Container pour l'authentification (label + checkbox)
auth_container = QWidget()
auth_layout = QHBoxLayout(auth_container)
auth_layout.setContentsMargins(0, 0, 0, 0)
auth_layout.setSpacing(5)
auth_label = QLabel("Authentification:")
self.auth_checkbox = QCheckBox()
auth_layout.addWidget(auth_label)
auth_layout.addWidget(self.auth_checkbox)
main_layout.addWidget(auth_container)
# 2. Container pour la validité (label + checkbox + champ de saisie sur la même ligne)
validity_container = QWidget()
validity_layout = QHBoxLayout(validity_container)
validity_layout.setContentsMargins(0, 0, 0, 0)
validity_layout.setSpacing(5)
validity_label = QLabel("Date de validité:")
self.validity_checkbox = QCheckBox()
self.validity_entry = QLineEdit()
self.validity_entry.setFixedWidth(60)
self.validity_entry.setFixedHeight(25)
self.validity_entry.setMaxLength(3)
self.validity_entry.setAlignment(Qt.AlignCenter)
self.validity_entry.setStyleSheet(input_style)
self.validity_entry.setVisible(False) # Caché par défaut
validity_layout.addWidget(validity_label)
validity_layout.addWidget(self.validity_checkbox)
validity_layout.addWidget(self.validity_entry)
main_layout.addWidget(validity_container)
# Ajouter un extensible pour pousser les éléments vers la gauche
main_layout.addStretch()
# Ajouter la ligne principale au layout
layout.addWidget(main_row)
# Ajouter un second espace pour descendre davantage le scrolltext
spacer2 = QWidget()
spacer2.setFixedHeight(15)
layout.addWidget(spacer2)
# Ajouter la ligne principale au layout
layout.addWidget(main_row)
# Ajouter un second espace pour descendre davantage le scrolltext
spacer2 = QWidget()
spacer2.setFixedHeight(15)
layout.addWidget(spacer2)
# Création du texte défilant pour les astuces avec un conteneur fixe
tips_container = QWidget()
tips_container.setFixedHeight(40) # Hauteur fixe augmentée
tips_layout = QVBoxLayout(tips_container)
tips_layout.setContentsMargins(0, 0, 0, 0)
self.tips_label = QLabel()
self.tips_label.setStyleSheet("""
QLabel {
background-color: #2c3e50;
color: #42effb;
padding: 5px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
""")
self.tips_label.setFixedHeight(30) # Légèrement plus haut
self.tips_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
tips_layout.addWidget(self.tips_label)
# Ajouter le conteneur au layout principal
layout.addWidget(tips_container)
# Initialiser l'animation du texte défilant
self.setup_scrolling_tips()
layout.addWidget(spacer2)
# Créer un bouton à trois états pour le contrôle des conseils
self.tips_control_button = QPushButton("Cliquez pour changer de mode: Mode Tips Défilant")
self.tips_control_button.setStyleSheet("""
QPushButton {
background-color: #6b6bf4;
color: white;
border: none;
padding: 5px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 11px;
}
QPushButton:hover {
background-color: #007B9A;
}
""")
# État initial: défilant (1)
settings = QSettings("Snoown2K", "MA2C")
self.tips_mode = settings.value("tips_mode", 1, type=int) # 0 = désactivé, 1 = défilant, 2 = aléatoire avec fondu
self.apply_tips_mode(self.tips_mode)
self.tips_control_button.clicked.connect(self.toggle_tips_mode)
layout.addWidget(self.tips_control_button)
# Connecter le checkbox de validité pour afficher/masquer le champ de validité
self.validity_checkbox.stateChanged.connect(
lambda state: self.validity_entry.setVisible(state == Qt.Checked)
)
# Connecter le signal de la checkbox d'authentification
self.auth_checkbox.stateChanged.connect(self.toggle_auth_interface)
# Ajouter un espace extensible à la fin
layout.addStretch()
return group
def apply_tips_mode(self, mode):
# Arrêter TOUS les timers avant de changer de mode
if hasattr(self, 'tip_animation_timer') and self.tip_animation_timer.isActive():
self.tip_animation_timer.stop()
if hasattr(self, 'tip_change_timer') and self.tip_change_timer.isActive():
self.tip_change_timer.stop()
if hasattr(self, 'tips_fade_timer') and self.tips_fade_timer.isActive():
self.tips_fade_timer.stop()
if mode == 1:
self.setup_scrolling_tips()
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Défilant")
elif mode == 2:
self.setup_fading_tips()
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Fondu")
else:
self.tips_label.clear()
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Désactivé")
def toggle_tips_mode(self):
"""Change le mode d'affichage des conseils entre désactivé, défilant et aléatoire avec fondu"""
try:
# Passer au mode suivant (cycle entre 0, 1, 2)
self.tips_mode = (self.tips_mode + 1) % 3
# Nettoyage général des timers avant tout
if hasattr(self, 'tip_animation_timer') and self.tip_animation_timer.isActive():
self.tip_animation_timer.stop()
if hasattr(self, 'tip_change_timer') and self.tip_change_timer.isActive():
self.tip_change_timer.stop()
if hasattr(self, 'tips_fade_timer') and self.tips_fade_timer.isActive():
self.tips_fade_timer.stop()
# Appliquer le comportement selon le mode
if self.tips_mode == 0:
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Désactivé")
self.tips_label.setVisible(False)
self.log_action("📴 Mode Tips : Désactivé")
elif self.tips_mode == 1:
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Défilant")
self.tips_label.setVisible(True)
self.setup_scrolling_tips()
self.tips_label.setWindowOpacity(1.0)
self.log_action("📜 Mode Tips : Défilement")
else: # self.tips_mode == 2
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Fondu")
self.tips_label.setVisible(True)
self.setup_fading_tips()
self.log_action("🌫️ Mode Tips : Fondu")
# Sauvegarder le mode dans les paramètres
settings = QSettings("Snoown2K", "MA2C")
settings.setValue("tips_mode", self.tips_mode)
settings.sync()
except Exception as e:
self.log_action(f"❌ Erreur dans toggle_tips_mode : {e}")
def restore_tips_mode(self):
"""Restaure le mode tips (0, 1, 2) depuis les paramètres enregistrés"""
try:
settings = QSettings("Snoown2K", "MA2C")
saved_mode = int(settings.value("tips_mode", 0))
self.tips_mode = saved_mode if saved_mode in [0, 1, 2] else 0
# Répliquer l’effet visuel du mode au lancement
if self.tips_mode == 0:
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Désactivé")
self.tips_label.setVisible(False)
elif self.tips_mode == 1:
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Défilant")
self.tips_label.setVisible(True)
self.setup_scrolling_tips()
self.tips_label.setWindowOpacity(1.0)
elif self.tips_mode == 2:
self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Fondu")
self.tips_label.setVisible(True)
self.setup_fading_tips()
self.log_action(f"Mode Tips restauré : {['Désactivé', 'Défilement', 'Fondu'][self.tips_mode]}")
except Exception as e:
self.log_action(f"⚠️ Erreur restauration mode Tips : {e}")
# Nouvelle méthode pour le mode conseils en fondu
def setup_fading_tips(self):
"""Configure l'animation des conseils en mode fondu aléatoire SANS défilement"""
import random
from PyQt5.QtWidgets import QGraphicsOpacityEffect
# ARRÊTER complètement tous les timers de défilement
if hasattr(self, 'tip_animation_timer') and self.tip_animation_timer.isActive():
self.tip_animation_timer.stop()
if hasattr(self, 'tip_change_timer') and self.tip_change_timer.isActive():
self.tip_change_timer.stop()
# Liste des conseils
tips = [
"Videz corbeille pour libérer l'espace",
"Modifiez mots-clés dans les paramètres",
"Clic sur lien ouvre le mini-navigateur",
"Profils sauvent vos critères de scan",
"Pensez à consulter le journal de log",
"Sauvez profil avant de lancer le scan"
]
# Sélectionner un conseil aléatoire
self.current_tip = random.choice(tips)
# Configurer le label pour un affichage STATIQUE centré
self.tips_label.setAlignment(Qt.AlignCenter)
self.tips_label.setText(self.current_tip) # Texte fixe, pas de défilement
# Style initial avec opacité 0
self.tips_label.setStyleSheet("""
QLabel {
background-color: #2c3e50;
color: #42effb;
padding: 5px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
""")
# Initialiser les variables pour l'effet de fondu
self.tips_opacity = 0.0
self.fade_phase = 0 # 0: fade in, 1: hold, 2: fade out
self.fade_counter = 0
self.hold_duration = 40 # Nombre de tics pour maintenir l'affichage (4 secondes)
# Créer le timer pour l'effet de fondu uniquement
self.tips_fade_timer = QTimer(self)
self.tips_fade_timer.timeout.connect(self.update_fading_tip)
self.tips_fade_timer.start(100) # Actualisation tous les 100ms
# Méthode pour mettre à jour l'animation de fondu
def update_fading_tip(self):
"""Met à jour l'animation du conseil en fondu SANS défilement"""
from PyQt5.QtWidgets import QGraphicsOpacityEffect
if self.fade_phase == 0: # Fade in
self.tips_opacity += 0.05 # Plus lent pour un effet plus doux
if self.tips_opacity >= 1.0:
self.tips_opacity = 1.0
self.fade_phase = 1 # Passer à la phase de maintien
self.fade_counter = 0
elif self.fade_phase == 1: # Hold (maintien)
self.fade_counter += 1
if self.fade_counter >= self.hold_duration:
self.fade_phase = 2 # Passer à la phase de fondu sortant
elif self.fade_phase == 2: # Fade out
self.tips_opacity -= 0.05 # Plus lent pour un effet plus doux
if self.tips_opacity <= 0.0:
self.tips_opacity = 0.0
self.fade_phase = 0 # Recommencer le cycle
# Changer le conseil pour le prochain cycle
import random
tips = [
"Videz corbeille pour libérer l'espace",
"Modifiez mots-clés dans les paramètres",
"Clic sur lien ouvre le mini-navigateur",
"Profils sauvent vos critères de scan",
"Pensez à consulter le journal de log",
"Sauvez profil avant de lancer le scan"
]
# Sélectionner un nouveau conseil différent du précédent
new_tip = self.current_tip
while new_tip == self.current_tip and len(tips) > 1:
new_tip = random.choice(tips)
self.current_tip = new_tip
# Mettre à jour le texte STATIQUE (pas de défilement)
self.tips_label.setText(self.current_tip)
# Appliquer l'effet d'opacité graphique
effect = QGraphicsOpacityEffect(self.tips_label)
effect.setOpacity(self.tips_opacity)
self.tips_label.setGraphicsEffect(effect)
def setup_scrolling_tips(self):
"""Configure l'animation du texte défilant avec des conseils aléatoires"""
# Liste des conseils
tips = [
"Videz corbeille pour libérer l'espace",
"Modifiez mots-clés dans les paramètres",
"Clic sur lien ouvre le mini-navigateur",
"Profils sauvent vos critères de scan",
"Pensez à consulter le journal de log",
"Sauvez profil avant de lancer le scan"
]
# Sélectionner un conseil aléatoire
import random
self.current_tip = random.choice(tips)
self.tips_label.setText(self.current_tip + " " * 50) # Ajouter plus d'espace après le texte
# Configurer l'animation
self.tip_animation_pos = 0
self.tip_animation_timer = QTimer(self)
self.tip_animation_timer.timeout.connect(self.update_scrolling_tip)
self.tip_animation_timer.start(50) # Mise à jour plus fréquente pour un défilement plus fluide
# Timer pour changer de conseil régulièrement
self.tip_change_timer = QTimer(self)
self.tip_change_timer.timeout.connect(self.change_tip)
self.tip_change_timer.start(15000) # Changer toutes les 15 secondes
def update_scrolling_tip(self):
"""Met à jour la position du texte défilant"""
if not hasattr(self, 'current_tip'):
return
# Faire défiler le texte de droite à gauche
text = self.current_tip + " " * 50 # Plus d'espace entre les répétitions
self.tip_animation_pos = (self.tip_animation_pos + 1) % len(text)
scrolled_text = text[self.tip_animation_pos:] + text[:self.tip_animation_pos]
# Mettre à jour le texte affiché
self.tips_label.setText(scrolled_text)
def change_tip(self):
"""Change le conseil affiché pour un nouveau conseil aléatoire"""
import random
tips = [
"Videz corbeille pour libérer l'espace",
"Modifiez mots-clés dans les paramètres",
"Clic sur lien ouvre le mini-navigateur",
"Profils sauvent vos critères de scan",
"Pensez à consulter le journal de log",
"Sauvez profil avant de lancer le scan"
]
# Sélectionner un nouveau conseil différent du conseil actuel
new_tip = self.current_tip
while new_tip == self.current_tip:
new_tip = random.choice(tips)
self.current_tip = new_tip
self.tip_animation_pos = 0 # Réinitialiser la position
# Méthode pour basculer l'interface d'authentification
def toggle_auth_interface(self, state):
"""Méthode pour basculer l'interface d'authentification"""
try:
if state == Qt.Checked:
from main import AuthDialog # Import local pour éviter les imports circulaires
from PyQt5.QtWidgets import QDesktopWidget # Ajout de l'import manquant
auth_dialog = AuthDialog(self, checkbox=self.auth_checkbox)
auth_dialog.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;
}
""")
# Centrer la boîte de dialogue
screen = QDesktopWidget().screenGeometry()
dialog_geometry = auth_dialog.geometry()
x = screen.center().x() - dialog_geometry.width() // 2
y = screen.center().y() - dialog_geometry.height() // 2
auth_dialog.move(x, y)
if auth_dialog.exec_() == QDialog.Accepted:
self.log_action("Authentification validée avec succès")
# Sauvegarder les valeurs si nécessaire
self.url_field = auth_dialog.url_field.text()
self.login_field = auth_dialog.login_field.text()
self.password_field = auth_dialog.password_field.text()
# Sauvegarder les identifiants pour usage ultérieur
self.outlook_handler.username = self.login_field
self.outlook_handler.password = self.password_field
# Proposer le remplissage automatique si on est sur une page
if hasattr(self, 'mini_browser') and self.mini_browser:
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
current_url = current_tab.web_view.url().toString()
if current_url and current_url != "about:blank":
reply = QMessageBox.question(
self,
"Remplissage automatique",
"Voulez-vous remplir automatiquement les champs de cette page ?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.auto_fill_credentials()
else:
self.log_action("Authentification annulée")
self.auth_checkbox.setChecked(False)
except Exception as e:
self.log_action(f"Erreur lors de l'authentification: {str(e)}")
self.auth_checkbox.setChecked(False)
def open_scheduler(self):
"""Ouvre le planificateur de tâches du volet paramètre"""
# Vérifie si la MainWindow est accessible
main_window = None
parent = self.parent()
# Remonter la hiérarchie des widgets pour trouver la MainWindow
while parent:
if hasattr(parent, 'show_scheduler'):
main_window = parent
break
parent = parent.parent()
# Si la MainWindow est trouvée, appeler sa méthode show_scheduler
if main_window:
main_window.show_scheduler()
else:
# Si MainWindow n'est pas accessible, ouvrir d'abord le volet des paramètres
if hasattr(self, 'toggle_sidebar') and callable(self.toggle_sidebar):
self.toggle_sidebar()
# Attendre un peu pour que le sidebar s'ouvre
QTimer.singleShot(300, lambda: self.parent().show_scheduler() if hasattr(self.parent(),
'show_scheduler') else None)
def create_progress_section(self):
group = QGroupBox("Progression et Statut")
group.setMaximumHeight(400) # Hauteur réduite
group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
layout = QVBoxLayout(group)
# Styles différenciés pour chaque type de frame
status_frame_style = """
QFrame {
background-color: #1a2a3a;
border-radius: 4px;
padding: 4px;
}
QLabel {
color: white;
font-size: 12px;
}
"""
count_frame_style = """
QFrame {
background-color: #1a2a3a;
border-radius: 4px;
padding: 4px;
}
QLabel {
color: white;
font-size: 12px;
}
"""
reward_frame_style = """
QFrame {
background-color: #1a2a3a;
border-radius: 4px;
padding: 4px;
}
QLabel {
color: white;
font-size: 12px;
}
"""
time_frame_style = """
QFrame {
background-color: #1a2a3a;
border-radius: 4px;
padding: 4px;
}
QLabel {
color: white;
font-size: 12px;
}
"""
counter_frame_style = """
QFrame {
background-color: #1a2a3a;
border-radius: 4px;
padding: 4px;
}
QLabel {
color: white;
font-size: 12px;
}
"""
# Barre de progression
self.progress_bar = AnimatedProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setFormat("%p%")
self.progress_bar.setMinimumHeight(20)
layout.addWidget(self.progress_bar)
# Container pour status et comptage
status_container = QHBoxLayout()
status_container.setSpacing(5)
# Status Frame
status_frame = QFrame()
status_frame.setStyleSheet(status_frame_style)
status_layout = QVBoxLayout(status_frame)
status_layout.setSpacing(1)
status_layout.setContentsMargins(2, 2, 2, 2)
status_title = QLabel("📊 Status")
status_title.setAlignment(Qt.AlignCenter)
self.status_label = QLabel("En attente de démarrage...")
self.status_label.setStyleSheet("color: #5babfb;")
self.status_label.setAlignment(Qt.AlignCenter)
status_layout.addWidget(status_title)
status_layout.addWidget(self.status_label)
# Count Frame
count_frame = QFrame()
count_frame.setStyleSheet(count_frame_style)
count_layout = QVBoxLayout(count_frame)
count_layout.setSpacing(1)
count_layout.setContentsMargins(2, 2, 2, 2)
count_title = QLabel("🔢 Comptage")
count_title.setAlignment(Qt.AlignCenter)
self.count_label = QLabel("Nombre d'emails: 0")
self.count_label.setStyleSheet("color: #5babfb;")
self.count_label.setAlignment(Qt.AlignCenter)
count_layout.addWidget(count_title)
count_layout.addWidget(self.count_label)
# Ajouter les frames au container
status_container.addWidget(status_frame)
status_container.addWidget(count_frame)
layout.addLayout(status_container)
# Afficheur de prochaine tâche planifiée
self.next_task_label = QLabel()
self.next_task_label.setStyleSheet("""
QLabel {
background-color: #2c3e50;
color: #65ddfb;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
font-size: 11px;
}
""")
self.update_next_task_display()
layout.addWidget(self.next_task_label)
# Container pour les indicateurs
indicators_container = QHBoxLayout()
indicators_container.setSpacing(5)
# Reward Display Frame
reward_frame = QFrame()
reward_frame.setStyleSheet(reward_frame_style)
reward_layout = QVBoxLayout(reward_frame)
reward_layout.setSpacing(1)
reward_layout.setContentsMargins(2, 2, 2, 2)
reward_title = QLabel("💰 Récompense")
reward_title.setAlignment(Qt.AlignCenter)
self.reward_display = QLabel("---")
self.reward_display.setStyleSheet("color: #5babfb;")
self.reward_display.setAlignment(Qt.AlignCenter)
reward_layout.addWidget(reward_title)
reward_layout.addWidget(self.reward_display)
# Time Display Frame
time_frame = QFrame()
time_frame.setStyleSheet(time_frame_style)
time_layout = QVBoxLayout(time_frame)
time_layout.setSpacing(1)
time_layout.setContentsMargins(2, 2, 2, 2)
time_title = QLabel("⏱️ Temps")
time_title.setAlignment(Qt.AlignCenter)
self.time_display = QLabel("--:--")
self.time_display.setStyleSheet("color: #5babfb;")
self.time_display.setAlignment(Qt.AlignCenter)
time_layout.addWidget(time_title)
time_layout.addWidget(self.time_display)
# Email Counter Frame
counter_frame = QFrame()
counter_frame.setStyleSheet(counter_frame_style)
counter_layout = QVBoxLayout(counter_frame)
counter_layout.setSpacing(1)
counter_layout.setContentsMargins(2, 2, 2, 2)
counter_title = QLabel("📧 Emails")
counter_title.setAlignment(Qt.AlignCenter)
self.email_counter = QLabel("---")
self.email_counter.setStyleSheet("color: #FFD700;")
self.email_counter.setAlignment(Qt.AlignCenter)
counter_layout.addWidget(counter_title)
counter_layout.addWidget(self.email_counter)
# Ajouter les frames au container
indicators_container.addWidget(reward_frame)
indicators_container.addWidget(time_frame)
indicators_container.addWidget(counter_frame)
layout.addLayout(indicators_container)
# Créer un container dédié pour le timer de pause de cycle
cycle_timer_container = QHBoxLayout()
cycle_timer_container.setSpacing(0)
# Créer le timer de pause entre cycles
self.cycle_pause_timer = CyclePauseTimer()
cycle_timer_container.addWidget(self.cycle_pause_timer)
# Ajouter le container au layout principal
layout.addLayout(cycle_timer_container)
return group
def update_display_time(self, min_value, max_value):
"""Met à jour le temps d'affichage dans la section info"""
if hasattr(self, 'display_value'):
self.display_value.setText(f"{min_value}-{max_value}s")
# Mise à jour du handler si nécessaire
if hasattr(self, 'outlook_handler'):
self.outlook_handler.set_pause_time_range(min_value, max_value)
def update_pause_time(self, min_value, max_value):
"""Met à jour le temps de pause dans la section info"""
if hasattr(self, 'pause_value'):
self.pause_value.setText(f"{min_value}-{max_value}s")
# Mise à jour du handler si nécessaire
if hasattr(self, 'outlook_handler'):
self.outlook_handler.set_between_pages_time_range(min_value, max_value)
def update_links_count(self, value):
# S'assurer que la valeur est bien un entier
value = int(value)
# Mettre à jour la valeur interne
self.links_count = value
# Transmettre à l'OutlookHandler
if hasattr(self, 'outlook_handler') and self.outlook_handler:
self.outlook_handler.set_links_count(value)
# Log pour confirmer
self.log_action(f"Nombre de liens traités simultanément mis à jour: {value}")
def init_email_counter(self, messages, max_emails):
"""Initialise le compteur avec le nombre total d'emails dans le dossier"""
try:
total_in_folder = messages.Count
to_process = min(total_in_folder, max_emails) if max_emails else total_in_folder
counter_text = f"0/{to_process}"
if max_emails and max_emails < total_in_folder:
counter_text += f" ({total_in_folder})"
self.email_counter.setText(counter_text)
QApplication.processEvents()
except Exception as e:
self.log_action(f"Erreur d'initialisation du compteur: {str(e)}")
def update_email_counter(self, messages, processed_count=0, is_scanning=False):
"""
Met à jour le compteur avec le nombre réel d'emails dans le dossier
Args:
messages: Items du dossier Outlook
processed_count: Nombre d'emails traités dans le scan actuel
is_scanning: Si True, nous sommes en cours de scan
"""
try:
current_total = messages.Count # Nombre actuel d'emails dans le dossier
if is_scanning:
# Pendant le scan, on soustrait uniquement les emails traités
display_count = current_total - processed_count
else:
# Hors scan, on affiche simplement le total actuel
display_count = current_total
self.email_counter.setText(str(display_count))
QApplication.processEvents()
except Exception as e:
self.log_action(f"Erreur de mise à jour du compteur: {str(e)}")
def setup_email_counter(self):
"""Configure le timer de mise à jour du compteur"""
self.count_update_timer = QTimer(self)
self.count_update_timer.timeout.connect(self.refresh_email_count)
self.count_update_timer.start(60000) # Mise à jour toute les minutes
def refresh_email_count(self):
"""Rafraîchit le compteur en dehors d'un scan"""
try:
if not hasattr(self, 'current_folder'):
return
messages = self.current_folder.Items
self.update_email_counter(messages)
except Exception as e:
self.log_action(f"Erreur lors du rafraîchissement du compteur: {str(e)}")
def update_next_task_display(self):
"""Met à jour l'affichage de la prochaine tâche avec protection complète"""
try:
# === PROTECTION WIDGETS ===
# Vérifier que les widgets essentiels existent avant de continuer
required_widgets = ['next_task_label']
for widget_name in required_widgets:
if not hasattr(self, widget_name):
# Widgets pas encore créés - sortie silencieuse
return
# === INITIALISATION ===
next_task_text = "Aucune tâche planifiée"
task_count = 0
next_task_time = None
# === RÉCUPÉRATION DES TÂCHES OUTLOOK ===
if self.outlook_handler:
try:
# Tentative de récupération des tâches via Outlook
tasks = self.outlook_handler.get_upcoming_tasks() if hasattr(self.outlook_handler,
'get_upcoming_tasks') else []
if tasks:
task_count = len(tasks)
# Prendre la première tâche (la plus proche)
next_task = tasks[0]
# Formatage de l'affichage de la tâche
if hasattr(next_task, 'Subject') and hasattr(next_task, 'DueDate'):
subject = next_task.Subject[:50] + "..." if len(
next_task.Subject) > 50 else next_task.Subject
due_date = next_task.DueDate
# Formatage de la date
if due_date:
try:
from datetime import datetime
if hasattr(due_date, 'strftime'):
date_str = due_date.strftime("%d/%m/%Y %H:%M")
else:
date_str = str(due_date)
next_task_text = f"📋 {subject}\n⏰ {date_str}"
next_task_time = due_date
except:
next_task_text = f"📋 {subject}\n⏰ Date non disponible"
else:
next_task_text = f"📋 {subject}\n⏰ Pas de date d'échéance"
else:
next_task_text = f"📋 Tâche disponible\n📊 {task_count} tâche(s) au total"
else:
next_task_text = "✅ Aucune tâche planifiée"
except Exception as task_error:
next_task_text = f"⚠️ Erreur accès tâches Outlook\n{str(task_error)[:30]}..."
self.log_action(f"❌ Erreur récupération tâches Outlook: {task_error}")
# === RÉCUPÉRATION ALTERNATIVE (TÂCHES SYSTÈME) ===
elif hasattr(self, 'get_system_tasks'):
try:
system_tasks = self.get_system_tasks()
if system_tasks:
task_count = len(system_tasks)
next_task_text = f"🔧 {task_count} tâche(s) système\n⏳ Prochaine: {system_tasks[0]['name'][:30]}"
else:
next_task_text = "✅ Aucune tâche système"
except Exception as sys_error:
next_task_text = f"⚠️ Erreur tâches système\n{str(sys_error)[:30]}..."
self.log_action(f"❌ Erreur tâches système: {sys_error}")
# === MISE À JOUR DE L'AFFICHAGE ===
# Widget principal des tâches
self.next_task_label.setText(next_task_text)
# Mise à jour du compteur de tâches (si le widget existe)
if hasattr(self, 'task_count_label'):
count_text = f"{task_count} tâche(s)" if task_count > 0 else "Aucune tâche"
self.task_count_label.setText(count_text)
# Mise à jour de l'indicateur de priorité (si le widget existe)
if hasattr(self, 'task_priority_indicator'):
if task_count > 0 and next_task_time:
try:
from datetime import datetime, timedelta
now = datetime.now()
if hasattr(next_task_time, 'replace'):
# Convertir en datetime si nécessaire
if hasattr(next_task_time, 'date'):
time_diff = next_task_time - now
else:
time_diff = timedelta(days=1) # Valeur par défaut
# Définir la couleur selon l'urgence
if time_diff.total_seconds() < 3600: # Moins d'1 heure
self.task_priority_indicator.setStyleSheet("background-color: #ff4444; color: white;")
self.task_priority_indicator.setText("🚨 URGENT")
elif time_diff.total_seconds() < 86400: # Moins d'1 jour
self.task_priority_indicator.setStyleSheet("background-color: #ff8800; color: white;")
self.task_priority_indicator.setText("⚠️ PROCHE")
else:
self.task_priority_indicator.setStyleSheet("background-color: #00aa00; color: white;")
self.task_priority_indicator.setText("✅ PLANIFIÉ")
else:
self.task_priority_indicator.setStyleSheet("background-color: #888888; color: white;")
self.task_priority_indicator.setText("❓ INCONNU")
except:
self.task_priority_indicator.setStyleSheet("background-color: #888888; color: white;")
self.task_priority_indicator.setText("❓ ERREUR")
else:
self.task_priority_indicator.setStyleSheet("background-color: #cccccc; color: #666666;")
self.task_priority_indicator.setText("⭕ AUCUNE")
# === LOG DE SUCCÈS ===
if task_count > 0:
self.log_action(f"📋 Tâches mises à jour: {task_count} trouvée(s)")
except Exception as e:
# === GESTION D'ERREUR ROBUSTE ===
error_message = f"❌ Erreur update_next_task_display: {str(e)[:50]}..."
# Tentative de mise à jour du widget principal
if hasattr(self, 'next_task_label'):
try:
self.next_task_label.setText("⚠️ Erreur récupération tâches\nVérifiez la connexion Outlook")
except:
pass # Si même cette opération échoue, on continue silencieusement
# Mise à jour des widgets secondaires avec des valeurs d'erreur
if hasattr(self, 'task_count_label'):
try:
self.task_count_label.setText("Erreur")
except:
pass
if hasattr(self, 'task_priority_indicator'):
try:
self.task_priority_indicator.setStyleSheet("background-color: #cc0000; color: white;")
self.task_priority_indicator.setText("❌ ERREUR")
except:
pass
# Log de l'erreur
if hasattr(self, 'log_action'):
try:
self.log_action(error_message)
except:
print(error_message) # Fallback vers print si log_action échoue
else:
print(error_message)
# Log détaillé pour debug (optionnel)
if hasattr(self, 'log_action'):
try:
import traceback
detailed_error = traceback.format_exc()
self.log_action(f"🔍 Détail erreur tâches: {detailed_error[:200]}...")
except:
pass # Si le log détaillé échoue, on continue
def get_upcoming_tasks(self):
"""Méthode auxiliaire pour récupérer les tâches (à adapter selon votre logique Outlook)"""
try:
if not self.outlook_handler:
return []
# Exemple d'implémentation - adaptez selon votre handler Outlook
if hasattr(self.outlook_handler, 'get_tasks'):
return self.outlook_handler.get_tasks(limit=10)
elif hasattr(self.outlook_handler, 'outlook') and hasattr(self.outlook_handler.outlook, 'GetNamespace'):
namespace = self.outlook_handler.outlook.GetNamespace("MAPI")
tasks_folder = namespace.GetDefaultFolder(13) # 13 = olFolderTasks
tasks = tasks_folder.Items
tasks.Sort("[DueDate]") # Trier par date d'échéance
# Retourner les 10 premières tâches non terminées
upcoming_tasks = []
for task in tasks:
if len(upcoming_tasks) >= 10:
break
if hasattr(task, 'Complete') and not task.Complete:
upcoming_tasks.append(task)
return upcoming_tasks
else:
return []
except Exception as e:
if hasattr(self, 'log_action'):
self.log_action(f"❌ Erreur get_upcoming_tasks: {e}")
return []
def get_system_tasks(self):
"""Méthode auxiliaire pour les tâches système (placeholder)"""
try:
# Exemple de tâches système - adaptez selon vos besoins
system_tasks = [
{"name": "Nettoyage cache", "time": "14:00", "priority": "low"},
{"name": "Sauvegarde données", "time": "18:00", "priority": "high"},
{"name": "Mise à jour système", "time": "02:00", "priority": "medium"}
]
return system_tasks
except:
return []
# Timer pour mettre à jour l'affichage régulièrement
def setup_task_update_timer(self):
"""Configure le timer pour mettre à jour l'affichage de la prochaine tâche"""
self.task_update_timer = QTimer()
self.task_update_timer.timeout.connect(self.update_next_task_display)
self.task_update_timer.start(300000) # Mise à jour toutes les 5 minutes
def start_scan_timer(self, duration_seconds):
self.remaining_time = duration_seconds
self.update_time_display()
self.scan_timer.start(1000) # Met à jour toutes les secondes
def stop_scan_timer(self):
self.scan_timer.stop()
self.time_display.setText("--:--")
def update_time_display(self):
# Ne mettez pas à jour si l'afficheur est en pause
if hasattr(self, 'time_display_paused') and self.time_display_paused:
return
if self.remaining_time > 0:
minutes = self.remaining_time // 60
seconds = self.remaining_time % 60
self.time_display.setText(f"{minutes:02d}:{seconds:02d}")
self.remaining_time -= 1
else:
self.stop_scan_timer()
def calculate_scan_duration(self, max_emails, pause_time):
# Estimation du temps total basée sur le nombre d'emails et le temps de pause
estimated_duration = max_emails * (pause_time + 2) # +2 secondes pour le traitement
return estimated_duration
def create_browser_section(self):
group = QGroupBox("Navigateur et Journal")
layout = QHBoxLayout(group)
layout.setSpacing(5)
layout.setContentsMargins(0, 0, 0, 0)
# --- Container principal pour les contrôles ---
controls_container = QWidget()
controls_layout = QHBoxLayout(controls_container)
controls_layout.setSpacing(20) # Espace entre les deux groupes
controls_layout.setContentsMargins(5, 0, 5, 5)
# --- GROUPE ZOOM ---
zoom_container = QWidget()
zoom_layout = QVBoxLayout(zoom_container)
zoom_layout.setSpacing(4)
zoom_layout.setContentsMargins(0, 0, 0, 0)
zoom_layout.setAlignment(Qt.AlignCenter) # Centre tous les éléments
# Label Zoom
zoom_label = QLabel("Zoom affichage")
zoom_label.setStyleSheet("""
QLabel {
color: #FFFFFF;
font-size: 11px;
margin-bottom: 2px;
font-weight: bold;
}
""")
zoom_label.setAlignment(Qt.AlignCenter)
zoom_layout.addWidget(zoom_label)
# Container pour les boutons + et -
zoom_container = QWidget()
zoom_layout = QVBoxLayout(zoom_container)
zoom_buttons_layout.setSpacing(4) # Réduit l'espace entre les boutons
zoom_buttons_layout.setContentsMargins(0, 0, 0, 0)
zoom_buttons_layout.setAlignment(Qt.AlignCenter) # Centre les boutons
self.zoom_out_button = QPushButton("-")
self.zoom_in_button = QPushButton("+")
buttons_style = """
QPushButton {
background-color: #008CBA;
color: white;
border: none;
border-radius: 3px;
font-weight: bold;
}
QPushButton:hover {
background-color: #007B9A;
}
"""
# Configuration et ajout des boutons
for button in [self.zoom_out_button, self.zoom_in_button]:
button.setFixedSize(24, 24)
button.setStyleSheet(buttons_style)
zoom_buttons_layout.addWidget(button)
# --- GROUPE MODE D'AFFICHAGE ---
display_container = QWidget()
display_layout = QVBoxLayout(display_container)
display_layout.setSpacing(4)
display_layout.setContentsMargins(0, 0, 0, 0)
display_layout.setAlignment(Qt.AlignCenter)
# Label Mode d'affichage
display_label = QLabel("Mode d'affichage")
display_label.setStyleSheet("""
QLabel {
color: #FFFFFF;
font-size: 11px;
margin-bottom: 2px;
font-weight: bold;
}
""")
display_label.setAlignment(Qt.AlignCenter)
display_layout.addWidget(display_label)
self.toggle_browser_button = QPushButton("AGRANDIR")
self.toggle_browser_button.setFixedSize(85, 24)
self.toggle_browser_button.setStyleSheet("""
QPushButton {
background-color: #008CBA;
color: white;
border: none;
border-radius: 3px;
font-weight: bold;
font-size: 11px;
}
QPushButton:hover {
background-color: #007B9A;
}
""")
display_layout.addWidget(self.toggle_browser_button)
controls_layout.addWidget(zoom_container)
controls_layout.addWidget(display_container)
# Ajouter le container au layout du navigateur
browser_layout.addWidget(controls_container)
# Zone de texte pour le log
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setStyleSheet("""
QTextEdit {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #34495e;
border-radius: 4px;
padding: 10px;
font-family: Consolas, monospace;
}
QScrollBar:vertical {
background: #2d2d2d;
width: 12px;
margin: 0px;
}
QScrollBar::handle:vertical {
background-color: #4a4a4a;
min-height: 20px;
border-radius: 6px;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0px;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: none;
}
""")
log_layout.addWidget(self.log_text)
# Ajouter le conteneur de log au layout principal
layout.addWidget(log_container)
return group
def adjust_mini_browser_content(self):
try:
if self.mini_browser:
# Injecter du JavaScript pour ajuster le contenu
js_code = """
(function() {
// Permettre seulement le défilement horizontal
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.overflowX = 'auto';
document.body.style.overflowY = 'hidden';
document.body.style.overflowX = 'auto';
document.body.style.margin = '0';
document.body.style.padding = '0';
// SUPPRESSION DU SCALING AUTOMATIQUE - on garde seulement le contrôle du scroll
// Le zoom est maintenant contrôlé uniquement par setZoomFactor()
// Forcer le rafraîchissement
window.dispatchEvent(new Event('resize'));
})();
"""
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.page().runJavaScript(js_code)
except Exception as e:
self.log_action(f"Erreur lors de l'ajustement du mini navigateur: {str(e)}")
def initialize_browser_zoom(self):
"""Initialise le zoom du navigateur en fonction des paramètres ou du profil actuel"""
try:
# Obtenir le zoom par défaut des paramètres
settings = QSettings("VotreEntreprise", "AutoLook")
default_zoom = settings.value("mini_browser_zoom", 0.3, type=float)
# Vérifier si un profil est actif et récupérer son zoom
zoom_factor = default_zoom
if hasattr(self.main_window, 'current_profile') and self.main_window.current_profile:
current_profile = self.main_window.current_profile.replace("⭐ ", "")
profile_path = os.path.join(r"C:\autolook\profiles", f"{current_profile}.json")
if os.path.exists(profile_path):
try:
with open(profile_path, 'r', encoding='utf-8') as f:
profile_data = json.load(f)
if 'settings' in profile_data and 'browser_zoom' in profile_data['settings']:
zoom_factor = profile_data['settings']['browser_zoom']
logger.info(f"Zoom initialisé depuis le profil: {zoom_factor * 100:.0f}%")
except Exception as e:
logger.error(f"Erreur lors de l'initialisation du zoom: {str(e)}")
# CORRECTION : Appliquer le zoom au mini navigateur via l'onglet actuel
if self.mini_browser:
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.setZoomFactor(zoom_factor)
# Mettre à jour les tooltips des boutons de zoom
if hasattr(self, 'zoom_in_button') and hasattr(self, 'zoom_out_button'):
zoom_percentage = int(zoom_factor * 100)
tooltip_text = f'Zoom actuel: {zoom_percentage}%'
self.zoom_in_button.setToolTip(tooltip_text)
self.zoom_out_button.setToolTip(tooltip_text)
if hasattr(self, 'zoom_reset_button'):
self.zoom_reset_button.setToolTip(tooltip_text)
self.log_action(f"🔧 Zoom initialisé à {zoom_factor * 100:.0f}%")
return zoom_factor
except Exception as e:
logger.error(f"Erreur dans initialize_browser_zoom: {str(e)}")
self.log_action(f"❌ Erreur initialisation zoom: {str(e)}")
return 0.3 # Valeur par défaut en cas d'erreur
def save_zoom_level(self, zoom_level):
"""Sauvegarde le niveau de zoom dans un fichier texte simple"""
try:
import os
# S'assurer que le répertoire existe
zoom_dir = r"C:\AUTOLOOK"
os.makedirs(zoom_dir, exist_ok=True)
# Chemin du fichier de configuration - format texte simple
zoom_file = os.path.join(zoom_dir, "miniview.json")
# Sauvegarder le zoom en tant que texte simple
with open(zoom_file, 'w') as f:
f.write(str(zoom_level))
# Message de débogage
logger.info(f"Zoom sauvegardé: {zoom_level * 100:.0f}% dans {zoom_file}")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du niveau de zoom: {str(e)}")
def expand_all_folders(self):
"""Déplie tous les dossiers"""
for i in range(self.folder_listbox.count()):
item = self.folder_listbox.item(i)
data = item.data(Qt.UserRole)
if data['has_subfolders'] and not data['is_expanded']:
item.setHidden(False)
self.toggle_folder(item)
def collapse_all_folders(self):
"""Replie tous les dossiers"""
for i in range(self.folder_listbox.count()):
item = self.folder_listbox.item(i)
data = item.data(Qt.UserRole)
if data['has_subfolders'] and data['is_expanded']:
self.toggle_folder(item)
def refresh_folders(self):
"""Rafraîchit la liste des dossiers"""
if hasattr(self, 'mailbox_listbox') and self.mailbox_listbox.currentItem():
selected_mailbox = self.mailbox_listbox.currentItem().text()
self.update_folders(selected_mailbox)
self.log_action("Liste des dossiers actualisée")
def show_folder_stats(self):
"""Affiche les statistiques du dossier sélectionné"""
self.setCursor(Qt.WaitCursor) # Curseur de chargement
try:
if not self.folder_listbox.currentItem():
QMessageBox.warning(self, "Erreur", "Veuillez sélectionner un dossier")
return
self.log_action("Récupération des statistiques en cours...")
# Récupération correcte de l'ID du dossier depuis l'item
folder_data = self.folder_listbox.currentItem().data(Qt.UserRole)
if not folder_data or 'id' not in folder_data:
self.log_action("Erreur: Pas d'ID de dossier trouvé")
QMessageBox.warning(self, "Erreur", "Impossible de récupérer les informations du dossier")
return
folder_id = folder_data['id']
self.log_action(f"ID du dossier: {folder_id}")
if not hasattr(self, 'folder_stats'):
self.folder_stats = FolderStats(self.outlook_handler)
stats = self.folder_stats.get_folder_stats(folder_id)
if stats:
# Création de la boîte de dialogue
dialog = QDialog(self)
dialog.setWindowTitle(f"Statistiques - {stats['folder_name']}")
dialog.setMinimumWidth(600)
dialog.setMinimumHeight(400)
layout = QVBoxLayout()
# Création du texte des statistiques avec mise en forme HTML
stats_text = QTextEdit()
stats_text.setReadOnly(True)
# Formatage des statistiques avec du HTML pour un meilleur rendu
html_content = f"""
📊 Informations générales
• Nombre total de messages : {stats['total_messages']}
• Messages non lus : {stats['unread_messages']}
• Taux de lecture : {(100 - (stats['unread_messages'] / stats['total_messages'] * 100 if stats['total_messages'] > 0 else 0)):.1f}%
⏱️ Distribution temporelle
• Premier message : {stats['time_stats']['oldest_message']}
• Dernier message : {stats['time_stats']['newest_message']}
💾 Analyse de la taille
• Taille totale : {self._format_size(stats['size_stats']['total_size'])}
• Taille moyenne : {self._format_size(stats['size_stats']['average_size'])}
• Plus grand message : {self._format_size(stats['size_stats']['max_size'])}
👥 Top 5 des expéditeurs
Expéditeur |
Nombre de messages |
"""
for sender, count in list(stats['sender_stats'].items())[:5]:
html_content += f"""
{sender} |
{count} |
"""
html_content += """
"""
stats_text.setHtml(html_content)
layout.addWidget(stats_text)
# Boutons de contrôle
button_layout = QHBoxLayout()
refresh_button = QPushButton("Rafraîchir")
refresh_button.clicked.connect(lambda: self.refresh_stats(folder_id, stats_text))
close_button = QPushButton("Fermer")
close_button.clicked.connect(dialog.accept)
for button in [refresh_button, close_button]:
button.setStyleSheet("""
QPushButton {
background-color: #008CBA;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #007B9A;
}
""")
button_layout.addWidget(button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec_()
else:
self.log_action("Erreur: Impossible de récupérer les statistiques")
QMessageBox.warning(self, "Erreur", "Impossible de récupérer les statistiques du dossier")
except Exception as e:
self.log_action(f"Erreur lors de l'affichage des statistiques : {str(e)}")
QMessageBox.critical(self, "Erreur", f"Une erreur s'est produite : {str(e)}")
finally:
self.setCursor(Qt.ArrowCursor)
def refresh_stats(self, folder_id, stats_text):
"""Rafraîchit l'affichage des statistiques"""
try:
stats = self.folder_stats.get_folder_stats(folder_id)
if stats:
self.update_stats_display(stats, stats_text)
except Exception as e:
QMessageBox.warning(self, "Erreur",
f"Erreur lors du rafraîchissement des statistiques : {str(e)}")
def _format_size(self, size_in_bytes):
"""Formate une taille en bytes en format lisible"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_in_bytes < 1024:
return f"{size_in_bytes:.1f} {unit}"
size_in_bytes /= 1024
return f"{size_in_bytes:.1f} TB"
def sync_url(self):
"""Synchronise l'URL entre le mini navigateur et le navigateur normal"""
current_url = None
try:
# Obtenir l'URL actuellement affichée
if self.is_mini_browser and self.mini_browser:
current_url = self.mini_browser.web_view.url().toString()
elif not self.is_mini_browser and self.browser_window:
current_url = self.browser_window.browser.url().toString()
# Si nous avons une URL valide, la synchroniser entre les navigateurs
if current_url:
if self.is_mini_browser and self.browser_window:
self.browser_window.browser.load(QUrl(current_url))
elif not self.is_mini_browser and self.mini_browser:
self.mini_browser.load_url(url, display_time)
self.log_action(f"URL synchronisée: {current_url}")
except Exception as e:
self.log_action(f"Erreur de synchronisation URL: {str(e)}")
def toggle_browser_view(self):
"""Bascule entre le mini navigateur et le navigateur normal"""
try:
self.is_mini_browser = not self.is_mini_browser
if self.is_mini_browser:
# Masquer le navigateur normal et afficher le mini navigateur
if self.browser_window:
self.browser_window.hide()
if self.mini_browser:
current_tab = self.browser_window.get_current_tab()
current_url = current_tab.web_view.url().toString() if current_tab else None
if current_url:
mini_tab = self.mini_browser.get_current_tab()
if mini_tab:
mini_tab.web_view.load(QUrl(current_url))
self.mini_browser.setVisible(True)
self.toggle_browser_button.setText("AGRANDIR")
else:
# Masquer le mini navigateur et afficher le navigateur normal
if self.mini_browser:
self.mini_browser.setVisible(False)
mini_tab = self.mini_browser.get_current_tab()
current_url = mini_tab.web_view.url().toString() if mini_tab else None
if self.browser_window:
if current_url:
current_tab = self.browser_window.get_current_tab()
if current_tab:
current_tab.web_view.load(QUrl(current_url))
self.browser_window.show()
self.toggle_browser_button.setText("RÉDUIRE")
# Mise à jour du style du bouton
self.toggle_browser_button.setStyleSheet("""
QPushButton {
background-color: #008CBA;
color: white;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
min-width: 80px;
padding: 5px;
}
QPushButton:hover {
background-color: #007B9A;
}
""")
self.log_action(f"Vue du navigateur basculée vers {'mini' if self.is_mini_browser else 'normal'}")
except Exception as e:
self.log_action(f"Erreur lors du basculement de vue: {str(e)}")
def update_browser(self, url):
"""
VERSION MODIFIÉE : Charge réellement l'URL pour permettre l'interaction
"""
try:
# === PROTECTION CONTRE RECHARGEMENT PENDANT LA PAUSE ===
if hasattr(self, '_threads_suspended') and self._threads_suspended:
self.log_action(f"⏸️ Rechargement bloqué pendant la pause: {url[:50]}...")
return
if not isinstance(url, str):
return
display_time = self.outlook_handler.get_random_pause_time()
# ═══ CHARGER LA VRAIE URL ═══
if self.mini_browser:
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
# CHARGER L'URL RÉELLE au lieu d'injecter du contenu
current_tab.web_view.load(QUrl(url))
# Mettre à jour la console avec les infos
if hasattr(self.mini_browser, 'console_header'):
self.mini_browser.console_header.show()
self.mini_browser.console_header.update_page_time(f"Display {display_time:.1f}s")
# Enregistrer dans l'historique
if isinstance(self.mini_browser, BrowserWindow):
self.mini_browser.update_url(QUrl(url))
# === PROTECTION : Ne pas attendre si en pause ===
if not (hasattr(self, '_threads_suspended') and self._threads_suspended):
# Simulation du temps d'affichage seulement si pas en pause
from PyQt5.QtCore import QEventLoop, QTimer
loop = QEventLoop()
timer = QTimer()
timer.setSingleShot(True)
timer.timeout.connect(loop.quit)
timer.start(int(display_time * 1000))
# Vérifier régulièrement si on passe en pause
check_timer = QTimer()
def check_pause():
if hasattr(self, '_threads_suspended') and self._threads_suspended:
timer.stop()
check_timer.stop()
loop.quit()
check_timer.timeout.connect(check_pause)
check_timer.start(100) # Vérifier toutes les 100ms
loop.exec_()
check_timer.stop()
self.outlook_handler.update_links_clicked()
self.log_action(f"🌐 URL chargée: {url[:50]}... (temps: {display_time:.1f}s)")
except Exception as e:
self.log_action(f"Erreur chargement URL: {str(e)}")
def auto_fill_credentials(self, username=None, password=None):
"""
Remplit automatiquement les champs de connexion si disponibles
"""
try:
if not username and hasattr(self, 'login_field'):
username = self.login_field
if not password and hasattr(self, 'password_field'):
password = self.password_field
if not username or not password:
self.log_action("⚠️ Identifiants non configurés - utilisez la checkbox Authentification")
return
if hasattr(self, 'mini_browser') and self.mini_browser:
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
# JavaScript pour remplir automatiquement les champs
fill_js = f"""
(function() {{
// Chercher les champs de connexion les plus communs
var usernameSelectors = [
'input[type="email"]',
'input[name*="email"]',
'input[name*="username"]',
'input[name*="login"]',
'input[id*="email"]',
'input[id*="username"]',
'input[id*="login"]'
];
var passwordSelectors = [
'input[type="password"]',
'input[name*="password"]',
'input[name*="passwd"]',
'input[id*="password"]',
'input[id*="passwd"]'
];
// Remplir le nom d'utilisateur
for (var selector of usernameSelectors) {{
var field = document.querySelector(selector);
if (field) {{
field.value = '{username}';
field.dispatchEvent(new Event('input', {{ bubbles: true }}));
console.log('Username rempli:', selector);
break;
}}
}}
// Remplir le mot de passe
for (var selector of passwordSelectors) {{
var field = document.querySelector(selector);
if (field) {{
field.value = '{password}';
field.dispatchEvent(new Event('input', {{ bubbles: true }}));
console.log('Password rempli:', selector);
break;
}}
}}
// Essayer de trouver et cliquer le bouton de connexion
var loginButtons = [
'button[type="submit"]',
'input[type="submit"]',
'button:contains("Login")',
'button:contains("Sign in")',
'button:contains("Connexion")',
'[class*="login"]',
'[class*="signin"]'
];
return 'Champs remplis automatiquement';
}})();
"""
current_tab.web_view.page().runJavaScript(fill_js, self._on_credentials_filled)
except Exception as e:
self.log_action(f"❌ Erreur remplissage automatique : {e}")
def _on_credentials_filled(self, result):
"""Callback après remplissage des identifiants"""
if result:
self.log_action(f"✅ {result}")
else:
self.log_action("⚠️ Aucun champ de connexion détecté sur cette page")
def cleanup_browser_resources(self):
"""Nettoie les ressources du navigateur entre les traitements"""
try:
if self.mini_browser:
current_tab = self.mini_browser.get_current_tab()
if current_tab and hasattr(current_tab, 'web_view'):
# Nettoyer la page actuelle
current_tab.web_view.page().setUrl(QUrl("about:blank"))
# Forcer la collecte des déchets
current_tab.web_view.page().runJavaScript("window.gc && window.gc();")
# Forcer le nettoyage Python
import gc
gc.collect()
except Exception as e:
self.log_action(f"Erreur nettoyage navigateur: {str(e)}")
def log_action(self, message, category='system', auto_scroll=True):
# NOUVEAU: Vérification de suspension système
if hasattr(self, '_threads_suspended') and self._threads_suspended:
return # Ne pas logger pendant la pause système
# Protection contre les appels récursifs
if hasattr(self, '_logging_in_progress') and self._logging_in_progress:
return
self._logging_in_progress = True
# Protection contre les appels récursifs
if hasattr(self, '_logging_in_progress') and self._logging_in_progress:
return
self._logging_in_progress = True
"""
Méthode de logging avancée avec :
- Auto-détection des catégories
- Protection contre le spam
- Gestion des couleurs et icônes
- Contrôle d'activation par catégorie
"""
try:
from PyQt5.QtCore import QDateTime
from PyQt5.QtWidgets import QApplication
import time
# === 1. INITIALISATION DES SYSTÈMES ===
# Initialiser les catégories si pas encore fait
if not hasattr(self, 'log_categories'):
self.log_categories = {
'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True},
'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True},
'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True},
'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True},
'info': {'color': '#87CEEB', 'icon': 'ℹ️', 'enabled': True},
'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True},
'warning': {'color': '#FFA500', 'icon': '⚠️', 'enabled': True},
'debug': {'color': '#DDA0DD', 'icon': '🔧', 'enabled': True}
}
# Initialiser le système anti-spam si pas encore fait
if not hasattr(self, '_log_throttler'):
self._log_throttler = {
'last_log_time': {}, # Dernier timestamp par type de message
'log_counts': {}, # Compteur par type de message
'throttle_interval': 0.5, # 0.5 seconde entre messages similaires
'max_identical_logs': 5, # Maximum 5 messages identiques consécutifs
'last_debug_time': 0, # Pour afficher les stats périodiquement
'last_process_events': 0 # Pour limiter processEvents()
}
# Initialiser le nom du journal si pas encore fait
if not hasattr(self, 'current_log_name'):
self.current_log_name = f"Journal - {QDateTime.currentDateTime().toString('dd/MM HH:mm')}"
# === 2. AUTO-DÉTECTION DE CATÉGORIE ===
if category == 'system':
message_lower = message.lower()
# Détection d'erreur
if any(word in message_lower for word in ['erreur', 'error', 'échec', 'failed', 'exception']):
category = 'error'
# Détection de succès
elif any(word in message_lower for word in ['succès', 'success', 'terminé', 'completed', 'réussi']):
category = 'success'
# Détection d'email
elif any(word in message_lower for word in ['email', 'mail', 'message', 'envoi', 'réception']):
category = 'email'
# Détection de comptage
elif any(word in message_lower for word in ['comptage', 'count', 'nombre', 'total', 'trouvé']):
category = 'count'
# Détection de warning
elif any(word in message_lower for word in ['attention', 'warning', 'avertissement']):
category = 'warning'
# Détection de debug
elif any(word in message_lower for word in ['debug', 'débogage', 'test', 'vérification']):
category = 'debug'
# Détection d'info
elif any(word in message_lower for word in ['info', 'information', 'démarrage', 'initialisation']):
category = 'info'
# === 3. VÉRIFICATION SI CATÉGORIE ACTIVÉE ===
if not self.log_categories.get(category, {}).get('enabled', True):
return # Cette catégorie est désactivée
# === 4. SYSTÈME ANTI-SPAM ===
current_time = time.time()
throttler = self._log_throttler
# Créer une clé unique pour ce type de message
log_key = f"{category}:{hash(str(message))}"
# Vérifier si on doit limiter ce message
should_log = True
if log_key in throttler['last_log_time']:
time_diff = current_time - throttler['last_log_time'][log_key]
# Si le message est trop récent, le compter mais ne pas l'afficher
if time_diff < throttler['throttle_interval']:
throttler['log_counts'][log_key] = throttler['log_counts'].get(log_key, 0) + 1
should_log = False
else:
# Réinitialiser le compteur après l'intervalle
throttler['log_counts'][log_key] = 1
# Vérifier le nombre de messages identiques
count = throttler['log_counts'].get(log_key, 0)
if count > throttler['max_identical_logs']:
should_log = False
# Afficher les statistiques de suppression périodiquement
if current_time - throttler['last_debug_time'] > 10.0:
total_suppressed = sum(max(0, c - 1) for c in throttler['log_counts'].values())
if total_suppressed > 0:
print(f"==> OutlookHandler: {total_suppressed} messages supprimés (anti-spam)")
throttler['last_debug_time'] = current_time
# Si le message est supprimé par l'anti-spam, on sort
if not should_log:
return
# === 5. PRÉPARATION DU MESSAGE ===
# Mettre à jour les timestamps et compteurs
throttler['last_log_time'][log_key] = current_time
throttler['log_counts'][log_key] = throttler['log_counts'].get(log_key, 0) + 1
# Récupérer les infos de la catégorie
cat_info = self.log_categories.get(category, self.log_categories['system'])
icon = cat_info['icon']
color = cat_info['color']
# Créer le timestamp
timestamp = QDateTime.currentDateTime().toString("HH:mm:ss")
# Ajouter un compteur si le message a été répété
count = throttler['log_counts'][log_key]
message_with_count = message
if count > 1:
message_with_count += f" [x{count}]"
# === 6. FORMATAGE ET AFFICHAGE ===
# Créer l'entrée de log avec HTML
log_entry = f'{timestamp} {icon} {message_with_count}
'
# Vérifier que le widget de log existe
if hasattr(self, 'log_text') and self.log_text:
# Vérifier si le journal n'est pas désactivé
if not (hasattr(self, 'toggle_log_button') and self.toggle_log_button.isChecked()):
self.log_text.append(log_entry)
# Auto-scroll si demandé
if auto_scroll:
scrollbar = self.log_text.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
# === 7. TRAITEMENT DES ÉVÉNEMENTS (AVEC LIMITATION) ===
self._logging_in_progress = False
# Limiter processEvents() pour éviter les ralentissements
if current_time - throttler['last_process_events'] > 0.1: # Max 10 fois par seconde
QApplication.processEvents()
throttler['last_process_events'] = current_time
except Exception as e:
# Éviter les boucles infinies dans la gestion d'erreur
current_time = time.time()
if not hasattr(self, '_last_error_log') or (current_time - self._last_error_log) > 5.0:
print(f"Erreur dans log_action: {e}")
self._last_error_log = current_time
def sync_filters_to_main_log(self):
"""Synchronise les filtres vers le journal principal"""
try:
# Reconstruire le journal principal avec les filtres actuels
original_content = self.log_text.toHtml()
self.log_text.clear()
for line in original_content.split('\n'):
if ' 80 else ''}", auto_scroll=True)
if keywords_found:
self.log_action(f"🔍 Mots-clés trouvés: {', '.join(keywords_found)}", auto_scroll=True)
if link and link != 'Aucun lien':
self.log_action(f"🔗 Lien: {link}", auto_scroll=True)
if action == 'cliqué':
self.log_action(f"✅ Action: Lien cliqué avec succès", auto_scroll=True)
elif action == 'supprimé':
self.log_action(f"🗑️ Action: Email supprimé", auto_scroll=True)
elif action == 'ignoré':
self.log_action(f"⏩ Action: Email ignoré", auto_scroll=True)
else:
self.log_action(f"⚙️ Action: {action}", auto_scroll=True)
self.log_action(f"📧 ──────────────────────", auto_scroll=True)
except Exception as e:
self.log_action(f"❌ Erreur lors du log détaillé: {str(e)}", auto_scroll=True)
def log_scan_progress(self, current, total, action="Progression"):
"""
Log de progression sans défilement automatique
Args:
current (int): Numéro actuel
total (int): Total à traiter
action (str): Action en cours
"""
percentage = int((current / total) * 100) if total > 0 else 0
progress_bar = "█" * (percentage // 5) + "░" * (20 - (percentage // 5))
message = f"📊 {action}: {current}/{total} ({percentage}%) [{progress_bar}]"
self.log_action(message, auto_scroll=False) # Pas de défilement pour la progression
def log_batch_summary(self, batch_info):
"""
Log de résumé d'un lot traité
Args:
batch_info (dict): Informations sur le lot
- size: taille du lot
- clicked: liens cliqués
- deleted: emails supprimés
- errors: erreurs rencontrées
"""
try:
size = batch_info.get('size', 0)
clicked = batch_info.get('clicked', 0)
deleted = batch_info.get('deleted', 0)
errors = batch_info.get('errors', 0)
self.log_action("📊 " + "─" * 25 + " RÉSUMÉ LOT " + "─" * 25, auto_scroll=True)
self.log_action(f"📧 Emails traités: {size}", auto_scroll=True)
self.log_action(f"🔗 Liens cliqués: {clicked}", auto_scroll=True)
self.log_action(f"🗑️ Emails supprimés: {deleted}", auto_scroll=True)
if errors > 0:
self.log_action(f"❌ Erreurs: {errors}", auto_scroll=True)
else:
self.log_action("✅ Aucune erreur", auto_scroll=True)
self.log_action("📊 " + "─" * 56, auto_scroll=True)
except Exception as e:
self.log_action(f"❌ Erreur résumé lot: {str(e)}", auto_scroll=True)
def log_cycle_info(self, cycle_num, emails_in_cycle, pause_time):
"""
Log d'informations sur un cycle
Args:
cycle_num (int): Numéro du cycle
emails_in_cycle (int): Nombre d'emails dans ce cycle
pause_time (float): Temps de pause après le cycle
"""
try:
self.log_action("🔄 " + "═" * 20 + f" CYCLE {cycle_num} " + "═" * 20, auto_scroll=True)
self.log_action(f"📧 Emails à traiter: {emails_in_cycle}", auto_scroll=True)
self.log_action(f"⏱️ Pause après cycle: {pause_time:.1f} secondes", auto_scroll=True)
self.log_action("🔄 " + "═" * 47, auto_scroll=True)
except Exception as e:
self.log_action(f"❌ Erreur log cycle: {str(e)}", auto_scroll=True)
def log_final_summary(self, summary_data):
"""
Log du résumé final SANS défilement pour les décomptes
Args:
summary_data (dict): Données du résumé final
"""
try:
self.log_action("🏁 " + "═" * 15 + " TRAITEMENT TERMINÉ " + "═" * 15, auto_scroll=True)
total_processed = summary_data.get('total_processed', 0)
total_clicked = summary_data.get('total_clicked', 0)
total_deleted = summary_data.get('total_deleted', 0)
total_errors = summary_data.get('total_errors', 0)
doublons = summary_data.get('doublons', 0)
self.log_action(f"📧 Total emails traités: {total_processed}", auto_scroll=True)
self.log_action(f"🔗 Total liens cliqués: {total_clicked}", auto_scroll=True)
self.log_action(f"🗑️ Total emails supprimés: {total_deleted}", auto_scroll=True)
if doublons > 0:
self.log_action(f"🔶 Doublons détectés: {doublons}", auto_scroll=True)
if total_errors > 0:
self.log_action(f"❌ Total erreurs: {total_errors}", auto_scroll=True)
else:
self.log_action("✅ Aucune erreur rencontrée", auto_scroll=True)
# Calcul du taux de succès
if total_processed > 0:
success_rate = ((total_processed - total_errors) / total_processed) * 100
self.log_action(f"📈 Taux de succès: {success_rate:.1f}%", auto_scroll=True)
self.log_action("🏁 " + "═" * 50, auto_scroll=True)
except Exception as e:
self.log_action(f"❌ Erreur résumé final: {str(e)}", auto_scroll=True)
def setup_log_display(self):
"""
Configure l'affichage des logs
"""
# Configuration du QTextEdit pour les logs
self.log_text.setReadOnly(True)
self.log_text.setStyleSheet("""
QTextEdit {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #34495e;
border-radius: 4px;
padding: 10px;
font-family: Consolas, monospace;
}
QScrollBar:vertical {
background: #2d2d2d;
width: 12px;
margin: 0px;
}
QScrollBar::handle:vertical {
background-color: #4a4a4a;
min-height: 20px;
border-radius: 6px;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0px;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: none;
}
""")
# Définir une taille de police fixe pour une meilleure lisibilité
font = QFont("Consolas", 8)
self.log_text.setFont(font)
def update_mini_browser(self, url):
self.mini_browser.load(QUrl(url))
if len(self.current_urls) > 1 and not self.carousel_timer.isActive():
self.carousel_timer.start(self.outlook_handler.pause_time * 1000)
def update_full_browser(self, url):
self.browser_window.load_url(url, self.outlook_handler.pause_time)
def rotate_carousel(self):
if self.current_urls:
self.carousel_index = (self.carousel_index + 1) % len(self.current_urls)
self.mini_browser.load(QUrl(self.current_urls[self.carousel_index]))
def show_mini_browser(self):
self.browser_window.hide()
self.mini_browser.show()
if self.current_urls:
self.mini_browser.load(QUrl(self.current_urls[-1]))
def show_full_browser(self):
# Utiliser directement ha2 précédemment créé
self.browser_window.show()
if self.current_urls:
self.browser_window.load(self.current_urls[-1])
# Supprimer clear_tabs car c'est spécifique à BrowserWindow
if self.current_urls:
self.browser_window.load(self.current_urls[-1])
def validate_inputs_for_scan(self):
if not self.mailbox_listbox.currentItem() or not self.folder_listbox.currentItem():
QMessageBox.warning(self, "Erreur", "Veuillez sélectionner une boîte mail et un dossier.")
return False
if not self.link_entry.text():
QMessageBox.warning(self, "Erreur", "Veuillez entrer des mots-clés pour le lien.")
return False
if not self.max_emails_entry.text():
QMessageBox.warning(self, "Erreur", "Veuillez entrer un nombre maximum d'emails à scanner.")
return False
if not self.max_emails_entry.text().isdigit():
QMessageBox.warning(self, "Erreur", "Le nombre maximum d'emails doit être un nombre entier positif.")
return False
return True
def validate_inputs_for_count(self):
if not self.mailbox_listbox.currentItem() or not self.folder_listbox.currentItem():
QMessageBox.warning(self, "Erreur", "Veuillez sélectionner une boîte mail et un dossier.")
return False
if not self.link_entry.text():
QMessageBox.warning(self, "Erreur", "Veuillez entrer des mots-clés pour le lien.")
return False
return True
def customEvent(self, event):
if event.type() == QEvent.User:
url = event.data()
self.update_mini_browser(url)
def update_mailboxes(self):
mailboxes = self.outlook_handler.get_mailboxes()
self.mailbox_listbox.clear()
self.mailbox_listbox.addItems(mailboxes)
def on_mailbox_selected(self, item):
selected_mailbox = item.text()
self.update_folders(selected_mailbox)
def update_folders(self, mailbox):
"""Met à jour l'affichage des dossiers en ne montrant que la boîte de réception et ses sous-dossiers"""
try:
folders = self.outlook_handler.get_folders(mailbox)
self.folder_listbox.clear()
# Stocker la liste complète des dossiers
self.all_folders = folders
# Afficher la structure des dossiers
for folder_data in folders:
item = QListWidgetItem()
item.setText(folder_data['display_name'])
item.setData(Qt.UserRole, {
'id': folder_data['id'],
'level': folder_data['level'],
'has_subfolders': folder_data['has_subfolders'],
'is_expanded': folder_data.get('is_expanded', False),
'name': folder_data['name']
})
# Cacher les sous-dossiers par défaut
if folder_data['level'] > 0:
item.setHidden(True)
self.folder_listbox.addItem(item)
self.folder_listbox.itemClicked.connect(self.toggle_folder)
self.log_action(f"Liste des dossiers mise à jour pour {mailbox}")
except Exception as e:
self.log_action(f"Erreur lors de la mise à jour des dossiers : {str(e)}")
def toggle_folder(self, item):
"""Gère le dépliage/repliage d'un dossier"""
try:
data = item.data(Qt.UserRole)
if not data['has_subfolders']:
return
# Récupérer les informations actuelles
current_level = data['level']
is_expanded = data['is_expanded']
# Inverser l'état d'expansion
is_expanded = not is_expanded
# Mettre à jour le symbole et l'état
indent = ' ' * current_level
new_symbol = '▼' if is_expanded else '▶'
item.setText(f"{indent}{new_symbol} {data['name']}")
# Mettre à jour les données de l'item
data['is_expanded'] = is_expanded
item.setData(Qt.UserRole, data)
# Trouver l'index de l'élément actuel
current_idx = self.folder_listbox.row(item)
# Parcourir les éléments suivants jusqu'à trouver un élément de niveau égal ou supérieur
idx = current_idx + 1
while idx < self.folder_listbox.count():
next_item = self.folder_listbox.item(idx)
next_data = next_item.data(Qt.UserRole)
if next_data['level'] <= current_level:
break
# Si on déplie, montrer uniquement les éléments du niveau immédiatement inférieur
if is_expanded:
next_item.setHidden(next_data['level'] > current_level + 1)
else:
next_item.setHidden(True)
idx += 1
except Exception as e:
self.log_action(f"Erreur lors du basculement du dossier : {str(e)}")
print(f"Erreur détaillée: {str(e)}")
def validate_reward(self):
"""Utilise le reward_entry de la section principale"""
value = self.reward_entry.text().strip() # Utilise le champ de la section principale
if not value:
QMessageBox.warning(self, "Erreur de validation", "Le champ ne peut pas être vide.")
return
try:
value = float(value.replace(',', '.'))
if value <= 0:
raise ValueError("La valeur doit être positive")
self.outlook_handler.set_reward_value(value)
self.reward_entry.setEnabled(False) # Désactive le champ récompense
self.validate_reward_button.setEnabled(False) # Désactive le bouton valider
self.reset_reward_button.setEnabled(True) # Active le bouton RAZ
self.status_label.setText(f"Récompense validée : {value}")
except ValueError as e:
QMessageBox.warning(self, "Erreur de validation",
f"Erreur : {str(e)}\nVeuillez entrer une valeur numérique positive valide.")
def reset_reward(self):
"""Réinitialise le reward_entry de la section principale"""
self.reward_entry.clear() # Efface le champ récompense
self.reward_entry.setEnabled(True) # Réactive le champ
self.validate_reward_button.setEnabled(True) # Réactive le bouton valider
self.reset_reward_button.setEnabled(False) # Désactive le bouton RAZ
self.outlook_handler.reset_reward_value()
self.status_label.setText("Valeur de récompense réinitialisée")
self.update_reward_display()
def update_reward_display(self):
"""Met à jour l'affichage de la récompense"""
try:
if hasattr(self, 'outlook_handler') and hasattr(self.outlook_handler, 'total_reward'):
reward = int(self.outlook_handler.total_reward)
self.reward_display.setText(f"{reward:03d} ")
except Exception as e:
print(f"Erreur dans update_reward_display: {e}")
finally:
QApplication.processEvents()
def update_font_size(self, size):
"""Met à jour la taille de police incluant le champ récompense"""
self.font_size = size
font = QFont()
font.setPointSize(size)
widgets_to_update = [
self.mailbox_listbox,
self.folder_listbox,
self.sender_entry,
self.subject_entry,
self.link_entry,
self.reward_entry, # Inclut le champ récompense
self.max_emails_entry
]
for widget in widgets_to_update:
widget.setFont(font)
if self.outlook_handler:
self.outlook_handler.set_font_size(size)
def verifier_doublons(self):
self.log_action("Début de la vérification des doublons basée sur les liens")
doublons = []
links_hashes = {}
try:
pythoncom.CoInitialize()
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
folder = outlook.Folders[self.mailbox_listbox.currentItem().text()].Folders[
self.folder_listbox.currentItem().text()]
messages = folder.Items
link_keywords = self.link_entry.text().split()
self.log_action(f"Analyse du dossier : {folder.Name}")
for message in messages:
try:
# Extraire le lien pertinent avec find_valid_link
valid_link = self.outlook_handler.find_valid_link(message, link_keywords)
if valid_link:
# Utiliser uniquement le lien comme base du hash
hash_key = hashlib.md5(valid_link.encode()).hexdigest()
if hash_key in links_hashes:
doublons.append((message, links_hashes[hash_key]))
self.log_action(f"Doublon trouvé : {valid_link}")
else:
links_hashes[hash_key] = message
except Exception as e:
self.log_action(f"Erreur lors du traitement d'un message : {str(e)}")
self.log_action(f"Vérification terminée. {len(doublons)} doublons trouvés.")
return doublons
except Exception as e:
self.log_action(f"Erreur lors de la vérification des doublons : {str(e)}")
return []
finally:
pythoncom.CoUninitialize()
def gerer_doublons(self, doublons):
self.log_action("Début de la gestion des doublons")
dialog = DoublonsDialog(self, doublons, self.settings.value("afficher_popup_toujours", False, type=bool))
result = dialog.exec_()
if result == QDialog.Accepted:
self.log_action("Paramètre 'afficher_popup_toujours' mis à jour")
if doublons:
self.log_action("Suppression des doublons acceptée")
self.supprimer_doublons(doublons)
return True
else:
self.log_action("Aucun doublon à supprimer")
return False
def afficher_popup_doublons(self, doublons):
self.log_action("Affichage du popup de doublons")
dialog = QDialog(self)
dialog.setWindowTitle("Doublons détectés")
layout = QVBoxLayout()
label = QLabel(f"{len(doublons)} doublons trouvés. Suppression automatique dans 8 secondes...")
layout.addWidget(label)
list_widget = QListWidget()
for doublon, original in doublons:
list_widget.addItem(f"Doublon: {doublon.Subject} - Original: {original.Subject}")
layout.addWidget(list_widget)
button_layout = QHBoxLayout()
cancel_button = QPushButton("Annuler")
cancel_button.clicked.connect(dialog.reject)
button_layout.addWidget(cancel_button)
continue_button = QPushButton("Continuer")
continue_button.clicked.connect(dialog.accept)
button_layout.addWidget(continue_button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
timer = QTimer(dialog)
timer.timeout.connect(dialog.accept)
timer.start(8000) # 8 secondes
result = dialog.exec_()
if result == QDialog.Accepted:
self.log_action("Suppression des doublons acceptée")
self.supprimer_doublons(doublons)
return True
else:
self.log_action("Suppression des doublons annulée")
return False
def supprimer_doublons(self, doublons):
self.log_action("Début de la suppression des doublons")
for doublon, _ in doublons:
try:
doublon.Delete()
self.log_action(f"Doublon supprimé : {doublon.Subject}")
except Exception as e:
self.log_action(f"Erreur lors de la suppression du doublon {doublon.Subject}: {str(e)}")
self.log_action("Fin de la suppression des doublons")
# Vérification des doublons
self.log_action("Vérification des doublons en cours...")
doublons = self.verifier_doublons()
if not self.gerer_doublons(doublons):
self.log_action("Scan annulé.")
return
# Effacer le contenu du QTextEdit (afficheur)
self.log_text.clear()
self.log_action("Nouveau traitement démarré")
self.is_scanning = True
self.set_scanning_state(True)
self.status_label.setText("Scan en cours...")
self.progress_bar.setValue(0)
mailbox = self.mailbox_listbox.currentItem().text()
folder = self.folder_listbox.currentItem().text()
sender = self.sender_entry.text()
subject_keywords = self.subject_entry.text().split()
link_keywords = self.link_entry.text().split()
max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None
add_debug_log("OutlookWidget.start_scan",
f"Paramètres du scan - Mailbox: {mailbox}, Folder: {folder}, Sender: {sender}, Subject keywords: {subject_keywords}, Link keywords: {link_keywords}, Max emails: {max_emails}")
self.outlook_handler.scan_progress.connect(self.update_progress)
try:
add_debug_log("OutlookWidget.start_scan", "Appel de scan_emails")
clicked_links = self.outlook_handler.scan_emails(mailbox, folder, sender, subject_keywords,
link_keywords, max_emails)
self.log_action(f"Scan terminé. {clicked_links} liens cliqués.")
add_debug_log("OutlookWidget.start_scan", f"Scan terminé. {clicked_links} liens cliqués.")
if self.is_scanning:
self.status_label.setText(f"Scan terminé. {clicked_links} liens cliqués.")
self.update_reward_display()
else:
self.status_label.setText("Scan arrêté.")
except Exception as e:
self.log_action(f"Erreur lors du scan : {str(e)}")
add_debug_log("OutlookWidget.start_scan", f"⚠️ Erreur lors du scan : {str(e)}")
QMessageBox.critical(self, "Erreur", f"Une erreur s'est produite lors du scan : {str(e)}")
self.status_label.setText("Erreur lors du scan")
finally:
self.outlook_handler.scan_progress.disconnect(self.update_progress)
self.is_scanning = False
self.set_scanning_state(False)
self.progress_bar.setValue(100)
self.time_display.setText("--:--")
add_debug_log("OutlookWidget.start_scan", "Fin de start_scan")
self.log_action(
f"Paramètres du scan - Mailbox: {mailbox}, Folder: {folder}, Sender: {sender}, Subject keywords: {subject_keywords}, Link keywords: {link_keywords}, Max emails: {max_emails}")
write_debug_file()
def update_progress(self, value, current_count=None, max_emails=None, total_in_folder=None):
try:
# Mettre à jour la progression
if hasattr(self, 'is_scanning') and self.is_scanning:
if max_emails:
progress = min(int((current_count or 0) / max_emails * 100), 100)
self.progress_bar.setValue(progress)
else:
self.progress_bar.setValue(value)
else:
self.progress_bar.setValue(value)
# Mettre à jour le texte de progression
if hasattr(self, 'outlook_handler'):
self.progress_bar.setFormat(f" %p% ")
self.status_label.setText(f"Progression - {value}%")
# Mettre à jour le compteur si toutes les informations sont disponibles
if all(v is not None for v in [current_count, max_emails, total_in_folder]):
self.update_email_counter(current_count, max_emails, total_in_folder)
# Forcer la mise à jour de l'interface
QApplication.processEvents()
except Exception as e:
print(f"Erreur dans update_progress: {str(e)}")
logger.error(f"Erreur lors de la mise à jour de la progression: {str(e)}")
def handle_scan_error(self, error_message):
QMessageBox.critical(self, "Erreur", f"Une erreur s'est produite lors du scan : {error_message}")
self.status_label.setText("Erreur lors du scan")
self.finish_scan()
def cleanup_emails(self):
"""
Interface améliorée pour le nettoyage des emails
"""
# Validation des entrées
if not self.validate_inputs_for_count():
return
# Demander confirmation avec plus de détails
confirm_dialog = QMessageBox(self)
confirm_dialog.setWindowTitle("🧹 Confirmation de nettoyage")
confirm_dialog.setText("Comment voulez-vous nettoyer les emails ?")
confirm_dialog.setDetailedText(
"• 'Tous les emails' : Analyse tous les emails du dossier\n"
"• 'Nombre indiqué' : Limite l'analyse au nombre spécifié\n"
"• Les emails périmés seront SUPPRIMÉS définitivement"
)
confirm_dialog.setIcon(QMessageBox.Question)
all_button = confirm_dialog.addButton("Tous les emails", QMessageBox.YesRole)
limited_button = confirm_dialog.addButton("Nombre indiqué", QMessageBox.NoRole)
cancel_button = confirm_dialog.addButton("Annuler", QMessageBox.RejectRole)
confirm_dialog.exec_()
clicked_button = confirm_dialog.clickedButton()
if clicked_button == cancel_button:
self.log_action("🚫 Nettoyage annulé par l'utilisateur")
return
# Déterminer le nombre max d'emails
max_emails = None
if clicked_button == limited_button:
try:
max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None
except ValueError:
QMessageBox.warning(self, "Erreur", "Le nombre doit être un entier valide.")
return
# Préparation de l'interface
self.is_counting = True
self.status_label.setText("🧹 Nettoyage en cours...")
self.set_scanning_state(True)
self.start_count_animation()
self.progress_bar.setValue(0)
try:
# Récupération des paramètres
mailbox = self.mailbox_listbox.currentItem().text()
folder = self.folder_listbox.currentItem().text()
sender = self.sender_entry.text()
subject_keywords = self.subject_entry.text().split() if self.subject_entry.text() else []
link_keywords = self.link_entry.text().split()
# Vérification de la validité
validity_days = None
if self.validity_checkbox.isChecked() and self.validity_entry.text():
try:
validity_days = int(self.validity_entry.text())
self.log_action(f"📅 Critère de validité: {validity_days} jours")
except ValueError:
QMessageBox.warning(self, "Erreur", "La validité doit être un nombre entier")
return
# Callback de progression
def progress_handler(current, total):
if total > 0:
progress = int((current / total) * 100)
self.progress_bar.setValue(progress)
self.status_label.setText(f"🔍 Analyse: {current}/{total}")
QApplication.processEvents()
# APPEL DE LA NOUVELLE MÉTHODE
result = self.outlook_handler.cleanup_expired_emails(
mailbox, folder, sender, subject_keywords,
link_keywords, max_emails, validity_days,
progress_callback=progress_handler
)
# AFFICHAGE DÉTAILLÉ DES RÉSULTATS
if isinstance(result, dict):
# Mise à jour de l'affichage principal
deleted_count = result.get('deleted_count', 0)
expired_count = result.get('expired_count', 0)
valid_count = result.get('valid_count', 0)
self.count_label.setText(
f"✅ Valides: {valid_count} | "
f"🗑️ Supprimés: {deleted_count}/{expired_count}"
)
self.status_label.setText("🏁 Nettoyage terminé")
self.progress_bar.setValue(100)
# Boîte de dialogue de résumé détaillé
summary_dialog = QMessageBox(self)
summary_dialog.setWindowTitle("📊 Résumé du nettoyage")
summary_dialog.setIcon(QMessageBox.Information)
summary_text = f"""
🏁 NETTOYAGE TERMINÉ
📊 Résumé:
- Emails analysés: {result.get('processed_count', 0)}
- Emails valides conservés: {valid_count}
- Emails périmés détectés: {expired_count}
- Emails effectivement supprimés: {deleted_count}
{f"❌ Erreurs de suppression: {len(result.get('deletion_errors', []))}" if result.get('deletion_errors') else "✅ Aucune erreur"}
"""
summary_dialog.setText(summary_text)
# Détails des emails supprimés si demandé
if result.get('expired_details'):
details = "📋 Détails des emails supprimés:\n\n"
for email in result['expired_details'][:5]: # Limiter à 5 pour l'affichage
details += f"• {email['subject'][:40]}...\n"
details += f" De: {email['sender']}\n"
details += f" Âge: {email['age_days']} jours\n\n"
if len(result['expired_details']) > 5:
details += f"... et {len(result['expired_details']) - 5} autres emails"
summary_dialog.setDetailedText(details)
summary_dialog.exec_()
except Exception as e:
self.log_action(f"❌ Erreur lors du nettoyage: {str(e)}")
self.status_label.setText("❌ Erreur lors du nettoyage")
QMessageBox.critical(self, "Erreur", f"Erreur pendant le nettoyage:\n{str(e)}")
finally:
self.is_counting = False
self.stop_count_animation()
self.set_scanning_state(False)
def start_count_animation(self):
self.count_animation_step = 0
self.count_timer.start(300) # Met à jour toutes les 300 ms pour une animation plus rapide
self.count_button.setStyleSheet("""
QPushButton {
background-color: #FFA500; /* Orange pour plus de visibilité */
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
font-weight: bold;
}
""")
def stop_count_animation(self):
self.count_timer.stop()
self.count_button.setText("Nettoyer les emails")
self.count_button.setStyleSheet(self.button_style)
def update_count_button_text(self):
dots = "." * (self.count_animation_step % 4)
self.count_button.setText(f"Comptage{dots}")
self.count_animation_step += 1
QApplication.processEvents() # Force la mise à jour de l'interface
def set_scanning_state(self, is_active):
print(f"Définition de l'état de scan: {is_active}")
try:
# Gérer séparément les différents groupes de boutons
# Boutons qui doivent être désactivés pendant le scan (activés après)
for button_name in ['start_button', 'count_button', 'reset_reload_button']:
if hasattr(self, button_name):
button = getattr(self, button_name)
button.setEnabled(not is_active) # Désactivé pendant scan, activé après
# Boutons qui doivent être activés pendant le scan (désactivés après)
for button_name in ['stop_button', 'pause_button', 'skip_button']:
if hasattr(self, button_name):
button = getattr(self, button_name)
button.setEnabled(is_active) # Activé pendant scan, désactivé après
except Exception as e:
print(f"Erreur dans set_scanning_state: {e}")
def toggle_pause(self):
"""PAUSE SYSTÈME COMPLÈTE - Arrêt de TOUS les processus Python"""
if self.is_scanning or self.is_counting:
if self.pause_button.text() == "Pause":
# === PAUSE SYSTÈME TOTALE ===
self.pause_button.setText("Reprendre")
self.status_label.setText("🔴 PAUSE SYSTÈME - Page web libérée")
self.log_action("🔴 ARRÊT COMPLET DE TOUS LES PROCESSUS")
# 1. ARRÊTER TOUS LES TIMERS IMMÉDIATEMENT
self._stop_all_timers()
# 2. SUSPENDRE TOUS LES THREADS PYTHON
self._suspend_all_threads()
# 3. ARRÊTER TOUTES LES BOUCLES D'ÉVÉNEMENTS
self._stop_event_loops()
# 4. LIBÉRER TOUTE LA MÉMOIRE POSSIBLE
self._force_memory_cleanup()
# 5. ARRÊTER LE PROCESSING QT (sauf navigateur)
self._minimize_qt_processing()
# 6. SAUVEGARDER L'ÉTAT POUR LA REPRISE
self._save_system_state()
self.log_action("✅ SYSTÈME EN PAUSE - CPU/RAM libérés pour la page web")
# === 🎯 AJOUTER ÇA JUSTE APRÈS ===
# Test pour détecter quand ça redémarre
QTimer.singleShot(8000, lambda: self.log_action("⏰ 8 secondes écoulées"))
QTimer.singleShot(12000, lambda: self.log_action("⏰ 12 secondes écoulées"))
QTimer.singleShot(15000, lambda: self.log_action("⏰ 15 secondes écoulées"))
QTimer.singleShot(20000, lambda: self.log_action("⏰ 20 secondes écoulées"))
# === FIN TEST ===
else:
# === REPRISE SYSTÈME ===
self.pause_button.setText("Pause")
self.status_label.setText("🟢 SYSTÈME ACTIF")
self.log_action("🔄 REDÉMARRAGE DE TOUS LES PROCESSUS")
# 1. RESTAURER LES TIMERS
self._restore_all_timers()
# 2. REDÉMARRER LES THREADS
self._resume_all_threads()
# 3. RELANCER LES BOUCLES D'ÉVÉNEMENTS
self._restart_event_loops()
# 4. RESTAURER L'ÉTAT SYSTÈME
self._restore_system_state()
self.log_action("✅ SYSTÈME REDÉMARRÉ - Traitement normal")
def _stop_all_timers(self):
"""Arrête TOUS les timers et bloque tous les automatismes"""
try:
# === 🛑 MARQUAGE GLOBAL DU SYSTÈME EN PAUSE ===
self._system_paused = True
self._threads_suspended = True
self.paused_timers_state = {}
timers_stopped = 0
self.log_action("🔴 DÉBUT PAUSE SYSTÈME COMPLÈTE")
# === 🛑 BLOCAGE DES MÉTHODES AUTOMATIQUES ===
# Sauvegarder et bloquer les méthodes critiques
self._blocked_methods = {}
methods_to_block = [
'update_browser', 'process_batch', 'start_scan', 'scan_emails',
'process_next_email', 'continue_processing', 'finish_processing',
'update_time_display', 'update_progress', 'record_action'
]
for method_name in methods_to_block:
if hasattr(self, method_name):
# Sauvegarder la méthode originale
self._blocked_methods[method_name] = getattr(self, method_name)
# La remplacer par une version bloquée
def create_blocked_method(orig_name):
return lambda *args, **kwargs: self.log_action(f"🛑 {orig_name} bloqué (pause active)")
setattr(self, method_name, create_blocked_method(method_name))
# Bloquer aussi les méthodes du handler Outlook
if hasattr(self, 'outlook_handler'):
handler_methods = ['scan_emails', 'process_batch', 'update_links_clicked', 'safe_rpc_call']
for method_name in handler_methods:
if hasattr(self.outlook_handler, method_name):
self._blocked_methods[f'handler_{method_name}'] = getattr(self.outlook_handler, method_name)
def create_handler_blocked_method(orig_name):
return lambda *args, **kwargs: self.log_action(f"🛑 Handler.{orig_name} bloqué")
setattr(self.outlook_handler, method_name, create_handler_blocked_method(method_name))
# === 🛑 ARRÊT BRUTAL DU TIMER WIDGET ===
if hasattr(self, 'timer_widget') and self.timer_widget:
# Sauvegarder son état complet
self.paused_timer_widget_state = {
'text': self.timer_widget.text() if hasattr(self.timer_widget, 'text') else "",
'current_mode': getattr(self.timer_widget, 'current_mode', 'waiting'),
'was_visible': self.timer_widget.isVisible(),
'was_enabled': self.timer_widget.isEnabled()
}
# L'arrêter complètement
if hasattr(self.timer_widget, 'stop_all_internal_timers'):
self.timer_widget.stop_all_internal_timers()
# Couper tous les signaux/slots
if hasattr(self.timer_widget, 'blockSignals'):
self.timer_widget.blockSignals(True)
# Afficher un message fixe de pause
try:
self.timer_widget.set_text("⏸️ SYSTÈME EN PAUSE", "paused")
except:
pass
timers_stopped += 1
self.log_action("🛑 TimerWidget COMPLÈTEMENT arrêté")
# === 🛑 ARRÊT DE TOUS LES TIMERS PYTHON ===
# Timer principal de scan
if hasattr(self, 'scan_timer') and self.scan_timer.isActive():
self.paused_timers_state['scan_timer'] = {
'was_active': True,
'interval': self.scan_timer.interval(),
'remaining_time': getattr(self, 'remaining_time', 0)
}
self.scan_timer.stop()
timers_stopped += 1
self.log_action(f"⏸️ Timer scan arrêté - Temps: {getattr(self, 'remaining_time', 0)}")
# Tous les timers spécifiques
timer_attributes = [
'task_update_timer', 'count_timer', 'log_clear_timer',
'tip_animation_timer', 'tip_change_timer', 'tips_fade_timer',
'countdown_timer', 'summary_timer', 'resize_timer', 'panel_timer',
'page_timer', 'display_timer', 'reload_timer', 'process_timer'
]
for timer_name in timer_attributes:
if hasattr(self, timer_name):
timer = getattr(self, timer_name)
if hasattr(timer, 'isActive') and timer.isActive():
self.paused_timers_state[timer_name] = {
'was_active': True,
'interval': timer.interval()
}
timer.stop()
timers_stopped += 1
# Timers du handler Outlook
if hasattr(self, 'outlook_handler'):
handler_timers = ['keep_alive_timer', 'auto_disconnect_timer']
for timer_name in handler_timers:
if hasattr(self.outlook_handler, timer_name):
timer = getattr(self.outlook_handler, timer_name)
if hasattr(timer, 'isActive') and timer.isActive():
self.paused_timers_state[f'handler_{timer_name}'] = {
'was_active': True,
'interval': timer.interval()
}
timer.stop()
timers_stopped += 1
# === 🛑 RECHERCHE ET ARRÊT DE TOUS LES TIMERS CACHÉS ===
from PyQt5.QtCore import QTimer
import gc
hidden_timers_stopped = 0
for obj in gc.get_objects():
if isinstance(obj, QTimer) and obj.isActive():
try:
# Identifier le timer
parent_info = str(type(obj.parent())) if obj.parent() else "No parent"
timer_id = id(obj)
# S'assurer qu'il n'est pas déjà traité
if f'hidden_{timer_id}' not in self.paused_timers_state:
self.paused_timers_state[f'hidden_{timer_id}'] = {
'was_active': True,
'interval': obj.interval(),
'timer_object': obj,
'parent_info': parent_info
}
obj.stop()
hidden_timers_stopped += 1
except Exception as e:
# Timer inaccessible, on continue
pass
if hidden_timers_stopped > 0:
self.log_action(f"🛑 {hidden_timers_stopped} timers cachés arrêtés")
# === 🛑 PROTECTION SPÉCIALE CONTRE RESTART RÉSEAU ===
# Bloquer processEvents pour limiter les redémarrages automatiques
from PyQt5.QtCore import QCoreApplication
if not hasattr(self, '_original_processEvents'):
self._original_processEvents = QCoreApplication.processEvents
# Version limitée qui traite seulement les événements critiques
def limited_processEvents():
try:
if not getattr(self, '_system_paused', False):
self._original_processEvents()
except:
pass
QCoreApplication.processEvents = limited_processEvents
# === 🛑 MARQUER L'AFFICHAGE EN PAUSE ===
self.time_display_paused = True
total_stopped = timers_stopped + hidden_timers_stopped
self.log_action(f"🔴 PAUSE COMPLÈTE ACTIVÉE - {total_stopped} timers arrêtés")
self.log_action("🌐 Navigateur reste ACTIF pour utilisation manuelle")
except Exception as e:
self.log_action(f"❌ Erreur arrêt système: {e}")
import traceback
self.log_action(f"Stack: {traceback.format_exc()[-200:]}")
def _restore_all_timers(self):
"""Restaure tous les timers et automatismes"""
try:
# === 🔄 DÉMARQUAGE GLOBAL ===
self._system_paused = False
self._threads_suspended = False
self.time_display_paused = False
self.log_action("🟢 DÉBUT REPRISE SYSTÈME COMPLÈTE")
timers_restarted = 0
# === 🔄 RESTAURATION DES MÉTHODES BLOQUÉES ===
if hasattr(self, '_blocked_methods'):
for method_name, original_method in self._blocked_methods.items():
try:
if method_name.startswith('handler_'):
# Méthode du handler
real_method_name = method_name.replace('handler_', '')
if hasattr(self.outlook_handler, real_method_name):
setattr(self.outlook_handler, real_method_name, original_method)
else:
# Méthode normale
if hasattr(self, method_name):
setattr(self, method_name, original_method)
except Exception as e:
self.log_action(f"⚠️ Erreur restauration {method_name}: {e}")
del self._blocked_methods
self.log_action("🔄 Toutes les méthodes automatiques restaurées")
# === 🔄 RESTAURATION DU TIMER WIDGET ===
if hasattr(self, 'paused_timer_widget_state'):
try:
state = self.paused_timer_widget_state
# Réactiver les signaux
if hasattr(self.timer_widget, 'blockSignals'):
self.timer_widget.blockSignals(False)
# Restaurer la visibilité et l'état
self.timer_widget.setVisible(state['was_visible'])
self.timer_widget.setEnabled(state['was_enabled'])
# Restaurer le texte et le mode
try:
self.timer_widget.set_text(state['text'], state['current_mode'])
except:
# Si ça échoue, au moins remettre un état par défaut
self.timer_widget.set_text("Reprise...", "waiting")
# Redémarrer ses timers internes si nécessaire
if hasattr(self.timer_widget, 'restart_internal_timers'):
self.timer_widget.restart_internal_timers()
del self.paused_timer_widget_state
self.log_action("🔄 TimerWidget complètement restauré")
except Exception as e:
self.log_action(f"⚠️ Erreur restauration TimerWidget: {e}")
# === 🔄 RESTAURATION DES TIMERS PYTHON ===
if hasattr(self, 'paused_timers_state'):
# Timer principal en premier
if 'scan_timer' in self.paused_timers_state:
timer_state = self.paused_timers_state['scan_timer']
if timer_state['was_active'] and hasattr(self, 'scan_timer'):
# Restaurer le temps restant exact
self.remaining_time = timer_state.get('remaining_time', 0)
if self.remaining_time > 0:
self.scan_timer.start(timer_state['interval'])
timers_restarted += 1
self.log_action(f"▶️ Timer scan restauré - Temps: {self.remaining_time}")
# Tous les autres timers
for timer_name, timer_state in self.paused_timers_state.items():
if timer_name == 'scan_timer':
continue # Déjà fait
if not timer_state.get('was_active', False):
continue
try:
# Timer caché identifié par ID
if timer_name.startswith('hidden_'):
timer_obj = timer_state.get('timer_object')
if timer_obj and hasattr(timer_obj, 'start'):
timer_obj.start(timer_state['interval'])
timers_restarted += 1
# Timer du handler
elif timer_name.startswith('handler_'):
real_timer_name = timer_name.replace('handler_', '')
if hasattr(self.outlook_handler, real_timer_name):
timer = getattr(self.outlook_handler, real_timer_name)
if hasattr(timer, 'start'):
timer.start(timer_state['interval'])
timers_restarted += 1
# Timer normal
elif hasattr(self, timer_name):
timer = getattr(self, timer_name)
if hasattr(timer, 'start'):
timer.start(timer_state['interval'])
timers_restarted += 1
except Exception as e:
self.log_action(f"⚠️ Erreur restauration timer {timer_name}: {e}")
# Nettoyer l'état sauvegardé
del self.paused_timers_state
# === 🔄 RESTAURATION DE PROCESSEVENT ===
if hasattr(self, '_original_processEvents'):
from PyQt5.QtCore import QCoreApplication
QCoreApplication.processEvents = self._original_processEvents
del self._original_processEvents
self.log_action(f"🟢 REPRISE COMPLÈTE - {timers_restarted} timers redémarrés")
self.log_action("🚀 Tous les automatismes sont maintenant actifs")
except Exception as e:
self.log_action(f"❌ Erreur reprise système: {e}")
import traceback
self.log_action(f"Stack: {traceback.format_exc()[-200:]}")
def _suspend_all_threads(self):
"""Suspend tous les threads Python actifs"""
try:
import threading
import time
# Marquer tous les threads pour suspension
self._threads_suspended = True
# Obtenir la liste des threads actifs
active_threads = threading.active_count()
# Attendre que les threads se mettent en pause
time.sleep(0.1)
self.log_action(f"🧵 {active_threads} threads suspendus")
except Exception as e:
self.log_action(f"❌ Erreur suspension threads: {e}")
def _stop_event_loops(self):
"""Arrête toutes les boucles d'événements non-critiques"""
try:
from PyQt5.QtCore import QCoreApplication
# Réduire drastiquement la fréquence de traitement des événements
QCoreApplication.processEvents = lambda: None
# Marquer l'arrêt des boucles
self._event_loops_stopped = True
self.log_action("🔄 Boucles d'événements suspendues")
except Exception as e:
self.log_action(f"❌ Erreur arrêt boucles: {e}")
def _force_memory_cleanup(self):
"""Force le nettoyage complet de la mémoire"""
try:
import gc
import psutil
# Mesurer la mémoire avant
process = psutil.Process()
memory_before = process.memory_info().rss / 1024 / 1024
# Nettoyage agressif
gc.collect()
gc.collect() # Double nettoyage
gc.collect() # Triple nettoyage pour être sûr
# Vider les caches Python
if hasattr(self, 'scan_log_history'):
self.scan_log_history.clear()
if hasattr(self, 'report_data'):
self.report_data.clear()
# Mesurer après
memory_after = process.memory_info().rss / 1024 / 1024
memory_freed = memory_before - memory_after
self.log_action(f"🧹 Mémoire libérée: {memory_freed:.1f} MB")
except Exception as e:
self.log_action(f"❌ Erreur nettoyage mémoire: {e}")
def _minimize_qt_processing(self):
"""Réduit au minimum le traitement Qt"""
try:
from PyQt5.QtCore import QCoreApplication
# Réduire la priorité du processus
import psutil
import os
current_process = psutil.Process(os.getpid())
current_process.nice(psutil.BELOW_NORMAL_PRIORITY_CLASS if os.name == 'nt' else 10)
# Sauvegarder l'ancien processEvents
self._original_processEvents = QCoreApplication.processEvents
# Remplacer par une version minimale
def minimal_processEvents():
pass
QCoreApplication.processEvents = minimal_processEvents
self.log_action("⚡ Traitement Qt minimisé")
except Exception as e:
self.log_action(f"❌ Erreur minimisation Qt: {e}")
def _save_system_state(self):
"""Sauvegarde l'état du système pour la reprise"""
try:
import time
self.system_state = {
'remaining_time': getattr(self, 'remaining_time', 0),
'current_progress': self.progress_bar.value() if hasattr(self, 'progress_bar') else 0,
'is_scanning': self.is_scanning,
'is_counting': self.is_counting,
'pause_timestamp': time.time(),
'display_time_paused': True # Marquer que l'affichage est en pause
}
# Marquer globalement que le système est en pause
self.time_display_paused = True
self.log_action("💾 État système sauvegardé")
except Exception as e:
self.log_action(f"❌ Erreur sauvegarde état: {e}")
def _resume_all_threads(self):
"""Redémarre tous les threads"""
try:
# Marquer les threads comme actifs
self._threads_suspended = False
import threading
active_threads = threading.active_count()
self.log_action(f"🧵 {active_threads} threads réactivés")
except Exception as e:
self.log_action(f"❌ Erreur reprise threads: {e}")
def _restart_event_loops(self):
"""Redémarre les boucles d'événements"""
try:
from PyQt5.QtCore import QCoreApplication
# Restaurer le processEvents original
if hasattr(self, '_original_processEvents'):
QCoreApplication.processEvents = self._original_processEvents
# Restaurer la priorité normale
import psutil
import os
current_process = psutil.Process(os.getpid())
current_process.nice(psutil.NORMAL_PRIORITY_CLASS if os.name == 'nt' else 0)
self._event_loops_stopped = False
self.log_action("🔄 Boucles d'événements restaurées")
except Exception as e:
self.log_action(f"❌ Erreur restart boucles: {e}")
def _restore_system_state(self):
"""Restaure l'état du système"""
try:
if hasattr(self, 'system_state'):
state = self.system_state
# Restaurer les états
self.remaining_time = state.get('remaining_time', 0)
if hasattr(self, 'progress_bar'):
self.progress_bar.setValue(state.get('current_progress', 0))
# Restaurer l'affichage du temps
self.time_display_paused = False
# Calculer le temps écoulé pendant la pause
pause_duration = time.time() - state.get('pause_timestamp', 0)
self.log_action(f"⏱️ Pause de {pause_duration:.1f} secondes")
# Mettre à jour l'affichage du temps immédiatement
if self.remaining_time > 0:
minutes = int(self.remaining_time // 60)
seconds = int(self.remaining_time % 60)
self.time_display.setText(f"{minutes:02d}:{seconds:02d}")
del self.system_state
self.log_action("🔄 État système restauré")
except Exception as e:
self.log_action(f"❌ Erreur restauration état: {e}")
def skip_current_email(self):
if self.is_scanning:
# Appeler la méthode existante pour ignorer l'email
self.outlook_handler.skip_current_email()
self.log_action("Email actuel ignoré")
# Mettre à jour l'affichage de la récompense et des liens
self.update_reward_display()
self.update_links_clicked_display(self.outlook_handler.links_clicked)
# Réduire le temps restant pour refléter l'élément sauté
# Estimer le temps moyen par email (pause d'affichage + pause entre emails)
if hasattr(self, 'remaining_time') and self.remaining_time > 0:
# Calculer le temps moyen par email
avg_display_time = (self.outlook_handler.min_pause_time + self.outlook_handler.max_pause_time) / 2
avg_between_time = (
self.outlook_handler.min_between_pages_time + self.outlook_handler.max_between_pages_time) / 2
time_per_email = avg_display_time + avg_between_time
# Déduire ce temps du temps restant total (avec un minimum pour éviter les valeurs négatives)
self.remaining_time = max(0, self.remaining_time - time_per_email)
# Mettre à jour l'affichage immédiatement
minutes = int(self.remaining_time // 60)
seconds = int(self.remaining_time % 60)
self.time_display.setText(f"{minutes:02d}:{seconds:02d}")
# Si le temps restant atteint zéro, arrêter le timer
if self.remaining_time <= 0:
if hasattr(self, 'scan_timer'):
self.scan_timer.stop()
# Fonction pour afficher le rapport (à ajouter)
def display_report(self):
"""Affiche le rapport de traitement"""
try:
if hasattr(self.main_window, 'report_toggle') and self.main_window.report_toggle.isChecked():
report_content = self.generate_report()
if report_content:
# Importer ReportDialog directement depuis main.py
from main import ReportDialog
dialog = ReportDialog(report_content, self.main_window)
dialog.exec_()
except Exception as e:
self.log_action(f"Erreur lors de l'affichage du rapport : {str(e)}")
def generate_report(self):
"""Génère le contenu du rapport complet avec statistiques par cycle"""
# DÉBOGAGE : Log du nombre d'entrées
if hasattr(self, 'report_data'):
print(f"DEBUG generate_report: Nombre d'entrées dans report_data: {len(self.report_data)}")
else:
print("DEBUG generate_report: report_data n'existe pas")
report = "RAPPORT DE TRAITEMENT\n"
report += "=" * 50 + "\n\n"
# En-tête avec date et heure
current_time = QDateTime.currentDateTime().toString("dd/MM/yyyy HH:mm:ss")
report += f"Date du rapport: {current_time}\n\n"
if not hasattr(self, 'report_data') or not self.report_data:
report += "Aucune donnée de traitement n'a été enregistrée.\n\n"
return report
# Section des emails traités
report += "EMAILS TRAITÉS\n"
report += "-" * 30 + "\n\n"
# DÉBOGAGE : Analyser les données avant traitement
print("DEBUG generate_report: Analyse des données:")
for i, data in enumerate(self.report_data):
print(f" {i + 1}: {data.get('source', 'N/A')} - {data.get('datetime', 'N/A')}")
# SOLUTION SIMPLE : Utiliser directement report_data SANS déduplication
total_processed = len(self.report_data)
total_reward = 0
print(f"DEBUG generate_report: Total à afficher: {total_processed}")
# Traitement de chaque email
for idx, data in enumerate(self.report_data, 1):
report += f"Email traité #{idx}\n"
report += "-" * 20 + "\n"
report += f"Source: {data.get('source', 'Inconnu')}\n"
report += f"Titre: {data.get('subject', 'Titre non disponible')}\n"
report += f"Date/Heure: {data.get('datetime', 'Date inconnue')}\n"
report += f"Lien traité: {data.get('link', 'Lien non disponible')}\n"
# Formatage des temps
display_time = data.get('display_time', 'N/A')
pause_time = data.get('pause_time', 'N/A')
if isinstance(display_time, (int, float)):
display_time = f"{display_time:.1f}"
if isinstance(pause_time, (int, float)):
pause_time = f"{pause_time:.1f}"
report += f"Temps d'affichage: {display_time} secondes\n"
report += f"Temps de pause: {pause_time} secondes\n"
# Calcul de la récompense
reward = data.get('reward', 0)
if reward != 'N/A':
try:
reward_value = float(reward)
total_reward += reward_value
report += f"Récompense: {reward_value:.2f}\n"
except (ValueError, TypeError):
report += f"Récompense: {reward}\n"
else:
report += f"Récompense: N/A\n"
report += "=" * 40 + "\n\n"
# Section des statistiques globales
report += "\nSTATISTIQUES GLOBALES\n"
report += "-" * 30 + "\n"
# Calculer les emails périmés
expired_emails = len(getattr(self, 'deleted_emails', []))
valid_emails = total_processed - expired_emails
report += f"Emails valides: {valid_emails}\n"
report += f"Emails périmés: {expired_emails}\n"
report += f"Total général: {total_processed}\n"
report += f"Récompense totale: {total_reward:.2f}\n\n"
# Section des statistiques par cycle (si disponible)
if hasattr(self, 'cycle_stats') and self.cycle_stats:
report += "\nSTATISTIQUES PAR CYCLE\n"
report += "-" * 30 + "\n"
total_cycles = len(self.cycle_stats)
total_cycle_emails = 0
for cycle_num, stats in enumerate(self.cycle_stats, 1):
emails_processed = stats.get('emails_processed', 0)
total_cycle_emails += emails_processed
report += f"Cycle #{cycle_num}\n"
report += f" Nombre d'emails traités: {emails_processed}\n"
# Formatage du temps de pause entre cycles
pause_between_cycles = stats.get('pause_between_cycles', 'N/A')
if isinstance(pause_between_cycles, (int, float)):
pause_between_cycles = f"{pause_between_cycles:.1f}"
report += f" Temps de pause après le cycle: {pause_between_cycles} secondes\n"
report += f" Heure de début: {stats.get('start_time', 'N/A')}\n"
report += f" Heure de fin: {stats.get('end_time', 'N/A')}\n"
# Calcul de la durée totale du cycle
if 'start_time' in stats and 'end_time' in stats:
try:
start = QDateTime.fromString(stats['start_time'], "dd/MM/yyyy HH:mm:ss")
end = QDateTime.fromString(stats['end_time'], "dd/MM/yyyy HH:mm:ss")
if start.isValid() and end.isValid():
duration_secs = start.secsTo(end)
minutes = duration_secs // 60
seconds = duration_secs % 60
report += f" Durée totale: {minutes} min {seconds} sec\n"
except Exception:
report += f" Durée totale: Calcul impossible\n"
report += "-" * 20 + "\n"
# Statistiques globales des cycles
avg_emails_per_cycle = total_cycle_emails / total_cycles if total_cycles > 0 else 0
report += f"\nRésumé des cycles:\n"
report += f"Nombre total de cycles: {total_cycles}\n"
report += f"Total d'emails traités en cycles: {total_cycle_emails}\n"
report += f"Moyenne d'emails par cycle: {avg_emails_per_cycle:.1f}\n\n"
# Section des emails périmés détaillés (si présents)
if hasattr(self, 'deleted_emails') and self.deleted_emails:
report += "\nDÉTAIL DES EMAILS PÉRIMÉS\n"
report += "-" * 30 + "\n"
for idx, email in enumerate(self.deleted_emails, 1):
report += f"Email périmé #{idx}\n"
report += f"Sujet: {email.get('subject', 'Sujet inconnu')}\n"
report += f"Expéditeur: {email.get('sender', 'Expéditeur inconnu')}\n"
report += f"Date de réception: {email.get('received_date', 'Date inconnue')}\n"
report += f"Âge: {email.get('age_days', 'N/A')} jours\n"
report += "-" * 20 + "\n"
# Pied de rapport
report += "\nFin du rapport"
report += "\n" + "=" * 50 + "\n"
return report
# Fonction pour enregistrer les actions
def record_action(self, source, link, message=None):
"""
Enregistre une action pour le rapport en évitant les doublons
"""
try:
if not hasattr(self.main_window, 'report_toggle') or not self.main_window.report_toggle.isChecked():
return
import psutil
# Récupérer la date de réception
received_date = message.ReceivedTime.strftime("%Y-%m-%d %H:%M:%S") if message and hasattr(message,
'ReceivedTime') else QDateTime.currentDateTime().toString(
"yyyy-MM-dd HH:mm:ss")
# Récupérer le titre du mail
subject = message.Subject if message and hasattr(message, 'Subject') else "Titre non disponible"
# Créer une clé unique pour cette action
action_key = f"{source}_{received_date}_{link}"
# DÉBOGAGE : Log de la clé générée
print(f"DEBUG record_action: Clé générée = {action_key}")
# Vérifier si cette action a déjà été enregistrée
if not hasattr(self, 'processed_actions'):
self.processed_actions = set()
if action_key not in self.processed_actions:
# Enregistrer l'action uniquement si elle n'est pas déjà traitée
action_data = {
'source': source,
'datetime': received_date,
'link': link,
'subject': subject,
'reward': self.outlook_handler.reward_value if hasattr(self.outlook_handler,
'reward_value') else 'N/A',
'cpu_usage': psutil.cpu_percent(),
'ram_usage': psutil.virtual_memory().percent,
'display_time': round(self.outlook_handler.display_time, 1),
'pause_time': round(self.outlook_handler.between_time, 1)
}
if not hasattr(self, 'report_data'):
self.report_data = []
self.report_data.append(action_data)
self.processed_actions.add(action_key)
# DÉBOGAGE : Log du nombre d'actions
print(f"DEBUG record_action: Action ajoutée. Total report_data: {len(self.report_data)}")
self.log_action(f"Action enregistrée: {source} - Date: {received_date}")
else:
# DÉBOGAGE : Log des doublons détectés
print(f"DEBUG record_action: DOUBLON DÉTECTÉ pour clé: {action_key}")
except Exception as e:
self.log_action(f"Erreur lors de l'enregistrement de l'action: {str(e)}")
def update_stats_display(self, min_value=None, max_value=None):
"""Updates the statistics display in the existing statistics window
Args:
min_value: Optional minimum value (ignored for folder stats)
max_value: Optional maximum value (ignored for folder stats)
"""
try:
if not hasattr(self, 'folder_listbox') or not self.folder_listbox.currentItem():
return
folder_data = self.folder_listbox.currentItem().data(Qt.UserRole)
if not folder_data or 'id' not in folder_data:
return
folder_id = folder_data['id']
# Get stats with error handling
try:
if not hasattr(self, 'folder_stats'):
self.folder_stats = FolderStats(self.outlook_handler)
stats = self.folder_stats.get_folder_stats(folder_id)
if not stats:
return
# Mettre à jour le texte des statistiques directement dans le widget
messages_text = f"Messages totaux: {stats['total_messages']}\n"
messages_text += f"Messages non lus: {stats['unread_messages']}\n"
messages_text += f"Taux de lecture: {((stats['total_messages'] - stats['unread_messages']) / stats['total_messages'] * 100 if stats['total_messages'] > 0 else 0):.1f}%\n"
messages_text += f"Premier message: {stats['time_stats']['oldest_message']}\n"
messages_text += f"Dernier message: {stats['time_stats']['newest_message']}\n"
if hasattr(self, 'count_label'):
self.count_label.setText(messages_text)
# Log success
if hasattr(self, 'log_action'):
self.log_action(f"Statistiques mises à jour pour {stats['folder_name']}")
except Exception as e:
logger.error(f"Error getting folder stats: {str(e)}")
if hasattr(self, 'log_action'):
self.log_action(f"Error getting folder statistics: {str(e)}")
if hasattr(self, 'count_label'):
self.count_label.setText("Erreur lors de la récupération des statistiques")
except Exception as e:
logger.error(f"Error updating stats display: {str(e)}")
# Log error but continue execution
class CyclePauseTimer(QFrame):
"""Widget pour afficher le temps de pause entre les cycles"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("cyclePauseTimer")
self.setFixedHeight(60) # Hauteur fixe pour correspondre aux autres indicateurs
# Style avec des couleurs qui correspondent à l'interface existante
self.setStyleSheet("""
#cyclePauseTimer {
background-color: #1a2a3a;
border-radius: 4px;
padding: 4px;
}
QLabel {
color: white;
font-size: 12px;
}
QLabel#timerValue {
color: #65ddfb;
font-size: 16px;
font-weight: bold;
}
""")
# Layout vertical
layout = QVBoxLayout(self)
layout.setSpacing(1)
layout.setContentsMargins(2, 2, 2, 2)
# Titre
title_label = QLabel("⏱️ Pause")
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Valeur du timer
self.timer_value = QLabel("00:00")
self.timer_value.setObjectName("timerValue")
self.timer_value.setAlignment(Qt.AlignCenter)
layout.addWidget(self.timer_value)
# Timer pour le décompte
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_timer)
self.remaining_seconds = 0
self.next_cycle = 0
# Masquer initialement
self.hide()
def start_countdown(self, seconds, next_cycle):
"""Démarre le compte à rebours"""
self.remaining_seconds = max(1, int(seconds))
self.next_cycle = next_cycle
self.update_display()
self.show()
self.timer.start(1000) # Mise à jour chaque seconde
def update_timer(self):
"""Met à jour le timer à chaque seconde"""
self.remaining_seconds -= 1
if self.remaining_seconds <= 0:
self.timer.stop()
self.hide()
else:
self.update_display()
def update_display(self):
"""Met à jour l'affichage du temps restant"""
minutes = self.remaining_seconds // 60
seconds = self.remaining_seconds % 60
self.timer_value.setText(f"{minutes:02d}:{seconds:02d}")
# Changer la couleur en fonction du temps restant
if self.remaining_seconds <= 10:
self.timer_value.setStyleSheet("color: #FF5252; font-size: 16px; font-weight: bold;") # Rouge
elif self.remaining_seconds <= 30:
self.timer_value.setStyleSheet("color: #FFD740; font-size: 16px; font-weight: bold;") # Jaune
else:
self.timer_value.setStyleSheet("color: #65ddfb; font-size: 16px; font-weight: bold;") # Bleu
class TimerWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.setup_ui()
# État du timer
self.timer_running = False
self.current_mode = "waiting" # "waiting", "display" ou "pause"
self.remaining_time = 0.0
self.initial_time = 0.0
# Timer pour le décompte
self.countdown_timer = QTimer(self)
self.countdown_timer.timeout.connect(self.update_countdown)
self.countdown_timer.setInterval(100) # Mise à jour toutes les 100ms
self.timer_paused = False
self.paused_time = 0.0
def setup_ui(self):
"""Crée l'interface utilisateur de l'afficheur timer"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.setAlignment(Qt.AlignCenter)
# Afficheur du timer
self.timer_display = QLabel("----")
self.timer_display.setFixedSize(90, 24)
self.timer_display.setStyleSheet("""
background-color: #000000;
color: #FFFFFF;
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
layout.addWidget(self.timer_display)
def set_text(self, text, mode="waiting"):
"""
Méthode utilisée par process_batch pour définir le texte et lancer le décompte.
Gère les modes "waiting", "display", "pause".
Empêche le démarrage du timer pendant un chargement de profil.
"""
# Arrêter l'ancien timer s'il est en cours
self.countdown_timer.stop()
# Définir le mode
self.current_mode = mode
# TRAITEMENT SPÉCIAL pour le mode "waiting"
if mode == "waiting":
self.timer_running = False
self.remaining_time = 0.0
self.initial_time = 0.0
self.timer_display.setText(text)
self.timer_display.setStyleSheet("""
background-color: #000000;
color: #FFFFFF;
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
return
# Pour les modes "display" et "pause"
self.timer_running = True
# Extraire le temps du texte si possible
try:
time_value = float(text.split()[-1].replace("s", ""))
self.initial_time = time_value
self.remaining_time = time_value
except (ValueError, IndexError):
self.initial_time = 0.0
self.remaining_time = 0.0
# Définir la couleur et le préfixe
if mode == "display":
color = "#00FF00"
prefix = "👁️"
elif mode == "pause":
color = "#FF6B6B"
prefix = "⏲️"
else:
color = "#FFFFFF"
prefix = ""
# Mettre à jour l'affichage
if self.remaining_time > 0:
self.timer_display.setText(f"{prefix} {int(self.remaining_time)}s")
self.timer_display.setStyleSheet(f"""
background-color: #000000;
color: {color};
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
# ✅ Bloquer le démarrage du timer pendant le chargement d'un profil
if hasattr(self.parent, 'is_loading_profile') and self.parent.is_loading_profile:
print(f"⚠️: Démarrage du timer bloqué (chargement profil) - text='{text}', mode='{mode}'")
else:
self.countdown_timer.start()
else:
self.timer_display.setText(text)
self.timer_display.setStyleSheet(f"""
background-color: #000000;
color: {color};
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
def pause_timer(self):
"""Met en pause le timer"""
if self.timer_running:
self.countdown_timer.stop()
self.timer_paused = True
# Sauvegarder l'état actuel pour permettre une reprise ultérieure
self.paused_time = self.remaining_time
# Optionnellement, changer la couleur ou ajouter (PAUSE) au texte
current_text = self.timer_display.text()
self.timer_display.setText(f"{current_text} (PAUSE)")
self.timer_display.setStyleSheet("""
background-color: #000000;
color: #FF6B6B; /* Rouge pour indiquer la pause */
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
def resume_timer(self):
"""Reprend le timer à partir du point où il a été mis en pause"""
if hasattr(self, 'timer_paused') and self.timer_paused:
self.timer_paused = False
self.remaining_time = self.paused_time
# Restaurer l'affichage normal (enlever le texte PAUSE)
if self.current_mode == "display":
self.timer_display.setText(f"👁️ {int(self.remaining_time)}s")
color = "#00FF00" # Vert pour le mode display
elif self.current_mode == "pause":
self.timer_display.setText(f"⏲️ {int(self.remaining_time)}s")
color = "#FF6B6B" # Rouge pour le mode pause
else:
self.timer_display.setText("----")
color = "#FFFFFF" # Blanc pour le mode waiting
# Restaurer le style normal
self.timer_display.setStyleSheet(f"""
background-color: #000000;
color: {color};
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
# Redémarrer le timer si on était en mode d'affichage
if self.current_mode in ["display", "pause"] and self.remaining_time > 0:
self.countdown_timer.start()
self.timer_running = True
def update_countdown(self):
"""Met à jour le décompte du timer SANS faire défiler le journal"""
if not self.timer_running:
return
self.remaining_time = max(0, self.remaining_time - 0.1)
# Mettre à jour l'affichage en fonction du mode
if self.current_mode == "display":
self.timer_display.setText(f"👁️ {int(self.remaining_time)}s")
elif self.current_mode == "pause":
self.timer_display.setText(f"⏲️ {int(self.remaining_time)}s")
# Si le timer atteint 0
if self.remaining_time <= 0:
self.countdown_timer.stop()
self.timer_running = False
if self.current_mode == "pause":
self.current_mode = "waiting"
self.timer_display.setText("----")
self.timer_display.setStyleSheet("""
background-color: #000000;
color: #FFFFFF;
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
def reset_to_waiting(self):
"""Remet le timer en mode attente"""
self.countdown_timer.stop()
self.timer_running = False
self.current_mode = "waiting"
self.remaining_time = 0.0
self.timer_display.setText("----")
self.timer_display.setStyleSheet("""
background-color: #000000;
color: #FFFFFF;
font-family: Consolas;
font-size: 12px;
padding: 2px 5px;
border-radius: 3px;
qproperty-alignment: AlignCenter;
""")
class FlipBanner(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(468, 120)
# Charger le GIF
gif_path = os.path.join(BANNER_PATH, "EzClixA468x60.jpg")
self.gif = QMovie(gif_path)
self.gif.setScaledSize(QSize(468, 60))
self.setMovie(self.gif)
self.gif.start()
# Configuration de l'animation
self.current_angle = 0
self.animation = QPropertyAnimation(self, b"rotation")
self.animation.setDuration(1500) # 1.5 secondes par cycle
self.animation.setStartValue(0)
self.animation.setEndValue(180)
self.animation.setEasingCurve(QEasingCurve.Linear)
self.animation.finished.connect(self.restart_animation)
self.animation.start()
def restart_animation(self):
# Inverser la direction
start_value = self.animation.startValue()
end_value = self.animation.endValue()
self.animation.setStartValue(end_value)
self.animation.setEndValue(start_value)
self.animation.start()
def get_rotation(self):
return self.current_angle
def set_rotation(self, angle):
self.current_angle = angle
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
# Configurer la transformation
painter.translate(self.width() / 2, self.height() / 2)
painter.rotate(self.current_angle)
painter.translate(-self.width() / 2, -self.height() / 2)
# Dessiner l'image
super().paintEvent(event)
# Définir la propriété pour l'animation
rotation = pyqtProperty(float, get_rotation, set_rotation)
class ProcessedLinksHandler:
def __init__(self, storage_path="C:/AUTOLOOK/processed_links.json"):
self.storage_path = storage_path
self.processed_links = self._load_processed_links()
self._ensure_directory_exists()
def _ensure_directory_exists(self):
"""S'assure que le répertoire de stockage existe"""
directory = os.path.dirname(self.storage_path)
if not os.path.exists(directory):
os.makedirs(directory)
def _load_processed_links(self):
"""Charge les liens traités depuis le fichier JSON"""
try:
if os.path.exists(self.storage_path):
with open(self.storage_path, 'r') as f:
return json.load(f)
return {}
except Exception as e:
print(f"Erreur lors du chargement des liens traités: {e}")
return {}
def _save_processed_links(self):
"""Sauvegarde les liens traités dans le fichier JSON"""
try:
with open(self.storage_path, 'w') as f:
json.dump(self.processed_links, f, indent=4)
except Exception as e:
print(f"Erreur lors de la sauvegarde des liens traités: {e}")
def _generate_link_hash(self, link):
"""Génère un hash unique pour un lien"""
return hashlib.md5(link.encode()).hexdigest()
def is_link_processed(self, link):
"""Vérifie si un lien a déjà été traité"""
link_hash = self._generate_link_hash(link)
return link_hash in self.processed_links
def add_processed_link(self, link, metadata=None):
"""Ajoute un lien traité avec ses métadonnées"""
link_hash = self._generate_link_hash(link)
self.processed_links[link_hash] = {
'link': link,
'processed_date': datetime.now().isoformat(),
'metadata': metadata or {}
}
self._save_processed_links()
def get_link_info(self, link):
"""Récupère les informations d'un lien traité"""
link_hash = self._generate_link_hash(link)
return self.processed_links.get(link_hash)
def clear_old_links(self, days=30):
"""Supprime les liens plus anciens qu'un certain nombre de jours"""
current_time = datetime.now()
links_to_remove = []
for link_hash, data in self.processed_links.items():
processed_date = datetime.fromisoformat(data['processed_date'])
age = (current_time - processed_date).days
if age > days:
links_to_remove.append(link_hash)
for link_hash in links_to_remove:
del self.processed_links[link_hash]
if links_to_remove:
self._save_processed_links()
return len(links_to_remove)
return 0
def get_statistics(self):
"""Retourne des statistiques sur les liens traités"""
return {
'total_links': len(self.processed_links),
'oldest_link': min((data['processed_date'] for data in self.processed_links.values()), default=None),
'newest_link': max((data['processed_date'] for data in self.processed_links.values()), default=None)
}
class FolderMonitor(QObject):
folder_changed = pyqtSignal(str, dict) # Signal émis avec l'ID du dossier et les changements
folder_error = pyqtSignal(str, str) # Signal émis en cas d'erreur
def __init__(self, outlook_handler):
super().__init__()
self.outlook_handler = outlook_handler
self.monitored_folders = {} # {folder_id: last_state}
self.timer = QTimer()
self.timer.timeout.connect(self.check_folders)
self.monitoring_active = False
self.check_interval = 30000 # 30 secondes par défaut
def start_monitoring(self, folder_id):
"""Démarre la surveillance d'un dossier spécifique"""
try:
initial_state = self.get_folder_state(folder_id)
self.monitored_folders[folder_id] = initial_state
if not self.monitoring_active:
self.timer.start(self.check_interval)
self.monitoring_active = True
return True
except Exception as e:
self.folder_error.emit(folder_id, str(e))
return False
def stop_monitoring(self, folder_id=None):
"""Arrête la surveillance d'un dossier ou de tous les dossiers"""
if folder_id:
self.monitored_folders.pop(folder_id, None)
if not self.monitored_folders:
self.timer.stop()
self.monitoring_active = False
else:
self.monitored_folders.clear()
self.timer.stop()
self.monitoring_active = False
def get_folder_state(self, folder_id):
"""Récupère l'état actuel d'un dossier"""
try:
folder = self.outlook_handler.get_folder_by_id(folder_id)
return {
'total_items': folder.Items.Count,
'unread_items': len([item for item in folder.Items if not item.UnRead]),
'last_modification': folder.Items.Sort("[ReceivedTime]")[
0].ReceivedTime if folder.Items.Count > 0 else None,
'size': sum(item.Size for item in folder.Items)
}
except Exception as e:
self.folder_error.emit(folder_id, f"Erreur lors de la récupération de l'état: {str(e)}")
return None
def check_folders(self):
"""Vérifie les changements dans tous les dossiers surveillés"""
for folder_id, last_state in list(self.monitored_folders.items()):
try:
current_state = self.get_folder_state(folder_id)
if current_state and self.detect_changes(last_state, current_state):
changes = self.compute_changes(last_state, current_state)
self.folder_changed.emit(folder_id, changes)
self.monitored_folders[folder_id] = current_state
except Exception as e:
self.folder_error.emit(folder_id, str(e))
def detect_changes(self, old_state, new_state):
"""Détecte s'il y a eu des changements entre deux états"""
if not old_state or not new_state:
return False
return any(old_state[key] != new_state[key] for key in old_state)
def compute_changes(self, old_state, new_state):
"""Calcule les changements précis entre deux états"""
changes = {}
for key in old_state:
if old_state[key] != new_state[key]:
changes[key] = {
'old': old_state[key],
'new': new_state[key],
'diff': new_state[key] - old_state[key] if isinstance(old_state[key], (int, float)) else None
}
return changes
class FolderStats:
def __init__(self, outlook_handler):
self.outlook_handler = outlook_handler
self.stats_cache = {}
self.cache_timeout = 300 # 5 minutes
def get_folder_stats(self, folder_id, force_refresh=False):
"""Récupère les statistiques complètes d'un dossier"""
current_time = time.time()
if not force_refresh and folder_id in self.stats_cache:
cached_time, stats = self.stats_cache[folder_id]
if current_time - cached_time < self.cache_timeout:
return stats
try:
folder = self.outlook_handler.get_folder_by_id(folder_id)
stats = self._calculate_folder_stats(folder)
self.stats_cache[folder_id] = (current_time, stats)
return stats
except Exception as e:
print(f"Erreur lors du calcul des statistiques: {str(e)}")
return None
def _calculate_folder_stats(self, folder):
"""Calcule toutes les statistiques pour un dossier"""
items = folder.Items
total_items = items.Count
if total_items == 0:
return self._empty_folder_stats(folder.Name)
items.Sort("[ReceivedTime]")
stats = {
'folder_name': folder.Name,
'total_messages': total_items,
'unread_messages': len([item for item in items if not item.UnRead]),
'time_stats': self._calculate_time_stats(items),
'size_stats': self._calculate_size_stats(items),
'sender_stats': self._calculate_sender_stats(items),
'category_stats': self._calculate_category_stats(items),
}
return stats
def _calculate_time_stats(self, items):
"""Calcule les statistiques temporelles complètes"""
try:
if items.Count == 0:
return {
'oldest_message': None,
'newest_message': None,
'messages_by_month': {},
'messages_by_day': {}
}
oldest = None
newest = None
messages_by_month = {}
messages_by_day = {}
for item in items:
received_time = item.ReceivedTime
if not received_time:
continue
# Mise à jour du plus ancien/récent
if not oldest or received_time < oldest:
oldest = received_time
if not newest or received_time > newest:
newest = received_time
# Groupement par mois
month_key = received_time.strftime("%Y-%m")
messages_by_month[month_key] = messages_by_month.get(month_key, 0) + 1
# Groupement par jour
day_key = received_time.strftime("%Y-%m-%d")
messages_by_day[day_key] = messages_by_day.get(day_key, 0) + 1
return {
'oldest_message': oldest,
'newest_message': newest,
'messages_by_month': dict(sorted(messages_by_month.items())),
'messages_by_day': dict(sorted(messages_by_day.items()))
}
except Exception as e:
print(f"Erreur lors du calcul des statistiques temporelles : {str(e)}")
return {
'oldest_message': None,
'newest_message': None,
'messages_by_month': {},
'messages_by_day': {}
}
def _calculate_size_stats(self, items):
"""Calcule les statistiques de taille"""
try:
total_size = 0
max_size = 0
count = 0
for item in items:
size = getattr(item, 'Size', 0)
total_size += size
max_size = max(max_size, size)
count += 1
return {
'total_size': total_size,
'average_size': total_size / count if count > 0 else 0,
'max_size': max_size
}
except Exception as e:
print(f"Erreur lors du calcul des statistiques de taille : {str(e)}")
return {
'total_size': 0,
'average_size': 0,
'max_size': 0
}
def _calculate_sender_stats(self, items):
"""Calcule les statistiques par expéditeur"""
try:
sender_stats = {}
for item in items:
sender = getattr(item, 'SenderEmailAddress', 'Unknown')
sender_stats[sender] = sender_stats.get(sender, 0) + 1
return dict(sorted(sender_stats.items(), key=lambda x: x[1], reverse=True))
except Exception as e:
print(f"Erreur lors du calcul des statistiques d'expéditeur : {str(e)}")
return {}
def _calculate_category_stats(self, items):
"""Calcule les statistiques par catégorie"""
try:
category_stats = {}
for item in items:
categories = getattr(item, 'Categories', '').split(', ')
for category in categories:
if category:
category_stats[category] = category_stats.get(category, 0) + 1
return dict(sorted(category_stats.items(), key=lambda x: x[1], reverse=True))
except Exception as e:
print(f"Erreur lors du calcul des statistiques de catégorie : {str(e)}")
return {}
def _group_by_month(self, items):
"""Groupe les messages par mois"""
try:
monthly_counts = {}
for item in items:
received_time = item.ReceivedTime
if received_time:
month_key = received_time.strftime("%Y-%m")
monthly_counts[month_key] = monthly_counts.get(month_key, 0) + 1
return dict(sorted(monthly_counts.items()))
except Exception as e:
print(f"Erreur lors du groupement par mois : {str(e)}")
return {}
def _group_by_day(self, items):
"""Groupe les messages par jour"""
try:
daily_counts = {}
for item in items:
received_time = item.ReceivedTime
if received_time:
day_key = received_time.strftime("%Y-%m-%d")
daily_counts[day_key] = daily_counts.get(day_key, 0) + 1
return dict(sorted(daily_counts.items()))
except Exception as e:
print(f"Erreur lors du groupement par jour : {str(e)}")
return {}
def generate_report(self, folder_id):
"""Génère un rapport détaillé des statistiques"""
stats = self.get_folder_stats(folder_id)
if not stats:
return "Impossible de générer le rapport : statistiques non disponibles"
report = f"""
Rapport de statistiques pour le dossier {stats['folder_name']}
========================================================
Informations générales :
- Nombre total de messages : {stats['total_messages']}
- Messages non lus : {stats['unread_messages']}
- Taux de lecture : {((stats['total_messages'] - stats['unread_messages']) / stats['total_messages'] * 100):.1f}%
Distribution temporelle :
- Message le plus ancien : {stats['time_stats']['oldest_message']}
- Message le plus récent : {stats['time_stats']['newest_message']}
Statistiques des expéditeurs (Top 5) :
{self._format_sender_stats(stats['sender_stats'])}
Distribution par catégorie :
{self._format_category_stats(stats['category_stats'])}
Analyse de la taille :
- Taille totale : {self._format_size(stats['size_stats']['total_size'])}
- Taille moyenne : {self._format_size(stats['size_stats']['average_size'])}
- Plus grand message : {self._format_size(stats['size_stats']['max_size'])}
"""
return report
def _format_size(self, size_in_bytes):
"""Formate une taille en bytes en format lisible"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_in_bytes < 1024:
return f"{size_in_bytes:.1f} {unit}"
size_in_bytes /= 1024
return f"{size_in_bytes:.1f} TB"
def _format_sender_stats(self, sender_stats):
"""Formate les statistiques des expéditeurs"""
return "\n".join([f"- {sender}: {count} messages"
for sender, count in sorted(sender_stats.items(),
key=lambda x: x[1], reverse=True)[:5]])
def _empty_folder_stats(self, folder_name):
"""Retourne des statistiques par défaut pour un dossier vide"""
return {
'folder_name': folder_name,
'total_messages': 0,
'unread_messages': 0,
'time_stats': {'oldest_message': None, 'newest_message': None},
'size_stats': {'total_size': 0, 'average_size': 0, 'max_size': 0},
'sender_stats': {},
'category_stats': {}
}
class DoublonsDialog(QDialog):
def __init__(self, parent, doublons, afficher_toujours):
super().__init__(parent)
self.doublons = doublons
self.setWindowTitle("Vérification des doublons")
self.setLayout(QVBoxLayout())
if doublons:
self.layout().addWidget(QLabel(f"{len(doublons)} doublons trouvés."))
list_widget = QListWidget()
for doublon, original in doublons:
list_widget.addItem(f"Doublon: {doublon.Subject} - Original: {original.Subject}")
self.layout().addWidget(list_widget)
else:
self.layout().addWidget(QLabel("Aucun doublon trouvé."))
button_layout = QHBoxLayout()
self.cancel_button = QPushButton("Annuler")
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_button)
self.timer_label = QLabel("Suppression automatique dans 8 secondes...")
button_layout.addWidget(self.timer_label)
self.layout().addLayout(button_layout)
self.countdown = 8
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_countdown)
self.timer.start(1000) # Mise à jour toutes les secondes
def update_countdown(self):
self.countdown -= 1
if self.countdown > 0:
self.timer_label.setText(f"Suppression automatique dans {self.countdown} secondes...")
else:
self.timer.stop()
self.accept()
def reject(self):
self.timer.stop()
super().reject()
def accept(self):
self.timer.stop()
super().accept()
stop_trace()
def main():
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)
try:
app = QApplication(sys.argv)
logger.info("Démarrage de l'application")
main_window = QMainWindow()
main_window.font_size = 10
# Créer et configurer l'interface principale
main_interface = QWidget()
main_layout = QVBoxLayout(main_interface)
main_window.setCentralWidget(main_interface)
# Associer l'interface au main_window pour un accès futur
main_window.main_interface = main_interface
main_window.main_interface_layout = main_layout
# Méthode pour afficher un message d'erreur
def show_error_message(message):
QMessageBox.critical(main_window, "Erreur", message)
main_window.show_error_message = show_error_message
# Appeler ta fonction de configuration réelle
setup_interface(main_window)
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()