Compare commits

..

No commits in common. "234a17768939ea1b206b00fb53f205d0e3d8f39f" and "6e074482ab40720e170f83c3e4257640f1fac96e" have entirely different histories.

13 changed files with 141 additions and 525 deletions

View file

@ -775,13 +775,11 @@ class CustomWindow(QMainWindow):
if clear: if clear:
self._set_status_font_size(16) self._set_status_font_size(16)
self.status_label.setText('') self.status_label.setText('')
self.disable_marquee()
return return
text = str(text)
full_text = 'Status: ' + text full_text = 'Status: ' + text
metrics = self.status_label.fontMetrics() metrics = self.status_label.fontMetrics()
available_width = getattr(self, 'text_width', self.status_label.width() - 10) available_width = self.status_label.width() - 10
if metrics.horizontalAdvance(full_text) > available_width: if metrics.horizontalAdvance(full_text) > available_width:
self.enable_marquee(text) self.enable_marquee(text)
@ -811,12 +809,10 @@ class CustomWindow(QMainWindow):
self.navigator.navigate("menu") self.navigator.navigate("menu")
def enable_marquee(self, text): def enable_marquee(self, text):
self.marquee_text = str(text) self.marquee_text = text + " "
self.marquee_gap = " "
self.marquee_position = 0 self.marquee_position = 0
self.marquee_enabled = True self.marquee_enabled = True
self.update_marquee() self.marquee_timer.start(500)
self.marquee_timer.start(180)
def disable_marquee(self): def disable_marquee(self):
self.marquee_enabled = False self.marquee_enabled = False
@ -827,33 +823,31 @@ class CustomWindow(QMainWindow):
return return
metrics = self.status_label.fontMetrics() metrics = self.status_label.fontMetrics()
prefix = 'Status: ' text_width = metrics.horizontalAdvance(self.marquee_text)
content_width = max(
80,
getattr(self, 'text_width', self.status_label.width() - 10)
- metrics.horizontalAdvance(prefix)
)
marquee_unit = self.marquee_text + getattr(self, 'marquee_gap', " ") self.marquee_position += self.scroll_speed
if not marquee_unit.strip(): if self.marquee_position >= text_width:
self.disable_marquee()
return
if self.marquee_position >= len(marquee_unit):
self.marquee_position = 0 self.marquee_position = 0
looped_text = marquee_unit + marquee_unit looped_text = self.marquee_text * 2
visible_text = ''
for char in looped_text[self.marquee_position:]:
next_text = visible_text + char
if metrics.horizontalAdvance(next_text) > content_width:
break
visible_text = next_text
display_text = prefix + visible_text 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) self.status_label.setText(display_text)
self.marquee_position += max(1, int(self.scroll_speed / 7))
def set_scroll_speed(self, speed): def set_scroll_speed(self, speed):
self.scroll_speed = speed self.scroll_speed = speed

0
gui/v2/actions/disable_profiles.py Executable file → Normal file
View file

35
gui/v2/actions/locations.py Executable file
View file

@ -0,0 +1,35 @@
def location_candidates(profile, preferred=None):
candidates = []
seen = set()
def add(val):
if val is None:
return
s = str(val).strip().lower().replace(' ', '_')
if s and s not in seen:
seen.add(s)
candidates.append(s)
if preferred:
add(preferred)
sources = []
try:
if profile and profile.connection and profile.connection.location:
sources.append(profile.connection.location)
except Exception:
pass
try:
if profile and profile.location:
sources.append(profile.location)
except Exception:
pass
for source in sources:
add(getattr(source, 'id', None))
add(getattr(source, 'country_name', None))
add(getattr(source, 'name', None))
add(getattr(source, 'code', None))
add(getattr(source, 'country_code', None))
return candidates

View file

