Compare commits
3 commits
2258eda245
...
8a0a1fc74d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a0a1fc74d | ||
|
|
bd6a9a4893 | ||
|
|
d8582c8d6d |
|
|
@ -61,6 +61,7 @@ from gui.v2.actions.profile_data import (
|
|||
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,
|
||||
|
|
@ -674,7 +675,7 @@ class CustomWindow(QMainWindow):
|
|||
self.connection_manager._connected_profiles.clear()
|
||||
for profile_id, profile in profiles.items():
|
||||
try:
|
||||
if ProfileController.is_enabled(profile):
|
||||
if is_profile_enabled_for_gui(profile):
|
||||
connected_profiles.append(profile_id)
|
||||
self.connection_manager.add_connected_profile(profile_id)
|
||||
except Exception:
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 87 KiB |
BIN
gui/resources/images/vless_mini.png
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 220 KiB |
27
gui/v2/actions/profile_status.py
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
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)
|
||||
20
gui/v2/actions/singbox_prereqs.py
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
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
|
||||
0
gui/v2/infrastructure/screen_size.py
Normal file → Executable file
|
|
@ -15,6 +15,18 @@ profile_observer = ProfileObserver()
|
|||
ticket_observer = TicketObserver()
|
||||
|
||||
|
||||
def _format_connecting_status(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:
|
||||
return str(subject)
|
||||
return 'Connecting..'
|
||||
|
||||
|
||||
def setup_observers(update_status):
|
||||
profile_observer.subscribe(
|
||||
'created', lambda event: update_status('Profile Created'))
|
||||
|
|
@ -33,7 +45,7 @@ def setup_observers(update_status):
|
|||
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}%'))
|
||||
f"Current progress: {event.meta.get('progress'):.2f}%"))
|
||||
client_observer.subscribe('updated', lambda event: update_status(
|
||||
'Restart client to apply update.'))
|
||||
|
||||
|
|
@ -58,7 +70,7 @@ def setup_observers(update_status):
|
|||
# f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
|
||||
|
||||
connection_observer.subscribe('connecting', lambda event: update_status(
|
||||
f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...'))
|
||||
_format_connecting_status(event)))
|
||||
|
||||
connection_observer.subscribe('tor_bootstrapping', lambda event: update_status(
|
||||
'Establishing Tor connection...'))
|
||||
|
|
|
|||
|
|
@ -94,3 +94,10 @@ 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)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ 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
|
||||
|
|
@ -208,6 +213,13 @@ 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'],
|
||||
|
|
@ -263,6 +275,20 @@ 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()
|
||||
|
|
@ -301,7 +327,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 != 'hidetor':
|
||||
if operator_name != 'Simplified Privacy' and operator_name != "" and protocol not in ('hidetor', *self.SINGBOX_PROTOCOLS):
|
||||
text_color = "white"
|
||||
if profile_obj and profile_obj.is_session_profile():
|
||||
text_color = "black"
|
||||
|
|
@ -413,9 +439,12 @@ 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':
|
||||
|
|
@ -579,7 +608,7 @@ class EditorPage(Page):
|
|||
|
||||
prev_button.setVisible(True)
|
||||
next_button.setVisible(True)
|
||||
if key == 'protocol' or (protocol == 'wireguard' and key == 'connection'):
|
||||
if key == 'protocol' or (connection == 'system-wide' and key == 'connection'):
|
||||
prev_button.setDisabled(True)
|
||||
next_button.setDisabled(True)
|
||||
|
||||
|
|
@ -587,6 +616,44 @@ 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.')
|
||||
|
|
|
|||
|
|
@ -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.button_next.clicked.connect(self.go_selected)
|
||||
self.replace_click_handler(self.button_next, self.go_selected)
|
||||
self.button_reverse.setVisible(True)
|
||||
self.button_reverse.clicked.connect(self.reverse)
|
||||
self.replace_click_handler(self.button_reverse, 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,6 +91,11 @@ 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")
|
||||
|
||||
def go_selected(self):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ class LocationPage(Page):
|
|||
self.update_status = main_window
|
||||
self.button_reverse.setVisible(True)
|
||||
self.connection_manager = main_window.connection_manager
|
||||
self.button_reverse.clicked.connect(self.reverse)
|
||||
self.replace_click_handler(self.button_reverse, self.reverse)
|
||||
self.replace_click_handler(self.button_next, self.go_selected)
|
||||
self.display.setGeometry(QtCore.QRect(5, 10, 390, 520))
|
||||
self.title.setGeometry(395, 40, 380, 40)
|
||||
self.title.setText("Pick a location")
|
||||
|
|
@ -66,6 +67,7 @@ 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):
|
||||
|
||||
|
|
@ -79,7 +81,7 @@ class LocationPage(Page):
|
|||
boton.setCheckable(True)
|
||||
|
||||
locations = self.connection_manager.get_location_info(icon_name)
|
||||
if locations and not (hasattr(locations, 'is_wireguard_capable') and locations.is_wireguard_capable):
|
||||
if locations and not self._location_supports_selected_protocol(locations):
|
||||
boton.setVisible(False)
|
||||
icon_path = os.path.join(self.btn_path, f"button_{icon_name}.png")
|
||||
boton.setIcon(QIcon(icon_path))
|
||||
|
|
@ -121,6 +123,30 @@ 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", "")
|
||||
|
|
@ -141,7 +167,6 @@ 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:
|
||||
|
|
@ -154,10 +179,19 @@ 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")
|
||||
|
||||
def go_selected(self):
|
||||
if self.connection_type == "system-wide":
|
||||
if self.update_swarp_json(get_connection=True) == "system-wide":
|
||||
self.custom_window.navigator.navigate("resume")
|
||||
else:
|
||||
self.custom_window.navigator.navigate("browser")
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ 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.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
|
||||
|
|
@ -307,15 +308,15 @@ class MenuPage(Page):
|
|||
|
||||
new_profile['location'] = location
|
||||
|
||||
if protocol == 'wireguard':
|
||||
new_profile['protocol'] = 'wireguard'
|
||||
if protocol in ('wireguard', 'hysteria2', 'vless'):
|
||||
new_profile['protocol'] = protocol
|
||||
else:
|
||||
new_profile['protocol'] = 'hidetor'
|
||||
|
||||
connection_type = 'browser-only' if isinstance(
|
||||
profile, SessionProfile) else 'system-wide'
|
||||
|
||||
if protocol == 'wireguard':
|
||||
if protocol in ('wireguard', 'hysteria2', 'vless'):
|
||||
new_profile['connection'] = connection_type
|
||||
elif protocol == 'tor':
|
||||
new_profile['connection'] = protocol
|
||||
|
|
@ -375,7 +376,7 @@ class MenuPage(Page):
|
|||
self.IsSystem = 0
|
||||
for profile_id, profile in profiles.items():
|
||||
try:
|
||||
is_enabled = ProfileController.is_enabled(profile)
|
||||
is_enabled = is_profile_enabled_for_gui(profile)
|
||||
except Exception:
|
||||
is_enabled = False
|
||||
if is_enabled:
|
||||
|
|
@ -575,7 +576,10 @@ 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':
|
||||
|
|
@ -584,6 +588,8 @@ 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")
|
||||
|
|
@ -841,9 +847,13 @@ class MenuPage(Page):
|
|||
nostr_txt.show()
|
||||
self.additional_labels.append(nostr_txt)
|
||||
|
||||
elif protocol.lower() in ["wireguard", "open", "residential", "hidetor"]:
|
||||
elif protocol.lower() in ["wireguard", "open", "residential", "hidetor", "hysteria2", "vless"]:
|
||||
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"))
|
||||
label_principal.setPixmap(pixmap)
|
||||
|
|
@ -851,7 +861,7 @@ class MenuPage(Page):
|
|||
label_principal.show()
|
||||
self.additional_labels.append(label_principal)
|
||||
|
||||
if protocol.lower() in ["wireguard", "open", "residential", "hidetor"]:
|
||||
if protocol.lower() in ["wireguard", "open", "residential", "hidetor", "hysteria2", "vless"]:
|
||||
|
||||
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':
|
||||
|
|
@ -954,6 +964,38 @@ 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)
|
||||
|
|
@ -1084,7 +1126,10 @@ class MenuPage(Page):
|
|||
def on_disconnect_done(self):
|
||||
self.disconnect_button.setEnabled(True)
|
||||
self.disconnect_system_wide_button.setEnabled(True)
|
||||
pass
|
||||
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}")
|
||||
|
||||
def _config_flag(self, section, key, default=False):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ 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
|
||||
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):
|
||||
|
|
@ -19,6 +21,10 @@ class NetworkingSetupPage(Page):
|
|||
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)
|
||||
|
|
@ -29,29 +35,32 @@ class NetworkingSetupPage(Page):
|
|||
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)
|
||||
|
||||
title = QLabel("Firewall & Managed-DNS Setup", self)
|
||||
title.setGeometry(138, 68, 590, 44)
|
||||
title.setStyleSheet("font-family: Arial; font-size: 26px; font-weight: bold; color: cyan;")
|
||||
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;")
|
||||
|
||||
subtitle = QLabel(
|
||||
self.subtitle_label = QLabel(
|
||||
"Optional privileged networking helpers",
|
||||
self)
|
||||
subtitle.setGeometry(140, 108, 560, 24)
|
||||
subtitle.setStyleSheet("font-family: Arial; font-size: 14px; color: #d8ffff;")
|
||||
self.subtitle_label.setGeometry(140, 108, 560, 24)
|
||||
self.subtitle_label.setStyleSheet("font-family: Arial; font-size: 14px; color: #d8ffff;")
|
||||
|
||||
description = QLabel(
|
||||
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)
|
||||
description.setGeometry(80, 152, 640, 72)
|
||||
description.setWordWrap(True)
|
||||
description.setStyleSheet("font-family: Arial; font-size: 15px; color: cyan;")
|
||||
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(
|
||||
|
|
@ -152,6 +161,94 @@ class NetworkingSetupPage(Page):
|
|||
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()
|
||||
|
|
@ -169,6 +266,10 @@ class NetworkingSetupPage(Page):
|
|||
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:
|
||||
|
|
@ -180,6 +281,10 @@ class NetworkingSetupPage(Page):
|
|||
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:
|
||||
|
|
@ -190,6 +295,9 @@ class NetworkingSetupPage(Page):
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ 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
|
||||
|
|
@ -18,7 +23,8 @@ class ProtocolPage(Page):
|
|||
self.connection_manager = main_window.connection_manager
|
||||
self.button_back.setVisible(True)
|
||||
self.update_status = main_window
|
||||
self.button_go.clicked.connect(self.go_selected)
|
||||
self.replace_click_handler(self.button_back, self.reverse)
|
||||
self.replace_click_handler(self.button_go, 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;")
|
||||
|
|
@ -36,9 +42,11 @@ 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, 90, 185, 75)),
|
||||
(QPushButton, "residential", "residential", (585, 90+30+75, 185, 75)),
|
||||
(QPushButton, "hidetor", "hidetor", (585, 90+30+75+30+75, 185, 75))
|
||||
(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))
|
||||
]):
|
||||
boton = object_type(self)
|
||||
boton.setGeometry(*geometry)
|
||||
|
|
@ -46,28 +54,40 @@ class ProtocolPage(Page):
|
|||
boton.setCheckable(True)
|
||||
boton.setDisabled(True)
|
||||
boton.setIcon(
|
||||
QIcon(os.path.join(self.btn_path, f"{icon_name}_button.png")))
|
||||
QIcon(self._button_asset(icon_name)))
|
||||
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):
|
||||
self.update_status.write_data(
|
||||
{"protocol": self.selected_protocol_icon})
|
||||
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)
|
||||
|
||||
def show_protocol(self, page_name, protocol):
|
||||
self.update_status.clear_data()
|
||||
self.display.setPixmap(QPixmap(os.path.join(self.btn_path, f"{protocol}.png")).scaled(
|
||||
self.display.setPixmap(QPixmap(self._display_asset(protocol)).scaled(
|
||||
self.display.size(), Qt.AspectRatioMode.KeepAspectRatio))
|
||||
self.selected_protocol_icon = protocol
|
||||
self.selected_page_name = page_name
|
||||
|
||||
if protocol in ["wireguard", "hidetor"]:
|
||||
if protocol in ["wireguard", "hidetor", *self.SINGBOX_PROTOCOLS]:
|
||||
self.button_go.setVisible(True)
|
||||
self.coming_soon_label.setVisible(False)
|
||||
else:
|
||||
|
|
@ -85,3 +105,10 @@ 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")
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ class ResidentialPage(Page):
|
|||
self.update_status = main_window
|
||||
self.connection_choice = None
|
||||
self.button_reverse.setVisible(True)
|
||||
self.button_reverse.clicked.connect(self.reverse)
|
||||
self.button_go.clicked.connect(self.go_selected)
|
||||
self.replace_click_handler(self.button_reverse, self.reverse)
|
||||
self.replace_click_handler(self.button_go, self.go_selected)
|
||||
|
||||
self.display_1 = QLabel(self)
|
||||
self.display_1.setGeometry(QtCore.QRect(
|
||||
|
|
@ -98,4 +98,5 @@ class ResidentialPage(Page):
|
|||
self.update_status.write_data(inserted_data)
|
||||
|
||||
def reverse(self):
|
||||
self.button_go.setVisible(False)
|
||||
self.custom_window.navigator.navigate("protocol")
|
||||
|
|
|
|||
|
|
@ -10,12 +10,18 @@ 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
|
||||
|
|
@ -23,13 +29,13 @@ class ResumePage(Page):
|
|||
self.btn_path = main_window.btn_path
|
||||
self.labels_creados = []
|
||||
self.additional_labels = []
|
||||
self.button_go.clicked.connect(self.copy_profile)
|
||||
self.replace_click_handler(self.button_go, self.copy_profile)
|
||||
self.button_back.setVisible(True)
|
||||
self.title.setGeometry(585, 40, 185, 40)
|
||||
self.title.setText("Profile Summary")
|
||||
self.display.setGeometry(QtCore.QRect(5, 50, 580, 435))
|
||||
self.buttonGroup = QButtonGroup(self)
|
||||
self.button_back.clicked.connect(self.reverse)
|
||||
self.replace_click_handler(self.button_back, self.reverse)
|
||||
self.create_arrow()
|
||||
self.create_interface_elements()
|
||||
|
||||
|
|
@ -199,6 +205,9 @@ class ResumePage(Page):
|
|||
parent_label.setPixmap(base_image)
|
||||
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")
|
||||
|
|
@ -278,8 +287,8 @@ class ResumePage(Page):
|
|||
|
||||
elif connection_exists:
|
||||
if profile_1.get("connection", "") == "system-wide":
|
||||
image_path = os.path.join(
|
||||
self.btn_path, f"wireguard_{profile_1.get('location', '')}.png")
|
||||
image_path = self._system_profile_image(
|
||||
profile_1.get('protocol', 'wireguard'), profile_1.get('location', ''))
|
||||
main_label = QLabel(self)
|
||||
main_label.setGeometry(10, 130, 500, 375)
|
||||
main_label.setPixmap(QPixmap(image_path))
|
||||
|
|
@ -336,6 +345,21 @@ 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()))
|
||||
|
||||
|
|
@ -344,10 +368,6 @@ 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]
|
||||
|
|
@ -361,13 +381,29 @@ 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,
|
||||
profiles.keys())
|
||||
existing_profile_ids)
|
||||
|
||||
main = self.update_status
|
||||
if hasattr(main, 'navigate_after_profile_created'):
|
||||
|
|
@ -376,7 +412,6 @@ 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)
|
||||
|
|
@ -408,8 +443,8 @@ class ResumePage(Page):
|
|||
parts = profile.get('location').split('_')
|
||||
country_code = parts[0]
|
||||
location_code = parts[1]
|
||||
if profile.get('protocol') == 'wireguard':
|
||||
connection_type = 'wireguard'
|
||||
if profile.get('protocol') in ('wireguard', 'hysteria2', 'vless'):
|
||||
connection_type = profile.get('protocol')
|
||||
elif profile.get('protocol') == 'hidetor' or profile.get('protocol') == 'residential':
|
||||
if profile.get('connection') == 'tor':
|
||||
connection_type = 'tor'
|
||||
|
|
|
|||
|
|
@ -345,3 +345,6 @@ class ScreenPage(Page):
|
|||
|
||||
def gestionar_next(self):
|
||||
self.custom_window.navigator.navigate("resume")
|
||||
|
||||
def gestionar_back(self):
|
||||
self.custom_window.navigator.navigate("browser")
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QButtonGroup, QMessageBox, QPushButton
|
||||
from PyQt6.QtWidgets import QButtonGroup, QMessageBox, QPushButton
|
||||
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 core.Constants import Constants
|
||||
|
||||
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
|
||||
from gui.v2.ui.pages.Page import Page
|
||||
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
||||
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
||||
|
|
@ -66,28 +64,6 @@ class TicketCryptoPickerPage(Page):
|
|||
currency = selected_button.property('currency')
|
||||
self.start_initiate_payment(currency)
|
||||
|
||||
def check_if_paid_for_existing(self, temp_billing_code):
|
||||
self.update_status.update_status("Checking if paid...")
|
||||
result = check_if_paid(temp_billing_code, ticket_observer, connection_observer)
|
||||
if isinstance(result, dict) and result.get('valid') and result.get('payment_status') == 'paid':
|
||||
self.update_status.update_status("Already Paid!")
|
||||
self.custom_window.navigator.navigate("ticket_prep")
|
||||
prep_page = self.custom_window.navigator.get_cached("ticket_prep")
|
||||
if prep_page:
|
||||
prep_page.start_prep()
|
||||
else:
|
||||
self.update_status.update_status("Not yet paid.")
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(temp_billing_code)
|
||||
not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now."
|
||||
info = QMessageBox(self)
|
||||
info.setWindowTitle("Not Paid")
|
||||
info.setText(not_paid_msg)
|
||||
info.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
style_message_box(info)
|
||||
mark_confirm_button(info.button(QMessageBox.StandardButton.Ok))
|
||||
info.exec()
|
||||
|
||||
def start_initiate_payment(self, currency):
|
||||
self.update_status.update_status("Initiating payment...")
|
||||
self.worker = TicketingWorkerThread('INITIATE_PAYMENT', params={
|
||||
|
|
@ -105,70 +81,83 @@ class TicketCryptoPickerPage(Page):
|
|||
self.update_status.update_status("Could not initiate payment.")
|
||||
return
|
||||
|
||||
if not invoice.valid:
|
||||
if not getattr(invoice, 'is_valid', False):
|
||||
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)
|
||||
payment_page.set_ticket_invoice(invoice, 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:
|
||||
self._prompt_wipe_existing(invoice)
|
||||
def _handle_api_errors(self, invoice):
|
||||
error_code = getattr(invoice, 'error_code', None)
|
||||
if error_code == "already_exists" and not self.bypass_existing:
|
||||
self._prompt_wipe_existing()
|
||||
return
|
||||
elif error_code == ResultError.BILLING_CODE_EXISTS and not self.bypass_existing:
|
||||
elif 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)
|
||||
self._prompt_wipe_existing(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
|
||||
error_msg = getattr(invoice, 'final_error_msg', None) or error_code or "Could not initiate payment."
|
||||
self.update_status.update_status(error_msg)
|
||||
return
|
||||
|
||||
def _prompt_wipe_existing(self, invoice):
|
||||
def _prompt_wipe_existing(self, temp_billing_code=None):
|
||||
msg = QMessageBox(self)
|
||||
if temp_billing_code:
|
||||
msg.setWindowTitle("Existing billing code found")
|
||||
msg.setText("You already have a ticket billing code. Use it, or wipe it and start over?")
|
||||
use_button = msg.addButton("Use existing", QMessageBox.ButtonRole.AcceptRole)
|
||||
wipe_button = msg.addButton("Wipe and start over", QMessageBox.ButtonRole.DestructiveRole)
|
||||
cancel_button = msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
||||
mark_confirm_button(use_button)
|
||||
else:
|
||||
msg.setWindowTitle("Existing tickets found")
|
||||
msg.setText("You already have ticket data. Wipe it and start over?")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
use_button = None
|
||||
wipe_button = msg.addButton("Wipe and start over", QMessageBox.ButtonRole.DestructiveRole)
|
||||
cancel_button = msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
||||
mark_confirm_button(wipe_button)
|
||||
style_message_box(msg)
|
||||
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
|
||||
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'))
|
||||
msg.exec()
|
||||
clicked = msg.clickedButton()
|
||||
if temp_billing_code and clicked == use_button:
|
||||
self.update_status.update_status("Reusing existing billing code")
|
||||
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.resume_ticket_billing(temp_billing_code)
|
||||
return
|
||||
if clicked == wipe_button:
|
||||
self._restart_payment_after_wipe()
|
||||
else:
|
||||
self.update_status.update_status("Cancelled.")
|
||||
|
||||
def _prompt_wipe_billingcode(self, temp_billing_code):
|
||||
msg = QMessageBox(self)
|
||||
msg.setWindowTitle("Existing billing code found")
|
||||
msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.")
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
style_message_box(msg)
|
||||
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
|
||||
result = msg.exec()
|
||||
if result == QMessageBox.StandardButton.Yes:
|
||||
self.update_status.update_status("Reusing same code")
|
||||
self.check_if_paid_for_existing(temp_billing_code)
|
||||
else:
|
||||
self.bypass_existing = True
|
||||
delete_ticket_data()
|
||||
def _restart_payment_after_wipe(self):
|
||||
if not self._delete_ticket_data():
|
||||
return
|
||||
self.bypass_existing = False
|
||||
currency_btn = self.buttonGroup.checkedButton()
|
||||
if currency_btn:
|
||||
self.start_initiate_payment(currency_btn.property('currency'))
|
||||
|
||||
def _delete_ticket_data(self):
|
||||
paths = [
|
||||
Constants.TICKET_TRACKER_PATH,
|
||||
os.path.join(Constants.HV_TICKETING_CONFIG_HOME, "billing_choices.json"),
|
||||
]
|
||||
try:
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
if os.path.isdir(Constants.HV_TICKETING_DATA_HOME):
|
||||
shutil.rmtree(Constants.HV_TICKETING_DATA_HOME)
|
||||
return True
|
||||
except OSError as error:
|
||||
self.update_status.update_status(f"Could not wipe ticket data: {error}")
|
||||
return False
|
||||
|
||||
def on_error(self, msg):
|
||||
self.update_status.update_status(f"Payment error: {msg}")
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ class TorPage(Page):
|
|||
QPixmap(os.path.join(self.btn_path, "browser only.png")))
|
||||
self.display0.lower()
|
||||
|
||||
self.button_go.clicked.connect(self.go_selected)
|
||||
self.replace_click_handler(self.button_go, self.go_selected)
|
||||
|
||||
self.button_reverse.setVisible(True)
|
||||
self.button_reverse.clicked.connect(self.reverse_selected)
|
||||
self.replace_click_handler(self.button_reverse, self.reverse_selected)
|
||||
self.replace_click_handler(self.button_back, self.reverse_selected)
|
||||
|
||||
self.label = QLabel(self)
|
||||
self.label.setGeometry(440, 370, 86, 130)
|
||||
|
|
@ -72,6 +73,8 @@ 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):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ class WireGuardPage(Page):
|
|||
self.selected_protocol = None
|
||||
self.selected_protocol_icon = None
|
||||
self.button_back.setVisible(True)
|
||||
self.button_go.clicked.connect(self.go_selected)
|
||||
self.replace_click_handler(self.button_back, self.reverse)
|
||||
self.replace_click_handler(self.button_go, self.go_selected)
|
||||
self.additional_labels = []
|
||||
self.title.setGeometry(585, 40, 185, 40)
|
||||
self.title.setText("Pick a Protocol")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import shlex
|
||||
import subprocess
|
||||
import inspect
|
||||
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
|
|
@ -20,8 +21,10 @@ 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.infrastructure.setup_observers import (
|
||||
application_version_observer,
|
||||
client_observer,
|
||||
connection_observer,
|
||||
invoice_observer,
|
||||
|
|
@ -50,6 +53,18 @@ 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()
|
||||
|
|
@ -69,6 +84,8 @@ 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,18 +141,50 @@ 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):
|
||||
try:
|
||||
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, ticket_observer=ticket_observer, connection_observer=connection_observer)
|
||||
self._disable_profile(profile)
|
||||
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, ticket_observer=ticket_observer, connection_observer=connection_observer)
|
||||
self._disable_profile(profile)
|
||||
self.text_output.emit("All profiles were successfully disabled")
|
||||
except SudoScript as e:
|
||||
self.text_output.emit(str(e))
|
||||
|
|
@ -244,8 +293,7 @@ class WorkerThread(QThread):
|
|||
try:
|
||||
profile = ProfileController.get(int(self.profile_data['id']))
|
||||
if profile:
|
||||
ProfileController.disable(
|
||||
profile, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer, wipe_assassin=True)
|
||||
self._disable_profile(profile)
|
||||
else:
|
||||
self.text_output.emit(
|
||||
f"No profile found with ID: {self.profile_data['id']}")
|
||||
|
|
|
|||