Changed the app startup file to first do a database Compatibility check, then launch the main GUI only after all is clear

This commit is contained in:
SimplifiedPrivacy 2026-06-28 21:50:22 -04:00
parent 33301b0da8
commit c508dddcac
5 changed files with 1332 additions and 906 deletions

961
gui/__main__.py Executable file → Normal file
View file

@ -1,909 +1,94 @@
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
from core.models.manage.version_check import check_database_compatibility, insert_new_version, get_custom_message
from core.Constants import Constants
# generic
import os import os
import sys import sys
_WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # ============================================================================
if _WORKSPACE_ROOT not in sys.path: # INITIALIZE DATABASE
sys.path.insert(0, _WORKSPACE_ROOT) # ============================================================================
import logging # (engine, Session, _session all initialized from session_management)
import traceback init_session()
session = get_session()
create_ONLY_db_version_table()
from PyQt6.QtWidgets import ( # ============================================================================
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton, # COMPARE DB VERISONS
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 compatability_dict = check_database_compatibility(session)
from core.controllers.ConfigurationController import ConfigurationController reason = compatability_dict.get("reason", "error")
from core.Errors import UnknownConnectionTypeError is_compatable = compatability_dict.get("result", False)
from core.errors.logger import logger as core_logger logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}")
core_logger.propagate = False # ============================================================================
# IF THE VERSIONS MATCH
# ============================================================================
if is_compatable:
made_tables = create_ALL_tables()
from gui.v2.infrastructure import orm # SCREEN FAILED TABLES
from gui.v2.infrastructure.navigator import Navigator if not made_tables:
from gui.v2.infrastructure.setup_observers import setup_observers logger.error(f"[DB MANAGEMENT] Critical Failure with starting the models of the database.")
from gui.v2.infrastructure.connection_manager import ConnectionManager close_session()
from gui.v2.infrastructure.page_registry import ( sys.exit()
PRELOAD_ORDER,
PRELOAD_SKIP,
ASYNC_PREPARE,
REGULAR_FLOW_ONLY,
PAGE_REGISTRY,
)
from gui.v2.workers.worker_thread import WorkerThread
from gui.v2.workers.page_data_worker import PageDataWorker
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,
is_fast_registration_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
# ASSUME TABLES CREATED
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing")
class CustomWindow(QMainWindow): # original reason was he had no database, now tag it that he's got the new version,
def __init__(self): if reason == "no_database":
super().__init__() logger.info(f"[DB MANAGEMENT] User had no database to begin with, so after we created the database, we are about to insert the new version in the DB metadata table for next time.")
sys.excepthook = self._handle_exception
self.setWindowFlags(Qt.WindowType.Window)
gui_dir = os.path.dirname(os.path.abspath(__file__)) # UPDATE DATA
font_path = os.path.join(gui_dir, 'resources', 'fonts') did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
if not did_it_insert:
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
# Get's SQLAlchemy going, logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window...")
orm.start() from main_ui import start_ui
force_sync = False
start_ui(force_sync)
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf') # ============================================================================
font_id = QFontDatabase.addApplicationFont(retro_gaming_path) # but IF the versions do NOT match
font_family = QFontDatabase.applicationFontFamilies(font_id)[0] # ============================================================================
app_font = QFont(font_family) else:
app_font.setPointSize(10) logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
QApplication.setFont(app_font) db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
self.open_sans_family = None # WHAT IS THE REASON?
open_sans_path = os.path.join(font_path, 'open-sans.ttf') custom_error = get_custom_message(reason, compatability_dict)
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) # shut down db connection
self.setWindowTitle('HydraVeil') close_session()
screen = QApplication.primaryScreen() # ============================================================================
ratio = screen.devicePixelRatio() # THEN DISPLAY CHOICES AND RECOVERY UI
self.host_screen_width = int(screen.size().width() * ratio) # ============================================================================
self.host_screen_height = int(screen.size().height() * ratio) from v2.ui.popups.Database_version import show_recovery_dialog
choice = show_recovery_dialog({"message": custom_error, "db_version": db_version_you_have, "status": "version_mismatch"})
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
self._data = {"Profile_1": {}} system_path = get_path()
database_path = system_path / "storage.db"
self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui') if choice == 1:
self.gui_config_file = os.path.join(self.gui_config_home, 'config.json') if os.path.exists(database_path):
self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui') os.remove(database_path)
self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log') logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}")
self._gui_log_handlers = [] logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..")
sys.exit()
setup_gui_directory( elif choice == 2:
self.gui_cache_home, self.gui_config_home, self.gui_config_file) logger.info(f"[DB MANAGEMENT] User opted to move the DB file.")
from v2.ui.popups.pick_folder_to_move import launch_file_picker
launch_file_picker(database_path)
sys.exit()
self.btn_path = os.getenv('BTN_PATH', os.path.join(
gui_dir, 'resources', 'images'))
self.css_path = os.getenv('CSS_PATH', os.path.join(
gui_dir, 'resources', 'styles'))
self.is_downloading = False
self.current_profile_id = None
self.connection_manager = ConnectionManager()
current_connection = None
self.is_tor_mode = current_connection == 'tor'
self.is_animating = False
self.animation_step = 0
self.page_history = []
self.log_path = None
self.setFixedSize(800, 570)
top = create_top_section(self, self.css_path)
self.__dict__.update(top)
self.marquee_text = ""
self.marquee_position = 0
self.marquee_enabled = False
self.scroll_speed = 1
self.page_stack = QStackedWidget(self)
self.page_stack.setGeometry(0, 0, 800, 570)
bottom = create_bottom_section(
self,
self.btn_path,
Constants.HV_CLIENT_VERSION_NUMBER,
should_be_synchronized(),
)
self.__dict__.update(bottom)
self.status_label = bottom['status_label']
self.tor_text = bottom['tor_text']
self.toggle_button = bottom['toggle_button']
self.tor_icon = bottom['tor_icon']
self.sync_button = bottom['sync_button']
self.page_stack.raise_()
self.observers = setup_observers(self.update_status)
self.update_image()
self.set_scroll_speed(7)
self.toggle_button.clicked.connect(self.toggle_mode)
self.sync_button.clicked.connect(self.sync)
QApplication.instance().installEventFilter(self)
self.create_toggle(self.is_tor_mode)
self.animation_timer = QTimer()
self.animation_timer.timeout.connect(self.animate_toggle)
self.check_logging()
self.navigator = Navigator(self.page_stack, self)
self._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)
self.page_stack.currentChanged.connect(self.page_changed)
self.show()
self.init_ui()
self.tray = None
self.tray = init_system_tray(self, self.btn_path)
current_connection = self.get_current_connection()
self.is_tor_mode = current_connection == 'tor'
self.set_toggle_state(self.is_tor_mode)
core_logger.info(
f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, "
f"connection={current_connection}, tor_mode={self.is_tor_mode})"
)
def init_ui(self):
if self.check_first_launch():
self.navigator.navigate("welcome")
else: else:
self.navigator.navigate("menu") logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
self._start_background_preload() sys.exit()
def _start_background_preload(self):
import time
self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP]
fast_mode = is_fast_registration_enabled(self.gui_config_file)
skipped_regular = []
if fast_mode:
skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY]
self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY]
self._preload_total = len(self._preload_queue)
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
if fast_mode:
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
self._preload_timer.start(700)
def _preload_next(self):
while self._preload_queue and self._preload_queue[0] in self.navigator._instances:
skipped = self._preload_queue.pop(0)
self._preload_skipped_runtime += 1
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
if not self._preload_queue:
self._maybe_emit_preload_summary()
return
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
ok = self.navigator.preload(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 _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, module_path, class_name, self)
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.navigator._instances:
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
else:
try:
page = self.navigator._instantiate(name, prepared=payload)
self.navigator.register_instance(name, page)
self._preload_done += 1
print(f"[preload] {name:30s} OK (async, cached={len(self.navigator._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
except Exception as e:
self._preload_failed += 1
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
if name not in self.navigator._instances:
ok = self.navigator.preload(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):
import time
total_s = time.perf_counter() - self._preload_t_start
print(f"[preload] ============================================================", flush=True)
print(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_failed} failed, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
f"in {total_s:.2f}s", flush=True)
print(f"[preload] cached pages now: {sorted(self.navigator._instances.keys())}", flush=True)
print(f"[preload] ============================================================", flush=True)
def perform_update_check(self):
from core.controllers.ClientController import ClientController
update_available = ClientController.can_be_updated()
if update_available:
menu_page = self.navigator.get_cached("menu")
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):
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)
]
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")
# Close SQLAlchemy,
orm.stop()
connected_profiles = self.connection_manager.get_connected_profiles()
if connected_profiles:
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
if event is not None:
event.accept()
else:
if event is not None:
event.accept()
def update_image(self, appearance_value="original"):
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
pixmap = QPixmap(image_path)
self.label.setPixmap(pixmap)
self.label.setScaledContents(True)
def read_data(self):
return read_profile_data(self._data)
def write_data(self, data):
write_profile_data(self._data, data)
def clear_data(self):
self._data = clear_profile_data()
def _set_status_font_size(self, font_size):
self.status_label.setStyleSheet(
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
def update_status(self, text, clear=False):
if text is None:
self._set_status_font_size(16)
self.status_label.setText('Status:')
self.disable_marquee()
return
if clear:
self._set_status_font_size(16)
self.status_label.setText('')
return
full_text = 'Status: ' + text
metrics = self.status_label.fontMetrics()
available_width = self.status_label.width() - 10
if metrics.horizontalAdvance(full_text) > available_width:
self.enable_marquee(text)
else:
self.status_label.setText(full_text)
self.disable_marquee()
def check_first_launch(self):
return check_first_launch(self.gui_config_file)
def has_shown_fast_mode_prompt(self):
return has_shown_fast_mode_prompt(self.gui_config_file)
def mark_fast_mode_prompt_shown(self):
mark_fast_mode_prompt_shown(self.gui_config_file)
def set_fast_mode_enabled(self, enabled):
set_fast_mode_enabled(self.gui_config_file, enabled)
def navigate_after_profile_created(self):
if not self.has_shown_fast_mode_prompt():
self.navigator.navigate("fast_mode_prompt")
else:
menu_page = self.navigator.get_cached("menu")
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
menu_page.refresh_menu_buttons()
self.navigator.navigate("menu")
def enable_marquee(self, text):
self.marquee_text = text + " "
self.marquee_position = 0
self.marquee_enabled = True
self.marquee_timer.start(500)
def disable_marquee(self):
self.marquee_enabled = False
self.marquee_timer.stop()
def update_marquee(self):
if not self.marquee_enabled:
return
metrics = self.status_label.fontMetrics()
text_width = metrics.horizontalAdvance(self.marquee_text)
self.marquee_position += self.scroll_speed
if self.marquee_position >= text_width:
self.marquee_position = 0
looped_text = self.marquee_text * 2
chars_that_fit = metrics.horizontalAdvance(
looped_text[:self.text_width])
visible_text = looped_text[self.marquee_position:
self.marquee_position + chars_that_fit]
while metrics.horizontalAdvance(visible_text) < self.text_width:
visible_text += self.marquee_text
while metrics.horizontalAdvance(visible_text) > self.text_width:
visible_text = visible_text[:-1]
display_text = 'Status: ' + ' ' * \
(self.text_start_x - metrics.horizontalAdvance('Status: '))
display_text += visible_text
self.status_label.setText(display_text)
def set_scroll_speed(self, speed):
self.scroll_speed = speed
def page_changed(self, index):
if should_be_synchronized():
sync_icon = QtGui.QIcon(os.path.join(
self.btn_path, 'icon_sync_urgent.png'))
else:
sync_icon = QtGui.QIcon(os.path.join(
self.btn_path, 'icon_sync.png'))
self.sync_button.setIcon(sync_icon)
current_page = self.page_stack.widget(index)
if self.page_history:
previous_page = self.page_history[-1]
else:
previous_page = None
current_name = getattr(current_page, 'name', None)
previous_name = getattr(previous_page, 'name', None)
if current_name == 'Menu':
self.update_status(None)
if previous_name in ('Resume', 'Editor', 'Settings', 'FastRegistration', 'FastModePrompt'):
if hasattr(current_page, 'eliminacion'):
current_page.eliminacion()
elif current_name == 'Resume':
self.update_status('Choose a name for your profile')
if hasattr(current_page, 'eliminacion'):
current_page.eliminacion()
elif current_name == 'Id':
pass
elif current_name == 'Location':
self.update_status(None, clear=True)
elif current_name == 'InstallPackage':
self.update_status('Please check package availability.')
else:
self.update_status(None)
if current_name == 'Menu':
self.sync_button.setVisible(True)
self.tor_text.setVisible(True)
self.toggle_button.setGeometry(670, 541, 50, 22)
self.tor_icon.setGeometry(730, 537, 25, 25)
else:
self.sync_button.setVisible(False)
self.tor_text.setVisible(False)
self.toggle_button.setGeometry(540, 541, 50, 22)
self.tor_icon.setGeometry(600, 537, 25, 25)
self.raise_bottom_controls()
if current_page != previous_page:
self.page_history.append(current_page)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = CustomWindow()
sys.exit(app.exec())

Binary file not shown.

912
gui/main_ui.py Executable file
View file

@ -0,0 +1,912 @@
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
)
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 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.infrastructure.page_registry import (
PRELOAD_ORDER,
PRELOAD_SKIP,
ASYNC_PREPARE,
REGULAR_FLOW_ONLY,
PAGE_REGISTRY,
)
from gui.v2.workers.worker_thread import WorkerThread
from gui.v2.workers.page_data_worker import PageDataWorker
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,
is_fast_registration_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):
super().__init__()
self.force_sync = force_sync # passed in from loader
sys.excepthook = self._handle_exception
self.setWindowFlags(Qt.WindowType.Window)
gui_dir = os.path.dirname(os.path.abspath(__file__))
font_path = os.path.join(gui_dir, 'resources', 'fonts')
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
app_font = QFont(font_family)
app_font.setPointSize(10)
QApplication.setFont(app_font)
self.open_sans_family = None
open_sans_path = os.path.join(font_path, 'open-sans.ttf')
if os.path.exists(open_sans_path):
open_sans_id = QFontDatabase.addApplicationFont(open_sans_path)
if open_sans_id != -1:
self.open_sans_family = QFontDatabase.applicationFontFamilies(open_sans_id)[0]
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
self.setWindowTitle('HydraVeil')
screen = QApplication.primaryScreen()
ratio = screen.devicePixelRatio()
self.host_screen_width = int(screen.size().width() * ratio)
self.host_screen_height = int(screen.size().height() * ratio)
self._data = {"Profile_1": {}}
self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui')
self.gui_config_file = os.path.join(self.gui_config_home, 'config.json')
self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui')
self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log')
self._gui_log_handlers = []
setup_gui_directory(
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
self.btn_path = os.getenv('BTN_PATH', os.path.join(
gui_dir, 'resources', 'images'))
self.css_path = os.getenv('CSS_PATH', os.path.join(
gui_dir, 'resources', 'styles'))
self.is_downloading = False
self.current_profile_id = None
self.connection_manager = ConnectionManager()
current_connection = None
self.is_tor_mode = current_connection == 'tor'
self.is_animating = False
self.animation_step = 0
self.page_history = []
self.log_path = None
self.setFixedSize(800, 570)
top = create_top_section(self, self.css_path)
self.__dict__.update(top)
self.marquee_text = ""
self.marquee_position = 0
self.marquee_enabled = False
self.scroll_speed = 1
self.page_stack = QStackedWidget(self)
self.page_stack.setGeometry(0, 0, 800, 570)
bottom = create_bottom_section(
self,
self.btn_path,
Constants.HV_CLIENT_VERSION_NUMBER,
should_be_synchronized(),
)
self.__dict__.update(bottom)
self.status_label = bottom['status_label']
self.tor_text = bottom['tor_text']
self.toggle_button = bottom['toggle_button']
self.tor_icon = bottom['tor_icon']
self.sync_button = bottom['sync_button']
self.page_stack.raise_()
self.observers = setup_observers(self.update_status)
self.update_image()
self.set_scroll_speed(7)
self.toggle_button.clicked.connect(self.toggle_mode)
self.sync_button.clicked.connect(self.sync)
QApplication.instance().installEventFilter(self)
self.create_toggle(self.is_tor_mode)
self.animation_timer = QTimer()
self.animation_timer.timeout.connect(self.animate_toggle)
self.check_logging()
self.navigator = Navigator(self.page_stack, self)
self._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)
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})"
)
if self.force_sync:
self.sync()
def init_ui(self):
if self.check_first_launch():
self.navigator.navigate("welcome")
else:
self.navigator.navigate("menu")
self._start_background_preload()
def _start_background_preload(self):
import time
self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP]
fast_mode = is_fast_registration_enabled(self.gui_config_file)
skipped_regular = []
if fast_mode:
skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY]
self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY]
self._preload_total = len(self._preload_queue)
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
if fast_mode:
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
self._preload_timer.start(700)
def _preload_next(self):
while self._preload_queue and self._preload_queue[0] in self.navigator._instances:
skipped = self._preload_queue.pop(0)
self._preload_skipped_runtime += 1
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
if not self._preload_queue:
self._maybe_emit_preload_summary()
return
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
ok = self.navigator.preload(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 _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, module_path, class_name, self)
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.navigator._instances:
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
else:
try:
page = self.navigator._instantiate(name, prepared=payload)
self.navigator.register_instance(name, page)
self._preload_done += 1
print(f"[preload] {name:30s} OK (async, cached={len(self.navigator._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
except Exception as e:
self._preload_failed += 1
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
if name not in self.navigator._instances:
ok = self.navigator.preload(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):
import time
total_s = time.perf_counter() - self._preload_t_start
print(f"[preload] ============================================================", flush=True)
print(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_failed} failed, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
f"in {total_s:.2f}s", flush=True)
print(f"[preload] cached pages now: {sorted(self.navigator._instances.keys())}", flush=True)
print(f"[preload] ============================================================", flush=True)
def perform_update_check(self):
from core.controllers.ClientController import ClientController
update_available = ClientController.can_be_updated()
if update_available:
menu_page = self.navigator.get_cached("menu")
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):
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)
]
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")
# Close SQLAlchemy,
orm.stop()
connected_profiles = self.connection_manager.get_connected_profiles()
if connected_profiles:
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
if event is not None:
event.accept()
else:
if event is not None:
event.accept()
def update_image(self, appearance_value="original"):
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
pixmap = QPixmap(image_path)
self.label.setPixmap(pixmap)
self.label.setScaledContents(True)
def read_data(self):
return read_profile_data(self._data)
def write_data(self, data):
write_profile_data(self._data, data)
def clear_data(self):
self._data = clear_profile_data()
def _set_status_font_size(self, font_size):
self.status_label.setStyleSheet(
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
def update_status(self, text, clear=False):
if text is None:
self._set_status_font_size(16)
self.status_label.setText('Status:')
self.disable_marquee()
return
if clear:
self._set_status_font_size(16)
self.status_label.setText('')
return
full_text = 'Status: ' + text
metrics = self.status_label.fontMetrics()
available_width = self.status_label.width() - 10
if metrics.horizontalAdvance(full_text) > available_width:
self.enable_marquee(text)
else:
self.status_label.setText(full_text)
self.disable_marquee()
def check_first_launch(self):
return check_first_launch(self.gui_config_file)
def has_shown_fast_mode_prompt(self):
return has_shown_fast_mode_prompt(self.gui_config_file)
def mark_fast_mode_prompt_shown(self):
mark_fast_mode_prompt_shown(self.gui_config_file)
def set_fast_mode_enabled(self, enabled):
set_fast_mode_enabled(self.gui_config_file, enabled)
def navigate_after_profile_created(self):
if not self.has_shown_fast_mode_prompt():
self.navigator.navigate("fast_mode_prompt")
else:
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)
def start_ui(force_sync):
app = QApplication(sys.argv)
window = CustomWindow(force_sync)
sys.exit(app.exec())

View file

@ -0,0 +1,279 @@
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
title_layout = QVBoxLayout()
title_layout.setSpacing(5)
title = QLabel("Database Compatibility")
title.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold))
title.setStyleSheet("color: white;")
title_layout.addWidget(title)
subtitle = QLabel("Your App version and Database don't match")
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

@ -0,0 +1,50 @@
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)