@ -1,74 +0,0 @@
import inspect
from enum import Enum
from typing import Any
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import ConnectionChoice
from core.models.Result import ResultError
from core.services.prepare_tickets import ticket_tracker
DEFAULT_ENUM_MAP = {
ResultError.SUBSCRIPTION: (
ticket_tracker.wipe_one_ticket_sub,
{},
),
ResultError.CONNECTION: (
ConfigurationController.set_connection_enum,
{'connection_enum': ConnectionChoice.SYSTEM},
),
ResultError.INVALID_API_REPLY: None,
}
def has_function(enum_map: dict[Enum, Any], enum_key: Enum) -> bool:
action = enum_map.get(enum_key)
if action is None:
return False
function, _ = _normalize_action(action)
return callable(function)
def error_dispatch(enum_map: dict[Enum, Any], enum_key: Enum, **kwargs: Any) -> Any:
if enum_key not in enum_map:
return None
action = enum_map[enum_key]
if action is None:
return None
function, default_kwargs = _normalize_action(action)
call_kwargs = dict(default_kwargs)
call_kwargs.update(kwargs)
return function(**_filter_kwargs(function, call_kwargs))
def has_result_action(result, enum_map: dict[Enum, Any] | None = None) -> bool:
return has_function(enum_map or DEFAULT_ENUM_MAP, result.error_type)
def dispatch_result_action(result, enum_map: dict[Enum, Any] | None = None, **kwargs: Any) -> Any:
return error_dispatch(enum_map or DEFAULT_ENUM_MAP, result.error_type, **kwargs)
def _normalize_action(action: Any) -> tuple[Any, dict[str, Any]]:
if isinstance(action, tuple):
function = action[0]
if len(action) > 1 and isinstance(action[1], dict):
return function, action[1]
return function, {}
return action, {}
def _filter_kwargs(function: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
signature = inspect.signature(function)
parameters = signature.parameters
if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values()):
return kwargs
accepted = {
name
for name, param in parameters.items()
if param.kind in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
}
return {key: value for key, value in kwargs.items() if key in accepted}

View file

@ -1,18 +0,0 @@
from core.models.Result import Result, ResultError
def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result:
message = str(exception) or type(exception).__name__
return Result(valid=False, error_type=error_type, message=message)
def result_from_payload(payload) -> Result:
if isinstance(payload, Result):
return payload
if isinstance(payload, Exception):
return result_from_exception(payload)
return Result(
valid=False,
error_type=ResultError.UNKNOWN,
message=str(payload) or 'The operation could not be completed.',
)

View file

@ -26,5 +26,5 @@ def create_top_section(parent, css_path):
'marquee_timer': marquee_timer, 'marquee_timer': marquee_timer,
'text_start_x': 15, 'text_start_x': 15,
'text_end_x': 420, 'text_end_x': 420,
'text_width': 500, 'text_width': 405,
} }

View file

