Compare commits

..

1 commit

Author SHA1 Message Date
JOhn
bd77439a24 update: new v2 structure 2026-06-18 19:18:30 -04:00
39 changed files with 13437 additions and 2862 deletions

17
.gitignore vendored
View file

@ -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

View file

@ -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.

View file

@ -1,205 +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.services.helpers.manage_assets import assets_folder_setup
from core.Constants import Constants
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.manage.migrations import migrate_sql
from core.models.manage.clear_sql_model import clear_sql_model
# generic
import os import os
import sys import sys
from pathlib import Path
_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
)
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
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.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):
# UTIL FUNCTIONS AND RECOVERY UI def __init__(self):
# ============================================================================ super().__init__()
def clear_sync_cache(): sys.excepthook = self._handle_exception
""" self.setWindowFlags(Qt.WindowType.Window)
Purpose:
Clear CachedSync
If that fails, prompt for a database wipe.
Why: gui_dir = os.path.dirname(os.path.abspath(__file__))
If they migrated, they have new schemas, but old data. font_path = os.path.join(gui_dir, 'resources', 'fonts')
The problem with that, is the newest CachedSync entry will fool the sync data functions into thinking they don't need to sync new data,
Even though they do need to sync with the new schema.
"""
from core.models.orm_models.CachedSync import CachedSync
result = clear_sql_model(CachedSync)
if result:
logger.info(f"[DB MANAGEMENT] After Migration cleared the CachedSync table: {result}.")
else:
custom_error = "There were issues with clearing the Sync Cache in your database. Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
recovery_dialog(custom_error, "CachedSync Issue")
def failed_migration(db_version_you_have): retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
close_session() # shut down db connection font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
custom_error = "There were issues with migrating your database. We transitioned to a new Database format for new features! Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions." font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
recovery_dialog(custom_error, db_version_you_have) app_font = QFont(font_family)
app_font.setPointSize(10)
QApplication.setFont(app_font)
def recovery_dialog(custom_error, db_version_you_have): self.open_sans_family = None
"""This ends the app by forcing them to delete the database, move it, or just quit.""" 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]
from gui.v2.ui.popups.Database_version import show_recovery_dialog self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
choice = show_recovery_dialog({"message": custom_error, "db_version": db_version_you_have, "status": "version_mismatch"}) self.setWindowTitle('HydraVeil')
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
if choice == 1: screen = QApplication.primaryScreen()
if os.path.exists(database_path): ratio = screen.devicePixelRatio()
os.remove(database_path) self.host_screen_width = int(screen.size().width() * ratio)
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}. Now putting lock on for forced sync..") self.host_screen_height = int(screen.size().height() * ratio)
lock_file.touch()
logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..")
sys.exit()
elif choice == 2: self._data = {"Profile_1": {}}
logger.info(f"[DB MANAGEMENT] User opted to move the DB file.")
from gui.v2.ui.popups.pick_folder_to_move import launch_file_picker
launch_file_picker(database_path)
sys.exit()
else: self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui')
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.") self.gui_config_file = os.path.join(self.gui_config_home, 'config.json')
sys.exit() 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(
# ASSETS FOLDER self.gui_cache_home, self.gui_config_home, self.gui_config_file)
# ============================================================================
logger.info("Welcome, checking assets..")
if not assets_folder_setup():
from gui.v2.ui.popups.generic_error_popup import show_error
custom_error = "Unable to Setup your Assets Folder. Please create an assets directory at ~/.local/share/hydra-veil/assets"
show_error(custom_error, "Critical Error")
# ============================================================================ self.btn_path = os.getenv('BTN_PATH', os.path.join(
# INITIALIZE DATABASE gui_dir, 'resources', 'images'))
# ============================================================================ self.css_path = os.getenv('CSS_PATH', os.path.join(
lock_file = Path(f"{Constants.HV_DATA_HOME}/deleted_db.lock") gui_dir, 'resources', 'styles'))
logger.info("[DB MANAGEMENT] Starting DB init..")
try:
# 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 self.is_downloading = False
init_session() # (engine, Session, _session all initialized from session_management) self.current_profile_id = None
session = get_session() self.connection_manager = ConnectionManager()
# does the version checker table exist? current_connection = None
version_table_exists = does_db_version_table_exist() self.is_tor_mode = current_connection == 'tor'
except:
logger.error("Critical Error with database initialization!")
sys.exit()
# ============================================================================ self.is_animating = False
# LEGACY DATABASE CHECKS self.animation_step = 0
# ============================================================================ self.page_history = []
migration_happened = False self.log_path = None
# are they upgrading from a legacy version? that would mean the table existed, but not the version table, self.setFixedSize(800, 570)
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.")
migration = migrate_sql() top = create_top_section(self, self.css_path)
self.__dict__.update(top)
logger.info(f"[DB MANAGEMENT] Migration result is {migration.valid}.") self.marquee_text = ""
if migration.valid: self.marquee_position = 0
migration_happened = True self.marquee_enabled = False
force_sync = True self.scroll_speed = 1
clear_sync_cache()
else:
# THEN DISPLAY CHOICES AND RECOVERY UI
db_version_you_have = "Old System"
failed_migration(db_version_you_have)
# If they're still here, then if it's NOT a legacy version, self.page_stack = QStackedWidget(self)
self.page_stack.setGeometry(0, 0, 800, 570)
# and the new version table doesn't exist, then create it: bottom = create_bottom_section(
if not version_table_exists: self,
create_ONLY_db_version_table() 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)
# COMPARE DB VERISONS
# ============================================================================
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}")
# ============================================================================ self.update_image()
# IF THE VERSIONS MATCH self.set_scroll_speed(7)
# ============================================================================
if is_compatable:
try:
made_tables = create_ALL_tables()
# SCREEN FAILED TABLES
if not made_tables:
custom_error = "Critical Failure with starting the models of the database."
logger.error(f"[DB MANAGEMENT] {custom_error}")
close_session()
from gui.v2.ui.popups.Database_version import show_recovery_dialog
choice = show_recovery_dialog({"message": custom_error, "db_version": 0, "status": "cant_make"})
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
sys.exit()
except: self.toggle_button.clicked.connect(self.toggle_mode)
logger.info(f"[DB MANAGEMENT] create ALL tables failed. Running migrations...") self.sync_button.clicked.connect(self.sync)
migration = migrate_sql() QApplication.instance().installEventFilter(self)
logger.info(f"[DB MANAGEMENT] Results of migration is {migration.valid}")
if not migration.valid: self.create_toggle(self.is_tor_mode)
db_version_you_have = 0 self.animation_timer = QTimer()
failed_migration(db_version_you_have) self.animation_timer.timeout.connect(self.animate_toggle)
self.check_logging()
# ASSUME TABLES CREATED self.navigator = Navigator(self.page_stack, self)
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing") self.page_stack.currentChanged.connect(self.page_changed)
# UPDATE DATA self.show()
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS) self.init_ui()
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: self.tray = None
from gui.main_ui import start_ui self.tray = init_system_tray(self, self.btn_path)
# If the user lacks a database, we want to force them to sync the new data, to avoid NoneType errors when enabling existing profiles, current_connection = self.get_current_connection()
if reason == "no_database": self.is_tor_mode = current_connection == 'tor'
logger.info(f"[DB MANAGEMENT] User had no database to begin with, so now we're entering GUI with sync on..") self.set_toggle_state(self.is_tor_mode)
force_sync = True
else: core_logger.info(
logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..") f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, "
if migration_happened: f"connection={current_connection}, tor_mode={self.is_tor_mode})"
force_sync = True )
clear_sync_cache()
def init_ui(self):
if self.check_first_launch():
self.navigator.navigate("welcome")
else: else:
force_sync = False self.navigator.navigate("menu")
# if we deleted their DB, we want to force sync, def perform_update_check(self):
if lock_file.exists(): from core.controllers.ClientController import ClientController
force_sync = True update_available = ClientController.can_be_updated()
lock_file.unlink() # unlock for next time
# Start GUI either way: if update_available:
logger.info(f"[DB MANAGEMENT] Starting GUI..") menu_page = self.navigator.get_cached("menu")
start_ui(force_sync) if menu_page is not None:
menu_page.on_update_check_finished()
# ============================================================================ def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
# but IF the versions do NOT match if not status:
# ============================================================================ self.update_status('Sync failed. Please try again later.')
else: return
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
# WHAT IS THE REASON? from PyQt6.QtWidgets import QPushButton
custom_error = get_custom_message(reason, compatability_dict) from gui.v2.actions.sync import generate_grid_positions
# 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))
recovery_dialog(custom_error, db_version_you_have) 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)
]
browser_page = self.navigator.get_cached("browser")
if browser_page is not None:
browser_page.create_interface_elements(available_browsers_list)
location_page = self.navigator.get_cached("location")
if location_page is not None:
location_page.create_interface_elements(available_locations_list)
hidetor_page = self.navigator.get_cached("hidetor")
if hidetor_page is not None:
hidetor_page.create_interface_elements(available_locations_list)
protocol_page = self.navigator.get_cached("protocol")
if protocol_page is not None:
protocol_page.enable_protocol_buttons()
menu_page = self.navigator.get_cached("menu")
if menu_page is not None:
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")
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())

