forked from Support/sp-hydra-veil-gui
899 lines
32 KiB
Python
Executable file
899 lines
32 KiB
Python
Executable file
import os
|
|
import sys
|
|
|
|
_WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if _WORKSPACE_ROOT not in sys.path:
|
|
sys.path.insert(0, _WORKSPACE_ROOT)
|
|
|
|
import logging
|
|
import traceback
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
|
|
QDialog, QVBoxLayout, QHBoxLayout, QMessageBox
|
|
)
|
|
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
|
|
from PyQt6 import QtGui
|
|
from PyQt6.QtCore import Qt, QTimer, QEvent
|
|
|
|
from core.Constants import Constants
|
|
from core.controllers.ConfigurationController import ConfigurationController
|
|
from core.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.workers.worker_thread import WorkerThread
|
|
from gui.v2.ui.builders.bottom_section import create_bottom_section
|
|
from gui.v2.ui.builders.top_section import create_top_section
|
|
from gui.v2.ui.builders.system_tray import init_system_tray
|
|
from gui.v2.actions.config import (
|
|
setup_gui_directory,
|
|
load_gui_config,
|
|
save_gui_config,
|
|
default_gui_config,
|
|
)
|
|
from gui.v2.actions.logging_setup import (
|
|
is_logging_enabled,
|
|
setup_gui_logging,
|
|
teardown_gui_logging,
|
|
)
|
|
from gui.v2.actions.flags import (
|
|
has_shown_systemwide_prompt,
|
|
mark_systemwide_prompt_shown,
|
|
has_shown_fast_mode_prompt,
|
|
mark_fast_mode_prompt_shown,
|
|
set_fast_mode_enabled,
|
|
check_first_launch,
|
|
)
|
|
from gui.v2.actions.profile_data import (
|
|
read_profile_data,
|
|
write_profile_data,
|
|
clear_profile_data,
|
|
)
|
|
from gui.v2.actions.ticket_failure import (
|
|
save_ticket_verification_failure,
|
|
get_ticket_verification_failure,
|
|
clear_ticket_verification_failure,
|
|
)
|
|
from gui.v2.actions.should_be_synchronized import should_be_synchronized
|
|
|
|
|
|
class CustomWindow(QMainWindow):
|
|
def __init__(self, force_sync=False):
|
|
super().__init__()
|
|
sys.excepthook = self._handle_exception
|
|
self.setWindowFlags(Qt.WindowType.Window)
|
|
|
|
gui_dir = os.path.dirname(os.path.abspath(__file__))
|
|
font_path = os.path.join(gui_dir, 'resources', 'fonts')
|
|
|
|
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]
|
|
app_font = QFont(font_family)
|
|
app_font.setPointSize(10)
|
|
QApplication.setFont(app_font)
|
|
|
|
self.open_sans_family = None
|
|
open_sans_path = os.path.join(font_path, 'open-sans.ttf')
|
|
if os.path.exists(open_sans_path):
|
|
open_sans_id = QFontDatabase.addApplicationFont(open_sans_path)
|
|
if open_sans_id != -1:
|
|
self.open_sans_family = QFontDatabase.applicationFontFamilies(open_sans_id)[0]
|
|
|
|
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
|
|
self.setWindowTitle('HydraVeil')
|
|
|
|
screen = QApplication.primaryScreen()
|
|
ratio = screen.devicePixelRatio()
|
|
self.host_screen_width = int(screen.size().width() * ratio)
|
|
self.host_screen_height = int(screen.size().height() * ratio)
|
|
|
|
self._data = {"Profile_1": {}}
|
|
|
|
self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui')
|
|
self.gui_config_file = os.path.join(self.gui_config_home, 'config.json')
|
|
self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui')
|
|
self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log')
|
|
self._gui_log_handlers = []
|
|
|
|
setup_gui_directory(
|
|
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
|
|
|
self.btn_path = os.getenv('BTN_PATH', os.path.join(
|
|
gui_dir, 'resources', 'images'))
|
|
self.css_path = os.getenv('CSS_PATH', os.path.join(
|
|
gui_dir, 'resources', 'styles'))
|
|
|
|
self.force_sync = force_sync
|
|
self.is_downloading = False
|
|
self.current_profile_id = None
|
|
self.connection_manager = ConnectionManager()
|
|
|
|
current_connection = None
|
|
self.is_tor_mode = current_connection == 'tor'
|
|
|
|
self.is_animating = False
|
|
self.animation_step = 0
|
|
self.page_history = []
|
|
self.log_path = None
|
|
self._close_confirmation_pending = False
|
|
self._close_disconnect_in_progress = False
|
|
self._closing_after_disconnect = False
|
|
|
|
self.setFixedSize(800, 570)
|
|
|
|
top = create_top_section(self, self.css_path)
|
|
self.__dict__.update(top)
|
|
|
|
self.marquee_text = ""
|
|
self.marquee_position = 0
|
|
self.marquee_enabled = False
|
|
self.scroll_speed = 1
|
|
|
|
self.page_stack = QStackedWidget(self)
|
|
self.page_stack.setGeometry(0, 0, 800, 570)
|
|
|
|
bottom = create_bottom_section(
|
|
self,
|
|
self.btn_path,
|
|
Constants.HV_CLIENT_VERSION_NUMBER,
|
|
should_be_synchronized(),
|
|
)
|
|
self.__dict__.update(bottom)
|
|
self.status_label = bottom['status_label']
|
|
self.tor_text = bottom['tor_text']
|
|
self.toggle_button = bottom['toggle_button']
|
|
self.tor_icon = bottom['tor_icon']
|
|
self.sync_button = bottom['sync_button']
|
|
self.page_stack.raise_()
|
|
|
|
self.observers = setup_observers(self.update_status)
|
|
|
|
self.update_image()
|
|
self.set_scroll_speed(7)
|
|
|
|
self.toggle_button.clicked.connect(self.toggle_mode)
|
|
self.sync_button.clicked.connect(self.sync)
|
|
QApplication.instance().installEventFilter(self)
|
|
|
|
self.create_toggle(self.is_tor_mode)
|
|
self.animation_timer = QTimer()
|
|
self.animation_timer.timeout.connect(self.animate_toggle)
|
|
self.check_logging()
|
|
|
|
self.navigator = Navigator(self)
|
|
|
|
self.page_stack.currentChanged.connect(self.page_changed)
|
|
|
|
self.show()
|
|
self.init_ui()
|
|
|
|
self.tray = None
|
|
self.tray = init_system_tray(self, self.btn_path)
|
|
|
|
current_connection = self.get_current_connection()
|
|
self.is_tor_mode = current_connection == 'tor'
|
|
self.set_toggle_state(self.is_tor_mode)
|
|
|
|
core_logger.info(
|
|
f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, "
|
|
f"connection={current_connection}, tor_mode={self.is_tor_mode})"
|
|
)
|
|
|
|
def init_ui(self):
|
|
if self.check_first_launch():
|
|
self.navigator.navigate("welcome")
|
|
else:
|
|
self.navigator.navigate("menu")
|
|
self.navigator.start_preload()
|
|
if self.force_sync:
|
|
QTimer.singleShot(0, self._prompt_force_sync)
|
|
|
|
|
|
def _prompt_force_sync(self):
|
|
"""
|
|
Popup to encourage remote database sync
|
|
|
|
Context:
|
|
Database loader in __main__ passes in the 'forced_sync' variable,
|
|
If true, this popup encourages sync to avoid NoneType errors.
|
|
"""
|
|
connection = ConfigurationController.get_connection()
|
|
box = QMessageBox(self)
|
|
box.setWindowTitle("Data Update Needed")
|
|
box.setText(
|
|
"Please download the new data to avoid errors. You have profiles that need the data to function.\n\n"
|
|
f"Connection type: {connection}")
|
|
|
|
# Apply styling
|
|
# import it inside the function to reduce load on non-display flows.
|
|
from gui.v2.ui.styles.css.main_ui_css import forced_sync_popup_style
|
|
stylesheet = forced_sync_popup_style
|
|
box.setStyleSheet(stylesheet)
|
|
|
|
# Add styled buttons
|
|
sync_button = box.addButton("Ok Fetch", QMessageBox.ButtonRole.AcceptRole)
|
|
sync_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
cancel_button = box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
box.setMinimumWidth(450) # button size
|
|
box.exec()
|
|
|
|
if box.clickedButton() == sync_button:
|
|
self.sync()
|
|
|
|
|
|
def perform_update_check(self):
|
|
from core.controllers.ClientController import ClientController
|
|
update_available = ClientController.can_be_updated()
|
|
|
|
if update_available:
|
|
self.navigator.update_if_cached(
|
|
"menu", lambda p: p.on_update_check_finished())
|
|
|
|
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
|
if not status:
|
|
self.update_status('Sync failed. Please try again later.')
|
|
return
|
|
|
|
from PyQt6.QtWidgets import QPushButton
|
|
from gui.v2.actions.sync import generate_grid_positions
|
|
|
|
self.connection_manager.set_synced(True)
|
|
self.connection_manager.store_locations(locations)
|
|
self.connection_manager.store_browsers(available_browsers)
|
|
|
|
location_positions = generate_grid_positions(len(available_locations))
|
|
browser_positions = generate_grid_positions(len(available_browsers))
|
|
available_locations_list = [
|
|
(QPushButton, loc, location_positions[i])
|
|
for i, loc in enumerate(available_locations)
|
|
]
|
|
available_browsers_list = [
|
|
(QPushButton, brw, browser_positions[i])
|
|
for i, brw in enumerate(available_browsers)
|
|
]
|
|
|
|
self.navigator.update_if_cached(
|
|
"browser", lambda p: p.create_interface_elements(available_browsers_list))
|
|
self.navigator.update_if_cached(
|
|
"location", lambda p: p.create_interface_elements(available_locations_list))
|
|
self.navigator.update_if_cached(
|
|
"hidetor", lambda p: p.create_interface_elements(available_locations_list))
|
|
self.navigator.update_if_cached(
|
|
"protocol", lambda p: p.enable_protocol_buttons())
|
|
self.navigator.update_if_cached("menu", self._enable_menu_verification_icons)
|
|
|
|
def _enable_menu_verification_icons(self, menu_page):
|
|
if hasattr(menu_page, 'refresh_profiles_data'):
|
|
menu_page.refresh_profiles_data()
|
|
if hasattr(menu_page, 'buttons'):
|
|
for button in menu_page.buttons:
|
|
parent = button.parent()
|
|
if parent:
|
|
verification_icons = parent.findChildren(QPushButton)
|
|
for icon in verification_icons:
|
|
if icon.geometry().width() == 20 and icon.geometry().height() == 20:
|
|
icon.setEnabled(True)
|
|
|
|
def sync(self):
|
|
core_logger.info("User clicked sync button")
|
|
if ConfigurationController.get_connection() == 'tor':
|
|
self.worker_thread = WorkerThread('SYNC_TOR')
|
|
else:
|
|
self.worker_thread = WorkerThread('SYNC')
|
|
self.sync_button.setIcon(QtGui.QIcon(
|
|
os.path.join(self.btn_path, 'icon_sync.png')))
|
|
self.worker_thread.finished.connect(
|
|
lambda: self.perform_update_check())
|
|
self.worker_thread.sync_output.connect(self.update_values)
|
|
self.worker_thread.start()
|
|
|
|
def _handle_exception(self, identifier, message, trace):
|
|
if issubclass(identifier, UnknownConnectionTypeError):
|
|
self.setup_popup()
|
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
else:
|
|
config = self._load_gui_config()
|
|
if config and config["logging"]["gui_logging_enabled"] == True:
|
|
logging.error(
|
|
f"Uncaught exception:\n"
|
|
f"Type: {identifier.__name__}\n"
|
|
f"Value: {str(message)}\n"
|
|
f"Traceback:\n{''.join(traceback.format_tb(trace))}"
|
|
)
|
|
sys.__excepthook__(identifier, message, trace)
|
|
|
|
def get_current_connection(self):
|
|
return ConfigurationController.get_connection()
|
|
|
|
def setup_popup(self):
|
|
connection_dialog = QDialog(self)
|
|
connection_dialog.setWindowTitle("Connection Type")
|
|
connection_dialog.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowStaysOnTopHint |
|
|
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint)
|
|
connection_dialog.setModal(True)
|
|
connection_dialog.setFixedSize(400, 200)
|
|
|
|
layout = QVBoxLayout(connection_dialog)
|
|
|
|
label = QLabel(
|
|
"Please set your preference for connecting to Simplified Privacy's billing server.")
|
|
label.setWordWrap(True)
|
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
label.setStyleSheet(
|
|
"font-size: 12px; margin-bottom: 20px; color: black;")
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
|
regular_btn = QPushButton("Regular")
|
|
tor_btn = QPushButton("Tor")
|
|
|
|
button_style = """
|
|
QPushButton {
|
|
background-color: #2b2b2b;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 5px;
|
|
font-size: 12px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #3b3b3b;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #1b1b1b;
|
|
}
|
|
"""
|
|
regular_btn.setStyleSheet(button_style)
|
|
tor_btn.setStyleSheet(button_style)
|
|
|
|
button_layout.addWidget(regular_btn)
|
|
button_layout.addWidget(tor_btn)
|
|
|
|
layout.addWidget(label)
|
|
layout.addLayout(button_layout)
|
|
|
|
def set_regular_connection():
|
|
ConfigurationController.set_connection('system')
|
|
self.is_tor_mode = False
|
|
self.set_toggle_state(False)
|
|
core_logger.info("User selected 'Regular' connection type from popup")
|
|
connection_dialog.accept()
|
|
|
|
def set_tor_connection():
|
|
ConfigurationController.set_connection('tor')
|
|
self.is_tor_mode = True
|
|
self.set_toggle_state(True)
|
|
core_logger.info("User selected 'Tor' connection type from popup")
|
|
connection_dialog.accept()
|
|
|
|
regular_btn.clicked.connect(set_regular_connection)
|
|
tor_btn.clicked.connect(set_tor_connection)
|
|
|
|
connection_dialog.exec()
|
|
|
|
return ConfigurationController.get_connection()
|
|
|
|
def _setup_gui_directory(self):
|
|
setup_gui_directory(
|
|
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
|
|
|
def _load_gui_config(self):
|
|
return load_gui_config(self.gui_config_file)
|
|
|
|
def _save_gui_config(self, config):
|
|
save_gui_config(self.gui_config_file, config)
|
|
|
|
def _default_gui_config(self):
|
|
return default_gui_config()
|
|
|
|
def save_ticket_verification_failure(self, result):
|
|
return save_ticket_verification_failure(self.gui_config_file, result)
|
|
|
|
def get_ticket_verification_failure(self):
|
|
return get_ticket_verification_failure(self.gui_config_file)
|
|
|
|
def clear_ticket_verification_failure(self):
|
|
clear_ticket_verification_failure(self.gui_config_file)
|
|
|
|
def check_logging(self):
|
|
config = self._load_gui_config()
|
|
if is_logging_enabled(config):
|
|
self._setup_gui_logging()
|
|
|
|
def has_shown_systemwide_prompt(self):
|
|
return has_shown_systemwide_prompt(self.gui_config_file)
|
|
|
|
def mark_systemwide_prompt_shown(self):
|
|
mark_systemwide_prompt_shown(self.gui_config_file)
|
|
|
|
def stop_gui_logging(self):
|
|
teardown_gui_logging(self.gui_log_file, self._gui_log_handlers)
|
|
self._gui_log_handlers = []
|
|
|
|
config = self._load_gui_config()
|
|
if config:
|
|
config["logging"]["gui_logging_enabled"] = False
|
|
self._save_gui_config(config)
|
|
|
|
def _setup_gui_logging(self):
|
|
self._gui_log_handlers = setup_gui_logging(
|
|
self.gui_cache_home, self.gui_log_file, self._gui_log_handlers)
|
|
|
|
def set_tor_icon(self):
|
|
if self.is_tor_mode:
|
|
icon_path = os.path.join(self.btn_path, "toricon_mini.png")
|
|
else:
|
|
icon_path = os.path.join(self.btn_path, "toricon_mini_false.png")
|
|
pixmap = QPixmap(icon_path)
|
|
self.tor_icon.setPixmap(pixmap)
|
|
self.tor_icon.setScaledContents(True)
|
|
|
|
def create_toggle(self, is_tor_mode):
|
|
handle_x = 30 if is_tor_mode else 3
|
|
colors = self._get_toggle_colors(is_tor_mode)
|
|
|
|
if not hasattr(self, 'animation_timer'):
|
|
self.animation_timer = QTimer()
|
|
self.animation_timer.timeout.connect(self.animate_toggle)
|
|
|
|
self.toggle_bg = QLabel(self.toggle_button)
|
|
self.toggle_bg.setGeometry(0, 0, 50, 22)
|
|
|
|
self.toggle_handle = QLabel(self.toggle_button)
|
|
self.toggle_handle.setGeometry(handle_x, 3, 16, 16)
|
|
self.toggle_handle.setStyleSheet("""
|
|
QLabel {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
|
|
self.on_label = QLabel("ON", self.toggle_button)
|
|
self.on_label.setGeometry(8, 4, 15, 14)
|
|
|
|
self.off_label = QLabel("OFF", self.toggle_button)
|
|
self.off_label.setGeometry(25, 4, 40, 14)
|
|
|
|
for widget in (self.toggle_bg, self.toggle_handle, self.on_label, self.off_label):
|
|
widget.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
|
|
|
self._update_toggle_colors(colors)
|
|
if is_tor_mode:
|
|
self.set_tor_icon()
|
|
|
|
self.raise_bottom_controls()
|
|
|
|
def _get_toggle_colors(self, is_tor_mode):
|
|
return {
|
|
'bg': "#00a8ff" if is_tor_mode else "#2c3e50",
|
|
'tor': "white" if is_tor_mode else "transparent",
|
|
'sys': "transparent" if is_tor_mode else "white"
|
|
}
|
|
|
|
def _update_toggle_colors(self, colors):
|
|
self.toggle_bg.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {colors['bg']};
|
|
border-radius: 11px;
|
|
}}
|
|
""")
|
|
|
|
self.on_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {colors['tor']};
|
|
font-size: 9px;
|
|
font-weight: bold;
|
|
}}
|
|
""")
|
|
|
|
self.off_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {colors['sys']};
|
|
font-size: 9px;
|
|
font-weight: bold;
|
|
}}
|
|
""")
|
|
|
|
def set_toggle_state(self, is_tor_mode):
|
|
self.is_tor_mode = is_tor_mode
|
|
self.toggle_button.setChecked(is_tor_mode)
|
|
handle_x = 30 if is_tor_mode else 3
|
|
self.toggle_handle.setGeometry(handle_x, 3, 16, 16)
|
|
self._update_toggle_colors(self._get_toggle_colors(is_tor_mode))
|
|
self.set_tor_icon()
|
|
|
|
def toggle_mode(self, *_args):
|
|
if self.is_animating and not self.animation_timer.isActive():
|
|
self.is_animating = False
|
|
|
|
if not self.is_animating:
|
|
target_is_tor = not self.is_tor_mode
|
|
new_connection = 'tor' if target_is_tor else 'system'
|
|
core_logger.info(f"Tor toggle clicked; switching connection mode to '{new_connection}'")
|
|
self.update_status(f"Tor toggle clicked: switching to {new_connection}")
|
|
|
|
try:
|
|
ConfigurationController.set_connection(new_connection)
|
|
except Exception as error:
|
|
core_logger.exception(
|
|
f"Tor toggle failed while switching connection mode to '{new_connection}'")
|
|
self.update_status(f"Tor toggle failed: {error}")
|
|
self.set_toggle_state(self.is_tor_mode)
|
|
return
|
|
|
|
self.set_toggle_state(target_is_tor)
|
|
self.is_animating = True
|
|
self.animation_step = 0
|
|
self.animation_timer.start(16)
|
|
return
|
|
|
|
core_logger.info("Tor toggle clicked while animation was still running")
|
|
self.update_status("Tor toggle clicked: still switching")
|
|
|
|
def raise_bottom_controls(self):
|
|
for widget in (self.status_label, self.tor_text, self.toggle_button, self.tor_icon, self.sync_button):
|
|
widget.raise_()
|
|
|
|
def eventFilter(self, source, event):
|
|
if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
|
|
if self._is_toggle_event(event):
|
|
self.toggle_mode()
|
|
return True
|
|
return super().eventFilter(source, event)
|
|
|
|
def _is_toggle_event(self, event):
|
|
if not hasattr(self, 'toggle_button') or not self.toggle_button.isVisible():
|
|
return False
|
|
if hasattr(event, 'globalPosition'):
|
|
global_pos = event.globalPosition().toPoint()
|
|
else:
|
|
global_pos = event.globalPos()
|
|
for widget in (self.tor_text, self.toggle_button, self.tor_icon):
|
|
if widget.isVisible() and widget.rect().contains(widget.mapFromGlobal(global_pos)):
|
|
return True
|
|
return False
|
|
|
|
def animate_toggle(self):
|
|
TOTAL_STEPS = 5
|
|
if self.animation_step <= TOTAL_STEPS:
|
|
progress = self.animation_step / TOTAL_STEPS
|
|
|
|
if self.is_tor_mode:
|
|
new_x = 20 + (10 * progress)
|
|
start_color = "#2c3e50"
|
|
end_color = "#00a8ff"
|
|
else:
|
|
new_x = 31 - (28 * progress)
|
|
start_color = "#00a8ff"
|
|
end_color = "#2c3e50"
|
|
|
|
self.toggle_handle.setGeometry(int(new_x), 3, 16, 16)
|
|
|
|
bg_color = self.interpolate_color(start_color, end_color, progress)
|
|
self.toggle_bg.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {bg_color};
|
|
border-radius: 11px;
|
|
}}
|
|
""")
|
|
|
|
self.off_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: rgba(255, 255, 255, {1 - progress if self.is_tor_mode else progress});
|
|
font-size: 9px;
|
|
font-weight: bold;
|
|
}}
|
|
""")
|
|
self.on_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: rgba(255, 255, 255, {progress if self.is_tor_mode else 1 - progress});
|
|
font-size: 9px;
|
|
font-weight: bold;
|
|
}}
|
|
""")
|
|
|
|
self.animation_step += 1
|
|
else:
|
|
self.animation_timer.stop()
|
|
self.is_animating = False
|
|
self.set_tor_icon()
|
|
|
|
def interpolate_color(self, start_color, end_color, progress):
|
|
def hex_to_rgb(hex_color):
|
|
hex_color = hex_color.lstrip('#')
|
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
|
|
start_rgb = hex_to_rgb(start_color)
|
|
end_rgb = hex_to_rgb(end_color)
|
|
|
|
current_rgb = tuple(
|
|
int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * progress)
|
|
for i in range(3)
|
|
)
|
|
|
|
return f"rgb{current_rgb}"
|
|
|
|
def closeEvent(self, event=None):
|
|
core_logger.info("HydraVeil application closing")
|
|
|
|
if self._closing_after_disconnect:
|
|
orm.stop()
|
|
if event is not None:
|
|
event.accept()
|
|
return
|
|
|
|
if self._close_confirmation_pending or self._close_disconnect_in_progress:
|
|
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 ProfileController.is_enabled(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)
|
|
|
|
def update_image(self, appearance_value="original"):
|
|
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
|
|
pixmap = QPixmap(image_path)
|
|
self.label.setPixmap(pixmap)
|
|
self.label.setScaledContents(True)
|
|
|
|
def read_data(self):
|
|
return read_profile_data(self._data)
|
|
|
|
def write_data(self, data):
|
|
write_profile_data(self._data, data)
|
|
|
|
def clear_data(self):
|
|
self._data = clear_profile_data()
|
|
|
|
def _set_status_font_size(self, font_size):
|
|
self.status_label.setStyleSheet(
|
|
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
|
|
|
|
@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:')
|
|
self.disable_marquee()
|
|
return
|
|
|
|
if clear:
|
|
self._set_status_font_size(16)
|
|
self.status_label.setText('')
|
|
return
|
|
|
|
full_text = 'Status: ' + text
|
|
metrics = self.status_label.fontMetrics()
|
|
available_width = self.status_label.width() - 10
|
|
|
|
if metrics.horizontalAdvance(full_text) > available_width:
|
|
self.enable_marquee(text)
|
|
else:
|
|
self.status_label.setText(full_text)
|
|
self.disable_marquee()
|
|
|
|
def check_first_launch(self):
|
|
return check_first_launch(self.gui_config_file)
|
|
|
|
def has_shown_fast_mode_prompt(self):
|
|
return has_shown_fast_mode_prompt(self.gui_config_file)
|
|
|
|
def mark_fast_mode_prompt_shown(self):
|
|
mark_fast_mode_prompt_shown(self.gui_config_file)
|
|
|
|
def set_fast_mode_enabled(self, enabled):
|
|
set_fast_mode_enabled(self.gui_config_file, enabled)
|
|
|
|
def navigate_after_profile_created(self):
|
|
if not self.has_shown_fast_mode_prompt():
|
|
self.navigator.navigate("fast_mode_prompt")
|
|
else:
|
|
self.navigator.update_if_cached(
|
|
"menu",
|
|
lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None)
|
|
self.navigator.navigate("menu")
|
|
|
|
def enable_marquee(self, text):
|
|
self.marquee_text = text + " "
|
|
self.marquee_position = 0
|
|
self.marquee_enabled = True
|
|
self.marquee_timer.start(500)
|
|
|
|
def disable_marquee(self):
|
|
self.marquee_enabled = False
|
|
self.marquee_timer.stop()
|
|
|
|
def update_marquee(self):
|
|
if not self.marquee_enabled:
|
|
return
|
|
|
|
metrics = self.status_label.fontMetrics()
|
|
text_width = metrics.horizontalAdvance(self.marquee_text)
|
|
|
|
self.marquee_position += self.scroll_speed
|
|
if self.marquee_position >= text_width:
|
|
self.marquee_position = 0
|
|
|
|
looped_text = self.marquee_text * 2
|
|
|
|
chars_that_fit = metrics.horizontalAdvance(
|
|
looped_text[:self.text_width])
|
|
|
|
visible_text = looped_text[self.marquee_position:
|
|
self.marquee_position + chars_that_fit]
|
|
|
|
while metrics.horizontalAdvance(visible_text) < self.text_width:
|
|
visible_text += self.marquee_text
|
|
|
|
while metrics.horizontalAdvance(visible_text) > self.text_width:
|
|
visible_text = visible_text[:-1]
|
|
|
|
display_text = 'Status: ' + ' ' * \
|
|
(self.text_start_x - metrics.horizontalAdvance('Status: '))
|
|
display_text += visible_text
|
|
|
|
self.status_label.setText(display_text)
|
|
|
|
def set_scroll_speed(self, speed):
|
|
self.scroll_speed = speed
|
|
|
|
def page_changed(self, index):
|
|
if should_be_synchronized():
|
|
sync_icon = QtGui.QIcon(os.path.join(
|
|
self.btn_path, 'icon_sync_urgent.png'))
|
|
else:
|
|
sync_icon = QtGui.QIcon(os.path.join(
|
|
self.btn_path, 'icon_sync.png'))
|
|
|
|
self.sync_button.setIcon(sync_icon)
|
|
|
|
current_page = self.page_stack.widget(index)
|
|
if self.page_history:
|
|
previous_page = self.page_history[-1]
|
|
else:
|
|
previous_page = None
|
|
|
|
current_name = getattr(current_page, 'name', None)
|
|
previous_name = getattr(previous_page, 'name', None)
|
|
|
|
if current_name == 'Menu':
|
|
self.update_status(None)
|
|
if previous_name in ('Resume', 'Editor', 'Settings', 'FastRegistration', 'FastModePrompt'):
|
|
if hasattr(current_page, 'eliminacion'):
|
|
current_page.eliminacion()
|
|
elif current_name == 'Resume':
|
|
self.update_status('Choose a name for your profile')
|
|
if hasattr(current_page, 'eliminacion'):
|
|
current_page.eliminacion()
|
|
elif current_name == 'Id':
|
|
pass
|
|
elif current_name == 'Location':
|
|
self.update_status(None, clear=True)
|
|
elif current_name == 'InstallPackage':
|
|
self.update_status('Please check package availability.')
|
|
else:
|
|
self.update_status(None)
|
|
|
|
if current_name == 'Menu':
|
|
self.sync_button.setVisible(True)
|
|
self.tor_text.setVisible(True)
|
|
self.toggle_button.setGeometry(670, 541, 50, 22)
|
|
self.tor_icon.setGeometry(730, 537, 25, 25)
|
|
else:
|
|
self.sync_button.setVisible(False)
|
|
self.tor_text.setVisible(False)
|
|
self.toggle_button.setGeometry(540, 541, 50, 22)
|
|
self.tor_icon.setGeometry(600, 537, 25, 25)
|
|
|
|
self.raise_bottom_controls()
|
|
|
|
if current_page != previous_page:
|
|
self.page_history.append(current_page)
|
|
|
|
|
|
def start_ui(force_sync):
|
|
app = QApplication(sys.argv)
|
|
window = CustomWindow(force_sync=force_sync)
|
|
sys.exit(app.exec())
|