@ -22,8 +22,6 @@ from core.models.system.SystemProfile import SystemProfile
from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.actions.database_health import GuiStorageDatabaseError from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.actions.operation_result_dispatch import has_result_action
from gui.v2.actions.operation_results import result_from_payload
from gui.v2.actions.profile_order import normalize_profile_order 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.profile_status import is_profile_enabled_for_gui
from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.Page import Page
@ -31,7 +29,6 @@ from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
from gui.v2.ui.popups.operation_result_popup import OperationResultPopup
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
from gui.v2.workers.worker import Worker from gui.v2.workers.worker import Worker
from gui.v2.workers.worker_thread import WorkerThread from gui.v2.workers.worker_thread import WorkerThread
@ -784,6 +781,7 @@ class MenuPage(Page):
profile_location = None profile_location = None
profile_operator = self._location_operator(profile_obj) profile_operator = self._location_operator(profile_obj)
operator_name = getattr(profile_operator, 'name', '') if profile_operator else "" operator_name = getattr(profile_operator, 'name', '') if profile_operator else ""
if protocol.lower() == "wireguard" and is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard': 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( verification_widget = self.create_verification_widget(
profile_obj) profile_obj)
@ -791,7 +789,7 @@ class MenuPage(Page):
show_verification_widget = True show_verification_widget = True
label_principal = None label_principal = None
elif operator_name != 'Simplified Privacy' and protocol.lower() in ("wireguard", "hysteria2", "vless"): elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard":
if isinstance(profile_obj, SessionProfile): if isinstance(profile_obj, SessionProfile):
text_color = "black" text_color = "black"
label_background = QLabel(self) label_background = QLabel(self)
@ -1260,43 +1258,10 @@ class MenuPage(Page):
self.worker.update_signal.connect(self.update_gui_main_thread) self.worker.update_signal.connect(self.update_gui_main_thread)
self.worker.change_page.connect(self.change_app_page) self.worker.change_page.connect(self.change_app_page)
self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup) self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup)
self.worker.operation_failed.connect(self.handle_operation_failure)
thread = threading.Thread(target=self.worker.run) thread = threading.Thread(target=self.worker.run)
thread.start() thread.start()
def handle_operation_failure(self, payload):
result = result_from_payload(payload)
if result.valid:
if result.message:
self.update_status.update_status(result.message)
return
message = result.message or result.user_message()
self.update_status.update_status(message)
self.boton_just.setEnabled(True)
self.boton_just_session.setEnabled(True)
self.disconnect_button.setEnabled(True)
self.disconnect_system_wide_button.setEnabled(True)
is_actionable = has_result_action(result)
self.popup = OperationResultPopup(
self,
message=message,
action_button_text="Yes" if is_actionable else "OK",
cancel_button_text="No" if is_actionable else None,
action_result=is_actionable,
)
self.popup.action_selected.connect(
lambda accepted: self.handle_operation_popup_action(accepted))
self.popup.show()
def handle_operation_popup_action(self, accepted):
if self.worker is None:
return
self.worker.handle_operation_popup_choice(accepted)
def show_ticket_data_loss_popup(self, ticket_number, billing_code): def show_ticket_data_loss_popup(self, ticket_number, billing_code):
self.custom_window.navigator.navigate("menu") self.custom_window.navigator.navigate("menu")
self.update_status.update_status('Critical: invalid billing code from ticket.') self.update_status.update_status('Critical: invalid billing code from ticket.')
@ -1342,8 +1307,6 @@ class MenuPage(Page):
lambda: self.handle_sync_after_verification(profile_id)) lambda: self.handle_sync_after_verification(profile_id))
self.worker_thread.sync_output.connect( self.worker_thread.sync_output.connect(
self.update_status.update_values) self.update_status.update_values)
self.worker_thread.sync_error.connect(
self.update_status.update_status)
self.worker_thread.start() self.worker_thread.start()
else: else:
self.update_status.update_status("Profile enable aborted") self.update_status.update_status("Profile enable aborted")

View file

@ -82,8 +82,6 @@ class SyncScreen(Page):
self.worker_thread = WorkerThread('SYNC') self.worker_thread = WorkerThread('SYNC')
self.worker_thread.sync_output.connect(self.update_output) self.worker_thread.sync_output.connect(self.update_output)
self.worker_thread.sync_error.connect(
self.update_status.update_status)
self.worker_thread.start() self.worker_thread.start()
def update_output(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): def update_output(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):

View file

@ -1,8 +1,10 @@
from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton
from PyQt6 import QtCore from PyQt6 import QtCore
from core.controllers.ProfileController import ProfileController
from core.controllers.tickets.UseTicketController import get_unused_tickets from core.controllers.tickets.UseTicketController import get_unused_tickets
from gui.v2.actions.locations import location_candidates
from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.Page import Page
@ -89,14 +91,20 @@ class TicketOrBillingChoicePage(Page):
self.status_label.setText("Pick a ticket from the list first.") self.status_label.setText("Pick a ticket from the list first.")
return return
which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole) which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole)
profile_id = self.update_status.current_profile_id
try: try:
profile_id = int(self.update_status.current_profile_id) profile = ProfileController.get(int(profile_id))
except (TypeError, ValueError): except Exception:
self.status_label.setText("Could not determine profile.") profile = None
candidates = location_candidates(profile)
if not candidates:
self.status_label.setText("Could not determine profile location.")
return return
profile_data = { profile_data = {
'id': profile_id, 'id': int(profile_id),
'use_ticket': which_ticket 'use_ticket': which_ticket,
'ticket_location': candidates[0],
'profile': profile
} }
menu_page = self.custom_window.navigator.get_cached("menu") menu_page = self.custom_window.navigator.get_cached("menu")
if menu_page: if menu_page:

