diff --git a/gui/__pycache__/main_ui.cpython-312.pyc b/gui/__pycache__/main_ui.cpython-312.pyc deleted file mode 100644 index 8bb45ea..0000000 Binary files a/gui/__pycache__/main_ui.cpython-312.pyc and /dev/null differ diff --git a/gui/main_ui.py b/gui/main_ui.py index e4ba5b7..34bedc6 100755 --- a/gui/main_ui.py +++ b/gui/main_ui.py @@ -10,7 +10,7 @@ import traceback from PyQt6.QtWidgets import ( QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton, - QDialog, QVBoxLayout, QHBoxLayout + QDialog, QVBoxLayout, QHBoxLayout, QMessageBox ) from PyQt6.QtGui import QPixmap, QFont, QFontDatabase from PyQt6 import QtGui @@ -61,6 +61,7 @@ from gui.v2.actions.ticket_failure import ( clear_ticket_verification_failure, ) from gui.v2.actions.should_be_synchronized import should_be_synchronized +from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button class CustomWindow(QMainWindow): @@ -192,6 +193,23 @@ class CustomWindow(QMainWindow): else: self.navigator.navigate("menu") self.navigator.start_preload() + if self.force_sync: + QTimer.singleShot(0, self._prompt_force_sync) + + def _prompt_force_sync(self): + connection = ConfigurationController.get_connection() + box = QMessageBox(self) + box.setWindowTitle("Data Update Needed") + box.setText( + "Please download the new data to avoid errors. You have profiles that need the data to function.\n\n" + f"Connection type: {connection}") + style_message_box(box) + sync_button = box.addButton("Sync", QMessageBox.ButtonRole.AcceptRole) + mark_confirm_button(sync_button) + box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole) + box.exec() + if box.clickedButton() == sync_button: + self.sync() def perform_update_check(self): from core.controllers.ClientController import ClientController diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index 1499649..a836bf7 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -157,10 +157,16 @@ class Navigator(QObject): else: self._maybe_emit_preload_summary() + def _prepare_page_data(self, name): + module_path, class_name = PAGE_REGISTRY[name] + module = importlib.import_module(module_path) + cls = getattr(module, class_name) + return cls.prepare_data(self.custom_window) + def _dispatch_async_preload(self, name): module_path, class_name = PAGE_REGISTRY[name] print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True) - worker = PageDataWorker(name, module_path, class_name, self.custom_window) + worker = PageDataWorker(name, self._prepare_page_data, name) worker.data_ready.connect(self._on_preload_data_ready) worker.failed.connect(self._on_preload_failed) worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n)) diff --git a/gui/v2/ui/pages/editor_page.py b/gui/v2/ui/pages/editor_page.py index ce00ca4..aafcb6b 100755 --- a/gui/v2/ui/pages/editor_page.py +++ b/gui/v2/ui/pages/editor_page.py @@ -16,6 +16,7 @@ from gui.v2.ui.pages.browser_page import BrowserPage from gui.v2.ui.pages.location_page import LocationPage from gui.v2.ui.pages.screen_page import ScreenPage from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup +from gui.v2.workers.page_data_worker import PageDataWorker class EditorPage(Page): @@ -80,6 +81,13 @@ class EditorPage(Page): self.brow_disp.hide() self.brow_disp.lower() + self._prefetched_profiles = None + self._editor_workers = set() + + @staticmethod + def prepare_data(custom_window): + return ProfileController.get_all() + def _selected_profiles_str(self): menu_page = self.find_menu_page() if menu_page is not None and menu_page.selected_profiles: @@ -117,8 +125,28 @@ class EditorPage(Page): def showEvent(self, event): super().showEvent(event) self.res_hint_shown = False + worker = PageDataWorker("editor", EditorPage.prepare_data, self.custom_window) + worker.data_ready.connect( + lambda name, profiles: self._on_editor_data_ready(profiles)) + worker.failed.connect( + lambda name, error: self._on_editor_data_failed(error)) + worker.finished.connect( + lambda w=worker: self._cleanup_editor_worker(w)) + self._editor_workers.add(worker) + worker.start() + + def _on_editor_data_ready(self, profiles): + self._prefetched_profiles = profiles self.extraccion() + def _on_editor_data_failed(self, error): + self.update_status.update_status(f"Editor data load failed: {error}") + self.extraccion() + + def _cleanup_editor_worker(self, worker): + self._editor_workers.discard(worker) + worker.deleteLater() + def extraccion(self): self.data_profile = {} for label in self.labels: @@ -131,7 +159,11 @@ class EditorPage(Page): selected_profiles_str = self._selected_profiles_str() menu_page = self.find_menu_page() if menu_page: - new_profiles = ProfileController.get_all() + if self._prefetched_profiles is not None: + new_profiles = self._prefetched_profiles + self._prefetched_profiles = None + else: + new_profiles = ProfileController.get_all() self.profiles_data = menu_page.match_core_profiles( profiles_dict=new_profiles) self.data_profile = self.profiles_data[selected_profiles_str].copy( diff --git a/gui/v2/ui/pages/fast_registration_page.py b/gui/v2/ui/pages/fast_registration_page.py index c06234b..3199b47 100755 --- a/gui/v2/ui/pages/fast_registration_page.py +++ b/gui/v2/ui/pages/fast_registration_page.py @@ -15,6 +15,7 @@ from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.browser_page import BrowserPage from gui.v2.ui.pages.location_page import LocationPage from gui.v2.ui.pages.location_verification_page import LocationVerificationPage +from gui.v2.workers.page_data_worker import PageDataWorker from gui.v2.workers.worker_thread import WorkerThread @@ -73,9 +74,15 @@ class FastRegistrationPage(Page): 'resolution': '1024x760' } self.res_index = 1 + self._prefetched_profiles = None + self._fast_reg_workers = set() self.initialize_default_selections() + @staticmethod + def prepare_data(custom_window): + return ProfileController.get_all() + def initialize_default_selections(self): if not self.selected_values['location']: locations = self.connection_manager.get_location_list() @@ -107,6 +114,24 @@ class FastRegistrationPage(Page): super().showEvent(event) self.initialize_default_selections() self.create_interface_elements() + self._start_profiles_prefetch() + + def _start_profiles_prefetch(self): + worker = PageDataWorker( + "fast_registration", FastRegistrationPage.prepare_data, self.custom_window) + worker.data_ready.connect( + lambda name, profiles: self._on_profiles_prefetched(profiles)) + worker.finished.connect( + lambda w=worker: self._cleanup_fast_reg_worker(w)) + self._fast_reg_workers.add(worker) + worker.start() + + def _on_profiles_prefetched(self, profiles): + self._prefetched_profiles = profiles + + def _cleanup_fast_reg_worker(self, worker): + self._fast_reg_workers.discard(worker) + worker.deleteLater() def create_interface_elements(self): for label in self.labels: @@ -728,7 +753,7 @@ class FastRegistrationPage(Page): self.update_status.update_status('Invalid location selected') return - profiles = ProfileController.get_all() + profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all() profile_id = self.get_next_available_profile_id(profiles) profile_data_for_resume = { 'id': profile_id, @@ -762,7 +787,7 @@ class FastRegistrationPage(Page): connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system' - profiles = ProfileController.get_all() + profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all() profile_id = self.get_next_available_profile_id(profiles) profile_data_for_resume = { 'id': profile_id, diff --git a/gui/v2/ui/pages/id_page.py b/gui/v2/ui/pages/id_page.py index bb44443..28bbea0 100755 --- a/gui/v2/ui/pages/id_page.py +++ b/gui/v2/ui/pages/id_page.py @@ -2,13 +2,16 @@ import os import re from PyQt6.QtWidgets import ( - QButtonGroup, QFrame, QLabel, QPushButton, QTextEdit, + QButtonGroup, QFrame, QLabel, QMessageBox, QPushButton, QTextEdit, ) from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtCore import Qt from PyQt6 import QtCore +from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id + from gui.v2.ui.pages.Page import Page +from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button HOW_MANY_PROFILES_DEFAULT = 6 @@ -150,11 +153,44 @@ class IdPage(Page): self.custom_window.navigator.navigate("duration_selection") def go_multiple_profiles(self): + billing_id = do_we_have_billing_id() + if billing_id: + self._prompt_existing_tickets(billing_id) + else: + self._continue_multiple_flow() + + def _continue_multiple_flow(self): self.custom_window.navigator.navigate("plan_picker") plan_page = self.custom_window.navigator.get_cached("plan_picker") if plan_page is not None: plan_page.start_sync() + def _prompt_existing_tickets(self, billing_id): + box = QMessageBox(self) + box.setWindowTitle("Existing ticket purchase found") + box.setText( + "You already have a pending ticket purchase. Continue to check if it's paid, " + "or wipe it and start over?") + style_message_box(box) + check_button = box.addButton( + "Check payment", QMessageBox.ButtonRole.AcceptRole) + mark_confirm_button(check_button) + wipe_button = box.addButton( + "Wipe and start over", QMessageBox.ButtonRole.DestructiveRole) + box.exec() + clicked = box.clickedButton() + if clicked == check_button: + self._resume_existing_ticket(billing_id) + elif clicked == wipe_button: + self._continue_multiple_flow() + + def _resume_existing_ticket(self, billing_id): + self.update_status.update_status("Checking existing ticket payment...") + self.custom_window.navigator.navigate("payment_details") + payment_page = self.custom_window.navigator.get_cached("payment_details") + if payment_page is not None: + payment_page.resume_ticket_billing(billing_id) + def toggle_button_state(self): text = self.text_edit.toPlainText() if text.strip(): diff --git a/gui/v2/ui/pages/payment_details_page.py b/gui/v2/ui/pages/payment_details_page.py index ffe9dca..20c383f 100755 --- a/gui/v2/ui/pages/payment_details_page.py +++ b/gui/v2/ui/pages/payment_details_page.py @@ -351,6 +351,16 @@ class PaymentDetailsPage(Page): self.ticket_poll_timer.start(3000) self.update_status.update_status('Awaiting ticket payment...') + def resume_ticket_billing(self, temp_billing_code): + self.ticketing = True + self.temp_billing_code = temp_billing_code + self.selected_currency = None + self.text_fields[0].setText(str(temp_billing_code or '')) + if self.ticket_poll_timer.isActive(): + self.ticket_poll_timer.stop() + self.ticket_poll_timer.start(3000) + self.update_status.update_status('Checking existing ticket payment...') + def _populate_ticket_fields(self, invoice): self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or '')) self.text_fields[1].setText(self.selected_duration) diff --git a/gui/v2/ui/pages/settings_page.py b/gui/v2/ui/pages/settings_page.py index a4af5e8..82b5a45 100755 --- a/gui/v2/ui/pages/settings_page.py +++ b/gui/v2/ui/pages/settings_page.py @@ -35,7 +35,6 @@ from core.errors.logger import logger as core_logger from gui.v2.actions import settings_data from gui.v2.actions.key_interpretation import interpret_key_results from gui.v2.actions.profile_order import normalize_profile_order -from gui.v2.infrastructure.page_registry import PAGE_REGISTRY from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.ui.pages.Page import Page from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup @@ -103,7 +102,6 @@ class Settings(Page): ("Create/Edit", self.show_registrations_page), ("Verification", self.show_verification_page), ("Legacy-Version", self.show_systemwide_page), - ("Bwrap Permission", self.show_bwrap_page), ("Delete Profile", self.show_delete_page), ("Error Logs", self.show_logs_page), ("Debug Help", self.show_debug_page) @@ -1176,9 +1174,8 @@ class Settings(Page): super().showEvent(event) self._settings_refresh_seq += 1 seq = self._settings_refresh_seq - module_path, class_name = PAGE_REGISTRY["settings"] worker = PageDataWorker( - "settings", module_path, class_name, self.custom_window) + "settings", settings_data.prepare, self.custom_window.gui_config_file) worker.data_ready.connect( lambda name, payload, s=seq: self._on_settings_data_ready(payload, s)) worker.failed.connect( @@ -1226,10 +1223,6 @@ class Settings(Page): self.systemwide_page = self.create_systemwide_page() self.content_layout.addWidget(self.systemwide_page) - self.content_layout.removeWidget(self.bwrap_page) - self.bwrap_page = self.create_bwrap_page() - self.content_layout.addWidget(self.bwrap_page) - self.content_layout.removeWidget(self.delete_page) self.delete_page = self.create_delete_page() self.content_layout.addWidget(self.delete_page) @@ -1443,7 +1436,6 @@ class Settings(Page): self.registrations_page = self.create_registrations_page() self.verification_page = self.create_verification_page() self.systemwide_page = self.create_systemwide_page() - self.bwrap_page = self.create_bwrap_page() self.logs_page = self.create_logs_page() self.delete_page = self.create_delete_page() self.debug_page = self.create_debug_page() @@ -1454,7 +1446,6 @@ class Settings(Page): self.content_layout.addWidget(self.registrations_page) self.content_layout.addWidget(self.verification_page) self.content_layout.addWidget(self.systemwide_page) - self.content_layout.addWidget(self.bwrap_page) self.content_layout.addWidget(self.logs_page) self.content_layout.addWidget(self.delete_page) self.content_layout.addWidget(self.debug_page) @@ -1508,11 +1499,6 @@ class Settings(Page): self.content_layout.setCurrentWidget(self.systemwide_page) self._select_menu_button("Legacy-Version") - def show_bwrap_page(self): - core_logger.info("User navigated to Settings -> Bwrap Permission") - self.content_layout.setCurrentWidget(self.bwrap_page) - self._select_menu_button("Bwrap Permission") - def show_logs_page(self): core_logger.info("User navigated to Settings -> Error Logs") self.content_layout.setCurrentWidget(self.logs_page) @@ -1995,59 +1981,6 @@ class Settings(Page): self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled)) return page - def create_bwrap_page(self): - page = QWidget() - layout = QVBoxLayout(page) - layout.setSpacing(20) - layout.setContentsMargins(20, 20, 20, 20) - - title = QLabel("BWRAP PERMISSION") - title.setStyleSheet( - f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") - layout.addWidget(title) - - description = QLabel( - "Control whether HydraVeil configures a capability policy so bwrap can be used without requiring additional permissions.") - description.setWordWrap(True) - description.setStyleSheet( - f"color: white; font-size: 14px; {self.font_style}") - layout.addWidget(description) - - status_layout = QHBoxLayout() - status_label = QLabel("Current status:") - status_label.setStyleSheet( - f"color: white; font-size: 14px; {self.font_style}") - self.bwrap_status_value = QLabel("") - self.bwrap_status_value.setStyleSheet( - f"color: #e67e22; font-size: 14px; {self.font_style}") - status_layout.addWidget(status_label) - status_layout.addWidget(self.bwrap_status_value) - status_layout.addStretch() - layout.addLayout(status_layout) - - toggle_layout = QHBoxLayout() - self.bwrap_toggle = QCheckBox("Enable capability policy") - self.bwrap_toggle.setStyleSheet(self.get_checkbox_style()) - toggle_layout.addWidget(self.bwrap_toggle) - toggle_layout.addStretch() - layout.addLayout(toggle_layout) - - save_button = QPushButton() - save_button.setFixedSize(75, 46) - save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png"))) - save_button.setIconSize(QSize(75, 46)) - save_button.clicked.connect(self.save_bwrap_settings) - - button_layout = QHBoxLayout() - button_layout.addWidget(save_button) - button_layout.addStretch() - layout.addLayout(button_layout) - - layout.addStretch() - self.load_bwrap_settings( - self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled)) - return page - def load_systemwide_settings(self, enabled=None): if enabled is None: enabled = settings_data.read_systemwide_enabled() @@ -2087,45 +2020,6 @@ class Settings(Page): self.systemwide_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") - def load_bwrap_settings(self, enabled=None): - if enabled is None: - enabled = settings_data.read_bwrap_enabled() - self.bwrap_toggle.setChecked(enabled) - if enabled: - self.bwrap_status_value.setText("Enabled") - self.bwrap_status_value.setStyleSheet( - f"color: #2ecc71; font-size: 14px; {self.font_style}") - else: - self.bwrap_status_value.setText("Disabled") - self.bwrap_status_value.setStyleSheet( - f"color: #e67e22; font-size: 14px; {self.font_style}") - - def save_bwrap_settings(self): - enable = self.bwrap_toggle.isChecked() - try: - capability_policy = PolicyController.get('capability') - if capability_policy is not None: - if enable: - PolicyController.instate(capability_policy) - else: - if PolicyController.is_instated(capability_policy): - PolicyController.revoke(capability_policy) - self.load_bwrap_settings() - self.update_status.update_status( - "Capability policy settings updated") - except CommandNotFoundError as e: - self.bwrap_status_value.setText(str(e)) - self.bwrap_status_value.setStyleSheet( - f"color: red; font-size: 14px; {self.font_style}") - except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e: - self.bwrap_status_value.setText(str(e)) - self.bwrap_status_value.setStyleSheet( - f"color: red; font-size: 14px; {self.font_style}") - except Exception: - self.bwrap_status_value.setText("Failed to update policy") - self.bwrap_status_value.setStyleSheet( - f"color: red; font-size: 14px; {self.font_style}") - def load_registrations_settings(self) -> None: try: config = self._prepared_value( diff --git a/gui/v2/ui/pages/ticket_crypto_picker_page.py b/gui/v2/ui/pages/ticket_crypto_picker_page.py index 60e798d..be110d6 100755 --- a/gui/v2/ui/pages/ticket_crypto_picker_page.py +++ b/gui/v2/ui/pages/ticket_crypto_picker_page.py @@ -9,6 +9,7 @@ from core.controllers.tickets.TicketPayController import check_if_paid from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer from gui.v2.ui.pages.Page import Page +from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread @@ -77,7 +78,13 @@ class TicketCryptoPickerPage(Page): clipboard = QApplication.clipboard() clipboard.setText(temp_billing_code) not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now." - QMessageBox.information(None, "Not Paid", not_paid_msg) + info = QMessageBox(self) + info.setWindowTitle("Not Paid") + info.setText(not_paid_msg) + info.setStandardButtons(QMessageBox.StandardButton.Ok) + style_message_box(info) + mark_confirm_button(info.button(QMessageBox.StandardButton.Ok)) + info.exec() def start_initiate_payment(self, currency): self.update_status.update_status("Initiating payment...") @@ -121,6 +128,8 @@ class TicketCryptoPickerPage(Page): msg.setWindowTitle("Existing tickets found") msg.setText("You already have ticket data. Wipe it and start over?") msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + style_message_box(msg) + mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes)) result = msg.exec() if result == QMessageBox.StandardButton.Yes: self.bypass_existing = True @@ -135,6 +144,8 @@ class TicketCryptoPickerPage(Page): msg.setWindowTitle("Existing billing code found") msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.") msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + style_message_box(msg) + mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes)) result = msg.exec() if result == QMessageBox.StandardButton.Yes: self.update_status.update_status("Reusing same code") diff --git a/gui/v2/ui/popups/message_box.py b/gui/v2/ui/popups/message_box.py new file mode 100755 index 0000000..6798785 --- /dev/null +++ b/gui/v2/ui/popups/message_box.py @@ -0,0 +1,18 @@ +MESSAGE_BOX_QSS = ( + "QMessageBox { background-color: white; }" + "QMessageBox QLabel { color: black; }" + "QMessageBox QPushButton { color: black; padding: 5px 14px; }" +) + +CONFIRM_BUTTON_QSS = "color: #2e7d32; font-weight: bold; padding: 5px 14px;" + + +def style_message_box(box): + box.setStyleSheet(MESSAGE_BOX_QSS) + + +def mark_confirm_button(button): + if button is None: + return + button.setText(f"✓ {button.text()}") + button.setStyleSheet(CONFIRM_BUTTON_QSS) diff --git a/gui/v2/workers/page_data_worker.py b/gui/v2/workers/page_data_worker.py index 630341e..378ab4f 100755 --- a/gui/v2/workers/page_data_worker.py +++ b/gui/v2/workers/page_data_worker.py @@ -1,5 +1,3 @@ -import importlib - from PyQt6.QtCore import QThread, pyqtSignal @@ -7,18 +5,15 @@ class PageDataWorker(QThread): data_ready = pyqtSignal(str, object) failed = pyqtSignal(str, str) - def __init__(self, name, module_path, class_name, custom_window): + def __init__(self, name, fn, *args): super().__init__() self.name = name - self.module_path = module_path - self.class_name = class_name - self.custom_window = custom_window + self.fn = fn + self.args = args def run(self): try: - module = importlib.import_module(self.module_path) - cls = getattr(module, self.class_name) - payload = cls.prepare_data(self.custom_window) + payload = self.fn(*self.args) self.data_ready.emit(self.name, payload) except Exception as error: self.failed.emit(self.name, f"{type(error).__name__}: {error}")