View file

@ -1,54 +0,0 @@
fields:
- name: country_code
path: ['country', 'code']
required: true
- name: code # this is city code
path: ['code']
required: true
- name: id
path: ['id']
- name: country_name
path: ['country', 'name']
- name: name # this is CITY name
path: ['name']
- name: time_zone
path: ['time_zone', 'code']
- name: operator_id
path: ['operator_id']
required: true
- name: provider_name
path: ['provider', 'name']
- name: is_proxy_capable
path: ['is_proxy_capable']
- name: is_wireguard_capable
path: ['is_wireguard_capable']
required: true
- name: is_hysteria2_capable
path: ['is_hysteria2_capable']
- name: is_vless_capable
path: ['is_vless_capable']
# original version:
# locations.append((
# location['country']['code'],
# location['code'],
# location['id'],
# location['country']['name'],
# location['name'],
# location['time_zone']['code'],
# location['operator_id'],
# location['provider']['name'],
# location['is_proxy_capable'],
# location['is_wireguard_capable']))

View file

@ -1,8 +0,0 @@
fields:
- name: group_a_code
path: ['group_a', 'code']
- name: code
path: ['code']
- name: id
path: ['id']
required: true

View file

@ -1,19 +0,0 @@
fields:
- name: id
path: ['id']
required: true
- name: name
path: ['name']
- name: public_key # this is ed25519
path: ['public_key']
- name: nostr_public_key
path: ['nostr_public_key']
- name: nostr_profile_reference
path: ['nostr_profile_reference']
- name: nostr_attestation_event_reference
path: ['nostr_attestation', 'event_reference']

View file

