# 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 = """ Mini Browser - Prêt

🚀 Mini Browser

✅ Prêt pour traitement

En attente de traitement d'emails...

""" # UTILISER setHtml au lieu de setUrl pour éviter les nouveaux processus self.web_view.setHtml(initial_html) # Zoom par défaut à 30% pour le mode mini self.web_view.setZoomFactor(0.3) # URL fictive pour la barre d'adresse initial_url_display = "mini://browser/ready" else: # Mode normal : page HTML simple aussi initial_html = """ Browser - Prêt

🌐 Hybrid Browser MA2C

✅ Navigateur initialisé

Prêt pour la navigation et le traitement d'emails

""" # UTILISER setHtml au lieu de setUrl self.web_view.setHtml(initial_html) # URL fictive pour la barre d'adresse initial_url_display = "browser://ready" # ═══ MISE À JOUR DE LA BARRE D'URL ═══ if hasattr(self, 'navigation_bar') and hasattr(self.navigation_bar, 'url_bar'): self.navigation_bar.url_bar.setText(initial_url_display) print(f"🔧 Browser configuré - Mode: {'Mini' if is_mini_mode else 'Normal'}") def inject_scrollbar_css(self, success): if success: main_window = self.window() if hasattr(main_window, 'is_mini') and main_window.is_mini: css_code = """ (function() { var style = document.createElement('style'); style.textContent = ` ::-webkit-scrollbar { width: 4px !important; height: 8px !important; } ::-webkit-scrollbar-track { background: transparent !important; } ::-webkit-scrollbar-thumb { background: rgba(255, 100, 100, 0.4) !important; /* ROUGE TRANSPARENT */ border-radius: 2px !important; } ::-webkit-scrollbar-thumb:hover { background: rgba(255, 100, 100, 0.8) !important; /* ROUGE PLUS VISIBLE */ } `; document.head.appendChild(style); })(); """ self.web_view.page().runJavaScript(css_code) def handle_url_changed(self, qurl): """Mise à jour de la barre d'URL quand l'URL change""" if hasattr(self, 'navigation_bar') and hasattr(self.navigation_bar, 'url_bar'): # Mettre à jour le texte de la barre d'URL avec l'URL actuelle self.navigation_bar.url_bar.setText(qurl.toString()) # Propager également le changement au niveau de la fenêtre principale pour l'historique main_window = self.window() if main_window and hasattr(main_window, 'update_url'): main_window.update_url(qurl) def handle_load_started(self): self.navigation_bar.progress_bar.setValue(0) self.navigation_bar.progress_bar.show() def handle_load_progress(self, progress): self.navigation_bar.progress_bar.setValue(progress) def handle_load_finished(self, success): if success: self.navigation_bar.progress_bar.setValue(100) QTimer.singleShot(1000, self.navigation_bar.progress_bar.hide) else: self.navigation_bar.progress_bar.setStyleSheet(""" QProgressBar::chunk { background: red; } """) def update_url(self, url): self.navigation_bar.url_bar.setText(url.toString()) class BrowserWindow(QMainWindow): def __init__(self, is_mini=False, *args, **kwargs): super().__init__(*args, **kwargs) self.is_mini = is_mini # Mode mini activé ou non self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) if self.is_mini: # Taille adaptative selon la résolution d'écran pour le mini browser screen_height = QDesktopWidget().availableGeometry().height() if screen_height >= 1080: # Écrans Full HD ou plus min_browser_height = int(screen_height * 0.25) # 25% de la hauteur d'écran elif screen_height >= 768: # Écrans HD min_browser_height = int(screen_height * 0.20) # 20% de la hauteur d'écran else: # Écrans plus petits min_browser_height = 200 self.resize(500, min_browser_height) else: self.resize(1200, 800) self.setWindowTitle("Hybrid Browser MA2C") self.setGeometry(100, 100, 1200, 800) # === Widget central et layout principal === central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) # === Bannière inférieure === self.bottom_banner = BottomBanner(self) layout.addWidget(self.bottom_banner) # === Historique de navigation === self.history = [] if os.path.exists("history.txt"): with open("history.txt", "r") as f: for line in f: parts = line.strip().split(" | ") if len(parts) != 2: continue timestamp, url = parts self.history.append({"timestamp": timestamp, "url": url}) # === Système d'onglets === self.tab_widget = QTabWidget() if not self.is_mini: self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self.close_tab) add_tab_button = QPushButton("+") add_tab_button.setMinimumWidth(20) add_tab_button.setFixedSize(30, 20) add_tab_button.setFont(QFont("Arial", 10, QFont.Bold)) add_tab_button.clicked.connect(self.add_tab) tab_bar = self.tab_widget.tabBar() tab_bar_layout = QHBoxLayout(tab_bar) tab_bar_layout.setContentsMargins(0, 0, 0, 0) tab_bar_layout.setSpacing(0) tab_bar_layout.addStretch() tab_bar_layout.addWidget(add_tab_button) tab_bar.setStyleSheet(""" QTabBar::tab { padding: 5px 10px; margin: 2px; background: lightgray; border: 1px solid darkgray; border-radius: 4px; } QPushButton { margin-left: 4px; padding: 2px 10px; background: #3498db; color: white; border-radius: 5px; font-weight: bold; } QPushButton:hover { background: #2980b9; } QPushButton:pressed { background: #2573a7; } """) else: self.tab_widget.setTabsClosable(False) self.tab_widget.setDocumentMode(True) self.tab_widget.setStyleSheet(""" QTabWidget::tab-bar { height: 0px; width: 0px; margin: 0px; padding: 0px; border: none; visibility: hidden; } QTabBar { height: 0px; width: 0px; margin: 0px; padding: 0px; border: none; background: transparent; visibility: hidden; } QTabBar::tab { height: 0px; width: 0px; margin: 0px; padding: 0px; border: none; background: transparent; visibility: hidden; max-height: 0px; max-width: 0px; } QTabWidget::pane { border: none; margin: 0px; padding: 0px; } """) self.tab_widget.setContentsMargins(0, 0, 0, 0) tab_bar = self.tab_widget.tabBar() tab_bar.setVisible(False) tab_bar.setMaximumHeight(0) self.tab_widget.setTabBarAutoHide(True) layout.addWidget(self.tab_widget) # === Créer le premier onglet === self.add_tab() # === Initialiser le zoom navigateur === self.initialize_browser_zoom() def on_load_finished(self, ok): if ok: print("✅ Page chargée avec succès") self.inject_js_fixes() # ✅ Injection JS anti-erreur else: print("❌ Échec du chargement de la page") def add_tab(self, url=None): # Créer un TabWidget au lieu d'un QWebEngineView directement tab_widget = TabWidget(self) # Si une URL est fournie, la charger if url: if isinstance(url, str): # Convertir la string en QUrl si nécessaire from PyQt5.QtCore import QUrl qurl = QUrl(url) else: qurl = url tab_widget.web_view.setUrl(qurl) # Injection JS après chargement tab_widget.web_view.loadFinished.connect(lambda ok: self.inject_js_fixes() if ok else None) # Ajouter l'onglet avec un titre par défaut tab_index = self.tab_widget.addTab(tab_widget, "Nouvel Onglet") self.tab_widget.setCurrentIndex(tab_index) def inject_js_fixes(self): js_code = """ (function() { try { // === SYSTÈME DE CORRECTION DYNAMIQUE DES ERREURS === // 1. Gestionnaire d'erreurs global avec auto-correction window.addEventListener('error', function(e) { const error = e.error || e; const message = error.message || e.message || ''; const source = e.filename || ''; // Auto-corrections basées sur des patterns if (message.includes('is not defined')) { const varName = message.match(/(.+?) is not defined/)?.[1]; if (varName) { // Créer automatiquement la variable/fonction manquante if (varName.includes('Interval')) { window[varName] = setInterval; } else if (varName.includes('Timeout')) { window[varName] = setTimeout; } else if (varName.includes('Element') || varName.includes('Container')) { window[varName] = function() { return document.createElement('div'); }; } else { // Fonction générique par défaut window[varName] = function() { console.info("🔧 Auto-polyfill créé pour :", varName); return {}; }; } console.info("✅ Variable manquante auto-corrigée :", varName); e.preventDefault(); return false; } } // Masquer les erreurs non-critiques const nonCriticalErrors = [ 'Mixed Content', 'SameSite', 'Content Security Policy', 'cross-site', 'chrome-extension', 'Graph container element not found', 'Network request failed' ]; if (nonCriticalErrors.some(pattern => message.includes(pattern))) { console.info("🔇 Erreur non-critique masquée :", message.substring(0, 100)); e.preventDefault(); return false; } return true; }); // 2. Proxy universel pour les objets manquants const createUniversalProxy = (name) => { return new Proxy({}, { get: function(target, property) { if (!(property in target)) { if (typeof property === 'string' && property !== 'then') { target[property] = createUniversalProxy(name + '.' + property); } } return target[property]; }, set: function(target, property, value) { target[property] = value; return true; } }); }; // 3. Intercepteur universel de propriétés manquantes const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; Object.getOwnPropertyDescriptor = function(obj, prop) { const desc = originalGetOwnPropertyDescriptor(obj, prop); if (!desc && typeof prop === 'string') { // Créer une propriété par défaut return { value: createUniversalProxy(prop), writable: true, enumerable: true, configurable: true }; } return desc; }; // 4. Auto-correction des URLs HTTP vers HTTPS const autoHttpsCorrection = () => { document.querySelectorAll('script[src^="http://"], link[href^="http://"], img[src^="http://"]').forEach(el => { const attr = el.src ? 'src' : 'href'; const oldUrl = el.getAttribute(attr); const newUrl = oldUrl.replace('http://', 'https://'); el.setAttribute(attr, newUrl); console.info("🔒 Auto-correction HTTP→HTTPS :", oldUrl.substring(0, 50)); }); }; // 5. Observer pour auto-corrections continues const universalObserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1) { // Auto-correction immédiate des nouveaux éléments if (node.src && node.src.startsWith('http://')) { node.src = node.src.replace('http://', 'https://'); } if (node.href && node.href.startsWith('http://')) { node.href = node.href.replace('http://', 'https://'); } // Créer des IDs manquants pour les graphiques if (node.className && node.className.includes('chart')) { if (!node.id) node.id = 'auto-chart-' + Date.now(); } } }); }); // Exécuter les corrections périodiques autoHttpsCorrection(); }); // 6. Polyfills universels basés sur des patterns const universalPolyfills = { // Fonctions de timing setInterval2: setInterval, setInterval3: setInterval, setTimeout2: setTimeout, setTimeout3: setTimeout, // Fonctions jQuery alternatives $: function(selector) { return document.querySelector(selector) || { ready: function(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }, on: function() { return this; }, hide: function() { return this; }, show: function() { return this; }, fadeIn: function() { return this; }, fadeOut: function() { return this; } }; }, // APIs manquantes communes Morris: { Line: function() { return { render: function() {} }; } }, Chart: { Line: function() { return { render: function() {} }; } }, gtag: function() { console.info("🔧 gtag polyfill"); }, ga: function() { console.info("🔧 Google Analytics polyfill"); } }; // Appliquer les polyfills Object.keys(universalPolyfills).forEach(key => { if (typeof window[key] === 'undefined') { window[key] = universalPolyfills[key]; console.info("🔧 Polyfill appliqué :", key); } }); // 7. Gestionnaire de cookies universels const originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie'); Object.defineProperty(document, 'cookie', { get: originalCookieDescriptor.get, set: function(value) { let correctedValue = value; // Auto-correction SameSite if (!/SameSite=/i.test(value)) { correctedValue += '; SameSite=None'; } // Auto-correction Secure pour HTTPS if (location.protocol === 'https:' && !/Secure/i.test(correctedValue)) { correctedValue += '; Secure'; } return originalCookieDescriptor.set.call(this, correctedValue); } }); // 8. Masquage intelligent des erreurs de console const originalConsoleError = console.error; const originalConsoleWarn = console.warn; console.error = function(...args) { const message = args.join(' '); const ignoredPatterns = [ 'Mixed Content', 'SameSite', 'chrome-extension', 'Content Security Policy', 'cross-site resource' ]; if (!ignoredPatterns.some(pattern => message.includes(pattern))) { return originalConsoleError.apply(console, args); } }; console.warn = function(...args) { const message = args.join(' '); if (!message.includes('SameSite') && !message.includes('cross-site')) { return originalConsoleWarn.apply(console, args); } }; // 9. Démarrer l'observation continue if (document.body) { universalObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href'] }); } // 10. Corrections initiales autoHttpsCorrection(); console.info("🚀 Système de correction dynamique activé"); } catch(e) { console.error("Erreur dans le système de correction :", e); } })(); """ web_view = self.get_current_web_view() if web_view: web_view.page().runJavaScript(js_code) def update_banner_position(self, position): layout = self.centralWidget().layout() layout.removeWidget(self.bottom_banner) if position == "disabled": self.bottom_banner.hide() else: self.bottom_banner.show() if position == "top": layout.insertWidget(0, self.bottom_banner) else: layout.addWidget(self.bottom_banner) def build_banner_text(self, subject: str) -> str: if hasattr(self, 'tab_widget'): tab_count = self.tab_widget.count() current_index = self.tab_widget.currentIndex() + 1 # Les onglets commencent à 0 return f"Onglets : 1-{tab_count} (actif: {current_index}) / {subject}" else: return f"Onglet unique / {subject}" def update_page_time(self, display_label, start_timer=True): print(f"update_page_time called with: {display_label}, start_timer={start_timer}") if hasattr(self, 'console_header') and self.console_header: self.console_header.update_page_time(display_label, start_timer=start_timer) def initialize_browser_zoom(self): """Initialise le navigateur avec le zoom sauvegardé""" current_tab = self.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): try: import os # Chemin du fichier de configuration zoom_file = os.path.join(r"C:\AUTOLOOK", "miniview.json") # Valeur de zoom par défaut saved_zoom = 0.4 # 40% par défaut # Charger la configuration si elle existe if os.path.exists(zoom_file): try: with open(zoom_file, 'r') as f: content = f.read().strip() if content: saved_zoom = float(content) logger.info(f"Zoom chargé: {saved_zoom * 100:.0f}% depuis {zoom_file}") except Exception as e: logger.error(f"Erreur lors du chargement du zoom: {str(e)}") else: logger.info(f"Fichier de zoom non trouvé: {zoom_file}, utilisation du zoom par défaut (40%)") # Appliquer le zoom current_tab.web_view.setZoomFactor(saved_zoom) except Exception as e: logger.error(f"Erreur lors de l'initialisation du zoom: {str(e)}") # En cas d'erreur, utiliser la valeur par défaut if current_tab and hasattr(current_tab, 'web_view'): current_tab.web_view.setZoomFactor(0.4) def showEvent(self, event): """Appelé lorsque la fenêtre est affichée - pour maintenir le masquage en mode mini""" super().showEvent(event) # ═══ MASQUAGE FORCÉ NAVIGATION MINI ═══ if self.is_mini and hasattr(self, 'tab_widget'): tab_bar = self.tab_widget.tabBar() tab_bar.setVisible(False) tab_bar.setMaximumHeight(0) # S'assurer que tous les onglets ont leur navigation_bar masquée DÉFINITIVEMENT for i in range(self.tab_widget.count()): tab = self.tab_widget.widget(i) if hasattr(tab, 'navigation_bar'): # Masquage complet et irréversible tab.navigation_bar.setVisible(False) tab.navigation_bar.setMaximumHeight(0) tab.navigation_bar.setMinimumHeight(0) # Désactiver tous les widgets enfants for child in tab.navigation_bar.findChildren(QWidget): child.setVisible(False) child.setMaximumHeight(0) # Retirer du layout si possible if tab.navigation_bar.parent(): layout = tab.navigation_bar.parent().layout() if layout: layout.removeWidget(tab.navigation_bar) # ═══ FIN MASQUAGE FORCÉ ═══ def setup_ui(self): layout = QHBoxLayout() button_style = "background-color: #3498db; color: white; padding: 5px;" # Bouton Console (toujours présent) self.console_button = QPushButton("Console") self.console_button.setStyleSheet(button_style) self.console_button.clicked.connect(self.toggle_console) layout.addWidget(self.console_button) # Ajouter les autres boutons seulement si ce n'est pas en mode mini if not self.is_mini: self.timer_button = QPushButton("Timer") self.timer_button.setStyleSheet(button_style) self.timer_button.clicked.connect(self.toggle_timer) layout.addWidget(self.timer_button) self.switch_button = QPushButton("Switch") self.switch_button.setStyleSheet(button_style) self.switch_button.clicked.connect(self.toggle_browser_view) layout.addWidget(self.switch_button) # Appliquer le layout au widget principal header_widget = QWidget() header_widget.setLayout(layout) self.setCentralWidget(header_widget) def setup_browser(self): profile = QWebEngineProfile.defaultProfile() web_view = self.get_current_web_view() if not web_view: return settings = web_view.settings() # Configuration des paramètres du navigateur settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) settings.setAttribute(QWebEngineSettings.LocalStorageEnabled, True) settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) settings.setAttribute(QWebEngineSettings.AutoLoadImages, True) # Connexion des signaux web_view.loadStarted.connect(self.handle_load_started) web_view.loadProgress.connect(self.handle_load_progress) web_view.loadFinished.connect(self.handle_load_finished) web_view.urlChanged.connect(self.update_url) # Chargement de la page initiale web_view.setUrl(QUrl("https://www.snoown2klabs.com")) def close_tab(self, index): """Ferme l'onglet à l'index spécifié""" if self.tab_widget.count() > 1: self.tab_widget.removeTab(index) else: # Si c'est le dernier onglet, créer un nouvel onglet avant de le fermer self.add_tab() self.tab_widget.removeTab(index) def get_current_tab(self): """Retourne le TabWidget actif""" return self.tab_widget.currentWidget() def load_url(self, url): """Charge une URL dans l'onglet actif""" if not url: return # S'assurer que l'URL a un schéma (http/https) if not isinstance(url, QUrl): if not url.startswith(('http://', 'https://')): url = 'https://' + url qurl = QUrl.fromUserInput(url) else: qurl = url # Obtenir l'onglet actif et charger l'URL current_tab = self.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): # Charger l'URL dans la vue web current_tab.web_view.setUrl(qurl) # S'assurer que la barre d'URL est mise à jour immédiatement if hasattr(current_tab, 'navigation_bar') and hasattr(current_tab.navigation_bar, 'url_bar'): current_tab.navigation_bar.url_bar.setText(qurl.toString()) def update_url(self, url): """ Met à jour l'historique avec la nouvelle URL et met à jour la barre d'URL. """ try: # CORRECTION 10: Mettre à jour la barre d'URL de l'onglet actif current_tab = self.get_current_tab() if current_tab and hasattr(current_tab, 'navigation_bar') and hasattr(current_tab.navigation_bar, 'url_bar'): current_tab.navigation_bar.url_bar.setText(url.toString()) # Vérifier les doublons avant d'ajouter à l'historique url_string = url.toString() if not self.history or self.history[-1]["url"] != url_string: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.history.append({"url": url_string, "timestamp": timestamp}) # Sauvegarder dans un fichier with open("history.txt", "a") as f: f.write(f"{timestamp} | {url_string}\n") logger.info(f"URL ajoutée à l'historique : {url_string}") except Exception as e: logger.error(f"Erreur lors de la mise à jour de l'URL : {e}") def handle_load_started(self): self.navigation_bar.progress_bar.setValue(0) self.navigation_bar.progress_bar.show() def handle_load_progress(self, progress): self.navigation_bar.progress_bar.setValue(progress) def handle_load_finished(self, success): if success: self.navigation_bar.progress_bar.setValue(100) QTimer.singleShot(1000, self.navigation_bar.progress_bar.hide) else: self.navigation_bar.progress_bar.setStyleSheet(""" QProgressBar::chunk { background: red; } """) def get_current_web_view(self): """Retourne le QWebEngineView de l'onglet actif""" current_tab = self.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): return current_tab.web_view return None def clear_browser(self): web_view = self.get_current_web_view() if web_view: web_view.setUrl(QUrl("about:blank")) self.console_header.page_time = 0.0 self.console_header.timer_label.setText("----") def hide_browser(self): """Cache le navigateur PyQt et tous ses composants""" self.tab_widget.hide() self.hide() def show_browser(self): """Affiche le navigateur PyQt et tous ses composants""" self.show() self.tab_widget.show() class BottomBanner(QFrame): def __init__(self, parent=None): super().__init__(parent) self.setFixedHeight(40) self.setStyleSheet("background-color: rgba(0, 0, 0, 120); border-radius: 6px;") self.setContentsMargins(10, 0, 10, 0) self.setVisible(False) layout = QHBoxLayout(self) self.label = QLabel("...") self.label.setStyleSheet("color: white; font-size: 8pt;") layout.addWidget(self.label) self.animation_timer = QTimer() self.animation_timer.timeout.connect(self.scroll_text) self.full_text = "" self.offset = 0 def show_banner(self, text): self.full_text = text self.offset = 0 self.label.setText(text) self.setVisible(True) if len(text) > 50: # Valeur à ajuster selon la largeur du navigateur self.animation_timer.start(150) else: self.animation_timer.stop() def scroll_text(self): visible_chars = 50 if len(self.full_text) <= visible_chars: return start = self.offset % len(self.full_text) scrolled = self.full_text[start:] + " • " + self.full_text[:start] self.label.setText(scrolled[:visible_chars]) self.offset += 1 def hide_banner(self): self.setVisible(False) self.animation_timer.stop() stop_trace() if __name__ == "__main__": # ═══ CONFIGURATION POUR RÉUTILISER LES PROCESSUS ═══ import os import sys # Configuration critique pour réutiliser les processus os.environ['QTWEBENGINE_CHROMIUM_FLAGS'] = ( '--single-process ' # FORCE un seul processus '--no-sandbox ' '--disable-web-security ' '--disable-features=VizDisplayCompositor ' '--process-per-site ' '--max_old_space_size=50 ' '--max-active-webgl-contexts=1' ) # Force le modèle single-process os.environ['QTWEBENGINE_PROCESS_MODEL'] = 'single-process' # Limite le cache pour éviter l'accumulation os.environ['QTWEBENGINE_DISK_CACHE_SIZE'] = '10485760' # 10MB print("🔧 Configuration single-process WebEngine appliquée") # ═══ FIN CONFIGURATION ═══ app = QApplication(sys.argv) window = BrowserWindow() window.show() sys.exit(app.exec_())