forked from Support/sp-hydra-veil-gui
Compare commits
1 commit
john-botto
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd77439a24 |
33 changed files with 13552 additions and 2197 deletions
17
.gitignore
vendored
17
.gitignore
vendored
|
|
@ -1,18 +1,5 @@
|
||||||
.dev
|
may23.py
|
||||||
|
old_main.py
|
||||||
.idea
|
.idea
|
||||||
.venv
|
.venv
|
||||||
dist
|
dist
|
||||||
gui/__pycache__/
|
|
||||||
gui/__pycache__
|
|
||||||
__pycache__
|
|
||||||
*.pyc
|
|
||||||
__pycache__/
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
*.egg-info/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
venv/
|
|
||||||
.env
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
This is the second version with isolated UI elements from action functions.
|
|
||||||
|
|
||||||
# sp-hydra-veil-gui
|
# sp-hydra-veil-gui
|
||||||
|
|
||||||
The `sp-hydra-veil-gui` graphical user interface implements the `sp-hydra-veil-core` library.
|
The `sp-hydra-veil-gui` graphical user interface implements the `sp-hydra-veil-core` library.
|
||||||
|
|
|
||||||
841
gui/__main__.py
841
gui/__main__.py
|
|
@ -1,132 +1,763 @@
|
||||||
from core.errors.logger import logger
|
|
||||||
from core.models.manage.session_management import init_session, get_session, close_session, create_ALL_tables, create_ONLY_db_version_table, get_path, does_it_exist, does_db_version_table_exist
|
|
||||||
from core.models.manage.version_check import check_database_compatibility, insert_new_version, get_custom_message
|
|
||||||
from core.Constants import Constants
|
|
||||||
# generic
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# ============================================================================
|
_WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
# DISPLAY CHOICES AND RECOVERY UI
|
if _WORKSPACE_ROOT not in sys.path:
|
||||||
# ============================================================================
|
sys.path.insert(0, _WORKSPACE_ROOT)
|
||||||
def recovery_dialog(custom_error, db_version_you_have):
|
|
||||||
"""This ends the app by forcing them to delete the database, move it, or just quit."""
|
|
||||||
|
|
||||||
from gui.v2.ui.popups.Database_version import show_recovery_dialog
|
import logging
|
||||||
choice = show_recovery_dialog({"message": custom_error, "db_version": db_version_you_have, "status": "version_mismatch"})
|
import traceback
|
||||||
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
|
|
||||||
|
|
||||||
if choice == 1:
|
from PyQt6.QtWidgets import (
|
||||||
if os.path.exists(database_path):
|
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
|
||||||
os.remove(database_path)
|
QDialog, QVBoxLayout, QHBoxLayout
|
||||||
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}")
|
)
|
||||||
logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..")
|
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
|
||||||
sys.exit()
|
from PyQt6 import QtGui
|
||||||
|
from PyQt6.QtCore import Qt, QTimer, QEvent
|
||||||
|
|
||||||
elif choice == 2:
|
from core.Constants import Constants
|
||||||
logger.info(f"[DB MANAGEMENT] User opted to move the DB file.")
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
from v2.ui.popups.pick_folder_to_move import launch_file_picker
|
from core.Errors import UnknownConnectionTypeError
|
||||||
launch_file_picker(database_path)
|
from core.errors.logger import logger as core_logger
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
|
core_logger.propagate = False
|
||||||
|
|
||||||
|
from gui.v2.infrastructure.navigator import Navigator
|
||||||
|
from gui.v2.infrastructure.setup_observers import setup_observers
|
||||||
|
from gui.v2.infrastructure.connection_manager import ConnectionManager
|
||||||
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
from gui.v2.ui.builders.bottom_section import create_bottom_section
|
||||||
|
from gui.v2.ui.builders.top_section import create_top_section
|
||||||
|
from gui.v2.ui.builders.system_tray import init_system_tray
|
||||||
|
from gui.v2.actions.config import (
|
||||||
|
setup_gui_directory,
|
||||||
|
load_gui_config,
|
||||||
|
save_gui_config,
|
||||||
|
default_gui_config,
|
||||||
|
)
|
||||||
|
from gui.v2.actions.logging_setup import (
|
||||||
|
is_logging_enabled,
|
||||||
|
setup_gui_logging,
|
||||||
|
teardown_gui_logging,
|
||||||
|
)
|
||||||
|
from gui.v2.actions.flags import (
|
||||||
|
has_shown_systemwide_prompt,
|
||||||
|
mark_systemwide_prompt_shown,
|
||||||
|
has_shown_fast_mode_prompt,
|
||||||
|
mark_fast_mode_prompt_shown,
|
||||||
|
set_fast_mode_enabled,
|
||||||
|
check_first_launch,
|
||||||
|
)
|
||||||
|
from gui.v2.actions.profile_data import (
|
||||||
|
read_profile_data,
|
||||||
|
write_profile_data,
|
||||||
|
clear_profile_data,
|
||||||
|
)
|
||||||
|
from gui.v2.actions.ticket_failure import (
|
||||||
|
save_ticket_verification_failure,
|
||||||
|
get_ticket_verification_failure,
|
||||||
|
clear_ticket_verification_failure,
|
||||||
|
)
|
||||||
|
from gui.v2.actions.should_be_synchronized import should_be_synchronized
|
||||||
|
|
||||||
|
|
||||||
|
class CustomWindow(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
sys.excepthook = self._handle_exception
|
||||||
|
self.setWindowFlags(Qt.WindowType.Window)
|
||||||
|
|
||||||
|
gui_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
font_path = os.path.join(gui_dir, 'resources', 'fonts')
|
||||||
|
|
||||||
|
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
|
||||||
|
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
|
||||||
|
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||||
|
app_font = QFont(font_family)
|
||||||
|
app_font.setPointSize(10)
|
||||||
|
QApplication.setFont(app_font)
|
||||||
|
|
||||||
|
self.open_sans_family = None
|
||||||
|
open_sans_path = os.path.join(font_path, 'open-sans.ttf')
|
||||||
|
if os.path.exists(open_sans_path):
|
||||||
|
open_sans_id = QFontDatabase.addApplicationFont(open_sans_path)
|
||||||
|
if open_sans_id != -1:
|
||||||
|
self.open_sans_family = QFontDatabase.applicationFontFamilies(open_sans_id)[0]
|
||||||
|
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
||||||
|
self.setWindowTitle('HydraVeil')
|
||||||
|
|
||||||
|
screen = QApplication.primaryScreen()
|
||||||
|
ratio = screen.devicePixelRatio()
|
||||||
|
self.host_screen_width = int(screen.size().width() * ratio)
|
||||||
|
self.host_screen_height = int(screen.size().height() * ratio)
|
||||||
|
|
||||||
|
self._data = {"Profile_1": {}}
|
||||||
|
|
||||||
|
self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui')
|
||||||
|
self.gui_config_file = os.path.join(self.gui_config_home, 'config.json')
|
||||||
|
self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui')
|
||||||
|
self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log')
|
||||||
|
self._gui_log_handlers = []
|
||||||
|
|
||||||
|
setup_gui_directory(
|
||||||
|
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
||||||
|
|
||||||
|
self.btn_path = os.getenv('BTN_PATH', os.path.join(
|
||||||
|
gui_dir, 'resources', 'images'))
|
||||||
|
self.css_path = os.getenv('CSS_PATH', os.path.join(
|
||||||
|
gui_dir, 'resources', 'styles'))
|
||||||
|
|
||||||
|
self.is_downloading = False
|
||||||
|
self.current_profile_id = None
|
||||||
|
self.connection_manager = ConnectionManager()
|
||||||
|
|
||||||
|
current_connection = None
|
||||||
|
self.is_tor_mode = current_connection == 'tor'
|
||||||
|
|
||||||
|
self.is_animating = False
|
||||||
|
self.animation_step = 0
|
||||||
|
self.page_history = []
|
||||||
|
self.log_path = None
|
||||||
|
|
||||||
|
self.setFixedSize(800, 570)
|
||||||
|
|
||||||
|
top = create_top_section(self, self.css_path)
|
||||||
|
self.__dict__.update(top)
|
||||||
|
|
||||||
|
self.marquee_text = ""
|
||||||
|
self.marquee_position = 0
|
||||||
|
self.marquee_enabled = False
|
||||||
|
self.scroll_speed = 1
|
||||||
|
|
||||||
|
self.page_stack = QStackedWidget(self)
|
||||||
|
self.page_stack.setGeometry(0, 0, 800, 570)
|
||||||
|
|
||||||
|
bottom = create_bottom_section(
|
||||||
|
self,
|
||||||
|
self.btn_path,
|
||||||
|
Constants.HV_CLIENT_VERSION_NUMBER,
|
||||||
|
should_be_synchronized(),
|
||||||
|
)
|
||||||
|
self.__dict__.update(bottom)
|
||||||
|
self.status_label = bottom['status_label']
|
||||||
|
self.tor_text = bottom['tor_text']
|
||||||
|
self.toggle_button = bottom['toggle_button']
|
||||||
|
self.tor_icon = bottom['tor_icon']
|
||||||
|
self.sync_button = bottom['sync_button']
|
||||||
|
self.page_stack.raise_()
|
||||||
|
|
||||||
|
self.observers = setup_observers(self.update_status)
|
||||||
|
|
||||||
|
self.update_image()
|
||||||
|
self.set_scroll_speed(7)
|
||||||
|
|
||||||
|
self.toggle_button.clicked.connect(self.toggle_mode)
|
||||||
|
self.sync_button.clicked.connect(self.sync)
|
||||||
|
QApplication.instance().installEventFilter(self)
|
||||||
|
|
||||||
|
self.create_toggle(self.is_tor_mode)
|
||||||
|
self.animation_timer = QTimer()
|
||||||
|
self.animation_timer.timeout.connect(self.animate_toggle)
|
||||||
|
self.check_logging()
|
||||||
|
|
||||||
|
self.navigator = Navigator(self.page_stack, self)
|
||||||
|
self.page_stack.currentChanged.connect(self.page_changed)
|
||||||
|
|
||||||
|
self.show()
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
self.tray = None
|
||||||
|
self.tray = init_system_tray(self, self.btn_path)
|
||||||
|
|
||||||
|
current_connection = self.get_current_connection()
|
||||||
|
self.is_tor_mode = current_connection == 'tor'
|
||||||
|
self.set_toggle_state(self.is_tor_mode)
|
||||||
|
|
||||||
|
core_logger.info(
|
||||||
|
f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, "
|
||||||
|
f"connection={current_connection}, tor_mode={self.is_tor_mode})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
if self.check_first_launch():
|
||||||
|
self.navigator.navigate("welcome")
|
||||||
else:
|
else:
|
||||||
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
|
self.navigator.navigate("menu")
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# ============================================================================
|
def perform_update_check(self):
|
||||||
# INITIALIZE DATABASE
|
from core.controllers.ClientController import ClientController
|
||||||
# ============================================================================
|
update_available = ClientController.can_be_updated()
|
||||||
# Does the database exist?
|
|
||||||
system_path = get_path()
|
|
||||||
database_path = system_path / "storage.db"
|
|
||||||
main_db_exists = does_it_exist(database_path)
|
|
||||||
|
|
||||||
# Setup operations on the main DB which create it
|
if update_available:
|
||||||
init_session() # (engine, Session, _session all initialized from session_management)
|
menu_page = self.navigator.get_cached("menu")
|
||||||
session = get_session()
|
if menu_page is not None:
|
||||||
|
menu_page.on_update_check_finished()
|
||||||
|
|
||||||
# does the version checker table exist?
|
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||||
version_table_exists = does_db_version_table_exist()
|
if not status:
|
||||||
|
self.update_status('Sync failed. Please try again later.')
|
||||||
|
return
|
||||||
|
|
||||||
# are they upgrading from a legacy version?
|
from PyQt6.QtWidgets import QPushButton
|
||||||
# that would mean the table existed, but not the version table,
|
from gui.v2.actions.sync import generate_grid_positions
|
||||||
if not version_table_exists and main_db_exists:
|
|
||||||
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
|
|
||||||
|
|
||||||
# shut down db connection
|
self.connection_manager.set_synced(True)
|
||||||
close_session()
|
self.connection_manager.store_locations(locations)
|
||||||
|
self.connection_manager.store_browsers(available_browsers)
|
||||||
|
|
||||||
# THEN DISPLAY CHOICES AND RECOVERY UI
|
location_positions = generate_grid_positions(len(available_locations))
|
||||||
custom_error = "You're using a Legacy version of the Database scheme. Please delete it and fetch the new data."
|
browser_positions = generate_grid_positions(len(available_browsers))
|
||||||
db_version_you_have = "Old System"
|
available_locations_list = [
|
||||||
recovery_dialog(custom_error, db_version_you_have)
|
(QPushButton, loc, location_positions[i])
|
||||||
sys.exit()
|
for i, loc in enumerate(available_locations)
|
||||||
|
]
|
||||||
|
available_browsers_list = [
|
||||||
|
(QPushButton, brw, browser_positions[i])
|
||||||
|
for i, brw in enumerate(available_browsers)
|
||||||
|
]
|
||||||
|
|
||||||
# If they're still here, then if it's NOT a legacy version,
|
browser_page = self.navigator.get_cached("browser")
|
||||||
|
if browser_page is not None:
|
||||||
|
browser_page.create_interface_elements(available_browsers_list)
|
||||||
|
|
||||||
# and the new version table doesn't exist, then create it:
|
location_page = self.navigator.get_cached("location")
|
||||||
if not version_table_exists:
|
if location_page is not None:
|
||||||
create_ONLY_db_version_table()
|
location_page.create_interface_elements(available_locations_list)
|
||||||
|
|
||||||
# ============================================================================
|
hidetor_page = self.navigator.get_cached("hidetor")
|
||||||
# COMPARE DB VERISONS
|
if hidetor_page is not None:
|
||||||
# ============================================================================
|
hidetor_page.create_interface_elements(available_locations_list)
|
||||||
compatability_dict = check_database_compatibility(session)
|
|
||||||
reason = compatability_dict.get("reason", "error")
|
|
||||||
is_compatable = compatability_dict.get("result", False)
|
|
||||||
logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}")
|
|
||||||
|
|
||||||
# ============================================================================
|
protocol_page = self.navigator.get_cached("protocol")
|
||||||
# IF THE VERSIONS MATCH
|
if protocol_page is not None:
|
||||||
# ============================================================================
|
protocol_page.enable_protocol_buttons()
|
||||||
if is_compatable:
|
|
||||||
made_tables = create_ALL_tables()
|
|
||||||
|
|
||||||
# SCREEN FAILED TABLES
|
menu_page = self.navigator.get_cached("menu")
|
||||||
if not made_tables:
|
if menu_page is not None:
|
||||||
custom_error = "Critical Failure with starting the models of the database."
|
if hasattr(menu_page, 'refresh_profiles_data'):
|
||||||
logger.error(f"[DB MANAGEMENT] {custom_error}")
|
menu_page.refresh_profiles_data()
|
||||||
close_session()
|
if hasattr(menu_page, 'buttons'):
|
||||||
from v2.ui.popups.Database_version import show_recovery_dialog
|
for button in menu_page.buttons:
|
||||||
choice = show_recovery_dialog({"message": custom_error, "db_version": 0, "status": "cant_make"})
|
parent = button.parent()
|
||||||
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
|
if parent:
|
||||||
sys.exit()
|
verification_icons = parent.findChildren(QPushButton)
|
||||||
|
for icon in verification_icons:
|
||||||
|
if icon.geometry().width() == 20 and icon.geometry().height() == 20:
|
||||||
|
icon.setEnabled(True)
|
||||||
|
|
||||||
# ASSUME TABLES CREATED
|
def sync(self):
|
||||||
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing")
|
core_logger.info("User clicked sync button")
|
||||||
|
if ConfigurationController.get_connection() == 'tor':
|
||||||
# UPDATE DATA
|
self.worker_thread = WorkerThread('SYNC_TOR')
|
||||||
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
|
|
||||||
if not did_it_insert:
|
|
||||||
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
|
|
||||||
|
|
||||||
# Load GUI either way:
|
|
||||||
from gui.main_ui import start_ui
|
|
||||||
|
|
||||||
# If the user lacks a database, we want to force them to sync the new data, to avoid NoneType errors when enabling existing profiles,
|
|
||||||
if reason == "no_database":
|
|
||||||
logger.info(f"[DB MANAGEMENT] User had no database to begin with, so now we're entering GUI with sync on..")
|
|
||||||
force_sync = True
|
|
||||||
else:
|
else:
|
||||||
logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..")
|
self.worker_thread = WorkerThread('SYNC')
|
||||||
force_sync = False
|
self.sync_button.setIcon(QtGui.QIcon(
|
||||||
|
os.path.join(self.btn_path, 'icon_sync.png')))
|
||||||
|
self.worker_thread.finished.connect(
|
||||||
|
lambda: self.perform_update_check())
|
||||||
|
self.worker_thread.sync_output.connect(self.update_values)
|
||||||
|
self.worker_thread.start()
|
||||||
|
|
||||||
# Start GUI either way:
|
def _handle_exception(self, identifier, message, trace):
|
||||||
start_ui(force_sync)
|
if issubclass(identifier, UnknownConnectionTypeError):
|
||||||
|
self.setup_popup()
|
||||||
# ============================================================================
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
# but IF the versions do NOT match
|
|
||||||
# ============================================================================
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
|
config = self._load_gui_config()
|
||||||
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
|
if config and config["logging"]["gui_logging_enabled"] == True:
|
||||||
|
logging.error(
|
||||||
|
f"Uncaught exception:\n"
|
||||||
|
f"Type: {identifier.__name__}\n"
|
||||||
|
f"Value: {str(message)}\n"
|
||||||
|
f"Traceback:\n{''.join(traceback.format_tb(trace))}"
|
||||||
|
)
|
||||||
|
sys.__excepthook__(identifier, message, trace)
|
||||||
|
|
||||||
# WHAT IS THE REASON?
|
def get_current_connection(self):
|
||||||
custom_error = get_custom_message(reason, compatability_dict)
|
return ConfigurationController.get_connection()
|
||||||
|
|
||||||
# shut down db connection
|
def setup_popup(self):
|
||||||
close_session()
|
connection_dialog = QDialog(self)
|
||||||
|
connection_dialog.setWindowTitle("Connection Type")
|
||||||
|
connection_dialog.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint)
|
||||||
|
connection_dialog.setModal(True)
|
||||||
|
connection_dialog.setFixedSize(400, 200)
|
||||||
|
|
||||||
# THEN DISPLAY CHOICES AND RECOVERY UI
|
layout = QVBoxLayout(connection_dialog)
|
||||||
recovery_dialog(custom_error, db_version_you_have)
|
|
||||||
|
|
||||||
|
label = QLabel(
|
||||||
|
"Please set your preference for connecting to Simplified Privacy's billing server.")
|
||||||
|
label.setWordWrap(True)
|
||||||
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
label.setStyleSheet(
|
||||||
|
"font-size: 12px; margin-bottom: 20px; color: black;")
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
regular_btn = QPushButton("Regular")
|
||||||
|
tor_btn = QPushButton("Tor")
|
||||||
|
|
||||||
|
button_style = """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #3b3b3b;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
regular_btn.setStyleSheet(button_style)
|
||||||
|
tor_btn.setStyleSheet(button_style)
|
||||||
|
|
||||||
|
button_layout.addWidget(regular_btn)
|
||||||
|
button_layout.addWidget(tor_btn)
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
def set_regular_connection():
|
||||||
|
ConfigurationController.set_connection('system')
|
||||||
|
self.is_tor_mode = False
|
||||||
|
self.set_toggle_state(False)
|
||||||
|
core_logger.info("User selected 'Regular' connection type from popup")
|
||||||
|
connection_dialog.accept()
|
||||||
|
|
||||||
|
def set_tor_connection():
|
||||||
|
ConfigurationController.set_connection('tor')
|
||||||
|
self.is_tor_mode = True
|
||||||
|
self.set_toggle_state(True)
|
||||||
|
core_logger.info("User selected 'Tor' connection type from popup")
|
||||||
|
connection_dialog.accept()
|
||||||
|
|
||||||
|
regular_btn.clicked.connect(set_regular_connection)
|
||||||
|
tor_btn.clicked.connect(set_tor_connection)
|
||||||
|
|
||||||
|
connection_dialog.exec()
|
||||||
|
|
||||||
|
return ConfigurationController.get_connection()
|
||||||
|
|
||||||
|
def _setup_gui_directory(self):
|
||||||
|
setup_gui_directory(
|
||||||
|
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
||||||
|
|
||||||
|
def _load_gui_config(self):
|
||||||
|
return load_gui_config(self.gui_config_file)
|
||||||
|
|
||||||
|
def _save_gui_config(self, config):
|
||||||
|
save_gui_config(self.gui_config_file, config)
|
||||||
|
|
||||||
|
def _default_gui_config(self):
|
||||||
|
return default_gui_config()
|
||||||
|
|
||||||
|
def save_ticket_verification_failure(self, result):
|
||||||
|
return save_ticket_verification_failure(self.gui_config_file, result)
|
||||||
|
|
||||||
|
def get_ticket_verification_failure(self):
|
||||||
|
return get_ticket_verification_failure(self.gui_config_file)
|
||||||
|
|
||||||
|
def clear_ticket_verification_failure(self):
|
||||||
|
clear_ticket_verification_failure(self.gui_config_file)
|
||||||
|
|
||||||
|
def check_logging(self):
|
||||||
|
config = self._load_gui_config()
|
||||||
|
if is_logging_enabled(config):
|
||||||
|
self._setup_gui_logging()
|
||||||
|
|
||||||
|
def has_shown_systemwide_prompt(self):
|
||||||
|
return has_shown_systemwide_prompt(self.gui_config_file)
|
||||||
|
|
||||||
|
def mark_systemwide_prompt_shown(self):
|
||||||
|
mark_systemwide_prompt_shown(self.gui_config_file)
|
||||||
|
|
||||||
|
def stop_gui_logging(self):
|
||||||
|
teardown_gui_logging(self.gui_log_file, self._gui_log_handlers)
|
||||||
|
self._gui_log_handlers = []
|
||||||
|
|
||||||
|
config = self._load_gui_config()
|
||||||
|
if config:
|
||||||
|
config["logging"]["gui_logging_enabled"] = False
|
||||||
|
self._save_gui_config(config)
|
||||||
|
|
||||||
|
def _setup_gui_logging(self):
|
||||||
|
self._gui_log_handlers = setup_gui_logging(
|
||||||
|
self.gui_cache_home, self.gui_log_file, self._gui_log_handlers)
|
||||||
|
|
||||||
|
def set_tor_icon(self):
|
||||||
|
if self.is_tor_mode:
|
||||||
|
icon_path = os.path.join(self.btn_path, "toricon_mini.png")
|
||||||
|
else:
|
||||||
|
icon_path = os.path.join(self.btn_path, "toricon_mini_false.png")
|
||||||
|
pixmap = QPixmap(icon_path)
|
||||||
|
self.tor_icon.setPixmap(pixmap)
|
||||||
|
self.tor_icon.setScaledContents(True)
|
||||||
|
|
||||||
|
def create_toggle(self, is_tor_mode):
|
||||||
|
handle_x = 30 if is_tor_mode else 3
|
||||||
|
colors = self._get_toggle_colors(is_tor_mode)
|
||||||
|
|
||||||
|
if not hasattr(self, 'animation_timer'):
|
||||||
|
self.animation_timer = QTimer()
|
||||||
|
self.animation_timer.timeout.connect(self.animate_toggle)
|
||||||
|
|
||||||
|
self.toggle_bg = QLabel(self.toggle_button)
|
||||||
|
self.toggle_bg.setGeometry(0, 0, 50, 22)
|
||||||
|
|
||||||
|
self.toggle_handle = QLabel(self.toggle_button)
|
||||||
|
self.toggle_handle.setGeometry(handle_x, 3, 16, 16)
|
||||||
|
self.toggle_handle.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.on_label = QLabel("ON", self.toggle_button)
|
||||||
|
self.on_label.setGeometry(8, 4, 15, 14)
|
||||||
|
|
||||||
|
self.off_label = QLabel("OFF", self.toggle_button)
|
||||||
|
self.off_label.setGeometry(25, 4, 40, 14)
|
||||||
|
|
||||||
|
for widget in (self.toggle_bg, self.toggle_handle, self.on_label, self.off_label):
|
||||||
|
widget.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||||
|
|
||||||
|
self._update_toggle_colors(colors)
|
||||||
|
if is_tor_mode:
|
||||||
|
self.set_tor_icon()
|
||||||
|
|
||||||
|
self.raise_bottom_controls()
|
||||||
|
|
||||||
|
def _get_toggle_colors(self, is_tor_mode):
|
||||||
|
return {
|
||||||
|
'bg': "#00a8ff" if is_tor_mode else "#2c3e50",
|
||||||
|
'tor': "white" if is_tor_mode else "transparent",
|
||||||
|
'sys': "transparent" if is_tor_mode else "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_toggle_colors(self, colors):
|
||||||
|
self.toggle_bg.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
background-color: {colors['bg']};
|
||||||
|
border-radius: 11px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.on_label.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
color: {colors['tor']};
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.off_label.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
color: {colors['sys']};
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def set_toggle_state(self, is_tor_mode):
|
||||||
|
self.is_tor_mode = is_tor_mode
|
||||||
|
self.toggle_button.setChecked(is_tor_mode)
|
||||||
|
handle_x = 30 if is_tor_mode else 3
|
||||||
|
self.toggle_handle.setGeometry(handle_x, 3, 16, 16)
|
||||||
|
self._update_toggle_colors(self._get_toggle_colors(is_tor_mode))
|
||||||
|
self.set_tor_icon()
|
||||||
|
|
||||||
|
def toggle_mode(self, *_args):
|
||||||
|
if self.is_animating and not self.animation_timer.isActive():
|
||||||
|
self.is_animating = False
|
||||||
|
|
||||||
|
if not self.is_animating:
|
||||||
|
target_is_tor = not self.is_tor_mode
|
||||||
|
new_connection = 'tor' if target_is_tor else 'system'
|
||||||
|
core_logger.info(f"Tor toggle clicked; switching connection mode to '{new_connection}'")
|
||||||
|
self.update_status(f"Tor toggle clicked: switching to {new_connection}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ConfigurationController.set_connection(new_connection)
|
||||||
|
except Exception as error:
|
||||||
|
core_logger.exception(
|
||||||
|
f"Tor toggle failed while switching connection mode to '{new_connection}'")
|
||||||
|
self.update_status(f"Tor toggle failed: {error}")
|
||||||
|
self.set_toggle_state(self.is_tor_mode)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.set_toggle_state(target_is_tor)
|
||||||
|
self.is_animating = True
|
||||||
|
self.animation_step = 0
|
||||||
|
self.animation_timer.start(16)
|
||||||
|
return
|
||||||
|
|
||||||
|
core_logger.info("Tor toggle clicked while animation was still running")
|
||||||
|
self.update_status("Tor toggle clicked: still switching")
|
||||||
|
|
||||||
|
def raise_bottom_controls(self):
|
||||||
|
for widget in (self.status_label, self.tor_text, self.toggle_button, self.tor_icon, self.sync_button):
|
||||||
|
widget.raise_()
|
||||||
|
|
||||||
|
def eventFilter(self, source, event):
|
||||||
|
if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
if self._is_toggle_event(event):
|
||||||
|
self.toggle_mode()
|
||||||
|
return True
|
||||||
|
return super().eventFilter(source, event)
|
||||||
|
|
||||||
|
def _is_toggle_event(self, event):
|
||||||
|
if not hasattr(self, 'toggle_button') or not self.toggle_button.isVisible():
|
||||||
|
return False
|
||||||
|
if hasattr(event, 'globalPosition'):
|
||||||
|
global_pos = event.globalPosition().toPoint()
|
||||||
|
else:
|
||||||
|
global_pos = event.globalPos()
|
||||||
|
for widget in (self.tor_text, self.toggle_button, self.tor_icon):
|
||||||
|
if widget.isVisible() and widget.rect().contains(widget.mapFromGlobal(global_pos)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def animate_toggle(self):
|
||||||
|
TOTAL_STEPS = 5
|
||||||
|
if self.animation_step <= TOTAL_STEPS:
|
||||||
|
progress = self.animation_step / TOTAL_STEPS
|
||||||
|
|
||||||
|
if self.is_tor_mode:
|
||||||
|
new_x = 20 + (10 * progress)
|
||||||
|
start_color = "#2c3e50"
|
||||||
|
end_color = "#00a8ff"
|
||||||
|
else:
|
||||||
|
new_x = 31 - (28 * progress)
|
||||||
|
start_color = "#00a8ff"
|
||||||
|
end_color = "#2c3e50"
|
||||||
|
|
||||||
|
self.toggle_handle.setGeometry(int(new_x), 3, 16, 16)
|
||||||
|
|
||||||
|
bg_color = self.interpolate_color(start_color, end_color, progress)
|
||||||
|
self.toggle_bg.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
background-color: {bg_color};
|
||||||
|
border-radius: 11px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.off_label.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
color: rgba(255, 255, 255, {1 - progress if self.is_tor_mode else progress});
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
self.on_label.setStyleSheet(f"""
|
||||||
|
QLabel {{
|
||||||
|
color: rgba(255, 255, 255, {progress if self.is_tor_mode else 1 - progress});
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: bold;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.animation_step += 1
|
||||||
|
else:
|
||||||
|
self.animation_timer.stop()
|
||||||
|
self.is_animating = False
|
||||||
|
self.set_tor_icon()
|
||||||
|
|
||||||
|
def interpolate_color(self, start_color, end_color, progress):
|
||||||
|
def hex_to_rgb(hex_color):
|
||||||
|
hex_color = hex_color.lstrip('#')
|
||||||
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
start_rgb = hex_to_rgb(start_color)
|
||||||
|
end_rgb = hex_to_rgb(end_color)
|
||||||
|
|
||||||
|
current_rgb = tuple(
|
||||||
|
int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * progress)
|
||||||
|
for i in range(3)
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"rgb{current_rgb}"
|
||||||
|
|
||||||
|
def closeEvent(self, event=None):
|
||||||
|
core_logger.info("HydraVeil application closing")
|
||||||
|
connected_profiles = self.connection_manager.get_connected_profiles()
|
||||||
|
if connected_profiles:
|
||||||
|
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
|
||||||
|
if event is not None:
|
||||||
|
event.accept()
|
||||||
|
else:
|
||||||
|
if event is not None:
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def update_image(self, appearance_value="original"):
|
||||||
|
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
|
||||||
|
pixmap = QPixmap(image_path)
|
||||||
|
self.label.setPixmap(pixmap)
|
||||||
|
self.label.setScaledContents(True)
|
||||||
|
|
||||||
|
def read_data(self):
|
||||||
|
return read_profile_data(self._data)
|
||||||
|
|
||||||
|
def write_data(self, data):
|
||||||
|
write_profile_data(self._data, data)
|
||||||
|
|
||||||
|
def clear_data(self):
|
||||||
|
self._data = clear_profile_data()
|
||||||
|
|
||||||
|
def _set_status_font_size(self, font_size):
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
|
||||||
|
|
||||||
|
def update_status(self, text, clear=False):
|
||||||
|
if text is None:
|
||||||
|
self._set_status_font_size(16)
|
||||||
|
self.status_label.setText('Status:')
|
||||||
|
self.disable_marquee()
|
||||||
|
return
|
||||||
|
|
||||||
|
if clear:
|
||||||
|
self._set_status_font_size(16)
|
||||||
|
self.status_label.setText('')
|
||||||
|
return
|
||||||
|
|
||||||
|
full_text = 'Status: ' + text
|
||||||
|
metrics = self.status_label.fontMetrics()
|
||||||
|
available_width = self.status_label.width() - 10
|
||||||
|
|
||||||
|
if metrics.horizontalAdvance(full_text) > available_width:
|
||||||
|
self.enable_marquee(text)
|
||||||
|
else:
|
||||||
|
self.status_label.setText(full_text)
|
||||||
|
self.disable_marquee()
|
||||||
|
|
||||||
|
def check_first_launch(self):
|
||||||
|
return check_first_launch(self.gui_config_file)
|
||||||
|
|
||||||
|
def has_shown_fast_mode_prompt(self):
|
||||||
|
return has_shown_fast_mode_prompt(self.gui_config_file)
|
||||||
|
|
||||||
|
def mark_fast_mode_prompt_shown(self):
|
||||||
|
mark_fast_mode_prompt_shown(self.gui_config_file)
|
||||||
|
|
||||||
|
def set_fast_mode_enabled(self, enabled):
|
||||||
|
set_fast_mode_enabled(self.gui_config_file, enabled)
|
||||||
|
|
||||||
|
def navigate_after_profile_created(self):
|
||||||
|
if not self.has_shown_fast_mode_prompt():
|
||||||
|
self.navigator.navigate("fast_mode_prompt")
|
||||||
|
else:
|
||||||
|
menu_page = self.navigator.get_cached("menu")
|
||||||
|
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
|
||||||
|
menu_page.refresh_menu_buttons()
|
||||||
|
self.navigator.navigate("menu")
|
||||||
|
|
||||||
|
def enable_marquee(self, text):
|
||||||
|
self.marquee_text = text + " "
|
||||||
|
self.marquee_position = 0
|
||||||
|
self.marquee_enabled = True
|
||||||
|
self.marquee_timer.start(500)
|
||||||
|
|
||||||
|
def disable_marquee(self):
|
||||||
|
self.marquee_enabled = False
|
||||||
|
self.marquee_timer.stop()
|
||||||
|
|
||||||
|
def update_marquee(self):
|
||||||
|
if not self.marquee_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
metrics = self.status_label.fontMetrics()
|
||||||
|
text_width = metrics.horizontalAdvance(self.marquee_text)
|
||||||
|
|
||||||
|
self.marquee_position += self.scroll_speed
|
||||||
|
if self.marquee_position >= text_width:
|
||||||
|
self.marquee_position = 0
|
||||||
|
|
||||||
|
looped_text = self.marquee_text * 2
|
||||||
|
|
||||||
|
chars_that_fit = metrics.horizontalAdvance(
|
||||||
|
looped_text[:self.text_width])
|
||||||
|
|
||||||
|
visible_text = looped_text[self.marquee_position:
|
||||||
|
self.marquee_position + chars_that_fit]
|
||||||
|
|
||||||
|
while metrics.horizontalAdvance(visible_text) < self.text_width:
|
||||||
|
visible_text += self.marquee_text
|
||||||
|
|
||||||
|
while metrics.horizontalAdvance(visible_text) > self.text_width:
|
||||||
|
visible_text = visible_text[:-1]
|
||||||
|
|
||||||
|
display_text = 'Status: ' + ' ' * \
|
||||||
|
(self.text_start_x - metrics.horizontalAdvance('Status: '))
|
||||||
|
display_text += visible_text
|
||||||
|
|
||||||
|
self.status_label.setText(display_text)
|
||||||
|
|
||||||
|
def set_scroll_speed(self, speed):
|
||||||
|
self.scroll_speed = speed
|
||||||
|
|
||||||
|
def page_changed(self, index):
|
||||||
|
if should_be_synchronized():
|
||||||
|
sync_icon = QtGui.QIcon(os.path.join(
|
||||||
|
self.btn_path, 'icon_sync_urgent.png'))
|
||||||
|
else:
|
||||||
|
sync_icon = QtGui.QIcon(os.path.join(
|
||||||
|
self.btn_path, 'icon_sync.png'))
|
||||||
|
|
||||||
|
self.sync_button.setIcon(sync_icon)
|
||||||
|
|
||||||
|
current_page = self.page_stack.widget(index)
|
||||||
|
if self.page_history:
|
||||||
|
previous_page = self.page_history[-1]
|
||||||
|
else:
|
||||||
|
previous_page = None
|
||||||
|
|
||||||
|
current_name = getattr(current_page, 'name', None)
|
||||||
|
previous_name = getattr(previous_page, 'name', None)
|
||||||
|
|
||||||
|
if current_name == 'Menu':
|
||||||
|
self.update_status(None)
|
||||||
|
if previous_name in ('Resume', 'Editor', 'Settings', 'FastRegistration', 'FastModePrompt'):
|
||||||
|
if hasattr(current_page, 'eliminacion'):
|
||||||
|
current_page.eliminacion()
|
||||||
|
elif current_name == 'Resume':
|
||||||
|
self.update_status('Choose a name for your profile')
|
||||||
|
if hasattr(current_page, 'eliminacion'):
|
||||||
|
current_page.eliminacion()
|
||||||
|
elif current_name == 'Id':
|
||||||
|
pass
|
||||||
|
elif current_name == 'Location':
|
||||||
|
self.update_status(None, clear=True)
|
||||||
|
elif current_name == 'InstallPackage':
|
||||||
|
self.update_status('Please check package availability.')
|
||||||
|
else:
|
||||||
|
self.update_status(None)
|
||||||
|
|
||||||
|
if current_name == 'Menu':
|
||||||
|
self.sync_button.setVisible(True)
|
||||||
|
self.tor_text.setVisible(True)
|
||||||
|
self.toggle_button.setGeometry(670, 541, 50, 22)
|
||||||
|
self.tor_icon.setGeometry(730, 537, 25, 25)
|
||||||
|
else:
|
||||||
|
self.sync_button.setVisible(False)
|
||||||
|
self.tor_text.setVisible(False)
|
||||||
|
self.toggle_button.setGeometry(540, 541, 50, 22)
|
||||||
|
self.tor_icon.setGeometry(600, 537, 25, 25)
|
||||||
|
|
||||||
|
self.raise_bottom_controls()
|
||||||
|
|
||||||
|
if current_page != previous_page:
|
||||||
|
self.page_history.append(current_page)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = CustomWindow()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
|
||||||
Binary file not shown.
803
gui/main_ui.py
803
gui/main_ui.py
|
|
@ -1,803 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
_WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
if _WORKSPACE_ROOT not in sys.path:
|
|
||||||
sys.path.insert(0, _WORKSPACE_ROOT)
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
|
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QMessageBox
|
|
||||||
)
|
|
||||||
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase, QIcon
|
|
||||||
from PyQt6 import QtGui
|
|
||||||
from PyQt6.QtCore import Qt, QTimer, QEvent
|
|
||||||
|
|
||||||
from core.Constants import Constants
|
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
|
||||||
from core.Errors import UnknownConnectionTypeError
|
|
||||||
from core.errors.logger import logger as core_logger
|
|
||||||
|
|
||||||
core_logger.propagate = False
|
|
||||||
|
|
||||||
from gui.v2.infrastructure import orm
|
|
||||||
from gui.v2.infrastructure.navigator import Navigator
|
|
||||||
from gui.v2.infrastructure.setup_observers import setup_observers
|
|
||||||
from gui.v2.infrastructure.connection_manager import ConnectionManager
|
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
|
||||||
from gui.v2.ui.builders.bottom_section import create_bottom_section
|
|
||||||
from gui.v2.ui.builders.top_section import create_top_section
|
|
||||||
from gui.v2.ui.builders.system_tray import init_system_tray
|
|
||||||
from gui.v2.actions.config import (
|
|
||||||
setup_gui_directory,
|
|
||||||
load_gui_config,
|
|
||||||
save_gui_config,
|
|
||||||
default_gui_config,
|
|
||||||
)
|
|
||||||
from gui.v2.actions.logging_setup import (
|
|
||||||
is_logging_enabled,
|
|
||||||
setup_gui_logging,
|
|
||||||
teardown_gui_logging,
|
|
||||||
)
|
|
||||||
from gui.v2.actions.flags import (
|
|
||||||
has_shown_systemwide_prompt,
|
|
||||||
mark_systemwide_prompt_shown,
|
|
||||||
has_shown_fast_mode_prompt,
|
|
||||||
mark_fast_mode_prompt_shown,
|
|
||||||
set_fast_mode_enabled,
|
|
||||||
check_first_launch,
|
|
||||||
)
|
|
||||||
from gui.v2.actions.profile_data import (
|
|
||||||
read_profile_data,
|
|
||||||
write_profile_data,
|
|
||||||
clear_profile_data,
|
|
||||||
)
|
|
||||||
from gui.v2.actions.ticket_failure import (
|
|
||||||
save_ticket_verification_failure,
|
|
||||||
get_ticket_verification_failure,
|
|
||||||
clear_ticket_verification_failure,
|
|
||||||
)
|
|
||||||
from gui.v2.actions.should_be_synchronized import should_be_synchronized
|
|
||||||
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
|
||||||
|
|
||||||
|
|
||||||
class CustomWindow(QMainWindow):
|
|
||||||
def __init__(self, force_sync=False):
|
|
||||||
super().__init__()
|
|
||||||
sys.excepthook = self._handle_exception
|
|
||||||
self.setWindowFlags(Qt.WindowType.Window)
|
|
||||||
|
|
||||||
gui_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
font_path = os.path.join(gui_dir, 'resources', 'fonts')
|
|
||||||
|
|
||||||
# # Get's SQLAlchemy going,
|
|
||||||
# orm.start()
|
|
||||||
|
|
||||||
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
|
|
||||||
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
|
|
||||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
|
||||||
app_font = QFont(font_family)
|
|
||||||
app_font.setPointSize(10)
|
|
||||||
QApplication.setFont(app_font)
|
|
||||||
|
|
||||||
self.open_sans_family = None
|
|
||||||
open_sans_path = os.path.join(font_path, 'open-sans.ttf')
|
|
||||||
if os.path.exists(open_sans_path):
|
|
||||||
open_sans_id = QFontDatabase.addApplicationFont(open_sans_path)
|
|
||||||
if open_sans_id != -1:
|
|
||||||
self.open_sans_family = QFontDatabase.applicationFontFamilies(open_sans_id)[0]
|
|
||||||
|
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
|
||||||
self.setWindowTitle('HydraVeil')
|
|
||||||
|
|
||||||
screen = QApplication.primaryScreen()
|
|
||||||
ratio = screen.devicePixelRatio()
|
|
||||||
self.host_screen_width = int(screen.size().width() * ratio)
|
|
||||||
self.host_screen_height = int(screen.size().height() * ratio)
|
|
||||||
|
|
||||||
self._data = {"Profile_1": {}}
|
|
||||||
|
|
||||||
self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui')
|
|
||||||
self.gui_config_file = os.path.join(self.gui_config_home, 'config.json')
|
|
||||||
self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui')
|
|
||||||
self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log')
|
|
||||||
self._gui_log_handlers = []
|
|
||||||
|
|
||||||
setup_gui_directory(
|
|
||||||
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
|
||||||
|
|
||||||
self.btn_path = os.getenv('BTN_PATH', os.path.join(
|
|
||||||
gui_dir, 'resources', 'images'))
|
|
||||||
self.css_path = os.getenv('CSS_PATH', os.path.join(
|
|
||||||
gui_dir, 'resources', 'styles'))
|
|
||||||
|
|
||||||
self.force_sync = force_sync
|
|
||||||
self.is_downloading = False
|
|
||||||
self.current_profile_id = None
|
|
||||||
self.connection_manager = ConnectionManager()
|
|
||||||
|
|
||||||
current_connection = None
|
|
||||||
self.is_tor_mode = current_connection == 'tor'
|
|
||||||
|
|
||||||
self.is_animating = False
|
|
||||||
self.animation_step = 0
|
|
||||||
self.page_history = []
|
|
||||||
self.log_path = None
|
|
||||||
|
|
||||||
self.setFixedSize(800, 570)
|
|
||||||
|
|
||||||
top = create_top_section(self, self.css_path)
|
|
||||||
self.__dict__.update(top)
|
|
||||||
|
|
||||||
self.marquee_text = ""
|
|
||||||
self.marquee_position = 0
|
|
||||||
self.marquee_enabled = False
|
|
||||||
self.scroll_speed = 1
|
|
||||||
|
|
||||||
self.page_stack = QStackedWidget(self)
|
|
||||||
self.page_stack.setGeometry(0, 0, 800, 570)
|
|
||||||
|
|
||||||
bottom = create_bottom_section(
|
|
||||||
self,
|
|
||||||
self.btn_path,
|
|
||||||
Constants.HV_CLIENT_VERSION_NUMBER,
|
|
||||||
should_be_synchronized(),
|
|
||||||
)
|
|
||||||
self.__dict__.update(bottom)
|
|
||||||
self.status_label = bottom['status_label']
|
|
||||||
self.tor_text = bottom['tor_text']
|
|
||||||
self.toggle_button = bottom['toggle_button']
|
|
||||||
self.tor_icon = bottom['tor_icon']
|
|
||||||
self.sync_button = bottom['sync_button']
|
|
||||||
self.page_stack.raise_()
|
|
||||||
|
|
||||||
self.observers = setup_observers(self.update_status)
|
|
||||||
|
|
||||||
self.update_image()
|
|
||||||
self.set_scroll_speed(7)
|
|
||||||
|
|
||||||
self.toggle_button.clicked.connect(self.toggle_mode)
|
|
||||||
self.sync_button.clicked.connect(self.sync)
|
|
||||||
QApplication.instance().installEventFilter(self)
|
|
||||||
|
|
||||||
self.create_toggle(self.is_tor_mode)
|
|
||||||
self.animation_timer = QTimer()
|
|
||||||
self.animation_timer.timeout.connect(self.animate_toggle)
|
|
||||||
self.check_logging()
|
|
||||||
|
|
||||||
self.navigator = Navigator(self)
|
|
||||||
|
|
||||||
self.page_stack.currentChanged.connect(self.page_changed)
|
|
||||||
|
|
||||||
self.show()
|
|
||||||
self.init_ui()
|
|
||||||
|
|
||||||
self.tray = None
|
|
||||||
self.tray = init_system_tray(self, self.btn_path)
|
|
||||||
|
|
||||||
current_connection = self.get_current_connection()
|
|
||||||
self.is_tor_mode = current_connection == 'tor'
|
|
||||||
self.set_toggle_state(self.is_tor_mode)
|
|
||||||
|
|
||||||
core_logger.info(
|
|
||||||
f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, "
|
|
||||||
f"connection={current_connection}, tor_mode={self.is_tor_mode})"
|
|
||||||
)
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
if self.check_first_launch():
|
|
||||||
self.navigator.navigate("welcome")
|
|
||||||
else:
|
|
||||||
self.navigator.navigate("menu")
|
|
||||||
self.navigator.start_preload()
|
|
||||||
if self.force_sync:
|
|
||||||
QTimer.singleShot(0, self._prompt_force_sync)
|
|
||||||
|
|
||||||
|
|
||||||
def _prompt_force_sync(self):
|
|
||||||
"""
|
|
||||||
Popup to encourage remote database sync
|
|
||||||
|
|
||||||
Context:
|
|
||||||
Database loader in __main__ passes in the 'forced_sync' variable,
|
|
||||||
If true, this popup encourages sync to avoid NoneType errors.
|
|
||||||
"""
|
|
||||||
connection = ConfigurationController.get_connection()
|
|
||||||
box = QMessageBox(self)
|
|
||||||
box.setWindowTitle("Data Update Needed")
|
|
||||||
box.setText(
|
|
||||||
"Please download the new data to avoid errors. You have profiles that need the data to function.\n\n"
|
|
||||||
f"Connection type: {connection}")
|
|
||||||
|
|
||||||
# Apply styling
|
|
||||||
# import it inside the function to reduce load on non-display flows.
|
|
||||||
from gui.v2.ui.styles.css.main_ui_css import forced_sync_popup_style
|
|
||||||
stylesheet = forced_sync_popup_style
|
|
||||||
box.setStyleSheet(stylesheet)
|
|
||||||
|
|
||||||
# Add styled buttons
|
|
||||||
sync_button = box.addButton("Ok Fetch", QMessageBox.ButtonRole.AcceptRole)
|
|
||||||
sync_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
cancel_button = box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
||||||
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
box.setMinimumWidth(450) # button size
|
|
||||||
box.exec()
|
|
||||||
|
|
||||||
if box.clickedButton() == sync_button:
|
|
||||||
self.sync()
|
|
||||||
|
|
||||||
|
|
||||||
def perform_update_check(self):
|
|
||||||
from core.controllers.ClientController import ClientController
|
|
||||||
update_available = ClientController.can_be_updated()
|
|
||||||
|
|
||||||
if update_available:
|
|
||||||
self.navigator.update_if_cached(
|
|
||||||
"menu", lambda p: p.on_update_check_finished())
|
|
||||||
|
|
||||||
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
|
||||||
if not status:
|
|
||||||
self.update_status('Sync failed. Please try again later.')
|
|
||||||
return
|
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QPushButton
|
|
||||||
from gui.v2.actions.sync import generate_grid_positions
|
|
||||||
|
|
||||||
self.connection_manager.set_synced(True)
|
|
||||||
self.connection_manager.store_locations(locations)
|
|
||||||
self.connection_manager.store_browsers(available_browsers)
|
|
||||||
|
|
||||||
location_positions = generate_grid_positions(len(available_locations))
|
|
||||||
browser_positions = generate_grid_positions(len(available_browsers))
|
|
||||||
available_locations_list = [
|
|
||||||
(QPushButton, loc, location_positions[i])
|
|
||||||
for i, loc in enumerate(available_locations)
|
|
||||||
]
|
|
||||||
available_browsers_list = [
|
|
||||||
(QPushButton, brw, browser_positions[i])
|
|
||||||
for i, brw in enumerate(available_browsers)
|
|
||||||
]
|
|
||||||
|
|
||||||
self.navigator.update_if_cached(
|
|
||||||
"browser", lambda p: p.create_interface_elements(available_browsers_list))
|
|
||||||
self.navigator.update_if_cached(
|
|
||||||
"location", lambda p: p.create_interface_elements(available_locations_list))
|
|
||||||
self.navigator.update_if_cached(
|
|
||||||
"hidetor", lambda p: p.create_interface_elements(available_locations_list))
|
|
||||||
self.navigator.update_if_cached(
|
|
||||||
"protocol", lambda p: p.enable_protocol_buttons())
|
|
||||||
self.navigator.update_if_cached("menu", self._enable_menu_verification_icons)
|
|
||||||
|
|
||||||
def _enable_menu_verification_icons(self, menu_page):
|
|
||||||
if hasattr(menu_page, 'refresh_profiles_data'):
|
|
||||||
menu_page.refresh_profiles_data()
|
|
||||||
if hasattr(menu_page, 'buttons'):
|
|
||||||
for button in menu_page.buttons:
|
|
||||||
parent = button.parent()
|
|
||||||
if parent:
|
|
||||||
verification_icons = parent.findChildren(QPushButton)
|
|
||||||
for icon in verification_icons:
|
|
||||||
if icon.geometry().width() == 20 and icon.geometry().height() == 20:
|
|
||||||
icon.setEnabled(True)
|
|
||||||
|
|
||||||
def sync(self):
|
|
||||||
core_logger.info("User clicked sync button")
|
|
||||||
if ConfigurationController.get_connection() == 'tor':
|
|
||||||
self.worker_thread = WorkerThread('SYNC_TOR')
|
|
||||||
else:
|
|
||||||
self.worker_thread = WorkerThread('SYNC')
|
|
||||||
self.sync_button.setIcon(QtGui.QIcon(
|
|
||||||
os.path.join(self.btn_path, 'icon_sync.png')))
|
|
||||||
self.worker_thread.finished.connect(
|
|
||||||
lambda: self.perform_update_check())
|
|
||||||
self.worker_thread.sync_output.connect(self.update_values)
|
|
||||||
self.worker_thread.start()
|
|
||||||
|
|
||||||
def _handle_exception(self, identifier, message, trace):
|
|
||||||
if issubclass(identifier, UnknownConnectionTypeError):
|
|
||||||
self.setup_popup()
|
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
||||||
else:
|
|
||||||
config = self._load_gui_config()
|
|
||||||
if config and config["logging"]["gui_logging_enabled"] == True:
|
|
||||||
logging.error(
|
|
||||||
f"Uncaught exception:\n"
|
|
||||||
f"Type: {identifier.__name__}\n"
|
|
||||||
f"Value: {str(message)}\n"
|
|
||||||
f"Traceback:\n{''.join(traceback.format_tb(trace))}"
|
|
||||||
)
|
|
||||||
sys.__excepthook__(identifier, message, trace)
|
|
||||||
|
|
||||||
def get_current_connection(self):
|
|
||||||
return ConfigurationController.get_connection()
|
|
||||||
|
|
||||||
def setup_popup(self):
|
|
||||||
connection_dialog = QDialog(self)
|
|
||||||
connection_dialog.setWindowTitle("Connection Type")
|
|
||||||
connection_dialog.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowStaysOnTopHint |
|
|
||||||
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint)
|
|
||||||
connection_dialog.setModal(True)
|
|
||||||
connection_dialog.setFixedSize(400, 200)
|
|
||||||
|
|
||||||
layout = QVBoxLayout(connection_dialog)
|
|
||||||
|
|
||||||
label = QLabel(
|
|
||||||
"Please set your preference for connecting to Simplified Privacy's billing server.")
|
|
||||||
label.setWordWrap(True)
|
|
||||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
label.setStyleSheet(
|
|
||||||
"font-size: 12px; margin-bottom: 20px; color: black;")
|
|
||||||
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
|
|
||||||
regular_btn = QPushButton("Regular")
|
|
||||||
tor_btn = QPushButton("Tor")
|
|
||||||
|
|
||||||
button_style = """
|
|
||||||
QPushButton {
|
|
||||||
background-color: #2b2b2b;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #3b3b3b;
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background-color: #1b1b1b;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
regular_btn.setStyleSheet(button_style)
|
|
||||||
tor_btn.setStyleSheet(button_style)
|
|
||||||
|
|
||||||
button_layout.addWidget(regular_btn)
|
|
||||||
button_layout.addWidget(tor_btn)
|
|
||||||
|
|
||||||
layout.addWidget(label)
|
|
||||||
layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
def set_regular_connection():
|
|
||||||
ConfigurationController.set_connection('system')
|
|
||||||
self.is_tor_mode = False
|
|
||||||
self.set_toggle_state(False)
|
|
||||||
core_logger.info("User selected 'Regular' connection type from popup")
|
|
||||||
connection_dialog.accept()
|
|
||||||
|
|
||||||
def set_tor_connection():
|
|
||||||
ConfigurationController.set_connection('tor')
|
|
||||||
self.is_tor_mode = True
|
|
||||||
self.set_toggle_state(True)
|
|
||||||
core_logger.info("User selected 'Tor' connection type from popup")
|
|
||||||
connection_dialog.accept()
|
|
||||||
|
|
||||||
regular_btn.clicked.connect(set_regular_connection)
|
|
||||||
tor_btn.clicked.connect(set_tor_connection)
|
|
||||||
|
|
||||||
connection_dialog.exec()
|
|
||||||
|
|
||||||
return ConfigurationController.get_connection()
|
|
||||||
|
|
||||||
def _setup_gui_directory(self):
|
|
||||||
setup_gui_directory(
|
|
||||||
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
|
||||||
|
|
||||||
def _load_gui_config(self):
|
|
||||||
return load_gui_config(self.gui_config_file)
|
|
||||||
|
|
||||||
def _save_gui_config(self, config):
|
|
||||||
save_gui_config(self.gui_config_file, config)
|
|
||||||
|
|
||||||
def _default_gui_config(self):
|
|
||||||
return default_gui_config()
|
|
||||||
|
|
||||||
def save_ticket_verification_failure(self, result):
|
|
||||||
return save_ticket_verification_failure(self.gui_config_file, result)
|
|
||||||
|
|
||||||
def get_ticket_verification_failure(self):
|
|
||||||
return get_ticket_verification_failure(self.gui_config_file)
|
|
||||||
|
|
||||||
def clear_ticket_verification_failure(self):
|
|
||||||
clear_ticket_verification_failure(self.gui_config_file)
|
|
||||||
|
|
||||||
def check_logging(self):
|
|
||||||
config = self._load_gui_config()
|
|
||||||
if is_logging_enabled(config):
|
|
||||||
self._setup_gui_logging()
|
|
||||||
|
|
||||||
def has_shown_systemwide_prompt(self):
|
|
||||||
return has_shown_systemwide_prompt(self.gui_config_file)
|
|
||||||
|
|
||||||
def mark_systemwide_prompt_shown(self):
|
|
||||||
mark_systemwide_prompt_shown(self.gui_config_file)
|
|
||||||
|
|
||||||
def stop_gui_logging(self):
|
|
||||||
teardown_gui_logging(self.gui_log_file, self._gui_log_handlers)
|
|
||||||
self._gui_log_handlers = []
|
|
||||||
|
|
||||||
config = self._load_gui_config()
|
|
||||||
if config:
|
|
||||||
config["logging"]["gui_logging_enabled"] = False
|
|
||||||
self._save_gui_config(config)
|
|
||||||
|
|
||||||
def _setup_gui_logging(self):
|
|
||||||
self._gui_log_handlers = setup_gui_logging(
|
|
||||||
self.gui_cache_home, self.gui_log_file, self._gui_log_handlers)
|
|
||||||
|
|
||||||
def set_tor_icon(self):
|
|
||||||
if self.is_tor_mode:
|
|
||||||
icon_path = os.path.join(self.btn_path, "toricon_mini.png")
|
|
||||||
else:
|
|
||||||
icon_path = os.path.join(self.btn_path, "toricon_mini_false.png")
|
|
||||||
pixmap = QPixmap(icon_path)
|
|
||||||
self.tor_icon.setPixmap(pixmap)
|
|
||||||
self.tor_icon.setScaledContents(True)
|
|
||||||
|
|
||||||
def create_toggle(self, is_tor_mode):
|
|
||||||
handle_x = 30 if is_tor_mode else 3
|
|
||||||
colors = self._get_toggle_colors(is_tor_mode)
|
|
||||||
|
|
||||||
if not hasattr(self, 'animation_timer'):
|
|
||||||
self.animation_timer = QTimer()
|
|
||||||
self.animation_timer.timeout.connect(self.animate_toggle)
|
|
||||||
|
|
||||||
self.toggle_bg = QLabel(self.toggle_button)
|
|
||||||
self.toggle_bg.setGeometry(0, 0, 50, 22)
|
|
||||||
|
|
||||||
self.toggle_handle = QLabel(self.toggle_button)
|
|
||||||
self.toggle_handle.setGeometry(handle_x, 3, 16, 16)
|
|
||||||
self.toggle_handle.setStyleSheet("""
|
|
||||||
QLabel {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.on_label = QLabel("ON", self.toggle_button)
|
|
||||||
self.on_label.setGeometry(8, 4, 15, 14)
|
|
||||||
|
|
||||||
self.off_label = QLabel("OFF", self.toggle_button)
|
|
||||||
self.off_label.setGeometry(25, 4, 40, 14)
|
|
||||||
|
|
||||||
for widget in (self.toggle_bg, self.toggle_handle, self.on_label, self.off_label):
|
|
||||||
widget.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
|
||||||
|
|
||||||
self._update_toggle_colors(colors)
|
|
||||||
if is_tor_mode:
|
|
||||||
self.set_tor_icon()
|
|
||||||
|
|
||||||
self.raise_bottom_controls()
|
|
||||||
|
|
||||||
def _get_toggle_colors(self, is_tor_mode):
|
|
||||||
return {
|
|
||||||
'bg': "#00a8ff" if is_tor_mode else "#2c3e50",
|
|
||||||
'tor': "white" if is_tor_mode else "transparent",
|
|
||||||
'sys': "transparent" if is_tor_mode else "white"
|
|
||||||
}
|
|
||||||
|
|
||||||
def _update_toggle_colors(self, colors):
|
|
||||||
self.toggle_bg.setStyleSheet(f"""
|
|
||||||
QLabel {{
|
|
||||||
background-color: {colors['bg']};
|
|
||||||
border-radius: 11px;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.on_label.setStyleSheet(f"""
|
|
||||||
QLabel {{
|
|
||||||
color: {colors['tor']};
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.off_label.setStyleSheet(f"""
|
|
||||||
QLabel {{
|
|
||||||
color: {colors['sys']};
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|
||||||
def set_toggle_state(self, is_tor_mode):
|
|
||||||
self.is_tor_mode = is_tor_mode
|
|
||||||
self.toggle_button.setChecked(is_tor_mode)
|
|
||||||
handle_x = 30 if is_tor_mode else 3
|
|
||||||
self.toggle_handle.setGeometry(handle_x, 3, 16, 16)
|
|
||||||
self._update_toggle_colors(self._get_toggle_colors(is_tor_mode))
|
|
||||||
self.set_tor_icon()
|
|
||||||
|
|
||||||
def toggle_mode(self, *_args):
|
|
||||||
if self.is_animating and not self.animation_timer.isActive():
|
|
||||||
self.is_animating = False
|
|
||||||
|
|
||||||
if not self.is_animating:
|
|
||||||
target_is_tor = not self.is_tor_mode
|
|
||||||
new_connection = 'tor' if target_is_tor else 'system'
|
|
||||||
core_logger.info(f"Tor toggle clicked; switching connection mode to '{new_connection}'")
|
|
||||||
self.update_status(f"Tor toggle clicked: switching to {new_connection}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
ConfigurationController.set_connection(new_connection)
|
|
||||||
except Exception as error:
|
|
||||||
core_logger.exception(
|
|
||||||
f"Tor toggle failed while switching connection mode to '{new_connection}'")
|
|
||||||
self.update_status(f"Tor toggle failed: {error}")
|
|
||||||
self.set_toggle_state(self.is_tor_mode)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.set_toggle_state(target_is_tor)
|
|
||||||
self.is_animating = True
|
|
||||||
self.animation_step = 0
|
|
||||||
self.animation_timer.start(16)
|
|
||||||
return
|
|
||||||
|
|
||||||
core_logger.info("Tor toggle clicked while animation was still running")
|
|
||||||
self.update_status("Tor toggle clicked: still switching")
|
|
||||||
|
|
||||||
def raise_bottom_controls(self):
|
|
||||||
for widget in (self.status_label, self.tor_text, self.toggle_button, self.tor_icon, self.sync_button):
|
|
||||||
widget.raise_()
|
|
||||||
|
|
||||||
def eventFilter(self, source, event):
|
|
||||||
if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
|
|
||||||
if self._is_toggle_event(event):
|
|
||||||
self.toggle_mode()
|
|
||||||
return True
|
|
||||||
return super().eventFilter(source, event)
|
|
||||||
|
|
||||||
def _is_toggle_event(self, event):
|
|
||||||
if not hasattr(self, 'toggle_button') or not self.toggle_button.isVisible():
|
|
||||||
return False
|
|
||||||
if hasattr(event, 'globalPosition'):
|
|
||||||
global_pos = event.globalPosition().toPoint()
|
|
||||||
else:
|
|
||||||
global_pos = event.globalPos()
|
|
||||||
for widget in (self.tor_text, self.toggle_button, self.tor_icon):
|
|
||||||
if widget.isVisible() and widget.rect().contains(widget.mapFromGlobal(global_pos)):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def animate_toggle(self):
|
|
||||||
TOTAL_STEPS = 5
|
|
||||||
if self.animation_step <= TOTAL_STEPS:
|
|
||||||
progress = self.animation_step / TOTAL_STEPS
|
|
||||||
|
|
||||||
if self.is_tor_mode:
|
|
||||||
new_x = 20 + (10 * progress)
|
|
||||||
start_color = "#2c3e50"
|
|
||||||
end_color = "#00a8ff"
|
|
||||||
else:
|
|
||||||
new_x = 31 - (28 * progress)
|
|
||||||
start_color = "#00a8ff"
|
|
||||||
end_color = "#2c3e50"
|
|
||||||
|
|
||||||
self.toggle_handle.setGeometry(int(new_x), 3, 16, 16)
|
|
||||||
|
|
||||||
bg_color = self.interpolate_color(start_color, end_color, progress)
|
|
||||||
self.toggle_bg.setStyleSheet(f"""
|
|
||||||
QLabel {{
|
|
||||||
background-color: {bg_color};
|
|
||||||
border-radius: 11px;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.off_label.setStyleSheet(f"""
|
|
||||||
QLabel {{
|
|
||||||
color: rgba(255, 255, 255, {1 - progress if self.is_tor_mode else progress});
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
self.on_label.setStyleSheet(f"""
|
|
||||||
QLabel {{
|
|
||||||
color: rgba(255, 255, 255, {progress if self.is_tor_mode else 1 - progress});
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: bold;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.animation_step += 1
|
|
||||||
else:
|
|
||||||
self.animation_timer.stop()
|
|
||||||
self.is_animating = False
|
|
||||||
self.set_tor_icon()
|
|
||||||
|
|
||||||
def interpolate_color(self, start_color, end_color, progress):
|
|
||||||
def hex_to_rgb(hex_color):
|
|
||||||
hex_color = hex_color.lstrip('#')
|
|
||||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
||||||
|
|
||||||
start_rgb = hex_to_rgb(start_color)
|
|
||||||
end_rgb = hex_to_rgb(end_color)
|
|
||||||
|
|
||||||
current_rgb = tuple(
|
|
||||||
int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * progress)
|
|
||||||
for i in range(3)
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"rgb{current_rgb}"
|
|
||||||
|
|
||||||
def closeEvent(self, event=None):
|
|
||||||
core_logger.info("HydraVeil application closing")
|
|
||||||
|
|
||||||
# Close SQLAlchemy,
|
|
||||||
orm.stop()
|
|
||||||
|
|
||||||
connected_profiles = self.connection_manager.get_connected_profiles()
|
|
||||||
if connected_profiles:
|
|
||||||
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
|
|
||||||
if event is not None:
|
|
||||||
event.accept()
|
|
||||||
else:
|
|
||||||
if event is not None:
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def update_image(self, appearance_value="original"):
|
|
||||||
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
|
|
||||||
pixmap = QPixmap(image_path)
|
|
||||||
self.label.setPixmap(pixmap)
|
|
||||||
self.label.setScaledContents(True)
|
|
||||||
|
|
||||||
def read_data(self):
|
|
||||||
return read_profile_data(self._data)
|
|
||||||
|
|
||||||
def write_data(self, data):
|
|
||||||
write_profile_data(self._data, data)
|
|
||||||
|
|
||||||
def clear_data(self):
|
|
||||||
self._data = clear_profile_data()
|
|
||||||
|
|
||||||
def _set_status_font_size(self, font_size):
|
|
||||||
self.status_label.setStyleSheet(
|
|
||||||
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
|
|
||||||
|
|
||||||
def update_status(self, text, clear=False):
|
|
||||||
if text is None:
|
|
||||||
self._set_status_font_size(16)
|
|
||||||
self.status_label.setText('Status:')
|
|
||||||
self.disable_marquee()
|
|
||||||
return
|
|
||||||
|
|
||||||
if clear:
|
|
||||||
self._set_status_font_size(16)
|
|
||||||
self.status_label.setText('')
|
|
||||||
return
|
|
||||||
|
|
||||||
full_text = 'Status: ' + text
|
|
||||||
metrics = self.status_label.fontMetrics()
|
|
||||||
available_width = self.status_label.width() - 10
|
|
||||||
|
|
||||||
if metrics.horizontalAdvance(full_text) > available_width:
|
|
||||||
self.enable_marquee(text)
|
|
||||||
else:
|
|
||||||
self.status_label.setText(full_text)
|
|
||||||
self.disable_marquee()
|
|
||||||
|
|
||||||
def check_first_launch(self):
|
|
||||||
return check_first_launch(self.gui_config_file)
|
|
||||||
|
|
||||||
def has_shown_fast_mode_prompt(self):
|
|
||||||
return has_shown_fast_mode_prompt(self.gui_config_file)
|
|
||||||
|
|
||||||
def mark_fast_mode_prompt_shown(self):
|
|
||||||
mark_fast_mode_prompt_shown(self.gui_config_file)
|
|
||||||
|
|
||||||
def set_fast_mode_enabled(self, enabled):
|
|
||||||
set_fast_mode_enabled(self.gui_config_file, enabled)
|
|
||||||
|
|
||||||
def navigate_after_profile_created(self):
|
|
||||||
if not self.has_shown_fast_mode_prompt():
|
|
||||||
self.navigator.navigate("fast_mode_prompt")
|
|
||||||
else:
|
|
||||||
self.navigator.update_if_cached(
|
|
||||||
"menu",
|
|
||||||
lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None)
|
|
||||||
self.navigator.navigate("menu")
|
|
||||||
|
|
||||||
def enable_marquee(self, text):
|
|
||||||
self.marquee_text = text + " "
|
|
||||||
self.marquee_position = 0
|
|
||||||
self.marquee_enabled = True
|
|
||||||
self.marquee_timer.start(500)
|
|
||||||
|
|
||||||
def disable_marquee(self):
|
|
||||||
self.marquee_enabled = False
|
|
||||||
self.marquee_timer.stop()
|
|
||||||
|
|
||||||
def update_marquee(self):
|
|
||||||
if not self.marquee_enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
metrics = self.status_label.fontMetrics()
|
|
||||||
text_width = metrics.horizontalAdvance(self.marquee_text)
|
|
||||||
|
|
||||||
self.marquee_position += self.scroll_speed
|
|
||||||
if self.marquee_position >= text_width:
|
|
||||||
self.marquee_position = 0
|
|
||||||
|
|
||||||
looped_text = self.marquee_text * 2
|
|
||||||
|
|
||||||
chars_that_fit = metrics.horizontalAdvance(
|
|
||||||
looped_text[:self.text_width])
|
|
||||||
|
|
||||||
visible_text = looped_text[self.marquee_position:
|
|
||||||
self.marquee_position + chars_that_fit]
|
|
||||||
|
|
||||||
while metrics.horizontalAdvance(visible_text) < self.text_width:
|
|
||||||
visible_text += self.marquee_text
|
|
||||||
|
|
||||||
while metrics.horizontalAdvance(visible_text) > self.text_width:
|
|
||||||
visible_text = visible_text[:-1]
|
|
||||||
|
|
||||||
display_text = 'Status: ' + ' ' * \
|
|
||||||
(self.text_start_x - metrics.horizontalAdvance('Status: '))
|
|
||||||
display_text += visible_text
|
|
||||||
|
|
||||||
self.status_label.setText(display_text)
|
|
||||||
|
|
||||||
def set_scroll_speed(self, speed):
|
|
||||||
self.scroll_speed = speed
|
|
||||||
|
|
||||||
def page_changed(self, index):
|
|
||||||
if should_be_synchronized():
|
|
||||||
sync_icon = QtGui.QIcon(os.path.join(
|
|
||||||
self.btn_path, 'icon_sync_urgent.png'))
|
|
||||||
else:
|
|
||||||
sync_icon = QtGui.QIcon(os.path.join(
|
|
||||||
self.btn_path, 'icon_sync.png'))
|
|
||||||
|
|
||||||
self.sync_button.setIcon(sync_icon)
|
|
||||||
|
|
||||||
current_page = self.page_stack.widget(index)
|
|
||||||
if self.page_history:
|
|
||||||
previous_page = self.page_history[-1]
|
|
||||||
else:
|
|
||||||
previous_page = None
|
|
||||||
|
|
||||||
current_name = getattr(current_page, 'name', None)
|
|
||||||
previous_name = getattr(previous_page, 'name', None)
|
|
||||||
|
|
||||||
if current_name == 'Menu':
|
|
||||||
self.update_status(None)
|
|
||||||
if previous_name in ('Resume', 'Editor', 'Settings', 'FastRegistration', 'FastModePrompt'):
|
|
||||||
if hasattr(current_page, 'eliminacion'):
|
|
||||||
current_page.eliminacion()
|
|
||||||
elif current_name == 'Resume':
|
|
||||||
self.update_status('Choose a name for your profile')
|
|
||||||
if hasattr(current_page, 'eliminacion'):
|
|
||||||
current_page.eliminacion()
|
|
||||||
elif current_name == 'Id':
|
|
||||||
pass
|
|
||||||
elif current_name == 'Location':
|
|
||||||
self.update_status(None, clear=True)
|
|
||||||
elif current_name == 'InstallPackage':
|
|
||||||
self.update_status('Please check package availability.')
|
|
||||||
else:
|
|
||||||
self.update_status(None)
|
|
||||||
|
|
||||||
if current_name == 'Menu':
|
|
||||||
self.sync_button.setVisible(True)
|
|
||||||
self.tor_text.setVisible(True)
|
|
||||||
self.toggle_button.setGeometry(670, 541, 50, 22)
|
|
||||||
self.tor_icon.setGeometry(730, 537, 25, 25)
|
|
||||||
else:
|
|
||||||
self.sync_button.setVisible(False)
|
|
||||||
self.tor_text.setVisible(False)
|
|
||||||
self.toggle_button.setGeometry(540, 541, 50, 22)
|
|
||||||
self.tor_icon.setGeometry(600, 537, 25, 25)
|
|
||||||
|
|
||||||
self.raise_bottom_controls()
|
|
||||||
|
|
||||||
if current_page != previous_page:
|
|
||||||
self.page_history.append(current_page)
|
|
||||||
|
|
||||||
|
|
||||||
def start_ui(force_sync):
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
window = CustomWindow(force_sync)
|
|
||||||
sys.exit(app.exec())
|
|
||||||
12363
gui/main_v1.py
Normal file
12363
gui/main_v1.py
Normal file
File diff suppressed because it is too large
Load diff
35
gui/presenters/interpret_key_results.py
Normal file
35
gui/presenters/interpret_key_results.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
def interpret_key_results(result: dict) -> str:
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
return "There was an error with the format of the reply from the operation."
|
||||||
|
|
||||||
|
# unpack results:
|
||||||
|
valid = result.get('valid', False)
|
||||||
|
comparison = result.get('comparison', 'error')
|
||||||
|
error_msg = result.get('message', None)
|
||||||
|
|
||||||
|
# errors:
|
||||||
|
if error_msg:
|
||||||
|
if error_msg == "api_connection_issue":
|
||||||
|
return "There was a connection error with connecting to the API."
|
||||||
|
elif error_msg == "invalid_key":
|
||||||
|
return "Your original public key is in an invalid format to begin with."
|
||||||
|
else:
|
||||||
|
return "Unknown Error."
|
||||||
|
|
||||||
|
# comparison:
|
||||||
|
if comparison == "same":
|
||||||
|
first_sentence = "The key you had locally matched what the server had also."
|
||||||
|
elif comparison == "different":
|
||||||
|
first_sentence = "Your local key was different than the server's key."
|
||||||
|
else:
|
||||||
|
first_sentence = "There was an error with the comparison."
|
||||||
|
|
||||||
|
# validity:
|
||||||
|
if valid:
|
||||||
|
second_sentence = "The signature now matches with the new key."
|
||||||
|
else:
|
||||||
|
second_sentence = "But the signature still does not match, even with the new key. If you prepare tickets anyway, they might not verify when used."
|
||||||
|
|
||||||
|
# final return
|
||||||
|
final_msg = first_sentence + " " + second_sentence
|
||||||
|
return final_msg
|
||||||
|
|
@ -35,20 +35,6 @@ def mark_fast_mode_prompt_shown(gui_config_file):
|
||||||
save_gui_config(gui_config_file, config)
|
save_gui_config(gui_config_file, config)
|
||||||
|
|
||||||
|
|
||||||
def is_fast_registration_enabled(gui_config_file):
|
|
||||||
config = load_gui_config(gui_config_file)
|
|
||||||
if not config:
|
|
||||||
return False
|
|
||||||
return config.get("registrations", {}).get("fast_registration_enabled", False)
|
|
||||||
|
|
||||||
|
|
||||||
def is_auto_sync_enabled(gui_config_file):
|
|
||||||
config = load_gui_config(gui_config_file)
|
|
||||||
if not config:
|
|
||||||
return False
|
|
||||||
return config.get("registrations", {}).get("auto_sync_enabled", False)
|
|
||||||
|
|
||||||
|
|
||||||
def set_fast_mode_enabled(gui_config_file, enabled):
|
def set_fast_mode_enabled(gui_config_file, enabled):
|
||||||
config = load_gui_config(gui_config_file)
|
config = load_gui_config(gui_config_file)
|
||||||
if config is None:
|
if config is None:
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from core.Constants import Constants
|
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
|
||||||
from core.controllers.PolicyController import PolicyController
|
|
||||||
from core.controllers.ProfileController import ProfileController
|
|
||||||
|
|
||||||
from gui.v2.actions.config import load_gui_config
|
|
||||||
from gui.v2.actions.profile_order import normalize_profile_order
|
|
||||||
from gui.v2.actions.ticket_failure import get_ticket_verification_failure
|
|
||||||
|
|
||||||
|
|
||||||
def read_systemwide_enabled():
|
|
||||||
try:
|
|
||||||
privilege_policy = PolicyController.get('privilege')
|
|
||||||
if privilege_policy is not None:
|
|
||||||
return PolicyController.is_instated(privilege_policy)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def read_bwrap_enabled():
|
|
||||||
try:
|
|
||||||
capability_policy = PolicyController.get('capability')
|
|
||||||
if capability_policy is not None:
|
|
||||||
return PolicyController.is_instated(capability_policy)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def prepare(gui_config_file):
|
|
||||||
profiles = ProfileController.get_all()
|
|
||||||
profile_order = normalize_profile_order(gui_config_file, profiles.keys())
|
|
||||||
try:
|
|
||||||
endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled()
|
|
||||||
except Exception:
|
|
||||||
endpoint_verification_enabled = False
|
|
||||||
return {
|
|
||||||
"profiles": profiles,
|
|
||||||
"profile_order": profile_order,
|
|
||||||
"current_connection": ConfigurationController.get_connection(),
|
|
||||||
"endpoint_verification_enabled": endpoint_verification_enabled,
|
|
||||||
"systemwide_enabled": read_systemwide_enabled(),
|
|
||||||
"bwrap_enabled": read_bwrap_enabled(),
|
|
||||||
"gui_config": load_gui_config(gui_config_file),
|
|
||||||
"ticket_failure": get_ticket_verification_failure(gui_config_file),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def empty_payload():
|
|
||||||
return {
|
|
||||||
"profiles": {},
|
|
||||||
"profile_order": [],
|
|
||||||
"current_connection": None,
|
|
||||||
"endpoint_verification_enabled": False,
|
|
||||||
"systemwide_enabled": False,
|
|
||||||
"bwrap_enabled": False,
|
|
||||||
"gui_config": None,
|
|
||||||
"ticket_failure": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def extract_endpoint_ip(profile):
|
|
||||||
try:
|
|
||||||
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}'
|
|
||||||
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
|
||||||
if not os.path.exists(wg_conf_path):
|
|
||||||
return None
|
|
||||||
with open(wg_conf_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
for line in content.split('\n'):
|
|
||||||
if line.strip().startswith('Endpoint = '):
|
|
||||||
endpoint = line.strip().split(' = ')[1]
|
|
||||||
return endpoint.split(':')[0]
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def truncate_key(text, max_length=50):
|
|
||||||
if not text or text == "N/A" or len(text) <= max_length:
|
|
||||||
return text
|
|
||||||
start_len = max_length // 2 - 2
|
|
||||||
end_len = max_length // 2 - 2
|
|
||||||
return text[:start_len] + "....." + text[-end_len:]
|
|
||||||
|
|
||||||
|
|
||||||
def build_verification_view(profile):
|
|
||||||
operator = None
|
|
||||||
if profile and getattr(profile, 'location', None) and profile.location.operator:
|
|
||||||
operator = profile.location.operator
|
|
||||||
if not operator:
|
|
||||||
return {
|
|
||||||
"operator_name": "N/A",
|
|
||||||
"nostr_public_key": "N/A",
|
|
||||||
"hydraveil_public_key": "N/A",
|
|
||||||
"nostr_attestation_event_reference": "N/A",
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"operator_name": operator.name or "N/A",
|
|
||||||
"nostr_public_key": operator.nostr_public_key or "N/A",
|
|
||||||
"hydraveil_public_key": operator.public_key or "N/A",
|
|
||||||
"nostr_attestation_event_reference": operator.nostr_attestation_event_reference or "N/A",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_subscription_view(profile):
|
|
||||||
subscription = profile.subscription
|
|
||||||
billing_code = str(subscription.billing_code)
|
|
||||||
if hasattr(subscription, 'expires_at') and subscription.expires_at:
|
|
||||||
expires_at = subscription.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
||||||
else:
|
|
||||||
expires_at = "Not available"
|
|
||||||
return {"billing_code": billing_code, "expires_at": expires_at}
|
|
||||||
|
|
||||||
|
|
||||||
def format_ticket_failure_status(failure):
|
|
||||||
if not failure:
|
|
||||||
return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here."
|
|
||||||
failed_validations = failure.get("failed_validations", [])
|
|
||||||
how_many_failed = failure.get("how_many_failed", len(failed_validations))
|
|
||||||
updated_at = failure.get("updated_at", "unknown time")
|
|
||||||
failed_text = ", ".join(str(item) for item in failed_validations)
|
|
||||||
return (
|
|
||||||
f"Saved verification failure: {how_many_failed} failed. "
|
|
||||||
f"Failed validation indices: {failed_text}. Saved: {updated_at}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_ticket_recovery_result(label, result):
|
|
||||||
try:
|
|
||||||
payload = json.dumps(result, indent=2, default=str)
|
|
||||||
except TypeError:
|
|
||||||
payload = str(result)
|
|
||||||
return f"{label}:\n{payload}"
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
def is_valid_sync_payload(available_locations, available_browsers, status, locations, all_browsers):
|
|
||||||
if status is not True:
|
|
||||||
return False
|
|
||||||
if isinstance(all_browsers, bool):
|
|
||||||
return False
|
|
||||||
if not available_locations or not available_browsers:
|
|
||||||
return False
|
|
||||||
if not locations or not all_browsers:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
@ -1,224 +1,33 @@
|
||||||
import importlib
|
import importlib
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from PyQt6.QtCore import QObject, QTimer
|
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
|
||||||
|
|
||||||
from gui.v2.infrastructure.page_registry import (
|
|
||||||
PAGE_REGISTRY,
|
|
||||||
ASYNC_PREPARE,
|
|
||||||
REGULAR_FLOW_ONLY,
|
|
||||||
PRELOAD_ORDER,
|
|
||||||
PRELOAD_SKIP,
|
|
||||||
)
|
|
||||||
from gui.v2.workers.page_data_worker import PageDataWorker
|
|
||||||
from gui.v2.actions.flags import is_fast_registration_enabled
|
|
||||||
|
|
||||||
|
|
||||||
class Navigator(QObject):
|
class Navigator:
|
||||||
def __init__(self, custom_window):
|
def __init__(self, page_stack, custom_window):
|
||||||
super().__init__()
|
self.page_stack = page_stack
|
||||||
self.custom_window = custom_window
|
self.custom_window = custom_window
|
||||||
self.page_stack = custom_window.page_stack
|
|
||||||
self._instances = {}
|
self._instances = {}
|
||||||
|
|
||||||
self._preload_queue = []
|
|
||||||
self._preload_total = 0
|
|
||||||
self._preload_done = 0
|
|
||||||
self._preload_failed = 0
|
|
||||||
self._preload_skipped_runtime = 0
|
|
||||||
self._preload_dispatched = 0
|
|
||||||
self._preload_inflight = 0
|
|
||||||
self._preload_workers = {}
|
|
||||||
self._preload_summary_emitted = False
|
|
||||||
self._preload_t_start = 0.0
|
|
||||||
self._preload_timer = QTimer(self)
|
|
||||||
self._preload_timer.setSingleShot(True)
|
|
||||||
self._preload_timer.timeout.connect(self._preload_next)
|
|
||||||
|
|
||||||
def get_class(self, name):
|
|
||||||
module_path, class_name = PAGE_REGISTRY[name]
|
|
||||||
module = importlib.import_module(module_path)
|
|
||||||
return getattr(module, class_name)
|
|
||||||
|
|
||||||
def _instantiate(self, name, prepared=None):
|
|
||||||
cls = self.get_class(name)
|
|
||||||
if name in ASYNC_PREPARE:
|
|
||||||
return cls(self.page_stack, self.custom_window, prepared=prepared)
|
|
||||||
return cls(self.page_stack, self.custom_window)
|
|
||||||
|
|
||||||
def register_instance(self, name, page):
|
|
||||||
self.page_stack.addWidget(page)
|
|
||||||
self._instances[name] = page
|
|
||||||
|
|
||||||
def navigate(self, name):
|
def navigate(self, name):
|
||||||
page = self._instances.get(name)
|
page = self._instances.get(name)
|
||||||
if page is None:
|
if page is None:
|
||||||
if name not in PAGE_REGISTRY:
|
if name not in PAGE_REGISTRY:
|
||||||
self._warn_not_migrated(name)
|
self._warn_not_migrated(name)
|
||||||
return None
|
return
|
||||||
page = self._instantiate(name)
|
module_path, class_name = PAGE_REGISTRY[name]
|
||||||
self.register_instance(name, page)
|
module = importlib.import_module(module_path)
|
||||||
|
cls = getattr(module, class_name)
|
||||||
|
page = cls(self.page_stack, self.custom_window)
|
||||||
|
self.page_stack.addWidget(page)
|
||||||
|
self._instances[name] = page
|
||||||
self.page_stack.setCurrentIndex(self.page_stack.indexOf(page))
|
self.page_stack.setCurrentIndex(self.page_stack.indexOf(page))
|
||||||
return page
|
|
||||||
|
|
||||||
def get_cached(self, name):
|
def get_cached(self, name):
|
||||||
return self._instances.get(name)
|
return self._instances.get(name)
|
||||||
|
|
||||||
def update_if_cached(self, name, fn):
|
|
||||||
page = self._instances.get(name)
|
|
||||||
if page is not None:
|
|
||||||
fn(page)
|
|
||||||
return page
|
|
||||||
|
|
||||||
def _warn_not_migrated(self, name):
|
def _warn_not_migrated(self, name):
|
||||||
try:
|
try:
|
||||||
self.custom_window.update_status(f"Page '{name}' not migrated yet.")
|
self.custom_window.update_status(f"Page '{name}' not migrated yet.")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _preload_one(self, name):
|
|
||||||
if name in self._instances:
|
|
||||||
print(f"[preload] {name:30s} SKIP (already cached)", flush=True)
|
|
||||||
return False
|
|
||||||
if name not in PAGE_REGISTRY:
|
|
||||||
print(f"[preload] {name:30s} SKIP (not in registry)", flush=True)
|
|
||||||
return False
|
|
||||||
module_path, class_name = PAGE_REGISTRY[name]
|
|
||||||
print(f"[preload] {name:30s} LOAD {module_path}.{class_name}", flush=True)
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
try:
|
|
||||||
page = self._instantiate(name)
|
|
||||||
self.register_instance(name, page)
|
|
||||||
dt_ms = (time.perf_counter() - t0) * 1000.0
|
|
||||||
print(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
|
|
||||||
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})",
|
|
||||||
flush=True)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
dt_ms = (time.perf_counter() - t0) * 1000.0
|
|
||||||
print(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
|
|
||||||
f"{type(e).__name__}: {e}", flush=True)
|
|
||||||
traceback.print_exc()
|
|
||||||
from core.errors.logger import logger as core_logger
|
|
||||||
core_logger.warning(f"Background preload failed for '{name}': "
|
|
||||||
f"{type(e).__name__}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def start_preload(self, fast_mode=None):
|
|
||||||
if fast_mode is None:
|
|
||||||
fast_mode = is_fast_registration_enabled(self.custom_window.gui_config_file)
|
|
||||||
self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP]
|
|
||||||
skipped_regular = []
|
|
||||||
if fast_mode:
|
|
||||||
skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY]
|
|
||||||
self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY]
|
|
||||||
self._preload_total = len(self._preload_queue)
|
|
||||||
self._preload_done = 0
|
|
||||||
self._preload_failed = 0
|
|
||||||
self._preload_skipped_runtime = 0
|
|
||||||
self._preload_dispatched = 0
|
|
||||||
self._preload_inflight = 0
|
|
||||||
self._preload_summary_emitted = False
|
|
||||||
self._preload_t_start = time.perf_counter()
|
|
||||||
print(f"[preload] ============================================================", flush=True)
|
|
||||||
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
|
|
||||||
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
|
|
||||||
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
|
|
||||||
if fast_mode:
|
|
||||||
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
|
|
||||||
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
|
|
||||||
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
|
|
||||||
print(f"[preload] ============================================================", flush=True)
|
|
||||||
self._preload_timer.start(700)
|
|
||||||
|
|
||||||
def _preload_next(self):
|
|
||||||
while self._preload_queue and self._preload_queue[0] in self._instances:
|
|
||||||
skipped = self._preload_queue.pop(0)
|
|
||||||
self._preload_skipped_runtime += 1
|
|
||||||
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
|
|
||||||
if not self._preload_queue:
|
|
||||||
self._maybe_emit_preload_summary()
|
|
||||||
return
|
|
||||||
self._preload_dispatched += 1
|
|
||||||
idx = self._preload_dispatched
|
|
||||||
name = self._preload_queue.pop(0)
|
|
||||||
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
|
|
||||||
if name in ASYNC_PREPARE:
|
|
||||||
self._dispatch_async_preload(name)
|
|
||||||
else:
|
|
||||||
ok = self._preload_one(name)
|
|
||||||
if ok:
|
|
||||||
self._preload_done += 1
|
|
||||||
else:
|
|
||||||
self._preload_failed += 1
|
|
||||||
if self._preload_queue:
|
|
||||||
self._preload_timer.start(250)
|
|
||||||
else:
|
|
||||||
self._maybe_emit_preload_summary()
|
|
||||||
|
|
||||||
def _prepare_page_data(self, name):
|
|
||||||
module_path, class_name = PAGE_REGISTRY[name]
|
|
||||||
module = importlib.import_module(module_path)
|
|
||||||
cls = getattr(module, class_name)
|
|
||||||
return cls.prepare_data(self.custom_window)
|
|
||||||
|
|
||||||
def _dispatch_async_preload(self, name):
|
|
||||||
module_path, class_name = PAGE_REGISTRY[name]
|
|
||||||
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
|
|
||||||
worker = PageDataWorker(name, self._prepare_page_data, name)
|
|
||||||
worker.data_ready.connect(self._on_preload_data_ready)
|
|
||||||
worker.failed.connect(self._on_preload_failed)
|
|
||||||
worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n))
|
|
||||||
self._preload_workers[name] = worker
|
|
||||||
self._preload_inflight += 1
|
|
||||||
worker.start()
|
|
||||||
|
|
||||||
def _on_preload_data_ready(self, name, payload):
|
|
||||||
self._preload_inflight -= 1
|
|
||||||
if name in self._instances:
|
|
||||||
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
page = self._instantiate(name, prepared=payload)
|
|
||||||
self.register_instance(name, page)
|
|
||||||
self._preload_done += 1
|
|
||||||
print(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
|
|
||||||
f"stack_size={self.page_stack.count()})", flush=True)
|
|
||||||
except Exception as e:
|
|
||||||
self._preload_failed += 1
|
|
||||||
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
|
|
||||||
self._maybe_emit_preload_summary()
|
|
||||||
|
|
||||||
def _on_preload_failed(self, name, error):
|
|
||||||
self._preload_inflight -= 1
|
|
||||||
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
|
|
||||||
if name not in self._instances:
|
|
||||||
ok = self._preload_one(name)
|
|
||||||
if ok:
|
|
||||||
self._preload_done += 1
|
|
||||||
else:
|
|
||||||
self._preload_failed += 1
|
|
||||||
self._maybe_emit_preload_summary()
|
|
||||||
|
|
||||||
def _cleanup_preload_worker(self, name):
|
|
||||||
worker = self._preload_workers.pop(name, None)
|
|
||||||
if worker is not None:
|
|
||||||
worker.deleteLater()
|
|
||||||
|
|
||||||
def _maybe_emit_preload_summary(self):
|
|
||||||
if self._preload_summary_emitted:
|
|
||||||
return
|
|
||||||
if not self._preload_queue and self._preload_inflight == 0:
|
|
||||||
self._preload_summary_emitted = True
|
|
||||||
self._emit_preload_summary()
|
|
||||||
|
|
||||||
def _emit_preload_summary(self):
|
|
||||||
total_s = time.perf_counter() - self._preload_t_start
|
|
||||||
print(f"[preload] ============================================================", flush=True)
|
|
||||||
print(f"[preload] complete: {self._preload_done} loaded, "
|
|
||||||
f"{self._preload_failed} failed, "
|
|
||||||
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
|
|
||||||
f"in {total_s:.2f}s", flush=True)
|
|
||||||
print(f"[preload] cached pages now: {sorted(self._instances.keys())}", flush=True)
|
|
||||||
print(f"[preload] ============================================================", flush=True)
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
from core.models.manage.session_management import init_session, close_session
|
|
||||||
|
|
||||||
def start():
|
|
||||||
init_session()
|
|
||||||
|
|
||||||
def stop():
|
|
||||||
close_session()
|
|
||||||
|
|
@ -30,57 +30,3 @@ PAGE_REGISTRY = {
|
||||||
"fast_registration": ("gui.v2.ui.pages.fast_registration_page", "FastRegistrationPage"),
|
"fast_registration": ("gui.v2.ui.pages.fast_registration_page", "FastRegistrationPage"),
|
||||||
"fast_mode_prompt": ("gui.v2.ui.pages.fast_mode_prompt_page", "FastModePromptPage"),
|
"fast_mode_prompt": ("gui.v2.ui.pages.fast_mode_prompt_page", "FastModePromptPage"),
|
||||||
}
|
}
|
||||||
|
|
||||||
PRELOAD_SKIP = {
|
|
||||||
"blank",
|
|
||||||
"welcome",
|
|
||||||
"payment_confirmed",
|
|
||||||
"ticket_prep",
|
|
||||||
}
|
|
||||||
|
|
||||||
ASYNC_PREPARE = {
|
|
||||||
"settings",
|
|
||||||
"fast_registration",
|
|
||||||
"editor",
|
|
||||||
}
|
|
||||||
|
|
||||||
REGULAR_FLOW_ONLY = {
|
|
||||||
"protocol",
|
|
||||||
"location",
|
|
||||||
"hidetor",
|
|
||||||
"residential",
|
|
||||||
"tor",
|
|
||||||
"connection",
|
|
||||||
"screen",
|
|
||||||
"browser",
|
|
||||||
"wireguard",
|
|
||||||
"sync_screen",
|
|
||||||
}
|
|
||||||
|
|
||||||
PRELOAD_ORDER = [
|
|
||||||
"settings",
|
|
||||||
"editor",
|
|
||||||
"ticket_or_billing_choice",
|
|
||||||
"payment_details",
|
|
||||||
"plan_picker",
|
|
||||||
"ticket_crypto_picker",
|
|
||||||
"duration_selection",
|
|
||||||
"currency_selection",
|
|
||||||
"policy_suggestion",
|
|
||||||
"systemwide_prompt",
|
|
||||||
"fast_mode_prompt",
|
|
||||||
"fast_registration",
|
|
||||||
"id",
|
|
||||||
"protocol",
|
|
||||||
"location",
|
|
||||||
"hidetor",
|
|
||||||
"residential",
|
|
||||||
"tor",
|
|
||||||
"connection",
|
|
||||||
"screen",
|
|
||||||
"browser",
|
|
||||||
"resume",
|
|
||||||
"wireguard",
|
|
||||||
"install_system_package",
|
|
||||||
"sync_screen",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,8 @@ from core.observers.InvoiceObserver import InvoiceObserver
|
||||||
from core.observers.ProfileObserver import ProfileObserver
|
from core.observers.ProfileObserver import ProfileObserver
|
||||||
from core.observers.TicketObserver import TicketObserver
|
from core.observers.TicketObserver import TicketObserver
|
||||||
from core.controllers.ApplicationController import ApplicationController
|
from core.controllers.ApplicationController import ApplicationController
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal
|
|
||||||
|
|
||||||
|
|
||||||
class StatusRelay(QObject):
|
|
||||||
status_requested = pyqtSignal(object, bool)
|
|
||||||
|
|
||||||
def __init__(self, update_status):
|
|
||||||
super().__init__()
|
|
||||||
self.update_status = update_status
|
|
||||||
self.status_requested.connect(self.apply_status)
|
|
||||||
|
|
||||||
def post(self, text, clear=False):
|
|
||||||
self.status_requested.emit(text, clear)
|
|
||||||
|
|
||||||
def apply_status(self, text, clear=False):
|
|
||||||
self.update_status(text, clear=clear)
|
|
||||||
|
|
||||||
application_version_observer = ApplicationVersionObserver()
|
application_version_observer = ApplicationVersionObserver()
|
||||||
client_observer = ClientObserver()
|
client_observer = ClientObserver()
|
||||||
connection_observer = ConnectionObserver()
|
connection_observer = ConnectionObserver()
|
||||||
|
|
@ -30,64 +15,56 @@ profile_observer = ProfileObserver()
|
||||||
ticket_observer = TicketObserver()
|
ticket_observer = TicketObserver()
|
||||||
|
|
||||||
|
|
||||||
def observer_message(topic, event):
|
|
||||||
subject = getattr(event, 'subject', None)
|
|
||||||
meta = getattr(event, 'meta', None)
|
|
||||||
if subject is not None:
|
|
||||||
return str(subject)
|
|
||||||
if meta:
|
|
||||||
return str(meta)
|
|
||||||
return str(topic)
|
|
||||||
|
|
||||||
|
|
||||||
def subscribe_observer_messages(observer, post_status):
|
|
||||||
for attr in dir(observer):
|
|
||||||
if not attr.startswith('on_'):
|
|
||||||
continue
|
|
||||||
callbacks = getattr(observer, attr, None)
|
|
||||||
if not isinstance(callbacks, list):
|
|
||||||
continue
|
|
||||||
topic = attr[3:]
|
|
||||||
observer.subscribe(
|
|
||||||
topic,
|
|
||||||
lambda event, topic=topic: post_status(observer_message(topic, event)))
|
|
||||||
|
|
||||||
|
|
||||||
def setup_observers(update_status):
|
def setup_observers(update_status):
|
||||||
relay = StatusRelay(update_status)
|
|
||||||
post_status = relay.post
|
|
||||||
|
|
||||||
profile_observer.subscribe(
|
profile_observer.subscribe(
|
||||||
'created', lambda event: post_status('Profile Created'))
|
'created', lambda event: update_status('Profile Created'))
|
||||||
profile_observer.subscribe(
|
profile_observer.subscribe(
|
||||||
'destroyed', lambda event: post_status('Profile destroyed'))
|
'destroyed', lambda event: update_status('Profile destroyed'))
|
||||||
|
|
||||||
subscribe_observer_messages(client_observer, post_status)
|
client_observer.subscribe(
|
||||||
|
'synchronizing', lambda event: update_status('Sync in progress...'))
|
||||||
|
client_observer.subscribe(
|
||||||
|
'synchronized', lambda event: update_status('Sync complete'))
|
||||||
|
|
||||||
application_version_observer.subscribe('downloading', lambda event: post_status(
|
client_observer.subscribe(
|
||||||
|
'updating', lambda event: update_status('Updating client...'))
|
||||||
|
client_observer.subscribe('update_progressing', lambda event: update_status(
|
||||||
|
f'Current progress: {event.meta.get('progress'):.2f}%'))
|
||||||
|
client_observer.subscribe('updated', lambda event: update_status(
|
||||||
|
'Restart client to apply update.'))
|
||||||
|
|
||||||
|
application_version_observer.subscribe('downloading', lambda event: update_status(
|
||||||
f'Downloading {ApplicationController.get(event.subject.application_code).name}'))
|
f'Downloading {ApplicationController.get(event.subject.application_code).name}'))
|
||||||
application_version_observer.subscribe('download_progressing', lambda event: post_status(
|
application_version_observer.subscribe('download_progressing', lambda event: update_status(
|
||||||
f'Downloading {ApplicationController.get(event.subject.application_code).name} {event.meta.get('progress'):.2f}%'))
|
f'Downloading {ApplicationController.get(event.subject.application_code).name} {event.meta.get('progress'):.2f}%'))
|
||||||
|
application_version_observer.subscribe('downloaded', lambda event: update_status(
|
||||||
application_version_observer.subscribe('downloaded', lambda event: post_status(
|
|
||||||
f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
|
f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
|
||||||
|
|
||||||
subscribe_observer_messages(connection_observer, post_status)
|
connection_observer.subscribe('connecting', lambda event: update_status(
|
||||||
|
f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...'))
|
||||||
|
|
||||||
ticket_observer.subscribe('connecting', lambda event: post_status('Connecting to ticket server...'))
|
connection_observer.subscribe('tor_bootstrapping', lambda event: update_status(
|
||||||
ticket_observer.subscribe('sync_done', lambda event: post_status('Ticket prices synced.'))
|
'Establishing Tor connection...'))
|
||||||
ticket_observer.subscribe('waiting', lambda event: post_status('Waiting for payment...'))
|
|
||||||
ticket_observer.subscribe('paid', lambda event: post_status('Payment received.'))
|
connection_observer.subscribe('tor_bootstrap_progressing', lambda event: update_status(
|
||||||
ticket_observer.subscribe('ticket_ready', lambda event: post_status('Ticket ready.'))
|
f'Bootstrapping Tor {event.meta.get('progress'):.2f}%'))
|
||||||
ticket_observer.subscribe('used', lambda event: post_status('Ticket used.'))
|
|
||||||
ticket_observer.subscribe('connection_error', lambda event: post_status('Ticket server connection error.'))
|
connection_observer.subscribe(
|
||||||
ticket_observer.subscribe('failed_output', lambda event: post_status(f'{event.subject if event.subject else ""}'))
|
'tor_bootstrapped', lambda event: update_status('Tor connection established.'))
|
||||||
ticket_observer.subscribe('failed_input', lambda event: post_status(f'{event.subject if event.subject else ""}'))
|
|
||||||
ticket_observer.subscribe('unknown_error', lambda event: post_status('Unknown ticket error.'))
|
ticket_observer.subscribe('connecting', lambda event: update_status('Connecting to ticket server...'))
|
||||||
ticket_observer.subscribe('error', lambda event: post_status(f'Ticket error: {event.subject if event.subject else ""}'))
|
ticket_observer.subscribe('sync_done', lambda event: update_status('Ticket prices synced.'))
|
||||||
|
ticket_observer.subscribe('waiting', lambda event: update_status('Waiting for payment...'))
|
||||||
|
ticket_observer.subscribe('paid', lambda event: update_status('Payment received.'))
|
||||||
|
ticket_observer.subscribe('ticket_ready', lambda event: update_status('Ticket ready.'))
|
||||||
|
ticket_observer.subscribe('used', lambda event: update_status('Ticket used.'))
|
||||||
|
ticket_observer.subscribe('connection_error', lambda event: update_status('Ticket server connection error.'))
|
||||||
|
ticket_observer.subscribe('failed_output', lambda event: update_status(f'{event.subject if event.subject else ""}'))
|
||||||
|
ticket_observer.subscribe('failed_input', lambda event: update_status(f'{event.subject if event.subject else ""}'))
|
||||||
|
ticket_observer.subscribe('unknown_error', lambda event: update_status('Unknown ticket error.'))
|
||||||
|
ticket_observer.subscribe('error', lambda event: update_status(f'Ticket error: {event.subject if event.subject else ""}'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status_relay': relay,
|
|
||||||
'application_version_observer': application_version_observer,
|
'application_version_observer': application_version_observer,
|
||||||
'client_observer': client_observer,
|
'client_observer': client_observer,
|
||||||
'connection_observer': connection_observer,
|
'connection_observer': connection_observer,
|
||||||
|
|
|
||||||
|
|
@ -13,22 +13,12 @@ class Page(QWidget):
|
||||||
self.btn_path = custom_window.btn_path
|
self.btn_path = custom_window.btn_path
|
||||||
self.name = name
|
self.name = name
|
||||||
self.page_stack = page_stack
|
self.page_stack = page_stack
|
||||||
self._prepared = None
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.selected_profiles = []
|
self.selected_profiles = []
|
||||||
self.selected_wireguard = []
|
self.selected_wireguard = []
|
||||||
self.selected_residential = []
|
self.selected_residential = []
|
||||||
self.buttons = []
|
self.buttons = []
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prepare_data(custom_window):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _prepared_value(self, key, loader):
|
|
||||||
if self._prepared is not None and key in self._prepared:
|
|
||||||
return self._prepared[key]
|
|
||||||
return loader()
|
|
||||||
|
|
||||||
def add_selected_profile(self, profile):
|
def add_selected_profile(self, profile):
|
||||||
self.selected_profiles.clear()
|
self.selected_profiles.clear()
|
||||||
self.selected_wireguard.clear()
|
self.selected_wireguard.clear()
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@ from gui.v2.ui.pages.browser_page import BrowserPage
|
||||||
from gui.v2.ui.pages.location_page import LocationPage
|
from gui.v2.ui.pages.location_page import LocationPage
|
||||||
from gui.v2.ui.pages.screen_page import ScreenPage
|
from gui.v2.ui.pages.screen_page import ScreenPage
|
||||||
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
||||||
from gui.v2.workers.page_data_worker import PageDataWorker
|
|
||||||
|
|
||||||
|
|
||||||
class EditorPage(Page):
|
class EditorPage(Page):
|
||||||
def __init__(self, page_stack, main_window, prepared=None):
|
def __init__(self, page_stack, main_window):
|
||||||
super().__init__("Editor", page_stack, main_window)
|
super().__init__("Editor", page_stack, main_window)
|
||||||
self.page_stack = page_stack
|
self.page_stack = page_stack
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
|
|
@ -81,13 +80,6 @@ class EditorPage(Page):
|
||||||
self.brow_disp.hide()
|
self.brow_disp.hide()
|
||||||
self.brow_disp.lower()
|
self.brow_disp.lower()
|
||||||
|
|
||||||
self._prefetched_profiles = prepared
|
|
||||||
self._editor_workers = set()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prepare_data(custom_window):
|
|
||||||
return ProfileController.get_all()
|
|
||||||
|
|
||||||
def _selected_profiles_str(self):
|
def _selected_profiles_str(self):
|
||||||
menu_page = self.find_menu_page()
|
menu_page = self.find_menu_page()
|
||||||
if menu_page is not None and menu_page.selected_profiles:
|
if menu_page is not None and menu_page.selected_profiles:
|
||||||
|
|
@ -125,42 +117,7 @@ class EditorPage(Page):
|
||||||
def showEvent(self, event):
|
def showEvent(self, event):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
self.res_hint_shown = False
|
self.res_hint_shown = False
|
||||||
if self._selected_profile_in_cache():
|
|
||||||
self.extraccion()
|
self.extraccion()
|
||||||
return
|
|
||||||
worker = PageDataWorker("editor", EditorPage.prepare_data, self.custom_window)
|
|
||||||
worker.data_ready.connect(
|
|
||||||
lambda name, profiles: self._on_editor_data_ready(profiles))
|
|
||||||
worker.failed.connect(
|
|
||||||
lambda name, error: self._on_editor_data_failed(error))
|
|
||||||
worker.finished.connect(
|
|
||||||
lambda w=worker: self._cleanup_editor_worker(w))
|
|
||||||
self._editor_workers.add(worker)
|
|
||||||
worker.start()
|
|
||||||
|
|
||||||
def _on_editor_data_ready(self, profiles):
|
|
||||||
self._prefetched_profiles = profiles
|
|
||||||
self.extraccion()
|
|
||||||
|
|
||||||
def _on_editor_data_failed(self, error):
|
|
||||||
self.update_status.update_status(f"Editor data load failed: {error}")
|
|
||||||
self.extraccion()
|
|
||||||
|
|
||||||
def _cleanup_editor_worker(self, worker):
|
|
||||||
self._editor_workers.discard(worker)
|
|
||||||
worker.deleteLater()
|
|
||||||
|
|
||||||
def _selected_profile_in_cache(self):
|
|
||||||
if not self._prefetched_profiles:
|
|
||||||
return False
|
|
||||||
selected = self._selected_profiles_str()
|
|
||||||
if not selected:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
profile_id = int(selected.split('_')[1])
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
return False
|
|
||||||
return profile_id in self._prefetched_profiles
|
|
||||||
|
|
||||||
def extraccion(self):
|
def extraccion(self):
|
||||||
self.data_profile = {}
|
self.data_profile = {}
|
||||||
|
|
@ -174,9 +131,6 @@ class EditorPage(Page):
|
||||||
selected_profiles_str = self._selected_profiles_str()
|
selected_profiles_str = self._selected_profiles_str()
|
||||||
menu_page = self.find_menu_page()
|
menu_page = self.find_menu_page()
|
||||||
if menu_page:
|
if menu_page:
|
||||||
if self._prefetched_profiles is not None:
|
|
||||||
new_profiles = self._prefetched_profiles
|
|
||||||
else:
|
|
||||||
new_profiles = ProfileController.get_all()
|
new_profiles = ProfileController.get_all()
|
||||||
self.profiles_data = menu_page.match_core_profiles(
|
self.profiles_data = menu_page.match_core_profiles(
|
||||||
profiles_dict=new_profiles)
|
profiles_dict=new_profiles)
|
||||||
|
|
@ -576,9 +530,11 @@ class EditorPage(Page):
|
||||||
|
|
||||||
def on_sync_complete_for_edit_profile(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
def on_sync_complete_for_edit_profile(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||||
if status:
|
if status:
|
||||||
|
self.update_status.update_status('Sync complete.')
|
||||||
self.extraccion()
|
self.extraccion()
|
||||||
else:
|
else:
|
||||||
self.connection_manager.set_synced(False)
|
self.update_status.update_status(
|
||||||
|
'Sync failed. Please try again later.')
|
||||||
|
|
||||||
def show_previous_value(self, key: str, index: int, parameters: dict) -> None:
|
def show_previous_value(self, key: str, index: int, parameters: dict) -> None:
|
||||||
if key == 'browser' or key == 'location':
|
if key == 'browser' or key == 'location':
|
||||||
|
|
@ -790,7 +746,6 @@ class EditorPage(Page):
|
||||||
|
|
||||||
self.temp_changes.pop(selected_profiles_str, None)
|
self.temp_changes.pop(selected_profiles_str, None)
|
||||||
self.original_values.pop(selected_profiles_str, None)
|
self.original_values.pop(selected_profiles_str, None)
|
||||||
self._prefetched_profiles = None
|
|
||||||
|
|
||||||
def update_core_profiles(self, key, new_value):
|
def update_core_profiles(self, key, new_value):
|
||||||
profile = ProfileController.get(
|
profile = ProfileController.get(
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,11 @@ from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.pages.browser_page import BrowserPage
|
from gui.v2.ui.pages.browser_page import BrowserPage
|
||||||
from gui.v2.ui.pages.location_page import LocationPage
|
from gui.v2.ui.pages.location_page import LocationPage
|
||||||
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
||||||
from gui.v2.workers.page_data_worker import PageDataWorker
|
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
||||||
|
|
||||||
class FastRegistrationPage(Page):
|
class FastRegistrationPage(Page):
|
||||||
def __init__(self, page_stack, main_window, prepared=None):
|
def __init__(self, page_stack, main_window):
|
||||||
super().__init__("FastRegistration", page_stack, main_window)
|
super().__init__("FastRegistration", page_stack, main_window)
|
||||||
self.page_stack = page_stack
|
self.page_stack = page_stack
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
|
|
@ -74,15 +73,9 @@ class FastRegistrationPage(Page):
|
||||||
'resolution': '1024x760'
|
'resolution': '1024x760'
|
||||||
}
|
}
|
||||||
self.res_index = 1
|
self.res_index = 1
|
||||||
self._prefetched_profiles = prepared
|
|
||||||
self._fast_reg_workers = set()
|
|
||||||
|
|
||||||
self.initialize_default_selections()
|
self.initialize_default_selections()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prepare_data(custom_window):
|
|
||||||
return ProfileController.get_all()
|
|
||||||
|
|
||||||
def initialize_default_selections(self):
|
def initialize_default_selections(self):
|
||||||
if not self.selected_values['location']:
|
if not self.selected_values['location']:
|
||||||
locations = self.connection_manager.get_location_list()
|
locations = self.connection_manager.get_location_list()
|
||||||
|
|
@ -114,24 +107,6 @@ class FastRegistrationPage(Page):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
self.initialize_default_selections()
|
self.initialize_default_selections()
|
||||||
self.create_interface_elements()
|
self.create_interface_elements()
|
||||||
self._start_profiles_prefetch()
|
|
||||||
|
|
||||||
def _start_profiles_prefetch(self):
|
|
||||||
worker = PageDataWorker(
|
|
||||||
"fast_registration", FastRegistrationPage.prepare_data, self.custom_window)
|
|
||||||
worker.data_ready.connect(
|
|
||||||
lambda name, profiles: self._on_profiles_prefetched(profiles))
|
|
||||||
worker.finished.connect(
|
|
||||||
lambda w=worker: self._cleanup_fast_reg_worker(w))
|
|
||||||
self._fast_reg_workers.add(worker)
|
|
||||||
worker.start()
|
|
||||||
|
|
||||||
def _on_profiles_prefetched(self, profiles):
|
|
||||||
self._prefetched_profiles = profiles
|
|
||||||
|
|
||||||
def _cleanup_fast_reg_worker(self, worker):
|
|
||||||
self._fast_reg_workers.discard(worker)
|
|
||||||
worker.deleteLater()
|
|
||||||
|
|
||||||
def create_interface_elements(self):
|
def create_interface_elements(self):
|
||||||
for label in self.labels:
|
for label in self.labels:
|
||||||
|
|
@ -753,7 +728,7 @@ class FastRegistrationPage(Page):
|
||||||
self.update_status.update_status('Invalid location selected')
|
self.update_status.update_status('Invalid location selected')
|
||||||
return
|
return
|
||||||
|
|
||||||
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
|
profiles = ProfileController.get_all()
|
||||||
profile_id = self.get_next_available_profile_id(profiles)
|
profile_id = self.get_next_available_profile_id(profiles)
|
||||||
profile_data_for_resume = {
|
profile_data_for_resume = {
|
||||||
'id': profile_id,
|
'id': profile_id,
|
||||||
|
|
@ -787,7 +762,7 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system'
|
connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system'
|
||||||
|
|
||||||
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
|
profiles = ProfileController.get_all()
|
||||||
profile_id = self.get_next_available_profile_id(profiles)
|
profile_id = self.get_next_available_profile_id(profiles)
|
||||||
profile_data_for_resume = {
|
profile_data_for_resume = {
|
||||||
'id': profile_id,
|
'id': profile_id,
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QButtonGroup, QFrame, QLabel, QMessageBox, QPushButton, QTextEdit,
|
QButtonGroup, QFrame, QLabel, QPushButton, QTextEdit,
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QIcon, QPixmap
|
from PyQt6.QtGui import QIcon, QPixmap
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6 import QtCore
|
from PyQt6 import QtCore
|
||||||
|
|
||||||
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
|
|
||||||
|
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
|
||||||
|
|
||||||
|
|
||||||
HOW_MANY_PROFILES_DEFAULT = 6
|
HOW_MANY_PROFILES_DEFAULT = 6
|
||||||
|
|
@ -153,44 +150,11 @@ class IdPage(Page):
|
||||||
self.custom_window.navigator.navigate("duration_selection")
|
self.custom_window.navigator.navigate("duration_selection")
|
||||||
|
|
||||||
def go_multiple_profiles(self):
|
def go_multiple_profiles(self):
|
||||||
billing_id = do_we_have_billing_id()
|
|
||||||
if billing_id:
|
|
||||||
self._prompt_existing_tickets(billing_id)
|
|
||||||
else:
|
|
||||||
self._continue_multiple_flow()
|
|
||||||
|
|
||||||
def _continue_multiple_flow(self):
|
|
||||||
self.custom_window.navigator.navigate("plan_picker")
|
self.custom_window.navigator.navigate("plan_picker")
|
||||||
plan_page = self.custom_window.navigator.get_cached("plan_picker")
|
plan_page = self.custom_window.navigator.get_cached("plan_picker")
|
||||||
if plan_page is not None:
|
if plan_page is not None:
|
||||||
plan_page.start_sync()
|
plan_page.start_sync()
|
||||||
|
|
||||||
def _prompt_existing_tickets(self, billing_id):
|
|
||||||
box = QMessageBox(self)
|
|
||||||
box.setWindowTitle("Existing ticket purchase found")
|
|
||||||
box.setText(
|
|
||||||
"You already have a pending ticket purchase. Continue to check if it's paid, "
|
|
||||||
"or wipe it and start over?")
|
|
||||||
style_message_box(box)
|
|
||||||
check_button = box.addButton(
|
|
||||||
"Check payment", QMessageBox.ButtonRole.AcceptRole)
|
|
||||||
mark_confirm_button(check_button)
|
|
||||||
wipe_button = box.addButton(
|
|
||||||
"Wipe and start over", QMessageBox.ButtonRole.DestructiveRole)
|
|
||||||
box.exec()
|
|
||||||
clicked = box.clickedButton()
|
|
||||||
if clicked == check_button:
|
|
||||||
self._resume_existing_ticket(billing_id)
|
|
||||||
elif clicked == wipe_button:
|
|
||||||
self._continue_multiple_flow()
|
|
||||||
|
|
||||||
def _resume_existing_ticket(self, billing_id):
|
|
||||||
self.update_status.update_status("Checking existing ticket payment...")
|
|
||||||
self.custom_window.navigator.navigate("payment_details")
|
|
||||||
payment_page = self.custom_window.navigator.get_cached("payment_details")
|
|
||||||
if payment_page is not None:
|
|
||||||
payment_page.resume_ticket_billing(billing_id)
|
|
||||||
|
|
||||||
def toggle_button_state(self):
|
def toggle_button_state(self):
|
||||||
text = self.text_edit.toPlainText()
|
text = self.text_edit.toPlainText()
|
||||||
if text.strip():
|
if text.strip():
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ class LocationPage(Page):
|
||||||
self.verification_button.setEnabled(False)
|
self.verification_button.setEnabled(False)
|
||||||
|
|
||||||
def create_interface_elements(self, available_locations):
|
def create_interface_elements(self, available_locations):
|
||||||
|
|
||||||
self.buttonGroup = QButtonGroup(self)
|
self.buttonGroup = QButtonGroup(self)
|
||||||
self.buttons = []
|
self.buttons = []
|
||||||
|
|
||||||
|
|
@ -85,15 +84,8 @@ class LocationPage(Page):
|
||||||
boton.setIcon(QIcon(icon_path))
|
boton.setIcon(QIcon(icon_path))
|
||||||
fallback_path = os.path.join(
|
fallback_path = os.path.join(
|
||||||
self.btn_path, "default_location_button.png")
|
self.btn_path, "default_location_button.png")
|
||||||
|
|
||||||
if locations:
|
|
||||||
try:
|
|
||||||
provider = locations.operator.name if locations and hasattr(
|
provider = locations.operator.name if locations and hasattr(
|
||||||
locations, 'operator') else None
|
locations, 'operator') else None
|
||||||
except:
|
|
||||||
print("Assign none")
|
|
||||||
provider = None
|
|
||||||
|
|
||||||
if boton.icon().isNull():
|
if boton.icon().isNull():
|
||||||
if locations and hasattr(locations, 'country_name'):
|
if locations and hasattr(locations, 'country_name'):
|
||||||
location_name = locations.country_name
|
location_name = locations.country_name
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ from core.models.system.SystemProfile import SystemProfile
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||||
from gui.v2.actions.profile_order import normalize_profile_order
|
from gui.v2.actions.profile_order import normalize_profile_order
|
||||||
from gui.v2.actions.sync_result import is_valid_sync_payload
|
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
||||||
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
|
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
|
||||||
|
|
@ -214,26 +213,12 @@ class MenuPage(Page):
|
||||||
profiles_dict.keys())
|
profiles_dict.keys())
|
||||||
|
|
||||||
for idx in profile_ids:
|
for idx in profile_ids:
|
||||||
try:
|
|
||||||
profile = profiles_dict[idx]
|
profile = profiles_dict[idx]
|
||||||
new_profile = {}
|
new_profile = {}
|
||||||
|
|
||||||
protocol = profile.connection.code
|
protocol = profile.connection.code
|
||||||
|
|
||||||
# print(f"DEBUG: the type is = {type(profile.location)}, value = {profile.location}")
|
|
||||||
|
|
||||||
if isinstance(profile.location, dict):
|
|
||||||
# Fake/missing data — show placeholders from data,
|
|
||||||
country_code = profile.location.get("country_code", "na")
|
|
||||||
code = profile.location.get("code", "na")
|
|
||||||
location = f'{country_code}_{code}'
|
|
||||||
else:
|
|
||||||
# Proper Location object
|
|
||||||
location = f'{profile.location.country_code}_{profile.location.code}'
|
location = f'{profile.location.country_code}_{profile.location.code}'
|
||||||
except:
|
|
||||||
import sys
|
|
||||||
print(f"idx: {idx}")
|
|
||||||
sys.exit("STOPPING TO READ PRINTS FOR IDX. this failed on getting the location data inside match_core_profiles")
|
|
||||||
|
|
||||||
new_profile['location'] = location
|
new_profile['location'] = location
|
||||||
|
|
||||||
|
|
@ -270,7 +255,6 @@ class MenuPage(Page):
|
||||||
|
|
||||||
new_profile['name'] = profile.name
|
new_profile['name'] = profile.name
|
||||||
|
|
||||||
|
|
||||||
new_dict[f'Profile_{idx}'] = new_profile
|
new_dict[f'Profile_{idx}'] = new_profile
|
||||||
|
|
||||||
return new_dict
|
return new_dict
|
||||||
|
|
@ -1009,14 +993,9 @@ class MenuPage(Page):
|
||||||
if fast_mode:
|
if fast_mode:
|
||||||
if not self.connection_manager.is_synced():
|
if not self.connection_manager.is_synced():
|
||||||
self.update_status.update_status('Syncing in progress..')
|
self.update_status.update_status('Syncing in progress..')
|
||||||
self.fast_registration_sync_worker = self._build_sync_worker()
|
self.update_status.sync()
|
||||||
self.fast_registration_sync_worker.text_output.connect(
|
self.update_status.worker_thread.sync_output.connect(
|
||||||
self.update_status.update_status)
|
|
||||||
self.fast_registration_sync_worker.sync_output.connect(
|
|
||||||
self.update_status.update_values)
|
|
||||||
self.fast_registration_sync_worker.sync_output.connect(
|
|
||||||
self.on_sync_complete_for_fast_registration)
|
self.on_sync_complete_for_fast_registration)
|
||||||
self.fast_registration_sync_worker.start()
|
|
||||||
else:
|
else:
|
||||||
self.custom_window.navigator.navigate("fast_registration")
|
self.custom_window.navigator.navigate("fast_registration")
|
||||||
return
|
return
|
||||||
|
|
@ -1027,15 +1006,11 @@ class MenuPage(Page):
|
||||||
self.custom_window.navigator.navigate("protocol")
|
self.custom_window.navigator.navigate("protocol")
|
||||||
|
|
||||||
def on_sync_complete_for_fast_registration(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
def on_sync_complete_for_fast_registration(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||||
if is_valid_sync_payload(available_locations, available_browsers, status, locations, all_browsers):
|
if status:
|
||||||
self.custom_window.navigator.navigate("fast_registration")
|
self.custom_window.navigator.navigate("fast_registration")
|
||||||
else:
|
else:
|
||||||
self.connection_manager.set_synced(False)
|
self.update_status.update_status(
|
||||||
|
'Sync failed. Please try again later.')
|
||||||
def _build_sync_worker(self):
|
|
||||||
if self.update_status.get_current_connection() == 'tor':
|
|
||||||
return WorkerThread('SYNC_TOR')
|
|
||||||
return WorkerThread('SYNC')
|
|
||||||
|
|
||||||
def change_connect_button(self):
|
def change_connect_button(self):
|
||||||
profile = ProfileController.get(int(self.reverse_id))
|
profile = ProfileController.get(int(self.reverse_id))
|
||||||
|
|
@ -1077,7 +1052,8 @@ class MenuPage(Page):
|
||||||
if status:
|
if status:
|
||||||
self.custom_window.navigator.navigate("editor")
|
self.custom_window.navigator.navigate("editor")
|
||||||
else:
|
else:
|
||||||
self.connection_manager.set_synced(False)
|
self.update_status.update_status(
|
||||||
|
'Sync failed. Please try again later.')
|
||||||
|
|
||||||
def settings_gui(self):
|
def settings_gui(self):
|
||||||
self.custom_window.navigator.navigate("settings")
|
self.custom_window.navigator.navigate("settings")
|
||||||
|
|
@ -1172,11 +1148,9 @@ class MenuPage(Page):
|
||||||
self.update_status.update_status("Syncing...")
|
self.update_status.update_status("Syncing...")
|
||||||
self.worker_thread = WorkerThread('SYNC')
|
self.worker_thread = WorkerThread('SYNC')
|
||||||
self.worker_thread.finished.connect(
|
self.worker_thread.finished.connect(
|
||||||
lambda ok: self.handle_sync_after_verification(profile_id) if ok else None)
|
lambda: self.handle_sync_after_verification(profile_id))
|
||||||
self.worker_thread.sync_output.connect(
|
self.worker_thread.sync_output.connect(
|
||||||
self.update_status.update_values)
|
self.update_status.update_values)
|
||||||
self.worker_thread.text_output.connect(
|
|
||||||
self.update_status.update_status)
|
|
||||||
self.worker_thread.start()
|
self.worker_thread.start()
|
||||||
else:
|
else:
|
||||||
self.update_status.update_status("Profile enable aborted")
|
self.update_status.update_status("Profile enable aborted")
|
||||||
|
|
|
||||||
|
|
@ -343,16 +343,6 @@ class PaymentDetailsPage(Page):
|
||||||
self.ticket_poll_timer.start(3000)
|
self.ticket_poll_timer.start(3000)
|
||||||
self.update_status.update_status('Awaiting ticket payment...')
|
self.update_status.update_status('Awaiting ticket payment...')
|
||||||
|
|
||||||
def resume_ticket_billing(self, temp_billing_code):
|
|
||||||
self.ticketing = True
|
|
||||||
self.temp_billing_code = temp_billing_code
|
|
||||||
self.selected_currency = None
|
|
||||||
self.text_fields[0].setText(str(temp_billing_code or ''))
|
|
||||||
if self.ticket_poll_timer.isActive():
|
|
||||||
self.ticket_poll_timer.stop()
|
|
||||||
self.ticket_poll_timer.start(3000)
|
|
||||||
self.update_status.update_status('Checking existing ticket payment...')
|
|
||||||
|
|
||||||
def _populate_ticket_fields(self, invoice):
|
def _populate_ticket_fields(self, invoice):
|
||||||
self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or ''))
|
self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or ''))
|
||||||
self.text_fields[1].setText(self.selected_duration)
|
self.text_fields[1].setText(self.selected_duration)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
|
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
|
||||||
|
|
@ -32,7 +34,6 @@ from core.Errors import (
|
||||||
)
|
)
|
||||||
from core.errors.logger import logger as core_logger
|
from core.errors.logger import logger as core_logger
|
||||||
|
|
||||||
from gui.v2.actions import settings_data
|
|
||||||
from gui.v2.actions.key_interpretation import interpret_key_results
|
from gui.v2.actions.key_interpretation import interpret_key_results
|
||||||
from gui.v2.actions.profile_order import normalize_profile_order
|
from gui.v2.actions.profile_order import normalize_profile_order
|
||||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||||
|
|
@ -42,25 +43,20 @@ from gui.v2.ui.styles.styles import (
|
||||||
SCROLLBAR_CYAN_QSS,
|
SCROLLBAR_CYAN_QSS,
|
||||||
TERMINAL_LIST_QSS,
|
TERMINAL_LIST_QSS,
|
||||||
checkbox_style,
|
checkbox_style,
|
||||||
combobox_style,
|
|
||||||
)
|
)
|
||||||
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
|
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
|
||||||
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
||||||
from gui.v2.workers.page_data_worker import PageDataWorker
|
|
||||||
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
||||||
|
|
||||||
class Settings(Page):
|
class Settings(Page):
|
||||||
def __init__(self, page_stack, main_window, parent=None, prepared=None):
|
def __init__(self, page_stack, main_window, parent=None):
|
||||||
super().__init__("Settings", page_stack, main_window, parent)
|
super().__init__("Settings", page_stack, main_window, parent)
|
||||||
self.font_style = f"font-family: '{self.custom_window.open_sans_family}';"
|
self.font_style = f"font-family: '{self.custom_window.open_sans_family}';"
|
||||||
|
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.update_logging = main_window
|
self.update_logging = main_window
|
||||||
self._prepared = prepared if prepared is not None else settings_data.empty_payload()
|
|
||||||
self._settings_refresh_seq = 0
|
|
||||||
self._settings_workers = set()
|
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.button_reverse.setEnabled(True)
|
self.button_reverse.setEnabled(True)
|
||||||
self.button_reverse.clicked.connect(self.reverse)
|
self.button_reverse.clicked.connect(self.reverse)
|
||||||
|
|
@ -68,10 +64,6 @@ class Settings(Page):
|
||||||
self.title.setText("Settings")
|
self.title.setText("Settings")
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prepare_data(custom_window):
|
|
||||||
return settings_data.prepare(custom_window.gui_config_file)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
main_container = QWidget(self)
|
main_container = QWidget(self)
|
||||||
main_container.setGeometry(0, 0, 800, 520)
|
main_container.setGeometry(0, 0, 800, 520)
|
||||||
|
|
@ -102,6 +94,7 @@ class Settings(Page):
|
||||||
("Create/Edit", self.show_registrations_page),
|
("Create/Edit", self.show_registrations_page),
|
||||||
("Verification", self.show_verification_page),
|
("Verification", self.show_verification_page),
|
||||||
("Legacy-Version", self.show_systemwide_page),
|
("Legacy-Version", self.show_systemwide_page),
|
||||||
|
("Bwrap Permission", self.show_bwrap_page),
|
||||||
("Delete Profile", self.show_delete_page),
|
("Delete Profile", self.show_delete_page),
|
||||||
("Error Logs", self.show_logs_page),
|
("Error Logs", self.show_logs_page),
|
||||||
("Debug Help", self.show_debug_page)
|
("Debug Help", self.show_debug_page)
|
||||||
|
|
@ -205,13 +198,11 @@ class Settings(Page):
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def create_delete_profile_buttons(self):
|
def create_delete_profile_buttons(self):
|
||||||
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
profiles = ProfileController.get_all()
|
||||||
|
|
||||||
profile_ids = self._prepared_value(
|
profile_ids = normalize_profile_order(
|
||||||
"profile_order",
|
|
||||||
lambda: normalize_profile_order(
|
|
||||||
getattr(self.update_status, 'gui_config_file', None),
|
getattr(self.update_status, 'gui_config_file', None),
|
||||||
profiles.keys()))
|
profiles.keys())
|
||||||
|
|
||||||
for index, profile_id in enumerate(profile_ids):
|
for index, profile_id in enumerate(profile_ids):
|
||||||
profile = profiles[profile_id]
|
profile = profiles[profile_id]
|
||||||
|
|
@ -300,7 +291,7 @@ class Settings(Page):
|
||||||
profile_selection_layout.addWidget(profile_label)
|
profile_selection_layout.addWidget(profile_label)
|
||||||
|
|
||||||
self.debug_profile_selector = QComboBox()
|
self.debug_profile_selector = QComboBox()
|
||||||
self.debug_profile_selector.setStyleSheet(combobox_style(self.font_style))
|
self.debug_profile_selector.setStyleSheet(self.get_combobox_style())
|
||||||
self.debug_profile_selector.currentTextChanged.connect(
|
self.debug_profile_selector.currentTextChanged.connect(
|
||||||
self.on_debug_profile_selected)
|
self.on_debug_profile_selected)
|
||||||
profile_selection_layout.addWidget(self.debug_profile_selector)
|
profile_selection_layout.addWidget(self.debug_profile_selector)
|
||||||
|
|
@ -609,7 +600,7 @@ class Settings(Page):
|
||||||
|
|
||||||
def update_debug_profile_list(self):
|
def update_debug_profile_list(self):
|
||||||
self.debug_profile_selector.clear()
|
self.debug_profile_selector.clear()
|
||||||
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
profiles = ProfileController.get_all()
|
||||||
system_profiles = {pid: profile for pid, profile in profiles.items(
|
system_profiles = {pid: profile for pid, profile in profiles.items(
|
||||||
) if isinstance(profile, SystemProfile)}
|
) if isinstance(profile, SystemProfile)}
|
||||||
|
|
||||||
|
|
@ -646,7 +637,7 @@ class Settings(Page):
|
||||||
self.cli_command.setText(
|
self.cli_command.setText(
|
||||||
f"'{app_path}' --cli profile enable -i {profile_id}")
|
f"'{app_path}' --cli profile enable -i {profile_id}")
|
||||||
self.cli_copy_button.setEnabled(True)
|
self.cli_copy_button.setEnabled(True)
|
||||||
ip_address = settings_data.extract_endpoint_ip(profile)
|
ip_address = self.extract_endpoint_ip(profile)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
self.ping_instruction_label.setText(
|
self.ping_instruction_label.setText(
|
||||||
f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}")
|
f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}")
|
||||||
|
|
@ -678,6 +669,25 @@ class Settings(Page):
|
||||||
self.wg_quick_up_button.setEnabled(False)
|
self.wg_quick_up_button.setEnabled(False)
|
||||||
self.wg_quick_down_button.setEnabled(False)
|
self.wg_quick_down_button.setEnabled(False)
|
||||||
|
|
||||||
|
def extract_endpoint_ip(self, profile):
|
||||||
|
try:
|
||||||
|
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}'
|
||||||
|
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
||||||
|
if not os.path.exists(wg_conf_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(wg_conf_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
for line in content.split('\n'):
|
||||||
|
if line.strip().startswith('Endpoint = '):
|
||||||
|
endpoint = line.strip().split(' = ')[1]
|
||||||
|
ip_address = endpoint.split(':')[0]
|
||||||
|
return ip_address
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def copy_ping_command(self):
|
def copy_ping_command(self):
|
||||||
profile_id = self.debug_profile_selector.currentData()
|
profile_id = self.debug_profile_selector.currentData()
|
||||||
if profile_id is None:
|
if profile_id is None:
|
||||||
|
|
@ -685,7 +695,7 @@ class Settings(Page):
|
||||||
|
|
||||||
profile = ProfileController.get(profile_id)
|
profile = ProfileController.get(profile_id)
|
||||||
if profile and isinstance(profile, SystemProfile):
|
if profile and isinstance(profile, SystemProfile):
|
||||||
ip_address = settings_data.extract_endpoint_ip(profile)
|
ip_address = self.extract_endpoint_ip(profile)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
ping_command = f"ping {ip_address}"
|
ping_command = f"ping {ip_address}"
|
||||||
clipboard = QApplication.clipboard()
|
clipboard = QApplication.clipboard()
|
||||||
|
|
@ -700,7 +710,7 @@ class Settings(Page):
|
||||||
|
|
||||||
profile = ProfileController.get(profile_id)
|
profile = ProfileController.get(profile_id)
|
||||||
if profile and isinstance(profile, SystemProfile):
|
if profile and isinstance(profile, SystemProfile):
|
||||||
ip_address = settings_data.extract_endpoint_ip(profile)
|
ip_address = self.extract_endpoint_ip(profile)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
self.test_ping_button.setEnabled(False)
|
self.test_ping_button.setEnabled(False)
|
||||||
self.ping_result_label.setText("Testing ping...")
|
self.ping_result_label.setText("Testing ping...")
|
||||||
|
|
@ -790,9 +800,75 @@ class Settings(Page):
|
||||||
self.content_layout.addWidget(self.delete_page)
|
self.content_layout.addWidget(self.delete_page)
|
||||||
self.content_layout.setCurrentWidget(self.delete_page)
|
self.content_layout.setCurrentWidget(self.delete_page)
|
||||||
|
|
||||||
|
def get_combobox_style(self) -> str:
|
||||||
|
return f"""
|
||||||
|
QComboBox {{
|
||||||
|
color: black;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 5px 30px 5px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 120px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
{self.font_style}
|
||||||
|
}}
|
||||||
|
QComboBox:disabled {{
|
||||||
|
color: #666;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}}
|
||||||
|
QComboBox::drop-down {{
|
||||||
|
border: none;
|
||||||
|
width: 30px;
|
||||||
|
}}
|
||||||
|
QComboBox::down-arrow {{
|
||||||
|
image: url(assets/down_arrow.png);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}}
|
||||||
|
QComboBox QAbstractItemView {{
|
||||||
|
color: black;
|
||||||
|
background: white;
|
||||||
|
selection-background-color: #007bff;
|
||||||
|
selection-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
{self.font_style}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
def get_checkbox_style(self) -> str:
|
def get_checkbox_style(self) -> str:
|
||||||
return checkbox_style(self.font_style)
|
return checkbox_style(self.font_style)
|
||||||
|
|
||||||
|
def populate_wireguard_profiles(self) -> None:
|
||||||
|
self.wireguard_profile_selector.clear()
|
||||||
|
profiles = ProfileController.get_all()
|
||||||
|
for profile_id, profile in profiles.items():
|
||||||
|
if profile.connection.code == 'wireguard':
|
||||||
|
|
||||||
|
self.wireguard_profile_selector.addItem(
|
||||||
|
f"Profile {profile_id}: {profile.name}", profile_id)
|
||||||
|
|
||||||
|
def convert_duration(self, value: Union[str, int], to_hours: bool = True) -> Union[str, int]:
|
||||||
|
if to_hours:
|
||||||
|
number, unit = value.split(' ')
|
||||||
|
number = int(number)
|
||||||
|
if unit in ['day', 'days']:
|
||||||
|
return number * 24
|
||||||
|
elif unit in ['week', 'weeks']:
|
||||||
|
return number * 7 * 24
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported duration unit: {unit}")
|
||||||
|
else:
|
||||||
|
hours = int(value)
|
||||||
|
if hours % (7 * 24) == 0:
|
||||||
|
weeks = hours // (7 * 24)
|
||||||
|
return f"{weeks} {'week' if weeks == 1 else 'weeks'}"
|
||||||
|
elif hours % 24 == 0:
|
||||||
|
days = hours // 24
|
||||||
|
return f"{days} {'day' if days == 1 else 'days'}"
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Hours value {hours} cannot be converted to days or weeks cleanly")
|
||||||
|
|
||||||
def reverse(self):
|
def reverse(self):
|
||||||
self.custom_window.navigator.navigate("menu")
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
||||||
|
|
@ -827,8 +903,8 @@ class Settings(Page):
|
||||||
profile_layout = QVBoxLayout(profile_group)
|
profile_layout = QVBoxLayout(profile_group)
|
||||||
|
|
||||||
self.profile_selector = QComboBox()
|
self.profile_selector = QComboBox()
|
||||||
self.profile_selector.setStyleSheet(combobox_style(self.font_style))
|
self.profile_selector.setStyleSheet(self.get_combobox_style())
|
||||||
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
profiles = ProfileController.get_all()
|
||||||
if profiles:
|
if profiles:
|
||||||
for profile_id, profile in profiles.items():
|
for profile_id, profile in profiles.items():
|
||||||
self.profile_selector.addItem(
|
self.profile_selector.addItem(
|
||||||
|
|
@ -962,8 +1038,8 @@ class Settings(Page):
|
||||||
|
|
||||||
self.verification_profile_selector = QComboBox()
|
self.verification_profile_selector = QComboBox()
|
||||||
self.verification_profile_selector.setStyleSheet(
|
self.verification_profile_selector.setStyleSheet(
|
||||||
combobox_style(self.font_style))
|
self.get_combobox_style())
|
||||||
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
profiles = ProfileController.get_all()
|
||||||
if profiles:
|
if profiles:
|
||||||
for profile_id, profile in profiles.items():
|
for profile_id, profile in profiles.items():
|
||||||
self.verification_profile_selector.addItem(
|
self.verification_profile_selector.addItem(
|
||||||
|
|
@ -1058,9 +1134,7 @@ class Settings(Page):
|
||||||
self.endpoint_verification_checkbox = QCheckBox(page)
|
self.endpoint_verification_checkbox = QCheckBox(page)
|
||||||
self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30)
|
self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30)
|
||||||
self.endpoint_verification_checkbox.setChecked(
|
self.endpoint_verification_checkbox.setChecked(
|
||||||
self._prepared_value(
|
ConfigurationController.get_endpoint_verification_enabled())
|
||||||
"endpoint_verification_enabled",
|
|
||||||
ConfigurationController.get_endpoint_verification_enabled))
|
|
||||||
self.endpoint_verification_checkbox.setStyleSheet(
|
self.endpoint_verification_checkbox.setStyleSheet(
|
||||||
self.get_checkbox_style())
|
self.get_checkbox_style())
|
||||||
self.endpoint_verification_checkbox.show()
|
self.endpoint_verification_checkbox.show()
|
||||||
|
|
@ -1081,30 +1155,72 @@ class Settings(Page):
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
def truncate_key(self, text, max_length=50):
|
||||||
|
if not text or text == "N/A" or len(text) <= max_length:
|
||||||
|
return text
|
||||||
|
start_len = max_length // 2 - 2
|
||||||
|
end_len = max_length // 2 - 2
|
||||||
|
return text[:start_len] + "....." + text[-end_len:]
|
||||||
|
|
||||||
def update_verification_info(self, index):
|
def update_verification_info(self, index):
|
||||||
if index < 0:
|
if index < 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
profile_id = self.verification_profile_selector.itemData(index)
|
profile_id = self.verification_profile_selector.itemData(index)
|
||||||
profile = None
|
if profile_id is None:
|
||||||
if profile_id is not None:
|
for key, widget in self.verification_info.items():
|
||||||
profile = ProfileController.get(profile_id)
|
widget.setText("N/A")
|
||||||
|
self.verification_full_values[key] = "N/A"
|
||||||
|
if key in self.verification_checkmarks:
|
||||||
|
self.verification_checkmarks[key].hide()
|
||||||
|
return
|
||||||
|
|
||||||
view = settings_data.build_verification_view(profile)
|
profile = ProfileController.get(profile_id)
|
||||||
truncated_keys = (
|
if not profile:
|
||||||
"nostr_public_key",
|
for key, widget in self.verification_info.items():
|
||||||
"hydraveil_public_key",
|
widget.setText("N/A")
|
||||||
"nostr_attestation_event_reference",
|
self.verification_full_values[key] = "N/A"
|
||||||
)
|
if key in self.verification_checkmarks:
|
||||||
|
self.verification_checkmarks[key].hide()
|
||||||
|
return
|
||||||
|
|
||||||
|
operator = None
|
||||||
|
if profile.location and profile.location.operator:
|
||||||
|
operator = profile.location.operator
|
||||||
|
|
||||||
|
if operator:
|
||||||
|
operator_name = operator.name or "N/A"
|
||||||
|
self.verification_full_values["operator_name"] = operator_name
|
||||||
|
self.verification_info["operator_name"].setText(operator_name)
|
||||||
|
|
||||||
|
nostr_key = operator.nostr_public_key or "N/A"
|
||||||
|
self.verification_full_values["nostr_public_key"] = nostr_key
|
||||||
|
self.verification_info["nostr_public_key"].setText(
|
||||||
|
self.truncate_key(nostr_key) if nostr_key != "N/A" else nostr_key)
|
||||||
|
|
||||||
|
hydraveil_key = operator.public_key or "N/A"
|
||||||
|
self.verification_full_values["hydraveil_public_key"] = hydraveil_key
|
||||||
|
self.verification_info["hydraveil_public_key"].setText(
|
||||||
|
self.truncate_key(hydraveil_key) if hydraveil_key != "N/A" else hydraveil_key)
|
||||||
|
|
||||||
|
nostr_verification = operator.nostr_attestation_event_reference or "N/A"
|
||||||
|
self.verification_full_values["nostr_attestation_event_reference"] = nostr_verification
|
||||||
|
self.verification_info["nostr_attestation_event_reference"].setText(self.truncate_key(
|
||||||
|
nostr_verification) if nostr_verification != "N/A" else nostr_verification)
|
||||||
|
else:
|
||||||
|
self.verification_info["operator_name"].setText("N/A")
|
||||||
|
self.verification_full_values["operator_name"] = "N/A"
|
||||||
|
self.verification_info["nostr_public_key"].setText("N/A")
|
||||||
|
self.verification_full_values["nostr_public_key"] = "N/A"
|
||||||
|
self.verification_info["hydraveil_public_key"].setText("N/A")
|
||||||
|
self.verification_full_values["hydraveil_public_key"] = "N/A"
|
||||||
|
self.verification_info["nostr_attestation_event_reference"].setText(
|
||||||
|
"N/A")
|
||||||
|
self.verification_full_values["nostr_attestation_event_reference"] = "N/A"
|
||||||
|
|
||||||
for key, widget in self.verification_info.items():
|
for key, widget in self.verification_info.items():
|
||||||
full_value = view.get(key, "N/A")
|
|
||||||
self.verification_full_values[key] = full_value
|
|
||||||
if key in truncated_keys and full_value != "N/A":
|
|
||||||
widget.setText(settings_data.truncate_key(full_value))
|
|
||||||
else:
|
|
||||||
widget.setText(full_value)
|
|
||||||
if key in self.verification_checkmarks:
|
if key in self.verification_checkmarks:
|
||||||
|
full_value = self.verification_full_values.get(key, "")
|
||||||
if full_value and full_value != "N/A":
|
if full_value and full_value != "N/A":
|
||||||
self.verification_checkmarks[key].show()
|
self.verification_checkmarks[key].show()
|
||||||
else:
|
else:
|
||||||
|
|
@ -1149,9 +1265,16 @@ class Settings(Page):
|
||||||
|
|
||||||
if profile and hasattr(profile, 'subscription') and profile.subscription:
|
if profile and hasattr(profile, 'subscription') and profile.subscription:
|
||||||
try:
|
try:
|
||||||
view = settings_data.build_subscription_view(profile)
|
self.subscription_info["billing_code"].setText(
|
||||||
self.subscription_info["billing_code"].setText(view["billing_code"])
|
str(profile.subscription.billing_code))
|
||||||
self.subscription_info["expires_at"].setText(view["expires_at"])
|
|
||||||
|
if hasattr(profile.subscription, 'expires_at') and profile.subscription.expires_at:
|
||||||
|
expires_at = profile.subscription.expires_at.strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
self.subscription_info["expires_at"].setText(expires_at)
|
||||||
|
else:
|
||||||
|
self.subscription_info["expires_at"].setText(
|
||||||
|
"Not available")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating subscription info: {e}")
|
print(f"Error updating subscription info: {e}")
|
||||||
else:
|
else:
|
||||||
|
|
@ -1172,30 +1295,6 @@ class Settings(Page):
|
||||||
|
|
||||||
def showEvent(self, event):
|
def showEvent(self, event):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
self._settings_refresh_seq += 1
|
|
||||||
seq = self._settings_refresh_seq
|
|
||||||
worker = PageDataWorker(
|
|
||||||
"settings", settings_data.prepare, self.custom_window.gui_config_file)
|
|
||||||
worker.data_ready.connect(
|
|
||||||
lambda name, payload, s=seq: self._on_settings_data_ready(payload, s))
|
|
||||||
worker.failed.connect(
|
|
||||||
lambda name, error: self._on_settings_data_failed(error))
|
|
||||||
worker.finished.connect(
|
|
||||||
lambda w=worker: self._cleanup_settings_worker(w))
|
|
||||||
self._settings_workers.add(worker)
|
|
||||||
worker.start()
|
|
||||||
|
|
||||||
def _on_settings_data_failed(self, error):
|
|
||||||
core_logger.warning(f"Settings data refresh failed: {error}")
|
|
||||||
|
|
||||||
def _cleanup_settings_worker(self, worker):
|
|
||||||
self._settings_workers.discard(worker)
|
|
||||||
worker.deleteLater()
|
|
||||||
|
|
||||||
def _on_settings_data_ready(self, payload, seq):
|
|
||||||
if seq != self._settings_refresh_seq:
|
|
||||||
return
|
|
||||||
self._prepared = payload
|
|
||||||
|
|
||||||
current_index = self.content_layout.currentIndex()
|
current_index = self.content_layout.currentIndex()
|
||||||
|
|
||||||
|
|
@ -1223,6 +1322,10 @@ class Settings(Page):
|
||||||
self.systemwide_page = self.create_systemwide_page()
|
self.systemwide_page = self.create_systemwide_page()
|
||||||
self.content_layout.addWidget(self.systemwide_page)
|
self.content_layout.addWidget(self.systemwide_page)
|
||||||
|
|
||||||
|
self.content_layout.removeWidget(self.bwrap_page)
|
||||||
|
self.bwrap_page = self.create_bwrap_page()
|
||||||
|
self.content_layout.addWidget(self.bwrap_page)
|
||||||
|
|
||||||
self.content_layout.removeWidget(self.delete_page)
|
self.content_layout.removeWidget(self.delete_page)
|
||||||
self.delete_page = self.create_delete_page()
|
self.delete_page = self.create_delete_page()
|
||||||
self.content_layout.addWidget(self.delete_page)
|
self.content_layout.addWidget(self.delete_page)
|
||||||
|
|
@ -1304,7 +1407,7 @@ class Settings(Page):
|
||||||
layout = QGridLayout(widget)
|
layout = QGridLayout(widget)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(10)
|
||||||
|
|
||||||
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
profiles = ProfileController.get_all()
|
||||||
total_profiles = len(profiles)
|
total_profiles = len(profiles)
|
||||||
session_profiles = sum(1 for p in profiles.values()
|
session_profiles = sum(1 for p in profiles.values()
|
||||||
if isinstance(p, SessionProfile))
|
if isinstance(p, SessionProfile))
|
||||||
|
|
@ -1336,8 +1439,7 @@ class Settings(Page):
|
||||||
|
|
||||||
config_path = Constants.HV_CONFIG_HOME
|
config_path = Constants.HV_CONFIG_HOME
|
||||||
|
|
||||||
current_connection = self._prepared_value(
|
current_connection = self.update_status.get_current_connection()
|
||||||
"current_connection", self.update_status.get_current_connection)
|
|
||||||
|
|
||||||
if current_connection is not None:
|
if current_connection is not None:
|
||||||
current_connection = current_connection.capitalize()
|
current_connection = current_connection.capitalize()
|
||||||
|
|
@ -1436,6 +1538,7 @@ class Settings(Page):
|
||||||
self.registrations_page = self.create_registrations_page()
|
self.registrations_page = self.create_registrations_page()
|
||||||
self.verification_page = self.create_verification_page()
|
self.verification_page = self.create_verification_page()
|
||||||
self.systemwide_page = self.create_systemwide_page()
|
self.systemwide_page = self.create_systemwide_page()
|
||||||
|
self.bwrap_page = self.create_bwrap_page()
|
||||||
self.logs_page = self.create_logs_page()
|
self.logs_page = self.create_logs_page()
|
||||||
self.delete_page = self.create_delete_page()
|
self.delete_page = self.create_delete_page()
|
||||||
self.debug_page = self.create_debug_page()
|
self.debug_page = self.create_debug_page()
|
||||||
|
|
@ -1446,6 +1549,7 @@ class Settings(Page):
|
||||||
self.content_layout.addWidget(self.registrations_page)
|
self.content_layout.addWidget(self.registrations_page)
|
||||||
self.content_layout.addWidget(self.verification_page)
|
self.content_layout.addWidget(self.verification_page)
|
||||||
self.content_layout.addWidget(self.systemwide_page)
|
self.content_layout.addWidget(self.systemwide_page)
|
||||||
|
self.content_layout.addWidget(self.bwrap_page)
|
||||||
self.content_layout.addWidget(self.logs_page)
|
self.content_layout.addWidget(self.logs_page)
|
||||||
self.content_layout.addWidget(self.delete_page)
|
self.content_layout.addWidget(self.delete_page)
|
||||||
self.content_layout.addWidget(self.debug_page)
|
self.content_layout.addWidget(self.debug_page)
|
||||||
|
|
@ -1499,6 +1603,11 @@ class Settings(Page):
|
||||||
self.content_layout.setCurrentWidget(self.systemwide_page)
|
self.content_layout.setCurrentWidget(self.systemwide_page)
|
||||||
self._select_menu_button("Legacy-Version")
|
self._select_menu_button("Legacy-Version")
|
||||||
|
|
||||||
|
def show_bwrap_page(self):
|
||||||
|
core_logger.info("User navigated to Settings -> Bwrap Permission")
|
||||||
|
self.content_layout.setCurrentWidget(self.bwrap_page)
|
||||||
|
self._select_menu_button("Bwrap Permission")
|
||||||
|
|
||||||
def show_logs_page(self):
|
def show_logs_page(self):
|
||||||
core_logger.info("User navigated to Settings -> Error Logs")
|
core_logger.info("User navigated to Settings -> Error Logs")
|
||||||
self.content_layout.setCurrentWidget(self.logs_page)
|
self.content_layout.setCurrentWidget(self.logs_page)
|
||||||
|
|
@ -1588,8 +1697,7 @@ class Settings(Page):
|
||||||
inventory_layout.addWidget(self.refresh_tickets_button)
|
inventory_layout.addWidget(self.refresh_tickets_button)
|
||||||
layout.addWidget(inventory_group)
|
layout.addWidget(inventory_group)
|
||||||
|
|
||||||
saved_failure = self._prepared_value(
|
saved_failure = self.update_status.get_ticket_verification_failure()
|
||||||
"ticket_failure", self.update_status.get_ticket_verification_failure)
|
|
||||||
has_failure = saved_failure is not None
|
has_failure = saved_failure is not None
|
||||||
|
|
||||||
recovery_group = QGroupBox("Verification Failure Recovery")
|
recovery_group = QGroupBox("Verification Failure Recovery")
|
||||||
|
|
@ -1597,7 +1705,7 @@ class Settings(Page):
|
||||||
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
||||||
recovery_layout = QVBoxLayout(recovery_group)
|
recovery_layout = QVBoxLayout(recovery_group)
|
||||||
|
|
||||||
self.ticket_recovery_status_label = QLabel(settings_data.format_ticket_failure_status(saved_failure))
|
self.ticket_recovery_status_label = QLabel(self._format_ticket_failure_status(saved_failure))
|
||||||
self.ticket_recovery_status_label.setStyleSheet(
|
self.ticket_recovery_status_label.setStyleSheet(
|
||||||
f"color: white; font-size: 12px; {self.font_style}")
|
f"color: white; font-size: 12px; {self.font_style}")
|
||||||
self.ticket_recovery_status_label.setWordWrap(True)
|
self.ticket_recovery_status_label.setWordWrap(True)
|
||||||
|
|
@ -1651,11 +1759,23 @@ class Settings(Page):
|
||||||
page_layout.addWidget(scroll_area)
|
page_layout.addWidget(scroll_area)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
def _format_ticket_failure_status(self, failure):
|
||||||
|
if not failure:
|
||||||
|
return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here."
|
||||||
|
failed_validations = failure.get("failed_validations", [])
|
||||||
|
how_many_failed = failure.get("how_many_failed", len(failed_validations))
|
||||||
|
updated_at = failure.get("updated_at", "unknown time")
|
||||||
|
failed_text = ", ".join(str(item) for item in failed_validations)
|
||||||
|
return (
|
||||||
|
f"Saved verification failure: {how_many_failed} failed. "
|
||||||
|
f"Failed validation indices: {failed_text}. Saved: {updated_at}"
|
||||||
|
)
|
||||||
|
|
||||||
def _refresh_ticket_recovery_controls(self):
|
def _refresh_ticket_recovery_controls(self):
|
||||||
failure = self.update_status.get_ticket_verification_failure()
|
failure = self.update_status.get_ticket_verification_failure()
|
||||||
has_failure = failure is not None
|
has_failure = failure is not None
|
||||||
if hasattr(self, 'ticket_recovery_status_label'):
|
if hasattr(self, 'ticket_recovery_status_label'):
|
||||||
self.ticket_recovery_status_label.setText(settings_data.format_ticket_failure_status(failure))
|
self.ticket_recovery_status_label.setText(self._format_ticket_failure_status(failure))
|
||||||
if hasattr(self, 'evaluate_public_key_button'):
|
if hasattr(self, 'evaluate_public_key_button'):
|
||||||
self.evaluate_public_key_button.setEnabled(has_failure)
|
self.evaluate_public_key_button.setEnabled(has_failure)
|
||||||
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
||||||
|
|
@ -1671,6 +1791,13 @@ class Settings(Page):
|
||||||
if hasattr(self, 'ticket_recovery_output'):
|
if hasattr(self, 'ticket_recovery_output'):
|
||||||
self.ticket_recovery_output.setPlainText(text)
|
self.ticket_recovery_output.setPlainText(text)
|
||||||
|
|
||||||
|
def _format_ticket_recovery_result(self, label, result):
|
||||||
|
try:
|
||||||
|
payload = json.dumps(result, indent=2, default=str)
|
||||||
|
except TypeError:
|
||||||
|
payload = str(result)
|
||||||
|
return f"{label}:\n{payload}"
|
||||||
|
|
||||||
def _get_ticket_failure_for_action(self):
|
def _get_ticket_failure_for_action(self):
|
||||||
failure = self.update_status.get_ticket_verification_failure()
|
failure = self.update_status.get_ticket_verification_failure()
|
||||||
if failure is None:
|
if failure is None:
|
||||||
|
|
@ -1732,7 +1859,7 @@ class Settings(Page):
|
||||||
self.ticket_recovery_worker.start()
|
self.ticket_recovery_worker.start()
|
||||||
|
|
||||||
def on_saved_blind_prep_done(self, result):
|
def on_saved_blind_prep_done(self, result):
|
||||||
self._write_ticket_recovery_output(settings_data.format_ticket_recovery_result("Results of preparation", result))
|
self._write_ticket_recovery_output(self._format_ticket_recovery_result("Results of preparation", result))
|
||||||
if isinstance(result, dict) and result.get('valid') is True:
|
if isinstance(result, dict) and result.get('valid') is True:
|
||||||
self.update_status.clear_ticket_verification_failure()
|
self.update_status.clear_ticket_verification_failure()
|
||||||
self.update_status.update_status("Tickets prepared from saved blind signatures.")
|
self.update_status.update_status("Tickets prepared from saved blind signatures.")
|
||||||
|
|
@ -1827,8 +1954,7 @@ class Settings(Page):
|
||||||
|
|
||||||
def load_logs_settings(self) -> None:
|
def load_logs_settings(self) -> None:
|
||||||
try:
|
try:
|
||||||
config = self._prepared_value(
|
config = self.update_status._load_gui_config()
|
||||||
"gui_config", self.update_status._load_gui_config)
|
|
||||||
if config and "logging" in config:
|
if config and "logging" in config:
|
||||||
self.enable_gui_logging.setChecked(
|
self.enable_gui_logging.setChecked(
|
||||||
config["logging"]["gui_logging_enabled"])
|
config["logging"]["gui_logging_enabled"])
|
||||||
|
|
@ -1977,13 +2103,69 @@ class Settings(Page):
|
||||||
layout.addLayout(button_layout)
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
self.load_systemwide_settings(
|
self.load_systemwide_settings()
|
||||||
self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def load_systemwide_settings(self, enabled=None):
|
def create_bwrap_page(self):
|
||||||
if enabled is None:
|
page = QWidget()
|
||||||
enabled = settings_data.read_systemwide_enabled()
|
layout = QVBoxLayout(page)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
title = QLabel("BWRAP PERMISSION")
|
||||||
|
title.setStyleSheet(
|
||||||
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
description = QLabel(
|
||||||
|
"Control whether HydraVeil configures a capability policy so bwrap can be used without requiring additional permissions.")
|
||||||
|
description.setWordWrap(True)
|
||||||
|
description.setStyleSheet(
|
||||||
|
f"color: white; font-size: 14px; {self.font_style}")
|
||||||
|
layout.addWidget(description)
|
||||||
|
|
||||||
|
status_layout = QHBoxLayout()
|
||||||
|
status_label = QLabel("Current status:")
|
||||||
|
status_label.setStyleSheet(
|
||||||
|
f"color: white; font-size: 14px; {self.font_style}")
|
||||||
|
self.bwrap_status_value = QLabel("")
|
||||||
|
self.bwrap_status_value.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
status_layout.addWidget(status_label)
|
||||||
|
status_layout.addWidget(self.bwrap_status_value)
|
||||||
|
status_layout.addStretch()
|
||||||
|
layout.addLayout(status_layout)
|
||||||
|
|
||||||
|
toggle_layout = QHBoxLayout()
|
||||||
|
self.bwrap_toggle = QCheckBox("Enable capability policy")
|
||||||
|
self.bwrap_toggle.setStyleSheet(self.get_checkbox_style())
|
||||||
|
toggle_layout.addWidget(self.bwrap_toggle)
|
||||||
|
toggle_layout.addStretch()
|
||||||
|
layout.addLayout(toggle_layout)
|
||||||
|
|
||||||
|
save_button = QPushButton()
|
||||||
|
save_button.setFixedSize(75, 46)
|
||||||
|
save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
|
||||||
|
save_button.setIconSize(QSize(75, 46))
|
||||||
|
save_button.clicked.connect(self.save_bwrap_settings)
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.addWidget(save_button)
|
||||||
|
button_layout.addStretch()
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.load_bwrap_settings()
|
||||||
|
return page
|
||||||
|
|
||||||
|
def load_systemwide_settings(self):
|
||||||
|
enabled = False
|
||||||
|
try:
|
||||||
|
privilege_policy = PolicyController.get('privilege')
|
||||||
|
if privilege_policy is not None:
|
||||||
|
enabled = PolicyController.is_instated(privilege_policy)
|
||||||
|
except Exception:
|
||||||
|
enabled = False
|
||||||
self.systemwide_toggle.setChecked(enabled)
|
self.systemwide_toggle.setChecked(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
self.systemwide_status_value.setText("Enabled")
|
self.systemwide_status_value.setText("Enabled")
|
||||||
|
|
@ -2020,10 +2202,53 @@ class Settings(Page):
|
||||||
self.systemwide_status_value.setStyleSheet(
|
self.systemwide_status_value.setStyleSheet(
|
||||||
f"color: red; font-size: 14px; {self.font_style}")
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
def load_bwrap_settings(self):
|
||||||
|
enabled = False
|
||||||
|
try:
|
||||||
|
capability_policy = PolicyController.get('capability')
|
||||||
|
if capability_policy is not None:
|
||||||
|
enabled = PolicyController.is_instated(capability_policy)
|
||||||
|
except Exception:
|
||||||
|
enabled = False
|
||||||
|
self.bwrap_toggle.setChecked(enabled)
|
||||||
|
if enabled:
|
||||||
|
self.bwrap_status_value.setText("Enabled")
|
||||||
|
self.bwrap_status_value.setStyleSheet(
|
||||||
|
f"color: #2ecc71; font-size: 14px; {self.font_style}")
|
||||||
|
else:
|
||||||
|
self.bwrap_status_value.setText("Disabled")
|
||||||
|
self.bwrap_status_value.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
def save_bwrap_settings(self):
|
||||||
|
enable = self.bwrap_toggle.isChecked()
|
||||||
|
try:
|
||||||
|
capability_policy = PolicyController.get('capability')
|
||||||
|
if capability_policy is not None:
|
||||||
|
if enable:
|
||||||
|
PolicyController.instate(capability_policy)
|
||||||
|
else:
|
||||||
|
if PolicyController.is_instated(capability_policy):
|
||||||
|
PolicyController.revoke(capability_policy)
|
||||||
|
self.load_bwrap_settings()
|
||||||
|
self.update_status.update_status(
|
||||||
|
"Capability policy settings updated")
|
||||||
|
except CommandNotFoundError as e:
|
||||||
|
self.bwrap_status_value.setText(str(e))
|
||||||
|
self.bwrap_status_value.setStyleSheet(
|
||||||
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e:
|
||||||
|
self.bwrap_status_value.setText(str(e))
|
||||||
|
self.bwrap_status_value.setStyleSheet(
|
||||||
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
except Exception:
|
||||||
|
self.bwrap_status_value.setText("Failed to update policy")
|
||||||
|
self.bwrap_status_value.setStyleSheet(
|
||||||
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
def load_registrations_settings(self) -> None:
|
def load_registrations_settings(self) -> None:
|
||||||
try:
|
try:
|
||||||
config = self._prepared_value(
|
config = self.update_status._load_gui_config()
|
||||||
"gui_config", self.update_status._load_gui_config)
|
|
||||||
if config and "registrations" in config:
|
if config and "registrations" in config:
|
||||||
registrations = config["registrations"]
|
registrations = config["registrations"]
|
||||||
auto_sync = registrations.get("auto_sync_enabled", False)
|
auto_sync = registrations.get("auto_sync_enabled", False)
|
||||||
|
|
@ -2081,3 +2306,26 @@ class Settings(Page):
|
||||||
logging.error(f"Error saving registration settings: {str(e)}")
|
logging.error(f"Error saving registration settings: {str(e)}")
|
||||||
self.update_status.update_status(
|
self.update_status.update_status(
|
||||||
"Error saving registration settings")
|
"Error saving registration settings")
|
||||||
|
|
||||||
|
def is_auto_sync_enabled(self) -> bool:
|
||||||
|
try:
|
||||||
|
config = self.update_status._load_gui_config()
|
||||||
|
if config and "registrations" in config:
|
||||||
|
registrations = config["registrations"]
|
||||||
|
return registrations.get("auto_sync_enabled", False)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error checking auto-sync setting: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_fast_registration_enabled(self) -> bool:
|
||||||
|
try:
|
||||||
|
config = self.update_status._load_gui_config()
|
||||||
|
if config and "registrations" in config:
|
||||||
|
registrations = config["registrations"]
|
||||||
|
return registrations.get("fast_registration_enabled", False)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(
|
||||||
|
f"Error checking fast registration setting: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from PyQt6.QtGui import QPixmap
|
||||||
|
|
||||||
from core.controllers.ClientController import ClientController
|
from core.controllers.ClientController import ClientController
|
||||||
|
|
||||||
from gui.v2.actions.sync_result import is_valid_sync_payload
|
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
||||||
|
|
@ -83,11 +82,10 @@ class SyncScreen(Page):
|
||||||
self.worker_thread = WorkerThread('SYNC')
|
self.worker_thread = WorkerThread('SYNC')
|
||||||
|
|
||||||
self.worker_thread.sync_output.connect(self.update_output)
|
self.worker_thread.sync_output.connect(self.update_output)
|
||||||
self.worker_thread.text_output.connect(self.update_status.update_status)
|
|
||||||
self.worker_thread.start()
|
self.worker_thread.start()
|
||||||
|
|
||||||
def update_output(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
def update_output(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||||
if status is True and isinstance(all_browsers, bool) and not all_browsers:
|
if isinstance(all_browsers, bool) and not all_browsers:
|
||||||
self.custom_window.navigator.navigate("install_system_package")
|
self.custom_window.navigator.navigate("install_system_package")
|
||||||
install_page = self.custom_window.navigator.get_cached("install_system_package")
|
install_page = self.custom_window.navigator.get_cached("install_system_package")
|
||||||
if install_page is not None:
|
if install_page is not None:
|
||||||
|
|
@ -97,12 +95,14 @@ class SyncScreen(Page):
|
||||||
self.button_back.setEnabled(True)
|
self.button_back.setEnabled(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not is_valid_sync_payload(available_locations, available_browsers, status, locations, all_browsers):
|
if status is False:
|
||||||
self.connection_manager.set_synced(False)
|
|
||||||
self.button_go.setEnabled(True)
|
self.button_go.setEnabled(True)
|
||||||
self.button_back.setEnabled(True)
|
self.button_back.setEnabled(True)
|
||||||
|
self.update_status.update_status('An error occurred during sync')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.update_status.update_status('Sync complete')
|
||||||
|
|
||||||
update_available = ClientController.can_be_updated()
|
update_available = ClientController.can_be_updated()
|
||||||
|
|
||||||
if update_available:
|
if update_available:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from core.controllers.tickets.TicketPayController import check_if_paid
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
|
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
|
||||||
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,13 +77,7 @@ class TicketCryptoPickerPage(Page):
|
||||||
clipboard = QApplication.clipboard()
|
clipboard = QApplication.clipboard()
|
||||||
clipboard.setText(temp_billing_code)
|
clipboard.setText(temp_billing_code)
|
||||||
not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now."
|
not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now."
|
||||||
info = QMessageBox(self)
|
QMessageBox.information(None, "Not Paid", not_paid_msg)
|
||||||
info.setWindowTitle("Not Paid")
|
|
||||||
info.setText(not_paid_msg)
|
|
||||||
info.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
||||||
style_message_box(info)
|
|
||||||
mark_confirm_button(info.button(QMessageBox.StandardButton.Ok))
|
|
||||||
info.exec()
|
|
||||||
|
|
||||||
def start_initiate_payment(self, currency):
|
def start_initiate_payment(self, currency):
|
||||||
self.update_status.update_status("Initiating payment...")
|
self.update_status.update_status("Initiating payment...")
|
||||||
|
|
@ -128,8 +121,6 @@ class TicketCryptoPickerPage(Page):
|
||||||
msg.setWindowTitle("Existing tickets found")
|
msg.setWindowTitle("Existing tickets found")
|
||||||
msg.setText("You already have ticket data. Wipe it and start over?")
|
msg.setText("You already have ticket data. Wipe it and start over?")
|
||||||
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
style_message_box(msg)
|
|
||||||
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
|
|
||||||
result = msg.exec()
|
result = msg.exec()
|
||||||
if result == QMessageBox.StandardButton.Yes:
|
if result == QMessageBox.StandardButton.Yes:
|
||||||
self.bypass_existing = True
|
self.bypass_existing = True
|
||||||
|
|
@ -144,8 +135,6 @@ class TicketCryptoPickerPage(Page):
|
||||||
msg.setWindowTitle("Existing billing code found")
|
msg.setWindowTitle("Existing billing code found")
|
||||||
msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.")
|
msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.")
|
||||||
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
style_message_box(msg)
|
|
||||||
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
|
|
||||||
result = msg.exec()
|
result = msg.exec()
|
||||||
if result == QMessageBox.StandardButton.Yes:
|
if result == QMessageBox.StandardButton.Yes:
|
||||||
self.update_status.update_status("Reusing same code")
|
self.update_status.update_status("Reusing same code")
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,6 @@ class TicketOrBillingChoicePage(Page):
|
||||||
if menu_page:
|
if menu_page:
|
||||||
self.update_status.update_status(f"Using ticket #{which_ticket}...")
|
self.update_status.update_status(f"Using ticket #{which_ticket}...")
|
||||||
menu_page.enabling_profile(profile_data)
|
menu_page.enabling_profile(profile_data)
|
||||||
self.custom_window.navigator.navigate("menu")
|
|
||||||
|
|
||||||
def on_use_billing(self):
|
def on_use_billing(self):
|
||||||
self.custom_window.navigator.navigate("id")
|
self.custom_window.navigator.navigate("id")
|
||||||
|
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
from core.Constants import Constants
|
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QScrollArea
|
|
||||||
from PyQt6.QtCore import Qt, QSize
|
|
||||||
from PyQt6.QtGui import QIcon, QFont, QColor, QPalette, QPixmap, QPainter
|
|
||||||
import sys
|
|
||||||
|
|
||||||
APP_VERSION = Constants.DB_VERSION_THIS_APP_WANTS
|
|
||||||
|
|
||||||
class DatabaseConflictDialog(QDialog):
|
|
||||||
WIPE = 1
|
|
||||||
MOVE_DB = 2
|
|
||||||
EXIT = 3
|
|
||||||
|
|
||||||
def __init__(self, check: dict, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Database Recovery")
|
|
||||||
self.setModal(True)
|
|
||||||
self.user_choice = self.EXIT
|
|
||||||
self.setMinimumWidth(550)
|
|
||||||
self.setMinimumHeight(400)
|
|
||||||
self.setMaximumWidth(750)
|
|
||||||
self.setMaximumHeight(600)
|
|
||||||
|
|
||||||
# Modern dark theme
|
|
||||||
self.setStyleSheet("""
|
|
||||||
DatabaseConflictDialog {
|
|
||||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
|
||||||
stop:0 #0f172a, stop:1 #1e293b);
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
main_layout = QVBoxLayout()
|
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
main_layout.setSpacing(0)
|
|
||||||
|
|
||||||
# Header with gradient and icon
|
|
||||||
header = QFrame()
|
|
||||||
header.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
||||||
stop:0 #3b82f6, stop:1 #1e40af);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
header.setFixedHeight(100)
|
|
||||||
header_layout = QHBoxLayout()
|
|
||||||
header_layout.setContentsMargins(30, 20, 30, 20)
|
|
||||||
|
|
||||||
# Warning icon (using unicode)
|
|
||||||
icon_label = QLabel("⚠️")
|
|
||||||
icon_label.setFont(QFont("Segoe UI", 48))
|
|
||||||
header_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
||||||
|
|
||||||
######## Title Section. ##############
|
|
||||||
|
|
||||||
# First which messages to display,
|
|
||||||
if check["status"] == "version_mismatch":
|
|
||||||
error_title = "Database Compatibility"
|
|
||||||
error_subtitle = "Your App version and Database don't match"
|
|
||||||
else:
|
|
||||||
error_title = "Database Error"
|
|
||||||
error_subtitle = "The Database can't properly start"
|
|
||||||
|
|
||||||
# then display them:
|
|
||||||
title_layout = QVBoxLayout()
|
|
||||||
title_layout.setSpacing(5)
|
|
||||||
|
|
||||||
title = QLabel(error_title)
|
|
||||||
title.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold))
|
|
||||||
title.setStyleSheet("color: white;")
|
|
||||||
title_layout.addWidget(title)
|
|
||||||
|
|
||||||
subtitle = QLabel(error_subtitle)
|
|
||||||
subtitle.setFont(QFont("Segoe UI", 11))
|
|
||||||
subtitle.setStyleSheet("color: rgba(255, 255, 255, 0.8);")
|
|
||||||
title_layout.addWidget(subtitle)
|
|
||||||
|
|
||||||
header_layout.addLayout(title_layout, 1)
|
|
||||||
header.setLayout(header_layout)
|
|
||||||
main_layout.addWidget(header)
|
|
||||||
|
|
||||||
# Content area with scroll support
|
|
||||||
content = QFrame()
|
|
||||||
content.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background: #1e293b;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
content_layout = QVBoxLayout()
|
|
||||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
content_layout.setSpacing(0)
|
|
||||||
|
|
||||||
# Scrollable area for dynamic content
|
|
||||||
scroll_area = QScrollArea()
|
|
||||||
scroll_area.setWidgetResizable(True)
|
|
||||||
scroll_area.setStyleSheet("""
|
|
||||||
QScrollArea {
|
|
||||||
background: #1e293b;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
QScrollBar:vertical {
|
|
||||||
background: #1e293b;
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
QScrollBar::handle:vertical {
|
|
||||||
background: #475569;
|
|
||||||
border-radius: 6px;
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
QScrollBar::handle:vertical:hover {
|
|
||||||
background: #64748b;
|
|
||||||
}
|
|
||||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Inner scrollable widget
|
|
||||||
scroll_widget = QFrame()
|
|
||||||
scroll_widget.setStyleSheet("background: #1e293b; border: none;")
|
|
||||||
scroll_layout = QVBoxLayout()
|
|
||||||
scroll_layout.setContentsMargins(40, 30, 40, 30)
|
|
||||||
scroll_layout.setSpacing(15)
|
|
||||||
|
|
||||||
# Main message
|
|
||||||
message = QLabel(check["message"])
|
|
||||||
message.setFont(QFont("Segoe UI", 12))
|
|
||||||
message.setStyleSheet("color: #e2e8f0; line-height: 1.6;")
|
|
||||||
message.setWordWrap(True)
|
|
||||||
scroll_layout.addWidget(message)
|
|
||||||
|
|
||||||
# Version info
|
|
||||||
if check.get("db_version"):
|
|
||||||
info_frame = QFrame()
|
|
||||||
info_frame.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
info_layout = QVBoxLayout()
|
|
||||||
info_layout.setContentsMargins(15, 12, 15, 12)
|
|
||||||
info_layout.setSpacing(5)
|
|
||||||
|
|
||||||
app_ver = QLabel(f"App is looking for database version: {APP_VERSION}")
|
|
||||||
app_ver.setFont(QFont("Segoe UI", 10))
|
|
||||||
app_ver.setStyleSheet("color: #94a3b8;")
|
|
||||||
info_layout.addWidget(app_ver)
|
|
||||||
|
|
||||||
db_ver = QLabel(f"Your Actual Database version: {check['db_version']}")
|
|
||||||
db_ver.setFont(QFont("Segoe UI", 10))
|
|
||||||
db_ver.setStyleSheet("color: #94a3b8;")
|
|
||||||
info_layout.addWidget(db_ver)
|
|
||||||
|
|
||||||
info_frame.setLayout(info_layout)
|
|
||||||
scroll_layout.addWidget(info_frame)
|
|
||||||
|
|
||||||
scroll_layout.addStretch()
|
|
||||||
scroll_widget.setLayout(scroll_layout)
|
|
||||||
scroll_area.setWidget(scroll_widget)
|
|
||||||
content_layout.addWidget(scroll_area, 1)
|
|
||||||
|
|
||||||
content.setLayout(content_layout)
|
|
||||||
main_layout.addWidget(content, 1)
|
|
||||||
|
|
||||||
# Button area
|
|
||||||
button_area = QFrame()
|
|
||||||
button_area.setStyleSheet("""
|
|
||||||
QFrame {
|
|
||||||
background: #0f172a;
|
|
||||||
border-top: 1px solid #334155;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
button_layout = QVBoxLayout()
|
|
||||||
button_layout.setContentsMargins(40, 20, 40, 20)
|
|
||||||
button_layout.setSpacing(12)
|
|
||||||
|
|
||||||
if check["status"] == "version_mismatch":
|
|
||||||
btn_backup = self._create_button(
|
|
||||||
"Create a Fresh Database. Then restart (recommended)",
|
|
||||||
"primary",
|
|
||||||
"✅"
|
|
||||||
)
|
|
||||||
btn_backup.clicked.connect(self.on_wipe_create)
|
|
||||||
button_layout.addWidget(btn_backup)
|
|
||||||
|
|
||||||
btn_different = self._create_button(
|
|
||||||
"Move the Stale Database for Debug Purposes",
|
|
||||||
"secondary",
|
|
||||||
"📁"
|
|
||||||
)
|
|
||||||
btn_different.clicked.connect(self.on_move_db)
|
|
||||||
button_layout.addWidget(btn_different)
|
|
||||||
|
|
||||||
btn_exit = self._create_button(
|
|
||||||
"Exit App without action.",
|
|
||||||
"danger",
|
|
||||||
"❌"
|
|
||||||
)
|
|
||||||
btn_exit.clicked.connect(self.on_exit)
|
|
||||||
button_layout.addWidget(btn_exit)
|
|
||||||
|
|
||||||
button_area.setLayout(button_layout)
|
|
||||||
main_layout.addWidget(button_area)
|
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
|
||||||
|
|
||||||
def _create_button(self, text, style_type, icon):
|
|
||||||
"""Create a styled button"""
|
|
||||||
btn = QPushButton(f" {icon} {text}")
|
|
||||||
btn.setFont(QFont("Segoe UI", 11, QFont.Weight.Medium))
|
|
||||||
btn.setFixedHeight(45)
|
|
||||||
btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
||||||
|
|
||||||
if style_type == "primary":
|
|
||||||
btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
||||||
stop:0 #3b82f6, stop:1 #2563eb);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
|
||||||
stop:0 #2563eb, stop:1 #1d4ed8);
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background: #1d4ed8;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
elif style_type == "secondary":
|
|
||||||
btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background: #334155;
|
|
||||||
color: #e2e8f0;
|
|
||||||
border: 1px solid #475569;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #475569;
|
|
||||||
border: 1px solid #64748b;
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background: #1e293b;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
elif style_type == "danger":
|
|
||||||
btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background: #64748b;
|
|
||||||
color: #e2e8f0;
|
|
||||||
border: 1px solid #475569;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #ef4444;
|
|
||||||
border: 1px solid #dc2626;
|
|
||||||
}
|
|
||||||
QPushButton:pressed {
|
|
||||||
background: #dc2626;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
return btn
|
|
||||||
|
|
||||||
def on_wipe_create(self):
|
|
||||||
self.user_choice = self.WIPE
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def on_move_db(self):
|
|
||||||
self.user_choice = self.MOVE_DB
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def on_exit(self):
|
|
||||||
self.user_choice = self.EXIT
|
|
||||||
self.reject()
|
|
||||||
|
|
||||||
def show_recovery_dialog(check: dict):
|
|
||||||
"""Show recovery dialog as standalone app and return user choice"""
|
|
||||||
app = QApplication.instance() or QApplication(sys.argv)
|
|
||||||
dialog = DatabaseConflictDialog(check)
|
|
||||||
dialog.exec()
|
|
||||||
return dialog.user_choice
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
MESSAGE_BOX_QSS = (
|
|
||||||
"QMessageBox { background-color: white; }"
|
|
||||||
"QMessageBox QLabel { color: black; }"
|
|
||||||
"QMessageBox QPushButton { color: black; padding: 5px 14px; }"
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIRM_BUTTON_QSS = "color: #2e7d32; font-weight: bold; padding: 5px 14px;"
|
|
||||||
|
|
||||||
|
|
||||||
def style_message_box(box):
|
|
||||||
box.setStyleSheet(MESSAGE_BOX_QSS)
|
|
||||||
|
|
||||||
|
|
||||||
def mark_confirm_button(button):
|
|
||||||
if button is None:
|
|
||||||
return
|
|
||||||
button.setText(f"✓ {button.text()}")
|
|
||||||
button.setStyleSheet(CONFIRM_BUTTON_QSS)
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from PyQt6.QtWidgets import QApplication, QFileDialog
|
|
||||||
|
|
||||||
def move_database_file(original_filepath: str) -> bool:
|
|
||||||
"""
|
|
||||||
Prompts the user to select a destination for a .db file and moves it there.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original_filepath: The current path to the .db file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the move was successful, False otherwise
|
|
||||||
"""
|
|
||||||
# Create a QApplication if one doesn't already exist
|
|
||||||
app = QApplication.instance()
|
|
||||||
if app is None:
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
|
|
||||||
# Validate that the original file exists
|
|
||||||
if not Path(original_filepath).exists():
|
|
||||||
print(f"Error: File not found at {original_filepath}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Open the file save dialog
|
|
||||||
destination, _ = QFileDialog.getSaveFileName(
|
|
||||||
None,
|
|
||||||
"Select destination for database file",
|
|
||||||
str(Path(original_filepath).parent), # Start in the original file's directory
|
|
||||||
"Database Files (*.db);;All Files (*)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the user cancelled the dialog
|
|
||||||
if not destination:
|
|
||||||
print("Operation cancelled by user")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Move the file
|
|
||||||
shutil.move(original_filepath, destination)
|
|
||||||
print(f"File successfully moved to: {destination}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error moving file: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def launch_file_picker(original_path):
|
|
||||||
move_database_file(original_path)
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
forced_sync_popup_style = """
|
|
||||||
QPushButton {
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 8px 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
QPushButton[text="Cancel"] {
|
|
||||||
background-color: #e74c3c;
|
|
||||||
}
|
|
||||||
QPushButton[text="Cancel"]:hover {
|
|
||||||
background-color: #c0392b;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
@ -116,42 +116,6 @@ POPUP_ACTION_BUTTON_RED_QSS = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def combobox_style(font_style=""):
|
|
||||||
return f"""
|
|
||||||
QComboBox {{
|
|
||||||
color: black;
|
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 5px 30px 5px 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-width: 120px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
{font_style}
|
|
||||||
}}
|
|
||||||
QComboBox:disabled {{
|
|
||||||
color: #666;
|
|
||||||
background: #e0e0e0;
|
|
||||||
}}
|
|
||||||
QComboBox::drop-down {{
|
|
||||||
border: none;
|
|
||||||
width: 30px;
|
|
||||||
}}
|
|
||||||
QComboBox::down-arrow {{
|
|
||||||
image: url(assets/down_arrow.png);
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}}
|
|
||||||
QComboBox QAbstractItemView {{
|
|
||||||
color: black;
|
|
||||||
background: white;
|
|
||||||
selection-background-color: #007bff;
|
|
||||||
selection-color: white;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
{font_style}
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def checkbox_style(font_style=""):
|
def checkbox_style(font_style=""):
|
||||||
return f"""
|
return f"""
|
||||||
QCheckBox {{
|
QCheckBox {{
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
from PyQt6.QtCore import QThread, pyqtSignal
|
|
||||||
|
|
||||||
|
|
||||||
class PageDataWorker(QThread):
|
|
||||||
data_ready = pyqtSignal(str, object)
|
|
||||||
failed = pyqtSignal(str, str)
|
|
||||||
|
|
||||||
def __init__(self, name, fn, *args):
|
|
||||||
super().__init__()
|
|
||||||
self.name = name
|
|
||||||
self.fn = fn
|
|
||||||
self.args = args
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
payload = self.fn(*self.args)
|
|
||||||
self.data_ready.emit(self.name, payload)
|
|
||||||
except Exception as error:
|
|
||||||
self.failed.emit(self.name, f"{type(error).__name__}: {error}")
|
|
||||||
|
|
@ -15,8 +15,6 @@ from core.models.session.SessionConnection import SessionConnection
|
||||||
from core.models.session.SessionProfile import SessionProfile
|
from core.models.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemConnection import SystemConnection
|
from core.models.system.SystemConnection import SystemConnection
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
from core.observers.ClientObserver import ClientObserver
|
|
||||||
from core.observers.ConnectionObserver import ConnectionObserver
|
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import (
|
from gui.v2.infrastructure.setup_observers import (
|
||||||
client_observer,
|
client_observer,
|
||||||
|
|
@ -224,81 +222,24 @@ class WorkerThread(QThread):
|
||||||
finally:
|
finally:
|
||||||
self.finished.emit(True)
|
self.finished.emit(True)
|
||||||
|
|
||||||
def _emit_observer_message(self, topic, event):
|
|
||||||
subject = getattr(event, 'subject', None)
|
|
||||||
meta = getattr(event, 'meta', None)
|
|
||||||
if subject is not None:
|
|
||||||
self.text_output.emit(str(subject))
|
|
||||||
elif meta:
|
|
||||||
self.text_output.emit(str(meta))
|
|
||||||
else:
|
|
||||||
self.text_output.emit(str(topic))
|
|
||||||
|
|
||||||
def _subscribe_observer_messages(self, observer, skip_topics=None):
|
|
||||||
skip_topics = skip_topics or set()
|
|
||||||
for attr in dir(observer):
|
|
||||||
if not attr.startswith('on_'):
|
|
||||||
continue
|
|
||||||
callbacks = getattr(observer, attr, None)
|
|
||||||
if not isinstance(callbacks, list):
|
|
||||||
continue
|
|
||||||
topic = attr[3:]
|
|
||||||
if topic in skip_topics:
|
|
||||||
continue
|
|
||||||
observer.subscribe(
|
|
||||||
topic,
|
|
||||||
lambda event, topic=topic: self._emit_observer_message(topic, event))
|
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
try:
|
try:
|
||||||
if self.action == 'SYNC_TOR':
|
if self.action == 'SYNC_TOR':
|
||||||
ConfigurationController.set_connection('tor')
|
ConfigurationController.set_connection('tor')
|
||||||
else:
|
else:
|
||||||
ConfigurationController.set_connection('system')
|
ConfigurationController.set_connection('system')
|
||||||
sync_succeeded = False
|
self.check_for_update()
|
||||||
|
|
||||||
def sync_complete(event):
|
|
||||||
nonlocal sync_succeeded
|
|
||||||
sync_succeeded = True
|
|
||||||
self._emit_observer_message('synchronized', event)
|
|
||||||
|
|
||||||
sync_client_observer = ClientObserver()
|
|
||||||
sync_connection_observer = ConnectionObserver()
|
|
||||||
self._subscribe_observer_messages(
|
|
||||||
sync_client_observer, {'synchronized'})
|
|
||||||
self._subscribe_observer_messages(sync_connection_observer)
|
|
||||||
sync_client_observer.subscribe('synchronized', sync_complete)
|
|
||||||
|
|
||||||
ClientController.sync(
|
|
||||||
client_observer=sync_client_observer,
|
|
||||||
connection_observer=sync_connection_observer)
|
|
||||||
|
|
||||||
if not sync_succeeded:
|
|
||||||
self.sync_output.emit([], [], False, False, [], [])
|
|
||||||
self.finished.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
locations = LocationController.get_all()
|
locations = LocationController.get_all()
|
||||||
|
|
||||||
|
|
||||||
browser = ApplicationVersionController.get_all()
|
browser = ApplicationVersionController.get_all()
|
||||||
all_browser_versions = [
|
all_browser_versions = [
|
||||||
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
|
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
|
||||||
all_location_codes = [
|
all_location_codes = [
|
||||||
f"{location.country_code}_{location.code}" for location in locations]
|
f"{location.country_code}_{location.code}" for location in locations]
|
||||||
|
|
||||||
if not all_location_codes or not all_browser_versions or not locations or not browser:
|
|
||||||
self.sync_output.emit([], [], False, False, [], [])
|
|
||||||
self.finished.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.sync_output.emit(
|
self.sync_output.emit(
|
||||||
all_location_codes, all_browser_versions, True, False, locations, browser)
|
all_location_codes, all_browser_versions, True, False, locations, browser)
|
||||||
self.finished.emit(True)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'the error is: {e}')
|
print(f'the error is: {e}')
|
||||||
self.sync_output.emit([], [], False, False, [], [])
|
self.sync_output.emit([], [], False, False, [], [])
|
||||||
self.finished.emit(False)
|
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
connection = ConfigurationController.get_connection()
|
connection = ConfigurationController.get_connection()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "sp-hydra-veil-gui"
|
name = "sp-hydra-veil-gui"
|
||||||
version = "2.4.6"
|
version = "2.3.1"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Simplified Privacy" },
|
{ name = "Simplified Privacy" },
|
||||||
]
|
]
|
||||||
|
|
@ -12,7 +12,7 @@ classifiers = [
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sp-hydra-veil-core == 2.3.8",
|
"sp-hydra-veil-core == 2.3.4",
|
||||||
"pyperclip ~= 1.9.0",
|
"pyperclip ~= 1.9.0",
|
||||||
"pyqt6 ~= 6.7.1",
|
"pyqt6 ~= 6.7.1",
|
||||||
"qrcode[pil] ~= 8.2"
|
"qrcode[pil] ~= 8.2"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue