1609 lines
34 KiB
Python
1609 lines
34 KiB
Python
|
|
from __future__ import annotations
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import QObject, Qt, Signal
|
|
from PySide6.QtGui import QAction, QIcon
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QCheckBox,
|
|
QDialog,
|
|
QFileDialog,
|
|
QFormLayout,
|
|
QGroupBox,
|
|
QHBoxLayout,
|
|
QLineEdit,
|
|
QListWidget,
|
|
QMainWindow,
|
|
QMenu,
|
|
QMessageBox,
|
|
QPlainTextEdit,
|
|
QPushButton,
|
|
QSplitter,
|
|
QSystemTrayIcon,
|
|
QTableView,
|
|
QTabWidget,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
QInputDialog,
|
|
)
|
|
|
|
from models import (
|
|
AddonModel,
|
|
NetworkTableModel,
|
|
PluginCrashTableModel,
|
|
PluginLogTableModel,
|
|
PluginTableModel,
|
|
ProfileContext,
|
|
)
|
|
from addons import AddonManager
|
|
from network import ProxyConfig
|
|
from config import (
|
|
PLUGIN_ALLOWED_EXTENSIONS,
|
|
PLUGIN_BLOCKED_PATTERNS,
|
|
PLUGIN_FILE_PATTERN,
|
|
PLUGIN_MAX_SIZE,
|
|
profile_extension_dir,
|
|
profile_plugin_dir,
|
|
profile_theme_dir,
|
|
profile_root,
|
|
)
|
|
|
|
# v2
|
|
class ContextManager:
|
|
def __init__(
|
|
self,
|
|
db,
|
|
settings: dict,
|
|
):
|
|
self.db = db
|
|
self.settings = settings
|
|
self._contexts: dict[
|
|
str,
|
|
ProfileContext,
|
|
] = {}
|
|
|
|
def current(
|
|
self,
|
|
) -> ProfileContext:
|
|
return self.profile(
|
|
self.settings.get(
|
|
"active_profile",
|
|
"default",
|
|
)
|
|
)
|
|
|
|
def get(
|
|
self,
|
|
profile_name: str,
|
|
) -> ProfileContext:
|
|
return self.profile(
|
|
profile_name
|
|
)
|
|
|
|
def register(
|
|
self,
|
|
context: ProfileContext,
|
|
) -> None:
|
|
self._contexts[
|
|
context.name
|
|
] = context
|
|
|
|
def remove(
|
|
self,
|
|
profile_name: str,
|
|
) -> None:
|
|
self._contexts.pop(
|
|
profile_name,
|
|
None,
|
|
)
|
|
|
|
def profile(
|
|
self,
|
|
profile_name: str,
|
|
) -> ProfileContext:
|
|
|
|
if (
|
|
profile_name
|
|
in self._contexts
|
|
):
|
|
return self._contexts[
|
|
profile_name
|
|
]
|
|
|
|
self.db.ensure_profile(
|
|
profile_name
|
|
)
|
|
|
|
root = profile_root(
|
|
profile_name
|
|
)
|
|
|
|
# v3
|
|
context = ProfileContext(
|
|
name=profile_name,
|
|
root=root,
|
|
storage_path=(
|
|
root / "storage"
|
|
),
|
|
cache_path=(
|
|
root / "cache"
|
|
),
|
|
downloads_path=(
|
|
root / "downloads"
|
|
),
|
|
plugins_path=(
|
|
profile_plugin_dir(
|
|
profile_name
|
|
)
|
|
),
|
|
themes_path=(
|
|
profile_theme_dir(
|
|
profile_name
|
|
)
|
|
),
|
|
extensions_path=(
|
|
profile_extension_dir(
|
|
profile_name
|
|
)
|
|
),
|
|
profile_db=(
|
|
root / "profile.db"
|
|
),
|
|
permissions_db=(
|
|
root / "permissions.db"
|
|
),
|
|
network_db=(
|
|
root / "network.db"
|
|
),
|
|
)
|
|
context.storage_path.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
context.cache_path.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
context.downloads_path.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
self._contexts[
|
|
profile_name
|
|
] = context
|
|
|
|
return context
|
|
|
|
def switch(
|
|
self,
|
|
profile_name: str,
|
|
) -> ProfileContext:
|
|
|
|
self.settings[
|
|
"active_profile"
|
|
] = profile_name
|
|
save_settings(
|
|
self.settings
|
|
)
|
|
return self.profile(
|
|
profile_name
|
|
)
|
|
class PluginManagerWindow(
|
|
QDialog
|
|
):
|
|
def __init__(
|
|
self,
|
|
browser,
|
|
db,
|
|
profile_name,
|
|
parent=None,
|
|
):
|
|
super().__init__(
|
|
parent
|
|
)
|
|
|
|
self.browser = browser
|
|
self.db = db
|
|
self.profile_name = (
|
|
profile_name
|
|
)
|
|
|
|
self.resize(
|
|
1200,
|
|
850,
|
|
)
|
|
|
|
self.setWindowTitle(
|
|
"Plugins"
|
|
)
|
|
|
|
self.model = (
|
|
PluginTableModel()
|
|
)
|
|
|
|
self.logs_model = (
|
|
PluginLogTableModel()
|
|
)
|
|
|
|
self.crash_model = (
|
|
PluginCrashTableModel()
|
|
)
|
|
|
|
self.plugins_table = (
|
|
QTableView()
|
|
)
|
|
|
|
self.logs_table = (
|
|
QTableView()
|
|
)
|
|
|
|
self.crashes_table = (
|
|
QTableView()
|
|
)
|
|
|
|
self.diagnostics = (
|
|
QPlainTextEdit()
|
|
)
|
|
|
|
self.diagnostics.setReadOnly(
|
|
True
|
|
)
|
|
|
|
self.plugins_table.setModel(
|
|
self.model
|
|
)
|
|
|
|
self.logs_table.setModel(
|
|
self.logs_model
|
|
)
|
|
|
|
self.crashes_table.setModel(
|
|
self.crash_model
|
|
)
|
|
|
|
tabs = QTabWidget()
|
|
|
|
tabs.addTab(
|
|
self.plugins_table,
|
|
"Plugins",
|
|
)
|
|
|
|
tabs.addTab(
|
|
self.logs_table,
|
|
"Logs",
|
|
)
|
|
|
|
tabs.addTab(
|
|
self.crashes_table,
|
|
"Crashes",
|
|
)
|
|
|
|
tabs.addTab(
|
|
self.diagnostics,
|
|
"Diagnostics",
|
|
)
|
|
|
|
self.install_file_btn = (
|
|
QPushButton(
|
|
"Install File"
|
|
)
|
|
)
|
|
|
|
self.install_url_btn = (
|
|
QPushButton(
|
|
"Install URL"
|
|
)
|
|
)
|
|
|
|
self.enable_btn = (
|
|
QPushButton(
|
|
"Enable"
|
|
)
|
|
)
|
|
|
|
self.disable_btn = (
|
|
QPushButton(
|
|
"Disable"
|
|
)
|
|
)
|
|
|
|
self.reload_btn = (
|
|
QPushButton(
|
|
"Reload"
|
|
)
|
|
)
|
|
|
|
self.delete_btn = (
|
|
QPushButton(
|
|
"Delete"
|
|
)
|
|
)
|
|
|
|
buttons = QHBoxLayout()
|
|
|
|
for button in (
|
|
self.install_file_btn,
|
|
self.install_url_btn,
|
|
self.enable_btn,
|
|
self.disable_btn,
|
|
self.reload_btn,
|
|
self.delete_btn,
|
|
):
|
|
buttons.addWidget(
|
|
button
|
|
)
|
|
|
|
layout = QVBoxLayout(
|
|
self
|
|
)
|
|
|
|
layout.addWidget(
|
|
tabs
|
|
)
|
|
|
|
layout.addLayout(
|
|
buttons
|
|
)
|
|
|
|
self.install_file_btn.clicked.connect(
|
|
self.install_file
|
|
)
|
|
|
|
self.install_url_btn.clicked.connect(
|
|
self.install_url
|
|
)
|
|
|
|
self.enable_btn.clicked.connect(
|
|
self.enable_plugin
|
|
)
|
|
|
|
self.disable_btn.clicked.connect(
|
|
self.disable_plugin
|
|
)
|
|
|
|
self.reload_btn.clicked.connect(
|
|
self.reload_plugins
|
|
)
|
|
|
|
self.delete_btn.clicked.connect(
|
|
self.delete_plugin
|
|
)
|
|
|
|
self.reload()
|
|
|
|
def selected_plugin(
|
|
self,
|
|
) -> str | None:
|
|
index = (
|
|
self.plugins_table.currentIndex()
|
|
)
|
|
|
|
if not index.isValid():
|
|
return None
|
|
|
|
row = self.model.row(
|
|
index.row()
|
|
)
|
|
|
|
return row.get(
|
|
"name"
|
|
)
|
|
|
|
def install_file(
|
|
self,
|
|
) -> None:
|
|
path, _ = (
|
|
QFileDialog.getOpenFileName(
|
|
self,
|
|
"Install Plugin",
|
|
"",
|
|
"*.plugin.js",
|
|
)
|
|
)
|
|
|
|
if not path:
|
|
return
|
|
|
|
try:
|
|
self.browser.install_plugin_file(
|
|
path
|
|
)
|
|
|
|
self.reload()
|
|
|
|
except Exception as exc:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Install Failed",
|
|
str(exc),
|
|
)
|
|
|
|
# v2
|
|
def install_url(
|
|
self,
|
|
) -> None:
|
|
url, accepted = (
|
|
QInputDialog.getText(
|
|
self,
|
|
"Install URL",
|
|
"Plugin URL",
|
|
)
|
|
)
|
|
|
|
url = url.strip()
|
|
|
|
if (
|
|
not accepted
|
|
or not url
|
|
):
|
|
return
|
|
|
|
try:
|
|
self.browser.install_plugin_url(
|
|
url
|
|
)
|
|
|
|
self.reload()
|
|
|
|
except Exception as exc:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Install Failed",
|
|
str(exc),
|
|
)
|
|
|
|
def enable_plugin(
|
|
self,
|
|
) -> None:
|
|
plugin = (
|
|
self.selected_plugin()
|
|
)
|
|
|
|
if not plugin:
|
|
return
|
|
|
|
self.browser.set_plugin_enabled(
|
|
plugin,
|
|
True,
|
|
)
|
|
|
|
self.reload()
|
|
|
|
def disable_plugin(
|
|
self,
|
|
) -> None:
|
|
plugin = (
|
|
self.selected_plugin()
|
|
)
|
|
|
|
if not plugin:
|
|
return
|
|
|
|
self.browser.set_plugin_enabled(
|
|
plugin,
|
|
False,
|
|
)
|
|
|
|
self.reload()
|
|
|
|
def delete_plugin(
|
|
self,
|
|
) -> None:
|
|
plugin = (
|
|
self.selected_plugin()
|
|
)
|
|
|
|
if not plugin:
|
|
return
|
|
|
|
result = (
|
|
QMessageBox.question(
|
|
self,
|
|
"Delete Plugin",
|
|
plugin,
|
|
)
|
|
)
|
|
|
|
if (
|
|
result
|
|
!= QMessageBox.StandardButton.Yes
|
|
):
|
|
return
|
|
|
|
self.browser.remove_plugin(
|
|
plugin
|
|
)
|
|
|
|
self.reload()
|
|
|
|
def reload_plugins(
|
|
self,
|
|
) -> None:
|
|
self.browser.reload_addons()
|
|
self.reload()
|
|
|
|
def reload(
|
|
self,
|
|
) -> None:
|
|
self.model.set_plugins(
|
|
self.browser.plugin_inventory()
|
|
)
|
|
|
|
self.logs_model.set_rows(
|
|
list(
|
|
self.db.plugin_logs(
|
|
self.profile_name
|
|
)
|
|
)
|
|
)
|
|
|
|
self.crash_model.set_rows(
|
|
list(
|
|
self.db.plugin_crashes(
|
|
self.profile_name
|
|
)
|
|
)
|
|
)
|
|
|
|
self.diagnostics.setPlainText(
|
|
json.dumps(
|
|
self.browser.plugin_diagnostics_snapshot(),
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
class SettingsWindow(QDialog):
|
|
settingsChanged = Signal()
|
|
|
|
def __init__(self, settings: dict, parent=None):
|
|
super().__init__(parent)
|
|
self.settings = settings
|
|
self.setWindowTitle("Settings")
|
|
self.resize(950, 700)
|
|
|
|
tabs = QTabWidget()
|
|
tabs.addTab(self._general_tab(), "General")
|
|
tabs.addTab(self._network_tab(), "Network")
|
|
tabs.addTab(self._permissions_tab(), "Permissions")
|
|
tabs.addTab(self._privacy_tab(), "Privacy")
|
|
tabs.addTab(self._developer_tab(), "Developer")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.addWidget(tabs)
|
|
|
|
save_button = QPushButton("Save")
|
|
save_button.clicked.connect(self._save)
|
|
layout.addWidget(save_button)
|
|
|
|
def _general_tab(self):
|
|
widget = QWidget()
|
|
layout = QFormLayout(widget)
|
|
|
|
self.download_path = QLineEdit(
|
|
self.settings.get("download_directory", "")
|
|
)
|
|
|
|
layout.addRow("Downloads", self.download_path)
|
|
return widget
|
|
|
|
def _network_tab(self):
|
|
widget = QWidget()
|
|
layout = QFormLayout(widget)
|
|
|
|
self.network_logging = QCheckBox()
|
|
self.network_logging.setChecked(
|
|
self.settings.get("network_logging", True)
|
|
)
|
|
|
|
layout.addRow("Enable Logging", self.network_logging)
|
|
return widget
|
|
|
|
def _permissions_tab(self):
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
permissions = self.settings.setdefault(
|
|
"permissions",
|
|
{},
|
|
)
|
|
|
|
self.permission_boxes = {}
|
|
|
|
for key, title in (
|
|
("microphone", "Microphone"),
|
|
("camera", "Camera"),
|
|
("clipboard", "Clipboard"),
|
|
("notifications", "Notifications"),
|
|
("downloads", "Downloads"),
|
|
("fullscreen", "Fullscreen"),
|
|
):
|
|
box = QCheckBox(title)
|
|
box.setChecked(
|
|
bool(
|
|
permissions.get(
|
|
key,
|
|
False,
|
|
)
|
|
)
|
|
)
|
|
self.permission_boxes[key] = box
|
|
layout.addWidget(box)
|
|
|
|
layout.addStretch()
|
|
return widget
|
|
|
|
def _privacy_tab(self):
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
blocked = set(
|
|
self.settings.get(
|
|
"blocked_hosts",
|
|
[],
|
|
)
|
|
)
|
|
|
|
self.discord_telemetry = QCheckBox(
|
|
"Block Discord Telemetry"
|
|
)
|
|
self.discord_telemetry.setChecked(
|
|
"telemetry.discord.com" in blocked
|
|
)
|
|
|
|
self.discord_crash = QCheckBox(
|
|
"Block Discord Crash Reporting"
|
|
)
|
|
self.discord_crash.setChecked(
|
|
"crash.discord.com" in blocked
|
|
)
|
|
|
|
self.sentry = QCheckBox(
|
|
"Block Sentry"
|
|
)
|
|
self.sentry.setChecked(
|
|
"sentry.io" in blocked
|
|
)
|
|
|
|
self.statsig = QCheckBox(
|
|
"Block Statsig"
|
|
)
|
|
self.statsig.setChecked(
|
|
"api.statsig.com" in blocked
|
|
)
|
|
|
|
layout.addWidget(self.discord_telemetry)
|
|
layout.addWidget(self.discord_crash)
|
|
layout.addWidget(self.sentry)
|
|
layout.addWidget(self.statsig)
|
|
|
|
group = QGroupBox(
|
|
"Blocked Hosts"
|
|
)
|
|
|
|
group_layout = QVBoxLayout(group)
|
|
|
|
self.blocked_hosts = QPlainTextEdit()
|
|
self.blocked_hosts.setPlainText(
|
|
"\n".join(
|
|
sorted(blocked)
|
|
)
|
|
)
|
|
|
|
group_layout.addWidget(
|
|
self.blocked_hosts
|
|
)
|
|
|
|
layout.addWidget(group)
|
|
|
|
return widget
|
|
|
|
def _developer_tab(self):
|
|
widget = QWidget()
|
|
layout = QFormLayout(widget)
|
|
|
|
self.enable_cdp = QCheckBox()
|
|
|
|
self.enable_cdp.setChecked(
|
|
self.settings.get(
|
|
"enable_cdp",
|
|
True,
|
|
)
|
|
)
|
|
|
|
layout.addRow(
|
|
"Enable CDP",
|
|
self.enable_cdp,
|
|
)
|
|
|
|
return widget
|
|
|
|
def _save(self):
|
|
self.settings["download_directory"] = (
|
|
self.download_path.text().strip()
|
|
)
|
|
|
|
self.settings["network_logging"] = (
|
|
self.network_logging.isChecked()
|
|
)
|
|
|
|
self.settings["enable_cdp"] = (
|
|
self.enable_cdp.isChecked()
|
|
)
|
|
|
|
permissions = self.settings.setdefault(
|
|
"permissions",
|
|
{},
|
|
)
|
|
|
|
for key, box in self.permission_boxes.items():
|
|
permissions[key] = (
|
|
box.isChecked()
|
|
)
|
|
|
|
blocked = {
|
|
line.strip().lower()
|
|
for line in self.blocked_hosts.toPlainText().splitlines()
|
|
if line.strip()
|
|
}
|
|
|
|
if self.discord_telemetry.isChecked():
|
|
blocked.add(
|
|
"telemetry.discord.com"
|
|
)
|
|
|
|
if self.discord_crash.isChecked():
|
|
blocked.add(
|
|
"crash.discord.com"
|
|
)
|
|
|
|
if self.sentry.isChecked():
|
|
blocked.add("sentry.io")
|
|
|
|
if self.statsig.isChecked():
|
|
blocked.add(
|
|
"api.statsig.com"
|
|
)
|
|
|
|
self.settings[
|
|
"blocked_hosts"
|
|
] = sorted(blocked)
|
|
|
|
self.settingsChanged.emit()
|
|
self.accept()
|
|
|
|
# v4
|
|
class ProfileManager(QDialog):
|
|
profileSelected = Signal(str)
|
|
|
|
PROFILE_MAX_FILES = 10000
|
|
PROFILE_MAX_FILE_SIZE = 50 * 1024 * 1024
|
|
PROFILE_MAX_TOTAL_SIZE = 500 * 1024 * 1024
|
|
|
|
PROFILE_EXPORT_SKIP = {
|
|
"cache",
|
|
"GPUCache",
|
|
"Code Cache",
|
|
"blob_storage",
|
|
"Service Worker",
|
|
"Session Storage",
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
profiles: list[str],
|
|
current: str,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.setWindowTitle(
|
|
"Profiles"
|
|
)
|
|
|
|
self.resize(500, 550)
|
|
|
|
self.list_widget = QListWidget()
|
|
|
|
profiles = sorted(profiles)
|
|
|
|
self.list_widget.addItems(
|
|
profiles
|
|
)
|
|
|
|
if current in profiles:
|
|
self.list_widget.setCurrentRow(
|
|
profiles.index(current)
|
|
)
|
|
|
|
switch_button = QPushButton(
|
|
"Switch"
|
|
)
|
|
|
|
export_button = QPushButton(
|
|
"Export"
|
|
)
|
|
|
|
import_button = QPushButton(
|
|
"Import"
|
|
)
|
|
|
|
switch_button.clicked.connect(
|
|
self._switch
|
|
)
|
|
|
|
export_button.clicked.connect(
|
|
self._export
|
|
)
|
|
|
|
import_button.clicked.connect(
|
|
self._import
|
|
)
|
|
|
|
buttons = QHBoxLayout()
|
|
buttons.addWidget(switch_button)
|
|
buttons.addWidget(export_button)
|
|
buttons.addWidget(import_button)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.addWidget(self.list_widget)
|
|
layout.addLayout(buttons)
|
|
|
|
def _switch(
|
|
self,
|
|
):
|
|
item = self.list_widget.currentItem()
|
|
|
|
if not item:
|
|
return
|
|
|
|
self.profileSelected.emit(
|
|
item.text()
|
|
)
|
|
|
|
self.accept()
|
|
|
|
def _export(
|
|
self,
|
|
):
|
|
item = (
|
|
self.list_widget.currentItem()
|
|
)
|
|
|
|
if not item:
|
|
return
|
|
|
|
profile_name = (
|
|
item.text()
|
|
)
|
|
|
|
path, _ = (
|
|
QFileDialog.getSaveFileName(
|
|
self,
|
|
"Export Profile",
|
|
f"{profile_name}.profilebundle",
|
|
"*.profilebundle",
|
|
)
|
|
)
|
|
|
|
if not path:
|
|
return
|
|
|
|
profile_root_path = (
|
|
profile_root(
|
|
profile_name
|
|
)
|
|
).resolve()
|
|
|
|
export = {
|
|
"version": 1,
|
|
"profile": profile_name,
|
|
"storage": {},
|
|
}
|
|
|
|
file_count = 0
|
|
total_size = 0
|
|
|
|
for file in (
|
|
profile_root_path.rglob(
|
|
"*"
|
|
)
|
|
):
|
|
if (
|
|
not file.is_file()
|
|
or file.is_symlink()
|
|
):
|
|
continue
|
|
|
|
try:
|
|
resolved = (
|
|
file.resolve()
|
|
)
|
|
|
|
if (
|
|
profile_root_path
|
|
not in resolved.parents
|
|
):
|
|
continue
|
|
|
|
relative = (
|
|
resolved.relative_to(
|
|
profile_root_path
|
|
)
|
|
)
|
|
|
|
if any(
|
|
part
|
|
in self.PROFILE_EXPORT_SKIP
|
|
for part in relative.parts
|
|
):
|
|
continue
|
|
|
|
size = (
|
|
resolved.stat()
|
|
.st_size
|
|
)
|
|
|
|
if (
|
|
size
|
|
> self.PROFILE_MAX_FILE_SIZE
|
|
):
|
|
continue
|
|
|
|
file_count += 1
|
|
total_size += size
|
|
|
|
if (
|
|
file_count
|
|
> self.PROFILE_MAX_FILES
|
|
):
|
|
raise ValueError(
|
|
"profile exceeds file limit"
|
|
)
|
|
|
|
if (
|
|
total_size
|
|
> self.PROFILE_MAX_TOTAL_SIZE
|
|
):
|
|
raise ValueError(
|
|
"profile exceeds size limit"
|
|
)
|
|
|
|
export[
|
|
"storage"
|
|
][
|
|
str(relative)
|
|
] = (
|
|
resolved.read_bytes()
|
|
.hex()
|
|
)
|
|
|
|
except Exception:
|
|
raise
|
|
|
|
Path(path).write_text(
|
|
json.dumps(
|
|
export,
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Export",
|
|
"Profile exported.",
|
|
)
|
|
|
|
def _import(
|
|
self,
|
|
):
|
|
path, _ = (
|
|
QFileDialog.getOpenFileName(
|
|
self,
|
|
"Import Profile",
|
|
"",
|
|
"*.profilebundle",
|
|
)
|
|
)
|
|
|
|
if not path:
|
|
return
|
|
|
|
try:
|
|
bundle = json.loads(
|
|
Path(path).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
|
|
profile_name = str(
|
|
bundle.get(
|
|
"profile",
|
|
"",
|
|
)
|
|
).strip()
|
|
|
|
if not profile_name:
|
|
raise ValueError(
|
|
"invalid profile"
|
|
)
|
|
|
|
root = profile_root(
|
|
profile_name
|
|
)
|
|
|
|
root.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
root_resolved = (
|
|
root.resolve()
|
|
)
|
|
|
|
file_count = 0
|
|
total_size = 0
|
|
|
|
for (
|
|
relative,
|
|
payload,
|
|
) in bundle.get(
|
|
"storage",
|
|
{},
|
|
).items():
|
|
|
|
file_count += 1
|
|
|
|
if (
|
|
file_count
|
|
> self.PROFILE_MAX_FILES
|
|
):
|
|
raise ValueError(
|
|
"profile exceeds file limit"
|
|
)
|
|
|
|
size = (
|
|
len(payload)
|
|
// 2
|
|
)
|
|
|
|
if (
|
|
size
|
|
> self.PROFILE_MAX_FILE_SIZE
|
|
):
|
|
continue
|
|
|
|
total_size += size
|
|
|
|
if (
|
|
total_size
|
|
> self.PROFILE_MAX_TOTAL_SIZE
|
|
):
|
|
raise ValueError(
|
|
"profile exceeds size limit"
|
|
)
|
|
|
|
target = (
|
|
root
|
|
/ relative
|
|
).resolve()
|
|
|
|
if (
|
|
root_resolved
|
|
not in target.parents
|
|
and target
|
|
!= root_resolved
|
|
):
|
|
continue
|
|
|
|
target.parent.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
target.write_bytes(
|
|
bytes.fromhex(
|
|
payload
|
|
)
|
|
)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Import",
|
|
"Profile imported.",
|
|
)
|
|
|
|
except Exception as exc:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Import",
|
|
str(exc),
|
|
)
|
|
class AddonManagerWindow(QDialog):
|
|
def __init__(
|
|
self,
|
|
context: ProfileContext,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.context = context
|
|
|
|
self.manager = AddonManager(
|
|
context.name,
|
|
getattr(
|
|
parent,
|
|
"db",
|
|
None,
|
|
),
|
|
)
|
|
|
|
self.setWindowTitle(
|
|
"Themes & Extensions"
|
|
)
|
|
|
|
self.resize(900, 650)
|
|
|
|
self.model = AddonModel()
|
|
|
|
self.table = QTableView()
|
|
self.table.setModel(self.model)
|
|
|
|
refresh = QPushButton(
|
|
"Refresh"
|
|
)
|
|
|
|
refresh.clicked.connect(
|
|
self.reload
|
|
)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.addWidget(self.table)
|
|
layout.addWidget(refresh)
|
|
|
|
self.reload()
|
|
|
|
|
|
def reload(
|
|
self,
|
|
):
|
|
rows = []
|
|
|
|
for theme in (
|
|
self.manager.themes.discover()
|
|
):
|
|
rows.append(
|
|
{
|
|
"name": theme.stem,
|
|
"enabled": True,
|
|
"type": "Theme",
|
|
}
|
|
)
|
|
|
|
for extension in (
|
|
self.manager.extensions.discover()
|
|
):
|
|
enabled = True
|
|
|
|
manifest = (
|
|
extension
|
|
/ "manifest.json"
|
|
)
|
|
|
|
if manifest.exists():
|
|
try:
|
|
enabled = bool(
|
|
json.loads(
|
|
manifest.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
).get(
|
|
"enabled",
|
|
True,
|
|
)
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
rows.append(
|
|
{
|
|
"name": extension.name,
|
|
"enabled": enabled,
|
|
"type": "Extension",
|
|
}
|
|
)
|
|
|
|
for plugin in (
|
|
self.manager.plugins.inventory()
|
|
):
|
|
rows.append(
|
|
{
|
|
"name": plugin[
|
|
"name"
|
|
],
|
|
"enabled": plugin.get(
|
|
"enabled",
|
|
True,
|
|
),
|
|
"type": "Plugin",
|
|
}
|
|
)
|
|
|
|
self.model.set_addons(
|
|
rows
|
|
)
|
|
|
|
|
|
class ThemeEditor(QDialog):
|
|
def __init__(
|
|
self,
|
|
theme_path: Path,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.theme_path = theme_path
|
|
|
|
self.setWindowTitle(
|
|
theme_path.name
|
|
)
|
|
|
|
self.resize(1000, 700)
|
|
|
|
self.editor = QPlainTextEdit()
|
|
|
|
if theme_path.exists():
|
|
self.editor.setPlainText(
|
|
theme_path.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
|
|
save_button = QPushButton(
|
|
"Save"
|
|
)
|
|
|
|
save_button.clicked.connect(
|
|
self.save
|
|
)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.addWidget(self.editor)
|
|
layout.addWidget(save_button)
|
|
|
|
def save(self):
|
|
self.theme_path.write_text(
|
|
self.editor.toPlainText(),
|
|
encoding="utf-8",
|
|
)
|
|
self.accept()
|
|
|
|
|
|
class ProxyManagerWindow(QDialog):
|
|
def __init__(
|
|
self,
|
|
network,
|
|
profile_name: str,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.network = network
|
|
self.profile_name = profile_name
|
|
|
|
self.setWindowTitle(
|
|
"Proxy Settings"
|
|
)
|
|
|
|
self.resize(500, 350)
|
|
|
|
proxy = (
|
|
network.proxy.get_proxy(
|
|
profile_name
|
|
)
|
|
)
|
|
|
|
self.enabled = QCheckBox()
|
|
self.enabled.setChecked(
|
|
proxy.enabled
|
|
)
|
|
|
|
self.host = QLineEdit(
|
|
proxy.host
|
|
)
|
|
|
|
self.port = QLineEdit(
|
|
str(proxy.port)
|
|
)
|
|
|
|
self.username = QLineEdit(
|
|
proxy.username
|
|
)
|
|
|
|
self.password = QLineEdit(
|
|
proxy.password
|
|
)
|
|
|
|
self.type_box = QLineEdit(
|
|
proxy.proxy_type
|
|
)
|
|
|
|
layout = QFormLayout(self)
|
|
|
|
layout.addRow(
|
|
"Enabled",
|
|
self.enabled,
|
|
)
|
|
|
|
layout.addRow(
|
|
"Type",
|
|
self.type_box,
|
|
)
|
|
|
|
layout.addRow(
|
|
"Host",
|
|
self.host,
|
|
)
|
|
|
|
layout.addRow(
|
|
"Port",
|
|
self.port,
|
|
)
|
|
|
|
layout.addRow(
|
|
"Username",
|
|
self.username,
|
|
)
|
|
|
|
layout.addRow(
|
|
"Password",
|
|
self.password,
|
|
)
|
|
|
|
save_button = QPushButton(
|
|
"Save"
|
|
)
|
|
|
|
save_button.clicked.connect(
|
|
self.save
|
|
)
|
|
|
|
layout.addRow(save_button)
|
|
|
|
def save(self):
|
|
try:
|
|
port = int(
|
|
self.port.text().strip()
|
|
or "0"
|
|
)
|
|
except ValueError:
|
|
port = 0
|
|
|
|
proxy = ProxyConfig(
|
|
enabled=self.enabled.isChecked(),
|
|
proxy_type=self.type_box.text().strip() or "http",
|
|
host=self.host.text().strip(),
|
|
port=port,
|
|
username=self.username.text(),
|
|
password=self.password.text(),
|
|
)
|
|
|
|
self.network.proxy.save_proxy(
|
|
self.profile_name,
|
|
proxy,
|
|
)
|
|
|
|
self.accept()
|
|
|
|
|
|
class NetworkInspector(QMainWindow):
|
|
def __init__(
|
|
self,
|
|
network,
|
|
parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.network = network
|
|
|
|
self.setWindowTitle(
|
|
"Network Inspector"
|
|
)
|
|
|
|
self.resize(1400, 850)
|
|
|
|
self.model = NetworkTableModel()
|
|
|
|
self.table = QTableView()
|
|
self.table.setModel(self.model)
|
|
|
|
self.table.horizontalHeader().setStretchLastSection(
|
|
True
|
|
)
|
|
|
|
self.search = QLineEdit()
|
|
self.search.setPlaceholderText(
|
|
"Search URL, host, method..."
|
|
)
|
|
|
|
self.search.textChanged.connect(
|
|
self.reload
|
|
)
|
|
|
|
self.details = QPlainTextEdit()
|
|
self.details.setReadOnly(True)
|
|
|
|
splitter = QSplitter(
|
|
Qt.Orientation.Vertical
|
|
)
|
|
|
|
splitter.addWidget(
|
|
self.table
|
|
)
|
|
|
|
splitter.addWidget(
|
|
self.details
|
|
)
|
|
|
|
container = QWidget()
|
|
|
|
layout = QVBoxLayout(
|
|
container
|
|
)
|
|
|
|
layout.addWidget(
|
|
self.search
|
|
)
|
|
|
|
layout.addWidget(
|
|
splitter
|
|
)
|
|
|
|
self.setCentralWidget(
|
|
container
|
|
)
|
|
|
|
self.table.clicked.connect(
|
|
self.show_details
|
|
)
|
|
|
|
self.reload()
|
|
|
|
def reload(self):
|
|
query = (
|
|
self.search.text().strip()
|
|
)
|
|
|
|
rows = (
|
|
self.network.search(query)
|
|
if query
|
|
else self.network.recent()
|
|
)
|
|
|
|
self.model.set_rows(rows)
|
|
|
|
def show_details(
|
|
self,
|
|
index,
|
|
):
|
|
try:
|
|
row = self.model.row(
|
|
index.row()
|
|
)
|
|
|
|
self.details.setPlainText(
|
|
json.dumps(
|
|
dict(row),
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
|
|
except Exception:
|
|
self.details.clear()
|
|
|
|
|
|
# v2
|
|
class TrayManager(QObject):
|
|
def __init__(
|
|
self,
|
|
parent,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
self.parent_window = parent
|
|
|
|
self.tray = QSystemTrayIcon(
|
|
parent
|
|
)
|
|
|
|
icon = (
|
|
QApplication.windowIcon()
|
|
)
|
|
|
|
if icon.isNull():
|
|
icon = (
|
|
self.parent_window.windowIcon()
|
|
)
|
|
|
|
self.tray.setIcon(
|
|
icon
|
|
)
|
|
|
|
self.menu = QMenu()
|
|
|
|
self.open_action = QAction(
|
|
"Open",
|
|
parent,
|
|
)
|
|
|
|
self.hide_action = QAction(
|
|
"Hide",
|
|
parent,
|
|
)
|
|
|
|
self.quit_action = QAction(
|
|
"Quit",
|
|
parent,
|
|
)
|
|
|
|
self.menu.addAction(
|
|
self.open_action
|
|
)
|
|
|
|
self.menu.addAction(
|
|
self.hide_action
|
|
)
|
|
|
|
self.menu.addSeparator()
|
|
|
|
self.menu.addAction(
|
|
self.quit_action
|
|
)
|
|
|
|
self.tray.setContextMenu(
|
|
self.menu
|
|
)
|
|
|
|
self.open_action.triggered.connect(
|
|
self.show_window
|
|
)
|
|
|
|
self.hide_action.triggered.connect(
|
|
parent.hide
|
|
)
|
|
|
|
self.quit_action.triggered.connect(
|
|
self.quit_application
|
|
)
|
|
|
|
self.tray.activated.connect(
|
|
self._activated
|
|
)
|
|
|
|
def _activated(
|
|
self,
|
|
reason,
|
|
):
|
|
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
self.show_window()
|
|
|
|
def show_window(self):
|
|
self.parent_window.showNormal()
|
|
self.parent_window.raise_()
|
|
self.parent_window.activateWindow()
|
|
|
|
def quit_application(self):
|
|
self.tray.hide()
|
|
self.parent_window.tray_manager = None
|
|
self.parent_window.settings_data.setdefault(
|
|
"tray",
|
|
{}
|
|
)["close_to_tray"] = False
|
|
self.parent_window.close() |