Compare commits

..

2 commits

Author SHA1 Message Date
JOhn
fac64de65d update: updated gui status handling 2026-07-09 18:26:26 -04:00
JOhn
43d47145a7 update: updated tor sync 2026-07-08 15:38:23 -04:00
62 changed files with 510 additions and 2838 deletions

View file

@ -1,50 +1,14 @@
from core.errors.logger import logger
from core.models.manage.session_management import init_session, get_session, close_session, create_ALL_tables, create_ONLY_db_version_table, get_path, does_it_exist, does_db_version_table_exist
from core.models.manage.version_check import check_database_compatibility, insert_new_version, get_custom_message
from core.services.helpers.manage_assets import assets_folder_setup
from core.Constants import Constants
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
from core.models.manage.migrations import migrate_sql
from core.models.manage.clear_sql_model import clear_sql_model
from core.controllers.SyncController import new_sync
from core.controllers.ConfigurationController import ConfigurationController
from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.ClientObserver import ClientObserver
# from gui.v2.actions.database_health import GuiStorageDatabaseError, validate_storage_database, has_required_sync_data
# generic
import os
import sys
from pathlib import Path
from functools import partial
# ============================================================================
# UTIL FUNCTIONS AND RECOVERY UI
# DISPLAY CHOICES AND RECOVERY UI
# ============================================================================
def clear_sync_cache():
"""
Purpose:
Clear CachedSync
If that fails, prompt for a database wipe.
Why:
If they migrated, they have new schemas, but old data.
The problem with that, is the newest CachedSync entry will fool the sync data functions into thinking they don't need to sync new data,
Even though they do need to sync with the new schema.
"""
from core.models.orm_models.CachedSync import CachedSync
result = clear_sql_model(CachedSync)
if result:
logger.info(f"[DB MANAGEMENT] After Migration cleared the CachedSync table: {result}.")
else:
custom_error = "There were issues with clearing the Sync Cache in your database. Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
recovery_dialog(custom_error, "CachedSync Issue")
def failed_migration(db_version_you_have):
close_session() # shut down db connection
custom_error = "There were issues with migrating your database. We transitioned to a new Database format for new features! Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
recovery_dialog(custom_error, db_version_you_have)
def recovery_dialog(custom_error, db_version_you_have):
"""This ends the app by forcing them to delete the database, move it, or just quit."""
@ -55,14 +19,13 @@ def recovery_dialog(custom_error, db_version_you_have):
if choice == 1:
if os.path.exists(database_path):
os.remove(database_path)
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}. Now putting lock on for forced sync..")
lock_file.touch()
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}")
logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..")
sys.exit()
elif choice == 2:
logger.info(f"[DB MANAGEMENT] User opted to move the DB file.")
from gui.v2.ui.popups.pick_folder_to_move import launch_file_picker
from v2.ui.popups.pick_folder_to_move import launch_file_picker
launch_file_picker(database_path)
sys.exit()
@ -70,121 +33,34 @@ def recovery_dialog(custom_error, db_version_you_have):
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
sys.exit()
def emergency_sync():
from gui.v2.ui.popups.generic_choice import show_generic_options
from gui.v2.ui.popups.terminal_threading import show_terminal
option = show_generic_options(
title="Sync the Database",
option_one="Clearweb (fastest)",
option_two="Tor",
option_three="No, Exit."
)
if option == 3:
sys.exit()
client_observer = ClientObserver()
connection_observer = ConnectionObserver()
if option == 1:
ConfigurationController.set_connection("system")
elif option == 2:
ConfigurationController.set_connection("tor")
# setup the function to be able to have the UI background thread run it.
do_this_function = partial(new_sync, client_observer, connection_observer)
return show_terminal(
do_this_function=do_this_function,
connection_observer=connection_observer,
client_observer=client_observer
)
# ============================================================================
# BEGIN PROGRAM
# ============================================================================
# ============================================================================
# ASSETS FOLDER
# ============================================================================
logger.info("Welcome, checking assets..")
if not assets_folder_setup():
from gui.v2.ui.popups.generic_error_popup import show_error
custom_error = "Unable to Setup your Assets Folder. Please create an assets directory at ~/.local/share/hydra-veil/assets"
show_error(custom_error, "Critical Error")
# ============================================================================
# INITIALIZE DATABASE
# ============================================================================
lock_file = Path(f"{Constants.HV_DATA_HOME}/deleted_db.lock")
# Does the database exist?
system_path = get_path()
database_path = system_path / "storage.db"
main_db_exists = False
logger.info("[DB MANAGEMENT] Starting DB init..")
try:
main_db_exists = does_it_exist(database_path)
main_db_exists = does_it_exist(database_path)
# Setup operations on the main DB which create it
init_session() # (engine, Session, _session all initialized from session_management)
session = get_session()
# Setup operations on the main DB which create it
init_session() # (engine, Session, _session all initialized from session_management)
session = get_session()
# does the version checker table exist?
version_table_exists = does_db_version_table_exist()
except Exception as error:
logger.error(f"[DB MANAGEMENT] Critical Error with database initialization: {error}")
close_session()
recovery_dialog("HydraVeil could not initialize the local storage database. Please move or reset storage.db, then restart.", "Startup Error")
# does the version checker table exist?
version_table_exists = does_db_version_table_exist()
# ============================================================================
# LEGACY DATABASE CHECKS
# ============================================================================
migration_happened = False
# ============================================================================
# ZERO DATABASE = SYNC NOW
# ============================================================================
if not main_db_exists:
made_tables = create_ALL_tables()
if made_tables:
# force sync:
emergency_sync()
# we don't have to worry about migrations, they just got a new DB
create_ONLY_db_version_table()
from gui.main_ui import start_ui
try:
start_ui(force_sync=False)
except Exception as e:
error_msg = f"Critical Error with loading UI: {str(e)}"
logger.error(error_msg)
recovery_dialog(error_msg, "Sync Error")
sys.exit()
# ============================================================================
# LEGACY VERSION. MIGRATE
# ============================================================================
# are they upgrading from a legacy version? that would mean the table existed, but not the version table,
if not version_table_exists:
# are they upgrading from a legacy version?
# that would mean the table existed, but not the version table,
if not version_table_exists and main_db_exists:
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
migration = migrate_sql()
# shut down db connection
close_session()
logger.info(f"[DB MANAGEMENT] Migration result is {migration.valid}.")
if migration.valid:
migration_happened = True
force_sync = True
clear_sync_cache()
else:
# THEN DISPLAY CHOICES AND RECOVERY UI
db_version_you_have = "Old System"
failed_migration(db_version_you_have)
# THEN DISPLAY CHOICES AND RECOVERY UI
custom_error = "You're using a Legacy version of the Database scheme. Please delete it and fetch the new data."
db_version_you_have = "Old System"
recovery_dialog(custom_error, db_version_you_have)
sys.exit()
# If they're still here, then if it's NOT a legacy version,
@ -192,7 +68,6 @@ if not version_table_exists:
if not version_table_exists:
create_ONLY_db_version_table()
# ============================================================================
# COMPARE DB VERISONS
# ============================================================================
@ -202,87 +77,56 @@ is_compatable = compatability_dict.get("result", False)
logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}")
# ============================================================================
# IF the versions do NOT match
# IF THE VERSIONS MATCH
# ============================================================================
migrated = False
if not is_compatable:
# MIGRATE?
if reason == "upgrade":
migration = migrate_sql()
if migration.valid:
migrated = True
# MIGRATE FAILED OR NOT POSSIBLE
if not migrated:
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
# WHAT IS THE REASON?
custom_error = get_custom_message(reason, compatability_dict)
# shut down db connection
close_session()
# THEN DISPLAY CHOICES AND RECOVERY UI
recovery_dialog(custom_error, db_version_you_have)
# goodbye. exits.
# ============================================================================
# THE VERSIONS MATCH IF THEY ARE STILL HERE
# ============================================================================
try:
if is_compatable:
made_tables = create_ALL_tables()
# SCREEN FAILED TABLES
if not made_tables:
custom_error = "Critical Failure with starting the models of the database."
logger.error(f"[DB MANAGEMENT] {custom_error}")
close_session()
from gui.v2.ui.popups.Database_version import show_recovery_dialog
from v2.ui.popups.Database_version import show_recovery_dialog
choice = show_recovery_dialog({"message": custom_error, "db_version": 0, "status": "cant_make"})
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
sys.exit()
except:
logger.info(f"[DB MANAGEMENT] create ALL tables failed. Running migrations...")
migration = migrate_sql()
logger.info(f"[DB MANAGEMENT] Results of migration is {migration.valid}")
# ASSUME TABLES CREATED
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing")
if not migration.valid:
db_version_you_have = 0
failed_migration(db_version_you_have)
# UPDATE DATA
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
if not did_it_insert:
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
# ASSUME TABLES CREATED
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing")
# Load GUI either way:
from gui.main_ui import start_ui
# UPDATE DATA
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
if not did_it_insert:
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
# If the user lacks a database, we want to force them to sync the new data, to avoid NoneType errors when enabling existing profiles,
if reason == "no_database":
logger.info(f"[DB MANAGEMENT] User had no database to begin with, so now we're entering GUI with sync on..")
force_sync = True
else:
logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..")
force_sync = False
# try:
# sync_data_missing = not has_required_sync_data(database_path)
# except GuiStorageDatabaseError as error:
# logger.error(f"[DB MANAGEMENT] Critical storage database error during synced data check: {error}")
# close_session()
# recovery_dialog(str(error), "Unreadable")
# sync_data_missing = True
# Start GUI either way:
start_ui(force_sync)
# Load GUI either way:
from gui.main_ui import start_ui
logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..")
if migration_happened:
force_sync = True
clear_sync_cache()
# ============================================================================
# but IF the versions do NOT match
# ============================================================================
else:
force_sync = False
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
# if we deleted their DB, we want to force sync,
if lock_file.exists():
force_sync = True
lock_file.unlink() # unlock for next time
# WHAT IS THE REASON?
custom_error = get_custom_message(reason, compatability_dict)
# shut down db connection
close_session()
# THEN DISPLAY CHOICES AND RECOVERY UI
recovery_dialog(custom_error, db_version_you_have)
# Start GUI either way:
logger.info(f"[DB MANAGEMENT] Starting GUI..")
start_ui(force_sync)

Binary file not shown.

View file

@ -1,24 +0,0 @@
fields:
- name: id
path: ['id']
required: true
- name: application_code
path: ['application', 'code']
required: true
- name: version_number
path: ['version_number']
required: true
- name: format_revision
path: ['format_revision']
- name: download_path
path: ['download_path']
- name: released_at
path: ['released_at']
- name: file_hash
path: ['file_hash']

View file

@ -1,54 +0,0 @@
fields:
- name: country_code
path: ['country', 'code']
required: true
- name: code # this is city code
path: ['code']
required: true
- name: id
path: ['id']
- name: country_name
path: ['country', 'name']
- name: name # this is CITY name
path: ['name']
- name: time_zone
path: ['time_zone', 'code']
- name: operator_id
path: ['operator_id']
required: true
- name: provider_name
path: ['provider', 'name']
- name: is_proxy_capable
path: ['is_proxy_capable']
- name: is_wireguard_capable
path: ['is_wireguard_capable']
required: true
- name: is_hysteria2_capable
path: ['is_hysteria2_capable']
- name: is_vless_capable
path: ['is_vless_capable']
# original version:
# locations.append((
# location['country']['code'],
# location['code'],
# location['id'],
# location['country']['name'],
# location['name'],
# location['time_zone']['code'],
# location['operator_id'],
# location['provider']['name'],
# location['is_proxy_capable'],
# location['is_wireguard_capable']))

View file

@ -1,8 +0,0 @@
fields:
- name: group_a_code
path: ['group_a', 'code']
- name: code
path: ['code']
- name: id
path: ['id']
required: true

View file

@ -1,19 +0,0 @@
fields:
- name: id
path: ['id']
required: true
- name: name
path: ['name']
- name: public_key # this is ed25519
path: ['public_key']
- name: nostr_public_key
path: ['nostr_public_key']
- name: nostr_profile_reference
path: ['nostr_profile_reference']
- name: nostr_attestation_event_reference
path: ['nostr_attestation', 'event_reference']

View file