@ -1,897 +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
from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QTimer, QEvent
from core.Constants import Constants
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.ProfileController import ProfileController
from core.Errors import UnknownConnectionTypeError
from core.errors.logger import logger as core_logger
core_logger.propagate = False
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
mainthread = ThreadSafetyTool()
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
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')
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._close_confirmation_pending = False
self._close_disconnect_in_progress = False
self._closing_after_disconnect = False
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")
if self._closing_after_disconnect:
orm.stop()
if event is not None:
event.accept()
return
if self._close_confirmation_pending or self._close_disconnect_in_progress:
if event is not None:
event.ignore()
return
connected_profiles = self._get_enabled_profile_ids()
if connected_profiles:
if event is not None:
event.ignore()
self._show_close_disconnect_confirmation(connected_profiles)
return
orm.stop()
if event is not None:
event.accept()
def _get_enabled_profile_ids(self):
try:
profiles = ProfileController.get_all()
except Exception:
return self.connection_manager.get_connected_profiles()
connected_profiles = []
self.connection_manager._connected_profiles.clear()
for profile_id, profile in profiles.items():
try:
if ProfileController.is_enabled(profile):
connected_profiles.append(profile_id)
self.connection_manager.add_connected_profile(profile_id)
except Exception:
pass
return connected_profiles
def _show_close_disconnect_confirmation(self, connected_profiles):
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
self._close_confirmation_pending = True
message = self._create_close_disconnect_message(connected_profiles)
self.popup = ConfirmationPopup(
self, message=message, action_button_text="Exit", cancel_button_text="Cancel")
self.popup.setWindowModality(Qt.WindowModality.ApplicationModal)
self.popup.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.popup.finished.connect(
lambda result: self._handle_close_disconnect_result(result, connected_profiles))
self.popup.destroyed.connect(
lambda *_: self._handle_close_disconnect_popup_closed())
self.popup.show()
def _create_close_disconnect_message(self, connected_profiles):
profile_numbers = ', '.join(str(profile_id)
for profile_id in connected_profiles)
is_are = "is" if len(connected_profiles) == 1 else "are"
return f'Profile{"" if len(connected_profiles) == 1 else "s"} {profile_numbers} {is_are} still connected.\nAll connected profiles will be disconnected on exit.\nDo you want to proceed?'
def _handle_close_disconnect_result(self, result, connected_profiles):
self._close_confirmation_pending = False
if not result:
return
self._close_disconnect_in_progress = True
self.update_status('Disabling profiles...')
self.worker_thread = WorkerThread(
'DISABLE_ALL_PROFILES', profile_data=connected_profiles)
self.worker_thread.text_output.connect(self.update_status)
self.worker_thread.finished.connect(self._finish_close_disconnect)
self.worker_thread.start()
def _handle_close_disconnect_popup_closed(self):
if self._close_confirmation_pending:
self._close_confirmation_pending = False
self.popup = None
def _finish_close_disconnect(self, _ok=True):
self._close_disconnect_in_progress = False
connected_profiles = self._get_enabled_profile_ids()
if connected_profiles:
self.update_status('Could not disconnect all profiles. Exit canceled.')
return
self.update_status('All profiles disabled. Bye!')
self._closing_after_disconnect = True
QTimer.singleShot(0, self.close)
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;")
@mainthread # wrapper of ThreadSafetyTool
def update_status(self, text, clear=False):
"""
Purpose:
Update the bottom left status bar
Features:
Forced on the main Thread by ThreadSafetyTool
Depends on:
v2.infrastructure.ThreadSafetyTool
Called by:
v2.infrastructure.setup_observers (outside this class)
"""
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=force_sync)
sys.exit(app.exec())

12363
gui/main_v1.py Normal file

File diff suppressed because it is too large Load diff

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -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:

View file

@ -1,165 +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_connection_setting(gui_config_file, key):
try:
config = load_gui_config(gui_config_file)
if config and "connection_settings" in config:
return bool(config["connection_settings"].get(key, False))
except Exception:
pass
return False
def read_firewall_setting(gui_config_file=None):
if gui_config_file is None:
return False
return read_connection_setting(gui_config_file, "firewall_enabled")
def read_managed_dns_setting(gui_config_file=None):
if gui_config_file is None:
return False
return read_connection_setting(gui_config_file, "managed_dns_enabled")
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(),
"firewall_setting": read_firewall_setting(gui_config_file),
"managed_dns_setting": read_managed_dns_setting(gui_config_file),
"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,
"firewall_setting": False,
"managed_dns_setting": 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}"

View file

@ -1,21 +0,0 @@
import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
from PyQt6.QtCore import QObject, pyqtSignal, Qt
class ThreadSafetyTool(QObject):
"""Marshal function calls to the main thread."""
_signal = pyqtSignal(object, tuple, dict)
def __init__(self):
super().__init__()
self._signal.connect(self._execute, Qt.ConnectionType.QueuedConnection)
def _execute(self, func, args, kwargs):
func(*args, **kwargs)
def __call__(self, func):
"""Decorator that forces a function to run on the main thread."""
def wrapper(*args, **kwargs):
self._signal.emit(func, args, kwargs)
return wrapper

View file

@ -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
from core.errors.logger import logger
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:
logger.debug(f"[preload] {name:30s} SKIP (already cached)")
return False
if name not in PAGE_REGISTRY:
logger.debug(f"[preload] {name:30s} SKIP (not in registry)")
return False
module_path, class_name = PAGE_REGISTRY[name]
logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}")
t0 = time.perf_counter()
try:
page = self._instantiate(name)
self.register_instance(name, page)
dt_ms = (time.perf_counter() - t0) * 1000.0
logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})")
return True
except Exception as e:
dt_ms = (time.perf_counter() - t0) * 1000.0
logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
f"{type(e).__name__}: {e}")
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()
logger.debug(f"[preload] ============================================================")
logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued")
logger.debug(f"[preload] order : {', '.join(self._preload_queue)}")
logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)")
if fast_mode:
logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}")
logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay")
logger.debug(f"[preload] ============================================================")
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
logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)")
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)
logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----")
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]
logger.debug(f"[preload] {name:30s} ASYNC {module_path}.{class_name}")
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:
logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)")
else:
try:
page = self._instantiate(name, prepared=payload)
self.register_instance(name, page)
self._preload_done += 1
logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})")
except Exception as e:
self._preload_failed += 1
logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}")
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}")
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
logger.debug(f"[preload] ============================================================")
logger.debug(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")
logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}")
logger.debug(f"[preload] ============================================================")

View file

@ -1,8 +0,0 @@
from core.models.manage.session_management import init_session, close_session
def start():
init_session()
def stop():
close_session()

View file

@ -1,7 +1,6 @@
PAGE_REGISTRY = { PAGE_REGISTRY = {
"blank": ("gui.v2.ui.pages._blank_page", "BlankPage"), "blank": ("gui.v2.ui.pages._blank_page", "BlankPage"),
"welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"), "welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"),
"networking_setup": ("gui.v2.ui.pages.networking_setup_page", "NetworkingSetupPage"),
"menu": ("gui.v2.ui.pages.menu_page", "MenuPage"), "menu": ("gui.v2.ui.pages.menu_page", "MenuPage"),
"protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"), "protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"),
"hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"), "hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"),
@ -31,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",
]

View file

