From c508dddcaceee3a51deee7c750228bc72129374b Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Sun, 28 Jun 2026 21:50:22 -0400 Subject: [PATCH] Changed the app startup file to first do a database Compatibility check, then launch the main GUI only after all is clear --- gui/__main__.py | 997 +++--------------------- gui/__pycache__/main_ui.cpython-312.pyc | Bin 0 -> 51946 bytes gui/main_ui.py | 912 ++++++++++++++++++++++ gui/v2/ui/popups/Database_version.py | 279 +++++++ gui/v2/ui/popups/pick_folder_to_move.py | 50 ++ 5 files changed, 1332 insertions(+), 906 deletions(-) mode change 100755 => 100644 gui/__main__.py create mode 100644 gui/__pycache__/main_ui.cpython-312.pyc create mode 100755 gui/main_ui.py create mode 100644 gui/v2/ui/popups/Database_version.py create mode 100644 gui/v2/ui/popups/pick_folder_to_move.py diff --git a/gui/__main__.py b/gui/__main__.py old mode 100755 new mode 100644 index 2b71abc..8aa34e1 --- a/gui/__main__.py +++ b/gui/__main__.py @@ -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 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): - super().__init__() - sys.excepthook = self._handle_exception - self.setWindowFlags(Qt.WindowType.Window) - - gui_dir = os.path.dirname(os.path.abspath(__file__)) - font_path = os.path.join(gui_dir, 'resources', 'fonts') - - # Get's SQLAlchemy going, - orm.start() - - retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf') - font_id = QFontDatabase.addApplicationFont(retro_gaming_path) - font_family = QFontDatabase.applicationFontFamilies(font_id)[0] - app_font = QFont(font_family) - app_font.setPointSize(10) - QApplication.setFont(app_font) - - self.open_sans_family = None - open_sans_path = os.path.join(font_path, 'open-sans.ttf') - if os.path.exists(open_sans_path): - open_sans_id = QFontDatabase.addApplicationFont(open_sans_path) - if open_sans_id != -1: - self.open_sans_family = QFontDatabase.applicationFontFamilies(open_sans_id)[0] - - self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) - self.setWindowTitle('HydraVeil') - - screen = QApplication.primaryScreen() - ratio = screen.devicePixelRatio() - self.host_screen_width = int(screen.size().width() * ratio) - self.host_screen_height = int(screen.size().height() * ratio) - - self._data = {"Profile_1": {}} - - self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui') - self.gui_config_file = os.path.join(self.gui_config_home, 'config.json') - self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui') - self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log') - self._gui_log_handlers = [] - - setup_gui_directory( - self.gui_cache_home, self.gui_config_home, self.gui_config_file) - - self.btn_path = os.getenv('BTN_PATH', os.path.join( - gui_dir, 'resources', 'images')) - self.css_path = os.getenv('CSS_PATH', os.path.join( - gui_dir, 'resources', 'styles')) - - self.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: - 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) - - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = CustomWindow() - sys.exit(app.exec()) +# ============================================================================ +# INITIALIZE DATABASE +# ============================================================================ + +# (engine, Session, _session all initialized from session_management) +init_session() +session = get_session() +create_ONLY_db_version_table() + +# ============================================================================ +# 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}") + +# ============================================================================ +# IF THE VERSIONS MATCH +# ============================================================================ +if is_compatable: + made_tables = create_ALL_tables() + + # SCREEN FAILED TABLES + if not made_tables: + logger.error(f"[DB MANAGEMENT] Critical Failure with starting the models of the database.") + close_session() + sys.exit() + + # ASSUME TABLES CREATED + logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing") + + # original reason was he had no database, now tag it that he's got the new version, + if reason == "no_database": + 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.") + + # UPDATE DATA + 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.") + + logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window...") + from main_ui import start_ui + force_sync = False + start_ui(force_sync) + +# ============================================================================ +# but IF the versions do NOT match +# ============================================================================ +else: + 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? + custom_error = get_custom_message(reason, compatability_dict) + + # shut down db connection + close_session() + +# ============================================================================ +# THEN DISPLAY CHOICES AND RECOVERY UI +# ============================================================================ + 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}") + + system_path = get_path() + database_path = system_path / "storage.db" + + if choice == 1: + if os.path.exists(database_path): + os.remove(database_path) + logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}") + logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..") + sys.exit() + + elif choice == 2: + 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() + + else: + logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.") + sys.exit() diff --git a/gui/__pycache__/main_ui.cpython-312.pyc b/gui/__pycache__/main_ui.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..273b5de27062afeb469b337aedfd0f918188778f GIT binary patch literal 51946 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 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()) diff --git a/gui/v2/ui/popups/Database_version.py b/gui/v2/ui/popups/Database_version.py new file mode 100644 index 0000000..bde74b0 --- /dev/null +++ b/gui/v2/ui/popups/Database_version.py @@ -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 diff --git a/gui/v2/ui/popups/pick_folder_to_move.py b/gui/v2/ui/popups/pick_folder_to_move.py new file mode 100644 index 0000000..daff390 --- /dev/null +++ b/gui/v2/ui/popups/pick_folder_to_move.py @@ -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)