@ -12,26 +12,21 @@ from PyQt6.QtWidgets import (
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
QDialog, QVBoxLayout, QHBoxLayout, QMessageBox
)
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase, QIcon
from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QTimer, QEvent
from core.Constants import Constants
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.ProfileController import ProfileController
from core.Errors import UnknownConnectionTypeError
from core.errors.logger import logger as core_logger
core_logger.propagate = False
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
mainthread = ThreadSafetyTool()
from gui.v2.infrastructure import orm
from gui.v2.infrastructure.navigator import Navigator
from gui.v2.infrastructure.setup_observers import setup_observers
from gui.v2.infrastructure.connection_manager import ConnectionManager
from gui.v2.infrastructure.screen_size import calculate_max_screensize
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
@ -60,14 +55,13 @@ from gui.v2.actions.profile_data import (
write_profile_data,
clear_profile_data,
)
from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
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
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
class CustomWindow(QMainWindow):
@ -79,6 +73,9 @@ class CustomWindow(QMainWindow):
gui_dir = os.path.dirname(os.path.abspath(__file__))
font_path = os.path.join(gui_dir, 'resources', 'fonts')
# # Get's SQLAlchemy going,
# orm.start()
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
@ -129,9 +126,6 @@ class CustomWindow(QMainWindow):
self.animation_step = 0
self.page_history = []
self.log_path = None
self._close_confirmation_pending = False
self._close_disconnect_in_progress = False
self._closing_after_disconnect = False
self.setFixedSize(800, 570)
@ -306,18 +300,6 @@ class CustomWindow(QMainWindow):
if issubclass(identifier, UnknownConnectionTypeError):
self.setup_popup()
os.execv(sys.executable, [sys.executable] + sys.argv)
elif issubclass(identifier, GuiStorageDatabaseError):
core_logger.error(
f"Storage database error:\n"
f"Type: {identifier.__name__}\n"
f"Value: {str(message)}\n"
f"Traceback:\n{''.join(traceback.format_tb(trace))}"
)
QMessageBox.critical(
self,
"Database Error",
"The local storage database could not be read. Restart HydraVeil and use the database recovery options.",
)
else:
config = self._load_gui_config()
if config and config["logging"]["gui_logging_enabled"] == True:
@ -643,94 +625,17 @@ class CustomWindow(QMainWindow):
def closeEvent(self, event=None):
core_logger.info("HydraVeil application closing")
if self._closing_after_disconnect:
orm.stop()
# Close SQLAlchemy,
orm.stop()
connected_profiles = self.connection_manager.get_connected_profiles()
if connected_profiles:
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
if event is not None:
event.accept()
return
if self._close_confirmation_pending or self._close_disconnect_in_progress:
else:
if event is not None:
event.ignore()
return
connected_profiles = self._get_enabled_profile_ids()
if connected_profiles:
if event is not None:
event.ignore()
self._show_close_disconnect_confirmation(connected_profiles)
return
orm.stop()
if event is not None:
event.accept()
def _get_enabled_profile_ids(self):
try:
profiles = ProfileController.get_all()
except Exception:
return self.connection_manager.get_connected_profiles()
connected_profiles = []
self.connection_manager._connected_profiles.clear()
for profile_id, profile in profiles.items():
try:
if is_profile_enabled_for_gui(profile):
connected_profiles.append(profile_id)
self.connection_manager.add_connected_profile(profile_id)
except Exception:
pass
return connected_profiles
def _show_close_disconnect_confirmation(self, connected_profiles):
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
self._close_confirmation_pending = True
message = self._create_close_disconnect_message(connected_profiles)
self.popup = ConfirmationPopup(
self, message=message, action_button_text="Exit", cancel_button_text="Cancel")
self.popup.setWindowModality(Qt.WindowModality.ApplicationModal)
self.popup.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.popup.finished.connect(
lambda result: self._handle_close_disconnect_result(result, connected_profiles))
self.popup.destroyed.connect(
lambda *_: self._handle_close_disconnect_popup_closed())
self.popup.show()
def _create_close_disconnect_message(self, connected_profiles):
profile_numbers = ', '.join(str(profile_id)
for profile_id in connected_profiles)
is_are = "is" if len(connected_profiles) == 1 else "are"
return f'Profile{"" if len(connected_profiles) == 1 else "s"} {profile_numbers} {is_are} still connected.\nAll connected profiles will be disconnected on exit.\nDo you want to proceed?'
def _handle_close_disconnect_result(self, result, connected_profiles):
self._close_confirmation_pending = False
if not result:
return
self._close_disconnect_in_progress = True
self.update_status('Disabling profiles...')
self.worker_thread = WorkerThread(
'DISABLE_ALL_PROFILES', profile_data=connected_profiles)
self.worker_thread.text_output.connect(self.update_status)
self.worker_thread.finished.connect(self._finish_close_disconnect)
self.worker_thread.start()
def _handle_close_disconnect_popup_closed(self):
if self._close_confirmation_pending:
self._close_confirmation_pending = False
self.popup = None
def _finish_close_disconnect(self, _ok=True):
self._close_disconnect_in_progress = False
connected_profiles = self._get_enabled_profile_ids()
if connected_profiles:
self.update_status('Could not disconnect all profiles. Exit canceled.')
return
self.update_status('All profiles disabled. Bye!')
self._closing_after_disconnect = True
QTimer.singleShot(0, self.close)
event.accept()
def update_image(self, appearance_value="original"):
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
@ -751,21 +656,7 @@ class CustomWindow(QMainWindow):
self.status_label.setStyleSheet(
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
@mainthread # wrapper of ThreadSafetyTool
def update_status(self, text, clear=False):
"""
Purpose:
Update the bottom left status bar
Features:
Forced on the main Thread by ThreadSafetyTool
Depends on:
v2.infrastructure.ThreadSafetyTool
Called by:
v2.infrastructure.setup_observers (outside this class)
"""
if text is None:
self._set_status_font_size(16)
self.status_label.setText('Status:')
@ -908,6 +799,5 @@ class CustomWindow(QMainWindow):
def start_ui(force_sync):
app = QApplication(sys.argv)
calculate_max_screensize(app)
window = CustomWindow(force_sync=force_sync)
window = CustomWindow(force_sync)
sys.exit(app.exec())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

BIN
gui/resources/images/wireguard_button.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
gui/resources/images/wireguard_mini.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -1,89 +0,0 @@
import sqlite3
from pathlib import Path
class GuiStorageDatabaseError(Exception):
def __init__(self, message, detail=None):
self.detail = detail
super().__init__(f"{message} Details: {detail}" if detail else message)
def _connect_readonly(database_path):
path = Path(database_path).resolve()
return sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
def validate_storage_database(database_path):
path = Path(database_path)
if not path.exists():
return
connection = None
try:
connection = _connect_readonly(path)
connection.execute("PRAGMA schema_version").fetchone()
result = connection.execute("PRAGMA quick_check").fetchone()
if result and result[0] != "ok":
raise GuiStorageDatabaseError(
"The local storage database failed SQLite integrity checks.",
str(result[0]),
)
table_rows = connection.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()
table_names = {row[0] for row in table_rows}
known_tables = {
"applications",
"application_versions",
"cached_sync",
"client_versions",
"database_version",
"encryptedproxies",
"locations",
"operators",
"subscription_plans",
}
if table_names and table_names.isdisjoint(known_tables):
raise GuiStorageDatabaseError(
"The local storage database does not look like a HydraVeil storage database.",
", ".join(sorted(table_names)),
)
except GuiStorageDatabaseError:
raise
except sqlite3.Error as error:
raise GuiStorageDatabaseError(
"The local storage database could not be read. It may be malformed, stale, or incompatible.",
str(error),
) from error
finally:
if connection is not None:
connection.close()
def _table_has_rows(database_path, table_name):
path = Path(database_path)
if not path.exists():
return False
connection = None
try:
connection = _connect_readonly(path)
return connection.execute(f'SELECT 1 FROM "{table_name}" LIMIT 1').fetchone() is not None
except sqlite3.OperationalError as error:
if "no such table" in str(error).lower():
return False
raise GuiStorageDatabaseError(
"The local storage database could not be checked for synced data.",
str(error),
) from error
except sqlite3.Error as error:
raise GuiStorageDatabaseError(
"The local storage database could not be checked for synced data.",
str(error),
) from error
finally:
if connection is not None:
connection.close()
def has_required_sync_data(database_path):
required_tables = ("applications", "application_versions", "locations")
return all(_table_has_rows(database_path, table_name) for table_name in required_tables)

View file

@ -1,65 +0,0 @@
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import BaseProfile
# not currently used, but can be added:
# application_version_observer,
# client_observer,
# invoice_observer,
from gui.v2.infrastructure.setup_observers import (
connection_observer,
profile_observer,
ticket_observer,
)
import inspect
def filter_profiles_by_type(profile_data: list) -> tuple:
"""
Purpose:
Isolate system and session profiles into separate lists
Why:
Session profiles must be disabled first before System profiles.
Called by:
worker_thread's disable_all_profiles
"""
system_profiles = []
session_profiles = []
for profile_id in profile_data:
profile = ProfileController.get(int(profile_id))
if isinstance(profile, SessionProfile):
session_profiles.append(profile)
elif isinstance(profile, SystemProfile):
system_profiles.append(profile)
else:
print(f"Skipping/discarding unknown profile {profile_id} type {type(profile)} full info: {profile}")
return session_profiles, system_profiles
def disable_profile_via_controller(profile: BaseProfile):
"""
Purpose:
Disable a profile via the controller
Why:
Inspect signature deals with changing observers
Called by:
worker_thread's disable_all_profiles
"""
kwargs = {
'profile_observer': profile_observer,
'ticket_observer': ticket_observer,
'connection_observer': connection_observer,
}
supported = inspect.signature(ProfileController.disable).parameters
ProfileController.disable(
profile,
**{key: value for key, value in kwargs.items() if key in supported}
)
# We return nothing, because core signals via observers for success. And raises errors for failure.

View file

@ -1,27 +0,0 @@
from core.controllers.ProfileController import ProfileController
from core.controllers.SystemStateController import SystemStateController
from core.models.system.SystemProfile import SystemProfile
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
def _connection_code(profile):
connection = getattr(profile, "connection", None)
return getattr(connection, "code", None)
def _system_state_profile_id():
state = SystemStateController.get()
if state is None:
return None
try:
return int(state.profile_id)
except (TypeError, ValueError):
return None
def is_profile_enabled_for_gui(profile):
if isinstance(profile, SystemProfile) and _connection_code(profile) in SINGBOX_PROTOCOLS:
return _system_state_profile_id() == int(profile.id)
return ProfileController.is_enabled(profile)

View file

@ -11,27 +11,6 @@ from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.actions.ticket_failure import get_ticket_verification_failure
def read_firewall_setting(gui_config_file=None):
try:
configuration = ConfigurationController.get()
if configuration is not None:
return bool(configuration.firewall)
except Exception:
pass
return False
def read_managed_dns_setting(gui_config_file=None):
try:
configuration = ConfigurationController.get()
if configuration is not None:
return bool(configuration.dns)
except Exception:
pass
return False
def read_systemwide_enabled():
try:
privilege_policy = PolicyController.get('privilege')
@ -53,28 +32,19 @@ def read_bwrap_enabled():
def prepare(gui_config_file):
try:
profiles = ProfileController.get_all()
except Exception:
profiles = {}
profiles = ProfileController.get_all()
profile_order = normalize_profile_order(gui_config_file, profiles.keys())
try:
endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled()
except Exception:
endpoint_verification_enabled = False
try:
current_connection = ConfigurationController.get_connection()
except Exception:
current_connection = None
return {
"profiles": profiles,
"profile_order": profile_order,
"current_connection": current_connection,
"current_connection": ConfigurationController.get_connection(),
"endpoint_verification_enabled": endpoint_verification_enabled,
"systemwide_enabled": read_systemwide_enabled(),
"bwrap_enabled": read_bwrap_enabled(),
"firewall_setting": read_firewall_setting(gui_config_file),
"managed_dns_setting": read_managed_dns_setting(gui_config_file),
"gui_config": load_gui_config(gui_config_file),
"ticket_failure": get_ticket_verification_failure(gui_config_file),
}
@ -88,8 +58,6 @@ def empty_payload():
"endpoint_verification_enabled": False,
"systemwide_enabled": False,
"bwrap_enabled": False,
"firewall_setting": False,
"managed_dns_setting": False,
"gui_config": None,
"ticket_failure": None,
}
@ -122,9 +90,8 @@ def truncate_key(text, max_length=50):
def build_verification_view(profile):
operator = None
location = getattr(profile, 'location', None) if profile else None
if location and not isinstance(location, dict) and getattr(location, 'operator', None):
operator = location.operator
if profile and getattr(profile, 'location', None) and profile.location.operator:
operator = profile.location.operator
if not operator:
return {
"operator_name": "N/A",

View file

@ -1,20 +0,0 @@
from core.services.helpers.install_dependencies import SUDO_SINGBOX_LOCATION
from core.services.helpers.setup_sudo_scripts import (
is_singbox_wrapper_ready,
test_if_in_sudo_folder,
)
from core.utils.basic_operations.does_file_exist import does_file_exist
def singbox_prereqs_installed():
try:
if not is_singbox_wrapper_ready():
return False
sudo_scripts = test_if_in_sudo_folder()
if not getattr(sudo_scripts, "valid", False):
return False
return does_file_exist(SUDO_SINGBOX_LOCATION)
except Exception:
return False

10
gui/v2/actions/sync_result.py Executable file
View file

@ -0,0 +1,10 @@
def is_valid_sync_payload(available_locations, available_browsers, status, locations, all_browsers):
if status is not True:
return False
if isinstance(all_browsers, bool):
return False
if not available_locations or not available_browsers:
return False
if not locations or not all_browsers:
return False
return True

View file

@ -1,21 +0,0 @@
import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
from PyQt6.QtCore import QObject, pyqtSignal, Qt
class ThreadSafetyTool(QObject):
"""Marshal function calls to the main thread."""
_signal = pyqtSignal(object, tuple, dict)
def __init__(self):
super().__init__()
self._signal.connect(self._execute, Qt.ConnectionType.QueuedConnection)
def _execute(self, func, args, kwargs):
func(*args, **kwargs)
def __call__(self, func):
"""Decorator that forces a function to run on the main thread."""
def wrapper(*args, **kwargs):
self._signal.emit(func, args, kwargs)
return wrapper

View file

@ -19,10 +19,7 @@ class ConnectionManager:
self.profile_button_objects[profile_id] = profile_button_objects
def get_available_resolutions(self, profile_id):
try:
profile = ProfileController.get(profile_id)
except Exception:
profile = None
profile = ProfileController.get(profile_id)
if profile and hasattr(profile, 'resolution') and profile.resolution:
self.available_resolutions.append(profile.resolution)
return self.available_resolutions

View file

@ -13,7 +13,6 @@ from gui.v2.infrastructure.page_registry import (
)
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.actions.flags import is_fast_registration_enabled
from core.errors.logger import logger
class Navigator(QObject):
@ -80,25 +79,26 @@ class Navigator(QObject):
def _preload_one(self, name):
if name in self._instances:
logger.debug(f"[preload] {name:30s} SKIP (already cached)")
print(f"[preload] {name:30s} SKIP (already cached)", flush=True)
return False
if name not in PAGE_REGISTRY:
logger.debug(f"[preload] {name:30s} SKIP (not in registry)")
print(f"[preload] {name:30s} SKIP (not in registry)", flush=True)
return False
module_path, class_name = PAGE_REGISTRY[name]
logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}")
print(f"[preload] {name:30s} LOAD {module_path}.{class_name}", flush=True)
t0 = time.perf_counter()
try:
page = self._instantiate(name)
self.register_instance(name, page)
dt_ms = (time.perf_counter() - t0) * 1000.0
logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})")
print(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})",
flush=True)
return True
except Exception as e:
dt_ms = (time.perf_counter() - t0) * 1000.0
logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
f"{type(e).__name__}: {e}")
print(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
f"{type(e).__name__}: {e}", flush=True)
traceback.print_exc()
from core.errors.logger import logger as core_logger
core_logger.warning(f"Background preload failed for '{name}': "
@ -121,29 +121,29 @@ class Navigator(QObject):
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
logger.debug(f"[preload] ============================================================")
logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued")
logger.debug(f"[preload] order : {', '.join(self._preload_queue)}")
logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)")
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
if fast_mode:
logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}")
logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay")
logger.debug(f"[preload] ============================================================")
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
self._preload_timer.start(700)
def _preload_next(self):
while self._preload_queue and self._preload_queue[0] in self._instances:
skipped = self._preload_queue.pop(0)
self._preload_skipped_runtime += 1
logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)")
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
if not self._preload_queue:
self._maybe_emit_preload_summary()
return
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----")
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
@ -165,7 +165,7 @@ class Navigator(QObject):
def _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
logger.debug(f"[preload] {name:30s} ASYNC {module_path}.{class_name}")
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, self._prepare_page_data, name)
worker.data_ready.connect(self._on_preload_data_ready)
worker.failed.connect(self._on_preload_failed)
@ -177,22 +177,22 @@ class Navigator(QObject):
def _on_preload_data_ready(self, name, payload):
self._preload_inflight -= 1
if name in self._instances:
logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)")
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
else:
try:
page = self._instantiate(name, prepared=payload)
self.register_instance(name, page)
self._preload_done += 1
logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})")
print(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
except Exception as e:
self._preload_failed += 1
logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}")
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}")
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
if name not in self._instances:
ok = self._preload_one(name)
if ok:
@ -215,10 +215,10 @@ class Navigator(QObject):
def _emit_preload_summary(self):
total_s = time.perf_counter() - self._preload_t_start
logger.debug(f"[preload] ============================================================")
logger.debug(f"[preload] complete: {self._preload_done} loaded, "
print(f"[preload] ============================================================", flush=True)
print(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_failed} failed, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
f"in {total_s:.2f}s")
logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}")
logger.debug(f"[preload] ============================================================")
f"in {total_s:.2f}s", flush=True)
print(f"[preload] cached pages now: {sorted(self._instances.keys())}", flush=True)
print(f"[preload] ============================================================", flush=True)

View file

@ -1,7 +1,6 @@
PAGE_REGISTRY = {
"blank": ("gui.v2.ui.pages._blank_page", "BlankPage"),
"welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"),
"networking_setup": ("gui.v2.ui.pages.networking_setup_page", "NetworkingSetupPage"),
"menu": ("gui.v2.ui.pages.menu_page", "MenuPage"),
"protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"),
"hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"),

View file

