import os from PyQt6.QtWidgets import QLabel, QPushButton, QWidget from PyQt6.QtGui import QPixmap from PyQt6.QtCore import Qt, QTimer from core.services.helpers.setup_sudo_scripts import auto_install_sudo_script, test_if_in_sudo_folder, is_singbox_wrapper_ready from core.services.helpers.manage_assets import sudo_assets_folder_setup from core.models.Result import Result, ResultError import sys from gui.v2.ui.pages.Page import Page from gui.v2.workers.worker_thread import WorkerThread class NetworkingSetupPage(Page): def __init__(self, page_stack, main_window=None, parent=None): super().__init__("NetworkingSetup", page_stack, main_window, parent) self.btn_path = main_window.btn_path self.update_status = main_window self.manual_scripts_ready = False self.singbox_completion = None self.singbox_setup_started = False self.singbox_worker = None self.latest_singbox_message = "" self.setStyleSheet("font-family: Arial;") self.button_next.clicked.disconnect(self.gestionar_next) self.button_next.setVisible(True) self.button_next.clicked.connect(self.go_to_install) self._setup_ui() def showEvent(self, event): super().showEvent(event) self.button_next.setVisible(True) self.button_next.raise_() if self._is_singbox_mode(): self.button_next.setVisible(False) QTimer.singleShot(0, self.prepare_singbox_prereqs) def _setup_ui(self): header_icon = self._icon_label("networking_shield.png", 48, self) header_icon.setGeometry(74, 66, 48, 48) self.title_label = QLabel("Firewall & Managed-DNS Setup", self) self.title_label.setGeometry(138, 68, 590, 44) self.title_label.setStyleSheet("font-family: Arial; font-size: 26px; font-weight: bold; color: cyan;") self.subtitle_label = QLabel( "Optional privileged networking helpers", self) self.subtitle_label.setGeometry(140, 108, 560, 24) self.subtitle_label.setStyleSheet("font-family: Arial; font-size: 14px; color: #d8ffff;") self.description_label = QLabel( "These firewall and managed-DNS helpers are separate bash scripts. " "They stay outside the AppImage Python runtime, and if installed they are placed " "in sudo-protected folders owned by root.", self) self.description_label.setGeometry(80, 152, 640, 72) self.description_label.setWordWrap(True) self.description_label.setStyleSheet("font-family: Arial; font-size: 15px; color: cyan;") manual_panel = self._option_panel(74, 248, "Option 1", "Manual review") manual_text = QLabel( "Copy the scripts to Downloads, review them, then run sudo bash setup.sh yourself.", manual_panel) manual_text.setGeometry(24, 76, 252, 48) manual_text.setWordWrap(True) manual_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;") self.manual_button = QPushButton("Get scripts", manual_panel) self.manual_button.setGeometry(24, 132, 252, 38) self.manual_button.setStyleSheet(self._button_style("#007AFF", "#0056CC")) self.manual_button.clicked.connect(self.handle_manual_setup) auto_panel = self._option_panel(426, 248, "Option 2", "Automated installer") auto_text = QLabel( "Let HydraVeil run the installer, then test that the scripts reached the sudo folder.", auto_panel) auto_text.setGeometry(24, 76, 265, 48) auto_text.setWordWrap(True) auto_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;") self.auto_button = QPushButton("Automated installer", auto_panel) self.auto_button.setGeometry(24, 132, 252, 38) self.auto_button.setStyleSheet(self._button_style("#16a085", "#117a65")) self.auto_button.clicked.connect(self.install_sudo_scripts_automatically) self.status_label = QLabel("Choose manual or automated setup, or press Next to skip and continue.", self) self.status_label.setGeometry(82, 462, 636, 38) self.status_label.setWordWrap(True) self.status_label.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;") def _option_panel(self, x, y, label, title): panel = QWidget(self) panel.setObjectName("networkingOptionPanel") panel.setGeometry(x, y, 300, 186) panel.setStyleSheet(""" QWidget#networkingOptionPanel { background-color: #17323d; border: 1px solid #2c7a8d; border-radius: 6px; } """) option_label = QLabel(label, panel) option_label.setGeometry(24, 20, 120, 20) option_label.setStyleSheet("font-family: Arial; font-size: 11px; font-weight: bold; color: #8de9ff; border: none;") title_label = QLabel(title, panel) title_label.setGeometry(24, 42, 240, 28) title_label.setStyleSheet("font-family: Arial; font-size: 18px; font-weight: bold; color: cyan; border: none;") return panel def _icon_label(self, icon_name, size, parent): label = QLabel(parent) icon_path = os.path.join(self.btn_path, icon_name) label.setPixmap(QPixmap(icon_path).scaled( size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) label.setFixedSize(size, size) label.setStyleSheet("border: none;") return label def _button_style(self, background, hover): return f""" QPushButton {{ background: {background}; color: #f4ffff; border: none; border-radius: 4px; font-family: Arial; font-size: 12px; font-weight: bold; padding: 4px 8px; }} QPushButton:hover {{ background: {hover}; }} QPushButton:disabled {{ background: #3f4954; color: #94a3ad; }} """ def _set_status(self, message, color="#d8ffff"): self.status_label.setText(message) self.status_label.setStyleSheet(f"font-family: Arial; font-size: 13px; color: {color};") self.update_status.update_status(message) def _result_is_valid(self, result: Result): if hasattr(result, "valid"): return result.valid if result is None: return True return bool(result) def _result_message(self, result: Result, fallback): return getattr(result, "message", fallback) def _is_singbox_mode(self): return self.singbox_completion is not None def configure_for_singbox_profile(self, completion_callback): self.singbox_completion = completion_callback self.singbox_setup_started = False self.singbox_worker = None self.latest_singbox_message = "" self.manual_scripts_ready = False self.title_label.setText("Singbox Prerequisites") self.subtitle_label.setText("Required for Hysteria2 and VLESS systemwide profiles") self.description_label.setText( "Hysteria2 and VLESS need the firewall, managed-DNS, Singbox wrapper, " "and Singbox binary before the profile can be created.") self.manual_button.setText("Get scripts") self.manual_button.setDisabled(False) self.auto_button.setDisabled(False) self.button_next.setVisible(False) self._set_status("Checking Singbox prerequisites...") return self def _sudo_scripts_ready_for_singbox(self): if not is_singbox_wrapper_ready(): return False confirmed = test_if_in_sudo_folder() return confirmed.valid def prepare_singbox_prereqs(self): if not self._is_singbox_mode(): return if not self._sudo_scripts_ready_for_singbox(): self.manual_button.setDisabled(False) self.auto_button.setDisabled(False) self.button_next.setVisible(False) self._set_status("Firewall, managed-DNS, and the Singbox wrapper are required. Choose manual or automated setup.", "#d8ffff") return self.start_singbox_binary_setup() def start_singbox_binary_setup(self): if self.singbox_setup_started: return self.singbox_setup_started = True self.latest_singbox_message = "" self.manual_button.setDisabled(True) self.auto_button.setDisabled(True) self.button_next.setVisible(False) self._set_status("Sudo wrapper scripts are ready. Setting up Singbox binary...") self.singbox_worker = WorkerThread('SETUP_SINGBOX_BINARY') self.singbox_worker.text_output.connect(self._set_singbox_status) self.singbox_worker.finished.connect(self.on_singbox_setup_finished) self.singbox_worker.start() def _set_singbox_status(self, message): self.latest_singbox_message = message self._set_status(message) def on_singbox_setup_finished(self, success): if success: self._set_status("Singbox setup complete. Creating profile...", "#2ecc71") QTimer.singleShot(600, self.finish_singbox_prereq_flow) return self.singbox_setup_started = False self.manual_button.setDisabled(False) self.auto_button.setDisabled(False) self.button_next.setVisible(True) self._set_status(self.latest_singbox_message or "Singbox setup failed.", "#ff6b6b") def finish_singbox_prereq_flow(self): completion_callback = self.singbox_completion self.singbox_completion = None self.singbox_setup_started = False self.latest_singbox_message = "" self.title_label.setText("Firewall & Managed-DNS Setup") self.subtitle_label.setText("Optional privileged networking helpers") self.description_label.setText( "These firewall and managed-DNS helpers are separate bash scripts. " "They stay outside the AppImage Python runtime, and if installed they are placed " "in sudo-protected folders owned by root.") self.manual_button.setText("Get scripts") self.manual_button.setDisabled(False) self.auto_button.setDisabled(False) if completion_callback is not None: completion_callback() def handle_manual_setup(self): if self.manual_scripts_ready: self.confirm_sudo_scripts() return results = sudo_assets_folder_setup() if not self._result_is_valid(results): self._set_status( f"Could not copy scripts: {self._result_message(results, 'Unknown error')}", "#ff6b6b") return self.manual_scripts_ready = True self.manual_button.setText("Confirm manual install") self._set_status( "Copied to Downloads/hydraveil_sudo_scripts. Review them, run 'sudo bash setup.sh', then click Confirm manual install.", "#d8ffff") def confirm_sudo_scripts(self): confirmed = test_if_in_sudo_folder() if confirmed.valid: if self._is_singbox_mode(): self._set_status("Confirmed it worked. Continuing to Singbox setup.", "#2ecc71") QTimer.singleShot(600, self.start_singbox_binary_setup) return self._set_status("Confirmed it worked. Continuing to prerequisite installation.", "#2ecc71") QTimer.singleShot(600, self.go_to_install) else: self._set_status( f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b") def install_sudo_scripts_automatically(self): results = auto_install_sudo_script() if results.valid: confirmed = test_if_in_sudo_folder() if confirmed.valid: if self._is_singbox_mode(): self._set_status("The setup script ran successfully. Continuing to Singbox setup.", "#2ecc71") QTimer.singleShot(600, self.start_singbox_binary_setup) return self._set_status("The setup script ran successfully. Continuing to prerequisite installation.", "#2ecc71") QTimer.singleShot(600, self.go_to_install) else: self._set_status( f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b") else: self._set_status( f"Setup script did NOT work because {results.message}", "#ff6b6b") def go_to_install(self): if self._is_singbox_mode(): self.prepare_singbox_prereqs() return 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') def back_to_welcome(self): self.custom_window.navigator.navigate("welcome")