v1
This commit is contained in:
691
main.py
Normal file
691
main.py
Normal file
@@ -0,0 +1,691 @@
|
||||
# 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()
|
||||
)
|
||||
Reference in New Issue
Block a user