691 lines
14 KiB
Python
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()
|
|
) |