# 1. **JavaScript plus avancé** # Mettre à jour `QWebEngineSettings` pour être sûr que toutes les options JavaScript sont activées from modules.debug_trace import start_trace, stop_trace start_trace("ha2.py", print_vars=True) def setup_javascript_settings(page): page.settings().setAttribute(QWebEngineSettings.JavascriptEnabled, True) page.settings().setAttribute(QWebEngineSettings.LocalStorageEnabled, True) page.settings().setAttribute(QWebEngineSettings.PluginsEnabled, True) # Activer les plugins pour être plus proche de Chrome page.settings().setAttribute(QWebEngineSettings.AutoLoadImages, True) # Charger les images automatiquement pour simuler un comportement naturel page.settings().setAttribute(QWebEngineSettings.XSSAuditingEnabled, False) # Désactiver l'audit XSS pour éviter les restrictions page.settings().setAttribute(QWebEngineSettings.ErrorPageEnabled, True) # Activer les pages d'erreur personnalisées page.settings().setAttribute(QWebEngineSettings.HyperlinkAuditingEnabled, False) # Désactiver le suivi des hyperliens # 2. **Cookies et Stockage** # Assurez-vous que tous les types de cookies sont gérés correctement. def setup_cookies_and_storage(profile): # Force l'utilisation de cookies persistants profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) # Chemins de stockage cookies_path = os.path.join(os.getcwd(), "browser_cookies") cache_path = os.path.join(os.getcwd(), "browser_cache") storage_path = os.path.join(os.getcwd(), "persistent_storage") # Créer les dossiers s'ils n'existent pas os.makedirs(cookies_path, exist_ok=True) os.makedirs(cache_path, exist_ok=True) os.makedirs(storage_path, exist_ok=True) # Configurer les chemins profile.setCachePath(cache_path) profile.setPersistentStoragePath(storage_path) profile.setCookiesPath(cookies_path) # Si disponible dans votre version de PyQt # Activer JavaScript settings = profile.settings() settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) settings.setAttribute(QWebEngineSettings.JavascriptCanOpenWindows, True) # 3. **User Agent plus Réaliste** # Ajouter une logique qui modifie dynamiquement le User Agent à chaque chargement. user_agents = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:87.0) Gecko/20100101 Firefox/87.0" ] import random def set_random_user_agent(profile): profile.setHttpUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") # 4. **Optimisation pour paraître plus humain et permettre les Cloudflare** # Ajouter des délais aléatoires lors de la navigation import time def delayed_navigation(browser, url): delay = random.uniform(1, 5) # Ajouter un délai de 1 à 5 secondes time.sleep(delay) browser.setUrl(QUrl(url)) # Ajouter des en-têtes HTTP supplémentaires pour simuler un vrai navigateur from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo class RequestInterceptor(QWebEngineUrlRequestInterceptor): def interceptRequest(self, info): """ Intercepteur de requêtes HTTP avancé avec rotation d'User-Agent, en-têtes adaptatifs et gestion d'erreurs robuste """ try: # === VÉRIFICATIONS PRÉLIMINAIRES === if not hasattr(info, 'setHttpHeader'): logger.debug("L'objet info ne supporte pas setHttpHeader") return # Obtenir l'URL de la requête pour l'adaptation contextuelle request_url = "" try: if hasattr(info, 'requestUrl'): request_url = info.requestUrl().toString() except Exception: pass # === GESTION USER-AGENT ROTATIF === user_agent = self._get_adaptive_user_agent(request_url) try: if user_agent: info.setHttpHeader(b"User-Agent", user_agent.encode('utf-8')) logger.debug(f"User-Agent appliqué: {user_agent[:50]}...") except Exception as e: logger.debug(f"Erreur User-Agent: {e}") # Fallback avec User-Agent par défaut fallback_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" try: info.setHttpHeader(b"User-Agent", fallback_ua.encode('utf-8')) except Exception: pass # === EN-TÊTES DE BASE ADAPTATIFS === base_headers = self._get_base_headers(request_url) self._apply_headers_safely(info, base_headers) # === EN-TÊTES SPÉCIFIQUES AU NAVIGATEUR === browser_headers = self._get_browser_specific_headers(user_agent) self._apply_headers_safely(info, browser_headers) # === EN-TÊTES FETCH METADATA ADAPTATIFS === fetch_metadata = self._get_fetch_metadata_headers(info, request_url) self._apply_headers_safely(info, fetch_metadata) # === EN-TÊTES DE SÉCURITÉ === security_headers = self._get_security_headers() self._apply_headers_safely(info, security_headers) # === EN-TÊTES SUPPLÉMENTAIRES ANTI-DÉTECTION === anti_detection_headers = self._get_anti_detection_headers(request_url) self._apply_headers_safely(info, anti_detection_headers) logger.debug("✅ En-têtes HTTP appliqués avec succès") except Exception as e: logger.error(f"❌ Erreur critique dans interceptRequest: {e}") logger.debug(f"Trace complète: {traceback.format_exc()}") def _get_adaptive_user_agent(self, url): """Obtient un User-Agent adaptatif selon le contexte""" try: if hasattr(self, 'user_agent_manager') and self.user_agent_manager: # Si on a un gestionnaire de User-Agent, l'utiliser if url: domain = self._extract_domain(url) return self.user_agent_manager.get_for_domain(domain) else: return self.user_agent_manager.get_current() else: # Pool de User-Agents de haute qualité en rotation import random ua_pool = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0" ] return random.choice(ua_pool) except Exception as e: logger.debug(f"Erreur User-Agent adaptatif: {e}") return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" def _get_base_headers(self, url): """Génère les en-têtes de base adaptatifs""" import random # Variation dans Accept pour éviter la détection accept_variants = [ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" ] # Languages variants pour paraître plus naturel lang_variants = [ "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4", "fr,en-US;q=0.9,en;q=0.8" ] return { b"Accept": random.choice(accept_variants).encode('utf-8'), b"Accept-Language": random.choice(lang_variants).encode('utf-8'), b"Accept-Encoding": b"gzip, deflate, br, zstd", b"DNT": b"1", b"Connection": b"keep-alive", b"Upgrade-Insecure-Requests": b"1", b"Cache-Control": b"max-age=0" } def _get_browser_specific_headers(self, user_agent): """Génère des en-têtes spécifiques au navigateur détecté""" headers = {} if not user_agent: return headers try: if "Chrome" in user_agent and "Edg/" not in user_agent: # En-têtes pour Chrome version_match = user_agent.split("Chrome/")[1].split(".")[0] if "Chrome/" in user_agent else "125" headers.update({ b"Sec-CH-UA": f'"Google Chrome";v="{version_match}", "Chromium";v="{version_match}", "Not-A.Brand";v="99"'.encode( 'utf-8'), b"Sec-CH-UA-Mobile": b"?0", b"Sec-CH-UA-Platform": b'"Windows"', b"Sec-CH-UA-Platform-Version": b'"15.0.0"', b"Sec-CH-UA-Arch": b'"x86"', b"Sec-CH-UA-Bitness": b'"64"', b"Sec-CH-UA-Model": b'""' }) elif "Edg/" in user_agent: # En-têtes pour Edge version_match = user_agent.split("Edg/")[1].split(".")[0] if "Edg/" in user_agent else "125" headers.update({ b"Sec-CH-UA": f'"Microsoft Edge";v="{version_match}", "Chromium";v="{version_match}", "Not-A.Brand";v="99"'.encode( 'utf-8'), b"Sec-CH-UA-Mobile": b"?0", b"Sec-CH-UA-Platform": b'"Windows"' }) elif "Firefox" in user_agent: # Firefox n'utilise pas les Client Hints headers.update({ b"Accept": b"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" }) except Exception as e: logger.debug(f"Erreur en-têtes navigateur: {e}") return headers def _get_fetch_metadata_headers(self, info, url): """Génère les en-têtes Fetch Metadata adaptatifs selon le type de requête""" headers = {} try: # Déterminer le type de ressource et ajuster les en-têtes if hasattr(info, 'resourceType'): resource_type = info.resourceType() # Adaptation selon le type de ressource if resource_type == 0: # Document principal headers.update({ b"Sec-Fetch-Dest": b"document", b"Sec-Fetch-Mode": b"navigate", b"Sec-Fetch-Site": b"none", b"Sec-Fetch-User": b"?1" }) else: # Sous-ressources (images, CSS, JS, etc.) headers.update({ b"Sec-Fetch-Dest": b"empty", b"Sec-Fetch-Mode": b"cors", b"Sec-Fetch-Site": b"same-origin" }) else: # En-têtes par défaut pour navigation headers.update({ b"Sec-Fetch-Dest": b"document", b"Sec-Fetch-Mode": b"navigate", b"Sec-Fetch-Site": b"none", b"Sec-Fetch-User": b"?1" }) except Exception as e: logger.debug(f"Erreur Fetch Metadata: {e}") # Fallback headers = { b"Sec-Fetch-Dest": b"document", b"Sec-Fetch-Mode": b"navigate", b"Sec-Fetch-Site": b"none", b"Sec-Fetch-User": b"?1" } return headers def _get_security_headers(self): """Génère des en-têtes de sécurité supplémentaires""" return { b"X-Forwarded-Proto": b"https", b"X-Requested-With": b"", # Vide pour éviter la détection AJAX b"Origin": b"", # Sera rempli automatiquement si nécessaire b"Pragma": b"no-cache" } def _get_anti_detection_headers(self, url): """Génère des en-têtes anti-détection avancés""" import random, time headers = {} try: # Simulation d'en-têtes variables pour éviter la détection if random.random() < 0.7: # 70% du temps headers[b"X-Forwarded-For"] = self._generate_fake_ip().encode('utf-8') if random.random() < 0.5: # 50% du temps headers[b"X-Real-IP"] = self._generate_fake_ip().encode('utf-8') # Simulation d'un timestamp pour paraître plus naturel if random.random() < 0.3: # 30% du temps timestamp = str(int(time.time() * 1000)) headers[b"X-Request-ID"] = timestamp.encode('utf-8') except Exception as e: logger.debug(f"Erreur en-têtes anti-détection: {e}") return headers def _generate_fake_ip(self): """Génère une IP aléatoire réaliste""" import random # Pools d'IP réalistes (éviter les plages privées) public_ranges = [ (8, 8, 8, 8), # Google DNS (1, 1, 1, 1), # Cloudflare DNS (208, 67, 222, 222), # OpenDNS (4, 2, 2, 1), # Level3 ] base = random.choice(public_ranges) # Variation légère pour paraître réaliste return f"{base[0]}.{base[1]}.{base[2]}.{random.randint(1, 254)}" def _extract_domain(self, url): """Extrait le domaine principal d'une URL""" try: if '://' in url: domain = url.split('://')[1].split('/')[0] else: domain = url.split('/')[0] return domain.lower() except Exception: return "" def _apply_headers_safely(self, info, headers): """Applique les en-têtes de manière sécurisée avec gestion d'erreurs""" if not headers: return for header_name, header_value in headers.items(): try: info.setHttpHeader(header_name, header_value) except Exception as e: header_str = header_name.decode('utf-8') if isinstance(header_name, bytes) else str(header_name) logger.debug(f"Erreur en-tête {header_str}: {e}") continue def setup_request_interceptor(profile): interceptor = RequestInterceptor() profile.setUrlRequestInterceptor(None) # Ajouter des logs pour suivre le comportement def log_action(console_header, message): console_header.log_action(message) import sys import os import random import platform import time from datetime import datetime import logging from logging.handlers import RotatingFileHandler import win32gui import traceback import win32con import win32api import subprocess from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QVBoxLayout, QTabWidget, QFileDialog, QLineEdit, QHBoxLayout, QLabel, QPushButton, QProgressBar, QLabel, QComboBox, QMessageBox, QDesktopWidget, QFrame, QLineEdit, QToolBar, QStyle, QTableWidget, QTableWidgetItem, QHeaderView) from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEngineSettings, QWebEngineSettings, QWebEnginePage from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, QSize, pyqtSlot, QObject from PyQt5.QtGui import QMovie, QIcon, QFont # Configuration du logging logging.basicConfig(level=logging.DEBUG) logging.basicConfig() def setup_ha2_logger(): # Créer le dossier log s'il n'existe pas log_dir = r"C:\autolook\log" os.makedirs(log_dir, exist_ok=True) # Créer le logger spécifique pour ha2 logger = logging.getLogger('ha2') logger.setLevel(logging.DEBUG) # Empêcher la propagation aux loggers parents logger.propagate = False # Nettoyer les handlers existants if logger.handlers: logger.handlers.clear() # Créer le nom du fichier de log avec timestamp timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") log_file = os.path.join(log_dir, f"ha2_{timestamp}.log") # Handler pour le fichier file_handler = RotatingFileHandler( log_file, maxBytes=10*1024*1024, # 10 Mo max backupCount=5, encoding='utf-8' ) file_handler.setLevel(logging.DEBUG) file_formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) # Handler pour la console console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_formatter = logging.Formatter( '%(levelname)s - %(message)s' ) console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) return logger # Initialiser le logger logger = setup_ha2_logger() def is_compatible(): """ Vérifie la compatibilité du module avec le système de manière robuste. Cette méthode est requise par module_integrator. """ try: # Vérification de la version Python if sys.version_info < (3, 6): logger.error("Python 3.6 ou supérieur est requis.") return False # Vérification du système d'exploitation if platform.system() != "Windows": logger.error("Ce module ne fonctionne que sur Windows.") return False # Vérification de PyQt5 try: from PyQt5.QtWidgets import QApplication app = QApplication.instance() or QApplication([]) logger.info("PyQt5 est correctement installé.") except Exception as e: logger.error(f"Erreur PyQt5: {e}") return False # Vérification des chemins critiques critical_paths = { "Banner": r"C:\autolook\Banniere" } for name, path in critical_paths.items(): if not os.path.exists(path): logger.error(f"{name} n'est pas installé au bon emplacement: {path}") return False else: logger.info(f"{name} trouvé au chemin: {path}") # Vérification de win32gui try: import win32gui import win32con logger.info("win32gui est correctement installé.") except ImportError: logger.error("win32gui n'est pas installé correctement.") return False logger.info("Le module HA2 est compatible avec le système.") return True except Exception as e: logger.error(f"Erreur lors de la vérification de compatibilité : {e}") return False def get_config(): """ Retourne la configuration du module. Cette méthode est utilisée par module_integrator. """ return { 'name': 'ha2', 'version': '1.0', 'description': 'Alternative browser implementation with PyQt5', 'author': 'Auteur', 'dependencies': [ 'PyQt5', 'win32gui' ], 'installed': True, 'settings': { 'window_size': { 'width': 1200, 'height': 800 }, 'paths': { 'banner': r"C:\autolook\Banniere\banner.gif" }, 'default_url': 'https://www.snoown2klabs.com', 'console_header': { 'height': 60, 'background': '#2D3236' } } } class ConsoleHeader(QFrame): def __init__(self, parent=None): super().__init__(parent) # Dimensions configurables self.BUTTON_SIZE = 40 self.TIMER_WIDTH = 120 self.TIMER_HEIGHT = 30 self.setFixedHeight(0) # Réduire la hauteur puisqu'on supprime des éléments self.setStyleSheet("background-color: #2D3236;") # États du timer (garder pour la fonctionnalité mais pas d'affichage) self.timer_running = True self.is_paused = False self.current_mode = "waiting" self.page_time = 0.0 self.initial_page_time = 0.0 self.current_display_text = "----" # Statistiques de la page self.page_stats = { 'load_time': 0, 'resources_loaded': 0, 'js_errors': 0, 'redirects': 0, 'status_code': None, 'content_type': None, 'page_size': 0, 'last_refresh': None } self.setup_ui() self.setup_timers() def setup_ui(self): layout = QHBoxLayout(self) layout.setContentsMargins(8, 0, 8, 0) layout.setSpacing(0) # Left Frame left_frame = QFrame() left_frame.setStyleSheet("background-color: #2D3236;") left_layout = QHBoxLayout(left_frame) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(1) left_layout.setAlignment(Qt.AlignLeft) # Style commun pour les boutons carrés button_style = f""" QPushButton {{ background-color: #3498db; color: white; border: none; border-radius: 2px; padding: 0px; font-size: 8pt; width: {self.BUTTON_SIZE}px; height: {self.BUTTON_SIZE}px; min-width: {self.BUTTON_SIZE}px; min-height: {self.BUTTON_SIZE}px; max-width: {self.BUTTON_SIZE}px; max-height: {self.BUTTON_SIZE}px; }} QPushButton:hover {{ background-color: #2980b9; }} QPushButton:pressed {{ background-color: #2573a7; }} """ # Right Frame - Progress Bar right_frame = QFrame() right_frame.setStyleSheet("background-color: #2D3236;") right_layout = QHBoxLayout(right_frame) right_layout.setContentsMargins(0, 0, 0, 0) self.progress_bar = QProgressBar() self.progress_bar.setFixedWidth(100) self.setup_progress_bar_style() self.progress_bar.hide() right_layout.addWidget(self.progress_bar) layout.addWidget(right_frame) # Set layout stretches layout.setStretch(0, 0) # Left frame - no stretch layout.setStretch(1, 1) # Center frame - stretch layout.setStretch(2, 0) # Right frame - no stretch def setup_progress_bar_style(self): """ Configure le style de la barre de progression. """ self.progress_bar.setStyleSheet(""" QProgressBar { border: none; border-radius: 4px; background-color: rgba(40, 40, 40, 0.2); text-align: center; color: white; font-weight: bold; font-size: 12px; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); } QProgressBar::chunk { border-radius: 4px; background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2196F3, stop:0.4 #03A9F4, stop:0.6 #00BCD4, stop:1 #3F51B5 ); } """) def setup_timers(self): """Configure les timers pour le décompte""" self.page_timer = QTimer(self) self.page_timer.timeout.connect(self.update_page_time) def update_page_time(self, display_label=None, start_timer=True): """Met à jour l'affichage du temps avec gestion des couleurs et cycle complet""" try: if display_label: # Initialisation d'un nouveau décompte self.page_timer.stop() self.timer_running = False self.is_paused = False # Définir le mode et la couleur if "Display" in display_label: self.current_mode = "display" elif "Pause" in display_label: self.current_mode = "pause" else: self.current_mode = "waiting" # Extraire le temps du label try: self.page_time = float(display_label.split()[-1].replace("s", "")) except: self.page_time = 0.0 self.initial_page_time = self.page_time # Mettre à jour le texte d'affichage self.current_display_text = display_label # Démarrer le décompte self.timer_running = True if start_timer: self.page_timer.start(100) else: # Mise à jour continue du temps if self.timer_running and not self.is_paused: if self.page_time > 0: self.page_time = max(0, self.page_time - 0.1) # Mettre à jour l'affichage selon le mode if self.current_mode == "display": self.current_display_text = f"👁️ {int(self.page_time)}s" else: self.current_display_text = f"⏲️ {int(self.page_time)}s" if self.page_time <= 0: if self.current_mode == "display": # Passer en mode pause self.update_page_time("Pause 5.0s") else: # Fin du cycle complet self.timer_running = False self.page_timer.stop() self.current_display_text = "----" # Réinitialiser le bouton Timer si présent for btn in self.findChildren(QPushButton): if btn.text() == "Timer": btn.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; border-radius: 3px; padding: 0px; font-size: 10pt; width: 40px; height: 40px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #2573a7; } """) except Exception as e: print(f"Erreur dans update_page_time: {str(e)}") self.current_display_text = "Error" self.timer_running = False self.page_timer.stop() class NavigationBar(QFrame): def __init__(self, browser_window, parent=None): super().__init__(parent) self.browser_window = browser_window self.setFixedHeight(30) self.buttons = [] # Pour stocker les boutons self.history_window = None # Référence à la fenêtre d'historique # Gestionnaire d'historique (nouvelle approche plus robuste) self.history_manager = HistoryManager(browser_window, self) # Configuration de l'interface self.setup_ui() def setup_ui(self): layout = QHBoxLayout(self) layout.setContentsMargins(8, 0, 8, 0) layout.setSpacing(8) # Style commun pour les boutons button_style = """ QPushButton { background-color: #2b2b2b; color: #e0e0e0; border: 1px solid #3d3d3d; border-radius: 4px; padding: 2px; min-width: 24px; min-height: 24px; } QPushButton:hover { background-color: #383838; border: 1px solid #4d4d4d; } QPushButton:pressed { background-color: #404040; border: 1px solid #555555; } """ # Boutons de navigation standard nav_buttons = [ ("◄", None, self.go_back), # Flèche arrière Unicode ("►", None, self.go_forward), # Flèche avant Unicode ("🔄", None, self.reload_page), # Emoji de rechargement ("🏠", None, self.go_home), # Emoji maison pour Home ] for text, icon, callback in nav_buttons: button = QPushButton(text) # Utilisation du texte Unicode directement if icon is not None: # Vérifie si une icône est définie avant de l'appliquer button.setIcon(self.style().standardIcon(icon)) button.setStyleSheet(button_style) button.setToolTip(text) button.clicked.connect(callback) layout.addWidget(button) # Bouton pour afficher/masquer l'historique self.history_button = QPushButton() self.history_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogListView)) self.history_button.setStyleSheet(button_style) self.history_button.setToolTip("Afficher/Masquer l'historique") self.history_button.clicked.connect(self.toggle_history) layout.addWidget(self.history_button) # Style des boutons avec couleur bleu ciel zoom_button_style = """ QPushButton { font-size: 8pt; font-weight: bold; color: #00BFFF; /* Bleu ciel */ background-color: transparent; border: none; padding: 5px; } QPushButton:hover { color: #1E90FF; /* Bleu plus foncé au survol */ } QPushButton:pressed { color: #4682B4; /* Bleu acier lors du clic */ } """ # Bouton Zoom Out avec emoji Unicode et couleur self.zoom_out_button = QPushButton("➖") # Icône Zoom Out self.zoom_out_button.setStyleSheet(zoom_button_style) self.zoom_out_button.setToolTip("Zoom Out") self.zoom_out_button.clicked.connect(self.zoom_out) layout.addWidget(self.zoom_out_button) # Bouton Zoom In avec emoji Unicode et couleur self.zoom_in_button = QPushButton("➕") # Icône Zoom In self.zoom_in_button.setStyleSheet(zoom_button_style) self.zoom_in_button.setToolTip("Zoom In") self.zoom_in_button.clicked.connect(self.zoom_in) layout.addWidget(self.zoom_in_button) # Ajout des boutons Console et Switch après le bouton History self.console_button = QPushButton() self.console_button.setIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) self.console_button.setStyleSheet(button_style) self.console_button.setToolTip("Console") self.console_button.clicked.connect(self.toggle_console) layout.addWidget(self.console_button) self.switch_button = QPushButton() self.switch_button.setIcon(self.style().standardIcon(QStyle.SP_BrowserStop)) self.switch_button.setStyleSheet(button_style) self.switch_button.setToolTip("Switch Browser") self.switch_button.clicked.connect(self.toggle_browser_view) layout.addWidget(self.switch_button) layout.addStretch() # Container pour la barre d'URL url_container = QFrame() url_container.setFixedWidth(600) url_layout = QHBoxLayout(url_container) url_layout.setContentsMargins(0, 0, 0, 0) url_layout.setSpacing(0) # Barre d'URL self.url_bar = QLineEdit() self.url_bar.setStyleSheet(""" QLineEdit { background-color: #404040; color: #ffffff; border: 1px solid #555555; border-radius: 4px; padding: 2px 5px; selection-background-color: #666666; font-size: 10pt; } """) # Connecter le signal returnPressed pour charger l'URL lorsque l'utilisateur appuie sur Entrée self.url_bar.returnPressed.connect(self.load_url) url_layout.addWidget(self.url_bar) layout.addWidget(url_container) # Ajout d'un stretch pour pousser la barre de progression vers le centre layout.addStretch() # Container pour la barre de progression (au centre) progress_container = QFrame() progress_container.setFixedWidth(200) progress_layout = QHBoxLayout(progress_container) progress_layout.setContentsMargins(5, 0, 5, 0) progress_layout.setSpacing(0) # Barre de progression self.progress_bar = QProgressBar() self.progress_bar.setStyleSheet(""" QProgressBar { border: none; border-radius: 4px; background-color: rgba(40, 40, 40, 0.2); text-align: center; color: white; font-weight: bold; font-size: 12px; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); } QProgressBar::chunk { border-radius: 4px; background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2196F3, stop:0.4 #03A9F4, stop:0.6 #00BCD4, stop:1 #3F51B5 ); } """) progress_layout.addWidget(self.progress_bar) layout.addWidget(progress_container) # Ajout d'un second stretch pour centrer la barre de progression layout.addStretch() # Container pour la barre de progression (au centre) progress_container = QFrame() progress_container.setFixedWidth(200) progress_layout = QHBoxLayout(progress_container) progress_layout.setContentsMargins(5, 0, 5, 0) progress_layout.setSpacing(0) # Barre de progression self.progress_bar = QProgressBar() self.progress_bar.setStyleSheet(""" QProgressBar { border: none; border-radius: 4px; background-color: rgba(40, 40, 40, 0.2); text-align: center; color: white; font-weight: bold; font-size: 12px; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); } QProgressBar::chunk { border-radius: 4px; background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2196F3, stop:0.4 #03A9F4, stop:0.6 #00BCD4, stop:1 #3F51B5 ); } """) progress_layout.addWidget(self.progress_bar) layout.addWidget(progress_container) # Ajout d'un second stretch pour centrer la barre de progression layout.addStretch() from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl def load_url(self): url = self.url_bar.text().strip() if not url: return # S'assurer que l'URL a un préfixe approprié if not url.startswith(('http://', 'https://')): url = 'https://' + url # CORRECTION 4: Mettre à jour le texte de la barre d'URL pour une cohérence immédiate self.url_bar.setText(url) # Utiliser le browser_window pour charger l'URL if self.browser_window: self.browser_window.load_url(url) def zoom_in(self): current_tab = self.browser_window.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, 5.0) # Maximum zoom 500% current_tab.web_view.setZoomFactor(new_zoom) self.update_zoom_tooltip(new_zoom) def zoom_out(self): current_tab = self.browser_window.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.1) # Minimum zoom 10% current_tab.web_view.setZoomFactor(new_zoom) self.update_zoom_tooltip(new_zoom) def update_zoom_tooltip(self, zoom_factor): zoom_percentage = int(zoom_factor * 100) self.zoom_in_button.setToolTip(f"Zoom actuel: {zoom_percentage}%") self.zoom_out_button.setToolTip(f"Zoom actuel: {zoom_percentage}%") def toggle_console(self): if not hasattr(self, 'status_window') or not self.status_window.isVisible(): self.status_window = QWidget() self.status_window.setWindowTitle("Console de monitoring") self.status_window.setGeometry(300, 300, 600, 400) self.status_window.setStyleSheet("background-color: #2D3236; color: white;") # Layout principal layout = QVBoxLayout(self.status_window) # Tableau des informations info_table = QTableWidget() info_table.setColumnCount(2) info_table.setHorizontalHeaderLabels(["Métrique", "Valeur"]) info_table.horizontalHeader().setStretchLastSection(True) info_table.setRowCount(10) # Style du tableau info_table.setStyleSheet(""" QTableWidget { background-color: #363940; gridline-color: #4a4d56; border: none; } QTableWidget::item { padding: 5px; color: white; } QHeaderView::section { background-color: #2D3236; color: white; padding: 5px; border: none; } """) # Remplissage des données metrics = [ ("État de la page", "En cours de chargement..."), ("Temps de chargement", "0 ms"), ("Ressources chargées", "0"), ("Erreurs JavaScript", "0"), ("Redirections", "0"), ("Code de statut HTTP", "-"), ("Type de contenu", "-"), ("Taille de la page", "0 KB"), ("Dernier rafraîchissement", "-"), ("Sécurité", "Non vérifié") ] for i, (metric, value) in enumerate(metrics): info_table.setItem(i, 0, QTableWidgetItem(metric)) info_table.setItem(i, 1, QTableWidgetItem(value)) layout.addWidget(info_table) self.info_table = info_table # Boutons de contrôle button_layout = QHBoxLayout() refresh_button = QPushButton("Rafraîchir") refresh_button.clicked.connect(self.refresh_stats) refresh_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 5px 15px; border-radius: 3px; } QPushButton:hover { background-color: #45a049; } """) clear_button = QPushButton("Effacer") clear_button.clicked.connect(self.clear_stats) clear_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; padding: 5px 15px; border-radius: 3px; } QPushButton:hover { background-color: #da190b; } """) button_layout.addWidget(refresh_button) button_layout.addWidget(clear_button) layout.addLayout(button_layout) # Connexion aux signaux du navigateur if hasattr(self.parent(), 'web_view'): web_view = self.parent().web_view web_view.loadStarted.connect(self.on_load_started) web_view.loadFinished.connect(self.on_load_finished) web_view.loadProgress.connect(self.on_load_progress) self.status_window.show() else: self.status_window.close() def update_table_item(self, row, value): if hasattr(self, 'info_table'): self.info_table.setItem(row, 1, QTableWidgetItem(str(value))) def on_load_started(self): self.page_stats['load_time'] = time.time() self.update_table_item(0, "Chargement en cours...") self.update_table_item(1, "Calcul en cours...") def on_load_progress(self, progress): if progress == 100: load_time = (time.time() - self.page_stats['load_time']) * 1000 self.page_stats['load_time'] = round(load_time, 2) self.update_table_item(1, f"{self.page_stats['load_time']} ms") def on_load_finished(self, success): if success: web_view = self.parent().web_view # Mise à jour des statistiques self.page_stats['status_code'] = web_view.page().history().currentItem().httpStatusCode() self.page_stats['last_refresh'] = datetime.now().strftime("%H:%M:%S") # Mise à jour du tableau self.update_table_item(0, "Chargée avec succès") self.update_table_item(5, self.page_stats['status_code']) self.update_table_item(8, self.page_stats['last_refresh']) # Vérification HTTPS url = web_view.url().toString() is_secure = url.startswith('https://') security_status = "Sécurisé (HTTPS)" if is_secure else "Non sécurisé (HTTP)" self.update_table_item(9, security_status) else: self.update_table_item(0, "Échec du chargement") def refresh_stats(self): """Rafraîchit manuellement les statistiques""" if hasattr(self, 'info_table'): self.on_load_started() web_view = self.parent().web_view web_view.reload() def clear_stats(self): """Réinitialise toutes les statistiques""" if hasattr(self, 'info_table'): for i in range(self.info_table.rowCount()): self.update_table_item(i, "-") self.page_stats = {key: 0 for key in self.page_stats} self.update_table_item(0, "Statistiques effacées") def toggle_browser_view(self): """ Bascule entre le navigateur PyQt et Firefox avec gestion complète du cycle de vie. """ firefox_path = r"C:\AUTOLOOK\Web_browser\FIREFOX\FirefoxPortable.exe" if not hasattr(self, 'firefox_process'): self.firefox_process = None self.current_browser = "python" self.switch_button = self.sender() if not os.path.exists(firefox_path): logger.error(f"Le fichier {firefox_path} n'existe pas.") return try: main_window = self.window() if self.current_browser == "python": # Passer à Firefox main_window.hide() main_window.hide_browser() self.current_browser = "mozilla" if self.switch_button: self.switch_button.setText("Switch to Python") # Lancer Firefox self.firefox_process = subprocess.Popen([firefox_path, "-url", "https://www.snoown2klabs.com"]) # Démarrer un thread pour surveiller Firefox import threading self.monitor_thread = threading.Thread(target=self.monitor_firefox, daemon=True) self.monitor_thread.start() # Configuration de la fenêtre Firefox QTimer.singleShot(1000, self.configure_firefox_window) else: # Revenir à Python if self.firefox_process: try: subprocess.run(["taskkill", "/F", "/IM", "firefoxportable.exe"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except Exception as e: logger.error(f"Erreur lors de la fermeture de Firefox : {e}") self.firefox_process = None self.current_browser = "python" if self.switch_button: self.switch_button.setText("Switch to Mozilla") main_window.show() main_window.show_browser() main_window.activateWindow() except Exception as e: logger.error(f"Erreur lors du basculement du navigateur : {e}") main_window.show() def monitor_firefox(self): """Surveille l'état de Firefox et réaffiche PyQt si Firefox est fermé""" while True: firefox_running = False try: def callback(hwnd, _): nonlocal firefox_running if win32gui.IsWindowVisible(hwnd) and "Mozilla Firefox" in win32gui.GetWindowText(hwnd): firefox_running = True return True win32gui.EnumWindows(callback, None) if not firefox_running and self.current_browser == "mozilla": # Firefox n'est plus en cours d'exécution, retourner au thread principal QTimer.singleShot(0, self.handle_firefox_closed) break except Exception as e: logger.error(f"Erreur lors de la surveillance de Firefox : {e}") break time.sleep(0.5) def handle_firefox_closed(self): """Gestionnaire appelé lorsque Firefox est fermé""" try: main_window = self.window() self.current_browser = "python" if self.switch_button: self.switch_button.setText("Switch to Mozilla") main_window.show() main_window.show_browser() main_window.activateWindow() self.firefox_process = None except Exception as e: logger.error(f"Erreur lors du retour à Python : {e}") def configure_firefox_window(self): """Configure l'apparence et la position de la fenêtre Firefox.""" try: hwnd = self.find_firefox_window() if hwnd: # Positionner et dimensionner la fenêtre x, y = 100, 100 width, height = 1200, 800 win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, x, y, width, height, win32con.SWP_SHOWWINDOW) except Exception as e: logger.error(f"Erreur lors de la configuration de la fenêtre Firefox : {e}") def find_firefox_window(self): """Trouve la fenêtre Firefox""" result = [None] def callback(hwnd, result): if win32gui.IsWindowVisible(hwnd) and "Mozilla Firefox" in win32gui.GetWindowText(hwnd): result[0] = hwnd return False return True win32gui.EnumWindows(callback, result) return result[0] def toggle_history(self): """Méthode de compatibilité pour afficher l'historique""" try: # Utiliser le gestionnaire d'historique dédié self.history_manager.show_history_window() except Exception as e: logger.error(f"Erreur lors de l'affichage de l'historique: {str(e)}") logger.error(traceback.format_exc()) def go_back(self): try: current_tab = self.browser_window.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): if current_tab.web_view.history().canGoBack(): current_tab.web_view.back() except Exception as e: logger.error(f"Error in go_back: {e}") def go_forward(self): try: current_tab = self.browser_window.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): if current_tab.web_view.history().canGoForward(): current_tab.web_view.forward() except Exception as e: logger.error(f"Error in go_forward: {e}") def reload_page(self): try: current_tab = self.browser_window.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): current_tab.web_view.reload() except Exception as e: logger.error(f"Error in reload_page: {e}") def go_home(self): try: url = "https://www.snoown2klabs.com" # CORRECTION: Mettre à jour aussi la barre d'URL self.url_bar.setText(url) # Charger l'URL via le browser_window plutôt que directement if self.browser_window: self.browser_window.load_url(url) # Fallback au cas où browser_window n'est pas disponible elif isinstance(self.parent(), TabWidget) and hasattr(self.parent(), 'web_view'): self.parent().web_view.setUrl(QUrl(url)) except Exception as e: logger.error(f"Error in go_home: {e}") class HistoryManager(QObject): """Classe dédiée à la gestion de l'historique pour éviter les problèmes de cycle de vie""" history_window_closed = pyqtSignal() def __init__(self, browser_window, parent=None): super().__init__(parent) self.browser_window = browser_window self.history_window = None self.history_table = None self.search_input = None self.sort_combo = None self.entries_info = None def show_history_window(self): """Affiche la fenêtre d'historique""" try: # Vérification de l'existence de la fenêtre if self.history_window and self.history_window.isVisible(): self.history_window.raise_() self.history_window.activateWindow() return # Création de la fenêtre self.history_window = QWidget() self.history_window.setWindowTitle("Historique de navigation") self.history_window.setGeometry(200, 200, 800, 600) self.history_window.setWindowFlags(self.history_window.windowFlags() | Qt.WindowStaysOnTopHint) # Application du style self._apply_styles() # Configuration de l'interface self._setup_ui() # Connexions des signaux self.history_window.closeEvent = self._on_close_event # Affichage self.history_window.show() # Focus sur la recherche if self.search_input: self.search_input.setFocus() except Exception as e: logger.error(f"Erreur lors de l'affichage de l'historique: {str(e)}") logger.error(traceback.format_exc()) self._cleanup_window() def _on_close_event(self, event): """Gestionnaire d'événement de fermeture de la fenêtre""" self._cleanup_window() self.history_window_closed.emit() event.accept() def _cleanup_window(self): """Nettoie les ressources de la fenêtre""" try: if self.history_window: self.history_window.hide() self.history_window = None self.history_table = None self.search_input = None self.sort_combo = None self.entries_info = None except Exception as e: logger.error(f"Erreur lors du nettoyage de la fenêtre: {str(e)}") def _apply_styles(self): """Applique les styles à la fenêtre d'historique""" self.history_window.setStyleSheet(""" QWidget { background-color: #333; color: #fff; } QTableWidget { background-color: #2D2D30; color: #E0E0E0; gridline-color: #555; border: none; } QTableWidget::item { padding: 4px; } QTableWidget::item:selected { background-color: #0078D7; } QHeaderView::section { background-color: #252526; color: #E0E0E0; padding: 5px; border: none; border-right: 1px solid #555; border-bottom: 1px solid #555; } QPushButton { background-color: #0078D7; color: white; border: none; padding: 6px 12px; border-radius: 3px; } QPushButton:hover { background-color: #0063B1; } QPushButton:pressed { background-color: #004E8C; } QLineEdit { background-color: #252526; color: #E0E0E0; border: 1px solid #555; padding: 5px; border-radius: 3px; } QComboBox { background-color: #252526; color: #E0E0E0; border: 1px solid #555; padding: 5px; border-radius: 3px; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background-color: #252526; color: #E0E0E0; selection-background-color: #0078D7; } """) def _setup_ui(self): """Configure l'interface utilisateur de la fenêtre d'historique""" layout = QVBoxLayout(self.history_window) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(10) # Titre title_layout = QHBoxLayout() title_label = QLabel("📋 Historique de navigation") title_label.setFont(QFont("Arial", 14, QFont.Bold)) title_layout.addWidget(title_label) title_layout.addStretch() layout.addLayout(title_layout) # Barre de recherche et options de tri search_layout = QHBoxLayout() search_label = QLabel("🔍 Rechercher:") search_layout.addWidget(search_label) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Entrez des mots-clés pour filtrer l'historique...") self.search_input.textChanged.connect(self.filter_history) search_layout.addWidget(self.search_input) sort_label = QLabel("Trier par:") search_layout.addWidget(sort_label) self.sort_combo = QComboBox() self.sort_combo.addItems(["Date (récent → ancien)", "Date (ancien → récent)", "URL (A → Z)", "URL (Z → A)"]) self.sort_combo.currentIndexChanged.connect(self.sort_history) search_layout.addWidget(self.sort_combo) layout.addLayout(search_layout) # Tableau d'historique if not hasattr(self.browser_window, 'history') or not self.browser_window.history: empty_label = QLabel("Aucun historique disponible") empty_label.setAlignment(Qt.AlignCenter) empty_label.setFont(QFont("Arial", 12)) empty_label.setStyleSheet("color: #888; margin: 20px;") layout.addWidget(empty_label) else: # Tableau self.history_table = QTableWidget() self.history_table.setColumnCount(4) self.history_table.setHorizontalHeaderLabels(["", "Date/Heure", "URL", "Actions"]) # Configuration self.history_table.setSelectionMode(QTableWidget.ExtendedSelection) self.history_table.setSelectionBehavior(QTableWidget.SelectRows) self.history_table.setEditTriggers(QTableWidget.NoEditTriggers) # Largeur des colonnes self.history_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.history_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.history_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.history_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.history_table.horizontalHeader().resizeSection(3, 110) # Remplir self.populate_history_table() # Double-clic self.history_table.cellDoubleClicked.connect(self.load_url_from_history) layout.addWidget(self.history_table) # Barre d'actions action_layout = QHBoxLayout() # Compteur self.entries_info = QLabel(f"Total: {len(self.browser_window.history)} entrées") action_layout.addWidget(self.entries_info) action_layout.addStretch() # Boutons export_button = QPushButton("Exporter") if hasattr(self.history_window, 'style'): export_button.setIcon(self.history_window.style().standardIcon(QStyle.SP_DialogSaveButton)) export_button.clicked.connect(self.export_history) action_layout.addWidget(export_button) clear_button = QPushButton("Effacer l'historique") if hasattr(self.history_window, 'style'): clear_button.setIcon(self.history_window.style().standardIcon(QStyle.SP_TrashIcon)) clear_button.clicked.connect(self.clear_history) action_layout.addWidget(clear_button) delete_button = QPushButton("Supprimer les sélectionnés") if hasattr(self.history_window, 'style'): delete_button.setIcon(self.history_window.style().standardIcon(QStyle.SP_DialogDiscardButton)) delete_button.clicked.connect(self.delete_selected_links) action_layout.addWidget(delete_button) layout.addLayout(action_layout) def populate_history_table(self): """Remplit le tableau d'historique avec les entrées""" try: if not self.history_table or not hasattr(self.browser_window, 'history') or not self.browser_window.history: return # Bloquer les signaux pendant le remplissage self.history_table.blockSignals(True) # Effacer les lignes existantes self.history_table.setRowCount(0) # Ajouter les nouvelles lignes for entry in self.browser_window.history: row = self.history_table.rowCount() self.history_table.insertRow(row) # Colonne 1: Icône icon_item = QTableWidgetItem("🌐") icon_item.setTextAlignment(Qt.AlignCenter) self.history_table.setItem(row, 0, icon_item) # Colonne 2: Date/Heure timestamp_item = QTableWidgetItem(entry.get('timestamp', 'N/A')) self.history_table.setItem(row, 1, timestamp_item) # Colonne 3: URL url_item = QTableWidgetItem(entry.get('url', 'N/A')) self.history_table.setItem(row, 2, url_item) # Colonne 4: Boutons d'action action_widget = QWidget() action_layout = QHBoxLayout(action_widget) action_layout.setContentsMargins(2, 4, 2, 4) action_layout.setSpacing(6) # Augmentation de l'espacement # Création d'une fonction de rappel sûre qui capture le row actuel def create_open_callback(row_idx): return lambda: self.load_url_from_history(row_idx, 0) def create_delete_callback(row_idx): return lambda: self.delete_history_entry(row_idx) # Bouton pour ouvrir l'URL - Style amélioré open_button = QPushButton("Ouvrir") open_button.setStyleSheet(""" QPushButton { background-color: #0078D7; color: white; border: none; padding: 3px 6px; /* Légèrement réduit */ border-radius: 3px; min-width: 60px; min-height: 15px; /* Ajusté pour éviter la coupure */ font-weight: bold; font-size: 11px; /* Taille de police plus petite */ } QPushButton:hover { background-color: #0063B1; } """) open_button.clicked.connect(create_open_callback(row)) action_layout.addWidget(open_button) # Bouton pour supprimer l'entrée - Style amélioré delete_button = QPushButton("✖") delete_button.setStyleSheet(""" QPushButton { background-color: #E74C3C; color: white; border: none; padding: 3px; border-radius: 3px; min-width: 24px; min-height: 15px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #C0392B; } """) delete_button.clicked.connect(create_delete_callback(row)) action_layout.addWidget(delete_button) action_widget.setLayout(action_layout) self.history_table.setCellWidget(row, 3, action_widget) # Ajuster la hauteur des lignes pour les boutons for i in range(self.history_table.rowCount()): self.history_table.setRowHeight(i, 36) # Hauteur uniforme pour toutes les lignes # Réactiver les signaux self.history_table.blockSignals(False) # Mettre à jour l'info du nombre d'entrées if self.entries_info: self.entries_info.setText(f"Total: {len(self.browser_window.history)} entrées") except Exception as e: logger.error(f"Erreur lors du remplissage du tableau: {str(e)}") logger.error(traceback.format_exc()) def filter_history(self): """Filtre l'historique en fonction du texte de recherche""" try: if not self.history_table or not self.search_input: return search_text = self.search_input.text().lower().strip() visible_count = 0 for row in range(self.history_table.rowCount()): url_item = self.history_table.item(row, 2) timestamp_item = self.history_table.item(row, 1) is_visible = False if url_item and timestamp_item: url = url_item.text().lower() timestamp = timestamp_item.text().lower() if search_text in url or search_text in timestamp: is_visible = True visible_count += 1 self.history_table.setRowHidden(row, not is_visible) # Mise à jour du compteur if self.entries_info: total = len(self.browser_window.history) if hasattr(self.browser_window, 'history') else 0 self.entries_info.setText(f"Visible: {visible_count}/{total} entrées") except Exception as e: logger.error(f"Erreur lors du filtrage: {str(e)}") logger.error(traceback.format_exc()) # Réinitialiser en cas d'erreur for row in range(self.history_table.rowCount()): self.history_table.setRowHidden(row, False) def sort_history(self): """Trie l'historique selon le critère sélectionné""" try: if not self.sort_combo or not hasattr(self.browser_window, 'history') or not self.browser_window.history: return sort_option = self.sort_combo.currentIndex() # Utiliser le tri stable pour préserver l'ordre relatif des éléments égaux if sort_option == 0: # Date (récent → ancien) self.browser_window.history.sort(key=lambda x: x.get('timestamp', ''), reverse=True) elif sort_option == 1: # Date (ancien → récent) self.browser_window.history.sort(key=lambda x: x.get('timestamp', '')) elif sort_option == 2: # URL (A → Z) self.browser_window.history.sort(key=lambda x: x.get('url', '').lower()) elif sort_option == 3: # URL (Z → A) self.browser_window.history.sort(key=lambda x: x.get('url', '').lower(), reverse=True) # Actualiser le tableau self.populate_history_table() # Réappliquer le filtre if self.search_input and self.search_input.text(): self.filter_history() except Exception as e: logger.error(f"Erreur lors du tri: {str(e)}") logger.error(traceback.format_exc()) def load_url_from_history(self, row, column=None): """Charge l'URL sélectionnée dans le navigateur""" try: # Obtenir la ligne if isinstance(row, int): selected_row = row else: if not self.history_table: return selected_row = self.history_table.currentRow() # Validation if not self.history_table or selected_row < 0 or selected_row >= self.history_table.rowCount(): return # Obtenir l'URL url_item = self.history_table.item(selected_row, 2) if not url_item: return url = url_item.text() if not url: return # S'assurer que l'URL est bien formée if not url.startswith(('http://', 'https://')): url = 'https://' + url # Vérifier les objets nécessaires if not hasattr(self.browser_window, 'load_url'): logger.error("La méthode load_url n'existe pas dans browser_window") return # Charger l'URL self.browser_window.load_url(url) # Mettre à jour la barre d'URL si elle existe if hasattr(self.browser_window, 'navigation_bar') and hasattr(self.browser_window.navigation_bar, 'url_bar'): self.browser_window.navigation_bar.url_bar.setText(url) # Fermer la fenêtre en utilisant QTimer pour éviter les problèmes QTimer.singleShot(100, self._cleanup_window) except Exception as e: logger.error(f"Erreur lors du chargement de l'URL: {str(e)}") logger.error(traceback.format_exc()) def delete_history_entry(self, row): """Supprime une entrée spécifique de l'historique""" try: if not self.history_table or row < 0 or row >= self.history_table.rowCount(): return # Obtenir les informations url_item = self.history_table.item(row, 2) timestamp_item = self.history_table.item(row, 1) if not url_item or not timestamp_item: return url = url_item.text() timestamp = timestamp_item.text() # Confirmation reply = QMessageBox.question( self.history_window, 'Confirmation', f'Voulez-vous vraiment supprimer cette entrée?\n\n{url}', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return # Supprimer entry_found = False for i, entry in enumerate(self.browser_window.history): if entry.get('url') == url and entry.get('timestamp') == timestamp: del self.browser_window.history[i] entry_found = True break if entry_found: # Supprimer du tableau self.history_table.removeRow(row) # Mettre à jour le fichier self.save_history_to_file() # Mettre à jour le compteur if self.entries_info: self.entries_info.setText(f"Total: {len(self.browser_window.history)} entrées") # Réappliquer le filtre if self.search_input and self.search_input.text(): self.filter_history() except Exception as e: logger.error(f"Erreur lors de la suppression: {str(e)}") logger.error(traceback.format_exc()) def delete_selected_links(self): """Supprime les entrées sélectionnées de l'historique""" try: if not self.history_table: return # Obtenir les lignes sélectionnées selected_rows = set() for item in self.history_table.selectedItems(): selected_rows.add(item.row()) if not selected_rows: return # Confirmation reply = QMessageBox.question( self.history_window, 'Confirmation', f'Voulez-vous vraiment supprimer {len(selected_rows)} entrée(s) de l\'historique?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return # Collecter les entrées à supprimer entries_to_delete = [] for row in sorted(selected_rows, reverse=True): url_item = self.history_table.item(row, 2) timestamp_item = self.history_table.item(row, 1) if url_item and timestamp_item: entries_to_delete.append({ 'url': url_item.text(), 'timestamp': timestamp_item.text(), 'row': row }) # Supprimer les entrées (d'abord du tableau pour éviter les problèmes d'index) for entry in entries_to_delete: self.history_table.removeRow(entry['row']) # Puis supprimer de l'historique for entry in entries_to_delete: for i, hist_entry in enumerate(self.browser_window.history[:]): if (hist_entry.get('url') == entry['url'] and hist_entry.get('timestamp') == entry['timestamp']): del self.browser_window.history[i] break # Mettre à jour le fichier self.save_history_to_file() # Actualiser l'affichage self.populate_history_table() # Réappliquer le filtre if self.search_input and self.search_input.text(): self.filter_history() except Exception as e: logger.error(f"Erreur lors de la suppression multiple: {str(e)}") logger.error(traceback.format_exc()) def clear_history(self): """Efface tout l'historique""" try: if not hasattr(self.browser_window, 'history') or not self.browser_window.history: return # Confirmation reply = QMessageBox.question( self.history_window, 'Confirmation', f'Voulez-vous vraiment effacer TOUT l\'historique ({len(self.browser_window.history)} entrées)?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return # Effacer self.browser_window.history = [] # Mettre à jour le fichier self.save_history_to_file() # Actualiser le tableau if self.history_table: self.history_table.setRowCount(0) # Message temporaire empty_row = self.history_table.rowCount() self.history_table.insertRow(empty_row) empty_cell = QTableWidgetItem("Historique effacé") empty_cell.setTextAlignment(Qt.AlignCenter) self.history_table.setSpan(empty_row, 0, 1, 4) self.history_table.setItem(empty_row, 0, empty_cell) # Mettre à jour le compteur if self.entries_info: self.entries_info.setText("Total: 0 entrées") except Exception as e: logger.error(f"Erreur lors de l'effacement: {str(e)}") logger.error(traceback.format_exc()) def export_history(self): """Exporte l'historique dans un fichier CSV""" try: if not hasattr(self.browser_window, 'history') or not self.browser_window.history: QMessageBox.information( self.history_window, "Information", "Aucune entrée d'historique à exporter." ) return # Demander le chemin filename, _ = QFileDialog.getSaveFileName( self.history_window, "Exporter l'historique", os.path.join(os.path.expanduser("~"), "historique_navigation.csv"), "Fichiers CSV (*.csv);;Tous les fichiers (*)" ) if not filename: return # Exporter try: with open(filename, 'w', newline='', encoding='utf-8') as f: import csv writer = csv.writer(f) # Entête writer.writerow(["Date/Heure", "URL"]) # Données for entry in self.browser_window.history: writer.writerow([entry.get('timestamp', ''), entry.get('url', '')]) QMessageBox.information( self.history_window, "Succès", f"L'historique a été exporté avec succès dans {filename}" ) except Exception as e: QMessageBox.critical( self.history_window, "Erreur", f"Une erreur est survenue lors de l'export: {str(e)}" ) except Exception as e: logger.error(f"Erreur lors de l'export: {str(e)}") logger.error(traceback.format_exc()) def save_history_to_file(self): """Sauvegarde l'historique dans le fichier""" try: if not hasattr(self.browser_window, 'history'): return history_file = "history.txt" with open(history_file, "w", encoding='utf-8') as f: for entry in self.browser_window.history: timestamp = entry.get('timestamp', '') url = entry.get('url', '') f.write(f"{timestamp} | {url}\n") except Exception as e: logger.error(f"Erreur lors de la sauvegarde du fichier: {str(e)}") logger.error(traceback.format_exc()) class CustomWebEnginePage(QWebEnginePage): """Page personnalisée pour capturer les messages de la console JavaScript""" def __init__(self, parent=None): super().__init__(parent) self.browser_window = None def javaScriptConsoleMessage(self, level, message, line_number, source_id): """Redéfinition de la méthode pour capturer les messages JS""" level_str = { QWebEnginePage.InfoMessageLevel: "INFO", QWebEnginePage.WarningMessageLevel: "WARNING", QWebEnginePage.ErrorMessageLevel: "ERROR" }.get(level, "UNKNOWN") log_message = f"JS {level_str} | Ligne {line_number} | Source: {source_id} | Message: {message}" print(log_message) try: logger = logging.getLogger("ha2") if level == QWebEnginePage.InfoMessageLevel: logger.info(log_message) elif level == QWebEnginePage.WarningMessageLevel: logger.warning(log_message) else: logger.error(log_message) except Exception as e: print(f"Erreur lors du log JS : {e}") class TabWidget(QWidget): """Widget contenant la barre de navigation et la vue web pour chaque onglet""" def __init__(self, parent=None): super().__init__(parent) # === Layout principal === layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # === Création de la vue web === self.web_view = QWebEngineView() # Utiliser notre page personnalisée pour capturer les messages JS custom_page = CustomWebEnginePage(self) custom_page.browser_window = self.window() self.web_view.setPage(custom_page) # === Barre de navigation === self.navigation_bar = NavigationBar(self.window(), self) layout.addWidget(self.navigation_bar) # === Configuration du navigateur === self.setup_browser() # === Ajout au layout === layout.addWidget(self.web_view) # Cacher la barre si on est en mode mini (détection robuste) if self._is_mini_mode(): self.navigation_bar.hide() def _is_mini_mode(self): parent = self while parent: if hasattr(parent, 'is_mini') and getattr(parent, 'is_mini'): return True parent = parent.parent() return False def showEvent(self, event): """Appelé lorsque l'onglet est affiché - pour maintenir le masquage""" super().showEvent(event) # Vérifier à nouveau si on est en mode mini if self._is_mini_mode(): if hasattr(self, 'navigation_bar'): self.navigation_bar.hide() def setup_browser(self): """Configuration optimisée du navigateur pour réutiliser les processus""" # ═══ CONFIGURATION POUR RÉUTILISER LE PROCESSUS ═══ # Créer un profil partagé pour réutiliser les ressources from PyQt5.QtWebEngineWidgets import QWebEngineProfile, QWebEnginePage from PyQt5.QtCore import QUrl # Utiliser le profil par défaut au lieu d'en créer un nouveau profile = QWebEngineProfile.defaultProfile() # Configuration du profil pour économiser les ressources profile.setPersistentCookiesPolicy(QWebEngineProfile.NoPersistentCookies) profile.setHttpCacheType(QWebEngineProfile.MemoryHttpCache) profile.setHttpCacheMaximumSize(1024 * 1024) # 1MB seulement # Appliquer le profil à la page page = QWebEnginePage(profile, self.web_view) self.web_view.setPage(page) # Configuration des paramètres settings = self.web_view.settings() settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) settings.setAttribute(QWebEngineSettings.LocalStorageEnabled, False) # Économie settings.setAttribute(QWebEngineSettings.PluginsEnabled, False) # Économie settings.setAttribute(QWebEngineSettings.AutoLoadImages, True) # Désactiver les fonctionnalités lourdes settings.setAttribute(QWebEngineSettings.WebGLEnabled, False) settings.setAttribute(QWebEngineSettings.Accelerated2dCanvasEnabled, False) print("🔧 Navigateur configuré pour réutiliser les processus") # ═══ CONNEXION DES SIGNAUX ═══ self.web_view.loadStarted.connect(self.handle_load_started) self.web_view.loadProgress.connect(self.handle_load_progress) self.web_view.loadFinished.connect(self.handle_load_finished) self.web_view.urlChanged.connect(self.handle_url_changed) # Connexion pour injecter le CSS d'ascenseur discret self.web_view.loadFinished.connect(self.inject_scrollbar_css) self.web_view.loadFinished.connect( lambda _: self.web_view.page().runJavaScript( "document.body.style.backgroundColor = '#1c2c3c';" ) ) # ═══ INITIALISATION SELON LE MODE ═══ main_window = self.window() is_mini_mode = hasattr(main_window, 'is_mini') and main_window.is_mini if is_mini_mode: # Mode mini : page HTML légère au lieu d'une URL externe initial_html = """
En attente de traitement d'emails...
Prêt pour la navigation et le traitement d'emails