@ -1,31 +0,0 @@
from core.errors.logger import logger
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QGuiApplication
import sys
max_screen_size = None
DEFAULT_VALUE = "800x800"
def calculate_max_screensize(app):
global max_screen_size
try:
screen = app.primaryScreen()
available = screen.availableGeometry()
dpi_ratio = screen.devicePixelRatio()
actual_width = int(available.width() * dpi_ratio)
actual_height = int(available.height() * dpi_ratio)
max_screen_size = f"{actual_width}x{actual_height}"
except Exception as e:
logger.error(f"Critical Error with calculating the screen size: {str(e)}")
max_screen_size = DEFAULT_VALUE
def get_max_screensize():
if max_screen_size is not None:
return max_screen_size
else:
return DEFAULT_VALUE

View file

@ -4,8 +4,23 @@ 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
from essentials.models.Event import Event
from core.controllers.ApplicationController import ApplicationController
from PyQt6.QtCore import QObject, pyqtSignal
class StatusRelay(QObject):
status_requested = pyqtSignal(object, bool)
def __init__(self, update_status):
super().__init__()
self.update_status = update_status
self.status_requested.connect(self.apply_status)
def post(self, text, clear=False):
self.status_requested.emit(text, clear)
def apply_status(self, text, clear=False):
self.update_status(text, clear=clear)
application_version_observer = ApplicationVersionObserver()
client_observer = ClientObserver()
@ -15,95 +30,64 @@ profile_observer = ProfileObserver()
ticket_observer = TicketObserver()
def _format_connecting_status(event):
def observer_message(topic, event):
subject = getattr(event, 'subject', None)
if isinstance(subject, dict):
attempt_count = subject.get("attempt_count")
maximum_attempts = subject.get("maximum_number_of_attempts")
if attempt_count is not None and maximum_attempts is not None:
return f'[{attempt_count}/{maximum_attempts}] Performing connection attempt...'
if subject:
meta = getattr(event, 'meta', None)
if subject is not None:
return str(subject)
return 'Connecting..'
if meta:
return str(meta)
return str(topic)
def subscribe_observer_messages(observer, post_status):
for attr in dir(observer):
if not attr.startswith('on_'):
continue
callbacks = getattr(observer, attr, None)
if not isinstance(callbacks, list):
continue
topic = attr[3:]
observer.subscribe(
topic,
lambda event, topic=topic: post_status(observer_message(topic, event)))
def setup_observers(update_status):
relay = StatusRelay(update_status)
post_status = relay.post
profile_observer.subscribe(
'created', lambda event: update_status('Profile Created'))
'created', lambda event: post_status('Profile Created'))
profile_observer.subscribe(
'destroyed', lambda event: update_status('Profile destroyed'))
'destroyed', lambda event: post_status('Profile destroyed'))
# client_observer.subscribe(
# 'synchronizing', lambda event: update_status('Sync in progress...'))
subscribe_observer_messages(client_observer, post_status)
client_observer.subscribe(
'synchronizing', lambda event: update_status(f'{event.subject if event.subject else "Sync in progress..."}'))
application_version_observer.subscribe('downloading', lambda event: post_status(
f'Downloading {ApplicationController.get(event.subject.application_code).name}'))
application_version_observer.subscribe('download_progressing', lambda event: post_status(
f'Downloading {ApplicationController.get(event.subject.application_code).name} {event.meta.get('progress'):.2f}%'))
client_observer.subscribe(
'synchronized', lambda event: update_status('Sync complete'))
application_version_observer.subscribe('downloaded', lambda event: post_status(
f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
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.'))
subscribe_observer_messages(connection_observer, post_status)
client_observer.subscribe(
'custom_message', lambda event: update_status(f'{event.subject if event.subject else "Error, check logs"}'))
application_version_observer.subscribe('downloading', lambda event: update_status(
f'{event.subject if event.subject else "Downloading.."}'))
application_version_observer.subscribe('download_progressing', lambda event: update_status(
f'{event.subject if event.subject else "Downloading.."}'))
application_version_observer.subscribe('downloaded', lambda event: update_status(
f'{event.subject if event.subject else "Downloaded"}'))
# 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(
_format_connecting_status(event)))
connection_observer.subscribe('tor_bootstrapping', lambda event: update_status(
'Establishing Tor connection...'))
connection_observer.subscribe(
'tor_bootstrap_progressing', lambda event: update_status(f'{event.subject if event.subject else "Tor Bootstrapping.."}'))
connection_observer.subscribe(
'message', lambda event: update_status(f'{event.subject if event.subject else "Connecting.."}'))
# original replaced version:
# 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.'))
client_observer.subscribe(
'custom_message', lambda event: update_status(f'{event.subject if event.subject else ""}'))
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 ""}'))
ticket_observer.subscribe('connecting', lambda event: post_status('Connecting to ticket server...'))
ticket_observer.subscribe('sync_done', lambda event: post_status('Ticket prices synced.'))
ticket_observer.subscribe('waiting', lambda event: post_status('Waiting for payment...'))
ticket_observer.subscribe('paid', lambda event: post_status('Payment received.'))
ticket_observer.subscribe('ticket_ready', lambda event: post_status('Ticket ready.'))
ticket_observer.subscribe('used', lambda event: post_status('Ticket used.'))
ticket_observer.subscribe('connection_error', lambda event: post_status('Ticket server connection error.'))
ticket_observer.subscribe('failed_output', lambda event: post_status(f'{event.subject if event.subject else ""}'))
ticket_observer.subscribe('failed_input', lambda event: post_status(f'{event.subject if event.subject else ""}'))
ticket_observer.subscribe('unknown_error', lambda event: post_status('Unknown ticket error.'))
ticket_observer.subscribe('error', lambda event: post_status(f'Ticket error: {event.subject if event.subject else ""}'))
return {
'status_relay': relay,
'application_version_observer': application_version_observer,
'client_observer': client_observer,
'connection_observer': connection_observer,

View file

@ -94,10 +94,3 @@ class Page(QWidget):
boton.setChecked(False)
self.button_next.setVisible(False)
def replace_click_handler(self, button, handler):
try:
button.clicked.disconnect()
except TypeError:
pass
button.clicked.connect(handler)

View file

@ -21,10 +21,10 @@ class BrowserPage(Page):
self.title.setText("Pick a Browser")
self.button_back.setVisible(True)
self.connection_manager = main_window.connection_manager
if self.connection_manager.is_synced():
cm = main_window.connection_manager
if cm.is_synced():
from gui.v2.actions.sync import generate_grid_positions
browsers = self.connection_manager.get_browser_list()
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)
@ -239,21 +239,3 @@ class BrowserPage(Page):
def gestionar_next(self):
self.custom_window.navigator.navigate("screen")
def gestionar_back(self):
profile_data = self.update_status.read_data()
protocol = profile_data.get("protocol", "")
if not self.connection_manager.is_synced() or not self.connection_manager.get_browser_list():
self.custom_window.navigator.navigate("menu")
return
if protocol == "wireguard":
self.custom_window.navigator.navigate("location")
return
if protocol in ("hidetor", "residential"):
self.custom_window.navigator.navigate("hidetor")
return
self.custom_window.navigator.navigate("menu")

View file

@ -10,8 +10,6 @@ from PyQt6 import QtCore, QtGui
from core.controllers.ProfileController import ProfileController
from core.controllers.LocationController import LocationController
from core.models.Result import Result, ResultError
from core.controllers.profile_state.update_profile import update_profile
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.pages.browser_page import BrowserPage
@ -22,11 +20,6 @@ from gui.v2.workers.page_data_worker import PageDataWorker
class EditorPage(Page):
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
PROTOCOL_BUTTON_ASSETS = {
"hysteria2": "hystria2",
}
def __init__(self, page_stack, main_window, prepared=None):
super().__init__("Editor", page_stack, main_window)
self.page_stack = page_stack
@ -213,13 +206,6 @@ class EditorPage(Page):
"dimentions": self.connection_manager.get_available_resolutions(data_profile.get('id', ''))
}, selected_profile_str)
elif protocol in self.SINGBOX_PROTOCOLS:
self.process_and_show_labels(data_profile, {
"protocol": [protocol],
"connection": ['system-wide'],
"location": self.connection_manager.get_location_list(),
}, selected_profile_str)
elif protocol == "residential" or protocol == "hidetor":
self.process_and_show_labels(data_profile, {
"protocol": ['residential', 'wireguard', 'hidetor'],
@ -275,20 +261,6 @@ class EditorPage(Page):
else:
self.brow_disp.hide()
if protocol in self.SINGBOX_PROTOCOLS:
if protocol == "vless":
self.display.setGeometry(0, 90, 540, 405)
else:
self.display.setGeometry(0, 60, 540, 405)
self.display.show()
self.display.setPixmap(self._encrypted_proxy_display_pixmap(
protocol, location).scaled(
self.display.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation))
self.garaje.hide()
self.brow_disp.hide()
if protocol == "residential":
self.display.setGeometry(0, 60, 540, 405)
self.brow_disp.show()
@ -327,7 +299,7 @@ class EditorPage(Page):
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 not in ('hidetor', *self.SINGBOX_PROTOCOLS):
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"
@ -385,25 +357,14 @@ class EditorPage(Page):
if browser_version == '':
browser_version = data_profile.get('browser_version', '')
browser_value = f"{browser_type} {browser_version}"
fallback_path = os.path.join(
self.btn_path, "default_browser_button.png")
normalized_browser = browser_value.strip().lower()
unknown_browser = (
not normalized_browser
or normalized_browser == "unknown"
or normalized_browser.startswith("unknown browser")
)
if connection == 'system-wide':
if connection == 'system-wide' or not browser_value.strip():
base_image = QPixmap()
elif unknown_browser:
base_image = QPixmap(fallback_path)
if base_image.isNull():
base_image = BrowserPage.create_browser_button_image(
"Browser", fallback_path, True)
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':
@ -439,12 +400,9 @@ class EditorPage(Page):
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, '')
if key == 'protocol':
image_path = self._protocol_button_asset(current_value)
else:
image_path = os.path.join(
self.btn_path, f"{current_value}_button.png")
base_image = QPixmap(image_path)
if key == 'dimentions':
@ -608,7 +566,7 @@ class EditorPage(Page):
prev_button.setVisible(True)
next_button.setVisible(True)
if key == 'protocol' or (connection == 'system-wide' and key == 'connection'):
if key == 'protocol' or (protocol == 'wireguard' and key == 'connection'):
prev_button.setDisabled(True)
next_button.setDisabled(True)
@ -616,51 +574,11 @@ class EditorPage(Page):
prev_button.setVisible(False)
next_button.setVisible(False)
def _protocol_button_asset(self, protocol):
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
return os.path.join(self.btn_path, f"{asset_name}_button.png")
def _encrypted_proxy_display_pixmap(self, protocol, location):
pixmap = QPixmap(os.path.join(self.btn_path, f"{protocol}.png"))
if pixmap.isNull():
pixmap = QPixmap(os.path.join(self.btn_path, "system_wide_global.png"))
if pixmap.isNull():
return pixmap
location_pixmap = QPixmap(os.path.join(
self.btn_path, f"icon_mini_{location}.png"))
if location_pixmap.isNull():
location_pixmap = QPixmap(os.path.join(
self.btn_path, "default_location_mini.png"))
if location_pixmap.isNull():
return pixmap
icon_size = max(42, min(72, int(min(pixmap.width(), pixmap.height()) * 0.22)))
location_pixmap = location_pixmap.scaled(
icon_size,
icon_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
x = int(pixmap.width() * 0.58)
if protocol == 'hysteria2':
y = int(pixmap.height() * 0.35)
else:
y = int(pixmap.height() * 0.30)
x = min(max(0, x), max(0, pixmap.width() - location_pixmap.width()))
y = min(max(0, y), max(0, pixmap.height() - location_pixmap.height()))
painter = QPainter(pixmap)
painter.drawPixmap(x, y, location_pixmap)
painter.end()
return pixmap
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.')
self.connection_manager.set_synced(False)
def show_previous_value(self, key: str, index: int, parameters: dict) -> None:
if key == 'browser' or key == 'location':
@ -671,14 +589,8 @@ class EditorPage(Page):
self.on_sync_complete_for_edit_profile)
return
values = parameters.get(key, [])
if not values:
self.update_status.update_status(
f"No {key} data available. Sync the database and try again.")
return
previous_index = (index - 1) % len(values)
previous_value = values[previous_index]
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:
@ -690,14 +602,8 @@ class EditorPage(Page):
self.on_sync_complete_for_edit_profile)
return
values = parameters.get(key, [])
if not values:
self.update_status.update_status(
f"No {key} data available. Sync the database and try again.")
return
next_index = (index + 1) % len(values)
next_value = values[next_index]
next_index = (index + 1) % len(parameters[key])
next_value = parameters[key][next_index]
self.update_temp_value(key, next_value)
@ -887,24 +793,49 @@ class EditorPage(Page):
self._prefetched_profiles = None
def update_core_profiles(self, key, new_value):
profile_id = int(self.update_status.current_profile_id)
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
# Sends to core directly,
result = update_profile(profile_id, key, new_value)
elif key == 'name':
profile.name = new_value
if result.valid:
self.update_status.update_status(
f'Updated profile {profile_id}')
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')
if key == "resolution":
self.update_res = True
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
if result.data == "edit_to_session":
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:
self.update_status.update_status(result.message)
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)

View file