@ -6,6 +6,7 @@ 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
application_version_observer = ApplicationVersionObserver() application_version_observer = ApplicationVersionObserver()
client_observer = ClientObserver() client_observer = ClientObserver()
connection_observer = ConnectionObserver() connection_observer = ConnectionObserver()
@ -20,12 +21,8 @@ def setup_observers(update_status):
profile_observer.subscribe( profile_observer.subscribe(
'destroyed', lambda event: update_status('Profile destroyed')) 'destroyed', lambda event: update_status('Profile destroyed'))
# client_observer.subscribe(
# 'synchronizing', lambda event: update_status('Sync in progress...'))
client_observer.subscribe( client_observer.subscribe(
'synchronizing', lambda event: update_status(f'{event.subject if event.subject else "Sync in progress..."}')) 'synchronizing', lambda event: update_status('Sync in progress...'))
client_observer.subscribe( client_observer.subscribe(
'synchronized', lambda event: update_status('Sync complete')) 'synchronized', lambda event: update_status('Sync complete'))
@ -36,14 +33,10 @@ def setup_observers(update_status):
client_observer.subscribe('updated', lambda event: update_status( client_observer.subscribe('updated', lambda event: update_status(
'Restart client to apply update.')) 'Restart client to apply update.'))
client_observer.subscribe(
'custom_message', lambda event: update_status(f'{event.subject if event.subject else "Error, check logs"}'))
application_version_observer.subscribe('downloading', lambda event: update_status( 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: update_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: update_status(
f'Downloaded {ApplicationController.get(event.subject.application_code).name}')) f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
@ -53,19 +46,12 @@ def setup_observers(update_status):
connection_observer.subscribe('tor_bootstrapping', lambda event: update_status( connection_observer.subscribe('tor_bootstrapping', lambda event: update_status(
'Establishing Tor connection...')) 'Establishing Tor connection...'))
connection_observer.subscribe( connection_observer.subscribe('tor_bootstrap_progressing', lambda event: update_status(
'tor_bootstrap_progressing', lambda event: update_status(f'{event.subject if event.subject else "Tor Bootstrapping.."}')) f'Bootstrapping Tor {event.meta.get('progress'):.2f}%'))
# original replaced version:
# connection_observer.subscribe('tor_bootstrap_progressing', lambda event: update_status(
# f'Bootstrapping Tor {event.meta.get('progress'):.2f}%'))
connection_observer.subscribe( connection_observer.subscribe(
'tor_bootstrapped', lambda event: update_status('Tor connection established.')) 'tor_bootstrapped', lambda event: update_status('Tor connection established.'))
client_observer.subscribe(
'custom_message', lambda event: update_status(f'{event.subject if event.subject else ""}'))
ticket_observer.subscribe('connecting', lambda event: update_status('Connecting to ticket server...')) ticket_observer.subscribe('connecting', lambda event: update_status('Connecting to ticket server...'))
ticket_observer.subscribe('sync_done', lambda event: update_status('Ticket prices synced.')) 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('waiting', lambda event: update_status('Waiting for payment...'))

View file

@ -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()

View file

@ -10,19 +10,16 @@ from PyQt6 import QtCore, QtGui
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
from core.controllers.LocationController import LocationController from core.controllers.LocationController import LocationController
from core.models.Result import Result, ResultError
from core.controllers.profile_state.update_profile import update_profile
from gui.v2.ui.pages.Page import Page 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.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
@ -83,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:
@ -127,43 +117,8 @@ 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()
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() 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 = {}
for label in self.labels: for label in self.labels:
@ -176,10 +131,7 @@ 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 = ProfileController.get_all()
new_profiles = self._prefetched_profiles
else:
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)
self.data_profile = self.profiles_data[selected_profiles_str].copy( self.data_profile = self.profiles_data[selected_profiles_str].copy(
@ -794,27 +746,51 @@ 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_id = int(self.update_status.current_profile_id) profile = ProfileController.get(
int(self.update_status.current_profile_id))
self.update_res = False
if key == 'dimentions':
profile.resolution = new_value
self.update_res = True
# Sends to core directly, elif key == 'name':
result = update_profile(profile_id, key, new_value) profile.name = new_value
if result.valid: elif key == 'connection':
self.update_status.update_status( if new_value == 'tor':
f'Updated profile {profile_id}') profile.connection.code = new_value
profile.connection.masked = True
elif new_value == 'just proxy':
profile.connection.code = 'system'
profile.connection.masked = True
else:
self.update_status.update_status(
'System wide profiles not supported atm')
if key == "resolution": elif key == 'browser':
self.update_res = True browser_type, browser_version = new_value.split(':', 1)
profile.application_version.application_code = browser_type
profile.application_version.version_number = browser_version
if result.data == "edit_to_session": elif key == 'protocol':
if self.data_profile.get('connection') == 'system-wide':
self.edit_to_session() self.edit_to_session()
else:
profile.connection.code = 'wireguard' if new_value == 'wireguard' else 'tor'
profile.connection.masked = False if new_value == 'wireguard' else True
else: else:
self.update_status.update_status(result.message) location = self.connection_manager.get_location_info(new_value)
if location:
profile.location.code = location.code
profile.location.country_code = location.country_code
profile.location.time_zone = location.time_zone
profile.subscription = None
ProfileController.update(profile)
def edit_to_session(self): def edit_to_session(self):
id = int(self.update_status.current_profile_id) id = int(self.update_status.current_profile_id)

View file

@ -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,

View file

@ -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():

View file

@ -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")
provider = locations.operator.name if locations and hasattr(
if locations: locations, 'operator') else None
try:
provider = locations.operator.name if locations and hasattr(
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

View file

@ -213,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}") location = f'{profile.location.country_code}_{profile.location.code}'
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}'
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
@ -269,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

View file

@ -1,199 +0,0 @@
import os
from PyQt6.QtWidgets import QLabel, QPushButton, QWidget
from PyQt6.QtGui import QPixmap
from PyQt6.QtCore import Qt, QTimer
from core.services.helpers.setup_sudo_scripts import auto_install_sudo_script, test_if_in_sudo_folder
from core.services.helpers.manage_assets import sudo_assets_folder_setup
from core.models.Result import Result, ResultError
import sys
from gui.v2.ui.pages.Page import Page
class NetworkingSetupPage(Page):
def __init__(self, page_stack, main_window=None, parent=None):
super().__init__("NetworkingSetup", page_stack, main_window, parent)
self.btn_path = main_window.btn_path
self.update_status = main_window
self.manual_scripts_ready = False
self.setStyleSheet("font-family: Arial;")
self.button_next.clicked.disconnect(self.gestionar_next)
self.button_next.setVisible(True)
self.button_next.clicked.connect(self.go_to_install)
self._setup_ui()
def showEvent(self, event):
super().showEvent(event)
self.button_next.setVisible(True)
self.button_next.raise_()
def _setup_ui(self):
header_icon = self._icon_label("networking_shield.png", 48, self)
header_icon.setGeometry(74, 66, 48, 48)
title = QLabel("Firewall & Managed-DNS Setup", self)
title.setGeometry(138, 68, 590, 44)
title.setStyleSheet("font-family: Arial; font-size: 26px; font-weight: bold; color: cyan;")
subtitle = QLabel(
"Optional privileged networking helpers",
self)
subtitle.setGeometry(140, 108, 560, 24)
subtitle.setStyleSheet("font-family: Arial; font-size: 14px; color: #d8ffff;")
description = QLabel(
"These firewall and managed-DNS helpers are separate bash scripts. "
"They stay outside the AppImage Python runtime, and if installed they are placed "
"in sudo-protected folders owned by root.",
self)
description.setGeometry(80, 152, 640, 72)
description.setWordWrap(True)
description.setStyleSheet("font-family: Arial; font-size: 15px; color: cyan;")
manual_panel = self._option_panel(74, 248, "Option 1", "Manual review")
manual_text = QLabel(
"Copy the scripts to Downloads, review them, then run sudo bash setup.sh yourself.",
manual_panel)
manual_text.setGeometry(24, 76, 252, 48)
manual_text.setWordWrap(True)
manual_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
self.manual_button = QPushButton("Get scripts", manual_panel)
self.manual_button.setGeometry(24, 132, 252, 38)
self.manual_button.setStyleSheet(self._button_style("#007AFF", "#0056CC"))
self.manual_button.clicked.connect(self.handle_manual_setup)
auto_panel = self._option_panel(426, 248, "Option 2", "Automated installer")
auto_text = QLabel(
"Let HydraVeil run the installer, then test that the scripts reached the sudo folder.",
auto_panel)
auto_text.setGeometry(24, 76, 265, 48)
auto_text.setWordWrap(True)
auto_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
self.auto_button = QPushButton("Automated installer", auto_panel)
self.auto_button.setGeometry(24, 132, 252, 38)
self.auto_button.setStyleSheet(self._button_style("#16a085", "#117a65"))
self.auto_button.clicked.connect(self.install_sudo_scripts_automatically)
self.status_label = QLabel("Choose manual or automated setup, or press Next to skip and continue.", self)
self.status_label.setGeometry(82, 462, 636, 38)
self.status_label.setWordWrap(True)
self.status_label.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
def _option_panel(self, x, y, label, title):
panel = QWidget(self)
panel.setObjectName("networkingOptionPanel")
panel.setGeometry(x, y, 300, 186)
panel.setStyleSheet("""
QWidget#networkingOptionPanel {
background-color: #17323d;
border: 1px solid #2c7a8d;
border-radius: 6px;
}
""")
option_label = QLabel(label, panel)
option_label.setGeometry(24, 20, 120, 20)
option_label.setStyleSheet("font-family: Arial; font-size: 11px; font-weight: bold; color: #8de9ff; border: none;")
title_label = QLabel(title, panel)
title_label.setGeometry(24, 42, 240, 28)
title_label.setStyleSheet("font-family: Arial; font-size: 18px; font-weight: bold; color: cyan; border: none;")
return panel
def _icon_label(self, icon_name, size, parent):
label = QLabel(parent)
icon_path = os.path.join(self.btn_path, icon_name)
label.setPixmap(QPixmap(icon_path).scaled(
size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
label.setFixedSize(size, size)
label.setStyleSheet("border: none;")
return label
def _button_style(self, background, hover):
return f"""
QPushButton {{
background: {background};
color: #f4ffff;
border: none;
border-radius: 4px;
font-family: Arial;
font-size: 12px;
font-weight: bold;
padding: 4px 8px;
}}
QPushButton:hover {{
background: {hover};
}}
QPushButton:disabled {{
background: #3f4954;
color: #94a3ad;
}}
"""
def _set_status(self, message, color="#d8ffff"):
self.status_label.setText(message)
self.status_label.setStyleSheet(f"font-family: Arial; font-size: 13px; color: {color};")
self.update_status.update_status(message)
def _result_is_valid(self, result: Result):
if hasattr(result, "valid"):
return result.valid
if result is None:
return True
return bool(result)
def _result_message(self, result: Result, fallback):
return getattr(result, "message", fallback)
def handle_manual_setup(self):
if self.manual_scripts_ready:
self.confirm_sudo_scripts()
return
results = sudo_assets_folder_setup()
if not self._result_is_valid(results):
self._set_status(
f"Could not copy scripts: {self._result_message(results, 'Unknown error')}", "#ff6b6b")
return
self.manual_scripts_ready = True
self.manual_button.setText("Confirm manual install")
self._set_status(
"Copied to Downloads/hydraveil_sudo_scripts. Review them, run 'sudo bash setup.sh', then click Confirm manual install.", "#d8ffff")
def confirm_sudo_scripts(self):
confirmed = test_if_in_sudo_folder()
if confirmed.valid:
self._set_status("Confirmed it worked. Continuing to prerequisite installation.", "#2ecc71")
QTimer.singleShot(600, self.go_to_install)
else:
self._set_status(
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
def install_sudo_scripts_automatically(self):
results = auto_install_sudo_script()
if results.valid:
confirmed = test_if_in_sudo_folder()
if confirmed.valid:
self._set_status("The setup script ran successfully. Continuing to prerequisite installation.", "#2ecc71")
QTimer.singleShot(600, self.go_to_install)
else:
self._set_status(
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
else:
self._set_status(
f"Setup script did NOT work because {results.message}", "#ff6b6b")
def go_to_install(self):
self.custom_window.navigator.navigate("install_system_package")
install_page = self.custom_window.navigator.get_cached("install_system_package")
if install_page is not None:
install_page.configure(package_name='all', distro='debian')
def back_to_welcome(self):
self.custom_window.navigator.navigate("welcome")

View file

@ -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)

View file

@ -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,
@ -17,7 +19,6 @@ from core.Constants import Constants
from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.PolicyController import PolicyController from core.controllers.PolicyController import PolicyController
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
from core.controllers.SystemStateController import SystemStateController
from core.controllers.tickets.UseTicketController import ( from core.controllers.tickets.UseTicketController import (
do_we_use_a_random_ticket, do_we_use_a_random_ticket,
get_unused_tickets, get_unused_tickets,
@ -32,9 +33,7 @@ from core.Errors import (
PolicyRevocationError, PolicyRevocationError,
) )
from core.errors.logger import logger as core_logger from core.errors.logger import logger as core_logger
from core.services.helpers.setup_sudo_scripts import test_if_in_sudo_folder
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
@ -44,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)
@ -70,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)
@ -105,7 +95,6 @@ class Settings(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), ("Bwrap Permission", self.show_bwrap_page),
("Connections", self.show_connection_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)
@ -209,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", getattr(self.update_status, 'gui_config_file', None),
lambda: normalize_profile_order( profiles.keys())
getattr(self.update_status, 'gui_config_file', None),
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]
@ -304,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)
@ -613,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)}
@ -650,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}")
@ -682,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:
@ -689,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()
@ -704,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...")
@ -794,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")
@ -831,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(
@ -966,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(
@ -1062,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()
@ -1085,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:
@ -1153,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:
@ -1176,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()
@ -1231,10 +1326,6 @@ class Settings(Page):
self.bwrap_page = self.create_bwrap_page() self.bwrap_page = self.create_bwrap_page()
self.content_layout.addWidget(self.bwrap_page) self.content_layout.addWidget(self.bwrap_page)
self.content_layout.removeWidget(self.connection_page)
self.connection_page = self.create_connection_page()
self.content_layout.addWidget(self.connection_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)
@ -1316,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))
@ -1348,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()
@ -1449,7 +1539,6 @@ class Settings(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.bwrap_page = self.create_bwrap_page()
self.connection_page = self.create_connection_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()
@ -1461,7 +1550,6 @@ class Settings(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.bwrap_page)
self.content_layout.addWidget(self.connection_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)
@ -1520,15 +1608,6 @@ class Settings(Page):
self.content_layout.setCurrentWidget(self.bwrap_page) self.content_layout.setCurrentWidget(self.bwrap_page)
self._select_menu_button("Bwrap Permission") self._select_menu_button("Bwrap Permission")
def show_connection_page(self):
core_logger.info("User navigated to Settings -> Connection")
if hasattr(self, "connection_gate_status"):
self.load_firewall_settings()
self.load_managed_dns_settings()
self.refresh_connection_settings_gate()
self.content_layout.setCurrentWidget(self.connection_page)
self._select_menu_button("Connections")
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)
@ -1618,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")
@ -1627,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)
@ -1681,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'):
@ -1701,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:
@ -1762,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.")
@ -1857,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"])
@ -2007,8 +2103,7 @@ 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 create_bwrap_page(self): def create_bwrap_page(self):
@ -2060,13 +2155,17 @@ class Settings(Page):
layout.addLayout(button_layout) layout.addLayout(button_layout)
layout.addStretch() layout.addStretch()
self.load_bwrap_settings( self.load_bwrap_settings()
self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
return page return page
def load_systemwide_settings(self, enabled=None): def load_systemwide_settings(self):
if enabled is None: enabled = False
enabled = settings_data.read_systemwide_enabled() 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")
@ -2103,9 +2202,14 @@ 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=None): def load_bwrap_settings(self):
if enabled is None: enabled = False
enabled = settings_data.read_bwrap_enabled() 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) self.bwrap_toggle.setChecked(enabled)
if enabled: if enabled:
self.bwrap_status_value.setText("Enabled") self.bwrap_status_value.setText("Enabled")
@ -2116,35 +2220,6 @@ class Settings(Page):
self.bwrap_status_value.setStyleSheet( self.bwrap_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}") f"color: #e67e22; font-size: 14px; {self.font_style}")
def load_firewall_settings(self, enabled=None):
if enabled is None:
enabled = self.read_gui_connection_setting("firewall_enabled")
self.firewall_toggle.setChecked(enabled)
if enabled:
self.firewall_status_value.setText("Enabled")
self.firewall_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.firewall_status_value.setText("Disabled")
self.firewall_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
def load_managed_dns_settings(self, enabled=None):
if enabled is None:
enabled = self.read_gui_connection_setting("managed_dns_enabled")
self.managed_dns_toggle.setChecked(enabled)
if enabled:
self.managed_dns_status_value.setText("Enabled")
self.managed_dns_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.managed_dns_status_value.setText("Disabled")
self.managed_dns_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
def save_bwrap_settings(self): def save_bwrap_settings(self):
enable = self.bwrap_toggle.isChecked() enable = self.bwrap_toggle.isChecked()
try: try:
@ -2171,113 +2246,9 @@ class Settings(Page):
self.bwrap_status_value.setStyleSheet( self.bwrap_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}") f"color: red; font-size: 14px; {self.font_style}")
def save_connection_settings(self):
can_modify, message, _ = self.connection_settings_gate()
if not can_modify:
self.connection_gate_status.setText(message)
self.connection_gate_status.setStyleSheet(
f"color: #ff6b6b; font-size: 12px; {self.font_style}")
self.set_connection_controls_enabled(False)
self.update_status.update_status(message)
return
firewall_enabled = self.firewall_toggle.isChecked()
managed_dns_enabled = self.managed_dns_toggle.isChecked()
try:
self.save_gui_connection_settings(firewall_enabled, managed_dns_enabled)
self.load_firewall_settings(firewall_enabled)
self.load_managed_dns_settings(managed_dns_enabled)
self.update_status.update_status(
"Connection settings updated")
except CommandNotFoundError as e:
self.firewall_status_value.setText(str(e))
self.firewall_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
except Exception as e:
self.firewall_status_value.setText(f"Failed to update connection settings {e}")
self.firewall_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def save_firewall_settings(self):
self.save_connection_settings()
def read_gui_connection_setting(self, key):
try:
config = self.update_status._load_gui_config()
if config and "connection_settings" in config:
return bool(config["connection_settings"].get(key, False))
except Exception:
pass
return False
def save_gui_connection_settings(self, firewall_enabled, managed_dns_enabled):
config = self.update_status._load_gui_config()
if config is None:
config = {}
if "connection_settings" not in config:
config["connection_settings"] = {}
config["connection_settings"]["firewall_enabled"] = firewall_enabled
config["connection_settings"]["managed_dns_enabled"] = managed_dns_enabled
self.update_status._save_gui_config(config)
self._prepared["firewall_setting"] = firewall_enabled
self._prepared["managed_dns_setting"] = managed_dns_enabled
def connection_settings_gate(self):
installed, install_message = self.networking_scripts_installed()
if not installed:
return False, f"Firewall and managed-DNS helpers are not installed. {install_message}", True
if self.has_active_systemwide_profile():
return False, "Disable the active systemwide profile before changing firewall or managed-DNS settings.", False
return True, "Firewall and managed-DNS helpers are installed. No active systemwide profile detected.", False
def networking_scripts_installed(self):
try:
result = test_if_in_sudo_folder()
if result.valid:
return True, "Installed."
return False, result.message
except Exception as e:
return False, str(e)
def has_active_systemwide_profile(self):
try:
return SystemStateController.get() is not None
except Exception:
return True
def set_connection_controls_enabled(self, enabled):
for widget_name in ("firewall_toggle", "managed_dns_toggle", "connection_save_button"):
widget = getattr(self, widget_name, None)
if widget is not None:
widget.setEnabled(enabled)
def refresh_connection_settings_gate(self):
can_modify, message, show_install = self.connection_settings_gate()
self.connection_gate_status.setText(message)
color = "#2ecc71" if can_modify else "#ff6b6b"
self.connection_gate_status.setStyleSheet(
f"color: {color}; font-size: 12px; {self.font_style}")
self.set_connection_controls_enabled(can_modify)
self.connection_install_button.setVisible(show_install)
def open_networking_installer(self):
self.custom_window.navigator.navigate("networking_setup")
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)
@ -2336,103 +2307,25 @@ class Settings(Page):
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:
def create_connection_page(self): try:
page = QWidget() config = self.update_status._load_gui_config()
layout = QVBoxLayout(page) if config and "registrations" in config:
layout.setSpacing(16) registrations = config["registrations"]
layout.setContentsMargins(20, 20, 20, 20) return registrations.get("fast_registration_enabled", False)
return False
title = QLabel("CONNECTION PAGE") except Exception as e:
title.setStyleSheet( logging.error(
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") f"Error checking fast registration setting: {str(e)}")
layout.addWidget(title) return False
description = QLabel(
"Control firewall and managed-DNS behavior for systemwide profiles.")
description.setWordWrap(True)
description.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
layout.addWidget(description)
self.connection_gate_status = QLabel("")
self.connection_gate_status.setWordWrap(True)
self.connection_gate_status.setStyleSheet(
f"color: #e67e22; font-size: 12px; {self.font_style}")
layout.addWidget(self.connection_gate_status)
self.connection_install_button = QPushButton("Install networking helpers")
self.connection_install_button.setFixedSize(190, 38)
self.connection_install_button.setStyleSheet(f"""
QPushButton {{
background: #007AFF;
color: white;
border: none;
border-radius: 5px;
font-weight: bold;
{self.font_style}
}}
QPushButton:hover {{
background: #0056CC;
}}
""")
self.connection_install_button.clicked.connect(self.open_networking_installer)
layout.addWidget(self.connection_install_button)
firewall_status_layout = QHBoxLayout()
firewall_status_label = QLabel("Firewall status:")
firewall_status_label.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
self.firewall_status_value = QLabel("")
self.firewall_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
firewall_status_layout.addWidget(firewall_status_label)
firewall_status_layout.addWidget(self.firewall_status_value)
firewall_status_layout.addStretch()
layout.addLayout(firewall_status_layout)
dns_status_layout = QHBoxLayout()
dns_status_label = QLabel("Managed-DNS status:")
dns_status_label.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
self.managed_dns_status_value = QLabel("")
self.managed_dns_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
dns_status_layout.addWidget(dns_status_label)
dns_status_layout.addWidget(self.managed_dns_status_value)
dns_status_layout.addStretch()
layout.addLayout(dns_status_layout)
firewall_toggle_layout = QHBoxLayout()
self.firewall_toggle = QCheckBox("Enable Firewall Automatically when Systemwide Profiles are Activated")
self.firewall_toggle.setStyleSheet(self.get_checkbox_style())
firewall_toggle_layout.addWidget(self.firewall_toggle)
firewall_toggle_layout.addStretch()
layout.addLayout(firewall_toggle_layout)
dns_toggle_layout = QHBoxLayout()
self.managed_dns_toggle = QCheckBox("Enable Managed-DNS Automatically when Systemwide Profiles are Activated")
self.managed_dns_toggle.setStyleSheet(self.get_checkbox_style())
dns_toggle_layout.addWidget(self.managed_dns_toggle)
dns_toggle_layout.addStretch()
layout.addLayout(dns_toggle_layout)
self.connection_save_button = QPushButton()
self.connection_save_button.setFixedSize(75, 46)
self.connection_save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
self.connection_save_button.setIconSize(QSize(75, 46))
self.connection_save_button.clicked.connect(self.save_connection_settings)
button_layout = QHBoxLayout()
button_layout.addWidget(self.connection_save_button)
button_layout.addStretch()
layout.addLayout(button_layout)
layout.addStretch()
self.load_firewall_settings(
self._prepared_value("firewall_setting", lambda: self.read_gui_connection_setting("firewall_enabled")))
self.load_managed_dns_settings(
self._prepared_value("managed_dns_setting", lambda: self.read_gui_connection_setting("managed_dns_enabled")))
self.refresh_connection_settings_gate()
return page

View file

@ -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")

View file

@ -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")

View file

@ -13,7 +13,7 @@ class WelcomePage(Page):
self.btn_path = main_window.btn_path self.btn_path = main_window.btn_path
self.update_status = main_window self.update_status = main_window
self.ui_elements = [] self.ui_elements = []
self.button_next.clicked.connect(self.go_to_networking_setup) self.button_next.clicked.connect(self.go_to_install)
self.button_next.setVisible(True) self.button_next.setVisible(True)
self._setup_welcome_ui() self._setup_welcome_ui()
self._setup_stats_display() self._setup_stats_display()
@ -30,7 +30,7 @@ class WelcomePage(Page):
welcome_msg = QLabel( welcome_msg = QLabel(
"Before we begin your journey, we need to set up a few essential components. " "Before we begin your journey, we need to set up a few essential components. "
"Click 'Next' to review the optional networking setup.", self) "Click 'Next' to take you to the installation page.", self)
welcome_msg.setGeometry(40, 100, 720, 80) welcome_msg.setGeometry(40, 100, 720, 80)
welcome_msg.setWordWrap(True) welcome_msg.setWordWrap(True)
welcome_msg.setStyleSheet(""" welcome_msg.setStyleSheet("""
@ -95,7 +95,7 @@ class WelcomePage(Page):
"font-size: 16px; font-weight: bold; color: #2c3e50;") "font-size: 16px; font-weight: bold; color: #2c3e50;")
title_layout.addWidget(title_label) title_layout.addWidget(title_label)
status_indicator = QLabel(chr(9679)) status_indicator = QLabel("")
status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;") status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;")
title_layout.addWidget(status_indicator) title_layout.addWidget(status_indicator)
@ -117,5 +117,8 @@ class WelcomePage(Page):
grid_layout.addWidget(stat_widget, row, col) grid_layout.addWidget(stat_widget, row, col)
def go_to_networking_setup(self): def go_to_install(self):
self.custom_window.navigator.navigate("networking_setup") self.custom_window.navigator.navigate("install_system_package")
install_page = self.custom_window.navigator.get_cached("install_system_package")
if install_page is not None:
install_page.configure(package_name='all', distro='debian')

