hydraveil-gui/gui/v2/ui/pages/editor_page.py

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()