@ -11,7 +11,6 @@ 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.actions.singbox_prereqs import singbox_prereqs_installed
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
@ -21,12 +20,6 @@ from gui.v2.workers.worker_thread import WorkerThread
class FastRegistrationPage(Page):
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
PROTOCOLS = ("wireguard", "hysteria2", "vless", "hidetor")
PROTOCOL_BUTTON_ASSETS = {
"hysteria2": "hystria2",
}
def __init__(self, page_stack, main_window, prepared=None):
super().__init__("FastRegistration", page_stack, main_window)
self.page_stack = page_stack
@ -92,12 +85,10 @@ class FastRegistrationPage(Page):
def initialize_default_selections(self):
if not self.selected_values['location']:
locations = self._available_locations_for_protocol()
locations = self.connection_manager.get_location_list()
if locations:
random_index = random.randint(0, len(locations) - 1)
self.selected_values['location'] = locations[random_index]
else:
self._select_valid_location_for_protocol()
if not self.selected_values['browser']:
browsers = self.connection_manager.get_browser_list()
@ -176,8 +167,8 @@ class FastRegistrationPage(Page):
label = QLabel("Protocol", self)
label.setGeometry(300, 150, 185, 75)
protocol_image = QPixmap(self._protocol_button_asset(
self.selected_values['protocol']))
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()
@ -205,10 +196,6 @@ class FastRegistrationPage(Page):
next_button.setIconSize(next_button.size())
self.buttons.append(next_button)
def _protocol_button_asset(self, protocol):
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
return os.path.join(self.btn_path, f"{asset_name}_button.png")
def create_connection_section(self):
label = QLabel("Connection", self)
label.setGeometry(150, 250, 185, 75)
@ -240,8 +227,6 @@ class FastRegistrationPage(Page):
rotated_icon = icon.transformed(transform)
prev_button.setIcon(QIcon(rotated_icon))
prev_button.setIconSize(prev_button.size())
if self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS:
prev_button.setDisabled(True)
self.buttons.append(prev_button)
next_button = QPushButton(self)
@ -251,8 +236,6 @@ class FastRegistrationPage(Page):
next_button.setIcon(
QIcon(os.path.join(self.btn_path, "UP_button.png")))
next_button.setIconSize(next_button.size())
if self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS:
next_button.setDisabled(True)
self.buttons.append(next_button)
def create_location_section(self):
@ -319,7 +302,7 @@ class FastRegistrationPage(Page):
locations = self.connection_manager.get_location_info(
self.selected_values['location'])
if locations and not self._location_supports_selected_protocol(locations):
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()
@ -546,34 +529,46 @@ class FastRegistrationPage(Page):
def update_ui_state_for_connection(self):
is_system_wide = self.selected_values['connection'] == 'system-wide'
is_singbox = self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS
for button in self.buttons:
if hasattr(button, 'geometry'):
button_geometry = button.geometry()
if is_singbox and button_geometry.y() == 250 and button_geometry.x() in [115, 340]:
button.setEnabled(False)
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 = list(self.PROTOCOLS)
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]
self._apply_protocol_defaults()
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':
connections = self._connections_for_protocol()
if self.selected_values[key] not in connections:
self.selected_values[key] = connections[0]
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._available_locations_for_protocol()
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])
@ -622,21 +617,36 @@ class FastRegistrationPage(Page):
def show_next_value(self, key):
if key == 'protocol':
protocols = list(self.PROTOCOLS)
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]
self._apply_protocol_defaults()
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':
connections = self._connections_for_protocol()
if self.selected_values[key] not in connections:
self.selected_values[key] = connections[0]
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._available_locations_for_protocol()
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])
@ -683,57 +693,6 @@ class FastRegistrationPage(Page):
self.create_interface_elements()
def _connections_for_protocol(self):
protocol = self.selected_values['protocol']
if protocol == 'wireguard':
return ['browser-only', 'system-wide']
if protocol in self.SINGBOX_PROTOCOLS:
return ['system-wide']
return ['tor', 'just proxy']
def _apply_protocol_defaults(self):
protocol = self.selected_values['protocol']
if protocol == 'wireguard':
self.selected_values['connection'] = 'browser-only'
elif protocol in self.SINGBOX_PROTOCOLS:
self.selected_values['connection'] = 'system-wide'
else:
self.selected_values['connection'] = 'tor'
self._select_valid_location_for_protocol()
def _is_enabled_capability(self, value):
return value in (True, 1, "1", "true", "True")
def _location_supports_selected_protocol(self, location_info):
protocol = self.selected_values['protocol']
if protocol == 'hidetor':
return bool(getattr(location_info, 'is_proxy_capable', False))
capability_by_protocol = {
"wireguard": "is_wireguard_capable",
"hysteria2": "is_hysteria2_capable",
"vless": "is_vless_capable",
}
capability_name = capability_by_protocol.get(protocol)
if capability_name is None:
return True
return self._is_enabled_capability(getattr(location_info, capability_name, False))
def _available_locations_for_protocol(self):
locations = self.connection_manager.get_location_list()
return [
loc for loc in locations
if (info := self.connection_manager.get_location_info(loc))
and self._location_supports_selected_protocol(info)
]
def _select_valid_location_for_protocol(self):
locations = self._available_locations_for_protocol()
if not locations:
self.selected_values['location'] = ''
return
if self.selected_values['location'] not in locations:
self.selected_values['location'] = locations[0]
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'):
@ -775,9 +734,6 @@ class FastRegistrationPage(Page):
if self.selected_values['protocol'] == 'wireguard':
self.create_wireguard_profile(profile_data)
elif self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS:
self.create_singbox_profile(profile_data)
return
else:
self.create_tor_profile(profile_data)
@ -822,58 +778,6 @@ class FastRegistrationPage(Page):
profile_id,
profiles.keys())
def create_singbox_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 = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
profile_id = self.get_next_available_profile_id(profiles)
existing_profile_ids = tuple(profiles.keys())
if not singbox_prereqs_installed():
self.show_singbox_prereq_setup(profile_data, profile_id, existing_profile_ids)
return
self.finish_singbox_profile_creation(profile_data, profile_id, existing_profile_ids)
def show_singbox_prereq_setup(self, profile_data, profile_id, existing_profile_ids):
setup_page = self.custom_window.navigator.navigate("networking_setup")
if setup_page is None:
self.update_status.update_status("Singbox prerequisite page is unavailable.")
return
setup_page.configure_for_singbox_profile(
lambda: self.finish_singbox_profile_creation(profile_data, profile_id, existing_profile_ids))
def finish_singbox_profile_creation(self, profile_data, profile_id, existing_profile_ids):
location_info = self.connection_manager.get_location_info(
profile_data['location'])
if not location_info:
self.update_status.update_status('Invalid location selected')
return
profile_data_for_resume = {
'id': profile_id,
'name': profile_data['name'],
'country_code': location_info.country_code,
'code': location_info.code,
'application': '',
'connection_type': profile_data['protocol'],
'resolution': '',
}
self._spawn_create_profile_worker(
'CREATE_SYSTEM_PROFILE', profile_data_for_resume, 'system')
if ProfileController.get(profile_id) is not None:
append_profile_to_visual_order(
getattr(self.update_status, 'gui_config_file', None),
profile_id,
existing_profile_ids)
self.go_back()
def create_tor_profile(self, profile_data):
location_info = self.connection_manager.get_location_info(
profile_data['location'])

View file

@ -15,9 +15,9 @@ class HidetorPage(Page):
self.selected_location_icon = None
self.connection_manager = main_window.connection_manager
self.update_status = main_window
self.replace_click_handler(self.button_next, self.go_selected)
self.button_next.clicked.connect(self.go_selected)
self.button_reverse.setVisible(True)
self.replace_click_handler(self.button_reverse, self.reverse)
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")
@ -91,12 +91,7 @@ class HidetorPage(Page):
self.update_swarp_json()
def reverse(self):
profile_data = self.update_status.read_data()
self.limpiar()
if profile_data.get("protocol") == "residential":
self.custom_window.navigator.navigate("residential")
else:
self.custom_window.navigator.navigate("protocol")
self.custom_window.navigator.navigate("protocol")
def go_selected(self):
self.custom_window.navigator.navigate("browser")

View file

@ -8,8 +8,7 @@ from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtCore import Qt
from PyQt6 import QtCore
from core.services.payment_phase.ticket_config_tools import do_we_have_billing_id
from core.services.prepare_tickets.ticket_tracker import delete_ticket_data
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
@ -183,16 +182,7 @@ class IdPage(Page):
if clicked == check_button:
self._resume_existing_ticket(billing_id)
elif clicked == wipe_button:
deleted = delete_ticket_data()
if deleted:
self._continue_multiple_flow()
else:
failed_delete = QMessageBox(self)
failed_delete.setWindowTitle("Delete Failed")
failed_delete.setText(
"Delete ticket folders failed. Please speak with customer support, and check your file permissions/storage")
style_message_box(failed_delete)
failed_delete.exec()
self._continue_multiple_flow()
def _resume_existing_ticket(self, billing_id):
self.update_status.update_status("Checking existing ticket payment...")

View file

@ -15,8 +15,7 @@ class LocationPage(Page):
self.update_status = main_window
self.button_reverse.setVisible(True)
self.connection_manager = main_window.connection_manager
self.replace_click_handler(self.button_reverse, self.reverse)
self.replace_click_handler(self.button_next, self.go_selected)
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")
@ -67,7 +66,6 @@ class LocationPage(Page):
button.setChecked(False)
if hasattr(self, 'verification_button'):
self.verification_button.setEnabled(False)
self.refresh_protocol_location_visibility()
def create_interface_elements(self, available_locations):
@ -81,7 +79,7 @@ class LocationPage(Page):
boton.setCheckable(True)
locations = self.connection_manager.get_location_info(icon_name)
if locations and not self._location_supports_selected_protocol(locations):
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))
@ -123,30 +121,6 @@ class LocationPage(Page):
boton.clicked.connect(
lambda checked, loc=icon_name: self.show_location(loc))
def refresh_protocol_location_visibility(self):
for button in self.buttons:
location_key = getattr(button, 'location_icon_name', None)
locations = self.connection_manager.get_location_info(location_key)
button.setVisible(not locations or self._location_supports_selected_protocol(locations))
def _current_protocol(self):
profile_data = self.update_status.read_data()
return profile_data.get("protocol", "wireguard")
def _is_enabled_capability(self, value):
return value in (True, 1, "1", "true", "True")
def _location_supports_selected_protocol(self, locations):
capability_by_protocol = {
"wireguard": "is_wireguard_capable",
"hysteria2": "is_hysteria2_capable",
"vless": "is_vless_capable",
}
capability_name = capability_by_protocol.get(self._current_protocol())
if capability_name is None:
return True
return self._is_enabled_capability(getattr(locations, capability_name, False))
def update_swarp_json(self, get_connection=False):
profile_data = self.update_status.read_data()
self.connection_type = profile_data.get("connection", "")
@ -167,6 +141,7 @@ class LocationPage(Page):
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:
@ -179,19 +154,10 @@ class LocationPage(Page):
self.verification_button.setEnabled(True)
def reverse(self):
profile_data = self.update_status.read_data()
self.limpiar()
if hasattr(self, 'initial_display'):
self.initial_display.show()
if hasattr(self, 'verification_button'):
self.verification_button.setEnabled(False)
if profile_data.get("protocol") == "wireguard":
self.custom_window.navigator.navigate("wireguard")
else:
self.custom_window.navigator.navigate("protocol")
self.custom_window.navigator.navigate("protocol")
def go_selected(self):
if self.update_swarp_json(get_connection=True) == "system-wide":
if self.connection_type == "system-wide":
self.custom_window.navigator.navigate("resume")
else:
self.custom_window.navigator.navigate("browser")

View file