View file

@ -1,200 +0,0 @@
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QFont, QGuiApplication
from PyQt6.QtWidgets import (
QDialog,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.v2.ui.styles.styles import (
POPUP_ACTION_BUTTON_RED_QSS,
POPUP_BG_QSS,
POPUP_CANCEL_BUTTON_QSS,
)
class OperationResultPopup(QDialog):
action_selected = pyqtSignal(bool)
def __init__(self, parent=None, message="", title="Operation Failed", action_button_text="Yes", cancel_button_text="No", action_result=True):
super().__init__(parent)
self.parent_window = parent
self.title_text = title or "Operation Failed"
self.message = message or "The operation could not be completed."
self.action_button_text = action_button_text
self.cancel_button_text = cancel_button_text
self.action_result = action_result
self._completed = False
self._use_parent_local_geometry = QGuiApplication.platformName().lower().startswith("wayland")
self.initUI()
def initUI(self):
self.setFixedSize(560, 300)
if self._use_parent_local_geometry:
if self.parent_window is not None:
self.setParent(self.parent_window)
self.setWindowFlags(Qt.WindowType.Widget)
else:
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
bg_widget = QWidget(self)
bg_widget.setStyleSheet(POPUP_BG_QSS)
outer.addWidget(bg_widget)
content_layout = QVBoxLayout(bg_widget)
content_layout.setContentsMargins(22, 14, 22, 18)
content_layout.setSpacing(12)
top_row = QHBoxLayout()
top_row.setContentsMargins(0, 0, 0, 0)
top_row.setSpacing(10)
title_label = QLabel(self.title_text)
title_label.setFont(QFont("Arial", 18, QFont.Weight.Bold))
title_label.setStyleSheet("color: #d62828; background: transparent; border: none;")
title_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
title_label.setMinimumWidth(0)
top_row.addWidget(title_label, 1)
close_button = QPushButton("X")
close_button.setFixedSize(36, 36)
close_button.setFont(QFont("Arial", 14, QFont.Weight.Bold))
close_button.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #444444;
font-size: 18px;
font-weight: 900;
border: none;
}
QPushButton:hover {
color: #d62828;
}
""")
close_button.clicked.connect(lambda: self._choose(False))
top_row.addWidget(close_button, 0, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)
content_layout.addLayout(top_row)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setStyleSheet("background: transparent; border: none;")
content_layout.addWidget(scroll_area, 1)
scroll_content = QWidget()
scroll_content.setStyleSheet("background: transparent; border: none;")
scroll_layout = QVBoxLayout(scroll_content)
scroll_layout.setContentsMargins(0, 0, 0, 0)
message_label = QLabel(self.message)
message_label.setFont(QFont("Arial", 12))
message_label.setStyleSheet("color: #333333; background: transparent; border: none;")
message_label.setWordWrap(True)
message_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
message_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
scroll_layout.addWidget(message_label)
scroll_layout.addStretch()
scroll_area.setWidget(scroll_content)
button_row = QHBoxLayout()
button_row.addStretch()
if self.cancel_button_text is not None:
cancel_button = QPushButton(self.cancel_button_text)
cancel_button.setFixedSize(max(110, min(180, 22 + len(self.cancel_button_text) * 8)), 42)
cancel_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
cancel_button.setStyleSheet(POPUP_CANCEL_BUTTON_QSS)
cancel_button.clicked.connect(lambda: self._choose(False))
button_row.addWidget(cancel_button)
action_button = QPushButton(self.action_button_text)
action_button.setFixedSize(max(110, min(180, 22 + len(self.action_button_text) * 8)), 42)
action_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
action_button.setCursor(Qt.CursorShape.PointingHandCursor)
action_button.setStyleSheet(POPUP_ACTION_BUTTON_RED_QSS)
action_button.clicked.connect(lambda: self._choose(self.action_result))
button_row.addWidget(action_button)
content_layout.addLayout(button_row)
if not self._use_parent_local_geometry:
self.setWindowModality(Qt.WindowModality.ApplicationModal)
def _choose(self, accepted):
if self._completed:
return
self._completed = True
self.action_selected.emit(accepted)
self.accept()
def closeEvent(self, event):
if not self._completed:
self._completed = True
self.action_selected.emit(False)
event.accept()
def show(self):
self._center_on_parent()
super().show()
def showEvent(self, event):
super().showEvent(event)
self._center_on_parent()
QTimer.singleShot(0, self._center_on_parent)
def _center_on_parent(self):
parent = self.parent_window
if parent is None:
return
try:
if not hasattr(parent, 'mapToGlobal') or not hasattr(parent, 'rect'):
return
parent_local_rect = parent.rect()
if parent_local_rect.isEmpty():
return
my_size = self.size()
if my_size.isEmpty() or my_size.width() <= 0 or my_size.height() <= 0:
my_size = self.sizeHint()
if self._use_parent_local_geometry:
target_x = parent_local_rect.x() + (parent_local_rect.width() - my_size.width()) // 2
target_y = parent_local_rect.y() + (parent_local_rect.height() - my_size.height()) // 2
target_x = max(parent_local_rect.left(), min(target_x, parent_local_rect.right() - my_size.width() + 1))
target_y = max(parent_local_rect.top(), min(target_y, parent_local_rect.bottom() - my_size.height() + 1))
else:
parent_center = parent.mapToGlobal(parent_local_rect.center())
target_x = parent_center.x() - my_size.width() // 2
target_y = parent_center.y() - my_size.height() // 2
screen = None
if hasattr(parent, 'screen'):
try:
screen = parent.screen()
except Exception:
screen = None
if screen is not None:
avail = screen.availableGeometry()
target_x = max(avail.left(), min(target_x, avail.right() - my_size.width()))
target_y = max(avail.top(), min(target_y, avail.bottom() - my_size.height()))
self.setGeometry(target_x, target_y, my_size.width(), my_size.height())
if self._use_parent_local_geometry:
self.raise_()
self.setFocus(Qt.FocusReason.PopupFocusReason)
except Exception:
pass
def mousePressEvent(self, event):
self.oldPos = event.globalPosition().toPoint()
def mouseMoveEvent(self, event):
delta = event.globalPosition().toPoint() - self.oldPos
self.move(self.x() + delta.x(), self.y() + delta.y())
self.oldPos = event.globalPosition().toPoint()

View file

@ -7,6 +7,7 @@ from core.controllers.tickets.FailedVerificationController import (
evaluate_if_its_the_key, evaluate_if_its_the_key,
prepare_tickets_with_saved_blind_sigs, prepare_tickets_with_saved_blind_sigs,
) )
from core.controllers.tickets.UseTicketController import use_ticket
from gui.v2.infrastructure.setup_observers import ( from gui.v2.infrastructure.setup_observers import (
connection_observer, connection_observer,
@ -21,6 +22,7 @@ class TicketingWorkerThread(QThread):
not_paid = pyqtSignal() not_paid = pyqtSignal()
paid_check_failed = pyqtSignal(str) paid_check_failed = pyqtSignal(str)
prep_done = pyqtSignal(object) prep_done = pyqtSignal(object)
use_done = pyqtSignal(object)
failed_verification_evaluated = pyqtSignal(object) failed_verification_evaluated = pyqtSignal(object)
saved_blind_prep_done = pyqtSignal(object) saved_blind_prep_done = pyqtSignal(object)
error = pyqtSignal(str) error = pyqtSignal(str)
@ -69,5 +71,14 @@ class TicketingWorkerThread(QThread):
elif self.action == 'PREPARE_SAVED_BLIND_SIGS': elif self.action == 'PREPARE_SAVED_BLIND_SIGS':
result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer) result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer)
self.saved_blind_prep_done.emit(result) 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.use_done.emit(result)
except Exception as e: except Exception as e:
self.error.emit(str(e)) self.error.emit(str(e))

View file

@ -24,9 +24,8 @@ from core.Errors import (
UnsupportedApplicationVersionError, UnsupportedApplicationVersionError,
) )
from gui.v2.actions.locations import location_candidates
from gui.v2.actions.database_health import GuiStorageDatabaseError from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.actions.operation_result_dispatch import dispatch_result_action
from gui.v2.actions.operation_results import result_from_exception
from gui.v2.infrastructure.screen_size import get_max_screensize from gui.v2.infrastructure.screen_size import get_max_screensize
from gui.v2.infrastructure.setup_observers import ( from gui.v2.infrastructure.setup_observers import (
application_version_observer, application_version_observer,
@ -40,7 +39,6 @@ class Worker(QObject):
update_signal = pyqtSignal(str, bool, int, int, str) update_signal = pyqtSignal(str, bool, int, int, str)
change_page = pyqtSignal(str, bool) change_page = pyqtSignal(str, bool)
ticket_data_loss = pyqtSignal(str, str) ticket_data_loss = pyqtSignal(str, str)
operation_failed = pyqtSignal(object)
def __init__(self, profile_data): def __init__(self, profile_data):
self.profile_data = profile_data self.profile_data = profile_data
@ -52,8 +50,6 @@ class Worker(QObject):
self.profile_type = None self.profile_type = None
self._ticket_error_emitted = False self._ticket_error_emitted = False
self._consumed_ticket = None self._consumed_ticket = None
self._pending_operation_result = None
self._pending_operation_kwargs = {}
def run(self): def run(self):
try: try:
@ -74,7 +70,8 @@ class Worker(QObject):
if 'use_ticket' in self.profile_data: if 'use_ticket' in self.profile_data:
ticket_billing_code = self._consume_ticket( ticket_billing_code = self._consume_ticket(
self.profile_data['use_ticket']) self.profile_data['use_ticket'],
self.profile_data.get('ticket_location'))
if ticket_billing_code is None: if ticket_billing_code is None:
return return
self.profile_data['billing_code'] = ticket_billing_code self.profile_data['billing_code'] = ticket_billing_code
@ -87,20 +84,18 @@ class Worker(QObject):
self.profile_data['billing_code'] = ticket_billing_code self.profile_data['billing_code'] = ticket_billing_code
if 'billing_code' in self.profile_data: if 'billing_code' in self.profile_data:
try: subscription = SubscriptionController.get(
subscription = SubscriptionController.get( self.profile_data['billing_code'], connection_observer=connection_observer)
self.profile_data['billing_code'], connection_observer=connection_observer)
except Exception as e:
if self._consumed_ticket is not None:
self._emit_subscription_lookup_failure(exception=e)
return
subscription = None
if subscription is not None: if subscription is not None:
ProfileController.attach_subscription( ProfileController.attach_subscription(
self.profile, subscription) self.profile, subscription)
else: else:
if self._consumed_ticket is not None: if self._consumed_ticket is not None:
self._emit_subscription_lookup_failure() self._ticket_error_emitted = True
self.ticket_data_loss.emit(
self._consumed_ticket,
str(self.profile_data.get('billing_code', '')),
)
return return
self.change_page.emit('The billing code is invalid.', True) self.change_page.emit('The billing code is invalid.', True)
return return
@ -113,21 +108,15 @@ class Worker(QObject):
ignore_exceptions.append(ProfileStateConflictError) ignore_exceptions.append(ProfileStateConflictError)
ignore_tuple = tuple(ignore_exceptions) ignore_tuple = tuple(ignore_exceptions)
max_resolution = get_max_screensize() max_resolution = get_max_screensize()
enable_result = ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer, ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
application_version_observer=application_version_observer, application_version_observer=application_version_observer,
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution) connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
if isinstance(enable_result, Result) and not enable_result.valid:
self._emit_operation_failure(enable_result)
return
except EndpointVerificationError: except EndpointVerificationError:
self.update_signal.emit( self.update_signal.emit(
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None) "ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
except (InvalidSubscriptionError, MissingSubscriptionError) as e: except (InvalidSubscriptionError, MissingSubscriptionError) as e:
if self._consumed_ticket is not None: self.change_page.emit(
self._emit_subscription_lookup_failure(exception=e) f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
else:
self.change_page.emit(
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
except ProfileActivationError: except ProfileActivationError:
self.update_signal.emit( self.update_signal.emit(
"The profile could not be enabled", False, None, None, None) "The profile could not be enabled", False, None, None, None)
@ -181,6 +170,9 @@ class Worker(QObject):
return None return None
def _location_candidates(self, preferred=None):
return location_candidates(self.profile, preferred)
def _profile_has_valid_subscription(self): def _profile_has_valid_subscription(self):
try: try:
sub = getattr(self.profile, 'subscription', None) sub = getattr(self.profile, 'subscription', None)
@ -209,137 +201,55 @@ class Worker(QObject):
try: try:
print("using a random ticket from GUI...") print("using a random ticket from GUI...")
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer) which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
except Exception as e: except Exception:
self._emit_ticket_failure(None, exception=e)
return None return None
if error_msg: if error_msg:
self._emit_ticket_failure( self._ticket_error_emitted = True
None, self.change_page.emit(f'Ticket use failed: {error_msg}', True)
result=Result(
valid=False,
error_type=ResultError.TICKET,
message=f'Ticket use failed: {error_msg}',
),
)
return None return None
if which_ticket is None or which_ticket == 'error': if which_ticket is None or which_ticket == 'error':
return None return None
return self._consume_ticket(which_ticket) return self._consume_ticket(which_ticket, None)
def _ticket_location_id(self): def _consume_ticket(self, which_ticket, which_location):
location = getattr(self.profile, 'location', None) candidates = self._location_candidates(preferred=which_location)
location_id = getattr(location, 'id', None) if not candidates:
if location_id is None: self._ticket_error_emitted = True
return None self.change_page.emit('Could not determine profile location for ticket use.', True)
if isinstance(location_id, str):
location_id = location_id.strip()
if location_id == '':
return None
try:
return int(location_id)
except (TypeError, ValueError):
return None return None
def _emit_ticket_failure(self, which_ticket, result=None, message=None, exception=None): last_msg = None
self._ticket_error_emitted = True for cand in candidates:
if exception is not None: try:
result = result_from_exception(exception) outcome = use_ticket(
elif result is None: which_ticket=which_ticket,
result = Result( which_location=cand,
valid=False, ticket_observer=ticket_observer,
error_type=ResultError.UNKNOWN, connection_observer=connection_observer,
message=message or 'Ticket use failed.', profile=self.profile
)
self._emit_operation_failure(result, which_ticket=which_ticket)
def _emit_operation_failure(self, result, **kwargs):
self._pending_operation_result = result
self._pending_operation_kwargs = {
key: value for key, value in kwargs.items()
if value is not None
}
self.operation_failed.emit(result)
def handle_operation_popup_choice(self, accepted):
if not accepted:
return None
if not isinstance(self._pending_operation_result, Result):
return None
return dispatch_result_action(
self._pending_operation_result,
**self._pending_operation_kwargs,
)
def _emit_subscription_lookup_failure(self, exception=None):
message = (
"Ticket was accepted, but the subscription details could not be "
"retrieved. Check your connection and try again."
)
if exception is not None:
error_text = str(exception)
if error_text:
message = f"{message} {error_text}"
self._emit_ticket_failure(
self._consumed_ticket,
result=Result(
valid=False,
error_type=ResultError.CONNECTION,
message=message,
),
)
def _consume_ticket(self, which_ticket):
which_location = self._ticket_location_id()
if which_location is None:
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.MISSING_DATA,
message='Could not determine profile location for ticket use.',
),
)
return None
try:
outcome = use_ticket(
which_ticket=which_ticket,
which_location=which_location,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
profile=self.profile
)
except Exception as e:
self._emit_ticket_failure(which_ticket, exception=e)
return None
if not isinstance(outcome, Result):
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.INVALID_API_REPLY,
message=f'use_ticket returned {type(outcome).__name__}, expected Result.',
),
)
return None
if outcome.valid:
billing_code = outcome.data
if not billing_code:
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.INVALID_API_REPLY,
message='Ticket use succeeded but no billing code was returned.',
),
) )
except Exception as e:
last_msg = str(e)
continue
if not isinstance(outcome, dict):
last_msg = 'invalid_response'
continue
billing_code = outcome.get('billing_code')
if outcome.get('valid') and billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
if billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
msg = outcome.get('message', 'failed')
last_msg = msg
if msg != 'invalid_location':
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {msg}', True)
return None return None
self._consumed_ticket = str(which_ticket)
return billing_code
self._emit_ticket_failure(which_ticket, result=outcome) self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True)
return None return None
def handle_profile_status(self, profile, is_enabled): def handle_profile_status(self, profile, is_enabled):

View file

@ -8,7 +8,6 @@ from PyQt6.QtCore import QThread, pyqtSignal
from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.ClientController import ClientController from core.controllers.ClientController import ClientController
from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice
from core.Constants import Constants
from core.controllers.SyncController import new_sync from core.controllers.SyncController import new_sync
from core.controllers.InvoiceController import InvoiceController from core.controllers.InvoiceController import InvoiceController
@ -43,7 +42,6 @@ from gui.v2.infrastructure.setup_observers import (
class WorkerThread(QThread): class WorkerThread(QThread):
text_output = pyqtSignal(str) text_output = pyqtSignal(str)
sync_output = pyqtSignal(list, list, bool, bool, list, list) sync_output = pyqtSignal(list, list, bool, bool, list, list)
sync_error = pyqtSignal(str)
invoice_output = pyqtSignal(object, str) invoice_output = pyqtSignal(object, str)
invoice_finished = pyqtSignal(bool) invoice_finished = pyqtSignal(bool)
profiles_output = pyqtSignal(dict) profiles_output = pyqtSignal(dict)
@ -336,15 +334,7 @@ class WorkerThread(QThread):
ConfigurationController.set_connection('tor') ConfigurationController.set_connection('tor')
else: else:
ConfigurationController.set_connection('system') ConfigurationController.set_connection('system')
print(f"[SYNC] Syncing from URL: {Constants.SP_API_BASE_URL}/cachedsync") self.check_for_update()
sync_result = new_sync(
client_observer=client_observer,
connection_observer=connection_observer)
if not sync_result.valid:
error_message = sync_result.message or "Connection error or invalid sync URL"
self.sync_output.emit([], [], False, False, [], [])
self.sync_error.emit(f"Sync failed: {error_message}")
return
locations = LocationController.get_all() locations = LocationController.get_all()
@ -360,7 +350,6 @@ class WorkerThread(QThread):
except Exception as e: except Exception as e:
print(f'the error is: {e}') print(f'the error is: {e}')
self.sync_output.emit([], [], False, False, [], []) self.sync_output.emit([], [], False, False, [], [])
self.sync_error.emit(f"Sync failed: {e}")
def get_connection(self): def get_connection(self):
connection = ConfigurationController.get_connection() connection = ConfigurationController.get_connection()