From 4ebb5db9031dc6ece44ecca9b0aa27f245306192 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Jun 2025 23:22:48 +0100 Subject: [PATCH] Updated Payment page / Default locations and browsers --- gui/__main__.py | 1039 ++++++++++------- gui/resources/images/button_my.png | Bin 7244 -> 0 bytes .../images/default_browser_button.png | Bin 0 -> 6467 bytes gui/resources/images/default_browser_mini.png | Bin 0 -> 1979 bytes .../images/default_location_button.png | Bin 4180 -> 2911 bytes .../images/default_location_mini.png | Bin 2043 -> 494 bytes gui/resources/images/qr-code.png | Bin 0 -> 267 bytes 7 files changed, 637 insertions(+), 402 deletions(-) delete mode 100644 gui/resources/images/button_my.png create mode 100644 gui/resources/images/default_browser_button.png create mode 100644 gui/resources/images/default_browser_mini.png create mode 100644 gui/resources/images/qr-code.png 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 f02199fbd5b4e75275cb75ff6229851c8ac64ec9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7244 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 literal 0 HcmV?d00001 diff --git a/gui/resources/images/default_browser_mini.png b/gui/resources/images/default_browser_mini.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2c49f5618afcab1d3e44abdfde9882c28773a3 GIT binary patch literal 1979 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 DD@UoR literal 4180 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