@ -12,7 +12,6 @@ from PyQt6.QtWidgets import (
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.ProfileController import ProfileController
from core.errors.logger import logger
from core.controllers.tickets.UseTicketController import (
do_we_use_a_random_ticket,
get_unused_tickets,
@ -21,9 +20,8 @@ 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.database_health import GuiStorageDatabaseError
from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
from gui.v2.actions.sync_result import is_valid_sync_payload
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
@ -208,88 +206,6 @@ class MenuPage(Page):
except RuntimeError:
pass
def _safe_profiles(self):
try:
return ProfileController.get_all()
except GuiStorageDatabaseError as error:
logger.error(f"[MENU] Storage database error while loading profiles: {error}")
self.update_status.update_status("Local storage database could not be read. Restart and recover storage.db.")
except Exception as error:
logger.error(f"[MENU] Error while loading profiles: {error}")
self.update_status.update_status("Could not load profiles. Sync or restart and try again.")
return {}
def _safe_profile(self, profile_id):
try:
return ProfileController.get(int(profile_id))
except GuiStorageDatabaseError as error:
logger.error(f"[MENU] Storage database error while loading profile {profile_id}: {error}")
self.update_status.update_status("Local storage database could not be read. Restart and recover storage.db.")
except Exception as error:
logger.error(f"[MENU] Error while loading profile {profile_id}: {error}")
self.update_status.update_status("Could not load profile data. Sync or restart and try again.")
return None
def _connection_code(self, profile):
connection = getattr(profile, 'connection', None)
return getattr(connection, 'code', None) or 'unknown'
def _location_key(self, profile):
location = getattr(profile, 'location', None)
if isinstance(location, dict):
country_code = location.get("country_code") or "na"
code = location.get("code") or "na"
return f'{country_code}_{code}'
country_code = getattr(location, 'country_code', None)
code = getattr(location, 'code', None)
if country_code and code:
return f'{country_code}_{code}'
return 'na_na'
def _browser_info(self, profile):
if not isinstance(profile, SessionProfile):
return "unknown browser", "", False
application_version = getattr(profile, 'application_version', None)
if isinstance(application_version, dict):
browser = application_version.get('application_code') or "unknown browser"
version = application_version.get('version_number') or ""
return browser, version, False
browser = getattr(application_version, 'application_code', None) or "unknown browser"
version = getattr(application_version, 'version_number', None) or ""
supported = bool(getattr(application_version, 'supported', False))
return browser, version, supported
def _profile_incomplete_reason(self, profile):
if profile is None:
return "Profile data could not be loaded. Sync or restart and try again."
connection = getattr(profile, 'connection', None)
if not getattr(connection, 'code', None):
return "Profile connection data is incomplete. Sync the database and try again."
location = getattr(profile, 'location', None)
if location is None or isinstance(location, dict):
return "Profile location data is incomplete. Sync the database and try again."
if isinstance(profile, SessionProfile):
application_version = getattr(profile, 'application_version', None)
if isinstance(application_version, dict) or application_version is None:
return "Profile browser data is incomplete. Sync the database and try again."
if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None):
return "Profile browser data is incomplete. Sync the database and try again."
return None
def _location_operator(self, profile):
location = getattr(profile, 'location', None)
if isinstance(location, dict) or location is None:
return None
return getattr(location, 'operator', None)
def match_core_profiles(self, profiles_dict):
new_dict = {}
@ -298,42 +214,61 @@ class MenuPage(Page):
profiles_dict.keys())
for idx in profile_ids:
profile = profiles_dict.get(idx)
if profile is None:
continue
try:
profile = profiles_dict[idx]
new_profile = {}
new_profile = {}
protocol = self._connection_code(profile)
location = self._location_key(profile)
protocol = profile.connection.code
# print(f"DEBUG: the type is = {type(profile.location)}, value = {profile.location}")
if isinstance(profile.location, dict):
# Fake/missing data — show placeholders from data,
country_code = profile.location.get("country_code", "na")
code = profile.location.get("code", "na")
location = f'{country_code}_{code}'
else:
# Proper Location object
location = f'{profile.location.country_code}_{profile.location.code}'
except:
import sys
print(f"idx: {idx}")
sys.exit("STOPPING TO READ PRINTS FOR IDX. this failed on getting the location data inside match_core_profiles")
new_profile['location'] = location
if protocol in ('wireguard', 'hysteria2', 'vless'):
new_profile['protocol'] = protocol
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 in ('wireguard', 'hysteria2', 'vless'):
if protocol == 'wireguard':
new_profile['connection'] = connection_type
elif protocol == 'tor':
new_profile['connection'] = protocol
else:
new_profile['connection'] = 'just proxy'
browser, browser_version, browser_supported = self._browser_info(profile)
new_profile['browser'] = browser
new_profile['browser_version'] = browser_version
new_profile['browser_supported'] = browser_supported
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'] = getattr(profile, 'name', None) or f'Profile {idx}'
new_profile['incomplete_reason'] = self._profile_incomplete_reason(profile)
new_profile['name'] = profile.name
new_dict[f'Profile_{idx}'] = new_profile
@ -347,7 +282,7 @@ class MenuPage(Page):
if hasattr(self, 'verification_button'):
self.verification_button.setEnabled(False)
self.profiles_data = self.match_core_profiles(
self._safe_profiles())
ProfileController.get_all())
self.number_of_profiles = len(self.profiles_data)
self.profile_info = dict(self.profiles_data)
@ -362,7 +297,7 @@ class MenuPage(Page):
def refresh_profiles_data(self):
self.profiles_data = self.match_core_profiles(
self._safe_profiles())
ProfileController.get_all())
self.number_of_profiles = len(self.profiles_data)
self.profile_info = dict(self.profiles_data)
for profile_name, profile_value in self.profiles_data.items():
@ -370,16 +305,12 @@ class MenuPage(Page):
self.update_scroll_widget_size()
def refresh_menu_buttons(self):
profiles = self._safe_profiles()
profiles = ProfileController.get_all()
self.button_states.clear()
self.connection_manager._connected_profiles.clear()
self.IsSystem = 0
for profile_id, profile in profiles.items():
try:
is_enabled = is_profile_enabled_for_gui(profile)
except Exception:
is_enabled = False
if is_enabled:
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':
@ -494,7 +425,7 @@ class MenuPage(Page):
def create_profile_name_label(self, parent_label, name):
child_label = QLabel(parent_label)
child_label.setGeometry(0, 65, 175, 30)
child_label.setText(str(name or "Unnamed Profile"))
child_label.setText(name)
child_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
child_label.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents)
@ -576,10 +507,7 @@ class MenuPage(Page):
child_label.show()
def get_icon_path(self, label_name, value, connection_type):
protocol = value.get('protocol', '')
if label_name == 'protocol':
if protocol in ('hysteria2', 'vless'):
return os.path.join(self.btn_path, f"{protocol}_mini.png")
if connection_type == 'tor':
return os.path.join(self.btn_path, "toricon_mini.png")
elif connection_type == 'just proxy':
@ -588,8 +516,6 @@ class MenuPage(Page):
return os.path.join(self.btn_path, "wireguard_mini.png")
elif label_name == 'browser':
if connection_type == 'system-wide':
if protocol in ('hysteria2', 'vless'):
return os.path.join(self.btn_path, "wireguard_system_wide.png")
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")
@ -612,7 +538,9 @@ class MenuPage(Page):
verification_layout.setContentsMargins(20, 20, 20, 20)
verification_layout.setSpacing(15)
operator = self._location_operator(profile_obj)
operator = None
if profile_obj.location and profile_obj.location.operator:
operator = profile_obj.location.operator
info_items = [
("Operator Name", "operator_name"),
@ -769,18 +697,14 @@ class MenuPage(Page):
connection = profile.get("connection", "")
country_garaje = profile.get("country_garaje", "")
profile_obj = self._safe_profile(self.reverse_id)
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"
profile_location = getattr(profile_obj, 'location', None)
if isinstance(profile_location, dict):
profile_location = None
profile_operator = self._location_operator(profile_obj)
operator_name = getattr(profile_operator, 'name', '') if profile_operator else ""
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(
@ -790,7 +714,7 @@ class MenuPage(Page):
label_principal = None
elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard":
if isinstance(profile_obj, SessionProfile):
if profile_obj.is_session_profile():
text_color = "black"
label_background = QLabel(self)
label_background.setGeometry(0, 60, 410, 354)
@ -810,9 +734,9 @@ class MenuPage(Page):
l_img.setScaledContents(True)
l_img.show()
self.additional_labels.append(l_img)
l_name = f"{profile_location.country_name}, {profile_location.name}" if profile_location else ""
o_name = getattr(profile_operator, 'name', '') if profile_operator else ""
n_key = getattr(profile_operator, 'nostr_public_key', '') if profile_operator else ""
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)
@ -847,21 +771,17 @@ class MenuPage(Page):
nostr_txt.show()
self.additional_labels.append(nostr_txt)
elif protocol.lower() in ["wireguard", "open", "residential", "hidetor", "hysteria2", "vless"]:
elif protocol.lower() in ["wireguard", "open", "residential", "hidetor"]:
label_principal = QLabel(self)
label_principal.setGeometry(0, 90, 400, 300)
if protocol.lower() in ["hysteria2", "vless"]:
pixmap = self.build_encrypted_proxy_detail_pixmap(
protocol.lower(), location)
else:
pixmap = QPixmap(os.path.join(
self.btn_path, f"{protocol}_{location}.png"))
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", "hysteria2", "vless"]:
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':
@ -964,45 +884,12 @@ class MenuPage(Page):
self.current_profile_location = location
def build_encrypted_proxy_detail_pixmap(self, protocol, location):
pixmap = QPixmap(os.path.join(self.btn_path, f"{protocol}.png"))
if pixmap.isNull():
return pixmap
location_pixmap = QPixmap(os.path.join(
self.btn_path, f"icon_mini_{location}.png"))
if location_pixmap.isNull():
location_pixmap = QPixmap(os.path.join(
self.btn_path, "default_location_mini.png"))
if location_pixmap.isNull():
return pixmap
icon_size = max(42, min(72, int(min(pixmap.width(), pixmap.height()) * 0.22)))
location_pixmap = location_pixmap.scaled(
icon_size,
icon_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
x = int(pixmap.width() * 0.58)
if protocol == 'hysteria2':
y = int(pixmap.height() * 0.35)
else:
y = int(pixmap.height() * 0.30)
x = min(max(0, x), max(0, pixmap.width() - location_pixmap.width()))
y = min(max(0, y), max(0, pixmap.height() - location_pixmap.height()))
painter = QPainter(pixmap)
painter.drawPixmap(x, y, location_pixmap)
painter.end()
return pixmap
def show_profile_verification(self, profile_id):
profile = self._safe_profile(profile_id)
profile_location = getattr(profile, 'location', None)
if not profile or not profile_location or isinstance(profile_location, dict):
profile = ProfileController.get(profile_id)
if not profile or not profile.location:
return
location_key = f'{profile_location.country_code}_{profile_location.code}'
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
@ -1033,22 +920,9 @@ class MenuPage(Page):
self.boton_just.setEnabled(False)
self.boton_just_session.setEnabled(False)
profile = self._safe_profile(int(self.reverse_id))
incomplete_reason = self._profile_incomplete_reason(profile)
if incomplete_reason:
self.update_status.update_status(incomplete_reason)
self.boton_just.setEnabled(True)
self.boton_just_session.setEnabled(True)
return
try:
is_tor = self.update_status.get_current_connection() == 'tor'
except Exception:
is_tor = False
connection = getattr(profile, 'connection', None)
application_version = getattr(profile, 'application_version', None)
if getattr(connection, 'code', None) == 'tor' and not getattr(application_version, 'installed', False) and not is_tor:
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:
@ -1100,12 +974,7 @@ class MenuPage(Page):
profile_data = {
'id': int(self.reverse_id)
}
profile = self._safe_profile(int(self.reverse_id))
if profile is None:
self.update_status.update_status("Could not load profile data. Sync or restart and try again.")
self.disconnect_button.setEnabled(True)
self.disconnect_system_wide_button.setEnabled(True)
return
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()
@ -1118,18 +987,13 @@ class MenuPage(Page):
return
self.worker_thread = WorkerThread(
'DISABLE_PROFILE', profile_data=profile_data)
self.worker_thread.text_output.connect(
lambda text: self.update_status.update_status(str(text)))
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)
selected_profile_id = self.reverse_id
self.refresh_menu_buttons()
if selected_profile_id is not None and self._safe_profile(selected_profile_id):
self.print_profile_details(f"Profile_{selected_profile_id}")
pass
def _config_flag(self, section, key, default=False):
try:
@ -1145,9 +1009,14 @@ class MenuPage(Page):
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.fast_registration_sync_worker = self._build_sync_worker()
self.fast_registration_sync_worker.text_output.connect(
self.update_status.update_status)
self.fast_registration_sync_worker.sync_output.connect(
self.update_status.update_values)
self.fast_registration_sync_worker.sync_output.connect(
self.on_sync_complete_for_fast_registration)
self.fast_registration_sync_worker.start()
else:
self.custom_window.navigator.navigate("fast_registration")
return
@ -1158,14 +1027,18 @@ class MenuPage(Page):
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:
if is_valid_sync_payload(available_locations, available_browsers, status, locations, all_browsers):
self.custom_window.navigator.navigate("fast_registration")
else:
self.update_status.update_status(
'Sync failed. Please try again later.')
self.connection_manager.set_synced(False)
def _build_sync_worker(self):
if self.update_status.get_current_connection() == 'tor':
return WorkerThread('SYNC_TOR')
return WorkerThread('SYNC')
def change_connect_button(self):
profile = self._safe_profile(int(self.reverse_id))
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)
@ -1182,10 +1055,6 @@ class MenuPage(Page):
is_connected and not is_session_profile)
def update_status_message(self, profile, is_connected):
if profile is None:
self.update_status.update_status("Profile data could not be loaded. Sync or restart and try again.")
return
if is_connected:
message = Worker.generate_profile_message(profile, is_enabled=True)
self.update_status.enable_marquee(message)
@ -1208,8 +1077,7 @@ class MenuPage(Page):
if status:
self.custom_window.navigator.navigate("editor")
else:
self.update_status.update_status(
'Sync failed. Please try again later.')
self.connection_manager.set_synced(False)
def settings_gui(self):
self.custom_window.navigator.navigate("settings")
@ -1304,9 +1172,11 @@ class MenuPage(Page):
self.update_status.update_status("Syncing...")
self.worker_thread = WorkerThread('SYNC')
self.worker_thread.finished.connect(
lambda: self.handle_sync_after_verification(profile_id))
lambda ok: self.handle_sync_after_verification(profile_id) if ok else None)
self.worker_thread.sync_output.connect(
self.update_status.update_values)
self.worker_thread.text_output.connect(
self.update_status.update_status)
self.worker_thread.start()
else:
self.update_status.update_status("Profile enable aborted")
@ -1337,7 +1207,7 @@ class MenuPage(Page):
self.popup.show()
return
if profile_id is not None and profile_id < 0:
if profile_id < 0:
self.DisplayInstallScreen(text)
return
if is_enabled:
@ -1353,15 +1223,9 @@ class MenuPage(Page):
def change_app_page(self, text, Is_changed):
self.update_status.update_status(str(text))
if Is_changed:
try:
current_connection = self.update_status.get_current_connection()
except Exception:
current_connection = None
profile = self._safe_profile(int(self.reverse_id))
incomplete_reason = self._profile_incomplete_reason(profile)
if incomplete_reason:
self.update_status.update_status(incomplete_reason)
elif current_connection != 'tor' and getattr(getattr(profile, 'connection', None), 'code', None) == 'tor':
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(
@ -1406,7 +1270,7 @@ class MenuPage(Page):
self.boton_edit.setEnabled(False)
if profile_id == self.reverse_id:
profile_obj = self._safe_profile(profile_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:

View file

@ -1,307 +0,0 @@
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.actions.singbox_prereqs import singbox_prereqs_installed
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 singbox_prereqs_installed():
self._set_status("Singbox prerequisites are already installed. Creating profile...", "#2ecc71")
QTimer.singleShot(200, self.finish_singbox_prereq_flow)
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")

View file

@ -9,11 +9,6 @@ from gui.v2.ui.pages.Page import Page
class ProtocolPage(Page):
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
PROTOCOL_BUTTON_ASSETS = {
"hysteria2": "hystria2",
}
def __init__(self, page_stack, main_window=None, parent=None):
super().__init__("Protocol", page_stack, main_window, parent)
self.main_window = main_window
@ -23,8 +18,7 @@ class ProtocolPage(Page):
self.connection_manager = main_window.connection_manager
self.button_back.setVisible(True)
self.update_status = main_window
self.replace_click_handler(self.button_back, self.reverse)
self.replace_click_handler(self.button_go, self.go_selected)
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;")
@ -42,11 +36,9 @@ class ProtocolPage(Page):
self.buttons = []
self.selected_page_name = None
for j, (object_type, icon_name, page_name, geometry) in enumerate([
(QPushButton, "wireguard", "wireguard", (585, 80, 185, 75)),
(QPushButton, "hysteria2", "location", (585, 160, 185, 75)),
(QPushButton, "vless", "location", (585, 240, 185, 75)),
(QPushButton, "residential", "residential", (585, 320, 185, 75)),
(QPushButton, "hidetor", "hidetor", (585, 400, 185, 75))
(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)
@ -54,40 +46,28 @@ class ProtocolPage(Page):
boton.setCheckable(True)
boton.setDisabled(True)
boton.setIcon(
QIcon(self._button_asset(icon_name)))
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 _button_asset(self, protocol):
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
return os.path.join(self.btn_path, f"{asset_name}_button.png")
def _display_asset(self, protocol):
full_size_path = os.path.join(self.btn_path, f"{protocol}.png")
if os.path.exists(full_size_path):
return full_size_path
return self._button_asset(protocol)
def enable_protocol_buttons(self):
for button in self.buttons:
button.setDisabled(False)
def update_swarp_json(self):
data = {"protocol": self.selected_protocol_icon}
if self.selected_protocol_icon in self.SINGBOX_PROTOCOLS:
data["connection"] = "system-wide"
self.update_status.write_data(data)
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(self._display_asset(protocol)).scaled(
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.SINGBOX_PROTOCOLS]:
if protocol in ["wireguard", "hidetor"]:
self.button_go.setVisible(True)
self.coming_soon_label.setVisible(False)
else:
@ -105,10 +85,3 @@ class ProtocolPage(Page):
def find_menu_page(self):
return self.custom_window.navigator.get_cached("menu")
def reverse(self):
self.display.clear()
for boton in self.buttons:
boton.setChecked(False)
self.button_go.setVisible(False)
self.custom_window.navigator.navigate("menu")

View file

@ -15,8 +15,8 @@ class ResidentialPage(Page):
self.update_status = main_window
self.connection_choice = None
self.button_reverse.setVisible(True)
self.replace_click_handler(self.button_reverse, self.reverse)
self.replace_click_handler(self.button_go, self.go_selected)
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(
@ -98,5 +98,4 @@ class ResidentialPage(Page):
self.update_status.write_data(inserted_data)
def reverse(self):
self.button_go.setVisible(False)
self.custom_window.navigator.navigate("protocol")

View file

@ -10,18 +10,12 @@ 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.actions.singbox_prereqs import singbox_prereqs_installed
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):
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
PROTOCOL_BUTTON_ASSETS = {
"hysteria2": "hystria2",
}
def __init__(self, page_stack, main_window=None, parent=None):
super().__init__("Resume", page_stack, main_window, parent)
self.update_status = main_window
@ -29,13 +23,13 @@ class ResumePage(Page):
self.btn_path = main_window.btn_path
self.labels_creados = []
self.additional_labels = []
self.replace_click_handler(self.button_go, self.copy_profile)
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.replace_click_handler(self.button_back, self.reverse)
self.button_back.clicked.connect(self.reverse)
self.create_arrow()
self.create_interface_elements()
@ -206,11 +200,8 @@ class ResumePage(Page):
parent_label.show()
self.labels_creados.append(parent_label)
else:
if item == 'protocol':
icon_path = self._protocol_button_asset(text)
else:
icon_path = os.path.join(
self.btn_path, f"{text}_button.png")
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)
@ -287,8 +278,8 @@ class ResumePage(Page):
elif connection_exists:
if profile_1.get("connection", "") == "system-wide":
image_path = self._system_profile_image(
profile_1.get('protocol', 'wireguard'), profile_1.get('location', ''))
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))
@ -345,21 +336,6 @@ class ResumePage(Page):
if hasattr(self, 'arrow_label'):
self.arrow_label.raise_()
def _protocol_button_asset(self, protocol):
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
return os.path.join(self.btn_path, f"{asset_name}_button.png")
def _system_profile_image(self, protocol, location):
candidates = [
os.path.join(self.btn_path, f"{protocol}_{location}.png"),
os.path.join(self.btn_path, f"icon_{location}.png"),
os.path.join(self.btn_path, "system_wide_global.png"),
]
for candidate in candidates:
if os.path.exists(candidate):
return candidate
return candidates[-1]
def toggle_button_visibility(self):
self.button_go.setVisible(bool(self.line_edit.text()))
@ -368,6 +344,10 @@ class ResumePage(Page):
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]
@ -381,29 +361,13 @@ class ResumePage(Page):
profiles = ProfileController.get_all()
profile_id = self.get_next_available_id(profiles)
new_profile = profile_data
existing_profile_ids = tuple(profiles.keys())
if new_profile.get('protocol') in self.SINGBOX_PROTOCOLS and not singbox_prereqs_installed():
self.show_singbox_prereq_setup(new_profile, profile_id, existing_profile_ids)
return
self.finish_profile_creation(new_profile, profile_id, existing_profile_ids)
def show_singbox_prereq_setup(self, profile, profile_id, existing_profile_ids):
setup_page = self.custom_window.navigator.navigate("networking_setup")
if setup_page is None:
self.update_status.update_status("Singbox prerequisite page is unavailable.")
return
setup_page.configure_for_singbox_profile(
lambda: self.finish_profile_creation(profile, profile_id, existing_profile_ids))
def finish_profile_creation(self, new_profile, profile_id, existing_profile_ids):
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,
existing_profile_ids)
profiles.keys())
main = self.update_status
if hasattr(main, 'navigate_after_profile_created'):
@ -412,6 +376,7 @@ class ResumePage(Page):
self.custom_window.navigator.navigate("menu")
self.update_status.clear_data()
self.line_edit.clear()
self.display.clear()
self.button_go.setVisible(False)
@ -443,8 +408,8 @@ class ResumePage(Page):
parts = profile.get('location').split('_')
country_code = parts[0]
location_code = parts[1]
if profile.get('protocol') in ('wireguard', 'hysteria2', 'vless'):
connection_type = profile.get('protocol')
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'

