diff --git a/gui/__main__.py b/gui/__main__.py index 0794de9..c36d7a7 100644 --- a/gui/__main__.py +++ b/gui/__main__.py @@ -11,6 +11,8 @@ import logging import traceback import random import subprocess +import qrcode +from io import BytesIO from typing import Union from core.Errors import UnknownConnectionTypeError, CommandNotFoundError, MissingSubscriptionError, InvalidSubscriptionError, ProfileActivationError, UnsupportedApplicationVersionError, FileIntegrityError, ProfileModificationError, ProfileStateConflictError from core.controllers.ApplicationVersionController import ApplicationVersionController @@ -48,7 +50,7 @@ profile_observer = ProfileObserver() class WorkerThread(QThread): text_output = pyqtSignal(str) - sync_output = pyqtSignal(list, bool, bool, list, bool) + sync_output = pyqtSignal(list, list, bool, bool, list, bool) invoice_output = pyqtSignal(object, str) invoice_finished = pyqtSignal(bool) profiles_output = pyqtSignal(dict) @@ -230,8 +232,10 @@ class WorkerThread(QThread): ConfigurationController.set_connection('system') self.check_for_update() locations = LocationController.get_all() + browser = ApplicationVersionController.get_all() + all_browser_versions = [f"{browser.application_code}:{browser.version_number}" for browser in browser] all_location_codes = [f"{location.country_code}_{location.code}" for location in locations] - self.sync_output.emit(all_location_codes, True, False, locations, False) + self.sync_output.emit(all_location_codes, all_browser_versions, True, False, locations, False) except Exception as e: print(e) self.sync_output.emit([], False, False, [], False) @@ -445,12 +449,20 @@ class CustomWindow(QMainWindow): self.set_toggle_state(self.is_tor_mode) def perform_update_check(self): + update_available = ClientController.can_be_updated() if update_available: menu_page = self.page_stack.findChild(MenuPage) menu_page.on_update_check_finished() + def update_values(self, available_locations, available_browsers, status, is_tor, locations, is_os_error): + sync_screen = self.page_stack.findChild(SyncScreen) + if sync_screen: + sync_screen.update_after_sync(available_locations, available_browsers, locations) + + + def sync(self): if ConfigurationController.get_connection() == 'tor': self.worker_thread = WorkerThread('SYNC_TOR') @@ -458,8 +470,10 @@ class CustomWindow(QMainWindow): self.worker_thread = WorkerThread('SYNC') self.sync_button.setIcon(QtGui.QIcon(os.path.join(self.btn_path, 'icon_sync.png'))) self.worker_thread.finished.connect(lambda: self.perform_update_check()) + self.worker_thread.sync_output.connect(self.update_values) self.worker_thread.start() + def _handle_exception(self, identifier, message, trace): if issubclass(identifier, UnknownConnectionTypeError): self.setup_popup() @@ -872,7 +886,9 @@ class CustomWindow(QMainWindow): ResidentialPage(self.page_stack, self), InstallSystemPackage(self.page_stack, self), WelcomePage(self.page_stack, self), - PaymentPage(self.page_stack, self), + PaymentDetailsPage(self.page_stack, self), + DurationSelectionPage(self.page_stack, self), + CurrencySelectionPage(self.page_stack, self), PaymentConfirmed(self.page_stack, self), IdPage(self.page_stack, self), TorPage(self.page_stack, self), @@ -998,6 +1014,7 @@ class CustomWindow(QMainWindow): self.update_status(None) if isinstance(previous_page, (ResumePage, EditorPage, Settings)): current_page.eliminacion() + else: pass elif isinstance(current_page, ResumePage): @@ -1027,6 +1044,10 @@ class CustomWindow(QMainWindow): if current_page != previous_page: self.page_history.append(current_page) self.page_history = self.page_history[-5:] + + if isinstance(current_page, EditorPage): + if not self.connection_manager.is_synced(): + self.update_status('Not synchronized.') self.show() @@ -1465,7 +1486,6 @@ class MenuPage(Page): child_label.setGeometry(3 + j * 60, 5, 50, 50) child_label.setObjectName("label_profiles") child_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) - image_path = self.get_icon_path(label_name, value, connection_type) pixmap = QPixmap(image_path) if pixmap.isNull() and label_name == 'location': @@ -1474,6 +1494,12 @@ class MenuPage(Page): if pixmap.isNull(): pixmap = QPixmap(50, 50) pixmap.fill(QtGui.QColor(200, 200, 200)) + if pixmap.isNull() and label_name == 'browser': + fallback_path = os.path.join(self.btn_path, "default_browser_mini.png") + pixmap = QPixmap(fallback_path) + if pixmap.isNull(): + pixmap = QPixmap(50, 50) + pixmap.fill(QtGui.QColor(200, 200, 200)) child_label.setPixmap(pixmap) child_label.show() @@ -1493,6 +1519,12 @@ class MenuPage(Page): if pixmap.isNull(): pixmap = QPixmap(50, 50) pixmap.fill(QtGui.QColor(200, 200, 200)) + if pixmap.isNull() and label_name == 'browser': + fallback_path = os.path.join(self.btn_path, "default_browser_mini.png") + pixmap = QPixmap(fallback_path) + if pixmap.isNull(): + pixmap = QPixmap(50, 50) + pixmap.fill(QtGui.QColor(200, 200, 200)) child_label.setPixmap(pixmap) child_label.show() @@ -1905,19 +1937,7 @@ class ConnectionManager: self._is_synced = False self._is_profile_being_enabled = {} self.profile_button_objects = {} - self._location_mapping = { - 'md': 'Moldova', - 'nl': 'The Netherlands', - 'us': 'US (Ohio)', - 'ca': 'US (Seattle)', - 'fi': 'Finland', - 'is': 'Iceland', - 'my': 'Malaysia', - 'la': 'US (Los Angeles)', - 'ru': 'Russia', - 'ro': 'Romania', - 'ch': 'Switzerland' - } + self._timezone_mapping = { 'md': 'Europe/Chisinau', 'nl': 'Europe/Amsterdam', @@ -1932,6 +1952,7 @@ class ConnectionManager: 'ch': 'Europe/Zurich' } self._location_list = [] + self._browser_list = [] def get_profile_button_objects(self, profile_id): return self.profile_button_objects.get(profile_id, None) @@ -1961,20 +1982,31 @@ class ConnectionManager: def is_synced(self): return self._is_synced + def get_location_info(self, location_key): - if location_key in self._location_mapping: - return self._location_mapping[location_key] - - - return None + if not location_key: + return None + try: + country_code, location_code = location_key.split('_') + for location in self._location_list: + if location.country_code == country_code and location.code == location_code: + return location + + return None + except (ValueError, AttributeError): + return None def store_locations(self, locations): - self._location_mapping = {} self._location_list = locations - for location in locations: - self._location_mapping[location.country_name] = location - + + + def store_browsers(self, browsers): + self._browser_list = browsers + + def get_browser_list(self): + return self._browser_list + def get_location_list(self): if self.is_synced(): location_names = [f'{location.country_code}_{location.code}' for location in self._location_list] @@ -3040,12 +3072,11 @@ class BrowserPage(Page): self.title.setGeometry(395, 40, 380, 40) self.title.setText("Pick a Browser") self.button_back.setVisible(True) - self.create_interface_elements() - def create_interface_elements(self): + def create_interface_elements(self, available_browsers): self._setup_scroll_area() button_widget = self._create_button_widget() - self._populate_button_widget(button_widget) + self._populate_button_widget(button_widget, available_browsers) self.scroll_area.setWidget(button_widget) def _setup_scroll_area(self): @@ -3090,17 +3121,10 @@ class BrowserPage(Page): self.buttons = [] return button_widget - def _populate_button_widget(self, button_widget): - browser_icons = [ - ("brave 1.70.123", "librewolf 130.0.1-1"), - ("chromium 129.0.6668.89-1", "chromium 122.0.6261.94-1"), - ("firefox 131.0.2", "firefox 123.0"), - ("librewolf 125.0.2-1", "brave 1.63.165"), - ("firefox 128.0.2", "brave 1.71.121") - ] + def _populate_button_widget(self, button_widget, available_browsers): dimensions = self._get_button_dimensions() - total_height = self._create_browser_buttons(button_widget, browser_icons, dimensions) + total_height = self._create_browser_buttons(button_widget, available_browsers, dimensions) self._set_final_widget_size(button_widget, dimensions, total_height) def _get_button_dimensions(self): @@ -3113,12 +3137,12 @@ class BrowserPage(Page): def _create_browser_buttons(self, button_widget, browser_icons, dims): total_height = 0 - for row_idx, row in enumerate(browser_icons): - for col_idx, icon_name in enumerate(row): - y_coord = row_idx * (dims['height'] + dims['spacing']) - x_coord = 0 if col_idx == 0 else dims['x_offset'] - self._create_single_button(button_widget, icon_name, x_coord, y_coord, dims) - total_height = y_coord + dims['height'] + for browser_tuple in browser_icons: + _, browser_string, geometry = browser_tuple + x_coord = geometry[0] - 395 + y_coord = geometry[1] - 90 + self._create_single_button(button_widget, browser_string, x_coord, y_coord, dims) + total_height = max(total_height, y_coord + dims['height']) return total_height def _create_single_button(self, parent, icon_name, x_coord, y_coord, dims): @@ -3128,12 +3152,16 @@ class BrowserPage(Page): button.setIconSize(button.size()) button.setCheckable(True) - button.setIcon(QIcon(BrowserPage.create_browser_button_image(icon_name, self.btn_path))) + browser_name, version = icon_name.split(':') + button.setIcon(QIcon(BrowserPage.create_browser_button_image(f"{browser_name} {version}", self.btn_path))) + if button.icon().isNull(): + fallback_path = os.path.join(self.btn_path, "default_browser_button.png") + button.setIcon(QIcon(fallback_path)) self._apply_button_style(button) self.buttons.append(button) self.buttonGroup.addButton(button) - button.clicked.connect(lambda _, b=icon_name: self.show_browser(b)) + button.clicked.connect(lambda _, b=icon_name: self.show_browser(b.replace(':', ' '))) @staticmethod def create_browser_button_image(browser_text, btn_path): @@ -3154,7 +3182,6 @@ class BrowserPage(Page): painter.setFont(QFont('Arial', font_size)) painter.setPen(QColor(0x88, 0x88, 0x88)) text_rect = painter.fontMetrics().boundingRect(version) - while text_rect.width() > base_image.width() * 0.6 and font_size > 6: font_size -= 1 painter.setFont(QFont('Arial', font_size)) @@ -3179,8 +3206,11 @@ class BrowserPage(Page): def show_browser(self, browser): browser_name, version = browser.split()[0], ' '.join(browser.split()[1:]) - base_image = self._create_browser_display_image(browser_name, version) - self.display.setPixmap(base_image.scaled(self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + try: + base_image = self._create_browser_display_image(browser_name, version) + self.display.setPixmap(base_image.scaled(self.display.size(), Qt.AspectRatioMode.KeepAspectRatio)) + except Exception: + pass self.selected_browser_icon = browser self.button_next.setVisible(True) self.update_swarp_json() @@ -3346,6 +3376,8 @@ class ResumePage(Page): parent_label = QLabel(self) parent_label.setGeometry(*geometry) parent_label.setPixmap(base_image) + if parent_label.pixmap().isNull(): + parent_label.setPixmap(QPixmap(os.path.join(self.btn_path, "default_browser_button.png"))) parent_label.show() self.labels_creados.append(parent_label) elif item == 'location': @@ -3677,7 +3709,7 @@ class EditorPage(Page): "protocol": ['wireguard', 'residential', 'hidetor'], "connection": ['browser-only', 'system-wide'], "location": self.connection_manager.get_location_list(), - "browser": ['brave 1.63.165','brave 1.70.123', 'firefox 123.0', 'firefox 131.0.2', 'chromium 122.0.6261.94-1', 'chromium 129.0.6668.89-1', 'librewolf 125.0.2-1', 'librewolf 130.0.1-1', 'firefox 128.0.2', 'brave 1.71.121'], + "browser": self.connection_manager.get_browser_list(), "dimentions": ['800x600', '1024x760', '1152x1080', '1280x1024', '1920x1080'] }, selected_profile_str) @@ -3686,7 +3718,7 @@ class EditorPage(Page): "protocol": ['residential', 'wireguard', 'hidetor'], "connection": ['tor', 'just proxy'], "location": self.connection_manager.get_location_list(), - "browser": ['brave 1.63.165','brave 1.70.123', 'firefox 123.0', 'firefox 131.0.2', 'chromium 122.0.6261.94-1', 'chromium 129.0.6668.89-1', 'librewolf 125.0.2-1', 'librewolf 130.0.1-1', 'firefox 128.0.2', 'brave 1.71.121'], + "browser": self.connection_manager.get_browser_list(), "dimentions": ['800x600', '1024x760', '1152x1080', '1280x1024', '1920x1080'] }, selected_profile_str) @@ -3745,18 +3777,23 @@ class EditorPage(Page): for i, key in enumerate(parameters.keys()): if key == 'browser': - current_value = f"{data_profile.get(key, '')}" - split_value = current_value.split(' ', 1) + browser_value = f"{data_profile.get(key, '')}" + split_value = browser_value.split(':', 1) + browser_type = split_value[0] browser_version = split_value[1] if len(split_value) > 1 else '' + browser_value = f"{browser_type} {browser_version}" if browser_version == '': - browser_version = data_profile.get('browser_version', '') - current_value = f"{data_profile.get(key, '')} {browser_version}" - if connection == 'system-wide' or not current_value.strip(): + browser_version = data_profile.get('browser_version', '') + browser_value = f"{browser_type} {browser_version}" + if connection == 'system-wide' or not browser_value.strip(): base_image = QPixmap() else: - base_image = BrowserPage.create_browser_button_image(current_value, self.btn_path) + base_image = BrowserPage.create_browser_button_image(browser_value, self.btn_path) + if base_image.isNull(): + base_image = QPixmap(os.path.join(self.btn_path, "default_browser_button.png")) elif key == 'location': - image_path = os.path.join(self.btn_path, f"button_{data_profile.get(key, '')}.png") + current_value = f"{data_profile.get(key, '')}" + image_path = os.path.join(self.btn_path, f"button_{current_value}.png") base_image = QPixmap(image_path) if base_image.isNull(): base_image = QPixmap(os.path.join(self.btn_path, "default_location_button.png")) @@ -3774,7 +3811,10 @@ class EditorPage(Page): self.labels.append(label) try: - value_index = parameters[key].index(current_value) + if key == 'browser': + value_index = [item.replace(':', ' ') for item in parameters[key]].index(browser_value) + else: + value_index = parameters[key].index(current_value) except ValueError: value_index = 0 @@ -3823,6 +3863,7 @@ class EditorPage(Page): def show_next_value(self, key: str, index: int, parameters: dict) -> None: + if len(parameters[key]) > 1: next_index = (index + 1) % len(parameters[key]) next_value = parameters[key][next_index] @@ -3867,6 +3908,7 @@ class EditorPage(Page): self.popup.finished.connect(lambda result: self.handle_apply_changes_response(result)) self.popup.show() return True + def show_unsaved_changes_popup(self) -> None: self.popup = ConfirmationPopup( self, @@ -3933,7 +3975,7 @@ class EditorPage(Page): self.update_status.update_status('System wide profiles not supported atm') elif key == 'browser': - browser_type, browser_version = new_value.split(' ', 1) + browser_type, browser_version = new_value.split(':', 1) profile.application_version.application_code = browser_type profile.application_version.version_number = browser_version @@ -3947,7 +3989,6 @@ class EditorPage(Page): else: location = self.connection_manager.get_location_info(new_value) - if profile.has_wireguard_configuration(): profile.delete_wireguard_configuration() @@ -3955,8 +3996,9 @@ class EditorPage(Page): profile.delete_proxy_configuration() if location: - profile.location.code = location.country_code - profile.location.time_zone = self.connection_manager.get_timezone(location) + profile.location.code = location.code + profile.location.country_code = location.country_code + profile.location.time_zone = self.connection_manager.get_timezone(location.country_code) profile.subscription = None @@ -4818,9 +4860,9 @@ class IdPage(Page): self.button_go.clicked.connect(self.go_selected) objects_info = [ - (QPushButton, os.path.join(self.btn_path, "new_id.png"), self.show_next, PaymentPage, (575, 100, 185, 75)), + (QPushButton, os.path.join(self.btn_path, "new_id.png"), self.show_next, DurationSelectionPage, (575, 100, 185, 75)), (QLabel, os.path.join(self.btn_path, "button230x220.png"), None, None, (550, 220, 250, 220)), - (QTextEdit, None, self.validate_password, PaymentPage, (550, 230, 230, 190)) + (QTextEdit, None, self.validate_password, DurationSelectionPage, (550, 230, 230, 190)) ] @@ -4853,7 +4895,7 @@ class IdPage(Page): def show_next(self): self.text_edit.clear() - self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(PaymentPage))) + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(DurationSelectionPage))) def validate_password(self): @@ -4880,318 +4922,6 @@ class IdPage(Page): def reverse(self): self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(MenuPage))) -class PaymentPage(Page): - def __init__(self, page_stack, main_window=None, parent=None): - super().__init__("Payment", page_stack, main_window, parent) - self.update_status = main_window - self.text_fields = [] - self.invoice_data = {} - self.create_interface_elements() - self.button_reverse.setVisible(True) - - - def fetch_invoice_duration(self): - selected_time = self.get_selected_time_length() - duration_month_num = int(selected_time.split()[0]) - if duration_month_num == 12: - total_hours = 365 * 24 - else: - total_hours = duration_month_num * (30 * 24) - return total_hours - - - def check_invoice(self): - total_hours = self.fetch_invoice_duration() - if self.invoice_data and self.invoice_data['duration'] == total_hours: - self.update_status.update_status('Checking invoice status...') - self.check_invoice_status(self.invoice_data['billing_details'].billing_code) - else: - self.on_request_invoice() - - def on_request_invoice(self): - total_hours = self.fetch_invoice_duration() - selected_currency = self.get_selected_currency() - if selected_currency is None: - self.update_status.update_status('No currency selected') - return - profile_data = { - 'id': int(self.update_status.current_profile_id), - 'duration': total_hours, - 'currency': 'xmr' if selected_currency == 'monero' else 'btc' if selected_currency == 'bitcoin' else 'btc-ln' if selected_currency == 'lightning' else 'ltc' if selected_currency == 'litecoin' else None - } - self.update_status.update_status('Generating Invoice...') - self.worker_thread = WorkerThread('GET_SUBSCRIPTION', profile_data=profile_data) - self.worker_thread.text_output.connect(self.invoice_update_text_output) - self.worker_thread.invoice_output.connect(self.on_invoice_generation_finished) - self.worker_thread.invoice_finished.connect(self.on_invoice_finished) - self.worker_thread.start() - - def invoice_update_text_output(self, text): - self.update_status.update_status(text) - self.text_fields[3].setText(text) - self.text_fields[3].setStyleSheet('font-size: 15px;') - - if 'No compatible' in text: - for line in self.text_fields: - line.setText('') - - - def store_invoice_data(self, billing_details: dict, duration: int): - self.invoice_data = { - 'billing_details': billing_details, - 'duration': duration - } - - def check_invoice_status(self, billing_code: str): - self.worker_thread = WorkerThread('CHECK_INVOICE_STATUS', profile_data={'billing_code': billing_code}) - self.worker_thread.invoice_finished.connect(self.on_invoice_status_finished) - self.worker_thread.start() - - def on_invoice_status_finished(self, result): - if result: - self.update_status.update_status('Invoice is still valid. Parsing invoice data...') - self.parse_invoice_data(self.invoice_data['billing_details']) - else: - self.update_status.update_status('Invoice has expired. Generating new invoice...') - self.on_request_invoice() - - def on_invoice_generation_finished(self, billing_details: object, text: str): - total_hours = self.fetch_invoice_duration() - self.store_invoice_data(billing_details, duration=total_hours) - self.parse_invoice_data(billing_details) - - - def parse_invoice_data(self, invoice_data: object): - currency = self.get_selected_currency() - billing_details = { - 'billing_code': invoice_data.billing_code - } - - if currency.lower() == 'lightning': - currency = 'Bitcoin Lightning' - preferred_method = next((pm for pm in invoice_data.payment_methods if pm.name.lower() == currency.lower()), None) - - if preferred_method: - billing_details['due_amount'] = preferred_method.due - billing_details['address'] = preferred_method.address - else: - self.update_status.update_status('No payment method found for the selected currency.') - return - - - billing_values = list(billing_details.values()) - for i, dict_value in enumerate(billing_values): - if i < 3: - if i == 2: - text = str(dict_value) - self.full_address = text - metrics = self.text_fields[2].fontMetrics() - width = self.text_fields[2].width() - elided_text = metrics.elidedText(text, QtCore.Qt.TextElideMode.ElideMiddle, width) - self.text_fields[2].setText(elided_text) - elif i == 1: - text = str(dict_value) - self.text_fields[i].setProperty("fullText", text) - self.text_fields[i].setText(text) - font_size = 14 - if len(text) > 9: - font_size = 15 - if len(text) > 13: - font_size = 10 - if len(text) > 16: - font_size = 8 - - self.text_fields[i].setStyleSheet(f"font-size: {font_size}px; font-weight: bold;") - else: - self.text_fields[i].setProperty("fullText", str(dict_value)) - self.text_fields[i].setText(str(dict_value)) - - def on_invoice_finished(self, result): - if result: - self.invoice_data.clear() - self.show_payment_confirmed(self.text_fields[0].text()) - return - self.update_status.update_status('An error occurred when generating invoice') - - - def show_payment_confirmed(self, billing_code): - payment_page = self.find_payment_confirmed_page() - - if payment_page: - for line in self.text_fields: - line.setText('') - payment_page.set_billing_code(billing_code) - - self.page_stack.setCurrentWidget(payment_page) - else: - print("PaymentConfirmed page not found") - - def find_payment_confirmed_page(self): - for i in range(self.page_stack.count()): - page = self.page_stack.widget(i) - if isinstance(page, PaymentConfirmed): - return page - return None - - def create_interface_elements(self): - self.title = QLabel("Payment Money", self) - self.title.setGeometry(QtCore.QRect(530, 30, 210, 40)) - self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.button_reverse.clicked.connect(self.reverse) - - - labels_info = [ - ("Your new billing ID", None, (20, 80),(240,50)), - ("", "cuadro400x50", (20, 130),(400,50)), - ("Time length", None, (20, 195),(150,50)), - ("", "cuadro150x50", (200, 195),(150,50)), - ("Pay",None, (20, 260),(120,50)), - ("", "cuadro150x50", (200, 260),(150,50)), - ("To address",None, (20, 325),(120,50)), - ("", "cuadro400x50", (20, 375),(400,50)), - ("", "cuadro400x50", (20, 450),(400,50)) - ] - - self.clickable_labels = [] - - for j, (text, image_name, position, tamaño) in enumerate(labels_info): - label = QLabel(self) - label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - label.setGeometry(position[0], position[1], tamaño[0], tamaño[1]) - - if image_name: - pixmap = QPixmap(os.path.join(self.btn_path, f"{image_name}.png")) - label.setPixmap(pixmap) - label.setScaledContents(True) - else: - label.setText(text) - - - - self.combo_box = QComboBox(self) - self.combo_box.setGeometry(205, 205, 160, 30) - self.combo_box.addItems(["1 month ($1)", "3 months ($3)", "6 months ($5)", "12 months ($10)"]) - self.combo_box.setEditable(True) - self.combo_box.setMaxVisibleItems(4) - self.combo_box.setDuplicatesEnabled(True) - self.combo_box.setPlaceholderText("Select options") - - line_edit_info = [ - (20, 130, 400, 50), - (200, 260, 150, 50), - (20, 375, 400, 50), - (20, 450, 400, 50) - ] - - for j, (x, y, width, height) in enumerate(line_edit_info): - line_edit = QLineEdit(self) - line_edit.setGeometry(x, y, width, height) - line_edit.setReadOnly(True) - self.text_fields.append(line_edit) - line_edit.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - #line_edit.setStyleSheet("padding: 3px;") - - self.button = QPushButton(self) - self.button.setGeometry(430, 130, 50, 50) - icono = QIcon(os.path.join(self.btn_path, f"paste_button.png")) - self.button.setIcon(icono) - self.button.setIconSize(self.button.size()) - self.button.clicked.connect(lambda : self.copy_text(0)) - - self.button = QPushButton(self) - self.button.setGeometry(430, 375, 50, 50) - icono = QIcon(os.path.join(self.btn_path, f"paste_button.png")) - self.button.setIcon(icono) - self.button.setIconSize(self.button.size()) - self.button.clicked.connect(lambda: self.copy_text(2)) - - self.button = QPushButton(self) - self.button.setGeometry(360, 260, 50, 50) - icono = QIcon(os.path.join(self.btn_path, f"paste_button.png")) - self.button.setIcon(icono) - self.button.setIconSize(self.button.size()) - self.button.clicked.connect(lambda: self.copy_text(1)) - - button_info = [ - ("monero", self.show_monero, (545, 75)), - ("bitcoin", self.show_bitcoin, (545, 290)), - ("lightnering", self.show_lightning, (545, 180)), - ("litecoin", self.show_litecoin, (545, 395)) - ] - - self.buttonGroup = QButtonGroup(self) - self.buttons = [] - - for j, (icon_name, function, position) in enumerate(button_info): - boton = QPushButton(self) - boton.setGeometry(position[0], position[1], 185, 75) - boton.setIconSize(QSize(190, 120)) - boton.setCheckable(True) - self.buttons.append(boton) - self.buttonGroup.addButton(boton, j) - boton.setIcon(QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) - #boton.clicked.connect(lambda _, func=function: func()) - boton.clicked.connect(self.check_invoice) - - # Establecer la exclusividad de los botones - - - - def copy_text(self, field: int): - try: - original_status = self.update_status.status_label.text() - original_status = original_status.replace("Status: ", "") - - if int(field) == 2: - text = self.full_address - else: - text = self.text_fields[int(field)].text() - QApplication.clipboard().setText(text) - - if field == 0: - self.update_status.update_status('Billing code copied to clipboard!') - elif field == 2: - self.update_status.update_status('Address copied to clipboard!') - else: - self.update_status.update_status('Pay amount copied to clipboard!') - - QTimer.singleShot(2000, lambda: self.update_status.update_status(original_status)) - except AttributeError: - self.update_status.update_status('No content available for copying') - except Exception as e: - self.update_status.update_status(f'An error occurred when copying the text') - - def show_next(self): - pass - - def get_selected_time_length(self): - return self.combo_box.currentText() - - def get_selected_currency(self): - selected_button = self.buttonGroup.checkedButton() - if selected_button: - index = self.buttonGroup.id(selected_button) - currencies = ["monero", "bitcoin", "lightning", "litecoin"] - return currencies[index] - return None - - def show_monero(self): - print("Monero selected") - - - def show_bitcoin(self): - print("Bitcoin selected") - - def show_lightning(self): - print("Lightning Network selected") - - def show_litecoin(self): - print("Litecoin selected") - - def reverse(self): - for line in self.text_fields: - line.setText('') - self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(IdPage))) class ConfettiParticle: def __init__(self, x, y): @@ -5606,7 +5336,7 @@ class SyncScreen(Page): self.worker_thread.sync_output.connect(self.update_output) self.worker_thread.start() - def update_output(self, available_locations, status, is_tor, locations, is_os_error): + def update_output(self, available_locations, available_browsers, status, is_tor, locations, is_os_error): if is_os_error: install_page = self.page_stack.findChild(InstallSystemPackage) install_page.configure(package_name='tor', distro='debian', is_sync=True) @@ -5622,7 +5352,7 @@ class SyncScreen(Page): return self.update_status.update_status('Sync complete') - self.connection_manager.set_synced(True) + update_available = ClientController.can_be_updated() @@ -5631,31 +5361,36 @@ class SyncScreen(Page): menu_page.on_update_check_finished() - available_locations_list = [] - self.connection_manager.store_locations(locations) - - grid_positions = [ - (395, 90, 185, 75), - (585, 90, 185, 75), - (395, 195, 185, 75), - (585, 195, 185, 75), - (395, 300, 185, 75), - (585, 300, 185, 75), - (395, 405, 185, 75), - (585, 405, 185, 75) - ] - - list1 = [] - for i, location in enumerate(available_locations): - position_index = i % len(grid_positions) - list1.append((QPushButton, location, grid_positions[position_index])) + self.update_after_sync(available_locations, available_browsers, locations) - for item in list1: - available_locations_list.append(item) + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(ProtocolPage))) + + def update_after_sync(self, available_locations, available_browsers, locations): + + self.connection_manager.set_synced(True) + + available_locations_list = [] + available_browsers_list = [] + + self.connection_manager.store_locations(locations) + self.connection_manager.store_browsers(available_browsers) + + browser_positions = self.generate_grid_positions(len(available_browsers)) + location_positions = self.generate_grid_positions(len(available_locations)) + + for i, location in enumerate(available_locations): + available_locations_list.append((QPushButton, location, location_positions[i])) + + for i, browser in enumerate(available_browsers): + available_browsers_list.append((QPushButton, browser, browser_positions[i])) location_page = self.find_location_page() hidetor_page = self.find_hidetor_page() protocol_page = self.find_protocol_page() + browser_page = self.find_browser_page() + + if browser_page: + browser_page.create_interface_elements(available_browsers_list) if location_page: location_page.create_interface_elements(available_locations_list) @@ -5663,9 +5398,34 @@ class SyncScreen(Page): hidetor_page.create_interface_elements(available_locations_list) if protocol_page: protocol_page.enable_protocol_buttons() - - self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(ProtocolPage))) + def generate_grid_positions(self, num_items): + positions = [] + start_x = 395 + start_y = 90 + button_width = 185 + button_height = 75 + h_spacing = 20 + v_spacing = 105 + + for i in range(num_items): + col = i % 2 + row = i // 2 + + x = start_x + (col * (button_width + h_spacing)) + y = start_y + (row * v_spacing) + + positions.append((x, y, button_width, button_height)) + + return positions + + def find_browser_page(self): + for i in range(self.page_stack.count()): + page = self.page_stack.widget(i) + if isinstance(page, BrowserPage): + return page + return None + def find_location_page(self): for i in range(self.page_stack.count()): page = self.page_stack.widget(i) @@ -5687,6 +5447,10 @@ class SyncScreen(Page): return page return None + + + + class WelcomePage(Page): def __init__(self, page_stack, main_window=None, parent=None): super().__init__("Welcome", page_stack, main_window, parent) @@ -5802,6 +5566,477 @@ class WelcomePage(Page): self.page_stack.setCurrentIndex(self.page_stack.indexOf(install_page)) +class DurationSelectionPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Select Duration", page_stack, main_window, parent) + self.update_status = main_window + self.create_interface_elements() + self.button_reverse.setVisible(True) + + def create_interface_elements(self): + self.title = QLabel("Select Duration", self) + self.title.setGeometry(QtCore.QRect(530, 30, 210, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.button_reverse.clicked.connect(self.reverse) + + self.combo_box = QComboBox(self) + self.combo_box.setGeometry(300, 200, 200, 40) + self.combo_box.addItems(["1 month ($1)", "3 months ($3)", "6 months ($5)", "12 months ($10)"]) + self.combo_box.setEditable(True) + self.combo_box.setMaxVisibleItems(4) + self.combo_box.setDuplicatesEnabled(True) + self.combo_box.setPlaceholderText("Select options") + + self.next_button = QPushButton("Next", self) + self.next_button.setGeometry(350, 300, 100, 40) + self.next_button.clicked.connect(self.show_next) + + def show_next(self): + currency_page = self.page_stack.findChild(CurrencySelectionPage) + if currency_page: + currency_page.selected_duration = self.combo_box.currentText() + self.page_stack.setCurrentWidget(currency_page) + + def reverse(self): + self.page_stack.setCurrentIndex(self.page_stack.indexOf(self.page_stack.findChild(IdPage))) + +class CurrencySelectionPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Select Currency", page_stack, main_window, parent) + self.update_status = main_window + self.selected_duration = None + self.create_interface_elements() + self.button_reverse.setVisible(True) + self.button_next.setVisible(False) + self.button_next.clicked.connect(self.go_next) + + def create_interface_elements(self): + self.title = QLabel("Payment Method", self) + self.title.setGeometry(QtCore.QRect(510, 30, 250, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.button_reverse.clicked.connect(self.reverse) + + button_info = [ + ("monero", (545, 75)), + ("bitcoin", (545, 290)), + ("lightnering", (545, 180)), + ("litecoin", (545, 395)) + ] + + self.buttonGroup = QButtonGroup(self) + self.buttons = [] + + for j, (icon_name, position) in enumerate(button_info): + button = QPushButton(self) + button.setGeometry(position[0], position[1], 185, 75) + button.setIconSize(QSize(190, 120)) + button.setCheckable(True) + self.buttons.append(button) + self.buttonGroup.addButton(button, j) + button.setIcon(QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + button.clicked.connect(self.on_currency_selected) + + def on_currency_selected(self): + self.button_next.setVisible(True) + + def go_next(self): + payment_details_page = self.page_stack.findChild(PaymentDetailsPage) + if payment_details_page: + payment_details_page.selected_duration = self.selected_duration + payment_details_page.selected_currency = self.get_selected_currency() + payment_details_page.check_invoice() + self.page_stack.setCurrentWidget(payment_details_page) + self.update_status.update_status('Loading payment details...') + + def get_selected_currency(self): + selected_button = self.buttonGroup.checkedButton() + if selected_button: + index = self.buttonGroup.id(selected_button) + currencies = ["monero", "bitcoin", "lightning", "litecoin"] + return currencies[index] + return None + + def reverse(self): + duration_page = self.page_stack.findChild(DurationSelectionPage) + if duration_page: + self.page_stack.setCurrentWidget(duration_page) + +class PaymentDetailsPage(Page): + def __init__(self, page_stack, main_window=None, parent=None): + super().__init__("Payment Details", page_stack, main_window, parent) + self.update_status = main_window + self.text_fields = [] + self.invoice_data = {} + self.selected_duration = None + self.selected_currency = None + + self.create_interface_elements() + self.button_reverse.setVisible(True) + + def create_interface_elements(self): + self.title = QLabel("Payment Details", self) + self.title.setGeometry(QtCore.QRect(530, 30, 210, 40)) + self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.button_reverse.clicked.connect(self.reverse) + + self.qr_code_button = QPushButton(self) + self.qr_code_button.setGeometry(490, 375, 50, 50) + qr_icon = QIcon(os.path.join(self.btn_path, "qr-code.png")) + self.qr_code_button.setIcon(qr_icon) + self.qr_code_button.setIconSize(self.qr_code_button.size()) + self.qr_code_button.clicked.connect(self.show_qr_code) + self.qr_code_button.setToolTip("Show QR Code") + self.qr_code_button.setDisabled(True) + + labels_info = [ + ("Your new billing ID", None, (20, 80), (240, 50)), + ("", "cuadro400x50", (20, 130), (400, 50)), + ("Duration", None, (20, 195), (150, 50)), + ("", "cuadro150x50", (200, 195), (150, 50)), + ("Pay", None, (20, 260), (120, 50)), + ("", "cuadro150x50", (200, 260), (150, 50)), + ("To address", None, (20, 325), (150, 50)), + ("", "cuadro400x50", (20, 375), (400, 50)), + ("", "cuadro400x50", (20, 450), (400, 50)) + ] + + for text, image_name, position, size in labels_info: + label = QLabel(self) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + label.setGeometry(position[0], position[1], size[0], size[1]) + + if image_name: + pixmap = QPixmap(os.path.join(self.btn_path, f"{image_name}.png")) + label.setPixmap(pixmap) + label.setScaledContents(True) + else: + label.setText(text) + + line_edit_info = [ + (20, 130, 400, 50), + (200, 195, 150, 50), + (200, 260, 150, 50), + (20, 375, 400, 50), + (20, 450, 400, 50) + ] + + for x, y, width, height in line_edit_info: + line_edit = QLineEdit(self) + line_edit.setGeometry(x, y, width, height) + line_edit.setReadOnly(True) + self.text_fields.append(line_edit) + line_edit.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + copy_button_info = [ + (430, 130, 0), + (360, 260, 2), + (430, 375, 3) + ] + + for x, y, field_index in copy_button_info: + button = QPushButton(self) + button.setGeometry(x, y, 50, 50) + icon = QIcon(os.path.join(self.btn_path, "paste_button.png")) + button.setIcon(icon) + button.setIconSize(button.size()) + button.clicked.connect(lambda checked, index=field_index: self.copy_text(index)) + + currency_button_info = [ + ("monero", (545, 75)), + ("bitcoin", (545, 290)), + ("lightnering", (545, 180)), + ("litecoin", (545, 395)) + ] + + self.currency_display_buttons = [] + for icon_name, position in currency_button_info: + button = QPushButton(self) + button.setGeometry(position[0], position[1], 185, 75) + button.setIconSize(QSize(190, 120)) + button.setCheckable(True) + button.setEnabled(False) + self.currency_display_buttons.append(button) + button.setIcon(QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))) + + + + def fetch_invoice_duration(self): + duration_month_num = int(self.selected_duration.split()[0]) + if duration_month_num == 12: + total_hours = 365 * 24 + else: + total_hours = duration_month_num * (30 * 24) + return total_hours + + def display_selected_options(self): + if self.selected_duration: + self.text_fields[1].setText(self.selected_duration) + self.text_fields[1].setStyleSheet('font-size: 13px;') + + if self.selected_currency: + currency_index_map = { + "monero": 0, + "bitcoin": 1, + "lightning": 2, + "litecoin": 3 + } + currency_index = currency_index_map.get(self.selected_currency, -1) + if currency_index >= 0: + for i, button in enumerate(self.currency_display_buttons): + button.setChecked(i == currency_index) + button.setEnabled(i == currency_index) + + + + def on_request_invoice(self): + total_hours = self.fetch_invoice_duration() + if self.selected_currency is None: + self.update_status.update_status('No currency selected') + return + + profile_data = { + 'id': int(self.update_status.current_profile_id), + 'duration': total_hours, + 'currency': 'xmr' if self.selected_currency == 'monero' else 'btc' if self.selected_currency == 'bitcoin' else 'btc-ln' if self.selected_currency == 'lightning' else 'ltc' if self.selected_currency == 'litecoin' else None + } + + self.update_status.update_status('Generating Invoice...') + self.worker_thread = WorkerThread('GET_SUBSCRIPTION', profile_data=profile_data) + self.worker_thread.text_output.connect(self.invoice_update_text_output) + self.worker_thread.invoice_output.connect(self.on_invoice_generation_finished) + self.worker_thread.invoice_finished.connect(self.on_invoice_finished) + self.worker_thread.start() + + def invoice_update_text_output(self, text): + self.update_status.update_status(text) + self.text_fields[4].setText(text) + self.text_fields[4].setStyleSheet('font-size: 15px;') + + if 'No compatible' in text: + for line in self.text_fields: + line.setText('') + + def store_invoice_data(self, billing_details: dict, duration: int): + self.invoice_data = { + 'billing_details': billing_details, + 'duration': duration + } + + def check_invoice(self): + self.display_selected_options() + total_hours = self.fetch_invoice_duration() + if self.invoice_data and self.invoice_data['duration'] == total_hours: + self.update_status.update_status('Checking invoice status...') + self.check_invoice_status(self.invoice_data['billing_details'].billing_code) + else: + self.on_request_invoice() + + + def check_invoice_status(self, billing_code: str): + self.worker_thread = WorkerThread('CHECK_INVOICE_STATUS', profile_data={'billing_code': billing_code}) + self.worker_thread.invoice_finished.connect(self.on_invoice_status_finished) + self.worker_thread.start() + + def on_invoice_status_finished(self, result): + if result: + self.parse_invoice_data(self.invoice_data['billing_details']) + else: + self.update_status.update_status('Invoice has expired. Generating new invoice...') + self.on_request_invoice() + + def on_invoice_generation_finished(self, billing_details: object, text: str): + total_hours = self.fetch_invoice_duration() + self.store_invoice_data(billing_details, duration=total_hours) + self.parse_invoice_data(billing_details) + + def parse_invoice_data(self, invoice_data: object): + billing_details = { + 'billing_code': invoice_data.billing_code + } + + if self.selected_currency.lower() == 'lightning': + currency = 'Bitcoin Lightning' + else: + currency = self.selected_currency + + preferred_method = next((pm for pm in invoice_data.payment_methods if pm.name.lower() == currency.lower()), None) + + if preferred_method: + billing_details['due_amount'] = preferred_method.due + billing_details['address'] = preferred_method.address + else: + self.update_status.update_status('No payment method found for the selected currency.') + return + + billing_values = list(billing_details.values()) + text_field_indices = [0, 2, 3] + + for i, dict_value in enumerate(billing_values): + if i < 3: + field_index = text_field_indices[i] + if i == 2: + text = str(dict_value) + self.full_address = text + metrics = self.text_fields[field_index].fontMetrics() + width = self.text_fields[field_index].width() + elided_text = metrics.elidedText(text, QtCore.Qt.TextElideMode.ElideMiddle, width) + self.text_fields[field_index].setText(elided_text) + elif i == 1: + text = str(dict_value) + self.text_fields[field_index].setProperty("fullText", text) + self.text_fields[field_index].setText(text) + font_size = 14 + if len(text) > 9: + font_size = 15 + if len(text) > 13: + font_size = 10 + if len(text) > 16: + font_size = 10 + self.text_fields[field_index].setStyleSheet(f"font-size: {font_size}px; font-weight: bold;") + else: + self.text_fields[field_index].setProperty("fullText", str(dict_value)) + self.text_fields[field_index].setText(str(dict_value)) + + self.qr_code_button.setDisabled(False) + self.update_status.update_status('Invoice generated. Awaiting payment...') + + + def on_invoice_finished(self, result): + if result: + self.invoice_data.clear() + self.show_payment_confirmed(self.text_fields[0].text()) + return + self.update_status.update_status('An error occurred when generating invoice') + + def show_payment_confirmed(self, billing_code): + payment_page = self.find_payment_confirmed_page() + + if payment_page: + for line in self.text_fields: + line.setText('') + payment_page.set_billing_code(billing_code) + self.page_stack.setCurrentWidget(payment_page) + else: + print("PaymentConfirmed page not found") + + def find_payment_confirmed_page(self): + for i in range(self.page_stack.count()): + page = self.page_stack.widget(i) + if isinstance(page, PaymentConfirmed): + return page + return None + + def copy_text(self, field: int): + try: + original_status = self.update_status.status_label.text() + original_status = original_status.replace("Status: ", "") + + if int(field) == 3: + text = self.full_address + else: + text = self.text_fields[int(field)].text() + QApplication.clipboard().setText(text) + + if field == 0: + self.update_status.update_status('Billing code copied to clipboard!') + elif field == 3: + self.update_status.update_status('Address copied to clipboard!') + else: + self.update_status.update_status('Pay amount copied to clipboard!') + + QTimer.singleShot(2000, lambda: self.update_status.update_status(original_status)) + except AttributeError: + self.update_status.update_status('No content available for copying') + except Exception as e: + self.update_status.update_status(f'An error occurred when copying the text') + + def reverse(self): + currency_page = self.page_stack.findChild(CurrencySelectionPage) + if currency_page: + self.page_stack.setCurrentWidget(currency_page) + + def show_qr_code(self): + if hasattr(self, 'full_address') and self.full_address: + qr_dialog = QRCodeDialog(self.full_address, self) + qr_dialog.exec() + else: + self.update_status.update_status('No address available for QR code') + + def fetch_invoice_duration(self): + duration_month_num = int(self.selected_duration.split()[0]) + if duration_month_num == 12: + total_hours = 365 * 24 + else: + total_hours = duration_month_num * (30 * 24) + return total_hours + +class QRCodeDialog(QDialog): + def __init__(self, address_text, parent=None): + super().__init__(parent) + self.setWindowTitle("Payment Address QR Code") + self.setFixedSize(400, 450) + self.setModal(True) + + layout = QVBoxLayout(self) + + title_label = QLabel("Scan QR Code to Copy Address", self) + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #00ffff; margin: 10px;") + layout.addWidget(title_label) + + qr_label = QLabel(self) + qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + qr_label.setStyleSheet("margin: 10px; border: 2px solid #00ffff; border-radius: 10px;") + + qr_pixmap = self.generate_qr_code(address_text) + qr_label.setPixmap(qr_pixmap) + layout.addWidget(qr_label) + + address_label = QLabel("Address:", self) + address_label.setStyleSheet("font-size: 12px; color: #ffffff; margin-top: 10px;") + layout.addWidget(address_label) + + address_text_label = QLabel(address_text, self) + address_text_label.setWordWrap(True) + address_text_label.setStyleSheet("font-size: 10px; color: #cccccc; margin: 5px; padding: 5px; border: 1px solid #666; border-radius: 5px;") + layout.addWidget(address_text_label) + + close_button = QPushButton("Close", self) + close_button.setStyleSheet("font-size: 14px; padding: 8px; margin: 10px;") + close_button.clicked.connect(self.close) + layout.addWidget(close_button) + + self.setStyleSheet(""" + QDialog { + background-color: #2c3e50; + border: 2px solid #00ffff; + border-radius: 10px; + } + """) + + def generate_qr_code(self, text): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=8, + border=4, + ) + qr.add_data(text) + qr.make(fit=True) + + qr_image = qr.make_image(fill_color="black", back_color="white") + + buffer = BytesIO() + qr_image.save(buffer, format='PNG') + buffer.seek(0) + + pixmap = QPixmap() + pixmap.loadFromData(buffer.getvalue()) + + return pixmap.scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + + + if __name__ == "__main__": app = QApplication(sys.argv) window = CustomWindow() diff --git a/gui/resources/images/button_my.png b/gui/resources/images/button_my.png deleted file mode 100644 index f02199f..0000000 Binary files a/gui/resources/images/button_my.png and /dev/null differ diff --git a/gui/resources/images/default_browser_button.png b/gui/resources/images/default_browser_button.png new file mode 100644 index 0000000..6d6a1c1 Binary files /dev/null and b/gui/resources/images/default_browser_button.png differ diff --git a/gui/resources/images/default_browser_mini.png b/gui/resources/images/default_browser_mini.png new file mode 100644 index 0000000..8f2c49f Binary files /dev/null and b/gui/resources/images/default_browser_mini.png differ diff --git a/gui/resources/images/default_location_button.png b/gui/resources/images/default_location_button.png index b6d7149..d7c1c65 100644 Binary files a/gui/resources/images/default_location_button.png and b/gui/resources/images/default_location_button.png differ diff --git a/gui/resources/images/default_location_mini.png b/gui/resources/images/default_location_mini.png index eeedec0..c13249e 100644 Binary files a/gui/resources/images/default_location_mini.png and b/gui/resources/images/default_location_mini.png differ diff --git a/gui/resources/images/qr-code.png b/gui/resources/images/qr-code.png new file mode 100644 index 0000000..cebffa5 Binary files /dev/null and b/gui/resources/images/qr-code.png differ