forked from Support/sp-hydra-veil-gui
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:
parent
33301b0da8
commit
c508dddcac
5 changed files with 1332 additions and 906 deletions
961
gui/__main__.py
Executable file → Normal file
961
gui/__main__.py
Executable file → Normal 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())
|
|
||||||
|
|
|
||||||
BIN
gui/__pycache__/main_ui.cpython-312.pyc
Normal file
BIN
gui/__pycache__/main_ui.cpython-312.pyc
Normal file
Binary file not shown.
912
gui/main_ui.py
Executable file
912
gui/main_ui.py
Executable 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())
|
||||||
279
gui/v2/ui/popups/Database_version.py
Normal file
279
gui/v2/ui/popups/Database_version.py
Normal 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
|
||||||
50
gui/v2/ui/popups/pick_folder_to_move.py
Normal file
50
gui/v2/ui/popups/pick_folder_to_move.py
Normal 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)
|
||||||
Loading…
Reference in a new issue