View file

@ -345,6 +345,3 @@ class ScreenPage(Page):
def gestionar_next(self):
self.custom_window.navigator.navigate("resume")
def gestionar_back(self):
self.custom_window.navigator.navigate("browser")

View file

@ -17,7 +17,6 @@ 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.SystemStateController import SystemStateController
from core.controllers.tickets.UseTicketController import (
do_we_use_a_random_ticket,
get_unused_tickets,
@ -32,7 +31,6 @@ from core.Errors import (
PolicyRevocationError,
)
from core.errors.logger import logger as core_logger
from core.services.helpers.setup_sudo_scripts import test_if_in_sudo_folder
from gui.v2.actions import settings_data
from gui.v2.actions.key_interpretation import interpret_key_results
@ -104,8 +102,6 @@ class Settings(Page):
("Create/Edit", self.show_registrations_page),
("Verification", self.show_verification_page),
("Legacy-Version", self.show_systemwide_page),
("Bwrap Permission", self.show_bwrap_page),
("Connections", self.show_connection_page),
("Delete Profile", self.show_delete_page),
("Error Logs", self.show_logs_page),
("Debug Help", self.show_debug_page)
@ -1227,14 +1223,6 @@ class Settings(Page):
self.systemwide_page = self.create_systemwide_page()
self.content_layout.addWidget(self.systemwide_page)
self.content_layout.removeWidget(self.bwrap_page)
self.bwrap_page = self.create_bwrap_page()
self.content_layout.addWidget(self.bwrap_page)
self.content_layout.removeWidget(self.connection_page)
self.connection_page = self.create_connection_page()
self.content_layout.addWidget(self.connection_page)
self.content_layout.removeWidget(self.delete_page)
self.delete_page = self.create_delete_page()
self.content_layout.addWidget(self.delete_page)
@ -1448,10 +1436,8 @@ class Settings(Page):
self.registrations_page = self.create_registrations_page()
self.verification_page = self.create_verification_page()
self.systemwide_page = self.create_systemwide_page()
self.bwrap_page = self.create_bwrap_page()
self.connection_page = self.create_connection_page()
self.delete_page = self.create_delete_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)
@ -1460,10 +1446,8 @@ class Settings(Page):
self.content_layout.addWidget(self.registrations_page)
self.content_layout.addWidget(self.verification_page)
self.content_layout.addWidget(self.systemwide_page)
self.content_layout.addWidget(self.bwrap_page)
self.content_layout.addWidget(self.connection_page)
self.content_layout.addWidget(self.delete_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)
@ -1515,20 +1499,6 @@ class Settings(Page):
self.content_layout.setCurrentWidget(self.systemwide_page)
self._select_menu_button("Legacy-Version")
def show_bwrap_page(self):
core_logger.info("User navigated to Settings -> Bwrap Permission")
self.content_layout.setCurrentWidget(self.bwrap_page)
self._select_menu_button("Bwrap Permission")
def show_connection_page(self):
core_logger.info("User navigated to Settings -> Connection")
if hasattr(self, "connection_gate_status"):
self.load_firewall_settings()
self.load_managed_dns_settings()
self.refresh_connection_settings_gate()
self.content_layout.setCurrentWidget(self.connection_page)
self._select_menu_button("Connections")
def show_logs_page(self):
core_logger.info("User navigated to Settings -> Error Logs")
self.content_layout.setCurrentWidget(self.logs_page)
@ -2011,59 +1981,6 @@ class Settings(Page):
self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
return page
def create_bwrap_page(self):
page = QWidget()
layout = QVBoxLayout(page)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
title = QLabel("BWRAP PERMISSION")
title.setStyleSheet(
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
layout.addWidget(title)
description = QLabel(
"Control whether HydraVeil configures a capability policy so bwrap can be used without requiring additional permissions.")
description.setWordWrap(True)
description.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
layout.addWidget(description)
status_layout = QHBoxLayout()
status_label = QLabel("Current status:")
status_label.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
self.bwrap_status_value = QLabel("")
self.bwrap_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
status_layout.addWidget(status_label)
status_layout.addWidget(self.bwrap_status_value)
status_layout.addStretch()
layout.addLayout(status_layout)
toggle_layout = QHBoxLayout()
self.bwrap_toggle = QCheckBox("Enable capability policy")
self.bwrap_toggle.setStyleSheet(self.get_checkbox_style())
toggle_layout.addWidget(self.bwrap_toggle)
toggle_layout.addStretch()
layout.addLayout(toggle_layout)
save_button = QPushButton()
save_button.setFixedSize(75, 46)
save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
save_button.setIconSize(QSize(75, 46))
save_button.clicked.connect(self.save_bwrap_settings)
button_layout = QHBoxLayout()
button_layout.addWidget(save_button)
button_layout.addStretch()
layout.addLayout(button_layout)
layout.addStretch()
self.load_bwrap_settings(
self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
return page
def load_systemwide_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_systemwide_enabled()
@ -2103,157 +2020,6 @@ class Settings(Page):
self.systemwide_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def load_bwrap_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_bwrap_enabled()
self.bwrap_toggle.setChecked(enabled)
if enabled:
self.bwrap_status_value.setText("Enabled")
self.bwrap_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.bwrap_status_value.setText("Disabled")
self.bwrap_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
def load_firewall_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_firewall_setting()
self.firewall_toggle.setChecked(enabled)
if enabled:
self.firewall_status_value.setText("Enabled")
self.firewall_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.firewall_status_value.setText("Disabled")
self.firewall_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
def load_managed_dns_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_managed_dns_setting()
self.managed_dns_toggle.setChecked(enabled)
if enabled:
self.managed_dns_status_value.setText("Enabled")
self.managed_dns_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.managed_dns_status_value.setText("Disabled")
self.managed_dns_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 save_connection_settings(self):
can_modify, message, _ = self.connection_settings_gate()
if not can_modify:
self.connection_gate_status.setText(message)
self.connection_gate_status.setStyleSheet(
f"color: #ff6b6b; font-size: 12px; {self.font_style}")
self.set_connection_controls_enabled(False)
self.update_status.update_status(message)
return
firewall_enabled = self.firewall_toggle.isChecked()
managed_dns_enabled = self.managed_dns_toggle.isChecked()
try:
ConfigurationController.change_firewall(firewall_enabled)
ConfigurationController.change_dns(managed_dns_enabled)
self._prepared["firewall_setting"] = firewall_enabled
self._prepared["managed_dns_setting"] = managed_dns_enabled
self.load_firewall_settings(firewall_enabled)
self.load_managed_dns_settings(managed_dns_enabled)
self.update_status.update_status(
"Connection settings updated")
except CommandNotFoundError as e:
self.firewall_status_value.setText(str(e))
self.firewall_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
except Exception as e:
self.firewall_status_value.setText(f"Failed to update connection settings {e}")
self.firewall_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def save_firewall_settings(self):
self.save_connection_settings()
def connection_settings_gate(self):
installed, install_message = self.networking_scripts_installed()
if not installed:
return False, f"Firewall and managed-DNS helpers are not installed. {install_message}", True
if self.has_active_systemwide_profile():
return False, "Disable the active systemwide profile before changing firewall or managed-DNS settings.", False
return True, "Firewall and managed-DNS helpers are installed. No active systemwide profile detected.", False
def networking_scripts_installed(self):
try:
result = test_if_in_sudo_folder()
if result.valid:
return True, "Installed."
return False, result.message
except Exception as e:
return False, str(e)
def has_active_systemwide_profile(self):
try:
return SystemStateController.get() is not None
except Exception:
return True
def set_connection_controls_enabled(self, enabled):
for widget_name in ("firewall_toggle", "managed_dns_toggle", "connection_save_button"):
widget = getattr(self, widget_name, None)
if widget is not None:
widget.setEnabled(enabled)
def refresh_connection_settings_gate(self):
can_modify, message, show_install = self.connection_settings_gate()
self.connection_gate_status.setText(message)
color = "#2ecc71" if can_modify else "#ff6b6b"
self.connection_gate_status.setStyleSheet(
f"color: {color}; font-size: 12px; {self.font_style}")
self.set_connection_controls_enabled(can_modify)
self.connection_install_button.setVisible(show_install)
def open_networking_installer(self):
self.custom_window.navigator.navigate("networking_setup")
def load_registrations_settings(self) -> None:
try:
config = self._prepared_value(
@ -2315,104 +2081,3 @@ class Settings(Page):
logging.error(f"Error saving registration settings: {str(e)}")
self.update_status.update_status(
"Error saving registration settings")
def create_connection_page(self):
page = QWidget()
layout = QVBoxLayout(page)
layout.setSpacing(16)
layout.setContentsMargins(20, 20, 20, 20)
title = QLabel("CONNECTION PAGE")
title.setStyleSheet(
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
layout.addWidget(title)
description = QLabel(
"Control firewall and managed-DNS behavior for systemwide profiles.")
description.setWordWrap(True)
description.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
layout.addWidget(description)
self.connection_gate_status = QLabel("")
self.connection_gate_status.setWordWrap(True)
self.connection_gate_status.setStyleSheet(
f"color: #e67e22; font-size: 12px; {self.font_style}")
layout.addWidget(self.connection_gate_status)
self.connection_install_button = QPushButton("Install networking helpers")
self.connection_install_button.setFixedSize(190, 38)
self.connection_install_button.setStyleSheet(f"""
QPushButton {{
background: #007AFF;
color: white;
border: none;
border-radius: 5px;
font-weight: bold;
{self.font_style}
}}
QPushButton:hover {{
background: #0056CC;
}}
""")
self.connection_install_button.clicked.connect(self.open_networking_installer)
layout.addWidget(self.connection_install_button)
firewall_status_layout = QHBoxLayout()
firewall_status_label = QLabel("Firewall status:")
firewall_status_label.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
self.firewall_status_value = QLabel("")
self.firewall_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
firewall_status_layout.addWidget(firewall_status_label)
firewall_status_layout.addWidget(self.firewall_status_value)
firewall_status_layout.addStretch()
layout.addLayout(firewall_status_layout)
dns_status_layout = QHBoxLayout()
dns_status_label = QLabel("Managed-DNS status:")
dns_status_label.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
self.managed_dns_status_value = QLabel("")
self.managed_dns_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
dns_status_layout.addWidget(dns_status_label)
dns_status_layout.addWidget(self.managed_dns_status_value)
dns_status_layout.addStretch()
layout.addLayout(dns_status_layout)
firewall_toggle_layout = QHBoxLayout()
self.firewall_toggle = QCheckBox("Enable Firewall Automatically when Systemwide Profiles are Activated")
self.firewall_toggle.setStyleSheet(self.get_checkbox_style())
firewall_toggle_layout.addWidget(self.firewall_toggle)
firewall_toggle_layout.addStretch()
layout.addLayout(firewall_toggle_layout)
dns_toggle_layout = QHBoxLayout()
self.managed_dns_toggle = QCheckBox("Enable Managed-DNS Automatically when Systemwide Profiles are Activated")
self.managed_dns_toggle.setStyleSheet(self.get_checkbox_style())
dns_toggle_layout.addWidget(self.managed_dns_toggle)
dns_toggle_layout.addStretch()
layout.addLayout(dns_toggle_layout)
self.connection_save_button = QPushButton()
self.connection_save_button.setFixedSize(75, 46)
self.connection_save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
self.connection_save_button.setIconSize(QSize(75, 46))
self.connection_save_button.clicked.connect(self.save_connection_settings)
button_layout = QHBoxLayout()
button_layout.addWidget(self.connection_save_button)
button_layout.addStretch()
layout.addLayout(button_layout)
layout.addStretch()
self.load_firewall_settings(
self._prepared_value("firewall_setting", settings_data.read_firewall_setting))
self.load_managed_dns_settings(
self._prepared_value("managed_dns_setting", settings_data.read_managed_dns_setting))
self.refresh_connection_settings_gate()
return page

View file

@ -5,6 +5,7 @@ from PyQt6.QtGui import QPixmap
from core.controllers.ClientController import ClientController
from gui.v2.actions.sync_result import is_valid_sync_payload
from gui.v2.ui.pages.Page import Page
from gui.v2.workers.worker_thread import WorkerThread
@ -82,10 +83,11 @@ class SyncScreen(Page):
self.worker_thread = WorkerThread('SYNC')
self.worker_thread.sync_output.connect(self.update_output)
self.worker_thread.text_output.connect(self.update_status.update_status)
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:
if status is True and 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:
@ -95,14 +97,12 @@ class SyncScreen(Page):
self.button_back.setEnabled(True)
return
if status is False:
if not is_valid_sync_payload(available_locations, available_browsers, status, locations, all_browsers):
self.connection_manager.set_synced(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:

View file

@ -5,9 +5,7 @@ from PyQt6.QtGui import QIcon
from PyQt6.QtCore import QSize
from PyQt6 import QtCore
from core.services.prepare_tickets.ticket_tracker import delete_ticket_data
from core.controllers.tickets.TicketPayController import check_if_paid
from core.models.Result import Result, ResultError
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
from gui.v2.ui.pages.Page import Page
@ -105,35 +103,26 @@ class TicketCryptoPickerPage(Page):
self.update_status.update_status("Could not initiate payment.")
return
if not invoice.valid:
return self._handle_api_errors(invoice)
# error_code = getattr(invoice, 'error_code', None)
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.data, self.selected_plan)
def _handle_api_errors(self, invoice: Result):
error_code = invoice.error_type
if error_code == ResultError.ALREADY_EXISTS and not self.bypass_existing:
error_code = getattr(invoice, 'error_code', None)
if error_code == 'already_exists' and not self.bypass_existing:
self._prompt_wipe_existing(invoice)
return
elif error_code == ResultError.BILLING_CODE_EXISTS and not self.bypass_existing:
if error_code == 'billing_code_exists' and not self.bypass_existing:
temp_billing_code = getattr(invoice, 'temp_billing_code', None)
print(f"temp_billing_code is {temp_billing_code}")
if temp_billing_code:
self._prompt_wipe_billingcode(temp_billing_code)
return
else:
self._prompt_wipe_billingcode("NONE")
return
else:
error_msg = invoice.message
# msg = getattr(invoice, 'final_error_msg', None) or error_code
self.update_status.update_status(error_msg)
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")
@ -144,7 +133,6 @@ class TicketCryptoPickerPage(Page):
result = msg.exec()
if result == QMessageBox.StandardButton.Yes:
self.bypass_existing = True
delete_ticket_data()
currency_btn = self.buttonGroup.checkedButton()
if currency_btn:
self.start_initiate_payment(currency_btn.property('currency'))
@ -164,7 +152,6 @@ class TicketCryptoPickerPage(Page):
self.check_if_paid_for_existing(temp_billing_code)
else:
self.bypass_existing = True
delete_ticket_data()
currency_btn = self.buttonGroup.checkedButton()
if currency_btn:
self.start_initiate_payment(currency_btn.property('currency'))

View file

@ -104,7 +104,6 @@ class TicketOrBillingChoicePage(Page):
'id': int(profile_id),
'use_ticket': which_ticket,
'ticket_location': candidates[0],
'profile': profile
}
menu_page = self.custom_window.navigator.get_cached("menu")
if menu_page:

View file

@ -24,11 +24,10 @@ class TorPage(Page):
QPixmap(os.path.join(self.btn_path, "browser only.png")))
self.display0.lower()
self.replace_click_handler(self.button_go, self.go_selected)
self.button_go.clicked.connect(self.go_selected)
self.button_reverse.setVisible(True)
self.replace_click_handler(self.button_reverse, self.reverse_selected)
self.replace_click_handler(self.button_back, self.reverse_selected)
self.button_reverse.clicked.connect(self.reverse_selected)
self.label = QLabel(self)
self.label.setGeometry(440, 370, 86, 130)
@ -73,8 +72,6 @@ class TorPage(Page):
self.update_status.write_data(inserted_data)
def reverse_selected(self):
self.limpiar()
self.button_go.setVisible(False)
self.custom_window.navigator.navigate("residential")
def go_selected(self):

View file

@ -13,7 +13,7 @@ class WelcomePage(Page):
self.btn_path = main_window.btn_path
self.update_status = main_window
self.ui_elements = []
self.button_next.clicked.connect(self.go_to_networking_setup)
self.button_next.clicked.connect(self.go_to_install)
self.button_next.setVisible(True)
self._setup_welcome_ui()
self._setup_stats_display()
@ -30,7 +30,7 @@ class WelcomePage(Page):
welcome_msg = QLabel(
"Before we begin your journey, we need to set up a few essential components. "
"Click 'Next' to review the optional networking setup.", self)
"Click 'Next' to take you to the installation page.", self)
welcome_msg.setGeometry(40, 100, 720, 80)
welcome_msg.setWordWrap(True)
welcome_msg.setStyleSheet("""
@ -95,7 +95,7 @@ class WelcomePage(Page):
"font-size: 16px; font-weight: bold; color: #2c3e50;")
title_layout.addWidget(title_label)
status_indicator = QLabel(chr(9679))
status_indicator = QLabel("")
status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;")
title_layout.addWidget(status_indicator)
@ -117,5 +117,8 @@ class WelcomePage(Page):
grid_layout.addWidget(stat_widget, row, col)
def go_to_networking_setup(self):
self.custom_window.navigator.navigate("networking_setup")
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')

View file

@ -19,8 +19,7 @@ class WireGuardPage(Page):
self.selected_protocol = None
self.selected_protocol_icon = None
self.button_back.setVisible(True)
self.replace_click_handler(self.button_back, self.reverse)
self.replace_click_handler(self.button_go, self.go_selected)
self.button_go.clicked.connect(self.go_selected)
self.additional_labels = []
self.title.setGeometry(585, 40, 185, 40)
self.title.setText("Pick a Protocol")

View file

@ -56,7 +56,7 @@ class DatabaseConflictDialog(QDialog):
# First which messages to display,
if check["status"] == "version_mismatch":
error_title = "Upgrade Time!"
error_title = "Database Compatibility"
error_subtitle = "Your App version and Database don't match"
else:
error_title = "Database Error"

View file

@ -1,163 +0,0 @@
import sys
from PyQt6.QtWidgets import QApplication, QDialog, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QColor
from PyQt6.QtWidgets import QGraphicsDropShadowEffect
class GenericOptionMenu(QDialog):
def __init__(self, title, option_one, option_two, option_three):
super().__init__()
self.selected_option = None
self.init_ui(title, option_one, option_two, option_three)
def init_ui(self, title, option_one, option_two, option_three):
self.setWindowTitle(title)
self.setGeometry(100, 100, 500, 600)
self.setModal(True)
# Main container
layout = QVBoxLayout(self)
layout.setSpacing(25)
layout.setContentsMargins(40, 50, 40, 50)
# Apply dark cyberpunk background
self.setStyleSheet("""
QDialog {
background-color: #0a0e27;
color: #00ff88;
}
""")
# Title Label
title_label = QLabel(title)
title_label.setFont(QFont("Courier New", 16, QFont.Weight.Bold))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("""
QLabel {
color: #00ff88;
background-color: transparent;
padding: 20px;
border: 2px solid #00ff88;
border-radius: 5px;
letter-spacing: 2px;
}
""")
# Add glow effect to title
title_shadow = QGraphicsDropShadowEffect()
title_shadow.setBlurRadius(15)
title_shadow.setColor(QColor(0, 255, 136, 200))
title_shadow.setOffset(0, 0)
title_label.setGraphicsEffect(title_shadow)
layout.addWidget(title_label)
layout.addSpacing(30)
# Button 1
btn1 = self.create_button(option_one, 1)
layout.addWidget(btn1)
# Button 2
btn2 = self.create_button(option_two, 2)
layout.addWidget(btn2)
# Button 3
btn3 = self.create_button(option_three, 3)
layout.addWidget(btn3)
layout.addStretch()
# Status label
status_label = QLabel("▓░ READY FOR INPUT ░▓")
status_label.setFont(QFont("Courier New", 10))
status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
status_label.setStyleSheet("""
QLabel {
color: #ff0088;
background-color: transparent;
padding: 10px;
letter-spacing: 1px;
}
""")
layout.addWidget(status_label)
def create_button(self, text, button_num):
button = QPushButton(text)
button.setFont(QFont("Courier New", 12, QFont.Weight.Bold))
button.setMinimumHeight(60)
button.setCursor(Qt.CursorShape.PointingHandCursor)
# Color variations for each button
if button_num == 1:
neon_color = "#00ff88" # Cyan green
rgb_color = QColor(0, 255, 136)
elif button_num == 2:
neon_color = "#00d4ff" # Cyan blue
rgb_color = QColor(0, 212, 255)
else:
neon_color = "#ff0088" # Magenta
rgb_color = QColor(255, 0, 136)
button.setStyleSheet(f"""
QPushButton {{
background-color: #1a1f3a;
color: {neon_color};
border: 2px solid {neon_color};
border-radius: 8px;
padding: 10px 20px;
font-weight: bold;
letter-spacing: 2px;
}}
QPushButton:hover {{
background-color: {neon_color};
color: #0a0e27;
border: 2px solid {neon_color};
}}
QPushButton:pressed {{
background-color: #0a0e27;
border: 3px solid {neon_color};
}}
""")
# Add drop shadow glow effect
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(20)
shadow.setColor(rgb_color)
shadow.setOffset(0, 0)
button.setGraphicsEffect(shadow)
button.clicked.connect(lambda: self.on_button_clicked(button_num))
return button
def on_button_clicked(self, button_num):
self.selected_option = button_num
self.accept()
def show_generic_options(
title: str,
option_one: str,
option_two: str,
option_three: str
) -> int:
app = QApplication(sys.argv)
# Create the menu with custom title and options
menu = GenericOptionMenu(
title=title,
option_one=option_one,
option_two=option_two,
option_three=option_three
)
# Show the menu and get the result
result = menu.exec()
if result == QDialog.DialogCode.Accepted:
print(f"User selected option: {menu.selected_option}")
return menu.selected_option
else:
print("User cancelled")
return None
sys.exit(0)

View file

@ -1,10 +0,0 @@
from PyQt6.QtWidgets import QApplication, QMessageBox
import sys
def show_error(error_text, title="Error"):
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
QMessageBox.critical(None, title, error_text)
app.quit()

View file

@ -1,281 +0,0 @@
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.ClientObserver import ClientObserver
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.controllers.SyncController import new_sync
from core.models.Result import Result, ResultError
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
QLabel, QPushButton, QApplication)
from PyQt6.QtGui import QFont, QColor, QTextCursor
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QGraphicsDropShadowEffect
import sys
import traceback
from typing import Callable, Any
from PyQt6.QtGui import QTextCharFormat
import threading
from threading import Thread
class NotificationTerminalUI(QDialog):
def __init__(self, connection_observer: ConnectionObserver, client_observer: ClientObserver):
super().__init__()
self.mainthread = ThreadSafetyTool()
self.connection_observer = connection_observer
self.client_observer = client_observer
self.synchronized = False
self.worker_thread = None
self.task_success = False
self.connection_observer.subscribe("message",
lambda msg: self.add_notification("UPDATE", msg, "#00ff88"))
self.connection_observer.subscribe("connecting",
lambda msg: self.add_notification("CONNECT", msg, "#00ff88"))
self.connection_observer.subscribe("tor_bootstrapping",
lambda msg: self.add_notification("BOOTSTRAP", msg, "#00d4ff"))
self.connection_observer.subscribe("tor_bootstrap_progressing",
lambda msg: self.add_notification("PROGRESS", msg, "#ff0088"))
self.connection_observer.subscribe("tor_bootstrapped",
lambda msg: self.add_notification("SUCCESS", msg, "#00ff88"))
self.connection_observer.subscribe("custom_message",
lambda msg: self.add_notification("INFO", msg, "#ffff00"))
# Subscribe to client_observer events
self.client_observer.subscribe("synchronizing",
lambda event: self.add_notification("SYNC",
event.subject if event.subject else "Sync in progress...", "#00d4ff"))
self.client_observer.subscribe("synchronized",
lambda event: self.on_synchronized())
self.client_observer.subscribe("updating",
lambda event: self.add_notification("UPDATE", "Updating client...", "#ff0088"))
self.client_observer.subscribe("update_progressing",
lambda event: self.add_notification("PROGRESS",
f'Current progress: {event.meta.get("progress", 0):.2f}%', "#ffff00"))
self.client_observer.subscribe("updated",
lambda event: self.add_notification("COMPLETE",
"Restart client to apply update.", "#00ff88"))
self.client_observer.subscribe("custom_message",
lambda event: self.add_notification("ERROR",
event.subject if event.subject else "Error, check logs", "#ff0088"))
self.init_ui()
def init_ui(self):
self.setWindowTitle("SYNC PROCESS")
self.setGeometry(100, 100, 700, 600)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(30, 30, 30, 30)
self.setStyleSheet("""
QDialog {
background-color: #0a0e27;
color: #00ff88;
}
""")
# Title Label
title_label = QLabel("▓░ NOTIFICATION TERMINAL ░▓")
title_label.setFont(QFont("Courier New", 14, QFont.Weight.Bold))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("""
QLabel {
color: #00ff88;
background-color: transparent;
padding: 15px;
border: 2px solid #00ff88;
border-radius: 5px;
letter-spacing: 2px;
}
""")
title_shadow = QGraphicsDropShadowEffect()
title_shadow.setBlurRadius(15)
title_shadow.setColor(QColor(0, 255, 136, 200))
title_shadow.setOffset(0, 0)
title_label.setGraphicsEffect(title_shadow)
layout.addWidget(title_label)
# Scrollable text display
self.output_text = QTextEdit()
self.output_text.setReadOnly(True)
self.output_text.setFont(QFont("Courier New", 10))
self.output_text.setMinimumHeight(400)
self.output_text.setStyleSheet("""
QTextEdit {
background-color: #1a1f3a;
color: #00ff88;
border: 2px solid #00ff88;
border-radius: 5px;
padding: 15px;
font-size: 16pt;
font-family: 'Courier New', monospace;
selection-background-color: #ff0088;
}
QScrollBar:vertical {
background-color: #0a0e27;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #00ff88;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #00d4ff;
}
""")
layout.addWidget(self.output_text)
# Status label
self.status_label = QLabel("◄ WAITING FOR TASK START ►")
self.status_label.setFont(QFont("Courier New", 9))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("""
QLabel {
color: #ff0088;
background-color: transparent;
padding: 8px;
letter-spacing: 1px;
}
""")
layout.addWidget(self.status_label)
# Button layout
button_layout = QHBoxLayout()
clear_btn = QPushButton("CLEAR")
clear_btn.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
clear_btn.setMinimumHeight(40)
clear_btn.setCursor(Qt.CursorShape.PointingHandCursor)
clear_btn.setStyleSheet("""
QPushButton {
background-color: #1a1f3a;
color: #00d4ff;
border: 2px solid #00d4ff;
border-radius: 5px;
padding: 8px 20px;
font-weight: bold;
}
QPushButton:hover {
background-color: #00d4ff;
color: #0a0e27;
}
QPushButton:pressed {
background-color: #0a0e27;
border: 3px solid #00d4ff;
}
""")
clear_btn.clicked.connect(self.clear_output)
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(15)
shadow.setColor(QColor(0, 212, 255))
shadow.setOffset(0, 0)
clear_btn.setGraphicsEffect(shadow)
button_layout.addWidget(clear_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
def add_notification(self, prefix: str, message: str, color: str = "#00ff88"):
"""Queue to main thread safely."""
self.mainthread(lambda: self._add_notification_impl(prefix, message, color))()
def _add_notification_impl(self, prefix: str, message: str, color: str):
"""Actual implementation on main thread."""
cursor = self.output_text.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
fmt = QTextCharFormat()
fmt.setForeground(QColor(color))
cursor.insertText(f"[{prefix}] {message}\n", fmt)
self.output_text.setTextCursor(cursor)
self.output_text.ensureCursorVisible()
def set_status(self, text: str):
"""This runs on the main thread, safely."""
self.status_label.setText(text)
def on_synchronized(self):
"""Handle synchronized notification and exit."""
self.add_notification("SYNC", "Sync complete ✓", "#00ff88")
self.set_status("◄ SYNCHRONIZED - CLOSING ►")
self.synchronized = True
self.task_success = True
QTimer.singleShot(500, self.accept)
def run_task(self, task_func: Callable):
"""
Purpose:
Insert any function for a pure python background thread.
Which then pushes to the main UI via ThreadSafety
"""
if self.worker_thread is not None and self.worker_thread.is_alive():
self.add_notification("SYSTEM", "Task already running!", "#ff0088")
return
self.set_status("◄ TASK RUNNING ►")
self.add_notification("SYSTEM", "Background task started", "#00d4ff")
# Start in pure Python thread (daemon=True so it doesn't block exit)
self.worker_thread = Thread(target=self._run_task_wrapper, args=(task_func,), daemon=True)
self.worker_thread.start()
def _run_task_wrapper(self, task_func: Callable):
"""Wrapper that runs task and handles errors."""
try:
task_func()
self.task_success = True
except Exception as e:
self.add_notification("ERROR", f"Task failed: {str(e)}", "#ff0088")
self.set_status("◄ TASK FAILED ►")
self.task_success = False
def clear_output(self):
"""Clear the terminal output."""
self.output_text.clear()
self.add_notification("SYSTEM", "Terminal cleared", "#00d4ff")
def get_result(self) -> bool:
"""Return True if synchronized, False otherwise."""
return self.synchronized and self.task_success
def closeEvent(self, event):
"""Ensure thread is properly cleaned up on close."""
if self.worker_thread is not None and self.worker_thread.is_alive():
self.worker_thread.join(timeout=1)
event.accept()
def show_terminal(
do_this_function: Callable[[Any, Any], None],
connection_observer: ConnectionObserver,
client_observer: ClientObserver
) -> bool:
app = QApplication.instance() or QApplication(sys.argv)
# Create the UI
dialog = NotificationTerminalUI(connection_observer=connection_observer, client_observer=client_observer)
# Start the background task
dialog.run_task(do_this_function)
# Show dialog and wait
dialog.exec()
# Return success
return dialog.get_result()

View file

@ -73,11 +73,10 @@ class TicketingWorkerThread(QThread):
self.saved_blind_prep_done.emit(result)
elif self.action == 'USE_TICKET':
result = use_ticket(
which_ticket=self.params['which_ticket'],
which_location=self.params['which_location'],
ticket_observer=ticket_observer,
connection_observer=connection_observer,
profile=self.params['profile']
self.params['which_ticket'],
self.params['which_location'],
ticket_observer,
connection_observer,
)
self.use_done.emit(result)
except Exception as e:

View file

@ -10,8 +10,6 @@ from core.controllers.tickets.UseTicketController import (
)
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.models.Result import Result, ResultError
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
from core.Errors import (
CommandNotFoundError,
EndpointVerificationError,
@ -25,8 +23,6 @@ from core.Errors import (
)
from gui.v2.actions.locations import location_candidates
from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.infrastructure.screen_size import get_max_screensize
from gui.v2.infrastructure.setup_observers import (
application_version_observer,
connection_observer,
@ -52,22 +48,7 @@ class Worker(QObject):
self._consumed_ticket = None
def run(self):
try:
self.profile = ProfileController.get(int(self.profile_data['id']))
except GuiStorageDatabaseError:
self.update_signal.emit(
"Local storage database could not be read. Restart and recover storage.db.", False, None, None, None)
return
except Exception:
self.update_signal.emit(
"Could not load profile data. Sync or restart and try again.", False, None, None, None)
return
incomplete_reason = self._profile_incomplete_reason()
if incomplete_reason:
self.update_signal.emit(incomplete_reason, False, None, None, None)
return
self.profile = ProfileController.get(int(self.profile_data['id']))
if 'use_ticket' in self.profile_data:
ticket_billing_code = self._consume_ticket(
self.profile_data['use_ticket'],
@ -107,10 +88,9 @@ class Worker(QObject):
if self.profile_data.get('ignore_profile_state_conflict', False):
ignore_exceptions.append(ProfileStateConflictError)
ignore_tuple = tuple(ignore_exceptions)
max_resolution = get_max_screensize()
ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
application_version_observer=application_version_observer,
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
connection_observer=connection_observer)
except EndpointVerificationError:
self.update_signal.emit(
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
@ -134,12 +114,6 @@ class Worker(QObject):
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
except CommandNotFoundError as e:
self.update_signal.emit(str(e.subject), False, -1, None, None)
except SudoScript as e:
self.update_signal.emit(str(e), False, None, None, None)
except MissingPreReqs as e:
self.update_signal.emit(str(e), False, None, None, None)
except FirewallError as e:
self.update_signal.emit(str(e), False, None, None, None)
except Exception as e:
print(e)
self.update_signal.emit(
@ -149,27 +123,6 @@ class Worker(QObject):
self.update_signal.emit(
f"No profile found with ID: {self.profile_data['id']}", False, None, None, None)
def _profile_incomplete_reason(self):
if self.profile is None:
return None
connection = getattr(self.profile, 'connection', None)
if not getattr(connection, 'code', None):
return "Profile connection data is incomplete. Sync the database and try again."
location = getattr(self.profile, 'location', None)
if location is None or isinstance(location, dict):
return "Profile location data is incomplete. Sync the database and try again."
if isinstance(self.profile, SessionProfile):
application_version = getattr(self.profile, 'application_version', None)
if isinstance(application_version, dict) or application_version is None:
return "Profile browser data is incomplete. Sync the database and try again."
if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None):
return "Profile browser data is incomplete. Sync the database and try again."
return None
def _location_candidates(self, preferred=None):
return location_candidates(self.profile, preferred)
@ -195,11 +148,7 @@ class Worker(QObject):
print(error_msg)
return None
if self.profile.assassin:
print("skipping random ticket use for the assassin, as it already has it baked in.")
return None
try:
print("using a random ticket from GUI...")
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
except Exception:
return None
@ -221,13 +170,7 @@ class Worker(QObject):
last_msg = None
for cand in candidates:
try:
outcome = use_ticket(
which_ticket=which_ticket,
which_location=cand,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
profile=self.profile
)
outcome = use_ticket(which_ticket, cand, ticket_observer, connection_observer)
except Exception as e:
last_msg = str(e)
continue

View file

@ -1,15 +1,11 @@
import shlex
import subprocess
# import inspect
import time
from PyQt6.QtCore import QThread, pyqtSignal
from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.ClientController import ClientController
from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice
from core.controllers.SyncController import new_sync
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.InvoiceController import InvoiceController
from core.controllers.LocationController import LocationController
from core.controllers.ProfileController import ProfileController
@ -18,24 +14,15 @@ from core.controllers.SubscriptionPlanController import SubscriptionPlanControll
from core.models.session.SessionConnection import SessionConnection
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemConnection import SystemConnection
from core.models.BaseProfile import ProfileType
from core.models.system.SystemProfile import SystemProfile
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
from core.models.Result import Result, ResultError
from core.services.helpers.install_dependencies import setup_singbox_binary as install_singbox_binary
from gui.v2.actions.disable_profiles import (
filter_profiles_by_type,
disable_profile_via_controller
)
from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver
from gui.v2.infrastructure.setup_observers import (
application_version_observer,
client_observer,
connection_observer,
invoice_observer,
profile_observer,
ticket_observer,
)
@ -59,18 +46,6 @@ class WorkerThread(QThread):
self.is_running = True
self.is_disabling = False
# def _disable_profile(self, profile):
# kwargs = {
# 'profile_observer': profile_observer,
# 'ticket_observer': ticket_observer,
# 'connection_observer': connection_observer,
# }
# supported = inspect.signature(ProfileController.disable).parameters
# ProfileController.disable(
# profile,
# **{key: value for key, value in kwargs.items() if key in supported}
# )
def run(self):
if self.action == 'LIST_PROFILES':
self.list_profiles()
@ -90,8 +65,6 @@ class WorkerThread(QThread):
self.disable_all_profiles()
elif self.action == 'INSTALL_PACKAGE':
self.install_package()
elif self.action == 'SETUP_SINGBOX_BINARY':
self.setup_singbox_binary()
elif self.action == 'CHECK_FOR_UPDATE':
self.check_for_update()
elif self.action == 'DOWNLOAD_UPDATE':
@ -124,7 +97,8 @@ class WorkerThread(QThread):
def check_for_update(self):
self.text_output.emit("Checking for updates...")
new_sync(client_observer=client_observer, connection_observer=connection_observer)
ClientController.sync(client_observer=client_observer,
connection_observer=connection_observer)
update_available = ClientController.can_be_updated()
if update_available:
self.text_output.emit("An update is available. Downloading...")
@ -147,72 +121,20 @@ class WorkerThread(QThread):
f"An error occurred when installing {self.package_name}: {e}")
self.finished.emit(False)
def setup_singbox_binary(self):
connection_error = "Connection problems downloading Singbox or related data. Please disable Tor or try again with a better connection."
try:
setup_result = install_singbox_binary(application_version_observer, connection_observer)
except ConnectionError as e:
self.text_output.emit(f"{connection_error}: {str(e)}")
self.finished.emit(False)
return
except ValueError as e:
self.text_output.emit(f"Your configuration files may be corrupted, or a server-side error gave bad data: {str(e)}")
self.finished.emit(False)
return
except Exception as e:
self.text_output.emit(f"Unknown error: {str(e)}")
self.finished.emit(False)
return
if setup_result.valid:
self.text_output.emit("Setup done. You're all set to proceed with Singbox.")
self.finished.emit(True)
return
messages = {
ResultError.NEED_SYNC: "You must sync to find out which Singbox version is supported.",
ResultError.FILE_SYSTEM: "Please check the configuration file, disk space, permissions, and filesystem health.",
ResultError.CONNECTION: connection_error,
ResultError.INVALID_INPUT: "This is a rare bug. Check the error logs, then run the program again from the terminal with DEBUG=true.",
ResultError.PERMISSION: "Singbox requires sudo for setup. After that, the wrapper allows it to run without sudo on an ongoing basis.",
ResultError.MISSING_FILE: "Singbox download or file setup did not complete. Please try again.",
ResultError.UNKNOWN: f"Unknown error: {setup_result.message}",
}
self.text_output.emit(messages.get(setup_result.error_type, setup_result.message or "Unknown Singbox setup error"))
self.finished.emit(False)
def disable_all_profiles(self):
"""
Purpose:
Loop through all profiles in the class data,
Classify them, and disable them.
Why:
Session profiles must be disabled first before System profiles.
Called by:
Same Class run()
"""
session_profiles, system_profiles = filter_profiles_by_type(self.profile_data)
try:
# SESSION
for profile in session_profiles:
disable_profile_via_controller(profile)
print("finished with session profiles. now moving onto session profiles")
if session_profiles and system_profiles:
time.sleep(1)
# SYSTEM
for profile in system_profiles:
disable_profile_via_controller(profile)
for profile_id in self.profile_data:
profile = ProfileController.get(int(profile_id))
if isinstance(profile, SessionProfile):
ProfileController.disable(
profile, ignore=True, profile_observer=profile_observer)
for profile_id in self.profile_data:
profile = ProfileController.get(int(profile_id))
if isinstance(profile, SystemProfile):
ProfileController.disable(
profile, ignore=True, profile_observer=profile_observer)
self.text_output.emit("All profiles were successfully disabled")
except SudoScript as e:
self.text_output.emit(str(e))
except FirewallError as e:
self.text_output.emit(str(e))
except MissingPreReqs as e:
self.text_output.emit(str(e))
except Exception as e:
print(f"Error: {str(e)}")
except Exception:
self.text_output.emit("An error occurred when disabling profile")
finally:
self.finished.emit(True)
@ -223,7 +145,7 @@ class WorkerThread(QThread):
if profile is not None:
try:
ProfileController.destroy(profile, profile_observer, ticket_observer, connection_observer)
ProfileController.destroy(profile)
except Exception as e:
error_name = type(e).__name__
error_text = str(e) or 'Unknown deletion error'
@ -238,19 +160,12 @@ class WorkerThread(QThread):
self.finished.emit(False)
def list_profiles(self):
try:
profiles = ProfileController.get_all()
except Exception:
profiles = {}
self.text_output.emit("Could not load profiles. Sync or restart and try again.")
profiles = ProfileController.get_all()
self.profiles_output.emit(profiles)
def create_profile(self, profile_type):
try:
location = LocationController.get(
self.profile_data['country_code'], self.profile_data['code'])
except Exception:
location = None
location = LocationController.get(
self.profile_data['country_code'], self.profile_data['code'])
if location is None:
self.text_output.emit(
f"Invalid location code: {self.profile_data['location_code']}")
@ -265,11 +180,8 @@ class WorkerThread(QThread):
application_details = self.profile_data['application'].split(
':', 1)
try:
application_version = ApplicationVersionController.get(
application_details[0], application_details[1] if len(application_details) > 1 else None)
except Exception:
application_version = None
application_version = ApplicationVersionController.get(
application_details[0], application_details[1] if len(application_details) > 1 else None)
if application_version is None:
self.text_output.emit(
f"Invalid application: {self.profile_data['application']}")
@ -279,23 +191,12 @@ class WorkerThread(QThread):
resolution = self.profile_data['resolution']
connection = SessionConnection(connection_type, mask_connection)
profile = SessionProfile(
id=profile_id,
name=name,
subscription=None,
type=ProfileType.SESSION,
location=location,
resolution=resolution,
application_version=application_version,
connection=connection)
profile_id, name, None, location, resolution, application_version, connection)
elif profile_type == "system":
connection = SystemConnection(connection_type)
profile = SystemProfile(
id=profile_id,
name=name,
type=ProfileType.SYSTEM,
subscription=None,
location=location,
connection=connection)
profile_id, name, None, location, connection)
else:
self.text_output.emit(f"Invalid profile type: {profile_type}")
return
@ -313,43 +214,91 @@ class WorkerThread(QThread):
try:
profile = ProfileController.get(int(self.profile_data['id']))
if profile:
disable_profile_via_controller(profile)
ProfileController.disable(
profile, profile_observer=profile_observer)
else:
self.text_output.emit(
f"No profile found with ID: {self.profile_data['id']}")
except SudoScript as e:
self.text_output.emit(str(e))
except FirewallError as e:
self.text_output.emit(str(e))
except MissingPreReqs as e:
self.text_output.emit(str(e))
except Exception as e:
self.text_output.emit("An error occurred when disabling profile")
finally:
self.finished.emit(True)
def _emit_observer_message(self, topic, event):
subject = getattr(event, 'subject', None)
meta = getattr(event, 'meta', None)
if subject is not None:
self.text_output.emit(str(subject))
elif meta:
self.text_output.emit(str(meta))
else:
self.text_output.emit(str(topic))
def _subscribe_observer_messages(self, observer, skip_topics=None):
skip_topics = skip_topics or set()
for attr in dir(observer):
if not attr.startswith('on_'):
continue
callbacks = getattr(observer, attr, None)
if not isinstance(callbacks, list):
continue
topic = attr[3:]
if topic in skip_topics:
continue
observer.subscribe(
topic,
lambda event, topic=topic: self._emit_observer_message(topic, event))
def sync(self):
try:
if self.action == 'SYNC_TOR':
ConfigurationController.set_connection('tor')
else:
ConfigurationController.set_connection('system')
self.check_for_update()
sync_succeeded = False
def sync_complete(event):
nonlocal sync_succeeded
sync_succeeded = True
self._emit_observer_message('synchronized', event)
sync_client_observer = ClientObserver()
sync_connection_observer = ConnectionObserver()
self._subscribe_observer_messages(
sync_client_observer, {'synchronized'})
self._subscribe_observer_messages(sync_connection_observer)
sync_client_observer.subscribe('synchronized', sync_complete)
ClientController.sync(
client_observer=sync_client_observer,
connection_observer=sync_connection_observer)
if not sync_succeeded:
self.sync_output.emit([], [], False, False, [], [])
self.finished.emit(False)
return
locations = LocationController.get_all()
browser = ApplicationVersionController.get_all()
# print('the browser is: ', browser)
all_browser_versions = [
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
all_location_codes = [
f"{location.country_code}_{location.code}" for location in locations]
if not all_location_codes or not all_browser_versions or not locations or not browser:
self.sync_output.emit([], [], False, False, [], [])
self.finished.emit(False)
return
self.sync_output.emit(
all_location_codes, all_browser_versions, True, False, locations, browser)
self.finished.emit(True)
except Exception as e:
print(f'the error is: {e}')
self.sync_output.emit([], [], False, False, [], [])
self.finished.emit(False)
def get_connection(self):
connection = ConfigurationController.get_connection()

View file

@ -1,6 +1,6 @@
[project]
name = "sp-hydra-veil-gui"
version = "2.4.8"
version = "2.4.6"
authors = [
{ name = "Simplified Privacy" },
]
@ -12,7 +12,7 @@ classifiers = [
"Operating System :: POSIX :: Linux",
]
dependencies = [
"sp-hydra-veil-core == 2.6.5",
"sp-hydra-veil-core == 2.3.8",
"pyperclip ~= 1.9.0",
"pyqt6 ~= 6.7.1",
"qrcode[pil] ~= 8.2"