Files
devdisc/main.py
2026-06-08 06:17:48 +00:00

691 lines
14 KiB
Python

# 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()
)