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()
|
||||||
|
)
|
||||||
1609
managers.py
Normal file
1609
managers.py
Normal file
File diff suppressed because it is too large
Load Diff
768
models.py
Normal file
768
models.py
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
# v7
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
QAbstractTableModel,
|
||||||
|
QModelIndex,
|
||||||
|
Qt,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ProfileContext:
|
||||||
|
name: str
|
||||||
|
|
||||||
|
root: Path
|
||||||
|
|
||||||
|
storage_path: Path
|
||||||
|
cache_path: Path
|
||||||
|
downloads_path: Path
|
||||||
|
|
||||||
|
plugins_path: Path
|
||||||
|
themes_path: Path
|
||||||
|
extensions_path: Path
|
||||||
|
|
||||||
|
profile_db: Path
|
||||||
|
permissions_db: Path
|
||||||
|
network_db: Path
|
||||||
|
|
||||||
|
class NetworkTableModel(
|
||||||
|
QAbstractTableModel
|
||||||
|
):
|
||||||
|
HEADERS = (
|
||||||
|
"Method",
|
||||||
|
"Status",
|
||||||
|
"Host",
|
||||||
|
"URL",
|
||||||
|
"Type",
|
||||||
|
"Duration",
|
||||||
|
"Timestamp",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._rows: list[
|
||||||
|
dict[str, Any]
|
||||||
|
] = []
|
||||||
|
|
||||||
|
def set_rows(
|
||||||
|
self,
|
||||||
|
rows,
|
||||||
|
) -> None:
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
self._rows = [
|
||||||
|
dict(row)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def append_row(
|
||||||
|
self,
|
||||||
|
row: dict,
|
||||||
|
) -> None:
|
||||||
|
position = len(
|
||||||
|
self._rows
|
||||||
|
)
|
||||||
|
|
||||||
|
self.beginInsertRows(
|
||||||
|
QModelIndex(),
|
||||||
|
position,
|
||||||
|
position,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._rows.append(
|
||||||
|
row
|
||||||
|
)
|
||||||
|
|
||||||
|
self.endInsertRows()
|
||||||
|
|
||||||
|
def rowCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
) -> int:
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self._rows
|
||||||
|
)
|
||||||
|
|
||||||
|
def columnCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
) -> int:
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self.HEADERS
|
||||||
|
)
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section,
|
||||||
|
orientation,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
orientation
|
||||||
|
== Qt.Orientation.Horizontal
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
0
|
||||||
|
<= section
|
||||||
|
< len(
|
||||||
|
self.HEADERS
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return self.HEADERS[
|
||||||
|
section
|
||||||
|
]
|
||||||
|
|
||||||
|
return str(
|
||||||
|
section + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self,
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
row = self._rows[
|
||||||
|
index.row()
|
||||||
|
]
|
||||||
|
|
||||||
|
match index.column():
|
||||||
|
case 0:
|
||||||
|
return row.get(
|
||||||
|
"method",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return row.get(
|
||||||
|
"status_code",
|
||||||
|
row.get(
|
||||||
|
"status",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return row.get(
|
||||||
|
"host",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return row.get(
|
||||||
|
"url",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return row.get(
|
||||||
|
"resource_type",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
duration = row.get(
|
||||||
|
"duration_ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
if duration is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
f"{float(duration):.2f}"
|
||||||
|
)
|
||||||
|
except (
|
||||||
|
TypeError,
|
||||||
|
ValueError,
|
||||||
|
):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
return row.get(
|
||||||
|
"timestamp",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def row(
|
||||||
|
self,
|
||||||
|
position: int,
|
||||||
|
) -> dict:
|
||||||
|
return self._rows[
|
||||||
|
position
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileModel(
|
||||||
|
QAbstractTableModel
|
||||||
|
):
|
||||||
|
HEADERS = (
|
||||||
|
"Profile",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._profiles: list[
|
||||||
|
str
|
||||||
|
] = []
|
||||||
|
|
||||||
|
def set_profiles(
|
||||||
|
self,
|
||||||
|
profiles: list[str],
|
||||||
|
):
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
self._profiles = sorted(
|
||||||
|
profiles
|
||||||
|
)
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def rowCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self._profiles
|
||||||
|
)
|
||||||
|
|
||||||
|
def columnCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else 1
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section,
|
||||||
|
orientation,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
orientation
|
||||||
|
== Qt.Orientation.Horizontal
|
||||||
|
):
|
||||||
|
return self.HEADERS[
|
||||||
|
section
|
||||||
|
]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self,
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._profiles[
|
||||||
|
index.row()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AddonModel(
|
||||||
|
QAbstractTableModel
|
||||||
|
):
|
||||||
|
HEADERS = (
|
||||||
|
"Name",
|
||||||
|
"Enabled",
|
||||||
|
"Type",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._addons: list[
|
||||||
|
dict
|
||||||
|
] = []
|
||||||
|
|
||||||
|
def set_addons(
|
||||||
|
self,
|
||||||
|
addons: list[dict],
|
||||||
|
):
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
self._addons = list(
|
||||||
|
addons
|
||||||
|
)
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def rowCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self._addons
|
||||||
|
)
|
||||||
|
|
||||||
|
def columnCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else 3
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section,
|
||||||
|
orientation,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
orientation
|
||||||
|
== Qt.Orientation.Horizontal
|
||||||
|
):
|
||||||
|
return self.HEADERS[
|
||||||
|
section
|
||||||
|
]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self,
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
addon = self._addons[
|
||||||
|
index.row()
|
||||||
|
]
|
||||||
|
|
||||||
|
match index.column():
|
||||||
|
case 0:
|
||||||
|
return addon.get(
|
||||||
|
"name",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
"Yes"
|
||||||
|
if addon.get(
|
||||||
|
"enabled",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
else "No"
|
||||||
|
)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return addon.get(
|
||||||
|
"type",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PluginTableModel(
|
||||||
|
QAbstractTableModel
|
||||||
|
):
|
||||||
|
HEADERS = (
|
||||||
|
"Enabled",
|
||||||
|
"Name",
|
||||||
|
"Version",
|
||||||
|
"Author",
|
||||||
|
"Status",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._plugins = []
|
||||||
|
|
||||||
|
def set_plugins(
|
||||||
|
self,
|
||||||
|
plugins,
|
||||||
|
):
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
self._plugins = [
|
||||||
|
dict(plugin)
|
||||||
|
for plugin in plugins
|
||||||
|
]
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def rowCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self._plugins
|
||||||
|
)
|
||||||
|
|
||||||
|
def columnCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else 5
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section,
|
||||||
|
orientation,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
orientation
|
||||||
|
== Qt.Orientation.Horizontal
|
||||||
|
):
|
||||||
|
return self.HEADERS[
|
||||||
|
section
|
||||||
|
]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self,
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
plugin = self._plugins[
|
||||||
|
index.row()
|
||||||
|
]
|
||||||
|
|
||||||
|
match index.column():
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
"Yes"
|
||||||
|
if plugin.get(
|
||||||
|
"enabled",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
else "No"
|
||||||
|
)
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return plugin.get(
|
||||||
|
"name",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return plugin.get(
|
||||||
|
"version",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return plugin.get(
|
||||||
|
"author",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return plugin.get(
|
||||||
|
"status",
|
||||||
|
"Loaded",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def row(
|
||||||
|
self,
|
||||||
|
position: int,
|
||||||
|
) -> dict:
|
||||||
|
return self._plugins[
|
||||||
|
position
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PluginLogTableModel(
|
||||||
|
QAbstractTableModel
|
||||||
|
):
|
||||||
|
HEADERS = (
|
||||||
|
"Time",
|
||||||
|
"Plugin",
|
||||||
|
"Level",
|
||||||
|
"Message",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._rows = []
|
||||||
|
|
||||||
|
def set_rows(
|
||||||
|
self,
|
||||||
|
rows,
|
||||||
|
):
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
self._rows = [
|
||||||
|
dict(row)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def rowCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self._rows
|
||||||
|
)
|
||||||
|
|
||||||
|
def columnCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else 4
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section,
|
||||||
|
orientation,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
orientation
|
||||||
|
== Qt.Orientation.Horizontal
|
||||||
|
):
|
||||||
|
return self.HEADERS[
|
||||||
|
section
|
||||||
|
]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self,
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
row = self._rows[
|
||||||
|
index.row()
|
||||||
|
]
|
||||||
|
|
||||||
|
match index.column():
|
||||||
|
case 0:
|
||||||
|
return row.get(
|
||||||
|
"created_at",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return row.get(
|
||||||
|
"plugin_name",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return row.get(
|
||||||
|
"level",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return row.get(
|
||||||
|
"message",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PluginCrashTableModel(
|
||||||
|
QAbstractTableModel
|
||||||
|
):
|
||||||
|
HEADERS = (
|
||||||
|
"Time",
|
||||||
|
"Plugin",
|
||||||
|
"Error",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._rows = []
|
||||||
|
|
||||||
|
def set_rows(
|
||||||
|
self,
|
||||||
|
rows,
|
||||||
|
):
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
self._rows = [
|
||||||
|
dict(row)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def rowCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else len(
|
||||||
|
self._rows
|
||||||
|
)
|
||||||
|
|
||||||
|
def columnCount(
|
||||||
|
self,
|
||||||
|
parent=QModelIndex(),
|
||||||
|
):
|
||||||
|
return 0 if parent.isValid() else 3
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section,
|
||||||
|
orientation,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
orientation
|
||||||
|
== Qt.Orientation.Horizontal
|
||||||
|
):
|
||||||
|
return self.HEADERS[
|
||||||
|
section
|
||||||
|
]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self,
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or role
|
||||||
|
!= Qt.ItemDataRole.DisplayRole
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
row = self._rows[
|
||||||
|
index.row()
|
||||||
|
]
|
||||||
|
|
||||||
|
match index.column():
|
||||||
|
case 0:
|
||||||
|
return row.get(
|
||||||
|
"created_at",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return row.get(
|
||||||
|
"plugin_name",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return row.get(
|
||||||
|
"error",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
851
network.py
Normal file
851
network.py
Normal file
@@ -0,0 +1,851 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
|
||||||
|
from config import DISCORD_TELEMETRY_BLOCKS
|
||||||
|
|
||||||
|
from blocklists import (
|
||||||
|
BlocklistEngine,
|
||||||
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REDACT_HEADERS = {
|
||||||
|
"authorization",
|
||||||
|
"cookie",
|
||||||
|
"set-cookie",
|
||||||
|
"x-super-properties",
|
||||||
|
"x-fingerprint",
|
||||||
|
"token",
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
}
|
||||||
|
|
||||||
|
DISCORD_GATEWAY_HOSTS = {
|
||||||
|
"gateway.discord.gg",
|
||||||
|
"remote-auth-gateway.discord.gg",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ProxyConfig:
|
||||||
|
enabled: bool = False
|
||||||
|
proxy_type: str = "http"
|
||||||
|
host: str = ""
|
||||||
|
port: int = 0
|
||||||
|
username: str = ""
|
||||||
|
password: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid(self) -> bool:
|
||||||
|
return (
|
||||||
|
self.enabled
|
||||||
|
and bool(self.host)
|
||||||
|
and self.port > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(
|
||||||
|
self,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"proxy_type": self.proxy_type,
|
||||||
|
"host": self.host,
|
||||||
|
"port": self.port,
|
||||||
|
"username": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(
|
||||||
|
cls,
|
||||||
|
data: dict[str, Any] | None,
|
||||||
|
) -> "ProxyConfig":
|
||||||
|
data = data or {}
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
enabled=bool(
|
||||||
|
data.get(
|
||||||
|
"enabled",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
proxy_type=str(
|
||||||
|
data.get(
|
||||||
|
"proxy_type",
|
||||||
|
"http",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
host=str(
|
||||||
|
data.get(
|
||||||
|
"host",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
port=int(
|
||||||
|
data.get(
|
||||||
|
"port",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
username=str(
|
||||||
|
data.get(
|
||||||
|
"username",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
password=str(
|
||||||
|
data.get(
|
||||||
|
"password",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyManager(QObject):
|
||||||
|
proxyChanged = Signal(dict)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_proxy(
|
||||||
|
self,
|
||||||
|
profile_name: str,
|
||||||
|
) -> ProxyConfig:
|
||||||
|
return ProxyConfig.from_dict(
|
||||||
|
self.db.get_setting(
|
||||||
|
f"proxy:{profile_name}",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_proxy(
|
||||||
|
self,
|
||||||
|
profile_name: str,
|
||||||
|
proxy: ProxyConfig,
|
||||||
|
) -> None:
|
||||||
|
self.db.set_setting(
|
||||||
|
f"proxy:{profile_name}",
|
||||||
|
proxy.to_dict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.proxyChanged.emit(
|
||||||
|
proxy.to_dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
def clear_proxy(
|
||||||
|
self,
|
||||||
|
profile_name: str,
|
||||||
|
) -> None:
|
||||||
|
self.save_proxy(
|
||||||
|
profile_name,
|
||||||
|
ProxyConfig(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_proxy(
|
||||||
|
self,
|
||||||
|
proxy: ProxyConfig,
|
||||||
|
timeout: float = 5.0,
|
||||||
|
) -> bool:
|
||||||
|
if not proxy.valid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with socket.create_connection(
|
||||||
|
(
|
||||||
|
proxy.host,
|
||||||
|
proxy.port,
|
||||||
|
),
|
||||||
|
timeout=timeout,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# v2
|
||||||
|
class DomainBlocker:
|
||||||
|
|
||||||
|
DEFAULT_BLOCKS = set(
|
||||||
|
DISCORD_TELEMETRY_BLOCKS
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db,
|
||||||
|
blocklist_engine=None,
|
||||||
|
):
|
||||||
|
self.db = db
|
||||||
|
self.blocklist = (
|
||||||
|
blocklist_engine
|
||||||
|
)
|
||||||
|
|
||||||
|
def domains(
|
||||||
|
self,
|
||||||
|
) -> set[str]:
|
||||||
|
domains = set(
|
||||||
|
self.DEFAULT_BLOCKS
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
domains.update(
|
||||||
|
self.db.blocked_domains()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
log.exception(
|
||||||
|
"blocked domain load failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
str(domain)
|
||||||
|
.lower()
|
||||||
|
.strip()
|
||||||
|
for domain in domains
|
||||||
|
if domain
|
||||||
|
}
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
domain: str,
|
||||||
|
) -> None:
|
||||||
|
domain = (
|
||||||
|
domain
|
||||||
|
.lower()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if domain:
|
||||||
|
self.db.add_blocked_domain(
|
||||||
|
domain
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove(
|
||||||
|
self,
|
||||||
|
domain: str,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
with self.db.connection() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM blocked_domains
|
||||||
|
WHERE domain=?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
domain.lower()
|
||||||
|
.strip(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
log.exception(
|
||||||
|
"domain removal failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# v2
|
||||||
|
def is_blocked(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
host = (
|
||||||
|
host.lower()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.blocklist
|
||||||
|
and self.blocklist.contains(
|
||||||
|
host
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
for domain in self.domains():
|
||||||
|
|
||||||
|
if (
|
||||||
|
host == domain
|
||||||
|
or host.endswith(
|
||||||
|
f".{domain}"
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
class HeaderRules:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db,
|
||||||
|
):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def all(
|
||||||
|
self,
|
||||||
|
) -> list[dict]:
|
||||||
|
return self.db.get_setting(
|
||||||
|
"header_rules",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(
|
||||||
|
self,
|
||||||
|
rules: list[dict],
|
||||||
|
) -> None:
|
||||||
|
self.db.set_setting(
|
||||||
|
"header_rules",
|
||||||
|
rules,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def redact(
|
||||||
|
headers: dict,
|
||||||
|
) -> dict:
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, value in (
|
||||||
|
headers or {}
|
||||||
|
).items():
|
||||||
|
if (
|
||||||
|
str(key)
|
||||||
|
.lower()
|
||||||
|
.strip()
|
||||||
|
in REDACT_HEADERS
|
||||||
|
):
|
||||||
|
result[key] = (
|
||||||
|
"[REDACTED]"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
self,
|
||||||
|
headers: dict,
|
||||||
|
) -> dict:
|
||||||
|
result = dict(
|
||||||
|
headers or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
for rule in self.all():
|
||||||
|
action = str(
|
||||||
|
rule.get(
|
||||||
|
"action",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
).lower()
|
||||||
|
|
||||||
|
name = str(
|
||||||
|
rule.get(
|
||||||
|
"name",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
value = rule.get(
|
||||||
|
"value",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if action in {
|
||||||
|
"add",
|
||||||
|
"replace",
|
||||||
|
}:
|
||||||
|
result[name] = value
|
||||||
|
|
||||||
|
elif action == "remove":
|
||||||
|
result.pop(
|
||||||
|
name,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
# v5
|
||||||
|
|
||||||
|
|
||||||
|
# v3
|
||||||
|
class NetworkRules:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
settings: dict,
|
||||||
|
):
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
profile_name: str | None = None,
|
||||||
|
plugin_name: str | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
|
||||||
|
try:
|
||||||
|
rules = self.settings.get(
|
||||||
|
"network_rules",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(
|
||||||
|
rules,
|
||||||
|
list,
|
||||||
|
):
|
||||||
|
return []
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
log.exception(
|
||||||
|
"network rule load failed"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
output: list[dict] = []
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
|
||||||
|
if not isinstance(
|
||||||
|
rule,
|
||||||
|
dict,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
profile_name
|
||||||
|
and rule.get(
|
||||||
|
"profile"
|
||||||
|
)
|
||||||
|
not in {
|
||||||
|
None,
|
||||||
|
profile_name,
|
||||||
|
}
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
plugin_name
|
||||||
|
and rule.get(
|
||||||
|
"plugin"
|
||||||
|
)
|
||||||
|
not in {
|
||||||
|
None,
|
||||||
|
plugin_name,
|
||||||
|
}
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
output.append(
|
||||||
|
rule
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def evaluate(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
profile_name: str | None = None,
|
||||||
|
plugin_name: str | None = None,
|
||||||
|
) -> tuple[bool, str | None]:
|
||||||
|
|
||||||
|
host = (
|
||||||
|
host.lower()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
rules = sorted(
|
||||||
|
self.load(
|
||||||
|
profile_name,
|
||||||
|
plugin_name,
|
||||||
|
),
|
||||||
|
key=lambda r: (
|
||||||
|
0
|
||||||
|
if r.get(
|
||||||
|
"plugin"
|
||||||
|
)
|
||||||
|
else (
|
||||||
|
1
|
||||||
|
if r.get(
|
||||||
|
"profile"
|
||||||
|
)
|
||||||
|
else 2
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
|
||||||
|
pattern = (
|
||||||
|
str(
|
||||||
|
rule.get(
|
||||||
|
"pattern",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.lower()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not pattern:
|
||||||
|
continue
|
||||||
|
|
||||||
|
matched = (
|
||||||
|
host == pattern
|
||||||
|
or host.endswith(
|
||||||
|
f".{pattern}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
continue
|
||||||
|
|
||||||
|
action = (
|
||||||
|
str(
|
||||||
|
rule.get(
|
||||||
|
"action",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.lower()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if action in {
|
||||||
|
"block",
|
||||||
|
"deny",
|
||||||
|
}:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "allow":
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "redirect":
|
||||||
|
target = (
|
||||||
|
str(
|
||||||
|
rule.get(
|
||||||
|
"target",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
target or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
class RequestReplay:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db,
|
||||||
|
):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_request(
|
||||||
|
self,
|
||||||
|
request_id: int,
|
||||||
|
):
|
||||||
|
with self.db.connection() as conn:
|
||||||
|
return conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM network_requests
|
||||||
|
WHERE id=?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
request_id,
|
||||||
|
),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
class HARExporter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db,
|
||||||
|
):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def export(
|
||||||
|
self,
|
||||||
|
output_file: str | Path,
|
||||||
|
limit: int = 5000,
|
||||||
|
) -> Path:
|
||||||
|
output_file = Path(
|
||||||
|
output_file
|
||||||
|
)
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
for row in self.db.recent_requests(
|
||||||
|
limit
|
||||||
|
):
|
||||||
|
entries.append(
|
||||||
|
{
|
||||||
|
"startedDateTime": row[
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": row[
|
||||||
|
"method"
|
||||||
|
],
|
||||||
|
"url": row[
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": row[
|
||||||
|
"status_code"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
output_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"version": "1.2",
|
||||||
|
"creator": {
|
||||||
|
"name": "Discord Client",
|
||||||
|
"version": "1.0",
|
||||||
|
},
|
||||||
|
"entries": entries,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
return output_file
|
||||||
|
|
||||||
|
|
||||||
|
# v4# v5
|
||||||
|
class NetworkBackend(QObject):
|
||||||
|
requestAdded = Signal(dict)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db,
|
||||||
|
settings: dict,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.db = db
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
mode = self.db.get_setting(
|
||||||
|
"blocklist_mode",
|
||||||
|
"pro",
|
||||||
|
)
|
||||||
|
|
||||||
|
blocklist_dir = Path(
|
||||||
|
"config/blocklists"
|
||||||
|
)
|
||||||
|
|
||||||
|
blocklists = sorted(
|
||||||
|
path
|
||||||
|
for path in blocklist_dir.glob(
|
||||||
|
"*.txt"
|
||||||
|
)
|
||||||
|
if path.is_file()
|
||||||
|
)
|
||||||
|
|
||||||
|
if mode == "pro":
|
||||||
|
blocklists = [
|
||||||
|
path
|
||||||
|
for path in blocklists
|
||||||
|
if "ultimate"
|
||||||
|
not in path.name.lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
cache_file = (
|
||||||
|
blocklist_dir
|
||||||
|
/ f"{mode}.cache.bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.blocklist_engine = (
|
||||||
|
BlocklistEngine(
|
||||||
|
blocklists=blocklists,
|
||||||
|
cache_file=cache_file,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.blocklist_engine.load()
|
||||||
|
except Exception:
|
||||||
|
log.exception(
|
||||||
|
"blocklist load failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.blocker = DomainBlocker(
|
||||||
|
db,
|
||||||
|
self.blocklist_engine,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.headers = HeaderRules(
|
||||||
|
db
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rules = NetworkRules(
|
||||||
|
settings
|
||||||
|
)
|
||||||
|
|
||||||
|
self.proxy = ProxyManager(
|
||||||
|
db
|
||||||
|
)
|
||||||
|
|
||||||
|
self.har = HARExporter(
|
||||||
|
db
|
||||||
|
)
|
||||||
|
|
||||||
|
self.replay = RequestReplay(
|
||||||
|
db
|
||||||
|
)
|
||||||
|
|
||||||
|
def recent(
|
||||||
|
self,
|
||||||
|
limit: int = 1000,
|
||||||
|
):
|
||||||
|
return self.db.recent_requests(
|
||||||
|
limit
|
||||||
|
)
|
||||||
|
|
||||||
|
def search(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
limit: int = 1000,
|
||||||
|
):
|
||||||
|
return self.db.search_requests(
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def host_from_url(
|
||||||
|
url: str,
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
urlparse(url)
|
||||||
|
.hostname
|
||||||
|
or ""
|
||||||
|
).lower()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def process_request(
|
||||||
|
self,
|
||||||
|
request: dict,
|
||||||
|
) -> tuple[dict, bool]:
|
||||||
|
|
||||||
|
url = str(
|
||||||
|
request.get(
|
||||||
|
"url",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
host = self.host_from_url(
|
||||||
|
url
|
||||||
|
)
|
||||||
|
|
||||||
|
request["host"] = host
|
||||||
|
|
||||||
|
if host in DISCORD_GATEWAY_HOSTS:
|
||||||
|
|
||||||
|
self.requestAdded.emit(
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
return request, False
|
||||||
|
|
||||||
|
if (
|
||||||
|
host
|
||||||
|
and self.blocker.is_blocked(
|
||||||
|
host
|
||||||
|
)
|
||||||
|
):
|
||||||
|
|
||||||
|
request["_blocked"] = True
|
||||||
|
|
||||||
|
self.requestAdded.emit(
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
return request, True
|
||||||
|
|
||||||
|
allowed, redirect_url = (
|
||||||
|
self.rules.evaluate(
|
||||||
|
host=host,
|
||||||
|
profile_name=request.get(
|
||||||
|
"profile"
|
||||||
|
),
|
||||||
|
plugin_name=request.get(
|
||||||
|
"plugin"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not allowed:
|
||||||
|
|
||||||
|
request["_blocked"] = True
|
||||||
|
|
||||||
|
self.requestAdded.emit(
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
return request, True
|
||||||
|
|
||||||
|
if redirect_url:
|
||||||
|
|
||||||
|
request[
|
||||||
|
"redirect_url"
|
||||||
|
] = redirect_url
|
||||||
|
|
||||||
|
headers = self.headers.apply(
|
||||||
|
request.get(
|
||||||
|
"headers",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
request["headers"] = (
|
||||||
|
self.headers.redact(
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.requestAdded.emit(
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
|
return request, False
|
||||||
102
theme.py
Normal file
102
theme.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# v1
|
||||||
|
from PySide6.QtGui import QColor
|
||||||
|
from PySide6.QtGui import QPalette
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
|
||||||
|
def apply_dark_palette(
|
||||||
|
app: QApplication,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
palette = QPalette()
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.Window,
|
||||||
|
QColor(24, 24, 27),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.WindowText,
|
||||||
|
QColor(250, 250, 250),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.Base,
|
||||||
|
QColor(17, 17, 20),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.AlternateBase,
|
||||||
|
QColor(30, 30, 34),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.Text,
|
||||||
|
QColor(250, 250, 250),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.Button,
|
||||||
|
QColor(39, 39, 42),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.ButtonText,
|
||||||
|
QColor(250, 250, 250),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.Highlight,
|
||||||
|
QColor(59, 130, 246),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.HighlightedText,
|
||||||
|
QColor(255, 255, 255),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.ToolTipBase,
|
||||||
|
QColor(24, 24, 27),
|
||||||
|
)
|
||||||
|
|
||||||
|
palette.setColor(
|
||||||
|
QPalette.ColorRole.ToolTipText,
|
||||||
|
QColor(250, 250, 250),
|
||||||
|
)
|
||||||
|
|
||||||
|
app.setPalette(
|
||||||
|
palette
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_stylesheet(
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
return """
|
||||||
|
QPushButton {
|
||||||
|
border:none;
|
||||||
|
border-radius:6px;
|
||||||
|
padding:6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLineEdit,
|
||||||
|
QTextEdit,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QComboBox,
|
||||||
|
QListWidget,
|
||||||
|
QTreeWidget,
|
||||||
|
QTableView {
|
||||||
|
border:1px solid #3f3f46;
|
||||||
|
border-radius:6px;
|
||||||
|
padding:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTabWidget::pane {
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
QToolTip {
|
||||||
|
border:1px solid #3f3f46;
|
||||||
|
}
|
||||||
|
"""
|
||||||
Reference in New Issue
Block a user