forked from Support/sp-hydra-veil-gui
465 lines
19 KiB
Python
Executable file
465 lines
19 KiB
Python
Executable file
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()
|