import os from functools import partial from PyQt6.QtWidgets import ( QLabel, QPushButton, QLineEdit, QTextEdit, QFrame, QDialog, QVBoxLayout, QHBoxLayout, QMessageBox, QGraphicsDropShadowEffect, ) from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QTransform from PyQt6.QtCore import Qt, QSize, QTimer, QPointF, QSignalBlocker, pyqtSlot from PyQt6 import QtCore, QtGui from gui.v2.actions.editor_data import ( EditorResult, prepare_editor_data, set_editor_resolution, set_editor_value, ) from gui.v2.actions.editor_tasks import ( apply_editor_changes, continue_editor_task, cycle_editor_value, leave_editor, load_editor, ) from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.browser_page import BrowserPage from gui.v2.ui.pages.location_page import LocationPage from gui.v2.ui.pages.screen_page import ScreenPage from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup class EditorPage(Page): def __init__(self, page_stack, main_window, prepared=None): super().__init__("Editor", page_stack, main_window) self.page_stack = page_stack self.update_status = main_window self.connection_manager = main_window.connection_manager self.labels = [] self.buttons = [] self.title.setGeometry(570, 40, 185, 40) self.title.setText("Edit Profile") self.button_apply.setVisible(True) self.button_apply.clicked.connect(self.go_selected) self.button_back.setVisible(True) try: self.button_back.clicked.disconnect() except TypeError: pass self.button_back.clicked.connect(self.go_back) self.name_handle = QLabel(self) self.name_handle.setGeometry(110, 70, 400, 30) self.name_handle.setStyleSheet('color: #cacbcb;') self.name_handle.setText("Profile Name:") self.name_hint = QLabel(self) self.name_hint.setGeometry(265, 100, 190, 20) self.name_hint.setStyleSheet( 'color: #888888; font-size: 10px; font-style: italic;') self.name_hint.setText("Click here to edit profile name") self.name_hint.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.name = QLineEdit(self) self.name.setPlaceholderText("Enter name") self.name.setGeometry(265, 70, 190, 30) self.name.setStyleSheet( "color: cyan; border: 1px solid #666666; border-radius: 3px; background-color: rgba(0, 0, 0, 0.3);") self.name.setCursor(QtCore.Qt.CursorShape.IBeamCursor) self.name.focusInEvent = lambda event: self.name_hint.hide() self.name.focusOutEvent = lambda event: self.name_hint.show( ) if not self.name.text() else self.name_hint.hide() self.name.textChanged.connect( lambda text: self.name_hint.hide() if text else self.name_hint.show()) self.res_hint_shown = False self.display.setGeometry(QtCore.QRect(0, 60, 540, 405)) self.display.hide() self.garaje = QLabel(self) self.garaje.setGeometry(QtCore.QRect(0, 70, 540, 460)) self.brow_disp = QLabel(self) self.brow_disp.setGeometry(QtCore.QRect(0, 50, 540, 460)) self.brow_disp.setPixmap( QPixmap(os.path.join(self.btn_path, "browser only.png"))) self.brow_disp.hide() self.brow_disp.lower() self._prepared_data = prepared self.editor_state = None self._states = {} self._request_number = 0 self._request_name = "" self._worker = None self._busy = False self._arrow_controls = [] self.name.textChanged.connect(self.update_name_value) @staticmethod def prepare_data(custom_window): return prepare_editor_data() def _new_request(self): self._request_number += 1 self._request_name = str(self._request_number) return self._request_name def showEvent(self, event): super().showEvent(event) self.res_hint_shown = False profile_id = getattr(self.update_status, "current_profile_id", None) request_name = self._new_request() self.editor_state = None self._clear_editor_widgets() self.display.hide() self.garaje.hide() self.brow_disp.hide() self._set_editor_busy(True) prepared = self._prepared_data self._prepared_data = None self._worker = load_editor( profile_id, self.connection_manager.get_location_list(), self.connection_manager.get_browser_list(), self.connection_manager.available_resolutions, self.connection_manager.is_synced(), prepared, self._states.get(profile_id), request_name, self.handle_editor_result, ) def hideEvent(self, event): self._new_request() super().hideEvent(event) def _clear_editor_widgets(self): for widget in [*self.labels, *self.buttons]: widget.deleteLater() self.labels.clear() self.buttons.clear() self._arrow_controls.clear() def _set_editor_busy(self, busy): self._busy = busy self.name.setEnabled(not busy and self.editor_state is not None) self.button_apply.setEnabled(not busy and self.editor_state is not None) self.button_back.setEnabled(not busy) for button, enabled in self._arrow_controls: button.setEnabled(not busy and enabled) @pyqtSlot(str, object) def handle_editor_result(self, request_name: str, result: EditorResult): if request_name != self._request_name: return if "state" in result: self.editor_state = result["state"] self._states[self.editor_state["profile_id"]] = self.editor_state if "sync_data" in result: self.update_status.update_values(*result["sync_data"]) if "view" in result and result["status"] != "draft": self._render_editor(result["view"]) self._set_editor_busy(result["status"] == "confirmation") if result.get("refresh_menu"): menu = self.custom_window.navigator.get_cached("menu") if menu is not None: menu.refresh_menu_buttons() if result.get("message"): self.update_status.update_status(result["message"]) handlers = {"confirmation": self._show_confirmation, "navigate": self._navigate_result} handler = handlers.get(result["status"]) if handler is not None: handler(result) def _navigate_result(self, result): self.custom_window.navigator.navigate(result["destination"]) def _show_confirmation(self, result): self.popup = ConfirmationPopup( self, message=result["message"], action_button_text="Continue", cancel_button_text="Cancel", ) self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) self.popup.finished.connect(partial(self._confirmation_response, result["request"], self._request_name)) self.popup.show() def _confirmation_response(self, request, request_name, accepted): if request_name != self._request_name: return self._set_editor_busy(True) self._worker = continue_editor_task(request, accepted, request_name, self.handle_editor_result) def update_name_value(self): if self.editor_state is None: return self.handle_editor_result( self._request_name, set_editor_value(self.editor_state, "name", self.name.text()), ) def go_back(self): if self.editor_state is None: self.custom_window.navigator.navigate("menu") return self._set_editor_busy(True) leave_editor(self.editor_state, self._new_request(), self.handle_editor_result) def go_selected(self): if self.editor_state is None: return self._set_editor_busy(True) self._worker = apply_editor_changes(self.editor_state, self._new_request(), self.handle_editor_result) def _change_field(self, key, direction): self._set_editor_busy(True) self._worker = cycle_editor_value( self.editor_state, key, direction, self._new_request(), self.handle_editor_result, ) def _render_editor(self, view): self._clear_editor_widgets() if self.name.text() != view["name"]: with QSignalBlocker(self.name): self.name.setText(view["name"]) self.name_hint.setVisible(not bool(view["name"])) self._render_preview(view["preview"]) if view["preview"]["show_operator"]: self._render_operator_details(view["preview"]) for index, field in enumerate(view["fields"]): self._render_field(index, field) def _render_preview(self, preview): layouts = {"tor": (0, 100, 400, 394), "standard": (0, 60, 540, 405), "vless": (0, 90, 540, 405)} self.display.setGeometry(*layouts[preview["layout"]]) pixmap = QPixmap(os.path.join(self.btn_path, preview["asset"])) if preview["encrypted"]: pixmap = pixmap.scaled( self.display.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) self.display.setPixmap(pixmap) self.display.setScaledContents(not preview["encrypted"]) self.display.setVisible(preview["visible"]) self.brow_disp.setVisible(bool(preview["background"])) if preview["background"]: self.brow_disp.setPixmap(QPixmap(os.path.join(self.btn_path, preview["background"]))) self.garaje.setVisible(bool(preview["garage"])) if preview["garage"]: self.garaje.setPixmap(QPixmap(os.path.join(self.btn_path, preview["garage"]))) def _render_operator_details(self, preview): image = QLabel(self) image.setGeometry(20, 125, 150, 150) image.setPixmap(QPixmap(os.path.join(self.btn_path, preview["location_asset"]))) image.setScaledContents(True) image.show() self.labels.append(image) blocks = [ ((180, 120, 300, 250), f"Location: {preview['location_text']}\n\nOperator: {preview['operator']}", 19, QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere), ((20, 300, 450, 170), f"Nostr: {preview['nostr']}", 21, QtGui.QTextOption.WrapMode.WrapAnywhere), ] for geometry, text, size, wrap in blocks: widget = QTextEdit(self) widget.setGeometry(*geometry) widget.setReadOnly(True) widget.setFrameStyle(QFrame.Shape.NoFrame) widget.setStyleSheet(f"background: transparent; border: none; color: {preview['text_color']}; font-size: {size}px;") widget.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) widget.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) widget.setWordWrapMode(wrap) widget.setText(text) widget.show() self.labels.append(widget) def _field_pixmap(self, field): renderers = {"image": self._image_field, "browser": self._browser_field, "location": self._location_field, "resolution": self._resolution_field} return renderers[field["kind"]](field) def _image_field(self, field): return QPixmap(os.path.join(self.btn_path, field["asset"])) def _browser_field(self, field): if field["blank_browser"]: return QPixmap() fallback = os.path.join(self.btn_path, "default_browser_button.png") if field["unknown_browser"]: pixmap = QPixmap(fallback) return pixmap if not pixmap.isNull() else BrowserPage.create_browser_button_image("Browser", fallback, True) pixmap = BrowserPage.create_browser_button_image(field["browser_text"], self.btn_path) return pixmap if not pixmap.isNull() else BrowserPage.create_browser_button_image(field["browser_text"], fallback, True) def _location_field(self, field): asset = os.path.join(self.btn_path, field["location_asset"]) existing = asset if not QPixmap(asset).isNull() else None return LocationPage.create_location_button_image( field["value"] if existing else field["location_name"], os.path.join(self.btn_path, "default_location_button.png"), field["provider"], existing, ) def _resolution_field(self, field): if field["value"] == "None": return QPixmap() return ScreenPage.create_resolution_button_image(self, field["value"]) def _render_field(self, index, field): pixmap = self._field_pixmap(field) geometry = (565, 90 + index * 80, 185, 75) if field["kind"] == "resolution": label = QPushButton(self) label.setGeometry(*geometry) label.setFlat(True) label.setStyleSheet("border: none; background: transparent;") label.setIcon(QIcon(pixmap)) label.setIconSize(QSize(185, 75)) label.setCursor(Qt.CursorShape.PointingHandCursor) label.clicked.connect(self.show_custom_res_dialog) self._arrow_controls.append((label, field["visible"])) if field["visible"]: self._add_resolution_hint(label, QIcon(pixmap)) else: label = QLabel(self) label.setGeometry(*geometry) label.setPixmap(pixmap) label.setScaledContents(True) label.show() self.labels.append(label) self._create_arrow(field, -1, (535, 90 + index * 80, 30, 75)) self._create_arrow(field, 1, (750, 90 + index * 80, 30, 75)) def _create_arrow(self, field, direction, geometry): button = QPushButton(self) button.setGeometry(*geometry) button.clicked.connect(lambda checked=False: self._change_field(field["key"], direction)) button.setIcon(QIcon(self._arrow_pixmap(direction, field["needs_sync"]))) button.setIconSize(button.size()) button.setVisible(field["visible"]) self._arrow_controls.append((button, field["enabled"])) self.buttons.append(button) if not field["needs_sync"]: return button.setStyleSheet(""" QPushButton { background-color: transparent; border: none; } QPushButton:hover { background-color: rgba(0, 255, 0, 0.1); border-radius: 3px; } QPushButton:pressed { background-color: rgba(0, 255, 0, 0.2); border-radius: 3px; } """) def _arrow_pixmap(self, direction, outlined): if not outlined: pixmap = QPixmap(os.path.join(self.btn_path, "UP_button.png")) return pixmap.transformed(QTransform().rotate(180)) if direction == -1 else pixmap pixmap = QPixmap(30, 75) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) pen = QtGui.QPen(QColor(0, 255, 0)) pen.setWidth(3) painter.setPen(pen) painter.setBrush(Qt.BrushStyle.NoBrush) xs = {-1: (0.65, 0.25, 0.65), 1: (0.35, 0.75, 0.35)}[direction] points = [QPointF(30 * x, 75 * y) for x, y in zip(xs, (0.15, 0.50, 0.85))] painter.drawPolygon(QtGui.QPolygonF(points)) painter.end() return pixmap def _add_resolution_hint(self, button, original_icon): if self.res_hint_shown: return self.res_hint_shown = True template = QPixmap(os.path.join(self.btn_path, "resolution_template_button.png")) button.setIcon(QIcon(template.scaled(187, 75))) hint = QLabel("Click here to customize", button) hint.setGeometry(15, 0, 155, 75) hint.setWordWrap(True) hint.setAlignment(Qt.AlignmentFlag.AlignCenter) hint.setStyleSheet("color: #00ffff; font-weight: bold; font-size: 16px; background: transparent;") hint.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) shadow = QGraphicsDropShadowEffect(hint) shadow.setBlurRadius(20) shadow.setColor(QColor(0, 255, 255)) shadow.setOffset(0, 0) hint.setGraphicsEffect(shadow) hint.show() timer = QTimer(button) timer.setSingleShot(True) timer.timeout.connect(partial(self._restore_resolution_icon, button, hint, original_icon)) timer.start(3500) def _restore_resolution_icon(self, button, hint, icon): hint.hide() button.setIcon(icon) def show_custom_res_dialog(self): dialog = QDialog(self) dialog.setWindowTitle("Custom Resolution") dialog.setFixedSize(300, 150) dialog.setModal(True) dialog.setStyleSheet(""" QDialog { background-color: #2c3e50; border: 2px solid #00ffff; border-radius: 10px; } QLabel { color: white; font-size: 12px; } QLineEdit { background-color: #34495e; color: white; border: 1px solid #00ffff; border-radius: 5px; padding: 5px; font-size: 12px; } QPushButton { background-color: #2c3e50; color: white; border: 2px solid #00ffff; border-radius: 5px; padding: 8px; font-size: 12px; } QPushButton:hover { background-color: #34495e; } """) layout = QVBoxLayout() input_layout = QHBoxLayout() width_label = QLabel("Width:") width_input = QLineEdit() width_input.setPlaceholderText("1920") height_label = QLabel("Height:") height_input = QLineEdit() height_input.setPlaceholderText("1080") input_layout.addWidget(width_label) input_layout.addWidget(width_input) input_layout.addWidget(height_label) input_layout.addWidget(height_input) button_layout = QHBoxLayout() ok_button = QPushButton("OK") cancel_button = QPushButton("Cancel") button_layout.addWidget(cancel_button) button_layout.addWidget(ok_button) layout.addLayout(input_layout) layout.addLayout(button_layout) dialog.setLayout(layout) accept = partial(self._accept_custom_resolution, dialog, width_input, height_input) ok_button.clicked.connect(accept) cancel_button.clicked.connect(dialog.reject) width_input.returnPressed.connect(accept) height_input.returnPressed.connect(accept) dialog.exec() def _accept_custom_resolution(self, dialog, width_input, height_input, checked=False): if self.editor_state is None: dialog.reject() return result = set_editor_resolution( self.editor_state, width_input.text(), height_input.text(), self.custom_window.host_screen_width, self.custom_window.host_screen_height, ) if result["status"] == "error": QMessageBox.warning(dialog, "Invalid Resolution", result["message"]) return self.handle_editor_result(self._request_name, result) if result["adjusted"]: QMessageBox.information(dialog, "Notice", "Adjusted to fit host size") dialog.accept()