View file

@ -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 = "Upgrade Time!"
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

View file

@ -1,10 +0,0 @@
from PyQt6.QtWidgets import QApplication, QMessageBox
import sys
def show_error(error_text, title="Error"):
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
QMessageBox.critical(None, title, error_text)
app.quit()

View file

@ -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)

View file

@ -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)

View file

@ -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;
}
"""

View file

@ -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 {{

View file

@ -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}")

View file

@ -10,8 +10,6 @@ from core.controllers.tickets.UseTicketController import (
) )
from core.models.session.SessionProfile import SessionProfile from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemProfile import SystemProfile
from core.models.Result import Result, ResultError
from core.errors.exceptions import SudoScript, MissingPreReqs
from core.Errors import ( from core.Errors import (
CommandNotFoundError, CommandNotFoundError,
EndpointVerificationError, EndpointVerificationError,
@ -116,10 +114,6 @@ class Worker(QObject):
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None) "PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
except CommandNotFoundError as e: except CommandNotFoundError as e:
self.update_signal.emit(str(e.subject), False, -1, None, None) self.update_signal.emit(str(e.subject), False, -1, None, None)
except SudoScript as e:
self.update_signal.emit(str(e), False, None, None, None)
except MissingPreReqs as e:
self.update_signal.emit(str(e), False, None, None, None)
except Exception as e: except Exception as e:
print(e) print(e)
self.update_signal.emit( self.update_signal.emit(

View file

@ -14,10 +14,7 @@ from core.controllers.SubscriptionPlanController import SubscriptionPlanControll
from core.models.session.SessionConnection import SessionConnection 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.BaseProfile import ProfileType
from core.models.system.SystemProfile import SystemProfile from core.models.system.SystemProfile import SystemProfile
from core.errors.exceptions import SudoScript, MissingPreReqs
from core.models.Result import Result, ResultError
from gui.v2.infrastructure.setup_observers import ( from gui.v2.infrastructure.setup_observers import (
client_observer, client_observer,
@ -135,10 +132,6 @@ class WorkerThread(QThread):
ProfileController.disable( ProfileController.disable(
profile, ignore=True, profile_observer=profile_observer) profile, ignore=True, profile_observer=profile_observer)
self.text_output.emit("All profiles were successfully disabled") self.text_output.emit("All profiles were successfully disabled")
# except SudoScript as e:
# self.text_output.emit(e)
# except MissingPreReqs as e:
# self.text_output.emit(e)
except Exception: except Exception:
self.text_output.emit("An error occurred when disabling profile") self.text_output.emit("An error occurred when disabling profile")
finally: finally:
@ -196,23 +189,12 @@ class WorkerThread(QThread):
resolution = self.profile_data['resolution'] resolution = self.profile_data['resolution']
connection = SessionConnection(connection_type, mask_connection) connection = SessionConnection(connection_type, mask_connection)
profile = SessionProfile( profile = SessionProfile(
id=profile_id, profile_id, name, None, location, resolution, application_version, connection)
name=name,
subscription=None,
type=ProfileType.SESSION,
location=location,
resolution=resolution,
application_version=application_version,
connection=connection)
elif profile_type == "system": elif profile_type == "system":
connection = SystemConnection(connection_type) connection = SystemConnection(connection_type)
profile = SystemProfile( profile = SystemProfile(
id=profile_id, profile_id, name, None, location, connection)
name=name,
type=ProfileType.SYSTEM,
subscription=None,
location=location,
connection=connection)
else: else:
self.text_output.emit(f"Invalid profile type: {profile_type}") self.text_output.emit(f"Invalid profile type: {profile_type}")
return return
@ -235,10 +217,6 @@ class WorkerThread(QThread):
else: else:
self.text_output.emit( self.text_output.emit(
f"No profile found with ID: {self.profile_data['id']}") f"No profile found with ID: {self.profile_data['id']}")
# except SudoScript as e:
# self.text_output.emit(str(e))
# except MissingPreReqs as e:
# self.text_output.emit(str(e))
except Exception as e: except Exception as e:
self.text_output.emit("An error occurred when disabling profile") self.text_output.emit("An error occurred when disabling profile")
finally: finally:
@ -251,12 +229,8 @@ class WorkerThread(QThread):
else: else:
ConfigurationController.set_connection('system') ConfigurationController.set_connection('system')
self.check_for_update() self.check_for_update()
locations = LocationController.get_all() locations = LocationController.get_all()
browser = ApplicationVersionController.get_all() browser = ApplicationVersionController.get_all()
# print('the browser is: ', browser)
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 = [

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-gui" name = "sp-hydra-veil-gui"
version = "2.4.7" 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.4.3", "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"