# v6 from __future__ import annotations import logging import os import sys from logging.handlers import RotatingFileHandler import traceback from theme import ( apply_dark_palette, apply_stylesheet, ) from PySide6.QtCore import Qt from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QApplication, QLabel, QMainWindow, QMessageBox, QStatusBar, ) # whats needed from browser import BrowserView from config import ( APP_NAME, APP_VERSION, DATABASE_PATH, LOG_LEVEL, LOGS_DIR, START_URL, WINDOW_HEIGHT, WINDOW_WIDTH, ensure_directories, load_settings, save_settings, ) from db import Database from managers import ( AddonManagerWindow, NetworkInspector, PluginManagerWindow, ProfileManager, SettingsWindow, TrayManager, ContextManager, ) # v2 os.environ.setdefault( "QTWEBENGINE_CHROMIUM_FLAGS", ( "--disable-features=" "Translate," "MediaRouter " "--enable-features=" "DnsOverHttps " "--dns-over-https-mode=secure " "--dns-over-https-templates=" "https://dns.quad9.net/dns-query " "--disable-background-networking " "--disable-component-update " "--disable-domain-reliability " "--disable-breakpad " "--no-pings " "--enable-gpu-rasterization " "--enable-zero-copy " "--disable-sync " "--disable-translate " "--disable-features=AutofillServerCommunication " "--enable-native-gpu-memory-buffers " ), ) LOG_FILE = LOGS_DIR / "client.log" def configure_logging() -> None: LOG_FILE.parent.mkdir( parents=True, exist_ok=True, ) root = logging.getLogger() root.setLevel( LOG_LEVEL ) root.handlers.clear() formatter = logging.Formatter( "%(asctime)s %(levelname)s %(name)s: %(message)s" ) file_handler = ( RotatingFileHandler( LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8", ) ) file_handler.setFormatter( formatter ) console_handler = ( logging.StreamHandler() ) console_handler.setFormatter( formatter ) root.addHandler( file_handler ) root.addHandler( console_handler ) configure_logging() log = logging.getLogger( __name__ ) class MainWindow(QMainWindow): def __init__( self, db: Database, settings: dict, ): super().__init__() self.db = db self.settings_data = settings self.contexts = ContextManager( self.db, self.settings_data, ) self.plugins_window = None self.settings_window = None self.profile_window = None self.addons_window = None self.network_window = None self.tray_manager = None self.setWindowTitle( f"{APP_NAME}---{APP_VERSION}" ) self.resize( max( 800, int( settings.get( "window_width", WINDOW_WIDTH, ) ), ), max( 600, int( settings.get( "window_height", WINDOW_HEIGHT, ) ), ), ) self.browser = BrowserView( db=self.db, context=self.context, settings=self.settings_data, parent=self, ) self.setCentralWidget( self.browser ) self._build_status_bar() self._connect_signals() self._build_tray() self.browser.open( START_URL ) @property def context(self): return self.contexts.current() def _build_status_bar( self, ) -> None: self.status_bar = ( QStatusBar( self ) ) self.setStatusBar( self.status_bar ) self.download_label = QLabel( "Ready", self, ) self.status_bar.addPermanentWidget( self.download_label ) self.status_bar.addPermanentWidget( QLabel( f"Blocked: " f"{self.browser.network.blocklist_engine.count():,}" ) ) def _connect_signals( self, ) -> None: self.browser.downloadProgress.connect( self._download_progress ) self.browser.page_.consoleMessage.connect( self._console_message ) self.browser.bridge.messageReceived.connect( self._bridge_message ) def _build_tray( self, ) -> None: tray_cfg = self.settings_data.get( "tray", {}, ) if not tray_cfg.get( "enabled", True, ): return self.tray_manager = ( TrayManager( self ) ) self.tray_manager.open_action.triggered.connect( self._show_window ) self.tray_manager.tray.show() def _show_window( self, ) -> None: self.showNormal() self.raise_() self.activateWindow() def _console_message( self, message: str, ) -> None: ignored = { "setMemoryInformation not available", "[ProcessUtilsElectron]", } if any( token in message for token in ignored ): return log.info( "console=%s", message, ) def _bridge_message( self, message: str, ) -> None: log.info( "bridge=%s", message, ) def _download_progress( self, filename: str, received: int, total: int, ) -> None: if total <= 0: self.download_label.setText( f"{filename}: {received:,} B" ) return percent = int( ( received / max( total, 1, ) ) * 100 ) self.download_label.setText( ( f"{filename}: " f"{percent}% " f"({received:,}/{total:,})" ) ) def open_settings( self, ) -> None: window = SettingsWindow( self.settings_data, self, ) window.settingsChanged.connect( self._settings_changed ) self.settings_window = window window.exec() def open_plugins( self, ) -> None: self.plugins_window = ( PluginManagerWindow( self.browser, self.db, self.context.name, self, ) ) self.plugins_window.show() def open_profiles( self, ) -> None: window = ProfileManager( self.db.list_profiles(), self.settings_data.get( "active_profile", "default", ), self, ) window.profileSelected.connect( self._profile_selected ) self.profile_window = window window.exec() def open_addons( self, ) -> None: self.addons_window = ( AddonManagerWindow( self.context, self, ) ) self.addons_window.show() def open_network( self, ) -> None: self.network_window = ( NetworkInspector( self.browser.network, self, ) ) self.network_window.show() def open_devtools( self, ) -> None: self.browser.open_devtools() def _settings_changed( self, ) -> None: save_settings( self.settings_data ) def _profile_selected( self, profile_name: str, ) -> None: current = ( self.settings_data.get( "active_profile" ) ) if profile_name == current: return self.settings_data[ "active_profile" ] = profile_name save_settings( self.settings_data ) QMessageBox.information( self, APP_NAME, ( "Profile updated.\n\n" "Restart required." ), ) def _persist_window_state( self, ) -> None: size = self.size() self.settings_data[ "window_width" ] = size.width() self.settings_data[ "window_height" ] = size.height() save_settings( self.settings_data ) def _shutdown( self, ) -> None: try: self.browser.shutdown() except Exception: log.exception( "browser shutdown failed" ) try: self.db.shutdown() except Exception: log.exception( "database shutdown failed" ) # v2 def closeEvent( self, event, ): if getattr( self, "_closing", False, ): event.accept() return tray_cfg = self.settings_data.get( "tray", {}, ) if ( tray_cfg.get( "close_to_tray", True, ) and self.tray_manager ): event.ignore() self.hide() return self._closing = True try: self._persist_window_state() for widget in ( self.plugins_window, self.settings_window, self.profile_window, self.addons_window, self.network_window, ): if widget: try: widget.close() except Exception: log.exception( "child window close failed" ) self._shutdown() except Exception: log.exception( "shutdown failed" ) event.accept() super().closeEvent( event ) def build_database() -> Database: return Database( DATABASE_PATH ) # v3 def build_app() -> QApplication: os.environ.setdefault( "QTWEBENGINE_REMOTE_DEBUGGING", "0", ) os.environ.setdefault( "CHROME_CRASHPAD_PIPE_NAME", "", ) QApplication.setAttribute( Qt.ApplicationAttribute.AA_ShareOpenGLContexts ) return QApplication( sys.argv ) def fatal_error( text: str, ) -> None: try: app = QApplication.instance() if app is None: app = QApplication( sys.argv ) QMessageBox.critical( None, APP_NAME, text, ) except Exception: pass # v3 def main() -> int: try: ensure_directories() settings = load_settings() db = build_database() QApplication.setDesktopSettingsAware( True ) app = build_app() icon = QIcon( "app.ico" ) if icon.isNull(): for candidate in ( "ui/app.ico", "assets/app.ico", "icon.ico", ): test = QIcon( candidate ) if not test.isNull(): icon = test break if not icon.isNull(): app.setWindowIcon( icon ) app.setApplicationName( APP_NAME ) app.setApplicationVersion( APP_VERSION ) app.setStyle( "Fusion" ) apply_dark_palette( app ) app.setStyleSheet( apply_stylesheet() ) window = MainWindow( db=db, settings=settings, ) window.show() return app.exec() except KeyboardInterrupt: return 0 except SystemExit: return 0 except Exception as exc: log.exception( "fatal startup error" ) fatal_error( str(exc) ) return 1 if __name__ == "__main__": raise SystemExit( main() )