diff --git a/gui/v2/__init__.py b/gui/v2/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/__main__.py b/gui/v2/__main__.py new file mode 100755 index 0000000..6f371c7 --- /dev/null +++ b/gui/v2/__main__.py @@ -0,0 +1,709 @@ +import logging +import os +import sys +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 + +from core.Constants import Constants +from core.controllers.ConfigurationController import ConfigurationController +from core.Errors import UnknownConnectionTypeError +from core.errors.logger import logger as core_logger + +core_logger.propagate = False + +from gui.v2.infrastructure.navigator import Navigator +from gui.v2.infrastructure.setup_observers import setup_observers +from gui.v2.infrastructure.connection_manager import ConnectionManager +from gui.v2.workers.worker_thread import WorkerThread +from gui.v2.ui.builders.bottom_section import create_bottom_section +from gui.v2.ui.builders.top_section import create_top_section +from gui.v2.ui.builders.system_tray import init_system_tray +from gui.v2.actions.config import ( + setup_gui_directory, + load_gui_config, + save_gui_config, + default_gui_config, +) +from gui.v2.actions.logging_setup import ( + is_logging_enabled, + setup_gui_logging, + teardown_gui_logging, +) +from gui.v2.actions.flags import ( + has_shown_systemwide_prompt, + mark_systemwide_prompt_shown, + has_shown_fast_mode_prompt, + mark_fast_mode_prompt_shown, + set_fast_mode_enabled, + check_first_launch, +) +from gui.v2.actions.profile_data import ( + read_profile_data, + write_profile_data, + clear_profile_data, +) +from gui.v2.actions.ticket_failure import ( + save_ticket_verification_failure, + get_ticket_verification_failure, + clear_ticket_verification_failure, +) +from gui.v2.actions.should_be_synchronized import should_be_synchronized + + +class CustomWindow(QMainWindow): + def __init__(self): + super().__init__() + sys.excepthook = self._handle_exception + self.setWindowFlags(Qt.WindowType.Window) + + current_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(current_dir) + font_path = os.path.join(parent_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( + parent_dir, 'resources', 'images')) + self.css_path = os.getenv('CSS_PATH', os.path.join( + parent_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) + + 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.page_stack.currentChanged.connect(self.page_changed) + + self.show() + self.init_ui() + + self.tray = None + self.tray = init_system_tray(self, self.btn_path) + + current_connection = self.get_current_connection() + self.is_tor_mode = current_connection == 'tor' + self.set_toggle_state(self.is_tor_mode) + + core_logger.info( + f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, " + f"connection={current_connection}, tor_mode={self.is_tor_mode})" + ) + + def init_ui(self): + if self.check_first_launch(): + self.navigator.navigate("welcome") + else: + self.navigator.navigate("menu") + + 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) + + self._update_toggle_colors(colors) + if is_tor_mode: + self.set_tor_icon() + + 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 + 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): + if not self.is_animating: + self.is_animating = True + self.animation_step = 0 + self.animation_timer.start(16) + self.is_tor_mode = not self.is_tor_mode + new_connection = 'tor' if self.is_tor_mode else 'system' + ConfigurationController.set_connection(new_connection) + core_logger.info(f"User toggled connection mode to '{new_connection}'") + + def animate_toggle(self): + TOTAL_STEPS = 5 + if self.animation_step <= TOTAL_STEPS: + progress = self.animation_step / TOTAL_STEPS + + if self.is_tor_mode: + new_x = 20 + (10 * progress) + start_color = "#2c3e50" + end_color = "#00a8ff" + else: + new_x = 31 - (28 * progress) + start_color = "#00a8ff" + end_color = "#2c3e50" + + self.toggle_handle.setGeometry(int(new_x), 3, 16, 16) + + bg_color = self.interpolate_color(start_color, end_color, progress) + self.toggle_bg.setStyleSheet(f""" + QLabel {{ + background-color: {bg_color}; + border-radius: 11px; + }} + """) + + self.off_label.setStyleSheet(f""" + QLabel {{ + color: rgba(255, 255, 255, {1 - progress if self.is_tor_mode else progress}); + font-size: 9px; + font-weight: bold; + }} + """) + self.on_label.setStyleSheet(f""" + QLabel {{ + color: rgba(255, 255, 255, {progress if self.is_tor_mode else 1 - progress}); + font-size: 9px; + font-weight: bold; + }} + """) + + self.animation_step += 1 + else: + self.animation_timer.stop() + self.is_animating = False + self.set_tor_icon() + + def interpolate_color(self, start_color, end_color, progress): + def hex_to_rgb(hex_color): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + + start_rgb = hex_to_rgb(start_color) + end_rgb = hex_to_rgb(end_color) + + current_rgb = tuple( + int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * progress) + for i in range(3) + ) + + return f"rgb{current_rgb}" + + def closeEvent(self, event=None): + core_logger.info("HydraVeil application closing") + connected_profiles = self.connection_manager.get_connected_profiles() + if connected_profiles: + self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).') + if event is not None: + event.accept() + else: + if event is not None: + event.accept() + + def update_image(self, appearance_value="original"): + image_path = os.path.join(self.btn_path, f"{appearance_value}.png") + pixmap = QPixmap(image_path) + self.label.setPixmap(pixmap) + self.label.setScaledContents(True) + + def read_data(self): + return read_profile_data(self._data) + + def write_data(self, data): + write_profile_data(self._data, data) + + def clear_data(self): + self._data = clear_profile_data() + + def _set_status_font_size(self, font_size): + self.status_label.setStyleSheet( + f"color: rgb(0, 255, 255); font-size: {font_size}px;") + + def update_status(self, text, clear=False): + if text is None: + self._set_status_font_size(16) + self.status_label.setText('Status:') + self.disable_marquee() + return + + if clear: + self._set_status_font_size(16) + self.status_label.setText('') + return + + full_text = 'Status: ' + text + metrics = self.status_label.fontMetrics() + available_width = self.status_label.width() - 10 + + if metrics.horizontalAdvance(full_text) > available_width: + self.enable_marquee(text) + else: + self.status_label.setText(full_text) + self.disable_marquee() + + def check_first_launch(self): + return check_first_launch(self.gui_config_file) + + def has_shown_fast_mode_prompt(self): + return has_shown_fast_mode_prompt(self.gui_config_file) + + def mark_fast_mode_prompt_shown(self): + mark_fast_mode_prompt_shown(self.gui_config_file) + + def set_fast_mode_enabled(self, enabled): + set_fast_mode_enabled(self.gui_config_file, enabled) + + def navigate_after_profile_created(self): + if not self.has_shown_fast_mode_prompt(): + self.navigator.navigate("fast_mode_prompt") + else: + menu_page = self.navigator.get_cached("menu") + if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): + menu_page.refresh_menu_buttons() + self.navigator.navigate("menu") + + def enable_marquee(self, text): + self.marquee_text = text + " " + self.marquee_position = 0 + self.marquee_enabled = True + self.marquee_timer.start(500) + + def disable_marquee(self): + self.marquee_enabled = False + self.marquee_timer.stop() + + def update_marquee(self): + if not self.marquee_enabled: + return + + metrics = self.status_label.fontMetrics() + text_width = metrics.horizontalAdvance(self.marquee_text) + + self.marquee_position += self.scroll_speed + if self.marquee_position >= text_width: + self.marquee_position = 0 + + looped_text = self.marquee_text * 2 + + chars_that_fit = metrics.horizontalAdvance( + looped_text[:self.text_width]) + + visible_text = looped_text[self.marquee_position: + self.marquee_position + chars_that_fit] + + while metrics.horizontalAdvance(visible_text) < self.text_width: + visible_text += self.marquee_text + + while metrics.horizontalAdvance(visible_text) > self.text_width: + visible_text = visible_text[:-1] + + display_text = 'Status: ' + ' ' * \ + (self.text_start_x - metrics.horizontalAdvance('Status: ')) + display_text += visible_text + + self.status_label.setText(display_text) + + def set_scroll_speed(self, speed): + self.scroll_speed = speed + + def page_changed(self, index): + if should_be_synchronized(): + sync_icon = QtGui.QIcon(os.path.join( + self.btn_path, 'icon_sync_urgent.png')) + else: + sync_icon = QtGui.QIcon(os.path.join( + self.btn_path, 'icon_sync.png')) + + self.sync_button.setIcon(sync_icon) + + current_page = self.page_stack.widget(index) + if self.page_history: + previous_page = self.page_history[-1] + else: + previous_page = None + + current_name = getattr(current_page, 'name', None) + previous_name = getattr(previous_page, 'name', None) + + if current_name == 'Menu': + self.update_status(None) + if previous_name in ('Resume', 'Editor', 'Settings', 'FastRegistration', 'FastModePrompt'): + if hasattr(current_page, 'eliminacion'): + current_page.eliminacion() + elif current_name == 'Resume': + self.update_status('Choose a name for your profile') + if hasattr(current_page, 'eliminacion'): + current_page.eliminacion() + elif current_name == 'Id': + pass + elif current_name == 'Location': + self.update_status(None, clear=True) + elif current_name == 'InstallPackage': + self.update_status('Please check package availability.') + else: + self.update_status(None) + + if current_name == 'Menu': + self.sync_button.setVisible(True) + self.tor_text.setVisible(True) + self.toggle_button.setGeometry(670, 541, 50, 22) + self.tor_icon.setGeometry(730, 537, 25, 25) + else: + self.sync_button.setVisible(False) + self.tor_text.setVisible(False) + self.toggle_button.setGeometry(540, 541, 50, 22) + self.tor_icon.setGeometry(600, 537, 25, 25) + + if current_page != previous_page: + self.page_history.append(current_page) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = CustomWindow() + sys.exit(app.exec()) diff --git a/gui/v2/__pycache__/__init__.cpython-312.pyc b/gui/v2/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..af7ebd6 Binary files /dev/null and b/gui/v2/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/__pycache__/__main__.cpython-312.pyc b/gui/v2/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000..1e89a98 Binary files /dev/null and b/gui/v2/__pycache__/__main__.cpython-312.pyc differ diff --git a/gui/v2/actions/__init__.py b/gui/v2/actions/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/actions/__pycache__/__init__.cpython-312.pyc b/gui/v2/actions/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e2e5d3a Binary files /dev/null and b/gui/v2/actions/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/config.cpython-312.pyc b/gui/v2/actions/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..cc2d50a Binary files /dev/null and b/gui/v2/actions/__pycache__/config.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/flags.cpython-312.pyc b/gui/v2/actions/__pycache__/flags.cpython-312.pyc new file mode 100644 index 0000000..dab09e7 Binary files /dev/null and b/gui/v2/actions/__pycache__/flags.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/key_interpretation.cpython-312.pyc b/gui/v2/actions/__pycache__/key_interpretation.cpython-312.pyc new file mode 100644 index 0000000..8e630a1 Binary files /dev/null and b/gui/v2/actions/__pycache__/key_interpretation.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/locations.cpython-312.pyc b/gui/v2/actions/__pycache__/locations.cpython-312.pyc new file mode 100644 index 0000000..0cbb980 Binary files /dev/null and b/gui/v2/actions/__pycache__/locations.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/logging_setup.cpython-312.pyc b/gui/v2/actions/__pycache__/logging_setup.cpython-312.pyc new file mode 100644 index 0000000..ae774f2 Binary files /dev/null and b/gui/v2/actions/__pycache__/logging_setup.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/profile_data.cpython-312.pyc b/gui/v2/actions/__pycache__/profile_data.cpython-312.pyc new file mode 100644 index 0000000..39595b2 Binary files /dev/null and b/gui/v2/actions/__pycache__/profile_data.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/profile_order.cpython-312.pyc b/gui/v2/actions/__pycache__/profile_order.cpython-312.pyc new file mode 100644 index 0000000..45ec89f Binary files /dev/null and b/gui/v2/actions/__pycache__/profile_order.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/should_be_synchronized.cpython-312.pyc b/gui/v2/actions/__pycache__/should_be_synchronized.cpython-312.pyc new file mode 100644 index 0000000..d9f35f0 Binary files /dev/null and b/gui/v2/actions/__pycache__/should_be_synchronized.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/sync.cpython-312.pyc b/gui/v2/actions/__pycache__/sync.cpython-312.pyc new file mode 100644 index 0000000..8c2b2d4 Binary files /dev/null and b/gui/v2/actions/__pycache__/sync.cpython-312.pyc differ diff --git a/gui/v2/actions/__pycache__/ticket_failure.cpython-312.pyc b/gui/v2/actions/__pycache__/ticket_failure.cpython-312.pyc new file mode 100644 index 0000000..7a5d2f9 Binary files /dev/null and b/gui/v2/actions/__pycache__/ticket_failure.cpython-312.pyc differ diff --git a/gui/v2/actions/config.py b/gui/v2/actions/config.py new file mode 100755 index 0000000..602a14e --- /dev/null +++ b/gui/v2/actions/config.py @@ -0,0 +1,38 @@ +import json +import logging +import os + + +def setup_gui_directory(gui_cache_home, gui_config_home, gui_config_file): + os.makedirs(gui_cache_home, exist_ok=True) + os.makedirs(gui_config_home, exist_ok=True) + + if not os.path.exists(gui_config_file): + default_config = { + "logging": { + "gui_logging_enabled": False, + "log_level": "INFO" + } + } + with open(gui_config_file, 'w') as f: + json.dump(default_config, f, indent=4) + + +def load_gui_config(gui_config_file): + try: + with open(gui_config_file, 'r') as f: + return json.load(f) + except FileNotFoundError: + return None + except json.JSONDecodeError: + logging.error("Invalid GUI config file format") + return None + + +def save_gui_config(gui_config_file, config): + with open(gui_config_file, 'w') as f: + json.dump(config, f, indent=4) + + +def default_gui_config(): + return {"logging": {"gui_logging_enabled": False, "log_level": "INFO"}} diff --git a/gui/v2/actions/flags.py b/gui/v2/actions/flags.py new file mode 100755 index 0000000..a52add0 --- /dev/null +++ b/gui/v2/actions/flags.py @@ -0,0 +1,63 @@ +from gui.v2.actions.config import load_gui_config, save_gui_config, default_gui_config + + +def has_shown_systemwide_prompt(gui_config_file): + config = load_gui_config(gui_config_file) + if not config: + return False + return config.get("systemwide", {}).get("prompt_shown", False) + + +def mark_systemwide_prompt_shown(gui_config_file): + config = load_gui_config(gui_config_file) + if config is None: + config = default_gui_config() + if "systemwide" not in config: + config["systemwide"] = {} + config["systemwide"]["prompt_shown"] = True + save_gui_config(gui_config_file, config) + + +def has_shown_fast_mode_prompt(gui_config_file): + config = load_gui_config(gui_config_file) + if not config: + return False + return config.get("fast_mode", {}).get("prompt_shown", False) + + +def mark_fast_mode_prompt_shown(gui_config_file): + config = load_gui_config(gui_config_file) + if config is None: + config = default_gui_config() + if "fast_mode" not in config: + config["fast_mode"] = {} + config["fast_mode"]["prompt_shown"] = True + save_gui_config(gui_config_file, config) + + +def set_fast_mode_enabled(gui_config_file, enabled): + config = load_gui_config(gui_config_file) + if config is None: + config = default_gui_config() + if "registrations" not in config: + config["registrations"] = {} + config["registrations"]["fast_registration_enabled"] = bool(enabled) + save_gui_config(gui_config_file, config) + + +def check_first_launch(gui_config_file): + config = load_gui_config(gui_config_file) + if config is None: + config = default_gui_config() + + is_first_launch = not config.get( + "first_launch", {}).get("completed", False) + + if is_first_launch: + if "first_launch" not in config: + config["first_launch"] = {} + config["first_launch"]["completed"] = True + save_gui_config(gui_config_file, config) + return True + else: + return False diff --git a/gui/v2/actions/key_interpretation.py b/gui/v2/actions/key_interpretation.py new file mode 100755 index 0000000..7202537 --- /dev/null +++ b/gui/v2/actions/key_interpretation.py @@ -0,0 +1,30 @@ +def interpret_key_results(result: dict) -> str: + if not isinstance(result, dict): + return "There was an error with the format of the reply from the operation." + + valid = result.get('valid', False) + comparison = result.get('comparison', 'error') + error_msg = result.get('message', None) + + if error_msg: + if error_msg == "api_connection_issue": + return "There was a connection error with connecting to the API." + elif error_msg == "invalid_key": + return "Your original public key is in an invalid format to begin with." + else: + return "Unknown Error." + + if comparison == "same": + first_sentence = "The key you had locally matched what the server had also." + elif comparison == "different": + first_sentence = "Your local key was different than the server's key." + else: + first_sentence = "There was an error with the comparison." + + if valid: + second_sentence = "The signature now matches with the new key." + else: + second_sentence = "But the signature still does not match, even with the new key. If you prepare tickets anyway, they might not verify when used." + + final_msg = first_sentence + " " + second_sentence + return final_msg diff --git a/gui/v2/actions/locations.py b/gui/v2/actions/locations.py new file mode 100755 index 0000000..3e7ba6e --- /dev/null +++ b/gui/v2/actions/locations.py @@ -0,0 +1,34 @@ +def location_candidates(profile, preferred=None): + candidates = [] + seen = set() + + def add(val): + if val is None: + return + s = str(val).strip().lower().replace(' ', '_') + if s and s not in seen: + seen.add(s) + candidates.append(s) + + if preferred: + add(preferred) + + sources = [] + try: + if profile and profile.connection and profile.connection.location: + sources.append(profile.connection.location) + except Exception: + pass + try: + if profile and profile.location: + sources.append(profile.location) + except Exception: + pass + + for source in sources: + add(getattr(source, 'country_name', None)) + add(getattr(source, 'name', None)) + add(getattr(source, 'code', None)) + add(getattr(source, 'country_code', None)) + + return candidates diff --git a/gui/v2/actions/logging_setup.py b/gui/v2/actions/logging_setup.py new file mode 100755 index 0000000..a781abb --- /dev/null +++ b/gui/v2/actions/logging_setup.py @@ -0,0 +1,66 @@ +import logging +import os +import sys + +from core.errors.logger import logger as core_logger + + +def is_logging_enabled(config): + if config is None: + return False + return bool(config.get("logging", {}).get("gui_logging_enabled")) + + +def setup_gui_logging(gui_cache_home, gui_log_file, current_handlers): + os.makedirs(gui_cache_home, exist_ok=True) + + formatter = logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + file_handler = logging.FileHandler(gui_log_file) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(logging.INFO) + stream_handler.setFormatter(formatter) + + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + root_logger.handlers = [] + root_logger.addHandler(file_handler) + root_logger.addHandler(stream_handler) + + for handler in current_handlers: + try: + core_logger.removeHandler(handler) + except Exception: + pass + core_logger.addHandler(file_handler) + core_logger.addHandler(stream_handler) + + if not os.path.exists(gui_log_file) or os.path.getsize(gui_log_file) == 0: + logging.info("GUI logging system initialized") + + return [file_handler, stream_handler] + + +def teardown_gui_logging(gui_log_file, current_handlers): + for handler in current_handlers: + try: + core_logger.removeHandler(handler) + except Exception: + pass + + root_logger = logging.getLogger() + for handler in list(root_logger.handlers): + root_logger.removeHandler(handler) + try: + handler.close() + except Exception: + pass + + if os.path.exists(gui_log_file): + os.remove(gui_log_file) diff --git a/gui/v2/actions/profile_data.py b/gui/v2/actions/profile_data.py new file mode 100755 index 0000000..021d3a0 --- /dev/null +++ b/gui/v2/actions/profile_data.py @@ -0,0 +1,10 @@ +def read_profile_data(data): + return data["Profile_1"] + + +def write_profile_data(data, payload): + data["Profile_1"].update(payload) + + +def clear_profile_data(): + return {"Profile_1": {}} diff --git a/gui/v2/actions/profile_order.py b/gui/v2/actions/profile_order.py new file mode 100755 index 0000000..602fde7 --- /dev/null +++ b/gui/v2/actions/profile_order.py @@ -0,0 +1,61 @@ +from gui.v2.actions.config import default_gui_config, load_gui_config, save_gui_config + + +def _clean_ids(profile_ids): + ids = [] + for profile_id in profile_ids or []: + try: + profile_id = int(profile_id) + except (TypeError, ValueError): + continue + if profile_id not in ids: + ids.append(profile_id) + return ids + + +def _load_order(gui_config_file): + if not gui_config_file: + return [] + config = load_gui_config(gui_config_file) + if not config: + return [] + return _clean_ids(config.get("profiles", {}).get("visual_order", [])) + + +def _save_order(gui_config_file, order): + if not gui_config_file: + return + config = load_gui_config(gui_config_file) + if config is None: + config = default_gui_config() + if "profiles" not in config: + config["profiles"] = {} + config["profiles"]["visual_order"] = _clean_ids(order) + save_gui_config(gui_config_file, config) + + +def normalize_profile_order(gui_config_file, existing_ids): + existing_order = _clean_ids(existing_ids) + existing = set(existing_order) + saved_order = [profile_id for profile_id in _load_order( + gui_config_file) if profile_id in existing] + ordered = saved_order + [ + profile_id for profile_id in existing_order if profile_id not in saved_order] + _save_order(gui_config_file, ordered) + return ordered + + +def append_profile_to_visual_order(gui_config_file, profile_id, existing_ids): + try: + profile_id = int(profile_id) + except (TypeError, ValueError): + return + existing_order = [ + existing_id for existing_id in _clean_ids(existing_ids) if existing_id != profile_id] + existing = set(existing_order) + saved_order = [ + existing_id for existing_id in _load_order(gui_config_file) if existing_id in existing] + ordered = saved_order + [ + existing_id for existing_id in existing_order if existing_id not in saved_order] + ordered.append(profile_id) + _save_order(gui_config_file, ordered) diff --git a/gui/v2/actions/should_be_synchronized.py b/gui/v2/actions/should_be_synchronized.py new file mode 100755 index 0000000..dbffd62 --- /dev/null +++ b/gui/v2/actions/should_be_synchronized.py @@ -0,0 +1,9 @@ +from datetime import datetime, timezone, timedelta + +from core.controllers.ConfigurationController import ConfigurationController + + +def should_be_synchronized(): + current_datetime = datetime.today().astimezone(timezone.utc) + last_synced_at = ConfigurationController.get_last_synced_at() + return last_synced_at is None or (current_datetime - last_synced_at) >= timedelta(days=30) diff --git a/gui/v2/actions/sync.py b/gui/v2/actions/sync.py new file mode 100755 index 0000000..806bea3 --- /dev/null +++ b/gui/v2/actions/sync.py @@ -0,0 +1,19 @@ +def generate_grid_positions(num_items): + positions = [] + start_x = 395 + start_y = 90 + button_width = 185 + button_height = 75 + h_spacing = 10 + v_spacing = 105 + + for i in range(num_items): + col = i % 2 + row = i // 2 + + x = start_x + (col * (button_width + h_spacing)) + y = start_y + (row * v_spacing) + + positions.append((x, y, button_width, button_height)) + + return positions diff --git a/gui/v2/actions/ticket_failure.py b/gui/v2/actions/ticket_failure.py new file mode 100755 index 0000000..20c1cea --- /dev/null +++ b/gui/v2/actions/ticket_failure.py @@ -0,0 +1,57 @@ +import logging +from datetime import datetime, timezone + +from gui.v2.actions.config import load_gui_config, save_gui_config, default_gui_config + + +def save_ticket_verification_failure(gui_config_file, result): + try: + failed_validations = result.get('failed_validations', []) if isinstance(result, dict) else [] + if not failed_validations: + return False + + config = load_gui_config(gui_config_file) + if config is None: + config = default_gui_config() + if "tickets" not in config: + config["tickets"] = {} + + config["tickets"]["failed_verification"] = { + "message": result.get('message', 'verification_failed'), + "how_many_failed": result.get('how_many_failed', len(failed_validations)), + "failed_validations": list(failed_validations), + "updated_at": datetime.now(timezone.utc).isoformat(), + } + save_gui_config(gui_config_file, config) + return True + except Exception as e: + logging.error(f"Error saving ticket verification failure: {e}") + return False + + +def get_ticket_verification_failure(gui_config_file): + try: + config = load_gui_config(gui_config_file) + if not config: + return None + failure = config.get("tickets", {}).get("failed_verification") + if not isinstance(failure, dict): + return None + failed_validations = failure.get("failed_validations") + if not failed_validations: + return None + return failure + except Exception as e: + logging.error(f"Error loading ticket verification failure: {e}") + return None + + +def clear_ticket_verification_failure(gui_config_file): + try: + config = load_gui_config(gui_config_file) + if not config or "tickets" not in config: + return + config["tickets"].pop("failed_verification", None) + save_gui_config(gui_config_file, config) + except Exception as e: + logging.error(f"Error clearing ticket verification failure: {e}") diff --git a/gui/v2/infrastructure/__init__.py b/gui/v2/infrastructure/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/infrastructure/__pycache__/__init__.cpython-312.pyc b/gui/v2/infrastructure/__pycache__/__init__.cpython-312.pyc new file mode 100755 index 0000000..3c1f448 Binary files /dev/null and b/gui/v2/infrastructure/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/infrastructure/__pycache__/connection_manager.cpython-312.pyc b/gui/v2/infrastructure/__pycache__/connection_manager.cpython-312.pyc new file mode 100644 index 0000000..0c96f83 Binary files /dev/null and b/gui/v2/infrastructure/__pycache__/connection_manager.cpython-312.pyc differ diff --git a/gui/v2/infrastructure/__pycache__/navigator.cpython-312.pyc b/gui/v2/infrastructure/__pycache__/navigator.cpython-312.pyc new file mode 100644 index 0000000..e7da53c Binary files /dev/null and b/gui/v2/infrastructure/__pycache__/navigator.cpython-312.pyc differ diff --git a/gui/v2/infrastructure/__pycache__/page_registry.cpython-312.pyc b/gui/v2/infrastructure/__pycache__/page_registry.cpython-312.pyc new file mode 100644 index 0000000..79a24b3 Binary files /dev/null and b/gui/v2/infrastructure/__pycache__/page_registry.cpython-312.pyc differ diff --git a/gui/v2/infrastructure/__pycache__/setup_observers.cpython-312.pyc b/gui/v2/infrastructure/__pycache__/setup_observers.cpython-312.pyc new file mode 100644 index 0000000..1710a87 Binary files /dev/null and b/gui/v2/infrastructure/__pycache__/setup_observers.cpython-312.pyc differ diff --git a/gui/v2/infrastructure/connection_manager.py b/gui/v2/infrastructure/connection_manager.py new file mode 100755 index 0000000..b5aa5f8 --- /dev/null +++ b/gui/v2/infrastructure/connection_manager.py @@ -0,0 +1,76 @@ +from core.controllers.ProfileController import ProfileController + + +class ConnectionManager: + def __init__(self): + self._connected_profiles = set() + self._is_synced = False + self._is_profile_being_enabled = {} + self.profile_button_objects = {} + self.available_resolutions = ['800x600', '1024x760', '1152x1080', '1280x1024', + '1920x1080', '2560x1440', '2560x1600', '1920x1440', '1792x1344', '2048x1152'] + self._location_list = [] + self._browser_list = [] + + def get_profile_button_objects(self, profile_id): + return self.profile_button_objects.get(profile_id, None) + + def set_profile_button_objects(self, profile_id, profile_button_objects): + self.profile_button_objects[profile_id] = profile_button_objects + + def get_available_resolutions(self, profile_id): + profile = ProfileController.get(profile_id) + if profile and hasattr(profile, 'resolution') and profile.resolution: + self.available_resolutions.append(profile.resolution) + return self.available_resolutions + + def add_custom_resolution(self, resolution): + self.available_resolutions.append(resolution) + + def add_connected_profile(self, profile_id): + self._connected_profiles.add(profile_id) + + def remove_connected_profile(self, profile_id): + self._connected_profiles.discard(profile_id) + + def is_profile_connected(self, profile_id): + return profile_id in self._connected_profiles + + def get_connected_profiles(self): + return list(self._connected_profiles) + + def set_synced(self, status): + self._is_synced = status + + def is_synced(self): + return self._is_synced + + def get_location_info(self, location_key): + if not location_key: + return None + try: + country_code, location_code = location_key.split('_') + for location in self._location_list: + if location.country_code == country_code and location.code == location_code: + return location + + return None + except (ValueError, AttributeError): + return None + + def store_locations(self, locations): + self._location_list = locations + + def store_browsers(self, browsers): + self._browser_list = browsers + + def get_browser_list(self): + return self._browser_list + + def get_location_list(self): + if self.is_synced(): + location_names = [ + f'{location.country_code}_{location.code}' for location in self._location_list] + return location_names + else: + return [] diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py new file mode 100755 index 0000000..9f7cbaf --- /dev/null +++ b/gui/v2/infrastructure/navigator.py @@ -0,0 +1,33 @@ +import importlib + +from gui.v2.infrastructure.page_registry import PAGE_REGISTRY + + +class Navigator: + def __init__(self, page_stack, custom_window): + self.page_stack = page_stack + self.custom_window = custom_window + self._instances = {} + + def navigate(self, name): + page = self._instances.get(name) + if page is None: + if name not in PAGE_REGISTRY: + self._warn_not_migrated(name) + return + module_path, class_name = PAGE_REGISTRY[name] + module = importlib.import_module(module_path) + cls = getattr(module, class_name) + page = cls(self.page_stack, self.custom_window) + self.page_stack.addWidget(page) + self._instances[name] = page + self.page_stack.setCurrentIndex(self.page_stack.indexOf(page)) + + def get_cached(self, name): + return self._instances.get(name) + + def _warn_not_migrated(self, name): + try: + self.custom_window.update_status(f"Page '{name}' not migrated yet.") + except Exception: + pass diff --git a/gui/v2/infrastructure/page_registry.py b/gui/v2/infrastructure/page_registry.py new file mode 100755 index 0000000..774ca6e --- /dev/null +++ b/gui/v2/infrastructure/page_registry.py @@ -0,0 +1,32 @@ +PAGE_REGISTRY = { + "blank": ("gui.v2.ui.pages._blank_page", "BlankPage"), + "welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"), + "menu": ("gui.v2.ui.pages.menu_page", "MenuPage"), + "protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"), + "hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"), + "residential": ("gui.v2.ui.pages.residential_page", "ResidentialPage"), + "tor": ("gui.v2.ui.pages.tor_page", "TorPage"), + "connection": ("gui.v2.ui.pages.connection_page", "ConnectionPage"), + "location": ("gui.v2.ui.pages.location_page", "LocationPage"), + "screen": ("gui.v2.ui.pages.screen_page", "ScreenPage"), + "resume": ("gui.v2.ui.pages.resume_page", "ResumePage"), + "browser": ("gui.v2.ui.pages.browser_page", "BrowserPage"), + "editor": ("gui.v2.ui.pages.editor_page", "EditorPage"), + "install_system_package": ("gui.v2.ui.pages.install_system_package", "InstallSystemPackage"), + "wireguard": ("gui.v2.ui.pages.wireguard_page", "WireGuardPage"), + "sync_screen": ("gui.v2.ui.pages.sync_screen", "SyncScreen"), + "settings": ("gui.v2.ui.pages.settings_page", "Settings"), + "id": ("gui.v2.ui.pages.id_page", "IdPage"), + "payment_confirmed": ("gui.v2.ui.pages.payment_confirmed_page", "PaymentConfirmed"), + "systemwide_prompt": ("gui.v2.ui.pages.systemwide_prompt_page", "SystemwidePromptPage"), + "policy_suggestion": ("gui.v2.ui.pages.policy_suggestion_page", "PolicySuggestionPage"), + "duration_selection": ("gui.v2.ui.pages.duration_selection_page", "DurationSelectionPage"), + "currency_selection": ("gui.v2.ui.pages.currency_selection_page", "CurrencySelectionPage"), + "payment_details": ("gui.v2.ui.pages.payment_details_page", "PaymentDetailsPage"), + "plan_picker": ("gui.v2.ui.pages.plan_picker_page", "PlanPickerPage"), + "ticket_crypto_picker": ("gui.v2.ui.pages.ticket_crypto_picker_page", "TicketCryptoPickerPage"), + "ticket_prep": ("gui.v2.ui.pages.ticket_prep_page", "TicketPrepPage"), + "ticket_or_billing_choice": ("gui.v2.ui.pages.ticket_or_billing_choice_page", "TicketOrBillingChoicePage"), + "fast_registration": ("gui.v2.ui.pages.fast_registration_page", "FastRegistrationPage"), + "fast_mode_prompt": ("gui.v2.ui.pages.fast_mode_prompt_page", "FastModePromptPage"), +} diff --git a/gui/v2/infrastructure/setup_observers.py b/gui/v2/infrastructure/setup_observers.py new file mode 100755 index 0000000..51b3ef2 --- /dev/null +++ b/gui/v2/infrastructure/setup_observers.py @@ -0,0 +1,74 @@ +from core.observers.ApplicationVersionObserver import ApplicationVersionObserver +from core.observers.ClientObserver import ClientObserver +from core.observers.ConnectionObserver import ConnectionObserver +from core.observers.InvoiceObserver import InvoiceObserver +from core.observers.ProfileObserver import ProfileObserver +from core.observers.TicketObserver import TicketObserver +from core.controllers.ApplicationController import ApplicationController + + +application_version_observer = ApplicationVersionObserver() +client_observer = ClientObserver() +connection_observer = ConnectionObserver() +invoice_observer = InvoiceObserver() +profile_observer = ProfileObserver() +ticket_observer = TicketObserver() + + +def setup_observers(update_status): + profile_observer.subscribe( + 'created', lambda event: update_status('Profile Created')) + profile_observer.subscribe( + 'destroyed', lambda event: update_status('Profile destroyed')) + + client_observer.subscribe( + 'synchronizing', lambda event: update_status('Sync in progress...')) + client_observer.subscribe( + 'synchronized', lambda event: update_status('Sync complete')) + + client_observer.subscribe( + 'updating', lambda event: update_status('Updating client...')) + client_observer.subscribe('update_progressing', lambda event: update_status( + f'Current progress: {event.meta.get('progress'):.2f}%')) + client_observer.subscribe('updated', lambda event: update_status( + 'Restart client to apply update.')) + + application_version_observer.subscribe('downloading', lambda event: update_status( + f'Downloading {ApplicationController.get(event.subject.application_code).name}')) + application_version_observer.subscribe('download_progressing', lambda event: update_status( + f'Downloading {ApplicationController.get(event.subject.application_code).name} {event.meta.get('progress'):.2f}%')) + application_version_observer.subscribe('downloaded', lambda event: update_status( + f'Downloaded {ApplicationController.get(event.subject.application_code).name}')) + + connection_observer.subscribe('connecting', lambda event: update_status( + f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...')) + + connection_observer.subscribe('tor_bootstrapping', lambda event: update_status( + 'Establishing Tor connection...')) + + connection_observer.subscribe('tor_bootstrap_progressing', lambda event: update_status( + f'Bootstrapping Tor {event.meta.get('progress'):.2f}%')) + + connection_observer.subscribe( + 'tor_bootstrapped', lambda event: update_status('Tor connection established.')) + + ticket_observer.subscribe('connecting', lambda event: update_status('Connecting to ticket server...')) + ticket_observer.subscribe('sync_done', lambda event: update_status('Ticket prices synced.')) + ticket_observer.subscribe('waiting', lambda event: update_status('Waiting for payment...')) + ticket_observer.subscribe('paid', lambda event: update_status('Payment received.')) + ticket_observer.subscribe('ticket_ready', lambda event: update_status('Ticket ready.')) + ticket_observer.subscribe('used', lambda event: update_status('Ticket used.')) + ticket_observer.subscribe('connection_error', lambda event: update_status('Ticket server connection error.')) + ticket_observer.subscribe('failed_output', lambda event: update_status(f'{event.subject if event.subject else ""}')) + ticket_observer.subscribe('failed_input', lambda event: update_status(f'{event.subject if event.subject else ""}')) + ticket_observer.subscribe('unknown_error', lambda event: update_status('Unknown ticket error.')) + ticket_observer.subscribe('error', lambda event: update_status(f'Ticket error: {event.subject if event.subject else ""}')) + + return { + 'application_version_observer': application_version_observer, + 'client_observer': client_observer, + 'connection_observer': connection_observer, + 'invoice_observer': invoice_observer, + 'profile_observer': profile_observer, + 'ticket_observer': ticket_observer, + } diff --git a/gui/v2/ui/__init__.py b/gui/v2/ui/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/ui/__pycache__/__init__.cpython-312.pyc b/gui/v2/ui/__pycache__/__init__.cpython-312.pyc new file mode 100755 index 0000000..69f7ced Binary files /dev/null and b/gui/v2/ui/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/ui/builders/__init__.py b/gui/v2/ui/builders/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/ui/builders/__pycache__/__init__.cpython-312.pyc b/gui/v2/ui/builders/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..159212c Binary files /dev/null and b/gui/v2/ui/builders/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/ui/builders/__pycache__/bottom_section.cpython-312.pyc b/gui/v2/ui/builders/__pycache__/bottom_section.cpython-312.pyc new file mode 100644 index 0000000..a531564 Binary files /dev/null and b/gui/v2/ui/builders/__pycache__/bottom_section.cpython-312.pyc differ diff --git a/gui/v2/ui/builders/__pycache__/system_tray.cpython-312.pyc b/gui/v2/ui/builders/__pycache__/system_tray.cpython-312.pyc new file mode 100644 index 0000000..f03ade5 Binary files /dev/null and b/gui/v2/ui/builders/__pycache__/system_tray.cpython-312.pyc differ diff --git a/gui/v2/ui/builders/__pycache__/top_section.cpython-312.pyc b/gui/v2/ui/builders/__pycache__/top_section.cpython-312.pyc new file mode 100644 index 0000000..31cf23e Binary files /dev/null and b/gui/v2/ui/builders/__pycache__/top_section.cpython-312.pyc differ diff --git a/gui/v2/ui/builders/bottom_section.py b/gui/v2/ui/builders/bottom_section.py new file mode 100755 index 0000000..378cbb8 --- /dev/null +++ b/gui/v2/ui/builders/bottom_section.py @@ -0,0 +1,54 @@ +import os + +from PyQt6.QtWidgets import QLabel, QPushButton +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt + + +def create_bottom_section(parent, btn_path, client_version, is_sync_urgent): + status_label = QLabel(parent) + status_label.setGeometry(15, 518, 780, 20) + status_label.setAlignment( + Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + status_label.setStyleSheet( + "color: rgb(0, 255, 255); font-size: 16px;") + status_label.setText( + f"Status: Client version {client_version}") + + tor_text = QLabel("Billing & Browsers", parent) + tor_text.setGeometry(532, 541, 150, 20) + tor_text.setStyleSheet(""" + QLabel { + color: cyan; + font-size: 11px; + font-weight: bold; + } + """) + + toggle_button = QPushButton(parent) + toggle_button.setGeometry(670, 541, 50, 22) + toggle_button.setCheckable(True) + + tor_icon = QLabel(parent) + tor_icon.setGeometry(730, 537, 25, 25) + + sync_button = QPushButton(parent) + sync_button.setGeometry(771, 541, 22, 22) + sync_button.setObjectName('sync_button') + + if is_sync_urgent: + sync_icon = QIcon(os.path.join(btn_path, 'icon_sync_urgent.png')) + else: + sync_icon = QIcon(os.path.join(btn_path, 'icon_sync.png')) + + sync_button.setToolTip('Sync') + sync_button.setIcon(sync_icon) + sync_button.setIconSize(sync_button.size()) + + return { + 'status_label': status_label, + 'tor_text': tor_text, + 'toggle_button': toggle_button, + 'tor_icon': tor_icon, + 'sync_button': sync_button, + } diff --git a/gui/v2/ui/builders/system_tray.py b/gui/v2/ui/builders/system_tray.py new file mode 100755 index 0000000..104f791 --- /dev/null +++ b/gui/v2/ui/builders/system_tray.py @@ -0,0 +1,34 @@ +import os + +from PyQt6.QtWidgets import QSystemTrayIcon +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import QSize + + +def init_system_tray(parent, icon_base_path): + if not QSystemTrayIcon.isSystemTrayAvailable(): + return None + + tray_icon = QIcon() + window_icon = QIcon() + + tray_sizes = [22, 24] + for size in tray_sizes: + icon_path = os.path.join( + icon_base_path, f"hv-icon-{size}x{size}.png") + if os.path.exists(icon_path): + tray_icon.addFile(icon_path, QSize(size, size)) + + window_sizes = [32, 48, 64, 128] + for size in window_sizes: + icon_path = os.path.join( + icon_base_path, f"hv-icon-{size}x{size}.png") + if os.path.exists(icon_path): + window_icon.addFile(icon_path, QSize(size, size)) + + tray = QSystemTrayIcon(parent) + tray.setToolTip('HydraVeil') + tray.setIcon(tray_icon) + tray.setVisible(True) + parent.setWindowIcon(window_icon) + return tray diff --git a/gui/v2/ui/builders/top_section.py b/gui/v2/ui/builders/top_section.py new file mode 100755 index 0000000..0aa54a0 --- /dev/null +++ b/gui/v2/ui/builders/top_section.py @@ -0,0 +1,30 @@ +import os + +from PyQt6.QtWidgets import QLabel, QWidget +from PyQt6.QtCore import QTimer + + +def create_top_section(parent, css_path): + central_widget = QWidget(parent) + central_widget.setMaximumSize(800, 600) + central_widget.setGeometry(0, 0, 850, 600) + central_widget.setObjectName("centralwidget") + + label = QLabel(central_widget) + label.setGeometry(0, 0, 800, 570) + + css_file = os.path.join(css_path, 'look.css') + parent.setStyleSheet(open(css_file).read() + if os.path.exists(css_file) else '') + + marquee_timer = QTimer(parent) + marquee_timer.timeout.connect(parent.update_marquee) + + return { + 'central_widget': central_widget, + 'label': label, + 'marquee_timer': marquee_timer, + 'text_start_x': 15, + 'text_end_x': 420, + 'text_width': 405, + } diff --git a/gui/v2/ui/pages/Page.py b/gui/v2/ui/pages/Page.py new file mode 100755 index 0000000..f9cd37c --- /dev/null +++ b/gui/v2/ui/pages/Page.py @@ -0,0 +1,86 @@ +import os + +from PyQt6.QtWidgets import QWidget, QLabel, QPushButton +from PyQt6.QtGui import QIcon +from PyQt6 import QtCore + + +class Page(QWidget): + + def __init__(self, name, page_stack, custom_window, parent=None): + super().__init__(parent) + self.custom_window = custom_window + self.btn_path = custom_window.btn_path + self.name = name + self.page_stack = page_stack + self.init_ui() + self.selected_profiles = [] + self.selected_wireguard = [] + self.selected_residential = [] + self.buttons = [] + + def add_selected_profile(self, profile): + self.selected_profiles.clear() + self.selected_wireguard.clear() + self.selected_residential.clear() + self.selected_profiles.append(profile) + + def init_ui(self): + self.title = QLabel(self) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.title.setObjectName("titles") + + self.display = QLabel(self) + self.display.setObjectName("display") + self.display.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + buttons_info = [ + (QPushButton, "button_back", "back", (660, 534, 48, 35)), + (QPushButton, "button_next", "next", (720, 534, 48, 35)), + (QPushButton, "button_reverse", "back", (660, 534, 48, 35)), + (QPushButton, "button_go", "next", (720, 534, 48, 35)), + (QPushButton, "button_apply", "apply", (720, 534, 48, 35)), + ] + for button_type, object_name, icon_name, geometry in buttons_info: + button = button_type(self) + button.setObjectName(object_name) + button.setGeometry(*geometry) + button.setIcon( + QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + button.setIconSize(QtCore.QSize(48, 35)) + button.setVisible(False) + if object_name == "button_back": + self.button_back = button + self.button_back.clicked.connect(self.gestionar_back) + if object_name == "button_next": + self.button_next = button + self.button_next.clicked.connect(self.gestionar_next) + if object_name == "button_reverse": + self.button_reverse = button + self.button_reverse.clicked.connect(self.limpiar) + + if object_name == "button_go": + self.button_go = button + self.button_go.clicked.connect(self.limpiar) + + if object_name == "button_apply": + self.button_apply = button + + def gestionar_back(self): + current_index = self.page_stack.currentIndex() + if current_index == 18: + return + if current_index > 0: + self.page_stack.setCurrentIndex(current_index - 1) + + def gestionar_next(self): + current_index = self.page_stack.currentIndex() + next_index = (current_index + 1) % self.page_stack.count() + self.page_stack.setCurrentIndex(next_index) + self.limpiar() + + def limpiar(self): + self.display.clear() + for boton in self.buttons: + boton.setChecked(False) + + self.button_next.setVisible(False) diff --git a/gui/v2/ui/pages/__init__.py b/gui/v2/ui/pages/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/ui/pages/__pycache__/Page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/Page.cpython-312.pyc new file mode 100644 index 0000000..ab55743 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/Page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/__init__.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/__init__.cpython-312.pyc new file mode 100755 index 0000000..1fb2085 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/_dummy_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/_dummy_page.cpython-312.pyc new file mode 100755 index 0000000..1713287 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/_dummy_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/browser_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/browser_page.cpython-312.pyc new file mode 100644 index 0000000..9554d34 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/browser_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/editor_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/editor_page.cpython-312.pyc new file mode 100644 index 0000000..cfce092 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/editor_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/fast_registration_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/fast_registration_page.cpython-312.pyc new file mode 100644 index 0000000..af4ff32 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/fast_registration_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/hidetor_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/hidetor_page.cpython-312.pyc new file mode 100644 index 0000000..0879168 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/hidetor_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/location_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/location_page.cpython-312.pyc new file mode 100644 index 0000000..51e03f2 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/location_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/location_verification_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/location_verification_page.cpython-312.pyc new file mode 100644 index 0000000..25b1b57 Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/location_verification_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/menu_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/menu_page.cpython-312.pyc new file mode 100644 index 0000000..1ed7e8d Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/menu_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/protocol_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/protocol_page.cpython-312.pyc new file mode 100644 index 0000000..12f8a8e Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/protocol_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/screen_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/screen_page.cpython-312.pyc new file mode 100644 index 0000000..942a1ce Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/screen_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/__pycache__/settings_page.cpython-312.pyc b/gui/v2/ui/pages/__pycache__/settings_page.cpython-312.pyc new file mode 100644 index 0000000..6aa9a7d Binary files /dev/null and b/gui/v2/ui/pages/__pycache__/settings_page.cpython-312.pyc differ diff --git a/gui/v2/ui/pages/_blank_page.py b/gui/v2/ui/pages/_blank_page.py new file mode 100755 index 0000000..5dfa940 --- /dev/null +++ b/gui/v2/ui/pages/_blank_page.py @@ -0,0 +1,17 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel +from PyQt6.QtCore import Qt + + +class BlankPage(QWidget): + def __init__(self, page_stack, custom_window, parent=None): + super().__init__(parent) + self.page_stack = page_stack + self.custom_window = custom_window + + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + label = QLabel("v2 — no page migrated yet") + label.setStyleSheet("color: #888888; font-size: 14px;") + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(label) diff --git a/gui/v2/ui/pages/browser_page.py b/gui/v2/ui/pages/browser_page.py new file mode 100755 index 0000000..4834467 --- /dev/null +++ b/gui/v2/ui/pages/browser_page.py @@ -0,0 +1,241 @@ +import os + +from PyQt6.QtWidgets import ( + QApplication, QWidget, QPushButton, QButtonGroup, QScrollArea +) +from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QFont +from PyQt6.QtCore import Qt +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class BrowserPage(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("Browser", page_stack, main_window, parent) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.selected_browser_icon = None + self.display.setGeometry(QtCore.QRect(5, 10, 390, 520)) + self.title.setGeometry(395, 40, 380, 40) + self.title.setText("Pick a Browser") + self.button_back.setVisible(True) + + cm = main_window.connection_manager + if cm.is_synced(): + from gui.v2.actions.sync import generate_grid_positions + browsers = cm.get_browser_list() + positions = generate_grid_positions(len(browsers)) + available = [(QPushButton, brw, positions[i]) for i, brw in enumerate(browsers)] + self.create_interface_elements(available) + + def create_interface_elements(self, available_browsers): + self._setup_scroll_area() + button_widget = self._create_button_widget() + self._populate_button_widget(button_widget, available_browsers) + self.scroll_area.setWidget(button_widget) + + def _setup_scroll_area(self): + self.scroll_area = QScrollArea(self) + self.scroll_area.setGeometry(400, 90, 385, 400) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self._apply_scroll_area_style() + + def _apply_scroll_area_style(self): + self.scroll_area.setStyleSheet(""" + QScrollArea { + background: transparent; + border: none; + } + QScrollBar:vertical { + border: 1px solid white; + background: white; + width: 10px; + margin: 0px 0px 0px 0px; + } + QScrollBar::handle:vertical { + background: cyan; + min-height: 0px; + } + QScrollBar::add-line:vertical { + height: 0px; + subcontrol-position: bottom; + subcontrol-origin: margin; + } + QScrollBar::sub-line:vertical { + height: 0px; + subcontrol-position: top; + subcontrol-origin: margin; + } + """) + + def _create_button_widget(self): + button_widget = QWidget() + button_widget.setStyleSheet("background: transparent;") + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + return button_widget + + def _populate_button_widget(self, button_widget, available_browsers): + dimensions = self._get_button_dimensions() + total_height = self._create_browser_buttons( + button_widget, available_browsers, dimensions) + self._set_final_widget_size(button_widget, dimensions, total_height) + + def _get_button_dimensions(self): + return { + 'width': 185, + 'height': 75, + 'spacing': 20, + 'x_offset': 195 + } + + def _create_browser_buttons(self, button_widget, browser_icons, dims): + total_height = 0 + for browser_tuple in browser_icons: + _, browser_string, geometry = browser_tuple + x_coord = geometry[0] - 395 + y_coord = geometry[1] - 90 + self._create_single_button( + button_widget, browser_string, x_coord, y_coord, dims) + total_height = max(total_height, y_coord + dims['height']) + return total_height + + def _create_single_button(self, parent, icon_name, x_coord, y_coord, dims): + button = QPushButton(parent) + button.setFixedSize(dims['width'], dims['height']) + button.move(x_coord, y_coord) + button.setIconSize(button.size()) + button.setCheckable(True) + + browser_name, version = icon_name.split(':') + button.setIcon(QIcon(BrowserPage.create_browser_button_image( + f"{browser_name} {version}", self.btn_path))) + if button.icon().isNull(): + fallback_path = os.path.join( + self.btn_path, "default_browser_button.png") + button.setIcon(QIcon(BrowserPage.create_browser_button_image( + f"{browser_name} {version}", fallback_path, True))) + self._apply_button_style(button) + + self.buttons.append(button) + self.buttonGroup.addButton(button) + button.clicked.connect( + lambda _, b=icon_name: self.show_browser(b.replace(':', ' '))) + + @staticmethod + def create_browser_button_image(browser_text, btn_path, is_fallback=False): + + if ':' in browser_text: + browser_name, version = browser_text.split(':', 1) + else: + browser_name, version = browser_text.split( + )[0], ' '.join(browser_text.split()[1:]) + if is_fallback: + base_image = QPixmap(btn_path) + else: + base_image = QPixmap(os.path.join( + btn_path, f"{browser_name}_button.png")) + + if base_image.isNull(): + base_image = QPixmap(185, 75) + base_image.fill(QColor(44, 62, 80)) + + painter = QPainter(base_image) + if painter.isActive(): + BrowserPage._setup_version_font(painter, version, base_image) + if is_fallback: + BrowserPage._setup_browser_name( + painter, browser_name, base_image) + painter.end() + return base_image + + @staticmethod + def _setup_browser_name(painter, browser_name, base_image): + font_size = 13 + app_font = QApplication.font() + app_font.setPointSize(font_size) + app_font.setWeight(QFont.Weight.Bold) + painter.setFont(app_font) + painter.setPen(QColor(0, 255, 255)) + text_rect = painter.fontMetrics().boundingRect(browser_name) + while text_rect.width() > base_image.width() * 0.6 and font_size > 6: + font_size -= 1 + + x = (base_image.width() - text_rect.width()) // 2 - 30 + y = (base_image.height() + text_rect.height()) // 2 - 10 + painter.drawText(x, y, browser_name) + + @staticmethod + def _setup_version_font(painter, version, base_image): + font_size = 11 + painter.setFont(QFont('Arial', font_size)) + painter.setPen(QColor(0x88, 0x88, 0x88)) + text_rect = painter.fontMetrics().boundingRect(version) + while text_rect.width() > base_image.width() * 0.6 and font_size > 6: + font_size -= 1 + painter.setFont(QFont('Arial', font_size)) + text_rect = painter.fontMetrics().boundingRect(version) + + x = (base_image.width() - text_rect.width()) // 2 - 27 + y = (base_image.height() + text_rect.height()) // 2 + 10 + painter.drawText(x, y, version) + + def _apply_button_style(self, button): + button.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(200, 200, 200, 30); + } + QPushButton:checked { + background-color: rgba(200, 200, 200, 50); + } + """) + + def _set_final_widget_size(self, widget, dims, total_height): + widget.setFixedSize(dims['width'] * 2 + 5, + total_height + dims['spacing']) + + def show_browser(self, browser): + browser_name, version = browser.split( + )[0], ' '.join(browser.split()[1:]) + try: + base_image = self._create_browser_display_image( + browser_name, version) + self.display.setPixmap(base_image.scaled( + self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + except Exception: + pass + self.selected_browser_icon = browser + self.button_next.setVisible(True) + self.update_swarp_json() + + def _create_browser_display_image(self, browser_name, version): + base_image = QPixmap(os.path.join( + self.btn_path, f"{browser_name}_icon.png")) + painter = QPainter(base_image) + painter.setFont(QFont('Arial', 25)) + painter.setPen(QColor(0, 255, 255)) + + text_rect = painter.fontMetrics().boundingRect(version) + x = (base_image.width() - text_rect.width()) // 2 + y = base_image.height() - 45 + + painter.drawText(x, y, version) + painter.end() + return base_image + + def update_swarp_json(self): + inserted_data = { + "browser": self.selected_browser_icon + } + + self.update_status.write_data(inserted_data) + + def gestionar_next(self): + self.custom_window.navigator.navigate("screen") diff --git a/gui/v2/ui/pages/connection_page.py b/gui/v2/ui/pages/connection_page.py new file mode 100755 index 0000000..b4d8d70 --- /dev/null +++ b/gui/v2/ui/pages/connection_page.py @@ -0,0 +1,48 @@ +import os + +from PyQt6.QtWidgets import QLabel +from PyQt6.QtGui import QPixmap +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class ConnectionPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Connection", page_stack, main_window, parent) + + self.create_interface_elements() + + def create_interface_elements(self): + self.display.setGeometry(QtCore.QRect(5, 50, 390, 430)) + self.title.setGeometry(QtCore.QRect(465, 40, 300, 20)) + self.title.setText("Pick a TOR") + + labels_info = [ + ("server", None, (410, 115)), + ("", "rr2", (560, 115)), + ("port", None, (410, 205)), + ("", "rr2", (560, 205)), + ("username", None, (410, 295)), + ("", "rr2", (560, 295)), + ("password", None, (410, 385)), + ("", "rr2", (560, 385)) + ] + + for j, (text, image_name, position) in enumerate(labels_info): + label = QLabel(self) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + if image_name: + label.setGeometry(position[0], position[1], 220, 50) + else: + label.setGeometry(position[0], position[1], 140, 50) + label.setStyleSheet( + "background-color: rgba(255, 255, 255, 51); padding: 3px;") + + if image_name: + pixmap = QPixmap(os.path.join( + self.btn_path, f"{image_name}.png")) + label.setPixmap(pixmap) + label.setScaledContents(True) + else: + label.setText(text) diff --git a/gui/v2/ui/pages/currency_selection_page.py b/gui/v2/ui/pages/currency_selection_page.py new file mode 100755 index 0000000..e53f80f --- /dev/null +++ b/gui/v2/ui/pages/currency_selection_page.py @@ -0,0 +1,65 @@ +import os + +from PyQt6.QtWidgets import QButtonGroup, QLabel, QPushButton +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import QSize +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class CurrencySelectionPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Select Currency", page_stack, main_window, parent) + self.update_status = main_window + self.selected_duration = None + self.create_interface_elements() + self.button_reverse.setVisible(True) + self.button_next.setVisible(False) + + def create_interface_elements(self): + self.title = QLabel("Payment Method", self) + self.title.setGeometry(QtCore.QRect(510, 30, 250, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.button_reverse.clicked.connect(self.reverse) + + button_info = [ + ("monero", (545, 75)), + ("bitcoin", (545, 290)), + ("lightnering", (545, 180)), + ("litecoin", (545, 395)) + ] + + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + + for j, (icon_name, position) in enumerate(button_info): + button = QPushButton(self) + button.setGeometry(position[0], position[1], 185, 75) + button.setIconSize(QSize(190, 120)) + button.setCheckable(True) + self.buttons.append(button) + self.buttonGroup.addButton(button, j) + button.setIcon( + QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + button.clicked.connect(self.on_currency_selected) + + def on_currency_selected(self): + self.custom_window.navigator.navigate("payment_details") + payment_details_page = self.custom_window.navigator.get_cached("payment_details") + if payment_details_page is not None: + payment_details_page.selected_duration = self.selected_duration + payment_details_page.selected_currency = self.get_selected_currency() + payment_details_page.check_invoice() + self.update_status.update_status('Loading payment details...') + + def get_selected_currency(self): + selected_button = self.buttonGroup.checkedButton() + if selected_button: + index = self.buttonGroup.id(selected_button) + currencies = ["monero", "bitcoin", "lightning", "litecoin"] + return currencies[index] + return None + + def reverse(self): + self.custom_window.navigator.navigate("duration_selection") diff --git a/gui/v2/ui/pages/duration_selection_page.py b/gui/v2/ui/pages/duration_selection_page.py new file mode 100755 index 0000000..c355c4e --- /dev/null +++ b/gui/v2/ui/pages/duration_selection_page.py @@ -0,0 +1,92 @@ +from PyQt6.QtWidgets import QComboBox, QLabel, QPushButton +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class DurationSelectionPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Select Duration", page_stack, main_window, parent) + self.update_status = main_window + self.create_interface_elements() + self.button_reverse.setVisible(True) + + def create_interface_elements(self): + self.title = QLabel("Select Duration", self) + self.title.setGeometry(QtCore.QRect(530, 30, 210, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.button_reverse.clicked.connect(self.reverse) + + self.combo_box = QComboBox(self) + self.combo_box.setGeometry(200, 200, 400, 60) + self.combo_box.addItems( + ["1 month (€1.50)", "3 months (€4.30)", "6 months (€8)", "12 months (€16)"]) + self.combo_box.setEditable(False) + self.combo_box.setMaxVisibleItems(4) + self.combo_box.setDuplicatesEnabled(True) + self.combo_box.setPlaceholderText("Select options") + self.combo_box.setStyleSheet(""" + QComboBox { + font-size: 16px; + padding: 10px; + background-color: #000000; + color: #00ffff; + border: 2px solid #00ffff; + border-radius: 8px; + } + QComboBox::drop-down { + width: 30px; + background-color: #1a1a1a; + border-left: 1px solid #00ffff; + } + QComboBox::down-arrow { + width: 12px; + height: 12px; + color: #00ffff; + } + QComboBox QAbstractItemView { + background-color: #0a0a0a; + border: 1px solid #00ffff; + border-radius: 4px; + selection-background-color: #003333; + selection-color: #00ffff; + } + QComboBox QAbstractItemView::item { + padding: 8px; + border: none; + color: #00ffff; + background-color: #0a0a0a; + } + QComboBox QAbstractItemView::item:hover { + background-color: #1a1a1a; + color: #00ffff; + } + QComboBox QAbstractItemView::item:selected { + background-color: #003333; + color: #00ffff; + } + """) + + self.next_button = QPushButton("Next", self) + self.next_button.setGeometry(350, 300, 100, 40) + self.next_button.setStyleSheet(""" + QPushButton { + font-size: 18px; + padding: 10px; + border: 2px solid #ccc; + border-radius: 10px; + background-color: cyan; + color: black; + font-weight: bold; + } + """) + self.next_button.clicked.connect(self.show_next) + + def show_next(self): + self.custom_window.navigator.navigate("currency_selection") + currency_page = self.custom_window.navigator.get_cached("currency_selection") + if currency_page is not None: + currency_page.selected_duration = self.combo_box.currentText() + + def reverse(self): + self.custom_window.navigator.navigate("id") diff --git a/gui/v2/ui/pages/editor_page.py b/gui/v2/ui/pages/editor_page.py new file mode 100755 index 0000000..ce00ca4 --- /dev/null +++ b/gui/v2/ui/pages/editor_page.py @@ -0,0 +1,817 @@ +import os + +from PyQt6.QtWidgets import ( + QLabel, QPushButton, QLineEdit, QTextEdit, QFrame, QDialog, + QVBoxLayout, QHBoxLayout, QMessageBox, QGraphicsDropShadowEffect +) +from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QTransform +from PyQt6.QtCore import Qt, QSize, QTimer, QPointF +from PyQt6 import QtCore, QtGui + +from core.controllers.ProfileController import ProfileController +from core.controllers.LocationController import LocationController + +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.screen_page import ScreenPage +from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup + + +class EditorPage(Page): + def __init__(self, page_stack, main_window): + super().__init__("Editor", page_stack, main_window) + self.page_stack = page_stack + self.update_status = main_window + self.connection_manager = main_window.connection_manager + self.labels = [] + self.buttons = [] + self.title.setGeometry(570, 40, 185, 40) + self.title.setText("Edit Profile") + + self.button_apply.setVisible(True) + self.button_apply.clicked.connect(self.go_selected) + + self.button_back.setVisible(True) + try: + self.button_back.clicked.disconnect() + except TypeError: + pass + self.button_back.clicked.connect(self.go_back) + + self.name_handle = QLabel(self) + self.name_handle.setGeometry(110, 70, 400, 30) + self.name_handle.setStyleSheet('color: #cacbcb;') + self.name_handle.setText("Profile Name:") + + self.name_hint = QLabel(self) + self.name_hint.setGeometry(265, 100, 190, 20) + self.name_hint.setStyleSheet( + 'color: #888888; font-size: 10px; font-style: italic;') + self.name_hint.setText("Click here to edit profile name") + self.name_hint.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.name = QLineEdit(self) + self.name.setPlaceholderText("Enter name") + self.name.setGeometry(265, 70, 190, 30) + self.name.setStyleSheet( + "color: cyan; border: 1px solid #666666; border-radius: 3px; background-color: rgba(0, 0, 0, 0.3);") + self.name.setCursor(QtCore.Qt.CursorShape.IBeamCursor) + self.name.focusInEvent = lambda event: self.name_hint.hide() + self.name.focusOutEvent = lambda event: self.name_hint.show( + ) if not self.name.text() else self.name_hint.hide() + + self.name.textChanged.connect( + lambda text: self.name_hint.hide() if text else self.name_hint.show()) + + self.temp_changes = {} + self.original_values = {} + self.res_hint_shown = False + self.display.setGeometry(QtCore.QRect(0, 60, 540, 405)) + self.display.hide() + + self.garaje = QLabel(self) + self.garaje.setGeometry(QtCore.QRect(0, 70, 540, 460)) + + self.brow_disp = QLabel(self) + self.brow_disp.setGeometry(QtCore.QRect(0, 50, 540, 460)) + self.brow_disp.setPixmap( + QPixmap(os.path.join(self.btn_path, "browser only.png"))) + self.brow_disp.hide() + self.brow_disp.lower() + + def _selected_profiles_str(self): + menu_page = self.find_menu_page() + if menu_page is not None and menu_page.selected_profiles: + return ', '.join( + [f"Profile_{profile['profile_number']}" for profile in menu_page.selected_profiles]) + profile_id = getattr(self.update_status, 'current_profile_id', None) + if profile_id is not None: + return f"Profile_{int(profile_id)}" + return '' + + def update_name_value(self): + original_name = self.data_profile.get('name', '') + new_name = self.name.text() + if original_name != new_name: + self.update_temp_value('name', new_name) + else: + pass + + def go_back(self): + selected_profiles_str = self._selected_profiles_str() + if self.has_unsaved_changes(selected_profiles_str): + self.show_unsaved_changes_popup() + else: + self.custom_window.navigator.navigate("menu") + + def find_menu_page(self): + return self.custom_window.navigator.get_cached("menu") + + def find_resume_page(self): + return self.custom_window.navigator.get_cached("resume") + + def find_protocol_page(self): + return self.custom_window.navigator.get_cached("protocol") + + def showEvent(self, event): + super().showEvent(event) + self.res_hint_shown = False + self.extraccion() + + def extraccion(self): + self.data_profile = {} + for label in self.labels: + label.deleteLater() + self.labels = [] + for button in self.buttons: + button.deleteLater() + self.buttons = [] + + selected_profiles_str = self._selected_profiles_str() + menu_page = self.find_menu_page() + if menu_page: + 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( + ) + + profile_id = int(selected_profiles_str.split('_')[1]) + self.data_profile['id'] = profile_id + + if selected_profiles_str in self.temp_changes: + for key, value in self.temp_changes[selected_profiles_str].items(): + self.data_profile[key] = value + + self.name.textChanged.connect(self.update_name_value) + self.verificate(self.data_profile, selected_profiles_str) + + def verificate(self, data_profile, selected_profile_str): + protocol = data_profile.get("protocol", "") + + try: + if protocol == "wireguard": + self.process_and_show_labels(data_profile, { + "protocol": ['wireguard', 'residential', 'hidetor'], + "connection": ['browser-only', 'system-wide'], + "location": self.connection_manager.get_location_list(), + "browser": self.connection_manager.get_browser_list(), + "dimentions": self.connection_manager.get_available_resolutions(data_profile.get('id', '')) + }, selected_profile_str) + + elif protocol == "residential" or protocol == "hidetor": + self.process_and_show_labels(data_profile, { + "protocol": ['residential', 'wireguard', 'hidetor'], + "connection": ['tor', 'just proxy'], + "location": self.connection_manager.get_location_list(), + "browser": self.connection_manager.get_browser_list(), + "dimentions": self.connection_manager.get_available_resolutions(data_profile.get('id', '')) + }, selected_profile_str) + + elif protocol == "open": + self.process_and_show_labels(data_profile, { + "protocol": ['open'] + }, selected_profile_str) + + except Exception as e: + print(f'An error occurred in verificate: {e}') + + def process_and_show_labels(self, data_profile, parameters, selected_profile_str): + protocol = data_profile.get('protocol', "") + connection = data_profile.get('connection', "") + location = data_profile.get('location', "") + country_garaje = location + name = data_profile.get('name', "") + self.name.setText(name) + + if protocol == "hidetor": + if connection == "just proxy": + self.display.setPixmap(QPixmap(os.path.join( + self.btn_path, f"icon_{location}.png"))) + else: + self.display.setPixmap(QPixmap(os.path.join( + self.btn_path, f"hdtor_{location}.png"))) + self.display.show() + self.display.setGeometry(0, 100, 400, 394) + + self.display.setScaledContents(True) + + self.garaje.hide() + self.brow_disp.hide() + + if protocol == "wireguard": + self.display.setGeometry(0, 60, 540, 405) + self.display.show() + self.display.setPixmap(QPixmap(os.path.join( + self.btn_path, f"wireguard_{location}.png"))) + self.garaje.hide() + if connection == "browser-only": + is_supported = data_profile.get('browser_supported', False) + image_name = "browser only.png" if is_supported else "unsupported_browser_only.png" + self.brow_disp.setPixmap( + QPixmap(os.path.join(self.btn_path, image_name))) + self.brow_disp.show() + else: + self.brow_disp.hide() + + if protocol == "residential": + self.display.setGeometry(0, 60, 540, 405) + self.brow_disp.show() + self.garaje.show() + + self.display.hide() + if country_garaje: + country_garaje = 'brazil' + self.garaje.setPixmap(QPixmap(os.path.join( + self.btn_path, f"{country_garaje} garaje.png"))) + + profile_id = data_profile.get('id') + profile_obj = ProfileController.get(profile_id) + + location_info = None + if location: + try: + if '_' in location: + country_code, location_code = location.split('_', 1) + location_info = LocationController.get( + country_code, location_code) + except Exception: + location_info = None + + operator_name = "" + l_name = "" + o_name = "" + n_key = "" + + if location_info: + if hasattr(location_info, 'operator') and location_info.operator: + operator_name = location_info.operator.name + o_name = location_info.operator.name + n_key = location_info.operator.nostr_public_key + + l_name = f"{location_info.country_name}, {location_info.name}" if hasattr( + location_info, 'country_name') else "" + + if operator_name != 'Simplified Privacy' and operator_name != "" and protocol != 'hidetor': + text_color = "white" + if profile_obj and profile_obj.is_session_profile(): + text_color = "black" + + l_img = QLabel(self) + l_img.setGeometry(20, 125, 150, 150) + pixmap_loc = QPixmap(os.path.join( + self.btn_path, f"icon_{location}.png")) + l_img.setPixmap(pixmap_loc) + l_img.setScaledContents(True) + l_img.show() + self.labels.append(l_img) + + info_txt = QTextEdit(self) + info_txt.setGeometry(180, 120, 300, 250) + info_txt.setReadOnly(True) + info_txt.setFrameStyle(QFrame.Shape.NoFrame) + info_txt.setStyleSheet( + f"background: transparent; border: none; color: {text_color}; font-size: 19px;") + info_txt.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + info_txt.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + info_txt.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere) + info_txt.setText( + f"Location: {l_name}\n\nOperator: {o_name}") + info_txt.show() + self.labels.append(info_txt) + + nostr_txt = QTextEdit(self) + nostr_txt.setGeometry(20, 300, 450, 170) + nostr_txt.setReadOnly(True) + nostr_txt.setFrameStyle(QFrame.Shape.NoFrame) + nostr_txt.setStyleSheet( + f"background: transparent; border: none; color: {text_color}; font-size: 21px;") + nostr_txt.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + nostr_txt.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + nostr_txt.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAnywhere) + nostr_txt.setText(f"Nostr: {n_key}") + nostr_txt.show() + self.labels.append(nostr_txt) + + for i, key in enumerate(parameters.keys()): + if key == 'browser': + browser_value = f"{data_profile.get(key, '')}" + split_value = browser_value.split(':', 1) + browser_type = split_value[0] + browser_version = split_value[1] if len( + split_value) > 1 else '' + browser_value = f"{browser_type} {browser_version}" + if browser_version == '': + browser_version = data_profile.get('browser_version', '') + browser_value = f"{browser_type} {browser_version}" + if connection == 'system-wide' or not browser_value.strip(): + base_image = QPixmap() + else: + base_image = BrowserPage.create_browser_button_image( + browser_value, self.btn_path) + if base_image.isNull(): + fallback_path = os.path.join( + self.btn_path, "default_browser_button.png") + base_image = BrowserPage.create_browser_button_image( + browser_value, fallback_path, True) + elif key == 'location': + current_value = f"{data_profile.get(key, '')}" + image_path = os.path.join( + self.btn_path, f"button_{current_value}.png") + base_image = QPixmap(image_path) + locations = self.connection_manager.get_location_info( + current_value) + provider = locations.operator.name if locations and hasattr( + locations, 'operator') else None + fallback_path = os.path.join( + self.btn_path, "default_location_button.png") + if base_image.isNull(): + if locations and hasattr(locations, 'country_name'): + location_name = locations.country_name + base_image = LocationPage.create_location_button_image( + location_name, fallback_path, provider) + else: + base_image = LocationPage.create_location_button_image( + current_value, fallback_path, provider) + else: + if locations and hasattr(locations, 'country_name'): + base_image = LocationPage.create_location_button_image( + f'{locations.country_code}_{locations.code}', fallback_path, provider, image_path) + else: + base_image = LocationPage.create_location_button_image( + current_value, fallback_path, provider, image_path) + + elif key == 'dimentions': + current_value = f"{data_profile.get(key, '')}" + if current_value != 'None': + base_image = ScreenPage.create_resolution_button_image( + self, current_value) + else: + image_path = os.path.join( + self.btn_path, f"{data_profile.get(key, '')}_button.png") + current_value = data_profile.get(key, '') + base_image = QPixmap(image_path) + + if key == 'dimentions': + label = QPushButton(self) + label.setGeometry(565, 90 + i * 80, 185, 75) + label.setFlat(True) + label.setStyleSheet("border: none; background: transparent;") + original_icon = QIcon(base_image) + label.setIconSize(QSize(185, 75)) + label.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + label.clicked.connect(self.show_custom_res_dialog) + + template_path = os.path.join( + self.btn_path, "resolution_template_button.png") + template_pixmap = QPixmap(template_path).scaled( + 187, 75) if os.path.exists(template_path) else None + + if not getattr(self, 'res_hint_shown', False) and connection != 'system-wide': + self.res_hint_shown = True + if template_pixmap: + label.setIcon(QIcon(template_pixmap)) + else: + label.setIcon(QIcon()) + + hint_label = QLabel("Click here to customize", label) + hint_label.setGeometry(15, 0, 155, 75) + hint_label.setWordWrap(True) + hint_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter) + hint_label.setStyleSheet( + "color: #00ffff; font-weight: bold; font-size: 16px; background: transparent;") + hint_label.setCursor( + QtCore.Qt.CursorShape.PointingHandCursor) + hint_label.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents) + shadow = QGraphicsDropShadowEffect(hint_label) + shadow.setBlurRadius(20) + shadow.setColor(QColor(0, 255, 255)) + shadow.setOffset(0, 0) + hint_label.setGraphicsEffect(shadow) + + def restore(): + try: + if hint_label and not hint_label.isHidden(): + hint_label.hide() + label.setIcon(original_icon) + except: + pass + QTimer.singleShot(3500, restore) + else: + label.setIcon(original_icon) + else: + label = QLabel(key, self) + label.setGeometry(565, 90 + i * 80, 185, 75) + label.setPixmap(base_image) + label.setScaledContents(True) + + label.show() + self.labels.append(label) + + try: + if key == 'browser': + value_index = [item.replace( + ':', ' ') for item in parameters[key]].index(browser_value) + else: + value_index = parameters[key].index(current_value) + except ValueError: + value_index = 0 + + prev_button = QPushButton(self) + prev_button.setGeometry(535, 90 + i * 80, 30, 75) + prev_button.clicked.connect( + lambda _, k=key, idx=value_index: self.show_previous_value(k, idx, parameters)) + prev_button.show() + icon_path = os.path.join(self.btn_path, f"UP_button.png") + icon = QPixmap(icon_path) + transform = QTransform().rotate(180) + rotated_icon = icon.transformed(transform) + + prev_button.setIcon(QIcon(rotated_icon)) + prev_button.setIconSize(prev_button.size()) + + if (key == 'location' or key == 'browser') and not self.connection_manager.is_synced(): + w = prev_button.width() + h = prev_button.height() + outline_pix = QPixmap(w, h) + outline_pix.fill(Qt.GlobalColor.transparent) + painter = QPainter(outline_pix) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + pen = QtGui.QPen(QColor(0, 255, 0)) + pen.setWidth(3) + painter.setPen(pen) + painter.setBrush(Qt.BrushStyle.NoBrush) + left_points = [ + QPointF(w*0.65, h*0.15), QPointF(w*0.25, h*0.50), QPointF(w*0.65, h*0.85)] + painter.drawPolygon(QtGui.QPolygonF(left_points)) + painter.end() + prev_button.setIcon(QIcon(outline_pix)) + prev_button.setIconSize(prev_button.size()) + + if (key == 'location' or key == 'browser') and not self.connection_manager.is_synced(): + prev_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(0, 255, 0, 0.1); + border-radius: 3px; + } + QPushButton:pressed { + background-color: rgba(0, 255, 0, 0.2); + border-radius: 3px; + } + """) + + self.buttons.append(prev_button) + + next_button = QPushButton(self) + next_button.setGeometry(750, 90 + i * 80, 30, 75) + next_button.clicked.connect( + lambda _, k=key, idx=value_index: self.show_next_value(k, idx, parameters)) + next_button.show() + self.buttons.append(next_button) + next_button.setIcon( + QIcon(os.path.join(self.btn_path, f"UP_button.png"))) + next_button.setIconSize(next_button.size()) + if (key == 'location' or key == 'browser') and not self.connection_manager.is_synced(): + w = next_button.width() + h = next_button.height() + outline_pix_r = QPixmap(w, h) + outline_pix_r.fill(Qt.GlobalColor.transparent) + painter_r = QPainter(outline_pix_r) + painter_r.setRenderHint(QPainter.RenderHint.Antialiasing) + pen_r = QtGui.QPen(QColor(0, 255, 0)) + pen_r.setWidth(3) + painter_r.setPen(pen_r) + painter_r.setBrush(Qt.BrushStyle.NoBrush) + right_points = [ + QPointF(w*0.35, h*0.15), QPointF(w*0.75, h*0.50), QPointF(w*0.35, h*0.85)] + painter_r.drawPolygon(QtGui.QPolygonF(right_points)) + painter_r.end() + next_button.setIcon(QIcon(outline_pix_r)) + next_button.setIconSize(next_button.size()) + + if (key == 'location' or key == 'browser') and not self.connection_manager.is_synced(): + next_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(0, 255, 0, 0.1); + border-radius: 3px; + } + QPushButton:pressed { + background-color: rgba(0, 255, 0, 0.2); + border-radius: 3px; + } + """) + + prev_button.setVisible(True) + next_button.setVisible(True) + if key == 'protocol' or (protocol == 'wireguard' and key == 'connection'): + prev_button.setDisabled(True) + next_button.setDisabled(True) + + if connection == 'system-wide' and (key == 'browser' or key == 'dimentions'): + prev_button.setVisible(False) + next_button.setVisible(False) + + def on_sync_complete_for_edit_profile(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): + if status: + self.update_status.update_status('Sync complete.') + self.extraccion() + else: + self.update_status.update_status( + 'Sync failed. Please try again later.') + + def show_previous_value(self, key: str, index: int, parameters: dict) -> None: + if key == 'browser' or key == 'location': + if not self.connection_manager.is_synced(): + self.update_status.update_status('Syncing in progress..') + self.update_status.sync() + self.update_status.worker_thread.sync_output.connect( + self.on_sync_complete_for_edit_profile) + return + + previous_index = (index - 1) % len(parameters[key]) + previous_value = parameters[key][previous_index] + self.update_temp_value(key, previous_value) + + def show_next_value(self, key: str, index: int, parameters: dict) -> None: + if key == 'browser' or key == 'location': + if not self.connection_manager.is_synced(): + self.update_status.update_status('Syncing in progress..') + self.update_status.sync() + self.update_status.worker_thread.sync_output.connect( + self.on_sync_complete_for_edit_profile) + return + + next_index = (index + 1) % len(parameters[key]) + next_value = parameters[key][next_index] + + self.update_temp_value(key, next_value) + + def update_temp_value(self, key: str, new_value: str) -> None: + selected_profiles_str = self._selected_profiles_str() + if selected_profiles_str not in self.temp_changes: + self.temp_changes[selected_profiles_str] = {} + self.original_values[selected_profiles_str] = self.data_profile.copy( + ) + + self.temp_changes[selected_profiles_str][key] = new_value + self.extraccion() + + def show_custom_res_dialog(self): + dialog = QDialog(self) + dialog.setWindowTitle("Custom Resolution") + dialog.setFixedSize(300, 150) + dialog.setModal(True) + dialog.setStyleSheet(""" + QDialog { + background-color: #2c3e50; + border: 2px solid #00ffff; + border-radius: 10px; + } + QLabel { + color: white; + font-size: 12px; + } + QLineEdit { + background-color: #34495e; + color: white; + border: 1px solid #00ffff; + border-radius: 5px; + padding: 5px; + font-size: 12px; + } + QPushButton { + background-color: #2c3e50; + color: white; + border: 2px solid #00ffff; + border-radius: 5px; + padding: 8px; + font-size: 12px; + } + QPushButton:hover { + background-color: #34495e; + } + """) + + layout = QVBoxLayout() + input_layout = QHBoxLayout() + width_label = QLabel("Width:") + width_input = QLineEdit() + width_input.setPlaceholderText("1920") + height_label = QLabel("Height:") + height_input = QLineEdit() + height_input.setPlaceholderText("1080") + + input_layout.addWidget(width_label) + input_layout.addWidget(width_input) + input_layout.addWidget(height_label) + input_layout.addWidget(height_input) + + button_layout = QHBoxLayout() + ok_button = QPushButton("OK") + cancel_button = QPushButton("Cancel") + button_layout.addWidget(cancel_button) + button_layout.addWidget(ok_button) + + layout.addLayout(input_layout) + layout.addLayout(button_layout) + dialog.setLayout(layout) + + def validate_and_accept(): + try: + width = int(width_input.text()) + height = int(height_input.text()) + if width <= 0 or height <= 0: + return + host_w = self.custom_window.host_screen_width + host_h = self.custom_window.host_screen_height + + adjusted = False + if width > host_w: + width = host_w - 50 + adjusted = True + if height > host_h: + height = host_h - 50 + adjusted = True + + if adjusted: + QMessageBox.information( + dialog, "Notice", "Adjusted to fit host size") + + self.update_temp_value('dimentions', f"{width}x{height}") + dialog.accept() + except ValueError: + pass + + ok_button.clicked.connect(validate_and_accept) + cancel_button.clicked.connect(dialog.reject) + + width_input.returnPressed.connect(validate_and_accept) + height_input.returnPressed.connect(validate_and_accept) + + dialog.exec() + + def has_unsaved_changes(self, profile_str: str) -> bool: + return profile_str in self.temp_changes and bool(self.temp_changes[profile_str]) + + def show_apply_changes_popup(self, selected_profiles_str: str) -> bool: + if selected_profiles_str not in self.temp_changes: + return False + temp_changes = self.temp_changes[selected_profiles_str] + current_data = self.original_values[selected_profiles_str] + + current_browser = f"{current_data.get('browser', '')} {current_data.get('browser_version', '')}" + + if 'location' in temp_changes and temp_changes['location'] != current_data.get('location'): + message = "A new location would require a new subscription code.\nDo you want to proceed with erasing the old one?" + elif 'browser' in temp_changes and temp_changes['browser'] != current_browser: + message = "Changing the browser would delete all data associated with it. Proceed?" + else: + return False + + self.popup = ConfirmationPopup( + self, + message=message, + action_button_text="Continue", + cancel_button_text="Cancel" + ) + self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) + self.popup.finished.connect( + lambda result: self.handle_apply_changes_response(result)) + self.popup.show() + return True + + def show_unsaved_changes_popup(self) -> None: + self.popup = ConfirmationPopup( + self, + message="You have unsaved changes. Do you want to continue?", + action_button_text="Continue", + cancel_button_text="Cancel" + ) + self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) + self.popup.finished.connect( + lambda result: self.handle_unsaved_changes_response(result)) + self.popup.show() + + def _refresh_menu_and_navigate(self): + menu_page = self.find_menu_page() + if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): + menu_page.refresh_menu_buttons() + self.custom_window.navigator.navigate("menu") + + def handle_apply_changes_response(self, result: bool) -> None: + if result: + self.commit_changes() + self._refresh_menu_and_navigate() + + def handle_unsaved_changes_response(self, result: bool) -> None: + if result: + self.temp_changes.clear() + self._refresh_menu_and_navigate() + + def go_selected(self) -> None: + selected_profiles_str = self._selected_profiles_str() + + if selected_profiles_str in self.temp_changes and self.temp_changes[selected_profiles_str]: + needs_confirmation = self.show_apply_changes_popup( + selected_profiles_str) + if not needs_confirmation: + self.commit_changes() + self._refresh_menu_and_navigate() + + else: + self.custom_window.navigator.navigate("menu") + + def commit_changes(self) -> None: + selected_profiles_str = self._selected_profiles_str() + if selected_profiles_str in self.temp_changes: + for key, new_value in self.temp_changes[selected_profiles_str].items(): + self.update_core_profiles(key, new_value) + + self.temp_changes.pop(selected_profiles_str, None) + self.original_values.pop(selected_profiles_str, None) + + def update_core_profiles(self, key, new_value): + profile = ProfileController.get( + int(self.update_status.current_profile_id)) + self.update_res = False + if key == 'dimentions': + profile.resolution = new_value + self.update_res = True + + elif key == 'name': + profile.name = new_value + + elif key == 'connection': + if new_value == 'tor': + profile.connection.code = new_value + profile.connection.masked = True + elif new_value == 'just proxy': + profile.connection.code = 'system' + profile.connection.masked = True + else: + self.update_status.update_status( + 'System wide profiles not supported atm') + + elif key == 'browser': + browser_type, browser_version = new_value.split(':', 1) + profile.application_version.application_code = browser_type + profile.application_version.version_number = browser_version + + elif key == 'protocol': + if self.data_profile.get('connection') == 'system-wide': + self.edit_to_session() + else: + profile.connection.code = 'wireguard' if new_value == 'wireguard' else 'tor' + profile.connection.masked = False if new_value == 'wireguard' else True + + else: + location = self.connection_manager.get_location_info(new_value) + + if location: + profile.location.code = location.code + profile.location.country_code = location.country_code + profile.location.time_zone = location.time_zone + profile.subscription = None + + ProfileController.update(profile) + + def edit_to_session(self): + id = int(self.update_status.current_profile_id) + profile = self.data_profile + default_app = 'firefox:123.0' + default_resolution = '1024x760' + location_code = self.connection_manager.get_location_info( + profile.get('location')) + + connection_type = 'tor' + + profile_data = { + 'id': int(id), + 'name': profile.get('name'), + 'country_code': location_code.country_code, + 'code': location_code.code, + 'application': default_app, + 'connection_type': connection_type, + 'resolution': default_resolution, + } + resume_page = self.find_resume_page() + if resume_page: + resume_page.handle_core_action_create_profile( + 'CREATE_SESSION_PROFILE', profile_data, 'session') diff --git a/gui/v2/ui/pages/fast_mode_prompt_page.py b/gui/v2/ui/pages/fast_mode_prompt_page.py new file mode 100755 index 0000000..cdb250e --- /dev/null +++ b/gui/v2/ui/pages/fast_mode_prompt_page.py @@ -0,0 +1,105 @@ +import random + +from PyQt6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget +from PyQt6 import QtCore + +from core.controllers.ProfileController import ProfileController + +from gui.v2.ui.pages.Page import Page + + +class FastModePromptPage(Page): + def __init__(self, page_stack, main_window): + super().__init__("FastModePrompt", page_stack, main_window) + self.page_stack = page_stack + self.update_status = main_window + self.title.setGeometry(500, 40, 350, 40) + self.title.setText("Quick Setup Option") + self.button_back.setVisible(False) + self.button_apply.setVisible(False) + + container = QWidget(self) + container.setGeometry(QtCore.QRect(80, 100, 640, 360)) + v = QVBoxLayout(container) + v.setContentsMargins(0, 0, 0, 0) + v.setSpacing(20) + + large_text = QLabel( + "Would you like to switch to convenient \"fast mode\" for profile creation going forward? (recommended)") + large_text.setWordWrap(True) + large_text.setStyleSheet("color: white; font-size: 18px;") + large_text.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + v.addWidget(large_text) + + buttons_row = QWidget() + h = QHBoxLayout(buttons_row) + h.setContentsMargins(0, 0, 0, 0) + h.setSpacing(20) + + yes_btn = QPushButton("Yes Fast Mode") + yes_btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + yes_btn.setFixedSize(240, 56) + yes_btn.setStyleSheet( + "background-color: #27ae60; color: white; font-weight: bold; font-size: 16px; border: none; border-radius: 6px;") + yes_btn.clicked.connect(self.choose_yes) + + no_btn = QPushButton("No Keep This") + no_btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + no_btn.setFixedSize(160, 42) + no_btn.setStyleSheet( + "background-color: #c0392b; color: white; font-size: 14px; border: none; border-radius: 6px;") + no_btn.clicked.connect(self.choose_no) + + h.addStretch() + h.addWidget(yes_btn) + h.addWidget(no_btn) + h.addStretch() + v.addWidget(buttons_row) + + small_text = QLabel( + "You can toggle this anytime in the \"Options\" Menu, under \"Create/Edit\"") + small_text.setWordWrap(True) + small_text.setStyleSheet("color: #999999; font-size: 12px;") + small_text.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + v.addWidget(small_text) + + def finalize(self): + self.update_status.mark_fast_mode_prompt_shown() + menu_page = self.custom_window.navigator.get_cached("menu") + if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): + menu_page.refresh_menu_buttons() + self.custom_window.navigator.navigate("menu") + + def choose_yes(self): + self.update_status.set_fast_mode_enabled(True) + self.finalize() + + def choose_no(self): + self.update_status.set_fast_mode_enabled(False) + self.finalize() + + def initialize_default_selections(self): + if not self.selected_values['location']: + locations = self.connection_manager.get_location_list() + if locations: + random_index = random.randint(0, len(locations) - 1) + self.selected_values['location'] = locations[random_index] + + if not self.selected_values['browser']: + browsers = self.connection_manager.get_browser_list() + if browsers: + random_index = random.randint(0, len(browsers) - 1) + self.selected_values['browser'] = browsers[random_index] + + def get_next_available_profile_id(self) -> int: + profiles = ProfileController.get_all() + if not profiles: + return 1 + + existing_ids = sorted(profiles.keys()) + + for i in range(1, max(existing_ids) + 2): + if i not in existing_ids: + return i + + return 1 diff --git a/gui/v2/ui/pages/fast_registration_page.py b/gui/v2/ui/pages/fast_registration_page.py new file mode 100755 index 0000000..c06234b --- /dev/null +++ b/gui/v2/ui/pages/fast_registration_page.py @@ -0,0 +1,787 @@ +import os +import random + +from PyQt6.QtWidgets import ( + QDialog, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QVBoxLayout +) +from PyQt6.QtGui import QIcon, QPixmap, QTransform +from PyQt6.QtCore import QSize +from PyQt6 import QtCore + +from core.controllers.ProfileController import ProfileController + +from gui.v2.actions.profile_order import append_profile_to_visual_order +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.worker_thread import WorkerThread + + +class FastRegistrationPage(Page): + def __init__(self, page_stack, main_window): + super().__init__("FastRegistration", page_stack, main_window) + self.page_stack = page_stack + self.update_status = main_window + self.connection_manager = main_window.connection_manager + self.labels = [] + self.buttons = [] + self.title.setGeometry(550, 40, 250, 40) + self.title.setText("Fast Creation") + + self.button_apply.setVisible(True) + self.button_apply.clicked.connect(self.create_profile) + + self.button_back.setVisible(True) + try: + self.button_back.clicked.disconnect() + except TypeError: + pass + self.button_back.clicked.connect(self.go_back) + + self.name_handle = QLabel(self) + self.name_handle.setGeometry(110, 70, 400, 30) + self.name_handle.setStyleSheet('color: #cacbcb;') + self.name_handle.setText("Profile Name:") + + self.name_hint = QLabel(self) + self.name_hint.setGeometry(265, 100, 190, 20) + self.name_hint.setStyleSheet( + 'color: #888888; font-size: 10px; font-style: italic;') + self.name_hint.setText("Click here to edit profile name") + self.name_hint.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.name = QLineEdit(self) + self.name.setPlaceholderText("Enter name") + self.name.setGeometry(265, 70, 190, 30) + self.name.setStyleSheet( + "color: cyan; border: 1px solid #666666; border-radius: 3px; background-color: rgba(0, 0, 0, 0.3);") + self.name.setCursor(QtCore.Qt.CursorShape.IBeamCursor) + self.name.focusInEvent = lambda event: self.name_hint.hide() + self.name.focusOutEvent = lambda event: self.name_hint.show( + ) if not self.name.text() else self.name_hint.hide() + + self.name.textChanged.connect( + lambda text: self.name_hint.hide() if text else self.name_hint.show()) + + self.profile_data = {} + self.selected_values = { + 'protocol': 'wireguard', + 'connection': 'browser-only', + 'location': '', + 'browser': '', + 'resolution': '1024x760' + } + self.res_index = 1 + + self.initialize_default_selections() + + def initialize_default_selections(self): + if not self.selected_values['location']: + locations = self.connection_manager.get_location_list() + if locations: + random_index = random.randint(0, len(locations) - 1) + self.selected_values['location'] = locations[random_index] + + if not self.selected_values['browser']: + browsers = self.connection_manager.get_browser_list() + if browsers: + random_index = random.randint(0, len(browsers) - 1) + self.selected_values['browser'] = browsers[random_index] + + def get_next_available_profile_id(self, profiles=None) -> int: + if profiles is None: + profiles = ProfileController.get_all() + if not profiles: + return 1 + + existing_ids = sorted(profiles.keys()) + + for i in range(1, max(existing_ids) + 2): + if i not in existing_ids: + return i + + return 1 + + def showEvent(self, event): + super().showEvent(event) + self.initialize_default_selections() + self.create_interface_elements() + + def create_interface_elements(self): + for label in self.labels: + label.deleteLater() + self.labels = [] + for button in self.buttons: + button.deleteLater() + self.buttons = [] + + if not self.name.text(): + self.name_hint.show() + else: + self.name_hint.hide() + + self.host_info_label = QLabel( + f"Host Screen: {self.update_status.host_screen_width}x{self.update_status.host_screen_height}", self) + self.host_info_label.setGeometry(415, 470, 250, 20) + self.host_info_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.host_info_label.setStyleSheet( + "color: #00ffff; font-size: 13px; font-weight: bold;") + self.host_info_label.show() + self.labels.append(self.host_info_label) + + self.create_protocol_section() + self.create_connection_section() + self.create_location_section() + if self.selected_values['connection'] != 'system-wide': + self.create_browser_section() + self.create_resolution_section() + self.update_ui_state_for_connection() + + def create_protocol_section(self): + label = QLabel("Protocol", self) + label.setGeometry(300, 150, 185, 75) + + protocol_image = QPixmap(os.path.join( + self.btn_path, f"{self.selected_values['protocol']}_button.png")) + label.setPixmap(protocol_image) + label.setScaledContents(True) + label.show() + self.labels.append(label) + + prev_button = QPushButton(self) + prev_button.setGeometry(265, 150, 30, 75) + prev_button.clicked.connect( + lambda: self.show_previous_value('protocol')) + prev_button.show() + icon_path = os.path.join(self.btn_path, "UP_button.png") + icon = QPixmap(icon_path) + transform = QTransform().rotate(180) + rotated_icon = icon.transformed(transform) + prev_button.setIcon(QIcon(rotated_icon)) + prev_button.setIconSize(prev_button.size()) + self.buttons.append(prev_button) + + next_button = QPushButton(self) + next_button.setGeometry(490, 150, 30, 75) + next_button.clicked.connect(lambda: self.show_next_value('protocol')) + next_button.show() + next_button.setIcon( + QIcon(os.path.join(self.btn_path, "UP_button.png"))) + next_button.setIconSize(next_button.size()) + self.buttons.append(next_button) + + def create_connection_section(self): + label = QLabel("Connection", self) + label.setGeometry(150, 250, 185, 75) + + if self.selected_values['connection']: + connection_image = QPixmap(os.path.join( + self.btn_path, f"{self.selected_values['connection']}_button.png")) + if connection_image.isNull(): + fallback_path = os.path.join( + self.btn_path, "browser-only_button.png") + connection_image = QPixmap(fallback_path) + else: + connection_image = QPixmap(os.path.join( + self.btn_path, "browser-only_button.png")) + + label.setPixmap(connection_image) + label.setScaledContents(True) + label.show() + self.labels.append(label) + + prev_button = QPushButton(self) + prev_button.setGeometry(115, 250, 30, 75) + prev_button.clicked.connect( + lambda: self.show_previous_value('connection')) + prev_button.show() + icon_path = os.path.join(self.btn_path, "UP_button.png") + icon = QPixmap(icon_path) + transform = QTransform().rotate(180) + rotated_icon = icon.transformed(transform) + prev_button.setIcon(QIcon(rotated_icon)) + prev_button.setIconSize(prev_button.size()) + self.buttons.append(prev_button) + + next_button = QPushButton(self) + next_button.setGeometry(340, 250, 30, 75) + next_button.clicked.connect(lambda: self.show_next_value('connection')) + next_button.show() + next_button.setIcon( + QIcon(os.path.join(self.btn_path, "UP_button.png"))) + next_button.setIconSize(next_button.size()) + self.buttons.append(next_button) + + def create_location_section(self): + info_label = QLabel("Click on the location for more info", self) + info_label.setGeometry(480, 80, 300, 20) + info_label.setStyleSheet( + "color: #888888; font-size: 14px; font-style: italic;") + info_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + info_label.show() + self.labels.append(info_label) + + arrow_label = QLabel(self) + arrow_label.setGeometry(540, 100, 150, 150) + arrow_pixmap = QPixmap(os.path.join(self.btn_path, "arrow.png")) + transform = QTransform().rotate(270) + rotated_arrow = arrow_pixmap.transformed(transform) + arrow_label.setPixmap(rotated_arrow) + arrow_label.setScaledContents(True) + arrow_label.show() + self.labels.append(arrow_label) + + label = QPushButton(self) + label.setGeometry(435, 250, 185, 75) + label.setFlat(True) + label.setStyleSheet("background: transparent; border: none;") + + if self.selected_values['location']: + image_path = os.path.join( + self.btn_path, f"button_{self.selected_values['location']}.png") + location_image = QPixmap(image_path) + locations = self.connection_manager.get_location_info( + self.selected_values['location']) + provider = locations.operator.name if locations and hasattr( + locations, 'operator') else None + fallback_path = os.path.join( + self.btn_path, "default_location_button.png") + + if location_image.isNull(): + if locations and hasattr(locations, 'country_name'): + location_name = locations.country_name + location_image = LocationPage.create_location_button_image( + location_name, fallback_path, provider) + else: + location_image = LocationPage.create_location_button_image( + self.selected_values['location'], fallback_path, provider) + else: + if locations and hasattr(locations, 'country_name'): + location_image = LocationPage.create_location_button_image( + f'{locations.country_code}_{locations.code}', fallback_path, provider, image_path) + else: + location_image = LocationPage.create_location_button_image( + self.selected_values['location'], fallback_path, provider, image_path) + else: + location_image = QPixmap(os.path.join( + self.btn_path, "default_location_button.png")) + + label.setIcon(QIcon(location_image)) + label.setIconSize(QSize(185, 75)) + if self.selected_values['location']: + label.location_icon_name = self.selected_values['location'] + label.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + label.clicked.connect( + lambda checked, loc=self.selected_values['location']: self.show_location_verification(loc)) + + locations = self.connection_manager.get_location_info( + self.selected_values['location']) + if self.selected_values['protocol'] == 'hidetor' and locations and not (hasattr(locations, 'is_proxy_capable') and locations.is_proxy_capable): + label.hide() + else: + label.show() + self.labels.append(label) + + prev_button = QPushButton(self) + prev_button.setGeometry(400, 250, 30, 75) + prev_button.clicked.connect( + lambda: self.show_previous_value('location')) + prev_button.show() + icon_path = os.path.join(self.btn_path, "UP_button.png") + icon = QPixmap(icon_path) + transform = QTransform().rotate(180) + rotated_icon = icon.transformed(transform) + prev_button.setIcon(QIcon(rotated_icon)) + prev_button.setIconSize(prev_button.size()) + self.buttons.append(prev_button) + + next_button = QPushButton(self) + next_button.setGeometry(625, 250, 30, 75) + next_button.clicked.connect(lambda: self.show_next_value('location')) + next_button.show() + next_button.setIcon( + QIcon(os.path.join(self.btn_path, "UP_button.png"))) + next_button.setIconSize(next_button.size()) + self.buttons.append(next_button) + + def create_browser_section(self): + label = QLabel("Browser", self) + label.setGeometry(150, 350, 185, 75) + + if self.selected_values['browser']: + browser_image = BrowserPage.create_browser_button_image( + self.selected_values['browser'], self.btn_path) + if browser_image.isNull(): + fallback_path = os.path.join( + self.btn_path, "default_browser_button.png") + browser_image = BrowserPage.create_browser_button_image( + self.selected_values['browser'], fallback_path, True) + else: + browser_image = QPixmap() + + label.setPixmap(browser_image) + label.setScaledContents(True) + label.show() + self.labels.append(label) + + prev_button = QPushButton(self) + prev_button.setGeometry(115, 350, 30, 75) + prev_button.clicked.connect( + lambda: self.show_previous_value('browser')) + prev_button.show() + icon_path = os.path.join(self.btn_path, "UP_button.png") + icon = QPixmap(icon_path) + transform = QTransform().rotate(180) + rotated_icon = icon.transformed(transform) + prev_button.setIcon(QIcon(rotated_icon)) + prev_button.setIconSize(prev_button.size()) + self.buttons.append(prev_button) + + next_button = QPushButton(self) + next_button.setGeometry(340, 350, 30, 75) + next_button.clicked.connect(lambda: self.show_next_value('browser')) + next_button.show() + next_button.setIcon( + QIcon(os.path.join(self.btn_path, "UP_button.png"))) + next_button.setIconSize(next_button.size()) + self.buttons.append(next_button) + + def create_resolution_section(self): + from gui.v2.ui.pages.screen_page import ScreenPage + label = QLabel("Resolution", self) + label.setGeometry(435, 350, 185, 75) + screen_page = self.custom_window.navigator.get_cached("screen") + if screen_page is None: + self.custom_window.navigator.navigate("screen") + screen_page = self.custom_window.navigator.get_cached("screen") + self.custom_window.navigator.navigate("fast_registration") + if self.selected_values['resolution']: + resolution_image = screen_page.create_resolution_button_image( + self.selected_values['resolution']) + else: + resolution_image = screen_page.create_resolution_button_image( + "1024x760") + + label.setPixmap(resolution_image) + label.setScaledContents(True) + label.show() + self.labels.append(label) + + prev_button = QPushButton(self) + prev_button.setGeometry(400, 350, 30, 75) + prev_button.clicked.connect( + lambda: self.show_previous_value('resolution')) + prev_button.show() + icon_path = os.path.join(self.btn_path, "UP_button.png") + icon = QPixmap(icon_path) + transform = QTransform().rotate(180) + rotated_icon = icon.transformed(transform) + prev_button.setIcon(QIcon(rotated_icon)) + prev_button.setIconSize(prev_button.size()) + self.buttons.append(prev_button) + + next_button = QPushButton(self) + next_button.setGeometry(625, 350, 30, 75) + next_button.clicked.connect(lambda: self.show_next_value('resolution')) + next_button.show() + next_button.setIcon( + QIcon(os.path.join(self.btn_path, "UP_button.png"))) + next_button.setIconSize(next_button.size()) + self.buttons.append(next_button) + + custom_button = QPushButton("Custom", self) + custom_button.setGeometry(435, 430, 185, 30) + custom_button.setStyleSheet(""" + QPushButton { + background-color: rgba(0, 255, 255, 0.1); + color: #00ffff; + border: 1px solid #00ffff; + border-radius: 5px; + font-size: 12px; + font-weight: bold; + } + QPushButton:hover { + background-color: rgba(0, 255, 255, 0.2); + } + """) + custom_button.clicked.connect(self.show_custom_res_dialog) + custom_button.show() + self.buttons.append(custom_button) + + def show_custom_res_dialog(self): + dialog = QDialog(self) + dialog.setWindowTitle("Custom Resolution") + dialog.setFixedSize(300, 150) + dialog.setModal(True) + dialog.setStyleSheet(""" + QDialog { + background-color: #2c3e50; + border: 2px solid #00ffff; + border-radius: 10px; + } + QLabel { + color: white; + font-size: 12px; + } + QLineEdit { + background-color: #34495e; + color: white; + border: 1px solid #00ffff; + border-radius: 5px; + padding: 5px; + font-size: 12px; + } + QPushButton { + background-color: #2c3e50; + color: white; + border: 2px solid #00ffff; + border-radius: 5px; + padding: 8px; + font-size: 12px; + } + QPushButton:hover { + background-color: #34495e; + } + """) + + layout = QVBoxLayout() + input_layout = QHBoxLayout() + width_label = QLabel("Width:") + width_input = QLineEdit() + width_input.setPlaceholderText("1920") + height_label = QLabel("Height:") + height_input = QLineEdit() + height_input.setPlaceholderText("1080") + + input_layout.addWidget(width_label) + input_layout.addWidget(width_input) + input_layout.addWidget(height_label) + input_layout.addWidget(height_input) + + button_layout = QHBoxLayout() + ok_button = QPushButton("OK") + cancel_button = QPushButton("Cancel") + button_layout.addWidget(cancel_button) + button_layout.addWidget(ok_button) + + layout.addLayout(input_layout) + layout.addLayout(button_layout) + dialog.setLayout(layout) + + def validate_and_accept(): + try: + width = int(width_input.text()) + height = int(height_input.text()) + if width <= 0 or height <= 0: + return + host_w = self.update_status.host_screen_width + host_h = self.update_status.host_screen_height + adjusted = False + if width > host_w: + width = host_w - 50 + adjusted = True + if height > host_h: + height = host_h - 50 + adjusted = True + + if adjusted: + QMessageBox.information( + dialog, "Notice", "Adjusted to fit host size") + self.selected_values['resolution'] = f"{width}x{height}" + self.create_interface_elements() + dialog.accept() + except ValueError: + pass + + ok_button.clicked.connect(validate_and_accept) + cancel_button.clicked.connect(dialog.reject) + + width_input.returnPressed.connect(validate_and_accept) + height_input.returnPressed.connect(validate_and_accept) + + dialog.exec() + + def update_ui_state_for_connection(self): + is_system_wide = self.selected_values['connection'] == 'system-wide' + + for button in self.buttons: + if hasattr(button, 'geometry'): + button_geometry = button.geometry() + if button_geometry.y() == 350: + if button_geometry.x() in [115, 340, 400, 625]: + button.setEnabled(not is_system_wide) + + def show_previous_value(self, key): + if key == 'protocol': + protocols = ['wireguard', 'hidetor'] + current_index = protocols.index(self.selected_values[key]) + previous_index = (current_index - 1) % len(protocols) + self.selected_values[key] = protocols[previous_index] + if self.selected_values[key] == 'wireguard': + self.selected_values['connection'] = 'browser-only' + else: + self.selected_values['connection'] = 'tor' + loc_info = self.connection_manager.get_location_info( + self.selected_values['location']) + if not (loc_info and hasattr(loc_info, 'is_proxy_capable') and loc_info.is_proxy_capable): + locations = self.connection_manager.get_location_list() + proxy_locations = [loc for loc in locations if (l := self.connection_manager.get_location_info( + loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable] + if proxy_locations: + self.selected_values['location'] = proxy_locations[0] + elif key == 'connection': + if self.selected_values['protocol'] == 'wireguard': + connections = ['browser-only', 'system-wide'] + else: + connections = ['tor', 'just proxy'] + current_index = connections.index(self.selected_values[key]) + previous_index = (current_index - 1) % len(connections) + self.selected_values[key] = connections[previous_index] + self.update_ui_state_for_connection() + elif key == 'location': + locations = self.connection_manager.get_location_list() + if self.selected_values['protocol'] == 'hidetor': + locations = [loc for loc in locations if (l := self.connection_manager.get_location_info( + loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable] + + if locations and self.selected_values[key] in locations: + current_index = locations.index(self.selected_values[key]) + previous_index = (current_index - 1) % len(locations) + self.selected_values[key] = locations[previous_index] + elif locations: + self.selected_values[key] = locations[0] + elif key == 'browser': + browsers = self.connection_manager.get_browser_list() + if browsers and self.selected_values[key] in browsers: + current_index = browsers.index(self.selected_values[key]) + previous_index = (current_index - 1) % len(browsers) + self.selected_values[key] = browsers[previous_index] + elif browsers: + self.selected_values[key] = browsers[0] + elif key == 'resolution': + config = self.update_status._load_gui_config() + dynamic_enabled = config.get("registrations", {}).get( + "dynamic_larger_profiles", False) if config else False + + if dynamic_enabled: + resolutions = [] + host_w = self.update_status.host_screen_width + host_h = self.update_status.host_screen_height + for i in range(5): + resolutions.append( + f"{host_w - (50 * (i + 1))}x{host_h - (50 * (i + 1))}") + self.res_index = (self.res_index - 1) % len(resolutions) + self.selected_values[key] = resolutions[self.res_index] + else: + resolutions = ['800x600', '1024x760', '1152x1080', '1280x1024', '1920x1080', + '2560x1440', '2560x1600', '1920x1440', '1792x1344', '2048x1152'] + self.res_index = (self.res_index - 1) % len(resolutions) + choice = resolutions[self.res_index] + w, h = map(int, choice.split('x')) + host_w = self.update_status.host_screen_width + host_h = self.update_status.host_screen_height + new_w, new_h = w, h + if w > host_w: + new_w = host_w - 50 + if h > host_h: + new_h = host_h - 50 + self.selected_values[key] = f"{new_w}x{new_h}" + + self.create_interface_elements() + + def show_next_value(self, key): + if key == 'protocol': + protocols = ['wireguard', 'hidetor'] + current_index = protocols.index(self.selected_values[key]) + next_index = (current_index + 1) % len(protocols) + self.selected_values[key] = protocols[next_index] + if self.selected_values[key] == 'wireguard': + self.selected_values['connection'] = 'browser-only' + else: + self.selected_values['connection'] = 'tor' + loc_info = self.connection_manager.get_location_info( + self.selected_values['location']) + if not (loc_info and hasattr(loc_info, 'is_proxy_capable') and loc_info.is_proxy_capable): + locations = self.connection_manager.get_location_list() + proxy_locations = [loc for loc in locations if (l := self.connection_manager.get_location_info( + loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable] + if proxy_locations: + self.selected_values['location'] = proxy_locations[0] + elif key == 'connection': + if self.selected_values['protocol'] == 'wireguard': + connections = ['browser-only', 'system-wide'] + else: + connections = ['tor', 'just proxy'] + current_index = connections.index(self.selected_values[key]) + next_index = (current_index + 1) % len(connections) + self.selected_values[key] = connections[next_index] + self.update_ui_state_for_connection() + elif key == 'location': + locations = self.connection_manager.get_location_list() + if self.selected_values['protocol'] == 'hidetor': + locations = [loc for loc in locations if (l := self.connection_manager.get_location_info( + loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable] + + if locations and self.selected_values[key] in locations: + current_index = locations.index(self.selected_values[key]) + next_index = (current_index + 1) % len(locations) + self.selected_values[key] = locations[next_index] + elif locations: + self.selected_values[key] = locations[0] + elif key == 'browser': + browsers = self.connection_manager.get_browser_list() + if browsers and self.selected_values[key] in browsers: + current_index = browsers.index(self.selected_values[key]) + next_index = (current_index + 1) % len(browsers) + self.selected_values[key] = browsers[next_index] + elif browsers: + self.selected_values[key] = browsers[0] + elif key == 'resolution': + config = self.update_status._load_gui_config() + dynamic_enabled = config.get("registrations", {}).get( + "dynamic_larger_profiles", False) if config else False + + if dynamic_enabled: + resolutions = [] + host_w = self.update_status.host_screen_width + host_h = self.update_status.host_screen_height + for i in range(5): + resolutions.append( + f"{host_w - (50 * (i + 1))}x{host_h - (50 * (i + 1))}") + self.res_index = (self.res_index + 1) % len(resolutions) + self.selected_values[key] = resolutions[self.res_index] + else: + resolutions = ['800x600', '1024x760', '1152x1080', '1280x1024', '1920x1080', + '2560x1440', '2560x1600', '1920x1440', '1792x1344', '2048x1152'] + self.res_index = (self.res_index + 1) % len(resolutions) + choice = resolutions[self.res_index] + w, h = map(int, choice.split('x')) + host_w = self.update_status.host_screen_width + host_h = self.update_status.host_screen_height + new_w, new_h = w, h + if w > host_w: + new_w = host_w - 50 + if h > host_h: + new_h = host_h - 50 + self.selected_values[key] = f"{new_w}x{new_h}" + + self.create_interface_elements() + + def go_back(self): + menu_page = self.custom_window.navigator.get_cached("menu") + if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): + menu_page.refresh_menu_buttons() + self.custom_window.navigator.navigate("menu") + + def show_location_verification(self, location_icon_name): + verification_page = LocationVerificationPage( + self.page_stack, self.custom_window, location_icon_name, self) + self.page_stack.addWidget(verification_page) + self.page_stack.setCurrentIndex( + self.page_stack.indexOf(verification_page)) + + def create_profile(self): + profile_name = self.name.text() + if not profile_name: + self.update_status.update_status('Please enter a profile name') + return + + if not self.selected_values['location']: + self.update_status.update_status('Please select a location') + return + + if self.selected_values['connection'] != 'system-wide' and not self.selected_values['browser']: + self.update_status.update_status('Please select a browser') + return + + profile_data = { + 'name': profile_name, + 'protocol': self.selected_values['protocol'], + 'connection': self.selected_values['connection'], + 'location': self.selected_values['location'], + 'browser': self.selected_values['browser'], + 'resolution': self.selected_values['resolution'] + } + + self.profile_data = profile_data + self.update_status.write_data(profile_data) + + if self.selected_values['protocol'] == 'wireguard': + self.create_wireguard_profile(profile_data) + else: + self.create_tor_profile(profile_data) + + self.go_back() + + def _spawn_create_profile_worker(self, action, profile_data, profile_type): + self.worker_thread = WorkerThread(action, profile_data, profile_type) + self.worker_thread.text_output.connect( + self.update_status.update_status) + self.worker_thread.start() + self.worker_thread.wait() + + def create_wireguard_profile(self, profile_data): + location_info = self.connection_manager.get_location_info( + profile_data['location']) + if not location_info: + self.update_status.update_status('Invalid location selected') + return + + profiles = ProfileController.get_all() + profile_id = self.get_next_available_profile_id(profiles) + profile_data_for_resume = { + 'id': profile_id, + 'name': profile_data['name'], + 'country_code': location_info.country_code, + 'code': location_info.code, + 'application': profile_data['browser'], + 'connection_type': 'wireguard', + 'resolution': profile_data['resolution'], + } + + if profile_data['connection'] == 'system-wide': + self._spawn_create_profile_worker( + 'CREATE_SYSTEM_PROFILE', profile_data_for_resume, 'system') + else: + self._spawn_create_profile_worker( + 'CREATE_SESSION_PROFILE', profile_data_for_resume, 'session') + + if ProfileController.get(profile_id) is not None: + append_profile_to_visual_order( + getattr(self.update_status, 'gui_config_file', None), + profile_id, + profiles.keys()) + + def create_tor_profile(self, profile_data): + location_info = self.connection_manager.get_location_info( + profile_data['location']) + if not location_info: + self.update_status.update_status('Invalid location selected') + return + + connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system' + + profiles = ProfileController.get_all() + profile_id = self.get_next_available_profile_id(profiles) + profile_data_for_resume = { + 'id': profile_id, + 'name': profile_data['name'], + 'country_code': location_info.country_code, + 'code': location_info.code, + 'application': profile_data['browser'], + 'connection_type': connection_type, + 'resolution': profile_data['resolution'], + } + + self._spawn_create_profile_worker( + 'CREATE_SESSION_PROFILE', profile_data_for_resume, 'session') + + if ProfileController.get(profile_id) is not None: + append_profile_to_visual_order( + getattr(self.update_status, 'gui_config_file', None), + profile_id, + profiles.keys()) + + def find_resume_page(self): + return self.custom_window.navigator.get_cached("resume") diff --git a/gui/v2/ui/pages/hidetor_page.py b/gui/v2/ui/pages/hidetor_page.py new file mode 100755 index 0000000..a17d822 --- /dev/null +++ b/gui/v2/ui/pages/hidetor_page.py @@ -0,0 +1,105 @@ +import os + +from PyQt6.QtWidgets import QPushButton, QButtonGroup +from PyQt6.QtGui import QPixmap, QIcon +from PyQt6.QtCore import Qt +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.pages.location_page import LocationPage + + +class HidetorPage(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("HideTor", page_stack, main_window, parent) + self.selected_location_icon = None + self.connection_manager = main_window.connection_manager + self.update_status = main_window + self.button_next.clicked.connect(self.go_selected) + self.button_reverse.setVisible(True) + self.button_reverse.clicked.connect(self.reverse) + self.display.setGeometry(QtCore.QRect(5, 10, 390, 520)) + self.title.setGeometry(395, 40, 380, 40) + self.title.setText("Pick a location") + + if self.connection_manager.is_synced(): + from gui.v2.actions.sync import generate_grid_positions + locs = self.connection_manager.get_location_list() + positions = generate_grid_positions(len(locs)) + available = [(QPushButton, loc, positions[i]) for i, loc in enumerate(locs)] + self.create_interface_elements(available) + + def create_interface_elements(self, available_locations): + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + + for j, (object_type, icon_name, geometry) in enumerate(available_locations): + boton = object_type(self) + boton.setGeometry(*geometry) + boton.setIconSize(boton.size()) + boton.setCheckable(True) + locations = self.connection_manager.get_location_info(icon_name) + if icon_name == 'my_14': + boton.setVisible(False) + if locations and not (hasattr(locations, 'is_proxy_capable') and locations.is_proxy_capable): + boton.setVisible(False) + icon_path = os.path.join(self.btn_path, f"button_{icon_name}.png") + boton.setIcon(QIcon(icon_path)) + fallback_path = os.path.join( + self.btn_path, "default_location_button.png") + provider = locations.operator.name if locations and hasattr( + locations, 'operator') else None + if boton.icon().isNull(): + if locations and hasattr(locations, 'country_name'): + location_name = locations.country_name + base_image = LocationPage.create_location_button_image( + location_name, fallback_path, provider) + boton.setIcon(QIcon(base_image)) + else: + base_image = LocationPage.create_location_button_image( + '', fallback_path, provider) + boton.setIcon(QIcon(base_image)) + else: + if locations and hasattr(locations, 'country_name'): + base_image = LocationPage.create_location_button_image( + f'{locations.country_code}_{locations.code}', fallback_path, provider, icon_path) + boton.setIcon(QIcon(base_image)) + else: + base_image = LocationPage.create_location_button_image( + '', fallback_path, provider, icon_path) + boton.setIcon(QIcon(base_image)) + self.buttons.append(boton) + self.buttonGroup.addButton(boton, j) + boton.location_icon_name = icon_name + boton.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + boton.clicked.connect( + lambda checked, loc=icon_name: self.show_location(loc)) + + def update_swarp_json(self): + inserted_data = { + "location": self.selected_location_icon, + "connection": "tor" + } + self.update_status.write_data(inserted_data) + + def show_location(self, location): + tor_hide_img = f'hdtor_{location}' + self.display.setPixmap(QPixmap(os.path.join(self.btn_path, f"{tor_hide_img}.png")).scaled( + self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + self.selected_location_icon = location + self.button_next.setVisible(True) + self.update_swarp_json() + + def reverse(self): + self.custom_window.navigator.navigate("protocol") + + def go_selected(self): + self.custom_window.navigator.navigate("browser") + + def show_location_verification(self, location_icon_name): + from gui.v2.ui.pages.location_verification_page import LocationVerificationPage + verification_page = LocationVerificationPage( + self.page_stack, self.update_status, location_icon_name, self) + self.page_stack.addWidget(verification_page) + self.page_stack.setCurrentIndex( + self.page_stack.indexOf(verification_page)) diff --git a/gui/v2/ui/pages/id_page.py b/gui/v2/ui/pages/id_page.py new file mode 100755 index 0000000..bb44443 --- /dev/null +++ b/gui/v2/ui/pages/id_page.py @@ -0,0 +1,190 @@ +import os +import re + +from PyQt6.QtWidgets import ( + QButtonGroup, QFrame, QLabel, QPushButton, QTextEdit, +) +from PyQt6.QtGui import QIcon, QPixmap +from PyQt6.QtCore import Qt +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +HOW_MANY_PROFILES_DEFAULT = 6 + + +class IdPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Id", page_stack, main_window, parent) + self.update_status = main_window + self.btn_path = main_window.btn_path + self.buttonGroup = QButtonGroup(self) + self.display.setGeometry(QtCore.QRect(-10, 50, 550, 460)) + self.create_interface_elements() + + self.connect_button = QPushButton(self) + self.connect_button.setObjectName("connect_button") + self.connect_button.setGeometry(625, 450, 90, 50) + self.connect_button.setIconSize(self.connect_button.size()) + tor_icon = QIcon(os.path.join(self.btn_path, "connect.png")) + self.connect_button.setIcon(tor_icon) + self.connect_button.clicked.connect(self.on_connect) + self.connect_button.setDisabled(True) + + self.button_reverse.setVisible(True) + + def on_connect(self): + text = self.text_edit.toPlainText() + pattern = r'^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$' + if re.match(pattern, text): + self.update_status.update_status('Enabling profile in progress...') + profile_data = { + 'id': int(self.update_status.current_profile_id), + 'billing_code': str(text) + } + menu_page = self.find_menu_page() + if menu_page: + menu_page.enabling_profile(profile_data) + + else: + self.update_status.update_status( + 'Incorrect format for the billing code') + + def find_menu_page(self): + return self.custom_window.navigator.get_cached("menu") + + def create_interface_elements(self): + self.object_selected = None + self.display.setGeometry(QtCore.QRect(0, 0, 0, 0)) + + self.button_reverse.clicked.connect(self.reverse) + self.button_go.clicked.connect(self.go_selected) + + column_title_style = "color: #00ffff; font-size: 18px; font-weight: bold;" + column_button_style = """ + QPushButton { + background-color: #000000; + color: #00ffff; + border: 2px solid #00ffff; + border-radius: 10px; + font-size: 15px; + font-weight: bold; + padding: 10px; + } + QPushButton:hover { background-color: #003333; } + """ + column_desc_style = "color: #888888; font-size: 12px; font-style: italic;" + + self.single_title = QLabel("Single Profile", self) + self.single_title.setGeometry(15, 50, 240, 40) + self.single_title.setStyleSheet(column_title_style) + self.single_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.single_button = QPushButton("One Profile Only", self) + self.single_button.setGeometry(40, 220, 200, 100) + self.single_button.setStyleSheet(column_button_style) + self.single_button.clicked.connect(self.go_single_profile) + + self.single_desc = QLabel("Buy a single billing for one profile.", self) + self.single_desc.setGeometry(15, 335, 240, 40) + self.single_desc.setStyleSheet(column_desc_style) + self.single_desc.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.single_desc.setWordWrap(True) + + self.multiple_title = QLabel("Multiple Profiles", self) + self.multiple_title.setGeometry(285, 50, 240, 40) + self.multiple_title.setStyleSheet(column_title_style) + self.multiple_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.multiple_button = QPushButton("Multiple Profiles\nat Once", self) + self.multiple_button.setGeometry(310, 220, 200, 100) + self.multiple_button.setStyleSheet(column_button_style) + self.multiple_button.clicked.connect(self.go_multiple_profiles) + + self.multiple_desc = QLabel( + f"Buy a bundle of {HOW_MANY_PROFILES_DEFAULT} tickets at once, which are cryptographically seperated from each other.", self) + self.multiple_desc.setGeometry(285, 335, 240, 80) + self.multiple_desc.setStyleSheet(column_desc_style) + self.multiple_desc.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.multiple_desc.setWordWrap(True) + + self.title = QLabel("Use Existing", self) + self.title.setGeometry(QtCore.QRect(555, 50, 230, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.title.setStyleSheet(column_title_style) + + self.note_label = QLabel( + "Billing IDs are tied to a single location", self) + self.note_label.setGeometry(555, 90, 230, 50) + self.note_label.setStyleSheet(column_desc_style) + self.note_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.note_label.setWordWrap(True) + self.note_label.show() + + self.id_bg_label = QLabel(self) + self.id_bg_label.setGeometry(550, 220, 250, 220) + self.id_bg_label.setPixmap(QPixmap(os.path.join(self.btn_path, "button230x220.png")).scaled( + self.id_bg_label.size(), Qt.AspectRatioMode.KeepAspectRatio)) + + self.text_edit = QTextEdit(self) + self.text_edit.setGeometry(550, 230, 230, 190) + self.text_edit.setPlaceholderText("Enter your existing billing Id here") + self.text_edit.textChanged.connect(self.toggle_button_state) + + separator_style = "color: #00ffff; background-color: #00ffff;" + self.separator_left = QFrame(self) + self.separator_left.setFrameShape(QFrame.Shape.VLine) + self.separator_left.setFrameShadow(QFrame.Shadow.Plain) + self.separator_left.setGeometry(270, 60, 2, 420) + self.separator_left.setStyleSheet(separator_style) + + self.separator_right = QFrame(self) + self.separator_right.setFrameShape(QFrame.Shape.VLine) + self.separator_right.setFrameShadow(QFrame.Shadow.Plain) + self.separator_right.setGeometry(540, 60, 2, 420) + self.separator_right.setStyleSheet(separator_style) + + def go_single_profile(self): + self.text_edit.clear() + self.custom_window.navigator.navigate("duration_selection") + + def go_multiple_profiles(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 toggle_button_state(self): + text = self.text_edit.toPlainText() + if text.strip(): + self.connect_button.setEnabled(True) + else: + self.connect_button.setEnabled(False) + + def show_next(self): + self.text_edit.clear() + self.custom_window.navigator.navigate("duration_selection") + + def validate_password(self): + self.button_go.setVisible(False) + text = self.text_edit.toPlainText().strip().lower() + + if text == "zenaku": + self.button_go.setVisible(True) + + def show_object_selected(self, function, page_class): + function() + self.object_selected = page_class + + def go_selected(self): + if self.object_selected: + self.custom_window.navigator.navigate(self.object_selected) + self.display.clear() + for boton in self.buttons: + boton.setChecked(False) + + self.button_go.setVisible(False) + + def reverse(self): + self.custom_window.navigator.navigate("menu") diff --git a/gui/v2/ui/pages/install_system_package.py b/gui/v2/ui/pages/install_system_package.py new file mode 100755 index 0000000..dcce2a5 --- /dev/null +++ b/gui/v2/ui/pages/install_system_package.py @@ -0,0 +1,488 @@ +import os +import subprocess + +from PyQt6.QtWidgets import QApplication, QPushButton, QLabel +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import QSize, QTimer +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page +from gui.v2.workers.worker_thread import WorkerThread + + +class InstallSystemPackage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("InstallPackage", page_stack, main_window, parent) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.package_name = "" + self.install_command = "" + self.manual_install_command = "" + self.is_sync = False + self.ui_elements = [] + self.permanent_elements = [] + self.current_distro = "debian" + self.distro_buttons = {} + + self._setup_permanent_ui() + self._setup_distro_buttons() + + self.auto_install_package_commands = { + 'all': { + 'debian': 'pkexec apt install -y bubblewrap iproute2 microsocks proxychains4 ratpoison tor wireguard xserver-xephyr', + 'arch': 'pkexec pacman -S bubblewrap iproute2 microsocks proxychains-ng tor wireguard-tools xorg-server-xephyr', + 'fedora': 'pkexec dnf -y install bubblewrap iproute proxychains4 ratpoison tor wireguard-tools xorg-x11-server-Xephyr' + }, + 'bwrap': { + 'debian': 'pkexec apt -y install bubblewrap', + 'arch': 'pkexec pacman -S bubblewrap', + 'fedora': 'pkexec dnf -y install bubblewrap' + }, + 'ip': { + 'debian': 'pkexec apt -y install iproute2', + 'arch': 'pkexec pacman -S iproute2', + 'fedora': 'pkexec dnf -y install iproute' + }, + 'microsocks': { + 'debian': 'pkexec apt -y install microsocks', + 'arch': 'pkexec pacman -S microsocks', + 'fedora': 'zenity --warning --text="An essential dependency (microsocks) is not included in your distribution\'s default repository. Please build it from source or contact support."' + }, + 'proxychains4': { + 'debian': 'pkexec apt -y install proxychains4', + 'arch': 'pkexec pacman -S proxychains-ng', + 'fedora': 'pkexec dnf -y install proxychains4' + }, + 'ratpoison': { + 'debian': 'pkexec apt -y install ratpoison', + 'arch': 'zenity --warning --text="An essential dependency (ratpoison) is not included in your distribution\'s default repository. Please build it from source or contact support."', + 'fedora': 'pkexec dnf -y install ratpoison' + }, + 'tor': { + 'debian': 'pkexec apt -y install tor', + 'arch': 'pkexec pacman -S tor', + 'fedora': 'pkexec dnf -y install tor' + }, + 'wg-quick': { + 'debian': 'pkexec apt -y install wireguard', + 'arch': 'pkexec pacman -S wireguard-tools', + 'fedora': 'pkexec dnf -y install wireguard-tools' + }, + 'Xephyr': { + 'debian': 'pkexec apt -y install xserver-xephyr', + 'arch': 'pkexec pacman -S xorg-server-xephyr', + 'fedora': 'pkexec dnf -y install xorg-x11-server-Xephyr' + } + } + + self.manual_install_package_commands = { + 'all': { + 'debian': 'sudo apt install -y bubblewrap iproute2 microsocks proxychains4 ratpoison tor wireguard xserver-xephyr', + 'arch': 'sudo pacman -S bubblewrap iproute2 microsocks proxychains-ng tor wireguard-tools xorg-server-xephyr', + 'fedora': 'sudo dnf -y install bubblewrap iproute proxychains4 ratpoison tor wireguard-tools xorg-x11-server-Xephyr' + }, + 'bwrap': { + 'debian': 'sudo apt -y install bubblewrap', + 'arch': 'sudo pacman -S bubblewrap', + 'fedora': 'sudo dnf -y install bubblewrap' + }, + 'ip': { + 'debian': 'sudo apt -y install iproute2', + 'arch': 'sudo pacman -S iproute2', + 'fedora': 'sudo dnf -y install iproute' + }, + 'microsocks': { + 'debian': 'sudo apt -y install microsocks', + 'arch': 'sudo pacman -S microsocks', + 'fedora': '# Package not available.' + }, + 'proxychains4': { + 'debian': 'sudo apt -y install proxychains4', + 'arch': 'sudo pacman -S proxychains-ng', + 'fedora': 'sudo dnf -y install proxychains4' + }, + 'ratpoison': { + 'debian': 'sudo apt -y install ratpoison', + 'arch': '# Package not available.', + 'fedora': 'sudo dnf -y install ratpoison' + }, + 'tor': { + 'debian': 'sudo apt -y install tor', + 'arch': 'sudo pacman -S tor', + 'fedora': 'sudo dnf -y install tor' + }, + 'wg-quick': { + 'debian': 'sudo apt -y install wireguard', + 'arch': 'sudo pacman -S wireguard-tools', + 'fedora': 'sudo dnf -y install wireguard-tools' + }, + 'Xephyr': { + 'debian': 'sudo apt -y install xserver-xephyr', + 'arch': 'sudo pacman -S xorg-server-xephyr', + 'fedora': 'sudo dnf -y install xorg-x11-server-Xephyr' + } + } + + def _setup_permanent_ui(self): + self.title.setGeometry(20, 50, 300, 60) + self.title.setText("Package Installation") + self.title.setStyleSheet("font-size: 22px; font-weight: bold;") + self.permanent_elements.append(self.title) + + self.display.setGeometry(QtCore.QRect(100, 160, 580, 435)) + self.permanent_elements.append(self.display) + + def _setup_distro_buttons(self): + distros = ["debian", "arch", "fedora"] + + for i, distro in enumerate(distros): + btn = QPushButton(self) + btn.setGeometry(360 + i*140, 40, 120, 49) + btn.setIcon(QIcon(os.path.join(self.btn_path, f"{distro}.png"))) + btn.setIconSize(QSize(120, 49)) + btn.setCheckable(True) + btn.clicked.connect( + lambda checked, d=distro: self.switch_distro(d)) + self.distro_buttons[distro] = btn + self.permanent_elements.append(btn) + + self.distro_buttons[self.current_distro].setChecked(True) + + def switch_distro(self, distro): + self.current_distro = distro + + for d, btn in self.distro_buttons.items(): + btn.setChecked(d == distro) + + if self.package_name: + self.update_command() + self.setup_ui() + + def update_command(self): + if self.package_name in self.auto_install_package_commands: + if self.is_sync: + self.install_command = self.auto_install_package_commands[ + 'tor_sync'][self.current_distro] + self.manual_install_command = self.manual_install_package_commands[ + 'tor_sync'][self.current_distro] + else: + self.install_command = self.auto_install_package_commands[ + self.package_name][self.current_distro] + self.manual_install_command = self.manual_install_package_commands[ + self.package_name][self.current_distro] + else: + self.install_command = "" + self.manual_install_command = "" + + def configure(self, package_name, distro, is_sync=False): + self.package_name = package_name + self.current_distro = distro + self.is_sync = is_sync + self.update_command() + self.setup_ui() + return self + + def setup_ui(self): + self.button_back.setVisible(False) + self.button_go.setVisible(True) + if not self.package_name: + return + + self.clear_ui() + + self._setup_auto_install() + self._setup_manual_install() + + self._setup_status_and_nav() + + for element in self.ui_elements: + element.setParent(self) + element.setVisible(True) + element.raise_() + + def clear_ui(self): + for element in self.ui_elements: + element.setParent(None) + element.deleteLater() + self.ui_elements.clear() + + self.command_box = None + self.status_label = None + self.install_button = None + self.check_button = None + + def _setup_auto_install(self): + auto_install_label = QLabel( + f"Automatic Installation for {self.current_distro}", self) + auto_install_label.setGeometry(80, 130, 580, 30) + auto_install_label.setStyleSheet("font-size: 18px; font-weight: bold;") + auto_install_label.setVisible(True) + self.ui_elements.append(auto_install_label) + + auto_install_desc = QLabel( + "Click the button below to automatically install the missing packages:", self) + auto_install_desc.setStyleSheet("font-size: 14px; font-weight: bold;") + auto_install_desc.setGeometry(80, 170, 650, 30) + auto_install_desc.setVisible(True) + self.ui_elements.append(auto_install_desc) + + self.install_button = QPushButton(self) + self.install_button.setGeometry(135, 210, 100, 40) + self.install_button.setIcon( + QIcon(os.path.join(self.btn_path, "install.png"))) + self.install_button.setIconSize(self.install_button.size()) + self.install_button.clicked.connect(self.install_package) + self.install_button.setVisible(True) + self.ui_elements.append(self.install_button) + + if self.current_distro == 'arch': + auto_install_label.setGeometry(50, 110, 580, 30) + auto_install_desc.setGeometry(50, 140, 650, 30) + + self.install_button.setGeometry(150, 185, 100, 40) + + self.check_button = QPushButton(self) + self.check_button.setGeometry(270, 185, 100, 40) + self.check_button.setIcon( + QIcon(os.path.join(self.btn_path, "check.png"))) + self.check_button.setIconSize(self.check_button.size()) + self.check_button.clicked.connect(self.check_package) + self.check_button.setVisible(True) + self.ui_elements.append(self.check_button) + + def _setup_manual_install(self): + if self.current_distro != 'arch': + manual_install_label = QLabel( + f"Manual Installation for {self.current_distro}", self) + manual_install_label.setGeometry(80, 290, 650, 30) + manual_install_label.setStyleSheet( + "font-size: 18px; font-weight: bold;") + manual_install_label.setVisible(True) + self.ui_elements.append(manual_install_label) + + manual_install_desc = QLabel( + "Run this command in your terminal:", self) + manual_install_desc.setStyleSheet( + "font-size: 14px; font-weight: bold;") + manual_install_desc.setGeometry(80, 330, 380, 30) + manual_install_desc.setVisible(True) + + self.ui_elements.append(manual_install_desc) + + self.command_box = QLabel(self.manual_install_command, self) + + self.command_box.setGeometry(80, 370, 380, 40) + + if self.current_distro == 'arch': + self.command_box.setGeometry(50, 280, 380, 40) + + metrics = self.command_box.fontMetrics() + text_width = metrics.horizontalAdvance( + self.manual_install_command) + 100 + available_width = self.command_box.width() - 20 + font_size = 18 + MIN_FONT_SIZE = 10 + while text_width > available_width: + if font_size <= MIN_FONT_SIZE: + break + font_size -= 1 + text_width -= 10 + + self.command_box.setStyleSheet(f""" + background-color: #2b2b2b; + color: #ffffff; + padding: 10px; + border-radius: 5px; + font-family: monospace; + font-size: {font_size}px; + """) + + self.command_box.mousePressEvent = lambda _: self.copy_command() + self.command_box.setVisible(True) + self.ui_elements.append(self.command_box) + + if self.current_distro == 'arch': + pacman_label = QLabel("Pacman Installation:", self) + pacman_label.setGeometry(50, 240, 650, 30) + pacman_label.setStyleSheet( + "font-size: 18px; font-weight: bold; color: #00ffff;") + pacman_label.setVisible(True) + self.ui_elements.append(pacman_label) + + aur_label = QLabel("AUR / Yay Installation:", self) + aur_label.setGeometry(50, 350, 300, 30) + aur_label.setStyleSheet( + "font-size: 18px; font-weight: bold; color: #00ffff;") + self.ui_elements.append(aur_label) + + self.yay_command_box = QLabel("sudo yay -S ratpoison", self) + self.yay_command_box.setGeometry(50, 390, 300, 40) + self._setup_command_box_style(self.yay_command_box) + self.yay_command_box.mousePressEvent = lambda _: self.copy_command( + "sudo yay -S ratpoison", self.yay_command_box) + self.ui_elements.append(self.yay_command_box) + + aur_command = "git clone https://aur.archlinux.org/ratpoison.git\ncd ratpoison\nmakepkg -si --skippgpcheck" + self.aur_command_box = QLabel(aur_command, self) + self.aur_command_box.setGeometry(370, 390, 380, 60) + self._setup_command_box_style(self.aur_command_box) + self.aur_command_box.setWordWrap(True) + self.aur_command_box.mousePressEvent = lambda _: self.copy_command( + aur_command, self.aur_command_box) + self.ui_elements.append(self.aur_command_box) + + else: + self.command_box = QLabel(self.manual_install_command, self) + self.command_box.setGeometry(80, 370, 380, 40) + metrics = self.command_box.fontMetrics() + text_width = metrics.horizontalAdvance( + self.manual_install_command) + 100 + available_width = self.command_box.width() - 20 + font_size = 18 + MIN_FONT_SIZE = 10 + while text_width > available_width: + if font_size <= MIN_FONT_SIZE: + break + font_size -= 1 + text_width -= 10 + + self.command_box.setStyleSheet(f""" + background-color: #2b2b2b; + color: #ffffff; + padding: 10px; + border-radius: 5px; + font-family: monospace; + font-size: {font_size}px; + """) + + self.command_box.mousePressEvent = lambda _: self.copy_command() + self.command_box.setVisible(True) + self.ui_elements.append(self.command_box) + + def _setup_command_box_style(self, command_box): + metrics = command_box.fontMetrics() + text_width = metrics.horizontalAdvance(command_box.text()) + 100 + available_width = command_box.width() - 20 + font_size = 18 + MIN_FONT_SIZE = 10 + while text_width > available_width: + if font_size <= MIN_FONT_SIZE: + break + font_size -= 1 + text_width -= 10 + + command_box.setStyleSheet(f""" + background-color: #2b2b2b; + color: #ffffff; + padding: 10px; + border-radius: 5px; + font-family: monospace; + font-size: {font_size}px; + """) + command_box.setVisible(True) + + def copy_yay_command(self): + clipboard = QApplication.clipboard() + clipboard.setText("sudo yay -S ratpoison") + + original_style = self.yay_command_box.styleSheet() + self.yay_command_box.setStyleSheet( + original_style + "background-color: #27ae60;") + QTimer.singleShot( + 200, lambda: self.yay_command_box.setStyleSheet(original_style)) + self.update_status.update_status("Yay command copied to clipboard") + + def _setup_status_and_nav(self): + if self.current_distro != 'arch': + self.check_button = QPushButton(self) + self.check_button.setGeometry(130, 430, 100, 40) + self.check_button.setIcon( + QIcon(os.path.join(self.btn_path, "check.png"))) + self.check_button.setIconSize(self.check_button.size()) + self.check_button.clicked.connect(self.check_package) + self.ui_elements.append(self.check_button) + + self.button_go.clicked.connect(self.go_next) + self.button_back.clicked.connect(self.go_next) + + self.status_label = QLabel("", self) + self.status_label.setGeometry(300, 430, 250, 40) + if self.current_distro == 'arch': + self.status_label.setGeometry(450, 185, 250, 40) + self.ui_elements.append(self.status_label) + + def copy_command(self, command=None, command_box=None): + if command_box is None: + command_box = self.command_box + + clipboard = QApplication.clipboard() + if command: + clipboard.setText(command) + else: + clipboard.setText(self.manual_install_command) + + original_style = command_box.styleSheet() + command_box.setStyleSheet( + original_style + "background-color: #27ae60;") + QTimer.singleShot( + 200, lambda: command_box.setStyleSheet(original_style)) + self.update_status.update_status("Command copied to clipboard") + + def install_package(self): + if not self.install_command: + self.update_status.update_status( + f"Installation not supported for {self.current_distro}") + return + + if subprocess.getstatusoutput('pkexec --help')[0] == 127: + self.update_status.update_status("polkit toolkit is not installed") + self.install_button.setDisabled(True) + return + + self.worker_thread = WorkerThread('INSTALL_PACKAGE', + package_command=self.install_command, + package_name=self.package_name) + self.worker_thread.text_output.connect( + self.update_status.update_status) + self.worker_thread.finished.connect(self.installation_complete) + self.worker_thread.start() + + def installation_complete(self, success): + if success: + self.check_package() + + def check_package(self): + try: + if self.package_name.lower() == 'all': + packages = ['bwrap', 'ip', 'microsocks', + 'proxychains4', 'ratpoison', 'tor', 'Xephyr', 'wg'] + for package in packages: + if subprocess.getstatusoutput(f'{package} --help')[0] == 127: + self.status_label.setText( + f"{package} is not installed") + self.status_label.setStyleSheet("color: red;") + return + self.status_label.setText("All packages installed!") + self.status_label.setStyleSheet("color: green;") + elif self.package_name.lower() == 'wireguard': + self.install_name = 'wg' + elif self.package_name.lower() == 'bubblewrap': + self.install_name = 'bwrap' + elif self.package_name.lower() == 'xserver-xephyr': + self.install_name = 'Xephyr' + else: + self.install_name = self.package_name + if subprocess.getstatusoutput(f'{self.install_name.lower()} --help')[0] == 127: + self.status_label.setText( + f"{self.package_name} is not installed") + self.status_label.setStyleSheet("color: red;") + else: + self.status_label.setText( + f"{self.package_name} is installed!") + self.status_label.setStyleSheet("color: green;") + except Exception: + self.status_label.setText("Check failed") + self.status_label.setStyleSheet("color: red;") + + def go_next(self): + self.custom_window.navigator.navigate("menu") diff --git a/gui/v2/ui/pages/location_page.py b/gui/v2/ui/pages/location_page.py new file mode 100755 index 0000000..f6c8715 --- /dev/null +++ b/gui/v2/ui/pages/location_page.py @@ -0,0 +1,254 @@ +import os + +from PyQt6.QtWidgets import QApplication, QLabel, QPushButton, QButtonGroup +from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QFont, QTransform +from PyQt6.QtCore import Qt +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class LocationPage(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("Location", page_stack, main_window, parent) + self.selected_location_icon = None + self.update_status = main_window + self.button_reverse.setVisible(True) + self.connection_manager = main_window.connection_manager + self.button_reverse.clicked.connect(self.reverse) + self.display.setGeometry(QtCore.QRect(5, 10, 390, 520)) + self.title.setGeometry(395, 40, 380, 40) + self.title.setText("Pick a location") + + self.initial_display = QLabel(self) + self.initial_display.setGeometry(5, 22, 355, 485) + self.initial_display.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.initial_display.setWordWrap(True) + + self.verification_button = QPushButton("More Info", self) + self.verification_button.setGeometry(10, 70, 160, 40) + self.verification_button.setStyleSheet(f""" + QPushButton {{ + background-color: rgba(60, 80, 120, 1.0); + border: 2px solid rgba(100, 140, 200, 1.0); + border-radius: 5px; + color: rgb(255, 255, 255); + font-size: 15px; + font-weight: bold; + padding: 5px 10px; + }} + QPushButton:hover {{ + background-color: rgba(80, 100, 140, 1.0); + border: 2px solid rgba(120, 160, 220, 1.0); + }} + QPushButton:disabled {{ + background-color: rgba(40, 50, 70, 0.6); + border: 2px solid rgba(70, 90, 110, 0.6); + opacity: 0.5; + }} + """) + self.verification_button.setCursor( + QtCore.Qt.CursorShape.PointingHandCursor) + self.verification_button.setEnabled(False) + self.verification_button.show() + + if self.connection_manager.is_synced(): + from gui.v2.actions.sync import generate_grid_positions + locs = self.connection_manager.get_location_list() + positions = generate_grid_positions(len(locs)) + available = [(QPushButton, loc, positions[i]) for i, loc in enumerate(locs)] + self.create_interface_elements(available) + + def showEvent(self, event): + super().showEvent(event) + self.button_next.setVisible(False) + for button in self.buttons: + button.setChecked(False) + if hasattr(self, 'verification_button'): + self.verification_button.setEnabled(False) + + def create_interface_elements(self, available_locations): + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + + for j, (object_type, icon_name, geometry) in enumerate(available_locations): + boton = object_type(self) + boton.setGeometry(*geometry) + boton.setIconSize(boton.size()) + boton.setCheckable(True) + + locations = self.connection_manager.get_location_info(icon_name) + if locations and not (hasattr(locations, 'is_wireguard_capable') and locations.is_wireguard_capable): + boton.setVisible(False) + icon_path = os.path.join(self.btn_path, f"button_{icon_name}.png") + boton.setIcon(QIcon(icon_path)) + fallback_path = os.path.join( + self.btn_path, "default_location_button.png") + provider = locations.operator.name if locations and hasattr( + locations, 'operator') else None + if boton.icon().isNull(): + if locations and hasattr(locations, 'country_name'): + location_name = locations.country_name + base_image = LocationPage.create_location_button_image( + location_name, fallback_path, provider) + boton.setIcon(QIcon(base_image)) + else: + base_image = LocationPage.create_location_button_image( + '', fallback_path, provider) + boton.setIcon(QIcon(base_image)) + else: + if locations and hasattr(locations, 'country_name'): + base_image = LocationPage.create_location_button_image( + f'{locations.country_code}_{locations.code}', fallback_path, provider, icon_path) + boton.setIcon(QIcon(base_image)) + else: + base_image = LocationPage.create_location_button_image( + '', fallback_path, provider, icon_path) + boton.setIcon(QIcon(base_image)) + + self.buttons.append(boton) + self.buttonGroup.addButton(boton, j) + boton.location_icon_name = icon_name + boton.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + boton.clicked.connect( + lambda checked, loc=icon_name: self.show_location(loc)) + + def update_swarp_json(self, get_connection=False): + profile_data = self.update_status.read_data() + self.connection_type = profile_data.get("connection", "") + if get_connection: + return self.connection_type + inserted_data = { + "location": self.selected_location_icon + } + self.update_status.write_data(inserted_data) + + def show_location(self, location): + self.initial_display.hide() + + max_size = QtCore.QSize(300, 300) + target_size = self.display.size().boundedTo(max_size) + + self.display.setPixmap(QPixmap(os.path.join(self.btn_path, f"icon_{location}.png")).scaled( + target_size, Qt.AspectRatioMode.KeepAspectRatio)) + self.selected_location_icon = location + self.button_next.setVisible(True) + self.button_next.clicked.connect(self.go_selected) + self.update_swarp_json() + + try: + self.verification_button.clicked.disconnect() + except TypeError: + pass + + self.verification_button.clicked.connect( + lambda: self.show_location_verification(location)) + self.verification_button.setEnabled(True) + + def reverse(self): + self.custom_window.navigator.navigate("protocol") + + def go_selected(self): + if self.connection_type == "system-wide": + self.custom_window.navigator.navigate("resume") + else: + self.custom_window.navigator.navigate("browser") + + def show_location_verification(self, location_icon_name): + from gui.v2.ui.pages.location_verification_page import LocationVerificationPage + verification_page = LocationVerificationPage( + self.page_stack, self.update_status, location_icon_name, self) + self.page_stack.addWidget(verification_page) + self.page_stack.setCurrentIndex( + self.page_stack.indexOf(verification_page)) + + @staticmethod + def create_location_button_image(location_name, fallback_image_path, provider=None, existing_icon_path=None): + if existing_icon_path: + base_image = QPixmap(existing_icon_path) + if base_image.isNull(): + base_image = QPixmap(fallback_image_path) + draw_location_text = False + else: + base_image = QPixmap(fallback_image_path) + draw_location_text = True + + if base_image.isNull(): + return base_image + + painter = QPainter(base_image) + try: + if draw_location_text: + font_size = 12 + app_font = QApplication.font() + app_font.setPointSize(font_size) + app_font.setWeight(QFont.Weight.Bold) + text_length = len(location_name) + + if text_length <= 10: + font_stretch = 100 + text_width_scale = 1.1 + text_height_scale = 1.4 + elif text_length <= 15: + font_stretch = 90 + text_width_scale = 1.0 + text_height_scale = 2 + else: + font_stretch = 100 + text_width_scale = 1.1 + text_height_scale = 4.5 + app_font.setStretch(font_stretch) + painter.setFont(app_font) + painter.setPen(QColor(0, 255, 255)) + + text_rect = painter.fontMetrics().boundingRect(location_name) + max_width = 110 + + while text_rect.width() > max_width: + font_size -= 1 + app_font.setPointSize(font_size) + app_font.setWeight(QFont.Weight.Bold) + painter.setFont(app_font) + text_rect = painter.fontMetrics().boundingRect(location_name) + + x = ((base_image.width() - text_rect.width()) // 2) - 30 + y = ((base_image.height() + text_rect.height()) // 2) - 15 + + transform = QTransform() + transform.scale(text_width_scale, text_height_scale) + painter.setTransform(transform) + + x_scaled = int(x / text_width_scale) + y_scaled = int(y / text_height_scale) + + shadow_offset = 1 + painter.setPen(QColor(0, 0, 0)) + painter.drawText(x_scaled + shadow_offset, + y_scaled + shadow_offset, location_name) + + painter.setPen(QColor(0, 255, 255)) + painter.drawText(x_scaled, y_scaled, location_name) + + painter.setTransform(QTransform()) + + if provider: + + painter.setPen(QColor(128, 128, 128)) + + provider_text_font = QApplication.font() + provider_text_font.setPointSize(7) + provider_text_font.setWeight(QFont.Weight.Normal) + painter.setFont(provider_text_font) + provider_text_rect = painter.fontMetrics().boundingRect(provider) + + provider_x = ( + (base_image.width() - provider_text_rect.width()) // 2) - 30 + provider_y = 50 + px = int(provider_x) + py = int(provider_y) + + painter.setFont(provider_text_font) + painter.drawText(px + 5, py + 2, provider) + finally: + painter.end() + return base_image diff --git a/gui/v2/ui/pages/location_verification_page.py b/gui/v2/ui/pages/location_verification_page.py new file mode 100755 index 0000000..458a9a1 --- /dev/null +++ b/gui/v2/ui/pages/location_verification_page.py @@ -0,0 +1,284 @@ +import os + +from PyQt6.QtWidgets import QApplication, QLabel, QPushButton +from PyQt6.QtGui import QPixmap, QIcon +from PyQt6.QtCore import Qt, QSize +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class LocationVerificationPage(Page): + def __init__(self, page_stack, main_window, location_icon_name, previous_page, parent=None): + super().__init__("LocationVerification", page_stack, main_window, parent) + self.location_icon_name = location_icon_name + self.previous_page = previous_page + self.connection_manager = main_window.connection_manager + self.font_style = f"font-family: '{main_window.open_sans_family}';" + self.verification_info = {} + self.verification_checkmarks = {} + self.verification_full_values = {} + self.verification_labels = {} + self.verification_copy_buttons = {} + self.verification_display_names = { + "operator_name": "Operator Name", + "provider_name": "Provider Name", + "nostr_public_key": "Nostr Key", + "hydraveil_public_key": "HydraVeil Key", + "nostr_attestation_event_reference": "Nostr Verification" + } + self.setup_ui() + + def setup_ui(self): + location_display = QLabel(self) + location_display.setGeometry(0, 0, 390, 520) + location_display.setAlignment(Qt.AlignmentFlag.AlignCenter) + + icon_path = os.path.join( + self.btn_path, f"icon_{self.location_icon_name}.png") + location_pixmap = QPixmap(icon_path) + fallback_path = os.path.join( + self.btn_path, "default_location_button.png") + + if location_pixmap.isNull(): + location_pixmap = QPixmap(fallback_path) + + locations = self.connection_manager.get_location_info( + self.location_icon_name) + operator_name = locations.operator.name if locations and hasattr( + locations, 'operator') else "" + + max_size = QtCore.QSize(300, 300) + target_size = location_display.size().boundedTo(max_size) + + location_display.setPixmap(location_pixmap.scaled( + target_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + location_display.show() + + title = QLabel("Operator Information", self) + title.setGeometry(350, 50, 350, 30) + title.setStyleSheet( + f"color: #808080; font-size: 20px; font-weight: bold; {self.font_style}") + title.show() + + info_items = [ + ("Operator Name", "operator_name", 130), + ("Provider Name", "provider_name", 180), + ("Nostr Key", "nostr_public_key", 230), + ("HydraVeil Key", "hydraveil_public_key", 280), + ("Nostr Verification", "nostr_attestation_event_reference", 330), + ] + + label_x = 350 + label_width = 150 + label_height = 30 + value_x = 510 + value_width = 210 + value_height = 30 + checkmark_x = 725 + checkmark_width = 30 + checkmark_height = 30 + copy_button_x = 750 + copy_button_width = 30 + copy_button_height = 30 + + for label_text, key, y_pos in info_items: + label_widget = QLabel(label_text + ":", self) + label_widget.setGeometry(label_x, y_pos, label_width, label_height) + label_widget.setStyleSheet( + f"color: white; font-size: 18px; {self.font_style}") + label_widget.setAlignment( + Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + label_widget.show() + self.verification_labels[key] = label_widget + + value_widget = QLabel("N/A", self) + value_widget.setGeometry(value_x, y_pos, value_width, value_height) + value_widget.setStyleSheet( + f"color: white; font-size: 16px; {self.font_style}") + value_widget.setAlignment( + Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + value_widget.show() + + checkmark_widget = QLabel("✓", self) + checkmark_widget.setGeometry( + checkmark_x, y_pos, checkmark_width, checkmark_height) + checkmark_widget.setStyleSheet( + f"color: #4CAF50; font-size: 28px; font-weight: bold; {self.font_style}") + checkmark_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + checkmark_widget.hide() + + self.verification_info[key] = value_widget + self.verification_checkmarks[key] = checkmark_widget + + copy_button = QPushButton(self) + copy_button.setGeometry( + copy_button_x, y_pos, copy_button_width, copy_button_height) + copy_button.setIcon( + QIcon(os.path.join(self.btn_path, "paste_button.png"))) + copy_button.setIconSize(QSize(24, 24)) + copy_button.setStyleSheet(f""" + QPushButton {{ + background: transparent; + border: none; + }} + QPushButton:hover {{ + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + }} + """) + copy_button.clicked.connect( + lambda checked=False, k=key: self.copy_verification_value(k)) + copy_button.show() + self.verification_copy_buttons[key] = copy_button + + self.back_button = QPushButton(self) + self.back_button.setGeometry(720, 533, 48, 35) + self.back_button.setIcon( + QIcon(os.path.join(self.btn_path, "back.png"))) + self.back_button.setIconSize(QSize(48, 35)) + self.back_button.setStyleSheet(f""" + QPushButton {{ + background: transparent; + border: none; + }} + QPushButton:hover {{ + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + }} + """) + self.back_button.clicked.connect(self.go_back) + self.back_button.show() + + self.update_verification_info() + + def truncate_text_by_width(self, text, widget, max_width): + if not text or text == "N/A": + return text + + metrics = widget.fontMetrics() + text_width = metrics.horizontalAdvance(text) + + if text_width <= max_width: + return text + + ellipsis = "..." + ellipsis_width = metrics.horizontalAdvance(ellipsis) + available_width = max_width - ellipsis_width + + if available_width <= 0: + return ellipsis + + start_chars = 1 + end_chars = 1 + + while True: + start_text = text[:start_chars] + end_text = text[-end_chars:] + combined_width = metrics.horizontalAdvance( + start_text + ellipsis + end_text) + + if combined_width <= max_width: + if start_chars + end_chars >= len(text): + return text + start_chars += 1 + if start_chars + end_chars >= len(text): + return text + end_chars += 1 + else: + if start_chars > 1: + start_chars -= 1 + if end_chars > 1: + end_chars -= 1 + break + + if start_chars + end_chars >= len(text): + return text + + return text[:start_chars] + ellipsis + text[-end_chars:] + + def update_verification_info(self): + locations = self.connection_manager.get_location_info( + self.location_icon_name) + value_width = 210 + + if not locations: + for key, widget in self.verification_info.items(): + widget.setText("N/A") + self.verification_full_values[key] = "N/A" + if key in self.verification_checkmarks: + self.verification_checkmarks[key].hide() + return + + operator = None + if locations.operator: + operator = locations.operator + + if operator: + operator_name = operator.name or "N/A" + self.verification_full_values["operator_name"] = operator_name + display_operator = self.truncate_text_by_width( + operator_name, self.verification_info["operator_name"], value_width) + self.verification_info["operator_name"].setText(display_operator) + + provider_name = locations.provider_name if locations and hasattr( + locations, 'provider_name') and locations.provider_name else "N/A" + self.verification_full_values["provider_name"] = provider_name + display_provider = self.truncate_text_by_width( + provider_name, self.verification_info["provider_name"], value_width) + self.verification_info["provider_name"].setText(display_provider) + + nostr_key = operator.nostr_public_key or "N/A" + self.verification_full_values["nostr_public_key"] = nostr_key + display_nostr = self.truncate_text_by_width( + nostr_key, self.verification_info["nostr_public_key"], value_width) + self.verification_info["nostr_public_key"].setText(display_nostr) + + hydraveil_key = operator.public_key or "N/A" + self.verification_full_values["hydraveil_public_key"] = hydraveil_key + display_hydraveil = self.truncate_text_by_width( + hydraveil_key, self.verification_info["hydraveil_public_key"], value_width) + self.verification_info["hydraveil_public_key"].setText( + display_hydraveil) + + nostr_verification = operator.nostr_attestation_event_reference or "N/A" + self.verification_full_values["nostr_attestation_event_reference"] = nostr_verification + display_nostr_verif = self.truncate_text_by_width( + nostr_verification, self.verification_info["nostr_attestation_event_reference"], value_width) + self.verification_info["nostr_attestation_event_reference"].setText( + display_nostr_verif) + else: + self.verification_info["operator_name"].setText("N/A") + self.verification_full_values["operator_name"] = "N/A" + self.verification_info["provider_name"].setText("N/A") + self.verification_full_values["provider_name"] = "N/A" + self.verification_info["nostr_public_key"].setText("N/A") + self.verification_full_values["nostr_public_key"] = "N/A" + self.verification_info["hydraveil_public_key"].setText("N/A") + self.verification_full_values["hydraveil_public_key"] = "N/A" + self.verification_info["nostr_attestation_event_reference"].setText( + "N/A") + self.verification_full_values["nostr_attestation_event_reference"] = "N/A" + + for key, widget in self.verification_info.items(): + if key in self.verification_checkmarks: + full_value = self.verification_full_values.get(key, "") + if full_value and full_value != "N/A": + self.verification_checkmarks[key].show() + else: + self.verification_checkmarks[key].hide() + + def copy_verification_value(self, key): + full_value = self.verification_full_values.get(key, "") + if full_value and full_value != "N/A": + clipboard = QApplication.clipboard() + clipboard.setText(full_value) + display_name = self.verification_display_names.get( + key, "Verification value") + self.custom_window.update_status( + f"{display_name} copied to clipboard") + + def go_back(self): + if self.previous_page: + self.page_stack.setCurrentIndex( + self.page_stack.indexOf(self.previous_page)) diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py new file mode 100755 index 0000000..7e48766 --- /dev/null +++ b/gui/v2/ui/pages/menu_page.py @@ -0,0 +1,1247 @@ +import functools +import os +import threading + +from PyQt6 import QtCore, QtGui +from PyQt6.QtCore import Qt, QSize +from PyQt6.QtGui import QIcon, QPainter, QPixmap +from PyQt6.QtWidgets import ( + QApplication, QButtonGroup, QFrame, QHBoxLayout, QLabel, QLineEdit, + QMessageBox, QPushButton, QScrollArea, QTextEdit, QVBoxLayout, QWidget, +) + +from core.controllers.ConfigurationController import ConfigurationController +from core.controllers.ProfileController import ProfileController +from core.controllers.tickets.UseTicketController import ( + do_we_use_a_random_ticket, + get_unused_tickets, +) +from core.models.session.SessionProfile import SessionProfile +from core.models.system.SystemProfile import SystemProfile + +from gui.v2.infrastructure.setup_observers import ticket_observer +from gui.v2.actions.profile_order import normalize_profile_order +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.pages.location_verification_page import LocationVerificationPage +from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS +from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup +from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup +from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup +from gui.v2.workers.worker import Worker +from gui.v2.workers.worker_thread import WorkerThread + + +class MenuPage(Page): + def __init__(self, page_stack=None, main_window=None, parent=None): + super().__init__("Menu", page_stack, main_window, parent) + self.main_window = main_window + self.connection_manager = main_window.connection_manager + self.is_tor_mode = main_window.is_tor_mode + self.is_animating = main_window.is_animating + self.animation_step = main_window.animation_step + self.btn_path = main_window.btn_path + self.page_stack = page_stack + self.profile_info = {} + self.additional_labels = [] + self.update_status = main_window + self.title.setGeometry(400, 40, 380, 30) + self.title.setText("Load Profile") + + self.scroll_area = QScrollArea(self) + self.scroll_area.setGeometry(420, 80, 370, 350) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.scroll_area.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.scroll_area.setStyleSheet(SCROLLBAR_CYAN_QSS) + + self.scroll_widget = QWidget() + self.scroll_widget.setStyleSheet("background-color: transparent;") + self.scroll_area.setWidget(self.scroll_widget) + + self.not_connected_profile = os.path.join( + self.btn_path, "button_profile.png") + self.connected_profile = os.path.join( + self.btn_path, "button_session_profile.png") + self.system_wide_connected_profile = os.path.join( + self.btn_path, "button_system_profile.png") + self.connected_tor_profile = os.path.join( + self.btn_path, "button_session_tor.png") + self.worker = None + self.IsSystem: int = 0 + self.button_states = {} + self.is_system_connected = False + self.profile_button_map = {} + self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else "" + + self.create_interface_elements() + + def showEvent(self, event): + super().showEvent(event) + if hasattr(self, 'buttons'): + for button in self.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( + self.connection_manager.is_synced()) + + def on_update_check_finished(self): + reply = QMessageBox.question(self, 'Update Available', + 'An update is available. Would you like to download it?', + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + if reply == QMessageBox.StandardButton.Yes: + self.worker = WorkerThread('DOWNLOAD_UPDATE') + self.worker.start() + else: + self.update_status.update_status('Update canceled') + + def create_interface_elements(self): + for icon_name, function, position in [ + ("create_profile", self.create_prof, (410, 440, 110, 60)), + ("edit_profile", self.edit_prof, (540, 440, 110, 60)), + ("launch", self.launch, (0, 440, 185, 63)), + ("disconnect", self.disconnect, (200, 433, 185, 73)), + ("disconnect_system_wide", self.disconnect, (200, 433, 185, 73)), + ("just", self.connect, (200, 433, 185, 73)), + ("just_session", self.connect, (200, 433, 185, 73)), + ("settings", self.settings_gui, (670, 440, 110, 60)), + ]: + boton = QPushButton(self) + boton.setGeometry(QtCore.QRect(*position)) + boton.setObjectName("create_edit_settings_buttons") + icon = QtGui.QIcon(os.path.join(self.btn_path, f"{icon_name}.png")) + boton.setIcon(icon) + boton.setIconSize(boton.size()) + boton.clicked.connect(functools.partial(function)) + boton.setEnabled(False) + if icon_name == "disconnect": + boton.setEnabled(True) + boton.setVisible(False) + self.disconnect_button = boton + if icon_name == "launch": + self.boton_launch = boton + self.boton_launch.setVisible(False) + if icon_name == "create_profile": + self.boton_create = boton + self.boton_create.setEnabled(True) + if icon_name == "edit_profile": + self.boton_edit = boton + if icon_name == "just": + boton.setVisible(False) + self.boton_just = boton + self.connect_button = boton + if icon_name == "just_session": + self.boton_just_session = boton + self.connect_button = boton + if icon_name == "disconnect_system_wide": + boton.setEnabled(True) + boton.setVisible(False) + self.disconnect_system_wide_button = boton + if icon_name == "settings": + self.settings_button = boton + self.settings_button.setEnabled(True) + + self.create() + + def _make_confirmation_popup(self, message, button_text='Proceed'): + popup = ConfirmationPopup( + self, message=message, action_button_text=button_text, cancel_button_text="Cancel") + popup.setWindowModality(Qt.WindowModality.ApplicationModal) + return popup + + def _show_disconnect_confirmation(self, connected_profiles, button_text='Disable', message=None): + self.popup = self._make_confirmation_popup(message, button_text) + + def on_result(result): + if result: + self.update_status.update_status('Disabling profiles...') + self.worker_thread = WorkerThread( + 'DISABLE_ALL_PROFILES', profile_data=connected_profiles) + self.worker_thread.text_output.connect( + lambda text: self.update_status.update_status(text)) + self.worker_thread.start() + + self.popup.finished.connect(on_result) + self.popup.show() + + def delete_profile(self): + self.popup = ConfirmationPopup(self, message="Are you sure you want to\ndelete this profile?", + action_button_text="Delete", cancel_button_text="Cancel") + self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) + self.popup.finished.connect( + lambda result: self.on_popup_finished(result)) + self.popup.show() + + def on_popup_finished(self, result): + if result: + profile_id = self.reverse_id + self.update_status.update_status(f'Deleting profile...') + self.worker_thread = WorkerThread( + 'DESTROY_PROFILE', profile_data={'id': profile_id}) + self.worker_thread.text_output.connect(self.delete_status_update) + self.worker_thread.start() + else: + pass + + def delete_status_update(self, text): + self.update_status.update_status(text) + self.eliminacion() + + def show_disconnect_button(self, toggle, profile_type, target_button): + try: + if target_button.isChecked(): + if profile_type == 1: + self.disconnect_button.setVisible(toggle) + self.boton_just_session.setVisible(not toggle) + else: + self.disconnect_system_wide_button.setVisible(toggle) + self.boton_just.setVisible(not toggle) + except RuntimeError: + pass + + def match_core_profiles(self, profiles_dict): + new_dict = {} + + profile_ids = normalize_profile_order( + getattr(self.update_status, 'gui_config_file', None), + profiles_dict.keys()) + + for idx in profile_ids: + profile = profiles_dict[idx] + new_profile = {} + + protocol = profile.connection.code + + location = f'{profile.location.country_code}_{profile.location.code}' + + new_profile['location'] = location + + if protocol == 'wireguard': + new_profile['protocol'] = 'wireguard' + else: + new_profile['protocol'] = 'hidetor' + + connection_type = 'browser-only' if isinstance( + profile, SessionProfile) else 'system-wide' + + if protocol == 'wireguard': + new_profile['connection'] = connection_type + elif protocol == 'tor': + new_profile['connection'] = protocol + else: + new_profile['connection'] = 'just proxy' + + if isinstance(profile, SessionProfile): + browser = profile.application_version.application_code + else: + browser = 'unknown' + if browser != 'unknown': + new_profile['browser'] = browser + new_profile['browser_version'] = profile.application_version.version_number + new_profile['browser_supported'] = profile.application_version.supported + else: + new_profile['browser'] = 'unknown browser' + new_profile['browser_supported'] = False + + resolution = profile.resolution if hasattr( + profile, 'resolution') else 'None' + new_profile['dimentions'] = resolution + + new_profile['name'] = profile.name + + new_dict[f'Profile_{idx}'] = new_profile + + return new_dict + + def create(self): + self.boton_just.setEnabled(False) + self.boton_just_session.setEnabled(False) + self.boton_edit.setEnabled(False) + if hasattr(self, 'verification_button'): + self.verification_button.setEnabled(False) + self.profiles_data = self.match_core_profiles( + ProfileController.get_all()) + + self.number_of_profiles = len(self.profiles_data) + self.profile_info.update(self.profiles_data) + + self.button_group = QButtonGroup(self) + self.buttons = [] + + for profile_name, profile_value in self.profiles_data.items(): + self.create_profile_widget(profile_name, profile_value) + + self.update_scroll_widget_size() + + def refresh_profiles_data(self): + self.profiles_data = self.match_core_profiles( + ProfileController.get_all()) + self.number_of_profiles = len(self.profiles_data) + self.profile_info.update(self.profiles_data) + for profile_name, profile_value in self.profiles_data.items(): + self.create_profile_widget(profile_name, profile_value) + self.update_scroll_widget_size() + + def refresh_menu_buttons(self): + profiles = ProfileController.get_all() + self.button_states.clear() + self.connection_manager._connected_profiles.clear() + self.IsSystem = 0 + for profile_id, profile in profiles.items(): + if ProfileController.is_enabled(profile): + self.connection_manager.add_connected_profile(profile_id) + if isinstance(profile, SessionProfile): + if profile.connection and profile.connection.code == 'tor': + self.button_states[profile_id] = self.connected_tor_profile + else: + self.button_states[profile_id] = self.connected_profile + elif isinstance(profile, SystemProfile): + self.button_states[profile_id] = self.system_wide_connected_profile + self.IsSystem += 1 + + if self.IsSystem > 0: + self.update_status.update_image( + appearance_value='background_connected') + else: + self.update_status.update_image() + + for widget in self.scroll_widget.findChildren(QLabel): + widget.setParent(None) + + for widget in self.scroll_widget.findChildren(QPushButton): + widget.setParent(None) + + self.profile_button_map.clear() + self.button_group = QButtonGroup(self) + self.buttons = [] + + self.create() + + def update_scroll_widget_size(self): + if self.number_of_profiles == 0: + return + + rows = (self.number_of_profiles + 1) // 2 + height = rows * 120 + self.scroll_widget.setFixedSize(370, height) + + def create_profile_widget(self, key, value): + profile_id = int(key.split('_')[1]) + + parent_label = self.create_parent_label(profile_id) + button = self.create_profile_button(parent_label, profile_id, key) + + self.profile_button_map[profile_id] = button + + self.create_profile_name_label(parent_label, value["name"]) + self.create_profile_icons(parent_label, value) + + def create_parent_label(self, profile_id): + parent_label = QLabel(self.scroll_widget) + profile_list = list(self.profiles_data.keys()) + index = profile_list.index(f'Profile_{profile_id}') + row, column = divmod(index, 2) + + parent_label.setGeometry(column * 179, row * 120, 175, 100) + parent_label.setVisible(True) + return parent_label + + def create_profile_button(self, parent_label, profile_id, key): + button = QPushButton(parent_label) + button.setGeometry(0, 0, 175, 60) + button.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(200, 200, 200, 50); + } + QPushButton:checked { + background-color: rgba(200, 200, 200, 80); + } + """) + + icon = QIcon(self.button_states.get( + profile_id, self.not_connected_profile)) + button.setIcon(icon) + button.setIconSize(QtCore.QSize(175, 60)) + + button.show() + button.setCheckable(True) + self.button_group.addButton(button) + self.buttons.append(button) + + button.clicked.connect( + lambda checked, pid=profile_id: self.print_profile_details(f"Profile_{pid}")) + + return button + + def create_profile_name_label(self, parent_label, name): + child_label = QLabel(parent_label) + child_label.setGeometry(0, 65, 175, 30) + child_label.setText(name) + child_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + child_label.setAttribute( + Qt.WidgetAttribute.WA_TransparentForMouseEvents) + child_label.show() + + def create_profile_icons(self, parent_label, value): + connection_type = value.get('connection', '') + if connection_type in ['tor', 'just proxy']: + self.create_tor_proxy_icons(parent_label, value, connection_type) + else: + self.create_other_icons(parent_label, value, connection_type) + + def create_tor_proxy_icons(self, parent_label, value, connection_type): + for j, label_name in enumerate(['protocol', 'location', 'browser']): + child_label = QLabel(parent_label) + child_label.setObjectName(f"label_{j}") + child_label.setGeometry(3 + j * 60, 5, 50, 50) + child_label.setObjectName("label_profiles") + child_label.setAttribute( + Qt.WidgetAttribute.WA_TransparentForMouseEvents) + child_label.setStyleSheet( + "background-color: rgba(128, 128, 128, 0.5); border-radius: 25px;") + image_path = self.get_icon_path(label_name, value, connection_type) + pixmap = QPixmap(image_path) + if pixmap.isNull() and label_name == 'location': + fallback_path = os.path.join( + self.btn_path, "default_location_mini.png") + pixmap = QPixmap(fallback_path) + if pixmap.isNull(): + pixmap = QPixmap(50, 50) + pixmap.fill(QtGui.QColor(200, 200, 200)) + if pixmap.isNull() and label_name == 'browser': + fallback_path = os.path.join( + self.btn_path, "default_browser_mini.png") + pixmap = QPixmap(fallback_path) + if pixmap.isNull(): + pixmap = QPixmap(50, 50) + pixmap.fill(QtGui.QColor(200, 200, 200)) + child_label.setPixmap(pixmap) + child_label.show() + + def create_other_icons(self, parent_label, value, connection_type): + for j, label_name in enumerate(['protocol', 'location', 'browser']): + child_label = QLabel(parent_label) + child_label.setObjectName(f"label_{j}") + child_label.setGeometry(3 + j * 60, 5, 50, 50) + child_label.setObjectName("label_profiles") + child_label.setAttribute( + Qt.WidgetAttribute.WA_TransparentForMouseEvents) + child_label.setStyleSheet( + "background-color: rgba(128, 128, 128, 0.5); border-radius: 25px;") + + image_path = self.get_icon_path(label_name, value, connection_type) + pixmap = QPixmap(image_path) + if pixmap.isNull() and label_name == 'location': + fallback_path = os.path.join( + self.btn_path, "default_location_mini.png") + pixmap = QPixmap(fallback_path) + if pixmap.isNull(): + pixmap = QPixmap(50, 50) + pixmap.fill(QtGui.QColor(200, 200, 200)) + if pixmap.isNull() and label_name == 'browser': + fallback_path = os.path.join( + self.btn_path, "default_browser_mini.png") + pixmap = QPixmap(fallback_path) + if pixmap.isNull(): + pixmap = QPixmap(50, 50) + pixmap.fill(QtGui.QColor(200, 200, 200)) + + if label_name == 'browser' and not value.get('browser_supported', True) and value.get('browser') != 'unknown browser': + warning_pixmap = QPixmap( + os.path.join(self.btn_path, 'warning.png')) + painter = QPainter(pixmap) + painter.drawPixmap(pixmap.width() - 22, + pixmap.height() - 21, warning_pixmap) + painter.end() + + child_label.setPixmap(pixmap) + child_label.show() + + def get_icon_path(self, label_name, value, connection_type): + if label_name == 'protocol': + if connection_type == 'tor': + return os.path.join(self.btn_path, "toricon_mini.png") + elif connection_type == 'just proxy': + return os.path.join(self.btn_path, "just proxy_mini.png") + + return os.path.join(self.btn_path, "wireguard_mini.png") + elif label_name == 'browser': + if connection_type == 'system-wide': + return os.path.join(self.btn_path, "wireguard_system_wide.png") + else: + return os.path.join(self.btn_path, f"{value[label_name]} latest_mini.png") + + return os.path.join(self.btn_path, f"icon_mini_{value[label_name]}.png") + + def truncate_key(self, text, max_length=50): + if not text or text == "N/A" or len(text) <= max_length: + return text + start_len = max_length // 2 - 2 + end_len = max_length // 2 - 2 + return text[:start_len] + "....." + text[-end_len:] + + def create_verification_widget(self, profile_obj): + verification_widget = QWidget(self) + verification_widget.setGeometry(0, 90, 400, 300) + verification_widget.setStyleSheet("background: transparent;") + + verification_layout = QVBoxLayout(verification_widget) + verification_layout.setContentsMargins(20, 20, 20, 20) + verification_layout.setSpacing(15) + + operator = None + if profile_obj.location and profile_obj.location.operator: + operator = profile_obj.location.operator + + info_items = [ + ("Operator Name", "operator_name"), + ("Nostr Key", "nostr_public_key"), + ("HydraVeil Key", "hydraveil_public_key"), + ] + + for label, key in info_items: + container = QWidget() + container.setStyleSheet("background: transparent;") + container_layout = QHBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) + container_layout.setSpacing(10) + + label_widget = QLabel(label + ":") + label_widget.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + label_widget.setMinimumWidth(120) + container_layout.addWidget(label_widget) + + value_widget = QLineEdit() + value_widget.setReadOnly(True) + + if operator: + if key == "operator_name": + value = operator.name or "N/A" + elif key == "nostr_public_key": + value = operator.nostr_public_key or "N/A" + value = self.truncate_key( + value) if value != "N/A" else value + elif key == "hydraveil_public_key": + value = operator.public_key or "N/A" + value = self.truncate_key( + value) if value != "N/A" else value + else: + value = "N/A" + else: + value = "N/A" + + value_widget.setText(value) + value_widget.setCursorPosition(0) + value_widget.setStyleSheet(f""" + QLineEdit {{ + color: white; + background: transparent; + border: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + padding: 5px 0px; + font-size: 12px; + {self.font_style} + }} + """) + container_layout.addWidget(value_widget, 1) + + checkmark_widget = QLabel("✓") + checkmark_widget.setStyleSheet( + f"color: #4CAF50; font-size: 20px; font-weight: bold; {self.font_style}") + checkmark_widget.setFixedSize(20, 20) + checkmark_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + if value and value != "N/A": + checkmark_widget.show() + else: + checkmark_widget.hide() + container_layout.addWidget(checkmark_widget) + + copy_button = QPushButton() + copy_button.setIcon( + QIcon(os.path.join(self.btn_path, "paste_button.png"))) + copy_button.setIconSize(QSize(24, 24)) + copy_button.setFixedSize(30, 30) + copy_button.setStyleSheet(f""" + QPushButton {{ + background: transparent; + border: none; + }} + QPushButton:hover {{ + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + }} + """) + + if operator: + if key == "operator_name": + full_value = operator.name or "N/A" + elif key == "nostr_public_key": + full_value = operator.nostr_public_key or "N/A" + elif key == "hydraveil_public_key": + full_value = operator.public_key or "N/A" + else: + full_value = "N/A" + else: + full_value = "N/A" + + copy_button.clicked.connect( + lambda checked=False, val=full_value: self.copy_verification_value(val)) + container_layout.addWidget(copy_button) + + verification_layout.addWidget(container) + + verification_widget.show() + return verification_widget + + def copy_verification_value(self, value): + if value and value != "N/A": + clipboard = QApplication.clipboard() + clipboard.setText(value) + self.update_status.update_status( + "Verification value copied to clipboard") + + def print_profile_details(self, profile_name): + self.reverse_id = int(profile_name.split('_')[1]) + + target_button = self.profile_button_map.get(self.reverse_id) + if target_button: + self.connection_manager.set_profile_button_objects( + self.reverse_id, target_button) + else: + print(f"No button found for profile {self.reverse_id}") + is_connected = self.connection_manager.is_profile_connected( + int(self.reverse_id)) + if is_connected: + self.boton_edit.setEnabled(False) + else: + self.boton_edit.setEnabled(True) + + profile = self.profile_info.get(profile_name) + self.boton_launch.setEnabled(True) + self.boton_just.setEnabled(True) + self.boton_just_session.setEnabled(True) + self.boton_create.setEnabled(True) + if hasattr(self, 'buttons'): + for button in self.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( + self.connection_manager.is_synced()) + + for label in self.additional_labels: + label.deleteLater() + + self.additional_labels.clear() + + self.change_connect_button() + + if profile: + self.update_status.current_profile_id = self.reverse_id + profile_number = profile_name.split("_")[1] + name = profile.get("name", "") + protocol = profile.get("protocol", "") + location = profile.get("location", "") + connection = profile.get("connection", "") + country_garaje = profile.get("country_garaje", "") + + profile_obj = ProfileController.get(self.reverse_id) + is_profile_enabled = self.connection_manager.is_profile_connected( + self.reverse_id) + show_verification_widget = False + label_principal = None + label_tor = None + text_color = "white" + operator_name = profile_obj.location.operator.name if profile_obj.location and profile_obj.location.operator else "" + + if protocol.lower() == "wireguard" and is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard': + verification_widget = self.create_verification_widget( + profile_obj) + self.additional_labels.append(verification_widget) + show_verification_widget = True + label_principal = None + + elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard": + if profile_obj.is_session_profile(): + text_color = "black" + label_background = QLabel(self) + label_background.setGeometry(0, 60, 410, 354) + pixmap_bg = QPixmap(os.path.join( + self.btn_path, "browser only.png")) + label_background.setPixmap(pixmap_bg) + label_background.setScaledContents(True) + label_background.show() + label_background.lower() + self.additional_labels.append(label_background) + + l_img = QLabel(self) + l_img.setGeometry(20, 125, 100, 100) + pixmap_loc = QPixmap(os.path.join( + self.btn_path, f"icon_{location}.png")) + l_img.setPixmap(pixmap_loc) + l_img.setScaledContents(True) + l_img.show() + self.additional_labels.append(l_img) + l_name = f"{profile_obj.location.country_name}, {profile_obj.location.name}" if profile_obj.location else "" + o_name = profile_obj.location.operator.name if profile_obj.location and profile_obj.location.operator else "" + n_key = profile_obj.location.operator.nostr_public_key if profile_obj.location and profile_obj.location.operator else "" + + info_txt = QTextEdit(self) + info_txt.setGeometry(130, 110, 260, 250) + info_txt.setReadOnly(True) + info_txt.setFrameStyle(QFrame.Shape.NoFrame) + info_txt.setStyleSheet( + f"background: transparent; border: none; color: {text_color}; font-size: 17px;") + info_txt.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + info_txt.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + info_txt.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere) + info_txt.setText( + f"Location: {l_name}\n\nOperator: {o_name}") + info_txt.show() + self.additional_labels.append(info_txt) + + nostr_txt = QTextEdit(self) + nostr_txt.setGeometry(20, 260, 370, 80) + nostr_txt.setReadOnly(True) + nostr_txt.setFrameStyle(QFrame.Shape.NoFrame) + nostr_txt.setStyleSheet( + f"background: transparent; border: none; color: {text_color}; font-size: 17px;") + nostr_txt.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + nostr_txt.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + nostr_txt.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAnywhere) + nostr_txt.setText(f"Nostr: {n_key}") + nostr_txt.show() + self.additional_labels.append(nostr_txt) + + elif protocol.lower() in ["wireguard", "open", "residential", "hidetor"]: + label_principal = QLabel(self) + label_principal.setGeometry(0, 90, 400, 300) + pixmap = QPixmap(os.path.join( + self.btn_path, f"{protocol}_{location}.png")) + label_principal.setPixmap(pixmap) + label_principal.setScaledContents(True) + label_principal.show() + self.additional_labels.append(label_principal) + + if protocol.lower() in ["wireguard", "open", "residential", "hidetor"]: + + if protocol.lower() == "wireguard" and ConfigurationController.get_endpoint_verification_enabled(): + if is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard': + verified_icon = QLabel(self) + verified_icon.setGeometry(20, 50, 100, 80) + verified_pixmap = QPixmap(os.path.join( + self.btn_path, "verified_profile.png")) + verified_icon.setPixmap(verified_pixmap) + verified_icon.setScaledContents(True) + verified_icon.show() + self.additional_labels.append(verified_icon) + + if protocol.lower() == "hidetor" and not show_verification_widget: + + if label_principal: + label_principal.setGeometry(0, 80, 400, 300) + label_background = QLabel(self) + label_background.setGeometry(0, 60, 410, 354) + if connection == 'just proxy': + image_path = os.path.join( + self.btn_path, f"icon_{location}.png") + else: + image_path = os.path.join( + self.btn_path, f"hdtor_{location}.png") + pixmap = QPixmap(image_path) + label_background.setPixmap(pixmap) + label_background.show() + label_background.setScaledContents(True) + label_background.lower() + self.additional_labels.append(label_background) + + if protocol.lower() == "wireguard" and connection.lower() == "browser-only" and not show_verification_widget: + + if label_principal: + label_principal.setGeometry(0, 80, 400, 300) + + label_background = QLabel(self) + label_background.setGeometry(0, 60, 410, 354) + is_supported = profile.get('browser_supported', False) + image_name = "browser only.png" if is_supported else "unsupported_browser_only.png" + pixmap = QPixmap(os.path.join(self.btn_path, image_name)) + label_background.setPixmap(pixmap) + label_background.show() + label_background.setScaledContents(True) + label_background.lower() + self.additional_labels.append(label_background) + + if protocol.lower() == "residential" and connection.lower() == "tor": + + label_background = QLabel(self) + label_background.setGeometry(0, 60, 410, 354) + pixmap = QPixmap(os.path.join( + self.btn_path, "browser_tor.png")) + label_background.setPixmap(pixmap) + label_background.show() + label_background.setScaledContents(True) + self.additional_labels.append(label_background) + + label_garaje = QLabel(self) + label_garaje.setGeometry(5, 110, 400, 300) + pixmap = QPixmap(os.path.join( + self.btn_path, f"{country_garaje} garaje.png")) + label_garaje.setPixmap(pixmap) + label_garaje.show() + label_garaje.setScaledContents(True) + self.additional_labels.append(label_garaje) + + label_tor = QLabel(self) + label_tor.setGeometry(330, 300, 75, 113) + + pixmap = QPixmap(os.path.join(self.btn_path, "tor 86x130.png")) + if label_tor: + label_tor.setPixmap(pixmap) + label_tor.show() + label_tor.setScaledContents(True) + self.additional_labels.append(label_tor) + if protocol.lower() == "residential" and connection.lower() == "just proxy": + + label_background = QLabel(self) + label_background.setGeometry(0, 60, 410, 354) + pixmap = QPixmap(os.path.join( + self.btn_path, "browser_just_proxy.png")) + label_background.setPixmap(pixmap) + label_background.show() + label_background.setScaledContents(True) + self.additional_labels.append(label_background) + + label_garaje = QLabel(self) + label_garaje.setGeometry(5, 110, 400, 300) + pixmap = QPixmap(os.path.join( + self.btn_path, f"{country_garaje} garaje.png")) + label_garaje.setPixmap(pixmap) + label_garaje.show() + label_garaje.setScaledContents(True) + self.additional_labels.append(label_garaje) + + editar_profile = {"profile_number": profile_number, + "name": name, "protocol": protocol} + self.add_selected_profile(editar_profile) + + self.current_profile_location = location + + def show_profile_verification(self, profile_id): + profile = ProfileController.get(profile_id) + if not profile or not profile.location: + return + + location_key = f'{profile.location.country_code}_{profile.location.code}' + location_info = self.connection_manager.get_location_info(location_key) + if not location_info: + return + + location_icon_name = None + btn_path = self.btn_path + + for icon_file in os.listdir(btn_path): + if icon_file.startswith('button_') and icon_file.endswith('.png'): + icon_name = icon_file.replace( + 'button_', '').replace('.png', '') + test_loc = self.connection_manager.get_location_info(icon_name) + if test_loc and test_loc.country_code == location_info.country_code and test_loc.code == location_info.code: + location_icon_name = icon_name + break + + if location_icon_name: + verification_page = LocationVerificationPage( + self.page_stack, self.custom_window, location_icon_name, self) + self.page_stack.addWidget(verification_page) + self.page_stack.setCurrentIndex( + self.page_stack.indexOf(verification_page)) + + def launch(self): + pass + + def connect(self): + self.boton_just.setEnabled(False) + self.boton_just_session.setEnabled(False) + + profile = ProfileController.get(int(self.reverse_id)) + is_tor = self.update_status.get_current_connection() == 'tor' + if profile.connection.code == 'tor' and not profile.application_version.installed and not is_tor: + message = f'You are using a Tor profile, but the associated browser is not installed. If you want the browser to be downloaded with Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.' + + else: + self.get_billing_code_by_id() + return + + self.popup = self._make_confirmation_popup(message, button_text='Proceed') + self.popup.finished.connect( + lambda result: self._handle_popup_result(result)) + self.popup.show() + + def _handle_popup_result(self, result, change_screen=False): + if result: + if change_screen: + self._route_to_billing_entry() + else: + self.get_billing_code_by_id() + else: + self.popup.close() + + def _route_to_billing_entry(self): + try: + unused = get_unused_tickets(ticket_observer) + random_setting, _ = do_we_use_a_random_ticket(ticket_observer) + except Exception: + unused = {'valid': False} + random_setting = None + + has_tickets = isinstance(unused, dict) and unused.get('valid') and len(unused.get('data', [])) > 0 + random_off = random_setting is None + + if has_tickets and random_off: + chooser = self.custom_window.navigator.get_cached("ticket_or_billing_choice") + if chooser: + chooser.populate() + self.page_stack.setCurrentWidget(chooser) + return + + id_page = self.custom_window.navigator.get_cached("id") + if id_page: + self.page_stack.setCurrentWidget(id_page) + else: + self.custom_window.navigator.navigate("id") + + def disconnect(self): + self.disconnect_button.setEnabled(False) + self.disconnect_system_wide_button.setEnabled(False) + self.update_status.update_status('Disconnecting...') + profile_data = { + 'id': int(self.reverse_id) + } + profile = ProfileController.get(int(self.reverse_id)) + is_session_profile = isinstance(profile, SessionProfile) + if not is_session_profile: + connected_profiles = self.connection_manager.get_connected_profiles() + if len(connected_profiles) > 1: + message = f'Profile{"" if len(connected_profiles) == 1 else "s"} {", ".join(str(profile_id) for profile_id in connected_profiles)} are still connected.\nAll connected session profiles will be disconnected. \nDo you want to proceed?' + self._show_disconnect_confirmation( + connected_profiles, button_text='Disable', message=message) + self.disconnect_button.setEnabled(True) + self.disconnect_system_wide_button.setEnabled(True) + return + self.worker_thread = WorkerThread( + 'DISABLE_PROFILE', profile_data=profile_data) + self.worker_thread.finished.connect(self.on_disconnect_done) + self.worker_thread.start() + + def on_disconnect_done(self): + self.disconnect_button.setEnabled(True) + self.disconnect_system_wide_button.setEnabled(True) + pass + + def _config_flag(self, section, key, default=False): + try: + config = self.update_status._load_gui_config() + if config and section in config: + return config[section].get(key, default) + except Exception: + pass + return default + + def create_prof(self): + fast_mode = self._config_flag("registrations", "fast_registration_enabled") + if fast_mode: + if not self.connection_manager.is_synced(): + self.update_status.update_status('Syncing in progress..') + self.update_status.sync() + self.update_status.worker_thread.sync_output.connect( + self.on_sync_complete_for_fast_registration) + else: + self.custom_window.navigator.navigate("fast_registration") + return + + if not self.connection_manager.is_synced(): + self.custom_window.navigator.navigate("sync_screen") + else: + self.custom_window.navigator.navigate("protocol") + + def on_sync_complete_for_fast_registration(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): + if status: + self.custom_window.navigator.navigate("fast_registration") + else: + self.update_status.update_status( + 'Sync failed. Please try again later.') + + def change_connect_button(self): + profile = ProfileController.get(int(self.reverse_id)) + is_connected = self.connection_manager.is_profile_connected( + int(self.reverse_id)) + is_session_profile = isinstance(profile, SessionProfile) + + self.update_button_visibility(is_connected, is_session_profile) + self.update_status_message(profile, is_connected) + + def update_button_visibility(self, is_connected, is_session_profile): + self.boton_just_session.setVisible( + not is_connected and is_session_profile) + self.boton_just.setVisible(not is_connected and not is_session_profile) + self.disconnect_button.setVisible(is_connected and is_session_profile) + self.disconnect_system_wide_button.setVisible( + is_connected and not is_session_profile) + + def update_status_message(self, profile, is_connected): + if is_connected: + message = Worker.generate_profile_message(profile, is_enabled=True) + self.update_status.enable_marquee(message) + else: + message = Worker.generate_profile_message( + profile, is_enabled=True, idle=True) + self.update_status.update_status(message) + + def edit_prof(self): + auto_sync = self._config_flag("registrations", "auto_sync_enabled") + if auto_sync and not self.connection_manager.is_synced(): + self.update_status.update_status('Syncing in progress..') + self.update_status.sync() + self.update_status.worker_thread.sync_output.connect( + self.on_sync_complete_for_edit_profile) + else: + self.custom_window.navigator.navigate("editor") + + def on_sync_complete_for_edit_profile(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): + if status: + self.custom_window.navigator.navigate("editor") + else: + self.update_status.update_status( + 'Sync failed. Please try again later.') + + def settings_gui(self): + self.custom_window.navigator.navigate("settings") + + def eliminacion(self): + current_state = { + 'is_tor_mode': self.is_tor_mode, + 'is_animating': self.is_animating, + 'animation_step': self.animation_step + } + + self.main_window.toggle_bg.setParent(None) + self.main_window.toggle_handle.setParent(None) + self.main_window.on_label.setParent(None) + self.main_window.off_label.setParent(None) + + self.refresh_menu_buttons() + + self.main_window.toggle_bg.setParent(self.main_window.toggle_button) + self.main_window.toggle_handle.setParent( + self.main_window.toggle_button) + self.main_window.on_label.setParent(self.main_window.toggle_button) + self.main_window.off_label.setParent(self.main_window.toggle_button) + + self.main_window.toggle_bg.show() + self.main_window.toggle_handle.show() + self.main_window.on_label.show() + self.main_window.off_label.show() + + self.is_tor_mode = current_state['is_tor_mode'] + self.is_animating = current_state['is_animating'] + self.animation_step = current_state['animation_step'] + + self.create() + + def get_billing_code_by_id(self, force: bool = False): + profile_id = self.update_status.current_profile_id + profile_data = { + 'id': profile_id, + 'force': force + } + self.update_status.update_status('Attempting to connect...') + + self.enabling_profile(profile_data) + + def enabling_profile(self, profile_data): + self.worker = Worker(profile_data) + self.worker.update_signal.connect(self.update_gui_main_thread) + self.worker.change_page.connect(self.change_app_page) + self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup) + + thread = threading.Thread(target=self.worker.run) + thread.start() + + def show_ticket_data_loss_popup(self, ticket_number, billing_code): + self.custom_window.navigator.navigate("menu") + self.update_status.update_status('Critical: invalid billing code from ticket.') + self.popup = TicketDataLossPopup( + self, + ticket_number=ticket_number, + billing_code=billing_code, + ) + self.popup.show() + + def DisplayInstallScreen(self, package_name): + self.custom_window.navigator.navigate("install_system_package") + install_page = self.custom_window.navigator.get_cached("install_system_package") + if install_page is not None: + install_page.configure(package_name=package_name, distro='debian') + + def handle_enable_force_result(self, result): + if result: + profile_data = {'id': self.reverse_id, + 'ignore_profile_state_conflict': True} + if hasattr(self, '_pending_endpoint_verification_ignore') and self._pending_endpoint_verification_ignore: + profile_data['ignore_endpoint_verification'] = True + self.enabling_profile(profile_data) + else: + pass + if hasattr(self, '_pending_profile_state_conflict_ignore'): + delattr(self, '_pending_profile_state_conflict_ignore') + if hasattr(self, '_pending_endpoint_verification_ignore'): + delattr(self, '_pending_endpoint_verification_ignore') + self.popup.close() + + def handle_endpoint_verification_result(self, result, action, profile_id): + if action == "continue": + profile_data = {'id': profile_id, + 'ignore_endpoint_verification': True} + if hasattr(self, '_pending_profile_state_conflict_ignore') and self._pending_profile_state_conflict_ignore: + profile_data['ignore_profile_state_conflict'] = True + self.enabling_profile(profile_data) + elif action == "sync": + self.update_status.update_status("Syncing...") + self.worker_thread = WorkerThread('SYNC') + self.worker_thread.finished.connect( + lambda: self.handle_sync_after_verification(profile_id)) + self.worker_thread.sync_output.connect( + self.update_status.update_values) + self.worker_thread.start() + else: + self.update_status.update_status("Profile enable aborted") + if hasattr(self, 'popup'): + self.popup.close() + + def handle_sync_after_verification(self, profile_id): + profile_data = {'id': profile_id} + if hasattr(self, '_pending_profile_state_conflict_ignore') and self._pending_profile_state_conflict_ignore: + profile_data['ignore_profile_state_conflict'] = True + self.enabling_profile(profile_data) + + def update_gui_main_thread(self, text, is_enabled, profile_id, profile_type, profile_connection): + if text == "ENDPOINT_VERIFICATION_ERROR": + message = "Operator verification failed. Do you want to continue anyway?" + self.popup = EndpointVerificationPopup(self, message=message) + self.popup.finished.connect( + lambda result, action: self.handle_endpoint_verification_result(result, action, profile_id)) + self.popup.show() + return + + if text == "PROFILE_STATE_CONFLICT_ERROR": + message = f'The profile is already enabled. Do you want to force enable it anyway?' + self._pending_profile_state_conflict_ignore = True + self.popup = self._make_confirmation_popup(message, button_text='Proceed') + self.popup.finished.connect( + lambda result: self.handle_enable_force_result(result)) + self.popup.show() + return + + if profile_id < 0: + self.DisplayInstallScreen(text) + return + if is_enabled: + self.update_status.enable_marquee(str(text)) + else: + self.update_status.update_status(str(text)) + if profile_id is not None: + self.on_finished_update_gui( + is_enabled, profile_id, profile_type, profile_connection) + self.boton_just.setEnabled(True) + self.boton_just_session.setEnabled(True) + + def change_app_page(self, text, Is_changed): + self.update_status.update_status(str(text)) + if Is_changed: + current_connection = self.update_status.get_current_connection() + profile = ProfileController.get(int(self.reverse_id)) + if current_connection != 'tor' and profile.connection.code == 'tor': + message = f'You are using a Tor profile, but the profile subscription is missing or expired. If you want the billing to be done thourgh Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.' + self.popup = self._make_confirmation_popup(message, button_text='Proceed') + self.popup.finished.connect( + lambda result: self._handle_popup_result(result, True)) + self.popup.show() + else: + self._route_to_billing_entry() + + self.boton_just.setEnabled(True) + self.boton_just_session.setEnabled(True) + + def on_finished_update_gui(self, is_enabled, profile_id, profile_type, profile_connection): + try: + if is_enabled: + self.connection_manager.add_connected_profile(profile_id) + target_button: QPushButton = self.connection_manager.get_profile_button_objects( + profile_id) + + if profile_type == 1: + if target_button: + icon_path = self.connected_tor_profile if profile_connection == 'tor' else self.connected_profile + try: + target_button.setIcon(QIcon(icon_path)) + except RuntimeError: + pass + self.button_states[profile_id] = icon_path + else: + if target_button: + try: + target_button.setIcon( + QIcon(self.system_wide_connected_profile)) + except RuntimeError: + pass + self.update_status.update_image( + appearance_value='background_connected') + self.button_states[profile_id] = self.system_wide_connected_profile + self.IsSystem += 1 + + if target_button: + self.show_disconnect_button( + True, profile_type, target_button) + self.boton_edit.setEnabled(False) + + if profile_id == self.reverse_id: + profile_obj = ProfileController.get(profile_id) + if profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard' and ConfigurationController.get_endpoint_verification_enabled(): + self.print_profile_details(f"Profile_{profile_id}") + else: + self.connection_manager.remove_connected_profile(profile_id) + if profile_type == 1: + if profile_id in self.button_states: + del self.button_states[profile_id] + else: + self.IsSystem -= 1 + if profile_id in self.button_states: + del self.button_states[profile_id] + if self.IsSystem == 0: + self.update_status.update_image() + + self.boton_edit.setEnabled(True) + + if profile_id == self.reverse_id: + self.print_profile_details(f"Profile_{profile_id}") + + self.refresh_menu_buttons() + except IndexError: + self.update_status.update_status( + 'An error occurred while updating profile state.') diff --git a/gui/v2/ui/pages/payment_confirmed_page.py b/gui/v2/ui/pages/payment_confirmed_page.py new file mode 100755 index 0000000..34e1df1 --- /dev/null +++ b/gui/v2/ui/pages/payment_confirmed_page.py @@ -0,0 +1,149 @@ +import os + +from PyQt6.QtWidgets import QApplication, QButtonGroup, QFrame, QLabel, QPushButton +from PyQt6.QtGui import QIcon, QPainter +from PyQt6.QtCore import Qt, QPointF, QRect, QSize, QTimer + +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.widgets.clickable_label import ClickableLabel +from gui.v2.ui.widgets.confetti import ConfettiThread + + +class PaymentConfirmed(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Id", page_stack, main_window, parent) + + self.update_status = main_window + self.buttonGroup = QButtonGroup(self) + self.display.setGeometry(QRect(-10, 50, 550, 460)) + + self.button_reverse.setVisible(False) + + self.confetti = [] + + self.icon_label = QLabel(self) + icon = QIcon(os.path.join(self.btn_path, "check_icon.png")) + pixmap = icon.pixmap(QSize(64, 64)) + self.icon_label.setPixmap(pixmap) + self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.label = QLabel("Payment Completed!", + alignment=Qt.AlignmentFlag.AlignCenter, parent=self) + self.label.setStyleSheet( + "font-size: 24px; color: #2ecc71; font-weight: bold;") + + self.billing_label = QLabel( + "Your billing code:", alignment=Qt.AlignmentFlag.AlignCenter, parent=self) + self.billing_label.setStyleSheet("font-size: 18px; color: white;") + + self.billing_code = ClickableLabel( + "", alignment=Qt.AlignmentFlag.AlignCenter, parent=self) + self.billing_code.setStyleSheet(""" + font-size: 24px; + color: white; + font-weight: bold; + padding: 5px; + border-radius: 5px; + """) + self.billing_code.clicked.connect(self.copy_billing_code) + self.billing_code.set_hover_width(450) + + self.top_line = QFrame(self) + self.top_line.setFrameShape(QFrame.Shape.HLine) + self.top_line.setStyleSheet("color: #bdc3c7;") + + self.bottom_line = QFrame(self) + self.bottom_line.setFrameShape(QFrame.Shape.HLine) + self.bottom_line.setStyleSheet("color: #bdc3c7;") + + self.next_button = QPushButton("Next", self) + self.next_button.setStyleSheet(""" + QPushButton { + background-color: #3498db; + color: white; + border: none; + padding: 10px; + font-size: 18px; + border-radius: 5px; + } + QPushButton:hover { + background-color: #2980b9; + } + """) + self.next_button.clicked.connect(self.on_next_button) + + self.confetti_thread = ConfettiThread(self.width(), self.height()) + self.confetti_thread.update_signal.connect(self.update_confetti) + self.confetti_thread.start() + + def copy_billing_code(self): + clipboard = QApplication.clipboard() + clipboard.setText(self.billing_code.text()) + + original_style = self.billing_code.styleSheet() + + self.billing_code.setText("Copied!") + self.billing_code.setStyleSheet( + original_style + "background-color: #27ae60;") + + QTimer.singleShot( + 1000, lambda: self.reset_billing_code_style(original_style)) + + def reset_billing_code_style(self, original_style): + self.billing_code.setStyleSheet(original_style) + self.billing_code.setText(self.current_billing_code) + + def set_billing_code(self, code): + self.current_billing_code = code + self.billing_code.setText(code) + + def update_confetti(self, confetti): + self.confetti = confetti + self.update() + + def paintEvent(self, event): + super().paintEvent(event) + + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + for particle in self.confetti: + painter.setBrush(particle.color) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawEllipse(QPointF(particle.x, particle.y), + particle.size, particle.size) + + painter.end() + + def resizeEvent(self, event): + super().resizeEvent(event) + width = event.size().width() + height = event.size().height() + + self.icon_label.setGeometry( + QRect(width // 2 - 32, height // 8, 64, 64)) + self.label.setGeometry(QRect(0, height // 4, width, 50)) + self.billing_label.setGeometry(QRect(0, height // 2 - 50, width, 30)) + self.top_line.setGeometry( + QRect(width // 4, height // 2 + 20, width // 2, 1)) + self.billing_code.setGeometry(QRect(0, height // 2 + 28, width, 40)) + self.bottom_line.setGeometry( + QRect(width // 4, height // 2 + 75, width // 2, 1)) + self.next_button.setGeometry( + QRect(width // 4, height * 3 // 4, width // 2, 50)) + + self.confetti_thread.update_dimensions(width, height) + + def closeEvent(self, event): + self.confetti_thread.stop() + self.confetti_thread.wait() + super().closeEvent(event) + + def on_next_button(self): + self.custom_window.navigator.navigate("menu") + + def find_menu_page(self): + return self.custom_window.navigator.get_cached("menu") + + def reverse(self): + self.custom_window.navigator.navigate("wireguard") diff --git a/gui/v2/ui/pages/payment_details_page.py b/gui/v2/ui/pages/payment_details_page.py new file mode 100755 index 0000000..18cdf24 --- /dev/null +++ b/gui/v2/ui/pages/payment_details_page.py @@ -0,0 +1,419 @@ +import os + +from PyQt6.QtWidgets import QApplication, QLabel, QLineEdit, QPushButton +from PyQt6.QtGui import QIcon, QPixmap +from PyQt6.QtCore import QSize, QTimer +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.popups.qrcode_dialog import QRCodeDialog +from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread +from gui.v2.workers.worker_thread import WorkerThread + + +class PaymentDetailsPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Payment Details", page_stack, main_window, parent) + self.update_status = main_window + self.text_fields = [] + self.invoice_data = {} + self.selected_duration = None + self.selected_currency = None + + self.ticketing = False + self.temp_billing_code = None + self._ticket_check_running = False + self._ticket_worker = None + self.ticket_poll_timer = QTimer(self) + self.ticket_poll_timer.timeout.connect(self._tick_check_paid) + + self.create_interface_elements() + self.button_reverse.setVisible(True) + + def create_interface_elements(self): + self.title = QLabel("Payment Details", self) + self.title.setGeometry(QtCore.QRect(530, 30, 210, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.button_reverse.clicked.connect(self.reverse) + + self.qr_code_button = QPushButton(self) + self.qr_code_button.setGeometry(360, 435, 50, 50) + qr_icon = QIcon(os.path.join(self.btn_path, "qr-code.png")) + self.qr_code_button.setIcon(qr_icon) + self.qr_code_button.setIconSize(self.qr_code_button.size()) + self.qr_code_button.clicked.connect(self.show_qr_code) + self.qr_code_button.setToolTip("Show QR Code") + self.qr_code_button.setDisabled(True) + + labels_info = [ + ("Your new billing ID", None, (20, 40), (240, 50)), + ("", "cuadro400x50", (20, 90), (400, 50)), + ("Duration", None, (20, 155), (150, 50)), + ("", "cuadro150x50", (200, 155), (150, 50)), + ("Amount", None, (10, 220), (150, 50)), + ("", "cuadro150x50", (200, 220), (150, 50)), + ("Hit this white icon to copy paste the address:", + None, (15, 305), (500, 30)), + ("", "cuadro400x50", (20, 350), (400, 50)), + ("Hit this icon to Scan the QR code:", None, (15, 450), (360, 30)), + ] + + for text, image_name, position, size in labels_info: + label = QLabel(self) + label.setGeometry(position[0], position[1], size[0], size[1]) + + if "Hit" in text: + label.setStyleSheet("font-size: 16px;") + label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter) + else: + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + if image_name: + pixmap = QPixmap(os.path.join( + self.btn_path, f"{image_name}.png")) + label.setPixmap(pixmap) + label.setScaledContents(True) + else: + label.setText(text) + + line_edit_info = [ + (20, 90, 400, 50), + (200, 155, 150, 50), + (200, 220, 150, 50), + (20, 350, 400, 50), + ] + + for x, y, width, height in line_edit_info: + line_edit = QLineEdit(self) + line_edit.setGeometry(x, y, width, height) + line_edit.setReadOnly(True) + self.text_fields.append(line_edit) + line_edit.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + copy_button_info = [ + (430, 90, 0), + (360, 220, 2), + (480, 290, 3), + ] + + for x, y, field_index in copy_button_info: + button = QPushButton(self) + button.setGeometry(x, y, 50, 50) + icon = QIcon(os.path.join(self.btn_path, "paste_button.png")) + button.setIcon(icon) + button.setIconSize(button.size()) + button.clicked.connect( + lambda checked, index=field_index: self.copy_text(index)) + + currency_button_info = [ + ("monero", (545, 75)), + ("bitcoin", (545, 290)), + ("lightnering", (545, 180)), + ("litecoin", (545, 395)) + ] + + self.currency_display_buttons = [] + for icon_name, position in currency_button_info: + button = QPushButton(self) + button.setGeometry(position[0], position[1], 185, 75) + button.setIconSize(QSize(190, 120)) + button.setCheckable(True) + button.setEnabled(False) + self.currency_display_buttons.append(button) + button.setIcon( + QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + + def fetch_invoice_duration(self): + duration_month_num = int(self.selected_duration.split()[0]) + if duration_month_num == 12: + total_hours = 365 * 24 + else: + total_hours = duration_month_num * (30 * 24) + return total_hours + + def display_selected_options(self): + if self.selected_duration: + self.text_fields[1].setText(self.selected_duration) + self.text_fields[1].setStyleSheet('font-size: 13px;') + + if self.selected_currency: + currency_index_map = { + "monero": 0, + "bitcoin": 1, + "lightning": 2, + "litecoin": 3 + } + currency_index = currency_index_map.get(self.selected_currency, -1) + if currency_index >= 0: + for i, button in enumerate(self.currency_display_buttons): + button.setChecked(i == currency_index) + button.setEnabled(i == currency_index) + + def on_request_invoice(self): + total_hours = self.fetch_invoice_duration() + if self.selected_currency is None: + self.update_status.update_status('No currency selected') + return + + profile_data = { + 'id': int(self.update_status.current_profile_id), + 'duration': total_hours, + 'currency': 'xmr' if self.selected_currency == 'monero' else 'btc' if self.selected_currency == 'bitcoin' else 'btc-ln' if self.selected_currency == 'lightning' else 'ltc' if self.selected_currency == 'litecoin' else None + } + + self.update_status.update_status('Generating Invoice...') + self.worker_thread = WorkerThread( + 'GET_SUBSCRIPTION', profile_data=profile_data) + self.worker_thread.text_output.connect(self.invoice_update_text_output) + self.worker_thread.invoice_output.connect( + self.on_invoice_generation_finished) + self.worker_thread.invoice_finished.connect(self.on_invoice_finished) + self.worker_thread.start() + + def invoice_update_text_output(self, text): + self.update_status.update_status(text) + + if 'No compatible' in text: + for line in self.text_fields: + line.setText('') + + def store_invoice_data(self, billing_details: dict, duration: int): + self.invoice_data = { + 'billing_details': billing_details, + 'duration': duration + } + + def check_invoice(self): + self.display_selected_options() + total_hours = self.fetch_invoice_duration() + if self.invoice_data and self.invoice_data['duration'] == total_hours: + self.update_status.update_status('Checking invoice status...') + self.check_invoice_status( + self.invoice_data['billing_details'].billing_code) + else: + self.on_request_invoice() + + def check_invoice_status(self, billing_code: str): + self.worker_thread = WorkerThread('CHECK_INVOICE_STATUS', profile_data={ + 'billing_code': billing_code}) + self.worker_thread.invoice_finished.connect( + self.on_invoice_status_finished) + self.worker_thread.start() + + def on_invoice_status_finished(self, result): + if result: + self.parse_invoice_data(self.invoice_data['billing_details']) + else: + self.update_status.update_status( + 'Invoice has expired. Generating new invoice...') + self.on_request_invoice() + + def on_invoice_generation_finished(self, billing_details: object, text: str): + total_hours = self.fetch_invoice_duration() + self.store_invoice_data(billing_details, duration=total_hours) + self.parse_invoice_data(billing_details) + + def parse_invoice_data(self, invoice_data: object): + billing_details = { + 'billing_code': invoice_data.billing_code + } + + if self.selected_currency.lower() == 'lightning': + currency = 'Bitcoin Lightning' + else: + currency = self.selected_currency + + preferred_method = next( + (pm for pm in invoice_data.payment_methods if pm.name.lower() == currency.lower()), None) + + if preferred_method: + billing_details['due_amount'] = preferred_method.due + billing_details['address'] = preferred_method.address + else: + self.update_status.update_status( + 'No payment method found for the selected currency.') + return + + billing_values = list(billing_details.values()) + text_field_indices = [0, 2, 3] + + for i, dict_value in enumerate(billing_values): + if i < 3: + field_index = text_field_indices[i] + if i == 2: + text = str(dict_value) + self.full_address = text + metrics = self.text_fields[field_index].fontMetrics() + width = self.text_fields[field_index].width() + elided_text = metrics.elidedText( + text, QtCore.Qt.TextElideMode.ElideMiddle, width) + self.text_fields[field_index].setText(elided_text) + elif i == 1: + text = str(dict_value) + self.text_fields[field_index].setProperty("fullText", text) + self.text_fields[field_index].setText(text) + font_size = 14 + if len(text) > 9: + font_size = 15 + if len(text) > 13: + font_size = 10 + if len(text) > 16: + font_size = 10 + self.text_fields[field_index].setStyleSheet( + f"font-size: {font_size}px; font-weight: bold;") + else: + self.text_fields[field_index].setProperty( + "fullText", str(dict_value)) + self.text_fields[field_index].setText(str(dict_value)) + + self.qr_code_button.setDisabled(False) + self.update_status.update_status( + 'Invoice generated. Awaiting payment...') + + def on_invoice_finished(self, result): + if result: + self.invoice_data.clear() + self.show_payment_confirmed(self.text_fields[0].text()) + return + self.update_status.update_status( + 'An error occurred when generating invoice') + + def show_payment_confirmed(self, billing_code): + self.custom_window.navigator.navigate("payment_confirmed") + payment_page = self.custom_window.navigator.get_cached("payment_confirmed") + if payment_page is not None: + for line in self.text_fields: + line.setText('') + payment_page.set_billing_code(billing_code) + else: + print("PaymentConfirmed page not found") + + def find_payment_confirmed_page(self): + return self.custom_window.navigator.get_cached("payment_confirmed") + + def copy_text(self, field: int): + try: + original_status = self.update_status.status_label.text() + original_status = original_status.replace("Status: ", "") + + if int(field) == 3: + text = self.full_address + else: + text = self.text_fields[int(field)].text() + QApplication.clipboard().setText(text) + + if field == 0: + self.update_status.update_status( + 'Billing code copied to clipboard!') + elif field == 3: + self.update_status.update_status( + 'Address copied to clipboard!') + else: + self.update_status.update_status( + 'Pay amount copied to clipboard!') + + QTimer.singleShot( + 2000, lambda: self.update_status.update_status(original_status)) + except AttributeError: + self.update_status.update_status( + 'No content available for copying') + except Exception as e: + self.update_status.update_status( + f'An error occurred when copying the text') + + def reverse(self): + if self.ticket_poll_timer.isActive(): + self.ticket_poll_timer.stop() + if self.ticketing: + self.ticketing = False + self.custom_window.navigator.navigate("ticket_crypto_picker") + return + self.custom_window.navigator.navigate("currency_selection") + + def set_ticket_invoice(self, invoice, plan_key): + self.ticketing = True + self.temp_billing_code = getattr(invoice, 'temp_billing_code', None) + raw_currency = getattr(invoice, 'selected_currency', None) or '' + self.selected_currency = self._normalize_currency(raw_currency) + self.selected_duration = f"Plan {plan_key}" if plan_key else "Plan" + self._populate_ticket_fields(invoice) + if self.ticket_poll_timer.isActive(): + self.ticket_poll_timer.stop() + self.ticket_poll_timer.start(3000) + self.update_status.update_status('Awaiting 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) + self.text_fields[1].setStyleSheet('font-size: 13px;') + amount = getattr(invoice, 'due_amount', '') or '' + self.text_fields[2].setText(str(amount)) + addr = str(getattr(invoice, 'address', '') or '') + self.full_address = addr + metrics = self.text_fields[3].fontMetrics() + width = self.text_fields[3].width() + elided = metrics.elidedText(addr, QtCore.Qt.TextElideMode.ElideMiddle, width) + self.text_fields[3].setText(elided) + currency_index_map = {'monero': 0, 'bitcoin': 1, 'lightning': 2, 'litecoin': 3} + idx = currency_index_map.get(self.selected_currency, -1) + if idx >= 0: + for i, btn in enumerate(self.currency_display_buttons): + btn.setChecked(i == idx) + btn.setEnabled(i == idx) + self.qr_code_button.setDisabled(False) + + def _normalize_currency(self, raw): + if not raw: + return None + raw = str(raw).lower() + m = { + 'monero': 'monero', 'xmr': 'monero', + 'bitcoin': 'bitcoin', 'btc': 'bitcoin', + 'lightning': 'lightning', 'btc-ln': 'lightning', 'ln': 'lightning', + 'litecoin': 'litecoin', 'ltc': 'litecoin', + } + return m.get(raw, raw) + + def _tick_check_paid(self): + if not self.temp_billing_code or self._ticket_check_running: + return + self._ticket_check_running = True + self._ticket_worker = TicketingWorkerThread('CHECK_PAID', params={ + 'temp_billing_code': self.temp_billing_code + }) + self._ticket_worker.paid.connect(self._on_ticket_paid) + self._ticket_worker.not_paid.connect(self._on_ticket_not_paid) + self._ticket_worker.paid_check_failed.connect(self._on_ticket_check_failed) + self._ticket_worker.error.connect(self._on_ticket_check_failed) + self._ticket_worker.finished.connect(self._on_ticket_check_done) + self._ticket_worker.start() + + def _on_ticket_check_done(self): + self._ticket_check_running = False + + def _on_ticket_paid(self): + self.ticket_poll_timer.stop() + self.ticketing = False + self.update_status.update_status('Payment received.') + self.custom_window.navigator.navigate("ticket_prep") + prep_page = self.custom_window.navigator.get_cached("ticket_prep") + if prep_page is not None: + prep_page.start_prep() + + def _on_ticket_not_paid(self): + self.update_status.update_status('Awaiting ticket payment...') + + def _on_ticket_check_failed(self, msg): + self.update_status.update_status(f'Payment check failed: {msg}') + + def show_qr_code(self): + full_amount = self.text_fields[2].text() + if hasattr(self, 'full_address') and self.full_address: + currency_type = getattr(self, 'selected_currency', None) + qr_dialog = QRCodeDialog( + self.full_address, full_amount, currency_type, self) + qr_dialog.exec() + else: + self.update_status.update_status( + 'No address available for QR code') diff --git a/gui/v2/ui/pages/plan_picker_page.py b/gui/v2/ui/pages/plan_picker_page.py new file mode 100755 index 0000000..2a51ec0 --- /dev/null +++ b/gui/v2/ui/pages/plan_picker_page.py @@ -0,0 +1,101 @@ +from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.styles.styles import TERMINAL_LIST_QSS +from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread + + +class PlanPickerPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("PickPlan", page_stack, main_window, parent) + self.update_status = main_window + self.pricing_data = None + self.worker = None + + self.title.setText("Pick a Plan") + self.title.setGeometry(QtCore.QRect(290, 30, 220, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.button_reverse.setVisible(True) + self.button_reverse.clicked.connect(self.reverse) + + self.intro_label = QLabel( + "All members of the group expire at the same time. How much you pay depends on when you join.", + self) + self.intro_label.setGeometry(40, 90, 720, 40) + self.intro_label.setStyleSheet("color: white; font-size: 13px;") + self.intro_label.setWordWrap(True) + self.intro_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.plan_list = QListWidget(self) + self.plan_list.setGeometry(60, 140, 680, 320) + self.plan_list.setStyleSheet(TERMINAL_LIST_QSS) + self.plan_list.itemClicked.connect(self.on_plan_clicked) + + self.status_label = QLabel("", self) + self.status_label.setGeometry(60, 470, 680, 30) + self.status_label.setStyleSheet("color: #cccccc; font-size: 13px;") + self.status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + def start_sync(self): + self.plan_list.clear() + self.status_label.setText("Syncing prices...") + self.update_status.update_status("Syncing ticket prices...") + self.worker = TicketingWorkerThread('SYNC_PRICES') + self.worker.sync_done.connect(self.on_sync_done) + self.worker.error.connect(self.on_error) + self.worker.start() + + def on_sync_done(self, pricing_data): + if isinstance(pricing_data, dict) and pricing_data.get('valid') is False: + err = pricing_data.get('error_code', 'unknown') + self.status_label.setText(f"Sync failed: {err}") + self.update_status.update_status(f"Sync failed: {err}") + return + + if isinstance(pricing_data, dict) and isinstance(pricing_data.get('data'), dict): + plans = pricing_data['data'] + elif isinstance(pricing_data, dict): + plans = pricing_data + else: + self.status_label.setText("Unexpected pricing data.") + return + + self.pricing_data = plans + self.populate_plans(plans) + self.status_label.setText("Pick a plan above.") + self.update_status.update_status("Plans loaded.") + + def populate_plans(self, pricing_data): + self.plan_list.clear() + if not isinstance(pricing_data, dict): + self.status_label.setText("Unexpected pricing data.") + return + for plan_key, plan_data in pricing_data.items(): + if not isinstance(plan_data, dict): + continue + cost = plan_data.get('cost', '?') + months = plan_data.get('months', '?') + days = plan_data.get('days', '?') + profiles = plan_data.get('profiles', '?') + label = f" Plan {plan_key} | €{cost} | {months}months {days}d left | {profiles} profiles" + item = QListWidgetItem(label) + item.setData(QtCore.Qt.ItemDataRole.UserRole, plan_key) + self.plan_list.addItem(item) + + def on_plan_clicked(self, item): + plan_key = item.data(QtCore.Qt.ItemDataRole.UserRole) + if not plan_key: + return + self.custom_window.navigator.navigate("ticket_crypto_picker") + crypto_page = self.custom_window.navigator.get_cached("ticket_crypto_picker") + if crypto_page is not None: + crypto_page.set_selected_plan(plan_key) + + def on_error(self, msg): + self.status_label.setText(f"Error: {msg}") + self.update_status.update_status(f"Sync error: {msg}") + + def reverse(self): + self.custom_window.navigator.navigate("id") diff --git a/gui/v2/ui/pages/policy_suggestion_page.py b/gui/v2/ui/pages/policy_suggestion_page.py new file mode 100755 index 0000000..d2958cd --- /dev/null +++ b/gui/v2/ui/pages/policy_suggestion_page.py @@ -0,0 +1,136 @@ +from PyQt6.QtWidgets import QLabel, QPushButton, QTextEdit + +from core.controllers.PolicyController import PolicyController +from core.Errors import ( + CommandNotFoundError, + PolicyAssignmentError, + PolicyInstatementError, +) + +from gui.v2.ui.pages.Page import Page + + +class PolicySuggestionPage(Page): + def __init__(self, page_stack, main_window=None, parent=None, policy_type='capability'): + super().__init__("PolicySuggestion", page_stack, main_window, parent) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.policy_type = policy_type + self.policy = PolicyController.get(policy_type) + self.button_back.setVisible(False) + self.button_next.setVisible(False) + self.button_go.setVisible(False) + self.status_label = QLabel(self) + self.status_label.setGeometry(80, 430, 640, 40) + self.status_label.setStyleSheet("font-size: 14px; color: cyan;") + self.setup_ui() + + def setup_ui(self): + policy_name = "Capability" if self.policy_type == 'capability' else "Privilege" + self.title.setGeometry(20, 50, 760, 40) + self.title.setText(f"Policy Suggestion: {policy_name} Policy") + + description = QLabel(self) + description.setGeometry(80, 100, 640, 60) + description.setWordWrap(True) + description.setStyleSheet("font-size: 14px; color: cyan;") + description.setText( + f"A {policy_name.lower()} policy is available and can be instated to improve functionality. Review the policy details below before proceeding.") + + preview_label = QLabel(self) + preview_label.setGeometry(80, 170, 640, 30) + preview_label.setStyleSheet( + "font-size: 13px; color: white; font-weight: bold;") + preview_label.setText("Policy Preview:") + + preview_text = QTextEdit(self) + preview_text.setGeometry(80, 200, 640, 150) + preview_text.setReadOnly(True) + preview_text.setStyleSheet(""" + QTextEdit { + background-color: #1a1a1a; + color: #00ffff; + border: 1px solid #333; + border-radius: 4px; + font-family: monospace; + font-size: 11px; + padding: 5px; + } + """) + try: + preview_content = PolicyController.preview(self.policy) + preview_text.setText(preview_content) + except Exception as e: + preview_text.setText(f"Error loading preview: {str(e)}") + + yes_button = QPushButton(f"Instate {policy_name} Policy", self) + yes_button.setGeometry(170, 370, 250, 50) + yes_button.setStyleSheet(""" + QPushButton { + background: #007AFF; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: bold; + } + QPushButton:hover { + background: #0056CC; + } + """) + yes_button.clicked.connect(self.instate_policy) + + no_button = QPushButton("Skip", self) + no_button.setGeometry(440, 370, 120, 50) + no_button.setStyleSheet(""" + QPushButton { + background: #666; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: bold; + } + QPushButton:hover { + background: #555; + } + """) + no_button.clicked.connect(self.skip_policy) + + self.refresh_status() + + def refresh_status(self): + try: + if PolicyController.is_instated(self.policy): + self.status_label.setText( + f"Current status: {self.policy_type} policy is instated.") + self.status_label.setStyleSheet( + "font-size: 14px; color: #2ecc71;") + else: + self.status_label.setText( + f"Current status: {self.policy_type} policy is not instated.") + self.status_label.setStyleSheet( + "font-size: 14px; color: #e67e22;") + except Exception: + self.status_label.setText("Unable to determine policy status.") + self.status_label.setStyleSheet("font-size: 14px; color: #e67e22;") + + def instate_policy(self): + try: + PolicyController.instate(self.policy) + self.refresh_status() + policy_name = "Capability" if self.policy_type == 'capability' else "Privilege" + self.update_status.update_status(f"{policy_name} policy instated") + self.custom_window.navigator.navigate("menu") + except CommandNotFoundError as e: + self.status_label.setText(str(e)) + self.status_label.setStyleSheet("font-size: 14px; color: red;") + except (PolicyAssignmentError, PolicyInstatementError) as e: + self.status_label.setText(str(e)) + self.status_label.setStyleSheet("font-size: 14px; color: red;") + except Exception as e: + self.status_label.setText(f"Failed to instate policy: {str(e)}") + self.status_label.setStyleSheet("font-size: 14px; color: red;") + + def skip_policy(self): + self.custom_window.navigator.navigate("menu") diff --git a/gui/v2/ui/pages/protocol_page.py b/gui/v2/ui/pages/protocol_page.py new file mode 100755 index 0000000..a4d5b22 --- /dev/null +++ b/gui/v2/ui/pages/protocol_page.py @@ -0,0 +1,87 @@ +import os + +from PyQt6.QtWidgets import QPushButton, QLabel, QButtonGroup +from PyQt6.QtGui import QPixmap, QIcon +from PyQt6.QtCore import Qt +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class ProtocolPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Protocol", page_stack, main_window, parent) + self.main_window = main_window + self.selected_protocol = None + self.btn_path = main_window.btn_path + self.selected_protocol_icon = None + self.connection_manager = main_window.connection_manager + self.button_back.setVisible(True) + self.update_status = main_window + self.button_go.clicked.connect(self.go_selected) + self.coming_soon_label = QLabel("Coming soon", self) + self.coming_soon_label.setGeometry(210, 50, 200, 40) + self.coming_soon_label.setStyleSheet("font-size: 22px;") + + self.coming_soon_label.setVisible(False) + self.title.setGeometry(585, 40, 185, 40) + self.title.setText("Pick a Protocol") + self.display.setGeometry(QtCore.QRect(0, 50, 580, 435)) + self.create_interface_elements() + if self.connection_manager.is_synced(): + self.enable_protocol_buttons() + + def create_interface_elements(self): + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + self.selected_page_name = None + for j, (object_type, icon_name, page_name, geometry) in enumerate([ + (QPushButton, "wireguard", "wireguard", (585, 90, 185, 75)), + (QPushButton, "residential", "residential", (585, 90+30+75, 185, 75)), + (QPushButton, "hidetor", "hidetor", (585, 90+30+75+30+75, 185, 75)) + ]): + boton = object_type(self) + boton.setGeometry(*geometry) + boton.setIconSize(boton.size()) + boton.setCheckable(True) + boton.setDisabled(True) + boton.setIcon( + QIcon(os.path.join(self.btn_path, f"{icon_name}_button.png"))) + self.buttons.append(boton) + self.buttonGroup.addButton(boton, j) + boton.clicked.connect( + lambda _, name=page_name, protocol=icon_name: self.show_protocol(name, protocol)) + + def enable_protocol_buttons(self): + for button in self.buttons: + button.setDisabled(False) + + def update_swarp_json(self): + self.update_status.write_data( + {"protocol": self.selected_protocol_icon}) + + def show_protocol(self, page_name, protocol): + self.update_status.clear_data() + self.display.setPixmap(QPixmap(os.path.join(self.btn_path, f"{protocol}.png")).scaled( + self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + self.selected_protocol_icon = protocol + self.selected_page_name = page_name + + if protocol in ["wireguard", "hidetor"]: + self.button_go.setVisible(True) + self.coming_soon_label.setVisible(False) + else: + self.button_go.setVisible(False) + self.coming_soon_label.setVisible(True) + self.update_swarp_json() + + def go_selected(self): + if self.selected_page_name: + self.custom_window.navigator.navigate(self.selected_page_name) + self.display.clear() + for boton in self.buttons: + boton.setChecked(False) + self.button_go.setVisible(False) + + def find_menu_page(self): + return self.custom_window.navigator.get_cached("menu") diff --git a/gui/v2/ui/pages/residential_page.py b/gui/v2/ui/pages/residential_page.py new file mode 100755 index 0000000..60a907c --- /dev/null +++ b/gui/v2/ui/pages/residential_page.py @@ -0,0 +1,101 @@ +import os + +from PyQt6.QtWidgets import QPushButton, QLabel, QButtonGroup +from PyQt6.QtGui import QPixmap, QIcon +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class ResidentialPage(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("Wireguard", page_stack, main_window, parent) + self.title.setGeometry(585, 40, 185, 40) + self.title.setText("Pick a Protocol") + self.update_status = main_window + self.connection_choice = None + self.button_reverse.setVisible(True) + self.button_reverse.clicked.connect(self.reverse) + self.button_go.clicked.connect(self.go_selected) + + self.display_1 = QLabel(self) + self.display_1.setGeometry(QtCore.QRect( + 15, 50, 550, 465)) + self.display_1.setPixmap( + QPixmap(os.path.join(self.btn_path, "browser only.png"))) + self.label = QLabel(self) + self.label.setGeometry(440, 370, 86, 130) + self.label.setPixmap( + QPixmap(os.path.join(self.btn_path, "tor 86x130.png"))) + self.label.hide() + + def showEvent(self, event): + super().showEvent(event) + self.create_interface_elements() + + def create_interface_elements(self): + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + + for j, (object_type, icon_name, page_class, geometry) in enumerate([ + (QPushButton, "tor", self.set_connection_choice( + 'tor'), (585, 90, 185, 75)), + (QPushButton, "just proxy", self.set_connection_choice( + 'just proxy'), (585, 90+30+75+30+75, 185, 75)), + (QLabel, None, None, (570, 170, 210, 105)), + (QLabel, None, None, (570, 385, 210, 115)) + ]): + if object_type == QPushButton: + boton = object_type(self) + boton.setGeometry(*geometry) + boton.setIconSize(boton.size()) + boton.setCheckable(True) + boton.setIcon( + QIcon(os.path.join(self.btn_path, f"{icon_name}_button.png"))) + self.buttons.append(boton) + self.buttonGroup.addButton(boton, j) + + boton.show() + + boton.clicked.connect( + lambda checked, page=page_class, name=icon_name: self.show_residential(name, page)) + elif object_type == QLabel: + label = object_type(self) + label.setGeometry(*geometry) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + label.setWordWrap(True) + label.show() + if geometry == (570, 170, 210, 105): + text1 = "Connect to tor first\nThen the residential proxy\nThis hides Tor from websites" + label.setText(text1) + elif geometry == (570, 385, 210, 115): + text2 = "Connect directly\nTo the Proxy\nin a browser\nThis has no encryption" + label.setText(text2) + + def set_connection_choice(self, connection_choice): + pass + + def show_residential(self, icon_name, page_class): + self.connection_choice = icon_name + self.selected_page_class = page_class + + if icon_name == "tor": + self.label.show() + if icon_name == "just proxy": + self.label.hide() + + self.button_go.setVisible(True) + + def go_selected(self): + self.update_swarp_json(self.connection_choice) + self.custom_window.navigator.navigate("hidetor") + self.button_go.setVisible(False) + + def update_swarp_json(self, icon_name): + inserted_data = { + "connection": icon_name + } + self.update_status.write_data(inserted_data) + + def reverse(self): + self.custom_window.navigator.navigate("protocol") diff --git a/gui/v2/ui/pages/resume_page.py b/gui/v2/ui/pages/resume_page.py new file mode 100755 index 0000000..8bce4ed --- /dev/null +++ b/gui/v2/ui/pages/resume_page.py @@ -0,0 +1,463 @@ +import os + +from PyQt6.QtWidgets import ( + QLabel, QPushButton, QButtonGroup, QLineEdit, QTextEdit, QFrame +) +from PyQt6.QtGui import QPixmap, QIcon +from PyQt6.QtCore import Qt, QSize +from PyQt6 import QtCore, QtGui + +from core.controllers.ProfileController import ProfileController + +from gui.v2.actions.profile_order import append_profile_to_visual_order +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.pages.location_page import LocationPage +from gui.v2.ui.pages.screen_page import ScreenPage + + +class ResumePage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Resume", page_stack, main_window, parent) + self.update_status = main_window + self.connection_manager = main_window.connection_manager + self.btn_path = main_window.btn_path + self.labels_creados = [] + self.additional_labels = [] + self.button_go.clicked.connect(self.copy_profile) + self.button_back.setVisible(True) + self.title.setGeometry(585, 40, 185, 40) + self.title.setText("Profile Summary") + self.display.setGeometry(QtCore.QRect(5, 50, 580, 435)) + self.buttonGroup = QButtonGroup(self) + self.button_back.clicked.connect(self.reverse) + self.create_arrow() + self.create_interface_elements() + + def reverse(self): + if self.connection_type == "system-wide": + self.custom_window.navigator.navigate("location") + else: + self.custom_window.navigator.navigate("screen") + + def show_location_verification(self, location_icon_name): + from gui.v2.ui.pages.location_verification_page import LocationVerificationPage + verification_page = LocationVerificationPage( + self.page_stack, self.update_status, location_icon_name, self) + self.page_stack.addWidget(verification_page) + self.page_stack.setCurrentIndex( + self.page_stack.indexOf(verification_page)) + + def create_arrow(self): + self.arrow_label = QLabel(self) + self.arrow_label.setGeometry(400, 115, 200, 200) + arrow_pixmap = QPixmap(os.path.join(self.btn_path, "arrow.png")) + + self.arrow_label.setPixmap(arrow_pixmap) + self.arrow_label.setScaledContents(True) + + self.arrow_label.raise_() + + def showEvent(self, event): + super().showEvent(event) + self.arrow_label.show() + self.arrow_label.raise_() + + def create_interface_elements(self): + for j, (object_type, icon_name, geometry) in enumerate([ + (QLineEdit, None, (130, 73, 300, 24)), + (QLabel, None, (0, 0, 0, 0)) + ]): + + if object_type == QLabel: + label = object_type(self) + label.setGeometry(*geometry) + icon_path = os.path.join( + self.btn_path, f"{icon_name}_button.png") + if os.path.exists(icon_path): + label.setPixmap(QPixmap(icon_path)) + locations = self.connection_manager.get_location_info( + icon_name) + provider = locations.operator.name if locations and hasattr( + locations, 'operator') else None + if label.pixmap().isNull(): + if locations and hasattr(locations, 'country_name'): + location_name = locations.country_name + label.setPixmap(LocationPage.create_location_button_image( + location_name, os.path.join(self.btn_path, "default_location_button.png"), provider)) + else: + label.setPixmap(LocationPage.create_location_button_image( + '', os.path.join(self.btn_path, "default_location_button.png"), provider)) + else: + if locations and hasattr(locations, 'country_name'): + label.setPixmap(LocationPage.create_location_button_image(f'{locations.country_code}_{locations.code}', os.path.join( + self.btn_path, "default_location_button.png"), provider, icon_path)) + else: + label.setPixmap(LocationPage.create_location_button_image('', os.path.join( + self.btn_path, "default_location_button.png"), provider, icon_path)) + elif object_type == QLineEdit: + self.line_edit = object_type(self) + self.line_edit.setGeometry(*geometry) + self.line_edit.setMaxLength(13) + self.line_edit.textChanged.connect( + self.toggle_button_visibility) + self.line_edit.setStyleSheet( + "background-color: transparent; border: none; color: cyan;") + self.line_edit.setPlaceholderText("Enter profile name") + self.line_edit.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.create() + + def create(self): + for label in self.additional_labels: + label.deleteLater() + self.additional_labels.clear() + + profile_1 = self.update_status.read_data() + self.connection_type = profile_1.get("connection", "") + connection_exists = 'connection' in profile_1 + + if connection_exists and profile_1['connection'] not in ["tor", "just proxy"]: + items = ["protocol", "connection", + "location", "browser", "dimentions"] + initial_y = 90 + label_height = 80 + elif connection_exists: + items = ["protocol", "connection", + "location", "browser", "dimentions"] + initial_y = 90 + label_height = 80 + else: + items = ["protocol", "location", "browser", "dimentions"] + initial_y = 90 + label_height = 105 + for i, item in enumerate(items): + text = profile_1.get(item, "") + if text: + if item == 'browser': + from gui.v2.ui.pages.browser_page import BrowserPage + base_image = BrowserPage.create_browser_button_image( + text, self.btn_path) + geometry = (585, initial_y + i * label_height, 185, 75) + parent_label = QLabel(self) + parent_label.setGeometry(*geometry) + parent_label.setPixmap(base_image) + if parent_label.pixmap().isNull(): + fallback_path = os.path.join( + self.btn_path, "default_browser_button.png") + base_image = BrowserPage.create_browser_button_image( + text, fallback_path, True) + parent_label.setPixmap(base_image) + parent_label.show() + self.labels_creados.append(parent_label) + elif item == 'location': + icon_path = os.path.join( + self.btn_path, f"button_{text}.png") + geometry = (585, initial_y + i * label_height, 185, 75) + parent_label = QPushButton(self) + parent_label.setGeometry(*geometry) + parent_label.setFlat(True) + parent_label.setStyleSheet( + "background: transparent; border: none;") + location_pixmap = QPixmap(icon_path) + locations = self.connection_manager.get_location_info(text) + provider = locations.operator.name if locations and hasattr( + locations, 'operator') else None + fallback_path = os.path.join( + self.btn_path, "default_location_button.png") + if location_pixmap.isNull(): + if locations and hasattr(locations, 'country_name'): + location_name = locations.country_name + base_image = LocationPage.create_location_button_image( + location_name, fallback_path, provider) + parent_label.setIcon(QIcon(base_image)) + else: + base_image = LocationPage.create_location_button_image( + '', fallback_path, provider) + parent_label.setIcon(QIcon(base_image)) + else: + if locations and hasattr(locations, 'country_name'): + base_image = LocationPage.create_location_button_image( + f'{locations.country_code}_{locations.code}', fallback_path, provider, icon_path) + parent_label.setIcon(QIcon(base_image)) + else: + base_image = LocationPage.create_location_button_image( + '', fallback_path, provider, icon_path) + parent_label.setIcon(QIcon(base_image)) + parent_label.setIconSize(QSize(185, 75)) + parent_label.location_icon_name = text + parent_label.setCursor( + QtCore.Qt.CursorShape.PointingHandCursor) + parent_label.clicked.connect( + lambda checked, loc=text: self.show_location_verification(loc)) + parent_label.show() + self.labels_creados.append(parent_label) + elif item == 'dimentions': + base_image = ScreenPage.create_resolution_button_image( + self, text) + geometry = (585, initial_y + i * label_height, 185, 75) + parent_label = QLabel(self) + parent_label.setGeometry(*geometry) + parent_label.setPixmap(base_image) + parent_label.show() + self.labels_creados.append(parent_label) + else: + icon_path = os.path.join( + self.btn_path, f"{text}_button.png") + geometry = (585, initial_y + i * label_height, 185, 75) + parent_label = QLabel(self) + parent_label.setGeometry(*geometry) + parent_label.setPixmap(QPixmap(icon_path)) + parent_label.show() + self.labels_creados.append(parent_label) + location_text = profile_1.get("location", "") + location_info = self.connection_manager.get_location_info( + location_text) + operator_name = "" + if location_info and hasattr(location_info, 'operator') and location_info.operator: + operator_name = location_info.operator.name or "" + + if operator_name != 'Simplified Privacy' and operator_name != "": + text_color = "white" + if self.connection_type != "system-wide": + text_color = "black" + image_name = "browser only.png" + image_path = os.path.join(self.btn_path, image_name) + label_background = QLabel(self) + label_background.setGeometry(10, 50, 535, 460) + label_background.setPixmap(QPixmap(image_path)) + label_background.setScaledContents(True) + label_background.show() + label_background.lower() + self.labels_creados.append(label_background) + + l_img = QLabel(self) + l_img.setGeometry(30, 135, 150, 150) + pixmap_loc = QPixmap(os.path.join( + self.btn_path, f"icon_{location_text}.png")) + l_img.setPixmap(pixmap_loc) + l_img.setScaledContents(True) + l_img.show() + self.labels_creados.append(l_img) + + l_name = f"{location_info.country_name}, {location_info.name}" if location_info and hasattr( + location_info, 'country_name') else "" + o_name = operator_name + n_key = location_info.operator.nostr_public_key if location_info and hasattr( + location_info, 'operator') and location_info.operator else "" + + info_txt = QTextEdit(self) + info_txt.setGeometry(190, 130, 310, 250) + info_txt.setReadOnly(True) + info_txt.setFrameStyle(QFrame.Shape.NoFrame) + info_txt.setStyleSheet( + f"background: transparent; border: none; color: {text_color}; font-size: 19px;") + info_txt.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + info_txt.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + info_txt.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere) + info_txt.setText(f"Location: {l_name}\n\nOperator: {o_name}") + info_txt.show() + self.labels_creados.append(info_txt) + + nostr_txt = QTextEdit(self) + nostr_txt.setGeometry(30, 310, 480, 170) + nostr_txt.setReadOnly(True) + nostr_txt.setFrameStyle(QFrame.Shape.NoFrame) + nostr_txt.setStyleSheet( + f"background: transparent; border: none; color: {text_color}; font-size: 21px;") + nostr_txt.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + nostr_txt.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + nostr_txt.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAnywhere) + nostr_txt.setText(f"Nostr: {n_key}") + nostr_txt.show() + self.labels_creados.append(nostr_txt) + + elif connection_exists: + if profile_1.get("connection", "") == "system-wide": + image_path = os.path.join( + self.btn_path, f"wireguard_{profile_1.get('location', '')}.png") + main_label = QLabel(self) + main_label.setGeometry(10, 130, 500, 375) + main_label.setPixmap(QPixmap(image_path)) + main_label.setScaledContents(True) + main_label.show() + self.labels_creados.append(main_label) + + if profile_1.get("connection", "") == "just proxy": + image_path = os.path.join( + self.btn_path, f"icon_{profile_1.get('location', '')}.png") + main_label = QLabel(self) + main_label.setGeometry(10, 105, 530, 398) + main_label.setPixmap(QPixmap(image_path)) + main_label.setScaledContents(True) + main_label.show() + self.labels_creados.append(main_label) + if profile_1.get("connection", "") == "tor": + image_path = os.path.join( + self.btn_path, f"hdtor_{profile_1.get('location', '')}.png") + main_label = QLabel(self) + main_label.setGeometry(10, 105, 530, 398) + main_label.setPixmap(QPixmap(image_path)) + main_label.setScaledContents(True) + main_label.show() + self.labels_creados.append(main_label) + if profile_1.get("connection", "") == "browser-only": + image_name = "browser only.png" + image_path = os.path.join(self.btn_path, image_name) + label_background = QLabel(self) + label_background.setGeometry(10, 50, 535, 460) + label_background.setPixmap(QPixmap(image_path)) + label_background.setScaledContents(True) + label_background.show() + label_background.lower() + self.labels_creados.append(label_background) + image_path = os.path.join( + self.btn_path, f"wireguard_{profile_1.get('location', '')}.png") + main_label = QLabel(self) + main_label.setGeometry(10, 130, 500, 375) + main_label.setPixmap(QPixmap(image_path)) + main_label.setScaledContents(True) + main_label.show() + self.labels_creados.append(main_label) + else: + image_path = os.path.join( + self.btn_path, f"icon_{profile_1.get('location', '')}.png") + main_label = QLabel(self) + main_label.setGeometry(10, 130, 500, 375) + main_label.setPixmap(QPixmap(image_path)) + main_label.setScaledContents(True) + main_label.show() + self.labels_creados.append(main_label) + + if hasattr(self, 'arrow_label'): + self.arrow_label.raise_() + + def toggle_button_visibility(self): + self.button_go.setVisible(bool(self.line_edit.text())) + + def find_menu_page(self): + return self.custom_window.navigator.get_cached("menu") + + def copy_profile(self): + profile_name = self.line_edit.text() + menu_page = self.find_menu_page() + if menu_page: + number_of_profiles = menu_page.number_of_profiles + + profile_data = self.update_status.read_data() + + required_fields = [profile_data.get("protocol"), profile_name] + if not all(required_fields): + print("Error: Some required fields are empty!") + return + + profile_data["name"] = profile_name + profile_data["visible"] = "yes" + + profiles = ProfileController.get_all() + profile_id = self.get_next_available_id(profiles) + new_profile = profile_data + + self.create_core_profiles(new_profile, profile_id) + if ProfileController.get(profile_id) is not None: + append_profile_to_visual_order( + getattr(self.update_status, 'gui_config_file', None), + profile_id, + profiles.keys()) + + main = self.update_status + if hasattr(main, 'navigate_after_profile_created'): + main.navigate_after_profile_created() + else: + self.custom_window.navigator.navigate("menu") + + self.update_status.clear_data() + + self.line_edit.clear() + self.display.clear() + self.button_go.setVisible(False) + + def get_next_available_id(self, profiles: dict) -> int: + if not profiles: + return 1 + + existing_ids = sorted(profiles.keys()) + + for i in range(1, max(existing_ids) + 2): + if i not in existing_ids: + return i + + return None + + def create_core_profiles(self, profile, id): + if profile.get('connection') != 'system-wide': + browser_info = profile.get('browser', '').split() + if len(browser_info) < 2: + self.update_status.update_status( + 'Application version not supported') + return + + application = f"{browser_info[0].lower()}:{browser_info[1]}" + else: + application = '' + + parts = profile.get('location').split('_') + country_code = parts[0] + location_code = parts[1] + if profile.get('protocol') == 'wireguard': + connection_type = 'wireguard' + elif profile.get('protocol') == 'hidetor' or profile.get('protocol') == 'residential': + if profile.get('connection') == 'tor': + connection_type = 'tor' + elif profile.get('connection') == 'just proxy': + connection_type = 'system' + else: + self.update_status.update_status('Connection type not supported') + return + + profile_data = { + 'id': int(id), + 'name': profile.get('name'), + 'country_code': country_code, + 'code': location_code, + 'application': application, + 'connection_type': connection_type, + 'resolution': profile.get('dimentions', ''), + } + profile_type = 'system' if profile.get( + 'connection') == 'system-wide' else 'session' + action = f'CREATE_{profile_type.upper()}_PROFILE' + + self.handle_core_action_create_profile( + action, profile_data, profile_type) + + def eliminacion(self): + for label in self.labels_creados: + label.deleteLater() + + self.labels_creados = [] + self.create() + if hasattr(self, 'arrow_label'): + self.arrow_label.show() + self.arrow_label.raise_() + + def handle_core_action_create_profile(self, action, profile_data=None, profile_type=None): + from gui.v2.workers.worker_thread import WorkerThread + self.worker_thread = WorkerThread(action, profile_data, profile_type) + self.worker_thread.text_output.connect(self.update_output) + self.worker_thread.start() + self.worker_thread.wait() + + def update_output(self, text): + self.update_status.update_status(text) + + def on_profile_creation(self, result): + + if self.worker_thread: + self.worker_thread.quit() + self.worker_thread.wait() + self.worker_thread = None diff --git a/gui/v2/ui/pages/screen_page.py b/gui/v2/ui/pages/screen_page.py new file mode 100755 index 0000000..d6a091d --- /dev/null +++ b/gui/v2/ui/pages/screen_page.py @@ -0,0 +1,347 @@ +import os + +from PyQt6.QtWidgets import ( + QPushButton, QLabel, QButtonGroup, QDialog, QLineEdit, + QVBoxLayout, QHBoxLayout, QMessageBox +) +from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QFont +from PyQt6.QtCore import Qt, QRect +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class ScreenPage(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("Screen", page_stack, main_window, parent) + self.selected_dimentions = None + self.update_status = main_window + self.selected_dimentions_icon = None + self.button_back.setVisible(True) + self.title.setGeometry(585, 40, 200, 40) + self.title.setText("Pick a Resolution") + self.host_screen_info = QLabel( + f"Host: {self.custom_window.host_screen_width}x{self.custom_window.host_screen_height}", self) + self.host_screen_info.setGeometry(QtCore.QRect(355, 30, 200, 30)) + self.host_screen_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.host_screen_info.setStyleSheet( + "color: #00ffff; font-size: 14px; font-weight: bold;") + self.host_screen_info.show() + + self.display.setGeometry(QtCore.QRect(5, 50, 580, 435)) + self.showing_larger_resolutions = False + self.larger_resolution_buttons = [] + self.create_interface_elements() + + def create_interface_elements(self): + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + resolutions = [ + ("800x600", (595, 90, 180, 70)), + ("1024x760", (595, 170, 180, 70)), + ("1152x1080", (595, 250, 180, 70)), + ("1280x1024", (595, 330, 180, 70)), + ("1920x1080", (595, 410, 180, 70)) + ] + valid_resolutions = [] + for res, geom in resolutions: + w, h = map(int, res.split('x')) + if w < self.custom_window.host_screen_width and h < self.custom_window.host_screen_height: + valid_resolutions.append((QPushButton, res, geom)) + + for j, (object_type, icon_name, geometry) in enumerate(valid_resolutions): + boton = object_type(self) + boton.setGeometry(*geometry) + boton.setIconSize(boton.size()) + boton.setCheckable(True) + boton.setIcon( + QIcon(self.create_resolution_button_image(icon_name))) + self.buttons.append(boton) + self.buttonGroup.addButton(boton, j) + boton.clicked.connect( + lambda _, dimentions=icon_name: self.show_dimentions(dimentions)) + + self.larger_resolutions_button = QPushButton(self) + self.larger_resolutions_button.setGeometry(100, 450, 185, 50) + self.larger_resolutions_button.setIconSize( + self.larger_resolutions_button.size()) + self.larger_resolutions_button.setIcon( + QIcon(self.create_resolution_button_image("Larger Resolutions"))) + self.larger_resolutions_button.clicked.connect( + self.show_larger_resolutions) + + self.custom_resolution_button = QPushButton(self) + self.custom_resolution_button.setGeometry(300, 450, 195, 50) + self.custom_resolution_button.setIconSize( + self.custom_resolution_button.size()) + self.custom_resolution_button.setIcon( + QIcon(self.create_resolution_button_image("Custom Resolution"))) + self.custom_resolution_button.clicked.connect( + self.show_custom_resolution_dialog) + + def create_resolution_button_image(self, resolution_text): + template_path = os.path.join( + self.btn_path, "resolution_template_button.png") + + if os.path.exists(template_path): + base_image = QPixmap(template_path) + if 'Resolution' in resolution_text: + base_image = base_image.scaled(195, 50) + font_size = 10 + else: + base_image = base_image.scaled(187, 75) + font_size = 13 + else: + base_image = QPixmap(185, 75) + base_image.fill(QColor(44, 62, 80)) + font_size = 13 + + painter = QPainter(base_image) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + font = QFont() + font.setPointSize(font_size) + font.setBold(True) + painter.setFont(font) + + painter.setPen(QColor(0, 255, 255)) + + text_rect = base_image.rect() + painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter | + Qt.AlignmentFlag.AlignVCenter, resolution_text) + + painter.end() + return base_image + + def show_larger_resolutions(self): + if self.showing_larger_resolutions: + self.hide_larger_resolutions() + else: + self.hide_standard_resolutions() + self.showing_larger_resolutions = True + self.larger_resolutions_button.setIcon( + QIcon(self.create_resolution_button_image("Standard Resolutions"))) + + larger_resolutions_list = [ + ("2560x1600", (595, 90, 180, 70)), + ("2560x1440", (595, 170, 180, 70)), + ("1920x1440", (595, 250, 180, 70)), + ("1792x1344", (595, 330, 180, 70)), + ("2048x1152", (595, 410, 180, 70)) + ] + + valid_larger = list(larger_resolutions_list) + + for i, (resolution, geometry) in enumerate(valid_larger): + button = QPushButton(self) + button.setGeometry(*geometry) + button.setIconSize(button.size()) + button.setCheckable(True) + button.setVisible(True) + button.setIcon( + QIcon(self.create_resolution_button_image(resolution))) + button.clicked.connect( + lambda _, res=resolution: self.show_dimentions(res)) + self.larger_resolution_buttons.append(button) + self.buttonGroup.addButton(button, len(self.buttons) + i) + + def hide_larger_resolutions(self): + for button in self.larger_resolution_buttons: + self.buttonGroup.removeButton(button) + button.deleteLater() + self.larger_resolution_buttons.clear() + self.showing_larger_resolutions = False + self.larger_resolutions_button.setIcon( + QIcon(self.create_resolution_button_image("Larger Resolutions"))) + self.show_standard_resolutions() + + def hide_standard_resolutions(self): + for button in self.buttons: + button.setVisible(False) + + def show_standard_resolutions(self): + for button in self.buttons: + button.setVisible(True) + + def show_custom_resolution_dialog(self): + dialog = QDialog(self) + dialog.setWindowTitle("Custom Resolution") + dialog.setFixedSize(300, 150) + dialog.setModal(True) + dialog.setStyleSheet(""" + QDialog { + background-color: #2c3e50; + border: 2px solid #00ffff; + border-radius: 10px; + } + QLabel { + color: white; + font-size: 12px; + } + QLineEdit { + background-color: #34495e; + color: white; + border: 1px solid #00ffff; + border-radius: 5px; + padding: 5px; + font-size: 12px; + } + QPushButton { + background-color: #2c3e50; + color: white; + border: 2px solid #00ffff; + border-radius: 5px; + padding: 8px; + font-size: 12px; + } + QPushButton:hover { + background-color: #34495e; + } + """) + + layout = QVBoxLayout() + + input_layout = QHBoxLayout() + width_label = QLabel("Width:") + width_input = QLineEdit() + width_input.setPlaceholderText("1920") + height_label = QLabel("Height:") + height_input = QLineEdit() + height_input.setPlaceholderText("1080") + + input_layout.addWidget(width_label) + input_layout.addWidget(width_input) + input_layout.addWidget(height_label) + input_layout.addWidget(height_input) + + button_layout = QHBoxLayout() + ok_button = QPushButton("OK") + cancel_button = QPushButton("Cancel") + + button_layout.addWidget(cancel_button) + button_layout.addWidget(ok_button) + + layout.addLayout(input_layout) + layout.addLayout(button_layout) + dialog.setLayout(layout) + + def validate_and_accept(): + try: + width = int(width_input.text()) + height = int(height_input.text()) + + if width <= 0 or height <= 0: + QMessageBox.warning( + dialog, "Invalid Resolution", "Width and height must be positive numbers.") + return + + host_w = self.custom_window.host_screen_width + host_h = self.custom_window.host_screen_height + + adjusted = False + if width > host_w: + width = host_w - 50 + adjusted = True + if height > host_h: + height = host_h - 50 + adjusted = True + + if adjusted: + QMessageBox.information( + dialog, "Notice", "Adjusted to fit host size") + + if width > 10000 or height > 10000: + QMessageBox.warning( + dialog, "Invalid Resolution", "Resolution too large. Maximum 10000x10000.") + return + + custom_resolution = f"{width}x{height}" + self.show_dimentions(custom_resolution) + dialog.accept() + + except ValueError: + QMessageBox.warning( + dialog, "Invalid Input", "Please enter valid numbers for width and height.") + + ok_button.clicked.connect(validate_and_accept) + cancel_button.clicked.connect(dialog.reject) + + width_input.returnPressed.connect(validate_and_accept) + height_input.returnPressed.connect(validate_and_accept) + + dialog.exec() + + def update_swarp_json(self): + inserted_data = { + "dimentions": self.selected_dimentions_icon + } + + self.update_status.write_data(inserted_data) + + def show_dimentions(self, dimentions): + try: + image_path = os.path.join(self.btn_path, f"{dimentions}.png") + if os.path.exists(image_path): + self.display.setPixmap(QPixmap(image_path).scaled( + self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + else: + self.create_resolution_preview_image(dimentions) + except: + self.display.clear() + + self.selected_dimentions_icon = dimentions + self.button_next.setVisible(True) + self.update_swarp_json() + + def create_resolution_preview_image(self, resolution_text): + template_path = os.path.join(self.btn_path, "resolution_template.png") + + if os.path.exists(template_path): + base_image = QPixmap(template_path) + base_image = base_image.scaled( + 580, 435, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + else: + base_image = QPixmap(580, 435) + base_image.fill(QColor(44, 62, 80)) + + painter = QPainter(base_image) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + try: + width, height = map(int, resolution_text.split('x')) + gcd = self._gcd(width, height) + aspect_ratio = f"{width//gcd}:{height//gcd}" + except: + aspect_ratio = "" + + font = QFont() + font.setPointSize(24) + font.setBold(True) + painter.setFont(font) + painter.setPen(QColor(0, 255, 255)) + + text_rect = base_image.rect() + painter.drawText( + text_rect, Qt.AlignmentFlag.AlignCenter, resolution_text) + + if aspect_ratio: + font.setPointSize(16) + painter.setFont(font) + painter.setPen(QColor(200, 200, 200)) + + aspect_rect = QRect( + text_rect.x(), text_rect.y() + 80, text_rect.width(), 40) + painter.drawText( + aspect_rect, Qt.AlignmentFlag.AlignCenter, f"({aspect_ratio})") + + painter.end() + + self.display.setPixmap(base_image) + + def _gcd(self, a, b): + while b: + a, b = b, a % b + return a + + def gestionar_next(self): + self.custom_window.navigator.navigate("resume") diff --git a/gui/v2/ui/pages/settings_page.py b/gui/v2/ui/pages/settings_page.py new file mode 100755 index 0000000..d610778 --- /dev/null +++ b/gui/v2/ui/pages/settings_page.py @@ -0,0 +1,2325 @@ +import json +import logging +import os +import subprocess +import sys +import threading +from datetime import datetime, timezone +from typing import Union + +from PyQt6.QtWidgets import ( + QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout, + QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, + QScrollArea, QStackedLayout, QVBoxLayout, QWidget, +) +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt, QSize + +from core.Constants import Constants +from core.controllers.ConfigurationController import ConfigurationController +from core.controllers.PolicyController import PolicyController +from core.controllers.ProfileController import ProfileController +from core.controllers.tickets.UseTicketController import ( + do_we_use_a_random_ticket, + get_unused_tickets, + modify_random_tickets_setting, +) +from core.models.session.SessionProfile import SessionProfile +from core.models.system.SystemProfile import SystemProfile +from core.Errors import ( + CommandNotFoundError, + PolicyAssignmentError, + PolicyInstatementError, + PolicyRevocationError, +) +from core.errors.logger import logger as core_logger + +from gui.v2.actions.key_interpretation import interpret_key_results +from gui.v2.actions.profile_order import normalize_profile_order +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 +from gui.v2.ui.styles.styles import ( + SCROLLBAR_CYAN_QSS, + TERMINAL_LIST_QSS, + checkbox_style, +) +from gui.v2.ui.widgets.clickable_label import ClickableValueLabel +from gui.v2.ui.widgets.terminal_widget import TerminalWidget +from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread +from gui.v2.workers.worker_thread import WorkerThread + + +class Settings(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("Settings", page_stack, main_window, parent) + self.font_style = f"font-family: '{self.custom_window.open_sans_family}';" + + self.update_status = main_window + self.update_logging = main_window + self.button_reverse.setVisible(True) + self.button_reverse.setEnabled(True) + self.button_reverse.clicked.connect(self.reverse) + self.title.setGeometry(585, 40, 185, 40) + self.title.setText("Settings") + self.setup_ui() + + def setup_ui(self): + main_container = QWidget(self) + main_container.setGeometry(0, 0, 800, 520) + + self.layout = QHBoxLayout(main_container) + self.layout.setContentsMargins(0, 60, 0, 0) + + self.left_panel = QWidget() + self.left_panel.setFixedWidth(200) + self.left_layout = QVBoxLayout(self.left_panel) + self.left_layout.setContentsMargins(0, 0, 0, 0) + self.left_layout.setSpacing(0) + + self.content_widget = QWidget() + self.content_layout = QStackedLayout(self.content_widget) + + self.layout.addWidget(self.left_panel) + self.layout.addWidget(self.content_widget) + + self.setup_menu_buttons() + self.setup_pages() + + def setup_menu_buttons(self): + menu_items = [ + ("Overview", self.show_account_page), + ("Subscriptions", self.show_subscription_page), + ("Tickets", self.show_tickets_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) + ] + + self.menu_buttons = [] + for text, callback in menu_items: + btn = QPushButton(text) + btn.setFixedHeight(40) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + btn.setCheckable(True) + btn.clicked.connect(callback) + + btn.setStyleSheet(f""" + QPushButton {{ + text-align: left; + padding-left: 20px; + border: none; + background: transparent; + color: #808080; + font-size: 14px; + {self.font_style} + }} + QPushButton:checked {{ + background: rgba(255, 255, 255, 0.1); + color: white; + border-left: 3px solid #007AFF; + }} + QPushButton:hover:!checked {{ + background: rgba(255, 255, 255, 0.05); + }} + """) + + self.left_layout.addWidget(btn) + self.menu_buttons.append(btn) + + self.left_layout.addStretch() + + def create_delete_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(15) + + title = QLabel("DELETE PROFILE") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + self.delete_scroll_area = QScrollArea() + self.delete_scroll_area.setFixedSize(510, 300) + self.delete_scroll_area.setWidgetResizable(True) + self.delete_scroll_area.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.delete_scroll_area.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.delete_scroll_area.setStyleSheet(SCROLLBAR_CYAN_QSS) + + self.delete_scroll_widget = QWidget() + self.delete_scroll_widget.setStyleSheet( + "background-color: transparent;") + self.delete_scroll_area.setWidget(self.delete_scroll_widget) + + self.profile_buttons = QButtonGroup() + self.profile_buttons.setExclusive(True) + + self.create_delete_profile_buttons() + + layout.addWidget(self.delete_scroll_area) + + self.delete_button = QPushButton("Delete") + self.delete_button.setEnabled(False) + self.delete_button.setFixedSize(120, 40) + self.delete_button.setStyleSheet(f""" + QPushButton {{ + background: #ff4d4d; + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + {self.font_style} + }} + QPushButton:disabled {{ + background: #666666; + }} + QPushButton:hover:!disabled {{ + background: #ff3333; + }} + """) + self.delete_button.clicked.connect(self.delete_selected_profile) + self.profile_buttons.buttonClicked.connect(self.on_profile_selected) + + button_layout = QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(self.delete_button) + button_layout.addStretch() + + layout.addStretch() + layout.addLayout(button_layout) + + return page + + def create_delete_profile_buttons(self): + profiles = ProfileController.get_all() + + profile_ids = normalize_profile_order( + getattr(self.update_status, 'gui_config_file', None), + profiles.keys()) + + for index, profile_id in enumerate(profile_ids): + profile = profiles[profile_id] + row = index // 2 + col = index % 2 + + btn = QPushButton(f"Profile {profile_id}\n{profile.name}") + btn.setCheckable(True) + btn.setFixedSize(180, 60) + btn.setParent(self.delete_scroll_widget) + btn.setGeometry(col * 220 + 50, row * 100, 180, 60) + btn.setStyleSheet(f""" + QPushButton {{ + background: rgba(255, 255, 255, 0.1); + border: none; + color: white; + border-radius: 5px; + text-align: center; + {self.font_style} + }} + QPushButton:checked {{ + background: rgba(255, 255, 255, 0.3); + border: 2px solid #007AFF; + }} + QPushButton:hover:!checked {{ + background: rgba(255, 255, 255, 0.2); + }} + """) + self.profile_buttons.addButton(btn, profile_id) + + if profiles: + rows = (len(profiles) + 1) // 2 + height = rows * 100 + self.delete_scroll_widget.setFixedSize(510, height) + + def create_debug_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(15) + + title = QLabel("DEBUG HELP") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setStyleSheet(f""" + QScrollArea {{ + border: none; + background: transparent; + }} + QScrollArea > QWidget > QWidget {{ + background: transparent; + }} + """) + + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + scroll_layout.setSpacing(15) + + profile_selection_group = QGroupBox("Profile Selection") + profile_selection_group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + font-size: 10px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + profile_selection_layout = QVBoxLayout(profile_selection_group) + + profile_label = QLabel("Select System-wide Profile:") + profile_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + profile_selection_layout.addWidget(profile_label) + + self.debug_profile_selector = QComboBox() + self.debug_profile_selector.setStyleSheet(self.get_combobox_style()) + self.debug_profile_selector.currentTextChanged.connect( + self.on_debug_profile_selected) + profile_selection_layout.addWidget(self.debug_profile_selector) + + scroll_layout.addWidget(profile_selection_group) + + ping_group = QGroupBox("Ping Test") + ping_group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + font-size: 10px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + ping_layout = QVBoxLayout(ping_group) + + self.ping_instruction_label = QLabel("") + self.ping_instruction_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + self.ping_instruction_label.setWordWrap(True) + self.ping_instruction_label.hide() + ping_layout.addWidget(self.ping_instruction_label) + + ping_buttons_layout = QHBoxLayout() + + self.copy_ping_button = QPushButton("Copy Ping Command") + self.copy_ping_button.setFixedSize(140, 40) + self.copy_ping_button.setStyleSheet(f""" + QPushButton {{ + font-size: 12px; + background: #007AFF; + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + {self.font_style} + }} + QPushButton:hover {{ + background: #0056CC; + }} + QPushButton:disabled {{ + background: #666666; + }} + """) + self.copy_ping_button.clicked.connect(self.copy_ping_command) + self.copy_ping_button.setEnabled(False) + ping_buttons_layout.addWidget(self.copy_ping_button) + + self.test_ping_button = QPushButton("Test Ping") + self.test_ping_button.setFixedSize(120, 40) + self.test_ping_button.setStyleSheet(f""" + QPushButton {{ + font-size: 12px; + background: #4CAF50; + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + {self.font_style} + }} + QPushButton:hover {{ + background: #45a049; + }} + QPushButton:disabled {{ + background: #666666; + }} + """) + self.test_ping_button.clicked.connect(self.test_ping_connection) + self.test_ping_button.setEnabled(False) + ping_buttons_layout.addWidget(self.test_ping_button) + + ping_layout.addLayout(ping_buttons_layout) + + self.ping_result_label = QLabel("") + self.ping_result_label.setStyleSheet( + f"color: white; font-size: 12px; font-weight: bold; {self.font_style}") + self.ping_result_label.setWordWrap(True) + self.ping_result_label.hide() + ping_layout.addWidget(self.ping_result_label) + + scroll_layout.addWidget(ping_group) + + command_group = QGroupBox("CLI Commands") + command_group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + font-size: 10px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + command_layout = QVBoxLayout(command_group) + + self.cli_command = QLineEdit() + self.cli_command.setReadOnly(True) + self.cli_command.setText("Select a profile above to see command") + self.cli_command.setStyleSheet(f""" + QLineEdit {{ + color: white; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + padding: 8px; + font-family: 'Courier New', monospace; + font-size: 12px; + }} + """) + command_layout.addWidget(self.cli_command) + + cli_buttons_layout = QHBoxLayout() + + cli_copy_button = QPushButton("Copy CLI Command") + cli_copy_button.setFixedSize(140, 40) + cli_copy_button.setStyleSheet(f""" + QPushButton {{ + font-size: 12px; + background: #007AFF; + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + {self.font_style} + }} + QPushButton:hover {{ + background: #0056CC; + }} + QPushButton:disabled {{ + background: #666666; + }} + """) + cli_copy_button.clicked.connect(self.copy_cli_command) + cli_copy_button.setEnabled(False) + cli_buttons_layout.addWidget(cli_copy_button) + + command_layout.addLayout(cli_buttons_layout) + + find_buttons_layout = QHBoxLayout() + + command_layout.addLayout(find_buttons_layout) + + scroll_layout.addWidget(command_group) + + self.cli_copy_button = cli_copy_button + + wg_quick_group = QGroupBox("Direct Systemwide Wireguard") + wg_quick_group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + font-size: 10px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + wg_quick_layout = QVBoxLayout(wg_quick_group) + + wg_quick_subtitle = QLabel("This uses your Linux system's Wireguard") + wg_quick_subtitle.setStyleSheet( + f"font-size: 10px; color: #CCCCCC; {self.font_style}") + wg_quick_subtitle.setWordWrap(True) + wg_quick_layout.addWidget(wg_quick_subtitle) + + self.wg_quick_up_command = QLineEdit() + self.wg_quick_up_command.setReadOnly(True) + self.wg_quick_up_command.setText( + "Select a profile above to see commands") + self.wg_quick_up_command.setStyleSheet(f""" + QLineEdit {{ + color: black; + background: white; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + padding: 8px; + font-family: 'Courier New', monospace; + font-size: 12px; + }} + """) + wg_quick_layout.addWidget(self.wg_quick_up_command) + + copy_wg_up_button = QPushButton("Copy UP command") + copy_wg_up_button.setFixedSize(140, 40) + copy_wg_up_button.setStyleSheet(f""" + QPushButton {{ + font-size: 10px; + background: #4CAF50; + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + {self.font_style} + }} + QPushButton:hover {{ + background: #45a049; + }} + """) + copy_wg_up_button.clicked.connect(self.copy_wg_quick_up_command) + copy_wg_up_button.setEnabled(False) + wg_quick_layout.addWidget(copy_wg_up_button) + + self.wg_quick_down_command = QLineEdit() + self.wg_quick_down_command.setReadOnly(True) + self.wg_quick_down_command.setText( + "Select a profile above to see commands") + self.wg_quick_down_command.setStyleSheet(f""" + QLineEdit {{ + color: black; + background: white; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + padding: 8px; + font-family: 'Courier New', monospace; + font-size: 12px; + }} + """) + wg_quick_layout.addWidget(self.wg_quick_down_command) + + copy_wg_down_button = QPushButton("Copy DOWN command") + copy_wg_down_button.setFixedSize(140, 40) + copy_wg_down_button.setStyleSheet(f""" + QPushButton {{ + font-size: 10px; + background: #F44336; + color: white; + border: none; + border-radius: 5px; + font-weight: bold; + {self.font_style} + }} + QPushButton:hover {{ + background: #d32f2f; + }} + """) + copy_wg_down_button.clicked.connect(self.copy_wg_quick_down_command) + copy_wg_down_button.setEnabled(False) + wg_quick_layout.addWidget(copy_wg_down_button) + + self.wg_quick_up_button = copy_wg_up_button + self.wg_quick_down_button = copy_wg_down_button + + self.wg_quick_up_command_widget = self.wg_quick_up_command + self.wg_quick_down_command_widget = self.wg_quick_down_command + + scroll_layout.addWidget(wg_quick_group) + + self.update_debug_profile_list() + + scroll_area.setWidget(scroll_content) + layout.addWidget(scroll_area) + + return page + + def copy_cli_command(self): + clipboard = QApplication.clipboard() + clipboard.setText(self.cli_command.text()) + self.update_status.update_status("CLI command copied to clipboard") + + def execute_cli_command(self): + try: + command = self.cli_command.text().split() + subprocess.Popen(command) + self.update_status.update_status("CLI command executed") + except Exception as e: + self.update_status.update_status( + f"Error executing command: {str(e)}") + + def copy_find_command(self, command_text): + clipboard = QApplication.clipboard() + clipboard.setText(command_text) + self.update_status.update_status("Find command copied to clipboard") + + def execute_find_command(self, command_text): + try: + command = command_text.split() + subprocess.Popen(command) + self.update_status.update_status("Find command executed") + except Exception as e: + self.update_status.update_status( + f"Error executing find command: {str(e)}") + + def update_debug_profile_list(self): + self.debug_profile_selector.clear() + profiles = ProfileController.get_all() + system_profiles = {pid: profile for pid, profile in profiles.items( + ) if isinstance(profile, SystemProfile)} + + if system_profiles: + for profile_id, profile in system_profiles.items(): + self.debug_profile_selector.addItem( + f"Profile {profile_id}: {profile.name}", profile_id) + else: + self.debug_profile_selector.addItem( + "No system-wide profiles found", None) + + def on_debug_profile_selected(self, text): + profile_id = self.debug_profile_selector.currentData() + if profile_id is None: + self.ping_instruction_label.hide() + self.copy_ping_button.setEnabled(False) + self.test_ping_button.setEnabled(False) + self.ping_result_label.hide() + self.cli_command.setText("Select a profile above to see command") + self.cli_copy_button.setEnabled(False) + if hasattr(self, 'wg_quick_up_command_widget'): + self.wg_quick_up_command_widget.setText( + "Select a profile above to see commands") + self.wg_quick_down_command_widget.setText( + "Select a profile above to see commands") + self.wg_quick_up_button.setEnabled(False) + self.wg_quick_down_button.setEnabled(False) + return + + profile = ProfileController.get(profile_id) + + if profile and isinstance(profile, SystemProfile): + app_path = os.environ.get('APPIMAGE') or "/path/to/hydra-veil" + self.cli_command.setText( + f"'{app_path}' --cli profile enable -i {profile_id}") + self.cli_copy_button.setEnabled(True) + ip_address = self.extract_endpoint_ip(profile) + if ip_address: + self.ping_instruction_label.setText( + f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}") + self.ping_instruction_label.show() + self.copy_ping_button.setEnabled(True) + self.test_ping_button.setEnabled(True) + self.ping_result_label.hide() + + if hasattr(self, 'wg_quick_up_command_widget'): + self.wg_quick_up_command_widget.setText( + f"sudo wg-quick up '/etc/hydra-veil/profiles/{profile_id}/wg.conf'") + self.wg_quick_down_command_widget.setText( + f"sudo wg-quick down '/etc/hydra-veil/profiles/{profile_id}/wg.conf'") + self.wg_quick_up_button.setEnabled(True) + self.wg_quick_down_button.setEnabled(True) + else: + self.ping_instruction_label.setText( + "Could not extract IP address from WireGuard configuration") + self.ping_instruction_label.show() + self.copy_ping_button.setEnabled(False) + self.test_ping_button.setEnabled(False) + self.ping_result_label.hide() + + if hasattr(self, 'wg_quick_up_command_widget'): + self.wg_quick_up_command_widget.setText( + "Could not load profile configuration") + self.wg_quick_down_command_widget.setText( + "Could not load profile configuration") + self.wg_quick_up_button.setEnabled(False) + self.wg_quick_down_button.setEnabled(False) + + def extract_endpoint_ip(self, profile): + try: + profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}' + wg_conf_path = f'{profile_path}/wg.conf.bak' + if not os.path.exists(wg_conf_path): + return None + + with open(wg_conf_path, 'r') as f: + content = f.read() + + for line in content.split('\n'): + if line.strip().startswith('Endpoint = '): + endpoint = line.strip().split(' = ')[1] + ip_address = endpoint.split(':')[0] + return ip_address + return None + except Exception: + return None + + def copy_ping_command(self): + profile_id = self.debug_profile_selector.currentData() + if profile_id is None: + return + + profile = ProfileController.get(profile_id) + if profile and isinstance(profile, SystemProfile): + ip_address = self.extract_endpoint_ip(profile) + if ip_address: + ping_command = f"ping {ip_address}" + clipboard = QApplication.clipboard() + clipboard.setText(ping_command) + self.update_status.update_status( + "Ping command copied to clipboard") + + def test_ping_connection(self): + profile_id = self.debug_profile_selector.currentData() + if profile_id is None: + return + + profile = ProfileController.get(profile_id) + if profile and isinstance(profile, SystemProfile): + ip_address = self.extract_endpoint_ip(profile) + if ip_address: + self.test_ping_button.setEnabled(False) + self.ping_result_label.setText("Testing ping...") + self.ping_result_label.setStyleSheet( + f"color: #FFA500; font-size: 12px; font-weight: bold; {self.font_style}") + self.ping_result_label.show() + + def ping_test(): + try: + if sys.platform == "win32": + result = subprocess.run(['ping', '-n', '4', ip_address], + capture_output=True, text=True, timeout=10) + else: + result = subprocess.run(['ping', '-c', '4', ip_address], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + self.ping_result_label.setText( + "✅ Ping successful - Connection is working!") + self.ping_result_label.setStyleSheet( + f"color: #4CAF50; font-size: 12px; font-weight: bold; {self.font_style}") + else: + self.ping_result_label.setText( + "❌ Ping failed - Connection issue detected") + self.ping_result_label.setStyleSheet( + f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}") + except subprocess.TimeoutExpired: + self.ping_result_label.setText( + "❌ Ping timeout - Connection issue detected") + self.ping_result_label.setStyleSheet( + f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}") + except Exception as e: + self.ping_result_label.setText( + f"❌ Ping error: {str(e)}") + self.ping_result_label.setStyleSheet( + f"color: #F44336; font-size: 12px; font-weight: bold; {self.font_style}") + finally: + self.test_ping_button.setEnabled(True) + + threading.Thread(target=ping_test, daemon=True).start() + + def copy_wg_quick_up_command(self): + clipboard = QApplication.clipboard() + clipboard.setText(self.wg_quick_up_command_widget.text()) + self.update_status.update_status( + "wg-quick UP command copied to clipboard") + + def copy_wg_quick_down_command(self): + clipboard = QApplication.clipboard() + clipboard.setText(self.wg_quick_down_command_widget.text()) + self.update_status.update_status( + "wg-quick DOWN command copied to clipboard") + + def on_profile_selected(self, button): + self.delete_button.setEnabled(True) + self.selected_profile_id = self.profile_buttons.id(button) + + def delete_selected_profile(self): + if hasattr(self, 'selected_profile_id'): + self.popup = ConfirmationPopup( + self, + message=f"Are you sure you want to delete Profile {self.selected_profile_id}?", + action_button_text="Delete", + cancel_button_text="Cancel" + ) + self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) + self.popup.finished.connect(self.handle_delete_confirmation) + self.popup.show() + + def handle_delete_confirmation(self, confirmed): + if confirmed: + self.update_status.update_status(f'Deleting profile...') + self.worker_thread = WorkerThread('DESTROY_PROFILE', profile_data={ + 'id': self.selected_profile_id}) + self.worker_thread.text_output.connect(self.delete_status_update) + self.worker_thread.start() + + def delete_status_update(self, message): + self.update_status.update_status(message) + self.content_layout.removeWidget(self.delete_page) + self.delete_page = self.create_delete_page() + self.content_layout.addWidget(self.delete_page) + self.content_layout.setCurrentWidget(self.delete_page) + + def get_combobox_style(self) -> str: + return f""" + QComboBox {{ + color: black; + background: #f0f0f0; + padding: 5px 30px 5px 10px; + border: 1px solid #ccc; + border-radius: 4px; + min-width: 120px; + margin-bottom: 10px; + {self.font_style} + }} + QComboBox:disabled {{ + color: #666; + background: #e0e0e0; + }} + QComboBox::drop-down {{ + border: none; + width: 30px; + }} + QComboBox::down-arrow {{ + image: url(assets/down_arrow.png); + width: 12px; + height: 12px; + }} + QComboBox QAbstractItemView {{ + color: black; + background: white; + selection-background-color: #007bff; + selection-color: white; + border: 1px solid #ccc; + {self.font_style} + }} + """ + + def get_checkbox_style(self) -> str: + return checkbox_style(self.font_style) + + def populate_wireguard_profiles(self) -> None: + self.wireguard_profile_selector.clear() + profiles = ProfileController.get_all() + for profile_id, profile in profiles.items(): + if profile.connection.code == 'wireguard': + + self.wireguard_profile_selector.addItem( + f"Profile {profile_id}: {profile.name}", profile_id) + + def convert_duration(self, value: Union[str, int], to_hours: bool = True) -> Union[str, int]: + if to_hours: + number, unit = value.split(' ') + number = int(number) + if unit in ['day', 'days']: + return number * 24 + elif unit in ['week', 'weeks']: + return number * 7 * 24 + else: + raise ValueError(f"Unsupported duration unit: {unit}") + else: + hours = int(value) + if hours % (7 * 24) == 0: + weeks = hours // (7 * 24) + return f"{weeks} {'week' if weeks == 1 else 'weeks'}" + elif hours % 24 == 0: + days = hours // 24 + return f"{days} {'day' if days == 1 else 'days'}" + else: + raise ValueError( + f"Hours value {hours} cannot be converted to days or weeks cleanly") + + def reverse(self): + self.custom_window.navigator.navigate("menu") + + def create_subscription_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("Subscription Info") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + profile_group = QGroupBox("Profile Selection") + profile_group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + profile_layout = QVBoxLayout(profile_group) + + self.profile_selector = QComboBox() + self.profile_selector.setStyleSheet(self.get_combobox_style()) + profiles = ProfileController.get_all() + if profiles: + for profile_id, profile in profiles.items(): + self.profile_selector.addItem( + f"Profile {profile_id}: {profile.name}", profile_id) + profile_layout.addWidget(self.profile_selector) + layout.addWidget(profile_group) + + subscription_group = QGroupBox("Subscription Details") + subscription_group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + subscription_layout = QGridLayout(subscription_group) + subscription_layout.setSpacing(10) + + self.subscription_info = {} + info_items = [ + ("Billing Code", "billing_code"), + ("Expires At", "expires_at") + ] + + for i, (label, key) in enumerate(info_items): + if key == "billing_code": + billing_container = QWidget() + billing_layout = QVBoxLayout(billing_container) + billing_layout.setContentsMargins(0, 0, 0, 0) + billing_layout.setSpacing(5) + + stat_widget = self.create_stat_widget(label, "N/A") + billing_layout.addWidget(stat_widget) + + copy_button = QPushButton("Copy") + copy_button.setFixedSize(60, 30) + copy_button.setStyleSheet(f""" + QPushButton {{ + background: #007AFF; + color: white; + border: none; + border-radius: 4px; + font-size: 11px; + font-weight: bold; + {self.font_style} + }} + QPushButton:hover {{ + background: #0056CC; + }} + """) + copy_button.clicked.connect(lambda: self.copy_billing_code()) + billing_layout.addWidget(copy_button) + + row, col = divmod(i, 2) + subscription_layout.addWidget(billing_container, row, col) + + old_label = stat_widget.findChild(QLabel, "value_label") + if old_label: + old_label.setParent(None) + old_label.deleteLater() + + value_label = ClickableValueLabel("N/A", stat_widget) + value_label.setObjectName("value_label") + value_label.setStyleSheet( + f"color: #00ffff; font-size: 12px; font-family: 'Retro Gaming', sans-serif;") + value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + stat_widget.layout().insertWidget(0, value_label) + + self.subscription_info[key] = value_label + else: + stat_widget = self.create_stat_widget(label, "N/A") + row, col = divmod(i, 2) + subscription_layout.addWidget(stat_widget, row, col) + + old_label = stat_widget.findChild(QLabel, "value_label") + if old_label: + old_label.setParent(None) + old_label.deleteLater() + + value_label = ClickableValueLabel("N/A", stat_widget) + value_label.setObjectName("value_label") + value_label.setStyleSheet( + f"color: #00ffff; font-size: 12px; font-family: 'Retro Gaming', sans-serif;") + value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + stat_widget.layout().insertWidget(0, value_label) + + self.subscription_info[key] = value_label + + layout.addWidget(subscription_group) + + clipboard_hint = QLabel( + "💡 Click on the billing code or use the Copy button to copy it to clipboard") + clipboard_hint.setStyleSheet( + f"color: #666666; font-size: 11px; font-style: italic; {self.font_style}") + clipboard_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(clipboard_hint) + + layout.addStretch() + + self.profile_selector.currentIndexChanged.connect( + self.update_subscription_info) + if self.profile_selector.count() > 0: + self.update_subscription_info(0) + + return page + + def create_verification_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("Verification Information") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + profile_label = QLabel("Profile Selection:") + profile_label.setStyleSheet( + f"color: white; font-size: 14px; {self.font_style}") + layout.addWidget(profile_label) + + self.verification_profile_selector = QComboBox() + self.verification_profile_selector.setStyleSheet( + self.get_combobox_style()) + profiles = ProfileController.get_all() + if profiles: + for profile_id, profile in profiles.items(): + self.verification_profile_selector.addItem( + f"Profile {profile_id}: {profile.name}", profile_id) + layout.addWidget(self.verification_profile_selector) + + details_label = QLabel("Verification Details:") + details_label.setStyleSheet( + f"color: white; font-size: 14px; margin-top: 20px; {self.font_style}") + layout.addWidget(details_label) + + verification_layout = QVBoxLayout() + verification_layout.setSpacing(15) + self.verification_info = {} + self.verification_checkmarks = {} + self.verification_full_values = {} + info_items = [ + ("Operator Name", "operator_name"), + ("Nostr Key", "nostr_public_key"), + ("HydraVeil Key", "hydraveil_public_key"), + ("Nostr Verification", "nostr_attestation_event_reference"), + ] + + for label, key in info_items: + container = QWidget() + container_layout = QHBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) + container_layout.setSpacing(10) + + label_widget = QLabel(label + ":") + label_widget.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + label_widget.setMinimumWidth(120) + container_layout.addWidget(label_widget) + + value_widget = QLineEdit() + value_widget.setReadOnly(True) + value_widget.setText("N/A") + value_widget.setStyleSheet(f""" + QLineEdit {{ + color: white; + background: transparent; + border: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + padding: 5px 0px; + font-size: 12px; + {self.font_style} + }} + """) + container_layout.addWidget(value_widget, 1) + + checkmark_widget = QLabel("✓") + checkmark_widget.setStyleSheet( + f"color: #4CAF50; font-size: 20px; font-weight: bold; {self.font_style}") + checkmark_widget.setFixedSize(20, 20) + checkmark_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + checkmark_widget.hide() + container_layout.addWidget(checkmark_widget) + + copy_button = QPushButton() + copy_button.setIcon( + QIcon(os.path.join(self.btn_path, "paste_button.png"))) + copy_button.setIconSize(QSize(24, 24)) + copy_button.setFixedSize(30, 30) + copy_button.setStyleSheet(f""" + QPushButton {{ + background: transparent; + border: none; + }} + QPushButton:hover {{ + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + }} + """) + copy_button.clicked.connect( + lambda checked=False, k=key: self.copy_verification_value(k)) + container_layout.addWidget(copy_button) + + verification_layout.addWidget(container) + self.verification_info[key] = value_widget + self.verification_checkmarks[key] = checkmark_widget + + layout.addLayout(verification_layout) + layout.addStretch() + + endpoint_verification_label = QLabel("Endpoint Verification:", page) + endpoint_verification_label.setGeometry(20, 420, 150, 20) + endpoint_verification_label.setStyleSheet( + f"color: white; font-size: 15px; {self.font_style}") + endpoint_verification_label.show() + + self.endpoint_verification_checkbox = QCheckBox(page) + self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30) + self.endpoint_verification_checkbox.setChecked( + ConfigurationController.get_endpoint_verification_enabled()) + self.endpoint_verification_checkbox.setStyleSheet( + self.get_checkbox_style()) + self.endpoint_verification_checkbox.show() + + save_button = QPushButton(page) + save_button.setGeometry(350, 410, 60, 31) + save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png"))) + save_button.setIconSize(QSize(60, 31)) + save_button.clicked.connect(self.save_verification_settings) + save_button.show() + + self.verification_profile_selector.currentIndexChanged.connect( + self.update_verification_info) + self.verification_profile_selector.activated.connect( + self.update_verification_info) + if self.verification_profile_selector.count() > 0: + self.update_verification_info(0) + + return page + + def truncate_key(self, text, max_length=50): + if not text or text == "N/A" or len(text) <= max_length: + return text + start_len = max_length // 2 - 2 + end_len = max_length // 2 - 2 + return text[:start_len] + "....." + text[-end_len:] + + def update_verification_info(self, index): + if index < 0: + return + + profile_id = self.verification_profile_selector.itemData(index) + if profile_id is None: + for key, widget in self.verification_info.items(): + widget.setText("N/A") + self.verification_full_values[key] = "N/A" + if key in self.verification_checkmarks: + self.verification_checkmarks[key].hide() + return + + profile = ProfileController.get(profile_id) + if not profile: + for key, widget in self.verification_info.items(): + widget.setText("N/A") + self.verification_full_values[key] = "N/A" + if key in self.verification_checkmarks: + self.verification_checkmarks[key].hide() + return + + operator = None + if profile.location and profile.location.operator: + operator = profile.location.operator + + if operator: + operator_name = operator.name or "N/A" + self.verification_full_values["operator_name"] = operator_name + self.verification_info["operator_name"].setText(operator_name) + + nostr_key = operator.nostr_public_key or "N/A" + self.verification_full_values["nostr_public_key"] = nostr_key + self.verification_info["nostr_public_key"].setText( + self.truncate_key(nostr_key) if nostr_key != "N/A" else nostr_key) + + hydraveil_key = operator.public_key or "N/A" + self.verification_full_values["hydraveil_public_key"] = hydraveil_key + self.verification_info["hydraveil_public_key"].setText( + self.truncate_key(hydraveil_key) if hydraveil_key != "N/A" else hydraveil_key) + + nostr_verification = operator.nostr_attestation_event_reference or "N/A" + self.verification_full_values["nostr_attestation_event_reference"] = nostr_verification + self.verification_info["nostr_attestation_event_reference"].setText(self.truncate_key( + nostr_verification) if nostr_verification != "N/A" else nostr_verification) + else: + self.verification_info["operator_name"].setText("N/A") + self.verification_full_values["operator_name"] = "N/A" + self.verification_info["nostr_public_key"].setText("N/A") + self.verification_full_values["nostr_public_key"] = "N/A" + self.verification_info["hydraveil_public_key"].setText("N/A") + self.verification_full_values["hydraveil_public_key"] = "N/A" + self.verification_info["nostr_attestation_event_reference"].setText( + "N/A") + self.verification_full_values["nostr_attestation_event_reference"] = "N/A" + + for key, widget in self.verification_info.items(): + if key in self.verification_checkmarks: + full_value = self.verification_full_values.get(key, "") + if full_value and full_value != "N/A": + self.verification_checkmarks[key].show() + else: + self.verification_checkmarks[key].hide() + + def save_verification_settings(self): + try: + ConfigurationController.set_endpoint_verification_enabled( + self.endpoint_verification_checkbox.isChecked()) + self.update_status.update_status( + "Verification settings saved successfully") + except Exception as e: + logging.error(f"Error saving verification settings: {str(e)}") + self.update_status.update_status( + "Error saving verification settings") + + def copy_verification_value(self, key): + full_value = self.verification_full_values.get(key, "") + if full_value and full_value != "N/A": + clipboard = QApplication.clipboard() + clipboard.setText(full_value) + verification_display_names = { + "operator_name": "Operator Name", + "nostr_public_key": "Nostr Key", + "hydraveil_public_key": "HydraVeil Key", + "nostr_attestation_event_reference": "Nostr Verification" + } + display_name = verification_display_names.get( + key, "Verification value") + self.update_status.update_status( + f"{display_name} copied to clipboard") + + def update_subscription_info(self, index): + if index < 0: + return + + profile_id = self.profile_selector.itemData(index) + if profile_id is None: + return + + profile = ProfileController.get(profile_id) + + if profile and hasattr(profile, 'subscription') and profile.subscription: + try: + self.subscription_info["billing_code"].setText( + str(profile.subscription.billing_code)) + + if hasattr(profile.subscription, 'expires_at') and profile.subscription.expires_at: + expires_at = profile.subscription.expires_at.strftime( + "%Y-%m-%d %H:%M:%S UTC") + self.subscription_info["expires_at"].setText(expires_at) + else: + self.subscription_info["expires_at"].setText( + "Not available") + except Exception as e: + print(f"Error updating subscription info: {e}") + else: + for label in self.subscription_info.values(): + label.setText("N/A") + + def copy_billing_code(self): + if "billing_code" in self.subscription_info: + billing_code = self.subscription_info["billing_code"].text() + if billing_code and billing_code != "N/A": + clipboard = QApplication.clipboard() + clipboard.setText(billing_code) + self.update_status.update_status( + "Billing code copied to clipboard") + else: + self.update_status.update_status( + "No billing code available to copy") + + def showEvent(self, event): + super().showEvent(event) + + current_index = self.content_layout.currentIndex() + + self.content_layout.removeWidget(self.account_page) + self.account_page = self.create_account_page() + self.content_layout.addWidget(self.account_page) + + self.content_layout.removeWidget(self.subscription_page) + self.subscription_page = self.create_subscription_page() + self.content_layout.addWidget(self.subscription_page) + + self.content_layout.removeWidget(self.tickets_page) + self.tickets_page = self.create_tickets_page() + self.content_layout.addWidget(self.tickets_page) + + self.content_layout.removeWidget(self.registrations_page) + self.registrations_page = self.create_registrations_page() + self.content_layout.addWidget(self.registrations_page) + + self.content_layout.removeWidget(self.verification_page) + self.verification_page = self.create_verification_page() + self.content_layout.addWidget(self.verification_page) + + self.content_layout.removeWidget(self.systemwide_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) + + self.content_layout.removeWidget(self.logs_page) + self.logs_page = self.create_logs_page() + self.content_layout.addWidget(self.logs_page) + + self.content_layout.removeWidget(self.debug_page) + self.debug_page = self.create_debug_page() + self.content_layout.addWidget(self.debug_page) + + self.content_layout.setCurrentIndex(current_index) + + if self.content_layout.currentWidget() == self.subscription_page: + if self.profile_selector.count() > 0: + self.update_subscription_info(0) + + def create_account_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("Account Overview") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setStyleSheet(f""" + QScrollArea {{ + border: none; + background: transparent; + }} + QScrollArea > QWidget > QWidget {{ + background: transparent; + }} + """) + + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + scroll_layout.setSpacing(15) + + info_sections = [ + ("Profile Statistics", self.create_profile_stats()), + ("System Resources", self.create_system_stats()) + ] + + for section_title, content_widget in info_sections: + group = QGroupBox(section_title) + group.setStyleSheet(f""" + QGroupBox {{ + color: white; + font-weight: bold; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 15px; + margin-top: 15px; + {self.font_style} + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + }} + """) + group_layout = QVBoxLayout(group) + group_layout.addWidget(content_widget) + scroll_layout.addWidget(group) + + scroll_area.setWidget(scroll_content) + layout.addWidget(scroll_area) + return page + + def create_profile_stats(self): + widget = QWidget() + layout = QGridLayout(widget) + layout.setSpacing(10) + + profiles = ProfileController.get_all() + total_profiles = len(profiles) + session_profiles = sum(1 for p in profiles.values() + if isinstance(p, SessionProfile)) + system_profiles = sum(1 for p in profiles.values() + if isinstance(p, SystemProfile)) + active_profiles = sum( + 1 for p in profiles.values() + if p.subscription and p.subscription.expires_at and p.subscription.expires_at > datetime.now(timezone.utc) + ) + + stats = [ + ("Total Profiles", total_profiles), + ("Browser-only", session_profiles), + ("System-wide", system_profiles), + ("Active Profiles", active_profiles) + ] + + for i, (label, value) in enumerate(stats): + stat_widget = self.create_stat_widget(label, value) + row, col = divmod(i, 2) + layout.addWidget(stat_widget, row, col) + + return widget + + def create_system_stats(self): + widget = QWidget() + layout = QGridLayout(widget) + layout.setSpacing(10) + + config_path = Constants.HV_CONFIG_HOME + + current_connection = self.update_status.get_current_connection() + + if current_connection is not None: + current_connection = current_connection.capitalize() + + stats = [ + ("Config Path", config_path), + ("Client Version", Constants.HV_CLIENT_VERSION_NUMBER), + ("Connection", current_connection), + ] + + for i, (label, value) in enumerate(stats): + info_widget = QLabel(f"{label}: {value}") + info_widget.setStyleSheet( + f"font-size: 14px; color: white; padding: 5px; {self.font_style}") + info_widget.setWordWrap(True) + layout.addWidget(info_widget, i, 0) + + return widget + + def create_stat_widget(self, label, value): + widget = QFrame() + widget.setStyleSheet(f""" + QFrame {{ + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 10px; + {self.font_style} + }} + """) + layout = QVBoxLayout(widget) + + value_label = QLabel(str(value)) + value_label.setObjectName("value_label") + value_label.setStyleSheet( + "font-family: 'Retro Gaming', sans-serif; color: #00ffff; font-size: 22px; font-weight: bold") + value_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + desc_label = QLabel(label) + desc_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + layout.addWidget(value_label) + layout.addWidget(desc_label) + return widget + + def create_horizontal_stat(self, label, value, total): + widget = QWidget() + layout = QHBoxLayout(widget) + layout.setContentsMargins(0, 5, 0, 5) + + text_label = QLabel(f"{label}") + text_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + text_label.setFixedWidth(100) + + progress = QFrame() + progress.setStyleSheet(""" + QFrame { + background: rgba(0, 255, 255, 0.3); + border-radius: 3px; + } + """) + percentage = (value / total) * 100 if total > 0 else 0 + progress.setFixedSize(int(200 * (percentage / 100)), 20) + + value_label = QLabel(f"{value} ({percentage:.1f}%)") + value_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + value_label.setAlignment(Qt.AlignmentFlag.AlignRight) + + progress_container = QFrame() + progress_container.setStyleSheet(""" + QFrame { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + } + """) + progress_container.setFixedSize(200, 20) + progress_layout = QHBoxLayout(progress_container) + progress_layout.setContentsMargins(0, 0, 0, 0) + progress_layout.addWidget(progress) + progress.setFixedHeight(20) + + layout.addWidget(text_label) + layout.addWidget(progress_container) + layout.addWidget(value_label) + layout.addStretch() + + return widget + + def setup_pages(self): + self.account_page = self.create_account_page() + self.subscription_page = self.create_subscription_page() + self.tickets_page = self.create_tickets_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() + + self.content_layout.addWidget(self.account_page) + self.content_layout.addWidget(self.subscription_page) + self.content_layout.addWidget(self.tickets_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) + + self.content_layout.setCurrentIndex(0) + self.show_account_page() + + def show_account_page(self): + core_logger.info("User navigated to Settings -> Overview") + self.content_layout.setCurrentWidget(self.account_page) + self._select_menu_button("Overview") + + def show_subscription_page(self): + core_logger.info("User navigated to Settings -> Subscriptions") + self.content_layout.setCurrentWidget(self.subscription_page) + self._select_menu_button("Subscriptions") + + def show_tickets_page(self): + core_logger.info("User navigated to Settings -> Tickets") + self.content_layout.setCurrentWidget(self.tickets_page) + self._select_menu_button("Tickets") + self._load_random_tickets_state() + self._refresh_tickets_inventory() + self._refresh_ticket_recovery_controls() + + def _load_random_tickets_state(self): + if getattr(self, '_random_tickets_state_loaded', False): + return + try: + which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer) + checked = which_ticket is not None and which_ticket != 'error' + except Exception: + checked = False + self.random_tickets_checkbox.blockSignals(True) + self.random_tickets_checkbox.setChecked(checked) + self.random_tickets_checkbox.blockSignals(False) + self._random_tickets_state_loaded = True + + def show_registrations_page(self): + core_logger.info("User navigated to Settings -> Create/Edit") + self.content_layout.setCurrentWidget(self.registrations_page) + self._select_menu_button("Create/Edit") + + def show_verification_page(self): + core_logger.info("User navigated to Settings -> Verification") + self.content_layout.setCurrentWidget(self.verification_page) + self._select_menu_button("Verification") + + def show_systemwide_page(self): + core_logger.info("User navigated to Settings -> Legacy-Version") + 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) + self._select_menu_button("Error Logs") + + def show_delete_page(self): + core_logger.info("User navigated to Settings -> Delete Profile") + self.content_layout.setCurrentWidget(self.delete_page) + self._select_menu_button("Delete Profile") + + def show_debug_page(self): + core_logger.info("User navigated to Settings -> Debug Help") + self.content_layout.setCurrentWidget(self.debug_page) + self._select_menu_button("Debug Help") + + def _select_menu_button(self, label): + for btn in self.menu_buttons: + if btn.text() == label: + btn.setChecked(True) + else: + btn.setChecked(False) + + def create_tickets_page(self): + page = QWidget() + page_layout = QVBoxLayout(page) + page_layout.setSpacing(15) + page_layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("TICKETS") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + page_layout.addWidget(title) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + scroll_area.setStyleSheet(""" + QScrollArea { + background-color: transparent; + border: none; + } + QScrollArea > QWidget > QWidget { + background-color: transparent; + } + """) + + scroll_content = QWidget() + layout = QVBoxLayout(scroll_content) + layout.setSpacing(15) + layout.setContentsMargins(0, 0, 8, 0) + + random_group = QGroupBox("Random Ticket Use") + random_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + random_layout = QVBoxLayout(random_group) + + self.random_tickets_checkbox = QCheckBox("Use a random ticket automatically when enabling a profile") + self.random_tickets_checkbox.setStyleSheet(f"color: white; {self.font_style}") + self.random_tickets_checkbox.setChecked(False) + self.random_tickets_checkbox.toggled.connect(self._on_random_toggle) + self._random_tickets_state_loaded = False + random_layout.addWidget(self.random_tickets_checkbox) + layout.addWidget(random_group) + + inventory_group = QGroupBox("Unused Tickets") + inventory_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + inventory_layout = QVBoxLayout(inventory_group) + + self.tickets_inventory_label = QLabel("Loading tickets...") + self.tickets_inventory_label.setStyleSheet( + f"color: white; font-size: 13px; {self.font_style}") + self.tickets_inventory_label.setWordWrap(True) + inventory_layout.addWidget(self.tickets_inventory_label) + + self.refresh_tickets_button = QPushButton("Refresh") + self.refresh_tickets_button.setFixedSize(120, 32) + self.refresh_tickets_button.setStyleSheet(f""" + QPushButton {{ + background: #00aaff; color: white; border: none; + border-radius: 5px; font-weight: bold; {self.font_style} + }} + QPushButton:hover {{ background: #0088cc; }} + """) + self.refresh_tickets_button.clicked.connect(self._refresh_tickets_inventory) + inventory_layout.addWidget(self.refresh_tickets_button) + layout.addWidget(inventory_group) + + saved_failure = self.update_status.get_ticket_verification_failure() + has_failure = saved_failure is not None + + recovery_group = QGroupBox("Verification Failure Recovery") + recovery_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + recovery_layout = QVBoxLayout(recovery_group) + + self.ticket_recovery_status_label = QLabel(self._format_ticket_failure_status(saved_failure)) + self.ticket_recovery_status_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + self.ticket_recovery_status_label.setWordWrap(True) + recovery_layout.addWidget(self.ticket_recovery_status_label) + + self.evaluate_public_key_button = QPushButton("Evaluate Public Key") + self.evaluate_public_key_button.setFixedSize(180, 36) + self.evaluate_public_key_button.setEnabled(has_failure) + self.evaluate_public_key_button.setStyleSheet(f""" + QPushButton {{ + background: #00aaff; color: white; border: none; + border-radius: 5px; font-weight: bold; {self.font_style} + }} + QPushButton:hover:!disabled {{ background: #0088cc; }} + QPushButton:disabled {{ background: #666666; color: #bbbbbb; }} + """) + self.evaluate_public_key_button.clicked.connect(self.evaluate_ticket_public_key) + recovery_layout.addWidget(self.evaluate_public_key_button) + + self.ticket_recovery_output = TerminalWidget() + self.ticket_recovery_output.setFixedHeight(130) + if not has_failure: + self.ticket_recovery_output.setPlainText("No saved verification failure.") + recovery_layout.addWidget(self.ticket_recovery_output) + + layout.addWidget(recovery_group) + + debug_group = QGroupBox("Debug Recovery") + debug_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + debug_layout = QVBoxLayout(debug_group) + + self.prepare_saved_blind_sigs_button = QPushButton( + "Prepare Tickets even if validation of the server's signature failed") + self.prepare_saved_blind_sigs_button.setFixedSize(500, 40) + self.prepare_saved_blind_sigs_button.setEnabled(has_failure) + self.prepare_saved_blind_sigs_button.setStyleSheet(f""" + QPushButton {{ + background: #c0392b; color: white; border: none; + border-radius: 5px; font-size: 10px; font-weight: bold; {self.font_style} + }} + QPushButton:hover:!disabled {{ background: #a93226; }} + QPushButton:disabled {{ background: #666666; color: #bbbbbb; }} + """) + self.prepare_saved_blind_sigs_button.clicked.connect(self.prepare_tickets_with_saved_blind_signatures) + debug_layout.addWidget(self.prepare_saved_blind_sigs_button) + + layout.addWidget(debug_group) + layout.addStretch() + scroll_area.setWidget(scroll_content) + page_layout.addWidget(scroll_area) + return page + + def _format_ticket_failure_status(self, failure): + if not failure: + return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here." + failed_validations = failure.get("failed_validations", []) + how_many_failed = failure.get("how_many_failed", len(failed_validations)) + updated_at = failure.get("updated_at", "unknown time") + failed_text = ", ".join(str(item) for item in failed_validations) + return ( + f"Saved verification failure: {how_many_failed} failed. " + f"Failed validation indices: {failed_text}. Saved: {updated_at}" + ) + + def _refresh_ticket_recovery_controls(self): + failure = self.update_status.get_ticket_verification_failure() + has_failure = failure is not None + if hasattr(self, 'ticket_recovery_status_label'): + self.ticket_recovery_status_label.setText(self._format_ticket_failure_status(failure)) + if hasattr(self, 'evaluate_public_key_button'): + self.evaluate_public_key_button.setEnabled(has_failure) + if hasattr(self, 'prepare_saved_blind_sigs_button'): + self.prepare_saved_blind_sigs_button.setEnabled(has_failure) + + def _set_ticket_recovery_busy(self, busy): + if hasattr(self, 'evaluate_public_key_button'): + self.evaluate_public_key_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None) + if hasattr(self, 'prepare_saved_blind_sigs_button'): + self.prepare_saved_blind_sigs_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None) + + def _write_ticket_recovery_output(self, text): + if hasattr(self, 'ticket_recovery_output'): + self.ticket_recovery_output.setPlainText(text) + + def _format_ticket_recovery_result(self, label, result): + try: + payload = json.dumps(result, indent=2, default=str) + except TypeError: + payload = str(result) + return f"{label}:\n{payload}" + + def _get_ticket_failure_for_action(self): + failure = self.update_status.get_ticket_verification_failure() + if failure is None: + self._write_ticket_recovery_output("No saved verification failure.") + self.update_status.update_status("No ticket verification failure is saved.") + self._refresh_ticket_recovery_controls() + return None + return failure + + def evaluate_ticket_public_key(self): + core_logger.info("User clicked 'Evaluate Public Key' button") + failure = self._get_ticket_failure_for_action() + if failure is None: + return + failed_validations = list(failure.get("failed_validations", [])) + self._write_ticket_recovery_output("Evaluating public key...") + self.update_status.update_status("Evaluating ticket public key...") + self._set_ticket_recovery_busy(True) + + self.ticket_recovery_worker = TicketingWorkerThread( + 'EVALUATE_FAILED_VERIFICATION', + params={'failed_validations': failed_validations}, + ) + self.ticket_recovery_worker.failed_verification_evaluated.connect(self.on_failed_verification_evaluated) + self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error) + self.ticket_recovery_worker.start() + + def on_failed_verification_evaluated(self, result): + message = interpret_key_results(result) + self._write_ticket_recovery_output(message) + core_logger.info(f"Public key evaluation result: {message}") + self.update_status.update_status("Ticket public key evaluation complete.") + self._set_ticket_recovery_busy(False) + self._refresh_ticket_recovery_controls() + + def prepare_tickets_with_saved_blind_signatures(self): + core_logger.info("User clicked 'Prepare Tickets even if validation of the server's signature failed' button") + failure = self._get_ticket_failure_for_action() + if failure is None: + return + + reply = QMessageBox.warning( + self, + "Prepare Tickets Anyway", + "This will prepare tickets even though server signature validation failed. Continue only if you have evaluated the situation.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if reply != QMessageBox.StandardButton.Yes: + return + + self._write_ticket_recovery_output("Preparing tickets with saved blind signatures...") + self.update_status.update_status("Preparing tickets with saved blind signatures...") + self._set_ticket_recovery_busy(True) + + self.ticket_recovery_worker = TicketingWorkerThread('PREPARE_SAVED_BLIND_SIGS') + self.ticket_recovery_worker.saved_blind_prep_done.connect(self.on_saved_blind_prep_done) + self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error) + self.ticket_recovery_worker.start() + + def on_saved_blind_prep_done(self, result): + self._write_ticket_recovery_output(self._format_ticket_recovery_result("Results of preparation", result)) + if isinstance(result, dict) and result.get('valid') is True: + self.update_status.clear_ticket_verification_failure() + self.update_status.update_status("Tickets prepared from saved blind signatures.") + self._refresh_tickets_inventory() + else: + msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed' + self.update_status.update_status(f"Saved blind signature prep failed: {msg}") + self._set_ticket_recovery_busy(False) + self._refresh_ticket_recovery_controls() + + def on_ticket_recovery_error(self, msg): + core_logger.error(f"Ticket recovery error: {msg}") + self._write_ticket_recovery_output(f"EXCEPTION: {msg}") + self.update_status.update_status(f"Ticket recovery error: {msg}") + self._set_ticket_recovery_busy(False) + self._refresh_ticket_recovery_controls() + + def _on_random_toggle(self, checked): + core_logger.info(f"User toggled random ticket setting to {'on' if checked else 'off'}") + try: + result = modify_random_tickets_setting('on' if checked else 'off', ticket_observer) + if not (isinstance(result, dict) and result.get('valid')): + msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed' + core_logger.error(f"Could not change random ticket setting: {msg}") + self.update_status.update_status(f'Could not change setting: {msg}') + except Exception as e: + core_logger.error(f"Exception changing random ticket setting: {e}") + self.update_status.update_status(f'Could not change setting: {e}') + + def _refresh_tickets_inventory(self): + if not hasattr(self, 'tickets_inventory_label'): + return + core_logger.info("Refreshing tickets inventory") + try: + unused = get_unused_tickets(ticket_observer) + except Exception as e: + core_logger.error(f"Error fetching unused tickets: {e}") + self.tickets_inventory_label.setText(f"Error: {e}") + return + if isinstance(unused, dict) and unused.get('valid'): + data = unused.get('data', []) + if data: + self.tickets_inventory_label.setText( + f"You have {len(data)} unused tickets: " + ", ".join(f"#{t}" for t in data)) + else: + self.tickets_inventory_label.setText("No unused tickets.") + else: + msg = unused.get('message', 'No tickets found.') if isinstance(unused, dict) else 'No tickets found.' + self.tickets_inventory_label.setText(msg) + + def create_logs_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("LOGGING SETTINGS") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + log_text = QLabel( + f"Enabling this feature means error logs will be stored on your local\nmachine at: '{Constants.HV_CACHE_HOME}/gui'.") + log_text.setStyleSheet( + f"color: white; font-size: 14px; {self.font_style}") + layout.addWidget(log_text) + + logs_group = QGroupBox("Log Configuration") + logs_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + logs_layout = QVBoxLayout(logs_group) + + self.enable_gui_logging = QCheckBox("Enable GUI logging") + self.enable_gui_logging.setStyleSheet(self.get_checkbox_style()) + logs_layout.addWidget(self.enable_gui_logging) + layout.addWidget(logs_group) + + save_button = QPushButton() + save_button.setFixedSize(75, 46) + save_button.setIcon(QIcon(os.path.join(self.btn_path, f"save.png"))) + save_button.setIconSize(QSize(75, 46)) + save_button.clicked.connect(self.save_logs_settings) + + button_layout = QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(save_button) + layout.addLayout(button_layout) + + layout.addStretch() + self.load_logs_settings() + return page + + def load_logs_settings(self) -> None: + try: + config = self.update_status._load_gui_config() + if config and "logging" in config: + self.enable_gui_logging.setChecked( + config["logging"]["gui_logging_enabled"]) + except Exception as e: + logging.error(f"Error loading logging settings: {str(e)}") + self.enable_gui_logging.setChecked(True) + + def save_logs_settings(self) -> None: + try: + config = self.update_status._load_gui_config() + if config is None: + config = { + "logging": { + "gui_logging_enabled": True, + "log_level": "INFO" + } + } + + config["logging"]["gui_logging_enabled"] = self.enable_gui_logging.isChecked() + + self.update_status._save_gui_config(config) + + if self.enable_gui_logging.isChecked(): + self.update_status._setup_gui_logging() + core_logger.info("User enabled GUI logging") + else: + core_logger.info("User disabled GUI logging") + self.update_status.stop_gui_logging() + + self.update_status.update_status( + "Logging settings saved successfully") + + except Exception as e: + logging.error(f"Error saving logging settings: {str(e)}") + self.update_status.update_status("Error saving logging settings") + + def create_registrations_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("Create/Edit SETTINGS") + title.setStyleSheet( + f"color: #808080; font-size: 15px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + registrations_group = QGroupBox("Create/Edit Configuration") + registrations_group.setStyleSheet( + f"QGroupBox {{ color: white; font-size: 15px; padding: 15px; {self.font_style} }}") + registrations_layout = QVBoxLayout(registrations_group) + + self.enable_auto_sync = QCheckBox("Enable auto-sync on Edit") + self.enable_auto_sync.setStyleSheet(self.get_checkbox_style()) + registrations_layout.addWidget(self.enable_auto_sync) + + self.enable_fast_registration = QCheckBox( + "Enable Fast-mode for Profile Creation") + self.enable_fast_registration.setStyleSheet(self.get_checkbox_style()) + registrations_layout.addWidget(self.enable_fast_registration) + + self.dynamic_profiles_container = QWidget() + dynamic_layout = QVBoxLayout(self.dynamic_profiles_container) + dynamic_layout.setContentsMargins(20, 0, 0, 0) + + self.enable_dynamic_profiles = QCheckBox( + "Enable Dynamic Larger Profiles") + self.enable_dynamic_profiles.setStyleSheet(self.get_checkbox_style()) + dynamic_layout.addWidget(self.enable_dynamic_profiles) + + dynamic_desc = QLabel("This is for fast mode profile creation. Some users may wish to have screen sizes that are nearly as large as their host, but slightly smaller by differing amounts. This fools fingerprinters but still gives larger sizes. So for example if your host is 1200 x 800, then fast mode would offer 1150 x 750, 1100 x 700, 1050 x 650, etc.") + dynamic_desc.setWordWrap(True) + dynamic_desc.setStyleSheet( + f"color: #888888; font-size: 11px; font-style: italic; {self.font_style}") + dynamic_layout.addWidget(dynamic_desc) + + registrations_layout.addWidget(self.dynamic_profiles_container) + + self.enable_fast_registration.toggled.connect( + lambda checked: self.dynamic_profiles_container.setVisible(checked)) + + layout.addWidget(registrations_group) + + save_button = QPushButton() + save_button.setFixedSize(75, 46) + save_button.setIcon(QIcon(os.path.join(self.btn_path, f"save.png"))) + save_button.setIconSize(QSize(75, 46)) + save_button.clicked.connect(self.save_registrations_settings) + + button_layout = QHBoxLayout() + button_layout.addWidget(save_button) + button_layout.addStretch() + layout.addLayout(button_layout) + + layout.addStretch() + self.load_registrations_settings() + return page + + def create_systemwide_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + layout.setContentsMargins(20, 20, 20, 20) + + title = QLabel("SYSTEM-WIDE PROFILES") + title.setStyleSheet( + f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") + layout.addWidget(title) + + description = QLabel( + "This is for the old version of the app! The new version prior to 2.1.0 does not need it. This allows you to enable or disable a legacy systemwide policy for sudo.") + 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.systemwide_status_value = QLabel("") + self.systemwide_status_value.setStyleSheet( + f"color: #e67e22; font-size: 14px; {self.font_style}") + status_layout.addWidget(status_label) + status_layout.addWidget(self.systemwide_status_value) + status_layout.addStretch() + layout.addLayout(status_layout) + + toggle_layout = QHBoxLayout() + self.systemwide_toggle = QCheckBox( + "Enable \"No Sudo\" Systemwide Policy") + self.systemwide_toggle.setStyleSheet(self.get_checkbox_style()) + toggle_layout.addWidget(self.systemwide_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_systemwide_settings) + + button_layout = QHBoxLayout() + button_layout.addWidget(save_button) + button_layout.addStretch() + layout.addLayout(button_layout) + + layout.addStretch() + self.load_systemwide_settings() + 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() + return page + + def load_systemwide_settings(self): + enabled = False + try: + privilege_policy = PolicyController.get('privilege') + if privilege_policy is not None: + enabled = PolicyController.is_instated(privilege_policy) + except Exception: + enabled = False + self.systemwide_toggle.setChecked(enabled) + if enabled: + self.systemwide_status_value.setText("Enabled") + self.systemwide_status_value.setStyleSheet( + f"color: #2ecc71; font-size: 14px; {self.font_style}") + else: + self.systemwide_status_value.setText("Disabled") + self.systemwide_status_value.setStyleSheet( + f"color: #e67e22; font-size: 14px; {self.font_style}") + + def save_systemwide_settings(self): + enable = self.systemwide_toggle.isChecked() + try: + privilege_policy = PolicyController.get('privilege') + if privilege_policy is not None: + if enable: + PolicyController.instate(privilege_policy) + else: + if PolicyController.is_instated(privilege_policy): + PolicyController.revoke(privilege_policy) + self.load_systemwide_settings() + self.update_status.update_status( + "System-wide policy settings updated") + except CommandNotFoundError as e: + self.systemwide_status_value.setText(str(e)) + self.systemwide_status_value.setStyleSheet( + f"color: red; font-size: 14px; {self.font_style}") + except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e: + self.systemwide_status_value.setText(str(e)) + self.systemwide_status_value.setStyleSheet( + f"color: red; font-size: 14px; {self.font_style}") + except Exception: + self.systemwide_status_value.setText("Failed to update policy") + self.systemwide_status_value.setStyleSheet( + f"color: red; font-size: 14px; {self.font_style}") + + def load_bwrap_settings(self): + enabled = False + try: + capability_policy = PolicyController.get('capability') + if capability_policy is not None: + enabled = PolicyController.is_instated(capability_policy) + except Exception: + enabled = False + self.bwrap_toggle.setChecked(enabled) + 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.update_status._load_gui_config() + if config and "registrations" in config: + registrations = config["registrations"] + auto_sync = registrations.get("auto_sync_enabled", False) + fast_reg = registrations.get( + "fast_registration_enabled", False) + dynamic_profiles = registrations.get( + "dynamic_larger_profiles", False) + if "auto_sync_enabled" not in registrations or "fast_registration_enabled" not in registrations or "dynamic_larger_profiles" not in registrations: + registrations["auto_sync_enabled"] = auto_sync + registrations["fast_registration_enabled"] = fast_reg + registrations["dynamic_larger_profiles"] = dynamic_profiles + self.update_status._save_gui_config(config) + self.enable_auto_sync.setChecked(auto_sync) + self.enable_fast_registration.setChecked(fast_reg) + self.enable_dynamic_profiles.setChecked(dynamic_profiles) + self.dynamic_profiles_container.setVisible(fast_reg) + else: + self.enable_auto_sync.setChecked(False) + self.enable_fast_registration.setChecked(False) + self.enable_dynamic_profiles.setChecked(False) + self.dynamic_profiles_container.setVisible(False) + except Exception as e: + logging.error(f"Error loading registration settings: {str(e)}") + self.enable_auto_sync.setChecked(False) + self.enable_fast_registration.setChecked(False) + self.enable_dynamic_profiles.setChecked(False) + self.dynamic_profiles_container.setVisible(False) + + def save_registrations_settings(self) -> None: + try: + config = self.update_status._load_gui_config() + if config is None: + config = { + "logging": { + "gui_logging_enabled": True, + "log_level": "INFO" + }, + "registrations": { + "auto_sync_enabled": False, + "fast_registration_enabled": False + } + } + + if "registrations" not in config: + config["registrations"] = {} + + config["registrations"]["auto_sync_enabled"] = self.enable_auto_sync.isChecked() + config["registrations"]["fast_registration_enabled"] = self.enable_fast_registration.isChecked() + config["registrations"]["dynamic_larger_profiles"] = self.enable_dynamic_profiles.isChecked() + + self.update_status._save_gui_config(config) + self.update_status.update_status("Registration settings saved") + + except Exception as e: + logging.error(f"Error saving registration settings: {str(e)}") + self.update_status.update_status( + "Error saving registration settings") + + def is_auto_sync_enabled(self) -> bool: + try: + config = self.update_status._load_gui_config() + if config and "registrations" in config: + registrations = config["registrations"] + return registrations.get("auto_sync_enabled", False) + return False + except Exception as e: + logging.error(f"Error checking auto-sync setting: {str(e)}") + return False + + def is_fast_registration_enabled(self) -> bool: + try: + config = self.update_status._load_gui_config() + if config and "registrations" in config: + registrations = config["registrations"] + return registrations.get("fast_registration_enabled", False) + return False + except Exception as e: + logging.error( + f"Error checking fast registration setting: {str(e)}") + return False diff --git a/gui/v2/ui/pages/sync_screen.py b/gui/v2/ui/pages/sync_screen.py new file mode 100755 index 0000000..0bae2e8 --- /dev/null +++ b/gui/v2/ui/pages/sync_screen.py @@ -0,0 +1,177 @@ +import os + +from PyQt6.QtWidgets import QLabel, QPushButton +from PyQt6.QtGui import QPixmap + +from core.controllers.ClientController import ClientController + +from gui.v2.ui.pages.Page import Page +from gui.v2.workers.worker_thread import WorkerThread + + +class SyncScreen(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("SyncScreen", page_stack, main_window, parent) + self.main_window = main_window + self.btn_path = main_window.btn_path + self.update_status = main_window + self.connection_manager = main_window.connection_manager + self.is_tor_mode = main_window.is_tor_mode + + self.heading_label = QLabel( + "You're about to fetch data from\nSimplified Privacy.", self) + self.heading_label.setGeometry(15, 80, 750, 120) + font_style = "font-size: 34px; font-weight: bold; color: white;" + if self.custom_window.open_sans_family: + font_style += f" font-family: '{self.custom_window.open_sans_family}';" + self.heading_label.setStyleSheet(font_style) + + self.data_description = QLabel( + "This data includes what plans, countries,\nbrowsers, and version upgrades are\navailable. As well as coordinating billing.", self) + self.data_description.setGeometry(22, 190, 750, 120) + font_style = "font-size: 24px; font-weight: bold; color: #ffff00;" + if self.custom_window.open_sans_family: + font_style += f" font-family: '{self.custom_window.open_sans_family}';" + self.data_description.setStyleSheet(font_style) + + self.instructions = QLabel( + "Use the toggle in the bottom right to\ndecide if you want to use Tor or not.\nThen hit \"Next\"", self) + self.instructions.setGeometry(22, 345, 750, 120) + font_style = "font-size: 28px; font-weight: bold; color: white;" + if self.custom_window.open_sans_family: + font_style += f" font-family: '{self.custom_window.open_sans_family}';" + self.instructions.setStyleSheet(font_style) + + self.arrow_label = QLabel(self) + self.arrow_label.setGeometry(520, 400, 180, 130) + arrow_pixmap = QPixmap(os.path.join(self.btn_path, "arrow-down.png")) + + self.arrow_label.setPixmap(arrow_pixmap) + self.arrow_label.raise_() + + self.button_go.setVisible(True) + self.button_back.setVisible(True) + self.button_go.clicked.connect(self.perform_sync) + self.button_back.clicked.connect(self.go_back) + + def go_back(self): + self.custom_window.navigator.navigate("menu") + + def perform_sync(self): + self.button_go.setEnabled(False) + self.button_back.setEnabled(False) + self.update_status.update_status('Sync in progress...') + if self.main_window.get_current_connection() == 'tor': + self.worker_thread = WorkerThread('SYNC_TOR') + else: + self.worker_thread = WorkerThread('SYNC') + + self.worker_thread.sync_output.connect(self.update_output) + self.worker_thread.start() + + def update_output(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): + if isinstance(all_browsers, bool) and not all_browsers: + self.custom_window.navigator.navigate("install_system_package") + install_page = self.custom_window.navigator.get_cached("install_system_package") + if install_page is not None: + install_page.configure( + package_name='tor', distro='debian', is_sync=True) + self.button_go.setEnabled(True) + self.button_back.setEnabled(True) + return + + if status is False: + self.button_go.setEnabled(True) + self.button_back.setEnabled(True) + self.update_status.update_status('An error occurred during sync') + return + + self.update_status.update_status('Sync complete') + + update_available = ClientController.can_be_updated() + + if update_available: + menu_page = self.find_menu_page() + if menu_page is not None: + menu_page.on_update_check_finished() + + self.custom_window.update_values( + available_locations, available_browsers, status, is_tor, locations, all_browsers) + + self.custom_window.navigator.navigate("protocol") + + def update_after_sync(self, available_locations, available_browsers, locations, all_browsers=None): + self.connection_manager.set_synced(True) + + available_locations_list = [] + available_browsers_list = [] + + self.connection_manager.store_locations(locations) + self.connection_manager.store_browsers(available_browsers) + + browser_positions = self.generate_grid_positions( + len(available_browsers)) + location_positions = self.generate_grid_positions( + len(available_locations)) + + for i, location in enumerate(available_locations): + available_locations_list.append( + (QPushButton, location, location_positions[i])) + + for i, browser in enumerate(available_browsers): + available_browsers_list.append( + (QPushButton, browser, browser_positions[i])) + + location_page = self.find_location_page() + hidetor_page = self.find_hidetor_page() + protocol_page = self.find_protocol_page() + browser_page = self.find_browser_page() + + if browser_page: + browser_page.create_interface_elements(available_browsers_list) + + if location_page: + location_page.create_interface_elements(available_locations_list) + if hidetor_page: + hidetor_page.create_interface_elements(available_locations_list) + if protocol_page: + protocol_page.enable_protocol_buttons() + + menu_page = self.find_menu_page() + if menu_page: + menu_page.refresh_profiles_data() + + def generate_grid_positions(self, num_items): + positions = [] + start_x = 395 + start_y = 90 + button_width = 185 + button_height = 75 + h_spacing = 10 + v_spacing = 105 + + for i in range(num_items): + col = i % 2 + row = i // 2 + + x = start_x + (col * (button_width + h_spacing)) + y = start_y + (row * v_spacing) + + positions.append((x, y, button_width, button_height)) + + return positions + + def find_browser_page(self): + return self.custom_window.navigator.get_cached("browser") + + def find_location_page(self): + return self.custom_window.navigator.get_cached("location") + + def find_hidetor_page(self): + return self.custom_window.navigator.get_cached("hidetor") + + def find_protocol_page(self): + return self.custom_window.navigator.get_cached("protocol") + + def find_menu_page(self): + return self.custom_window.navigator.get_cached("menu") diff --git a/gui/v2/ui/pages/systemwide_prompt_page.py b/gui/v2/ui/pages/systemwide_prompt_page.py new file mode 100755 index 0000000..3ddf18a --- /dev/null +++ b/gui/v2/ui/pages/systemwide_prompt_page.py @@ -0,0 +1,115 @@ +import os + +from PyQt6.QtWidgets import QLabel, QPushButton +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt + +from core.controllers.PolicyController import PolicyController +from core.Errors import ( + CommandNotFoundError, + PolicyAssignmentError, + PolicyInstatementError, +) + +from gui.v2.ui.pages.Page import Page + + +class SystemwidePromptPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Systemwide", page_stack, main_window, parent) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.button_back.setVisible(True) + self.button_back.clicked.connect(self.back_to_install) + self.button_next.setVisible(False) + self.button_go.setVisible(False) + self.status_label = QLabel(self) + self.status_label.setGeometry(80, 430, 640, 40) + self.status_label.setStyleSheet("font-size: 14px; color: cyan;") + self.setup_ui() + + def setup_ui(self): + self.title.setGeometry(20, 50, 760, 40) + self.title.setText("Enable \"no sudo\" systemwide") + description = QLabel(self) + description.setGeometry(80, 100, 640, 120) + description.setWordWrap(True) + description.setStyleSheet("font-size: 14px; color: cyan;") + description.setText("If you're using Systemwide profiles, you may wish to enable them without having to enter the sudo password each time for convenience. This requires the sudo password to setup profiles and disable them (for security), but not to turn it on each time. You can choose to set this up now or later in the options menu.") + + icon_label = QLabel(self) + icon_label.setGeometry(80, 300, 64, 64) + icon_pix = QPixmap(os.path.join( + self.btn_path, "wireguard_system_wide.png")) + icon_label.setPixmap(icon_pix.scaled( + 64, 64, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + yes_button = QPushButton("Yes, enable system-wide profiles", self) + yes_button.setGeometry(170, 300, 300, 50) + yes_button.setStyleSheet(""" + QPushButton { + background: #007AFF; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: bold; + } + QPushButton:hover { + background: #0056CC; + } + """) + yes_button.clicked.connect(self.enable_systemwide) + no_button = QPushButton("Not now", self) + no_button.setGeometry(170, 370, 120, 40) + no_button.setStyleSheet(""" + QPushButton { + background: #007AFF; + color: white; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: bold; + } + QPushButton:hover { + background: #0056CC; + } + """) + no_button.clicked.connect(self.skip_systemwide) + self.refresh_status() + + def refresh_status(self): + privilege_policy = PolicyController.get('privilege') + if privilege_policy is not None and PolicyController.is_instated(privilege_policy): + self.status_label.setText( + "Current status: system-wide policy is enabled.") + self.status_label.setStyleSheet("font-size: 14px; color: #2ecc71;") + else: + self.status_label.setText( + "Current status: system-wide policy is disabled.") + self.status_label.setStyleSheet("font-size: 14px; color: #e67e22;") + + def enable_systemwide(self): + try: + privilege_policy = PolicyController.get('privilege') + if privilege_policy is not None: + PolicyController.instate(privilege_policy) + self.update_status.mark_systemwide_prompt_shown() + self.refresh_status() + self.update_status.update_status("System-wide policy enabled") + self.custom_window.navigator.navigate("menu") + except CommandNotFoundError as e: + self.status_label.setText(str(e)) + self.status_label.setStyleSheet("font-size: 14px; color: red;") + except (PolicyAssignmentError, PolicyInstatementError) as e: + self.status_label.setText(str(e)) + self.status_label.setStyleSheet("font-size: 14px; color: red;") + except Exception: + self.status_label.setText("Failed to enable system-wide policy") + self.status_label.setStyleSheet("font-size: 14px; color: red;") + + def skip_systemwide(self): + self.update_status.mark_systemwide_prompt_shown() + self.custom_window.navigator.navigate("menu") + + def back_to_install(self): + self.custom_window.navigator.navigate("install_system_package") diff --git a/gui/v2/ui/pages/ticket_crypto_picker_page.py b/gui/v2/ui/pages/ticket_crypto_picker_page.py new file mode 100755 index 0000000..36bd719 --- /dev/null +++ b/gui/v2/ui/pages/ticket_crypto_picker_page.py @@ -0,0 +1,112 @@ +import os + +from PyQt6.QtWidgets import QButtonGroup, QMessageBox, QPushButton +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import QSize +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page +from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread + + +HOW_MANY_PROFILES_DEFAULT = 6 + + +class TicketCryptoPickerPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("TicketCrypto", page_stack, main_window, parent) + self.update_status = main_window + self.selected_plan = None + self.bypass_existing = False + self.worker = None + + self.title.setText("Payment Method") + self.title.setGeometry(QtCore.QRect(510, 30, 250, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.button_reverse.setVisible(True) + self.button_reverse.clicked.connect(self.reverse) + + button_info = [ + ("monero", "monero", (545, 75)), + ("bitcoin", "bitcoin", (545, 290)), + ("lightning", "lightnering", (545, 180)), + ("litecoin", "litecoin", (545, 395)), + ] + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + for j, (currency, icon_name, position) in enumerate(button_info): + button = QPushButton(self) + button.setGeometry(position[0], position[1], 185, 75) + button.setIconSize(QSize(190, 120)) + button.setCheckable(True) + button.setIcon(QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + button.setProperty('currency', currency) + self.buttons.append(button) + self.buttonGroup.addButton(button, j) + button.clicked.connect(self.on_currency_selected) + + def set_selected_plan(self, plan_key): + self.selected_plan = plan_key + self.bypass_existing = False + self.update_status.update_status(f"Selected plan: {plan_key}") + for btn in self.buttons: + btn.setChecked(False) + + def on_currency_selected(self): + selected_button = self.buttonGroup.checkedButton() + if not selected_button or not self.selected_plan: + return + currency = selected_button.property('currency') + self.start_initiate_payment(currency) + + def start_initiate_payment(self, currency): + self.update_status.update_status("Initiating payment...") + self.worker = TicketingWorkerThread('INITIATE_PAYMENT', params={ + 'how_many_profiles': HOW_MANY_PROFILES_DEFAULT, + 'which_key': self.selected_plan, + 'which_cryptocurrency': currency, + 'bypass_existing': self.bypass_existing, + }) + self.worker.invoice_ready.connect(self.on_invoice_ready) + self.worker.error.connect(self.on_error) + self.worker.start() + + def on_invoice_ready(self, invoice): + if invoice is None or invoice is False: + self.update_status.update_status("Could not initiate payment.") + return + + error_code = getattr(invoice, 'error_code', None) + if error_code == 'already_exists' and not self.bypass_existing: + self._prompt_wipe_existing(invoice) + return + if error_code: + msg = getattr(invoice, 'final_error_msg', None) or error_code + self.update_status.update_status(f"Payment error: {msg}") + return + + 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.set_ticket_invoice(invoice, self.selected_plan) + + def _prompt_wipe_existing(self, invoice): + msg = QMessageBox(self) + 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) + result = msg.exec() + if result == QMessageBox.StandardButton.Yes: + self.bypass_existing = True + currency_btn = self.buttonGroup.checkedButton() + if currency_btn: + self.start_initiate_payment(currency_btn.property('currency')) + else: + self.update_status.update_status("Cancelled.") + + def on_error(self, msg): + self.update_status.update_status(f"Payment error: {msg}") + + def reverse(self): + self.custom_window.navigator.navigate("plan_picker") diff --git a/gui/v2/ui/pages/ticket_or_billing_choice_page.py b/gui/v2/ui/pages/ticket_or_billing_choice_page.py new file mode 100755 index 0000000..7b13173 --- /dev/null +++ b/gui/v2/ui/pages/ticket_or_billing_choice_page.py @@ -0,0 +1,117 @@ +from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton +from PyQt6 import QtCore + +from core.controllers.ProfileController import ProfileController +from core.controllers.tickets.UseTicketController import get_unused_tickets + +from gui.v2.actions.locations import location_candidates +from gui.v2.infrastructure.setup_observers import ticket_observer +from gui.v2.ui.pages.Page import Page + + +class TicketOrBillingChoicePage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("TicketOrBilling", page_stack, main_window, parent) + self.update_status = main_window + + self.title.setText("Pick a Ticket or Use Billing") + self.title.setGeometry(QtCore.QRect(220, 30, 360, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.button_reverse.setVisible(True) + self.button_reverse.clicked.connect(self.reverse) + + self.intro = QLabel( + "Random ticket use is OFF. Pick a ticket to use, or fall back to a billing code.", + self) + self.intro.setGeometry(40, 90, 720, 40) + self.intro.setStyleSheet("color: white; font-size: 13px;") + self.intro.setWordWrap(True) + self.intro.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.ticket_list = QListWidget(self) + self.ticket_list.setGeometry(60, 140, 380, 320) + self.ticket_list.setStyleSheet(""" + QListWidget { + background-color: #000000; color: #00ffff; + border: 2px solid #00ffff; border-radius: 8px; + font-size: 14px; + } + QListWidget::item { padding: 10px; } + QListWidget::item:hover { background-color: #003333; } + QListWidget::item:selected { background-color: #006666; color: white; } + """) + + self.use_ticket_button = QPushButton("Use Selected Ticket", self) + self.use_ticket_button.setGeometry(60, 470, 380, 40) + self.use_ticket_button.setStyleSheet(""" + QPushButton { + background-color: #00aaff; color: white; border: none; + border-radius: 5px; font-size: 14px; font-weight: bold; + } + QPushButton:disabled { background-color: #555555; } + """) + self.use_ticket_button.clicked.connect(self.on_use_ticket) + + self.use_billing_button = QPushButton("Use a Billing Code Instead", self) + self.use_billing_button.setGeometry(470, 200, 280, 60) + self.use_billing_button.setStyleSheet(""" + QPushButton { + background-color: #000000; color: #00ffff; + border: 2px solid #00ffff; border-radius: 10px; + font-size: 14px; font-weight: bold; + } + QPushButton:hover { background-color: #003333; } + """) + self.use_billing_button.clicked.connect(self.on_use_billing) + + self.status_label = QLabel("", self) + self.status_label.setGeometry(20, 520, 760, 20) + self.status_label.setStyleSheet("color: #cccccc; font-size: 12px;") + self.status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + def populate(self): + self.ticket_list.clear() + try: + unused = get_unused_tickets(ticket_observer) + except Exception as e: + self.status_label.setText(f"Error reading tickets: {e}") + return + if unused.get('valid'): + for t in unused.get('data', []): + item = QListWidgetItem(f"Ticket #{t}") + item.setData(QtCore.Qt.ItemDataRole.UserRole, t) + self.ticket_list.addItem(item) + else: + self.status_label.setText(unused.get('message', 'No tickets available.')) + + def on_use_ticket(self): + item = self.ticket_list.currentItem() + if not item: + self.status_label.setText("Pick a ticket from the list first.") + return + which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole) + profile_id = self.update_status.current_profile_id + try: + profile = ProfileController.get(int(profile_id)) + except Exception: + profile = None + candidates = location_candidates(profile) + if not candidates: + self.status_label.setText("Could not determine profile location.") + return + profile_data = { + 'id': int(profile_id), + 'use_ticket': which_ticket, + 'ticket_location': candidates[0], + } + menu_page = self.custom_window.navigator.get_cached("menu") + if menu_page: + self.update_status.update_status(f"Using ticket #{which_ticket}...") + menu_page.enabling_profile(profile_data) + + def on_use_billing(self): + self.custom_window.navigator.navigate("id") + + def reverse(self): + self.custom_window.navigator.navigate("menu") diff --git a/gui/v2/ui/pages/ticket_prep_page.py b/gui/v2/ui/pages/ticket_prep_page.py new file mode 100755 index 0000000..91bb30d --- /dev/null +++ b/gui/v2/ui/pages/ticket_prep_page.py @@ -0,0 +1,117 @@ +from PyQt6.QtWidgets import QLabel, QPushButton +from PyQt6 import QtCore + +from gui.v2.infrastructure.setup_observers import ticket_observer +from gui.v2.ui.pages.Page import Page +from gui.v2.ui.widgets.terminal_widget import TerminalWidget +from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread + + +HOW_MANY_PROFILES_DEFAULT = 6 + + +class TicketPrepPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("TicketPrep", page_stack, main_window, parent) + self.update_status = main_window + self.worker = None + self._terminal_bound = False + self._tickets_ready = False + + self.title.setText("Preparing Tickets") + self.title.setGeometry(QtCore.QRect(280, 20, 240, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.terminal = TerminalWidget(self) + self.terminal.setGeometry(20, 70, 760, 380) + + self.status_label = QLabel("", self) + self.status_label.setGeometry(20, 460, 760, 30) + self.status_label.setStyleSheet("color: #00ffff; font-size: 14px;") + self.status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.continue_button = QPushButton("Continue", self) + self.continue_button.setGeometry(340, 490, 120, 35) + self.continue_button.setStyleSheet(""" + QPushButton { + background-color: #2ecc71; color: white; border: none; + border-radius: 5px; font-size: 14px; font-weight: bold; + } + QPushButton:disabled { background-color: #555555; } + """) + self.continue_button.setEnabled(True) + self.continue_button.clicked.connect(self.on_continue) + + def _bind_terminal_once(self): + if self._terminal_bound: + return + self.terminal.bind_observer(ticket_observer, [ + 'preparing', 'connecting', 'sync_done', 'waiting', 'paid', + 'ticket_ready', 'used', 'failed_input', 'failed_output', + 'connection_error', 'unknown_error', 'error', + ]) + self._terminal_bound = True + + def start_prep(self): + self._bind_terminal_once() + self.terminal.append("=== Starting ticket preparation ===") + self._tickets_ready = False + self.continue_button.setEnabled(True) + self.status_label.setText("Preparing tickets...") + self.update_status.update_status("Preparing tickets...") + + self.worker = TicketingWorkerThread('PREPARE_TICKETS', params={ + 'how_many_profiles': HOW_MANY_PROFILES_DEFAULT, + }) + self.worker.prep_done.connect(self.on_prep_done) + self.worker.error.connect(self.on_error) + self.worker.start() + + def on_prep_done(self, result): + if not isinstance(result, dict): + self.terminal.append("ERROR: invalid result format.") + self.status_label.setText("Preparation failed.") + return + if result.get('valid') is True: + self._tickets_ready = True + self.update_status.clear_ticket_verification_failure() + self.terminal.append("=== Ticket preparation complete ===") + self.status_label.setText("Tickets ready. Click Continue to apply one to your profile.") + self.update_status.update_status("Tickets ready.") + self.continue_button.setEnabled(True) + else: + msg = result.get('message', 'failed') + self.terminal.append(f"ERROR: {msg}") + if msg == 'verification_failed': + how_many_failed = result.get('how_many_failed', '?') + failed_validations = result.get('failed_validations', []) + self.terminal.append(f" failed count: {how_many_failed}") + self.terminal.append(f" failed indices: {failed_validations}") + if self.update_status.save_ticket_verification_failure(result): + self.terminal.append("Recovery data saved. Open Settings > Tickets to evaluate or resume.") + self.status_label.setText("Verification failed. Open Settings > Tickets for recovery.") + self.update_status.update_status("Ticket verification failed") + self.continue_button.setEnabled(True) + return + self.status_label.setText(f"Preparation failed: {msg}") + self.update_status.update_status(f"An error occurred") + self.continue_button.setEnabled(True) + + def on_error(self, msg): + self.terminal.append(f"EXCEPTION: {msg}") + self.status_label.setText(f"An unkown error occured") + self.continue_button.setEnabled(True) + + def on_continue(self): + menu_page = self.custom_window.navigator.get_cached("menu") + profile_id = getattr(self.update_status, 'current_profile_id', None) + if menu_page: + self.custom_window.navigator.navigate("menu") + if not self._tickets_ready: + self.update_status.update_status("Ticket preparation was not completed.") + return + if profile_id is None or menu_page is None: + self.update_status.update_status("Tickets ready. Random ticket use is ON.") + return + self.update_status.update_status("Applying random ticket to profile...") + menu_page.enabling_profile({'id': int(profile_id)}) diff --git a/gui/v2/ui/pages/tor_page.py b/gui/v2/ui/pages/tor_page.py new file mode 100755 index 0000000..a1dae48 --- /dev/null +++ b/gui/v2/ui/pages/tor_page.py @@ -0,0 +1,84 @@ +import os + +from PyQt6.QtWidgets import QLabel, QButtonGroup +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class TorPage(Page): + def __init__(self, page_stack, main_window, parent=None): + super().__init__("TorPage", page_stack, main_window, parent) + self.update_status = main_window + self.button_back.setVisible(True) + self.title.setGeometry(585, 40, 185, 40) + self.title.setText("Pick a Country") + self.display.setGeometry(QtCore.QRect( + 0, 100, 540, 405)) + self.display0 = QLabel(self) + self.display0.setGeometry(QtCore.QRect( + 0, 50, 600, 465)) + self.display0.setPixmap( + QPixmap(os.path.join(self.btn_path, "browser only.png"))) + self.display0.lower() + + self.button_go.clicked.connect(self.go_selected) + + self.button_reverse.setVisible(True) + self.button_reverse.clicked.connect(self.reverse_selected) + + self.label = QLabel(self) + self.label.setGeometry(440, 370, 86, 130) + pixmap = QPixmap(os.path.join(self.btn_path, "tor 86x130.png")) + self.label.setPixmap(pixmap) + + self.label.hide() + + def showEvent(self, event): + super().showEvent(event) + self.extraccion() + + def extraccion(self): + self.data_profile = {} + profile = self.update_status.read_data() + profile_1 = profile.get('Profile_1') + self.verificate() + + def verificate(self, value='tor'): + if value == "just proxy": + self.display0.show() + self.label.hide() + elif value == "tor": + self.display0.show() + self.label.show() + else: + pass + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + + def show_dimentions(self, dimentions): + self.display.setPixmap(QPixmap(os.path.join(self.btn_path, f"{dimentions} garaje.png")).scaled( + self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + self.selected_dimentions_icon = dimentions + self.button_go.setVisible(True) + self.update_swarp_json() + + def update_swarp_json(self): + inserted_data = { + "country_garaje": self.selected_dimentions_icon + } + self.update_status.write_data(inserted_data) + + def reverse_selected(self): + self.custom_window.navigator.navigate("residential") + + def go_selected(self): + self.custom_window.navigator.navigate("browser") + + self.display.clear() + for boton in self.buttons: + boton.setChecked(False) + + self.button_go.setVisible(False) diff --git a/gui/v2/ui/pages/welcome_page.py b/gui/v2/ui/pages/welcome_page.py new file mode 100755 index 0000000..c7d0c83 --- /dev/null +++ b/gui/v2/ui/pages/welcome_page.py @@ -0,0 +1,124 @@ +import os + +from PyQt6.QtWidgets import QLabel, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt + +from gui.v2.ui.pages.Page import Page + + +class WelcomePage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Welcome", page_stack, main_window, parent) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.ui_elements = [] + self.button_next.clicked.connect(self.go_to_install) + self.button_next.setVisible(True) + self._setup_welcome_ui() + self._setup_stats_display() + + def _setup_welcome_ui(self): + welcome_title = QLabel('Welcome to HydraVeil', self) + welcome_title.setGeometry(20, 45, 760, 60) + welcome_title.setStyleSheet(""" + font-size: 32px; + font-weight: bold; + color: cyan; + """) + welcome_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + + welcome_msg = QLabel( + "Before we begin your journey, we need to set up a few essential components. " + "Click 'Next' to take you to the installation page.", self) + welcome_msg.setGeometry(40, 100, 720, 80) + welcome_msg.setWordWrap(True) + welcome_msg.setStyleSheet(""" + font-size: 16px; + color: cyan; + """) + welcome_msg.setAlignment(Qt.AlignmentFlag.AlignCenter) + + def _setup_stats_display(self): + stats_container = QWidget(self) + stats_container.setGeometry(70, 200, 730, 240) + + stats_title = QLabel(stats_container) + stats_title.setText("Features & Services") + stats_title.setGeometry(190, 10, 300, 30) + stats_title.setStyleSheet(""" + font-size: 22px; + font-weight: bold; + color: cyan; + """) + + grid_widget = QWidget(stats_container) + grid_widget.setGeometry(0, 70, 700, 190) + grid_layout = QGridLayout(grid_widget) + grid_layout.setSpacing(30) + + stats = [ + ("Browsers", "browsers_mini.png", "10 Supported Browsers", True), + ("WireGuard", "wireguard_mini.png", "6 Global Locations", True), + ("Proxy", "just proxy_mini.png", "5 Proxy Locations", True), + ("Tor", "toricon_mini.png", "Tor Network Access", True) + ] + + for i, (title, icon_name, count, available) in enumerate(stats): + row = i // 2 + col = i % 2 + + stat_widget = QWidget() + stat_layout = QHBoxLayout(stat_widget) + stat_layout.setContentsMargins(20, 10, 2, 10) + stat_layout.setSpacing(15) + + icon_label = QLabel() + icon_path = os.path.join(self.btn_path, icon_name) + icon_label.setPixmap(QPixmap(icon_path).scaled( + 30, 30, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + icon_label.setFixedSize(30, 30) + stat_layout.addWidget(icon_label) + + text_widget = QWidget() + text_layout = QVBoxLayout(text_widget) + text_layout.setSpacing(5) + text_layout.setContentsMargins(0, 0, 30, 0) + + title_widget = QWidget() + title_layout = QHBoxLayout(title_widget) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(10) + + title_label = QLabel(title) + title_label.setStyleSheet( + "font-size: 16px; font-weight: bold; color: #2c3e50;") + title_layout.addWidget(title_label) + + status_indicator = QLabel("●") + status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;") + title_layout.addWidget(status_indicator) + + title_layout.addStretch() + text_layout.addWidget(title_widget) + + count_label = QLabel(count) + count_label.setStyleSheet("font-size: 16px; color: #34495e;") + text_layout.addWidget(count_label) + + stat_layout.addWidget(text_widget, stretch=1) + + stat_widget.setStyleSheet(""" + QWidget { + background-color: transparent; + border-radius: 10px; + } + """) + + grid_layout.addWidget(stat_widget, row, col) + + def go_to_install(self): + self.custom_window.navigator.navigate("install_system_package") + install_page = self.custom_window.navigator.get_cached("install_system_package") + if install_page is not None: + install_page.configure(package_name='all', distro='debian') diff --git a/gui/v2/ui/pages/wireguard_page.py b/gui/v2/ui/pages/wireguard_page.py new file mode 100755 index 0000000..646c4b4 --- /dev/null +++ b/gui/v2/ui/pages/wireguard_page.py @@ -0,0 +1,136 @@ +import os + +from PyQt6.QtWidgets import QPushButton, QLabel, QButtonGroup +from PyQt6.QtGui import QPixmap, QIcon +from PyQt6 import QtCore + +from gui.v2.ui.pages.Page import Page + + +class WireGuardPage(Page): + browser_label_created = False + + def __init__(self, page_stack, main_window, parent=None): + super().__init__("Wireguard", page_stack, main_window, parent) + self.buttonGroup = QButtonGroup(self) + self.btn_path = main_window.btn_path + self.update_status = main_window + self.buttons = [] + self.selected_protocol = None + self.selected_protocol_icon = None + self.button_back.setVisible(True) + self.button_go.clicked.connect(self.go_selected) + self.additional_labels = [] + self.title.setGeometry(585, 40, 185, 40) + self.title.setText("Pick a Protocol") + self.sudo_warning = QLabel("Requires Sudo", self) + self.sudo_warning.setGeometry(165, 90, 300, 40) + self.sudo_warning.setStyleSheet(""" + font-size: 24px; + font-weight: bold; + color: cyan; + """) + self.sudo_warning.setVisible(False) + + self.create_interface_elements() + + def create_interface_elements(self): + for j, (object_type, icon_name, page_name, geometry) in enumerate([ + (QPushButton, "system-wide", "location", (585, 90, 185, 75)), + (QPushButton, "browser-only", "location", + (585, 90+30+75+30+75, 185, 75)), + (QLabel, None, None, (570, 170, 210, 105)), + (QLabel, None, None, (570, 385, 210, 115)) + ]): + if object_type == QPushButton: + boton = object_type(self) + boton.setGeometry(*geometry) + boton.setIconSize(boton.size()) + boton.setCheckable(True) + boton.setIcon( + QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + self.buttons.append(boton) + self.buttonGroup.addButton(boton, j) + boton.clicked.connect( + lambda _, name=page_name, protocol=icon_name: self.show_protocol(name, protocol)) + elif object_type == QLabel: + label = object_type(self) + label.setGeometry(*geometry) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + label.setWordWrap(True) + if geometry == (570, 170, 210, 105): + text1 = "VPN for the whole PC\n(€1/month - 1 location)" + label.setText(text1) + elif geometry == (570, 385, 210, 115): + text2 = "1 Profile.\n1 Country.\nIsolated VPN for just the browser\n(1€/month)" + label.setText(text2) + + def update_swarp_json(self): + inserted_data = { + "protocol": "wireguard", + "connection": self.selected_protocol_icon + } + self.update_status.write_data(inserted_data) + + def show_protocol(self, page_name, protocol): + if protocol == "browser-only": + self.sudo_warning.setVisible(False) + else: + self.sudo_warning.setVisible(True) + self.protocol_chosen = protocol + self.selected_protocol_icon = protocol + self.selected_page_name = page_name + + self.button_go.setVisible(True) + self.update_swarp_json() + for label in self.additional_labels: + label.deleteLater() + self.additional_labels.clear() + if protocol == "system-wide": + + label_principal = QLabel(self) + label_principal.setGeometry(0, 60, 540, 460) + pixmap = QPixmap(os.path.join(self.btn_path, "wireguard.png")) + label_principal.setPixmap(pixmap) + label_principal.show() + label_principal.setScaledContents(True) + label_principal.lower() + self.additional_labels.append(label_principal) + + if protocol == "browser-only": + label_principal = QLabel(self) + label_principal.setGeometry(0, 80, 530, 380) + pixmap = QPixmap(os.path.join(self.btn_path, "wireguard.png")) + label_principal.setPixmap(pixmap) + label_principal.show() + label_principal.setScaledContents(True) + + label_principal.show() + self.additional_labels.append(label_principal) + + label_background = QLabel(self) + label_background.setGeometry(0, 60, 540, 460) + pixmap = QPixmap(os.path.join(self.btn_path, "browser only.png")) + label_background.setPixmap(pixmap) + label_background.show() + label_background.setScaledContents(True) + label_background.lower() + self.additional_labels.append(label_background) + + def go_selected(self): + if self.selected_page_name: + self.custom_window.navigator.navigate("location") + + self.display.clear() + for boton in self.buttons: + boton.setChecked(False) + + self.button_go.setVisible(False) + + def reverse(self): + self.display.clear() + for boton in self.buttons: + boton.setChecked(False) + + self.button_go.setVisible(False) + self.custom_window.navigator.navigate("protocol") diff --git a/gui/v2/ui/popups/__init__.py b/gui/v2/ui/popups/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/gui/v2/ui/popups/__pycache__/__init__.cpython-312.pyc b/gui/v2/ui/popups/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..4754004 Binary files /dev/null and b/gui/v2/ui/popups/__pycache__/__init__.cpython-312.pyc differ diff --git a/gui/v2/ui/popups/__pycache__/confirmation_popup.cpython-312.pyc b/gui/v2/ui/popups/__pycache__/confirmation_popup.cpython-312.pyc new file mode 100644 index 0000000..1df0752 Binary files /dev/null and b/gui/v2/ui/popups/__pycache__/confirmation_popup.cpython-312.pyc differ diff --git a/gui/v2/ui/popups/__pycache__/endpoint_verification_popup.cpython-312.pyc b/gui/v2/ui/popups/__pycache__/endpoint_verification_popup.cpython-312.pyc new file mode 100644 index 0000000..7e16c72 Binary files /dev/null and b/gui/v2/ui/popups/__pycache__/endpoint_verification_popup.cpython-312.pyc differ diff --git a/gui/v2/ui/popups/__pycache__/ticket_data_loss_popup.cpython-312.pyc b/gui/v2/ui/popups/__pycache__/ticket_data_loss_popup.cpython-312.pyc new file mode 100644 index 0000000..d2e6410 Binary files /dev/null and b/gui/v2/ui/popups/__pycache__/ticket_data_loss_popup.cpython-312.pyc differ diff --git a/gui/v2/ui/popups/confirmation_popup.py b/gui/v2/ui/popups/confirmation_popup.py new file mode 100755 index 0000000..6ac12f5 --- /dev/null +++ b/gui/v2/ui/popups/confirmation_popup.py @@ -0,0 +1,91 @@ +from PyQt6.QtWidgets import ( + QWidget, QPushButton, QLabel, QFrame, QVBoxLayout, QHBoxLayout, QScrollArea +) +from PyQt6.QtGui import QFont +from PyQt6.QtCore import Qt, pyqtSignal + +from gui.v2.ui.styles.styles import ( + POPUP_ACTION_BUTTON_RED_QSS, + POPUP_BG_QSS, + POPUP_CANCEL_BUTTON_QSS, + POPUP_CLOSE_BUTTON_QSS, +) + + +class ConfirmationPopup(QWidget): + finished = pyqtSignal(bool) + + def __init__(self, parent=None, message="", action_button_text="", cancel_button_text="Cancel"): + super().__init__(parent) + self.parent_window = parent + self.message = message + self.action_button_text = action_button_text + self.cancel_button_text = cancel_button_text + self.initUI() + + def initUI(self): + self.setMinimumSize(400, 200) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + bg_widget = QWidget(self) + bg_widget.setStyleSheet(POPUP_BG_QSS) + main_layout.addWidget(bg_widget) + + content_layout = QVBoxLayout(bg_widget) + + close_button = QPushButton("✕", self) + close_button.setStyleSheet(POPUP_CLOSE_BUTTON_QSS) + close_button.setFixedSize(30, 30) + close_button.clicked.connect(self.close) + content_layout.addWidget( + close_button, alignment=Qt.AlignmentFlag.AlignRight) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QFrame.Shape.NoFrame) + content_layout.addWidget(scroll_area) + + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + message_label = QLabel(self.message) + message_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + message_label.setFont(QFont("Arial", 14)) + message_label.setStyleSheet("color: #333333; margin: 10px 0;") + message_label.setWordWrap(True) + scroll_layout.addWidget(message_label) + + scroll_area.setWidget(scroll_content) + + button_layout = QHBoxLayout() + content_layout.addLayout(button_layout) + + cancel_button = QPushButton(self.cancel_button_text) + cancel_button.setFixedSize(150, 50) + cancel_button.setFont(QFont("Arial", 12)) + cancel_button.setStyleSheet(POPUP_CANCEL_BUTTON_QSS) + cancel_button.clicked.connect(self.close) + button_layout.addWidget(cancel_button) + + action_button = QPushButton(self.action_button_text) + action_button.setFixedSize(150, 50) + action_button.setFont(QFont("Arial", 12)) + action_button.setStyleSheet(POPUP_ACTION_BUTTON_RED_QSS) + action_button.clicked.connect(self.perform_action) + button_layout.addWidget(action_button) + + def perform_action(self): + self.finished.emit(True) + self.close() + + def mousePressEvent(self, event): + self.oldPos = event.globalPosition().toPoint() + + def mouseMoveEvent(self, event): + delta = event.globalPosition().toPoint() - self.oldPos + self.move(self.x() + delta.x(), self.y() + delta.y()) + self.oldPos = event.globalPosition().toPoint() diff --git a/gui/v2/ui/popups/endpoint_verification_popup.py b/gui/v2/ui/popups/endpoint_verification_popup.py new file mode 100755 index 0000000..613f184 --- /dev/null +++ b/gui/v2/ui/popups/endpoint_verification_popup.py @@ -0,0 +1,122 @@ +from PyQt6.QtWidgets import ( + QWidget, QPushButton, QLabel, QFrame, QVBoxLayout, QHBoxLayout, QScrollArea +) +from PyQt6.QtGui import QFont +from PyQt6.QtCore import Qt, pyqtSignal + +from gui.v2.ui.styles.styles import ( + POPUP_BG_QSS, + POPUP_CANCEL_BUTTON_QSS, + POPUP_CLOSE_BUTTON_QSS, +) + + +class EndpointVerificationPopup(QWidget): + finished = pyqtSignal(bool, str) + + def __init__(self, parent=None, message=""): + super().__init__(parent) + self.parent_window = parent + self.message = message + self.initUI() + + def initUI(self): + self.setMinimumSize(500, 250) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + bg_widget = QWidget(self) + bg_widget.setStyleSheet(POPUP_BG_QSS) + main_layout.addWidget(bg_widget) + + content_layout = QVBoxLayout(bg_widget) + + close_button = QPushButton("✕", self) + close_button.setStyleSheet(POPUP_CLOSE_BUTTON_QSS) + close_button.setFixedSize(30, 30) + close_button.clicked.connect(lambda: self.close_with_action("abort")) + content_layout.addWidget( + close_button, alignment=Qt.AlignmentFlag.AlignRight) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QFrame.Shape.NoFrame) + content_layout.addWidget(scroll_area) + + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + message_label = QLabel(self.message) + message_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + message_label.setFont(QFont("Arial", 14)) + message_label.setStyleSheet("color: #333333; margin: 10px 0;") + message_label.setWordWrap(True) + scroll_layout.addWidget(message_label) + + scroll_area.setWidget(scroll_content) + + button_layout = QHBoxLayout() + content_layout.addLayout(button_layout) + + sync_button = QPushButton("Sync") + sync_button.setFixedSize(120, 50) + sync_button.setFont(QFont("Arial", 12)) + sync_button.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + border: none; + color: white; + border-radius: 5px; + font-weight: bold; + } + QPushButton:hover { + background-color: #45a049; + } + """) + sync_button.clicked.connect(lambda: self.close_with_action("sync")) + button_layout.addWidget(sync_button) + + abort_button = QPushButton("Abort") + abort_button.setFixedSize(120, 50) + abort_button.setFont(QFont("Arial", 12)) + abort_button.setStyleSheet(POPUP_CANCEL_BUTTON_QSS) + abort_button.clicked.connect(lambda: self.close_with_action("abort")) + button_layout.addWidget(abort_button) + + continue_button = QPushButton("Continue Anyway") + continue_button.setFixedSize(150, 50) + continue_button.setFont(QFont("Arial", 12)) + continue_button.setStyleSheet(""" + QPushButton { + background-color: #ff9800; + border: none; + color: white; + border-radius: 5px; + font-weight: bold; + } + QPushButton:hover { + background-color: #fb8c00; + } + """) + continue_button.clicked.connect( + lambda: self.close_with_action("continue")) + button_layout.addWidget(continue_button) + + def close_with_action(self, action): + self.finished.emit(True, action) + self.close() + + def closeEvent(self, event): + self.finished.emit(False, "abort") + event.accept() + + def mousePressEvent(self, event): + self.oldPos = event.globalPosition().toPoint() + + def mouseMoveEvent(self, event): + delta = event.globalPosition().toPoint() - self.oldPos + self.move(self.x() + delta.x(), self.y() + delta.y()) + self.oldPos = event.globalPosition().toPoint() diff --git a/gui/v2/ui/popups/qrcode_dialog.py b/gui/v2/ui/popups/qrcode_dialog.py new file mode 100755 index 0000000..984ae25 --- /dev/null +++ b/gui/v2/ui/popups/qrcode_dialog.py @@ -0,0 +1,105 @@ +from io import BytesIO + +import qrcode + +from PyQt6.QtWidgets import QDialog, QLabel, QPushButton, QVBoxLayout +from PyQt6.QtGui import QPixmap +from PyQt6.QtCore import Qt + + +class QRCodeDialog(QDialog): + def __init__(self, address_text, full_amount, currency_type=None, parent=None): + super().__init__(parent) + self.currency_type = currency_type + self.setWindowTitle("Payment Address QR Code") + self.setFixedSize(400, 450) + self.setModal(True) + + layout = QVBoxLayout(self) + + title_label = QLabel("Scan QR Code to Copy Address", self) + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_label.setStyleSheet( + "font-size: 16px; font-weight: bold; color: #00ffff; margin: 10px;") + layout.addWidget(title_label) + + qr_label = QLabel(self) + qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + qr_label.setStyleSheet( + "margin: 10px; border: 2px solid #00ffff; border-radius: 10px;") + + qr_pixmap = self.generate_qr_code(address_text, full_amount) + qr_label.setPixmap(qr_pixmap) + layout.addWidget(qr_label) + + amount_label = QLabel("Amount:", self) + amount_label.setStyleSheet( + "font-size: 12px; color: #ffffff; margin-top: 10px;") + layout.addWidget(amount_label) + + amount_text_label = QLabel(full_amount, self) + amount_text_label.setStyleSheet( + "font-size: 10px; color: #cccccc; margin: 5px; padding: 5px; border: 1px solid #666; border-radius: 5px;") + layout.addWidget(amount_text_label) + + address_label = QLabel("Address:", self) + address_label.setStyleSheet( + "font-size: 12px; color: #ffffff; margin-top: 10px;") + layout.addWidget(address_label) + + address_text_label = QLabel(address_text, self) + address_text_label.setWordWrap(True) + address_text_label.setStyleSheet( + "font-size: 10px; color: #cccccc; margin: 5px; padding: 5px; border: 1px solid #666; border-radius: 5px;") + layout.addWidget(address_text_label) + + close_button = QPushButton("Close", self) + close_button.setStyleSheet( + "font-size: 14px; padding: 8px; margin: 10px;") + close_button.clicked.connect(self.close) + layout.addWidget(close_button) + + self.setStyleSheet(""" + QDialog { + background-color: #2c3e50; + border: 2px solid #00ffff; + border-radius: 10px; + } + """) + + def generate_qr_code(self, text, full_amount): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=8, + border=4, + ) + + if self.currency_type: + currency_lower = self.currency_type.lower() + if currency_lower == 'bitcoin': + qr_data = f"bitcoin:{text}?amount={full_amount}" + elif currency_lower == 'monero': + qr_data = f"monero:{text}?amount={full_amount}" + elif currency_lower == 'lightning': + qr_data = f"bitcoin:{text}?amount={full_amount}&lightning=ln" + elif currency_lower == 'litecoin': + qr_data = f"litecoin:{text}?amount={full_amount}" + else: + qr_data = f"{text}?amount={full_amount}" + else: + qr_data = text + + qr.add_data(qr_data) + qr.make(fit=True) + + qr_image = qr.make_image(fill_color="black", back_color="white") + + buffer = BytesIO() + qr_image.save(buffer, format='PNG') + buffer.seek(0) + + pixmap = QPixmap() + pixmap.loadFromData(buffer.getvalue()) + + return pixmap.scaled(150, 150, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) diff --git a/gui/v2/ui/popups/ticket_data_loss_popup.py b/gui/v2/ui/popups/ticket_data_loss_popup.py new file mode 100755 index 0000000..4e24f98 --- /dev/null +++ b/gui/v2/ui/popups/ticket_data_loss_popup.py @@ -0,0 +1,232 @@ +from PyQt6.QtWidgets import ( + QApplication, QDialog, QWidget, QPushButton, QLabel, QVBoxLayout, QHBoxLayout +) +from PyQt6.QtGui import QFont, QGuiApplication +from PyQt6.QtCore import Qt, QTimer + + +class TicketDataLossPopup(QDialog): + def __init__(self, parent=None, ticket_number="", billing_code=""): + super().__init__(parent) + self.parent_window = parent + self._use_parent_local_geometry = QGuiApplication.platformName().lower().startswith("wayland") + self.ticket_number = str(ticket_number) + self.billing_code = str(billing_code) + self.initUI() + + def initUI(self): + self.setFixedSize(640, 380) + if self._use_parent_local_geometry: + if self.parent_window is not None: + self.setParent(self.parent_window) + self.setWindowFlags(Qt.WindowType.Widget) + else: + self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + outer = QVBoxLayout(self) + outer.setContentsMargins(0, 0, 0, 0) + + bg = QWidget(self) + bg.setStyleSheet(""" + background-color: white; + border-radius: 12px; + border: 2px solid #ff4d4d; + """) + outer.addWidget(bg) + + content = QVBoxLayout(bg) + content.setContentsMargins(24, 10, 24, 14) + content.setSpacing(8) + + top_row = QHBoxLayout() + top_row.setContentsMargins(0, 0, 0, 0) + + header = QLabel("Critical Error") + header.setFont(QFont("Arial", 18, QFont.Weight.Bold)) + header.setStyleSheet( + "color: #ff3333; border: none; background: transparent;") + header.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + top_row.addWidget(header) + + top_row.addStretch() + + close_button = QPushButton("✕") + close_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #888888; + font-size: 18px; + font-weight: bold; + border: none; + } + QPushButton:hover { color: #ff4d4d; } + """) + close_button.setFixedSize(28, 28) + close_button.clicked.connect(self.close) + top_row.addWidget(close_button) + + content.addLayout(top_row) + + ticket_html = ( + f"" + f"#{self.ticket_number}" + ) + code_html = ( + f"" + f"{self.billing_code}" + ) + + message_html = ( + "
There was a serious error. " + f"You used up ticket {ticket_html}, but the subscription code you " + f"received {code_html} is invalid.
" + "Please contact customer support " + "immediately with this data.
" + "Do not hit Enable on this " + "profile again until this is resolved — every retry burns another " + "ticket.
" + "