Some checks failed
Windows project source guards / test (push) Failing after 31s
Движок разложен на части с одной задачей каждая: - endpoint: вид сервера (TCP / QUIC / WireGuard), хосты, порты, DNS; - strategies: каталоги winws2 и правило для выбранного сервера; - presets: файлы пресетов; - command: сборка командной строки winws2 (чистая функция); - manager: процесс winws2 с одним входом apply(план) и ревизиями — готовность только по started процесса нужной ревизии, таймаут 5 с. Склейка с подключением (~450 строк в контроллере, два наслоенных механизма «UDP-защиты» и «профиля сервера», кэш и прогрев DNS) заменена шлюзом application/server_bypass.py. Сохранены: DNS только в воркере, остановка туннеля перед сменой правила (кроме восстановления Hysteria), забор перед стартом ядра, fail-closed при потере winws2 (теперь и по таймауту перезапуска), маскировка IP сервера в логах до старта ядра; адреса серверов пула изучаются в фоне после подключения. Поведение: - у QUIC и WireGuard теперь свои стратегии; ключ zapret_target совместим, старые сборки после отката читают udp_* как раньше; - в каталогах вернулись 82 составные стратегии с ветками --payload (валидатор принимал только --lua-desync и молча их выбрасывал); - смена пресета под защищённой сессией идёт через переход: туннель останавливается, winws2 перезапускается, VPN подключается снова; - пропавший файл выбранного пресета — откат к Default, а не отказ; - правка работающего пресета перезапускает его; переименование больше не оставляет копию, занятое имя отклоняется; - настройки Zapret не откатываются при сохранении страницы «Настройки». Интерфейс: карточка состояния (работает/нет, пресет, одна кнопка, автозапуск), выбранный VPN-сервер с живым правилом, три строки видов серверов с подстраницей стратегии, пресеты — отдельной подстраницей. Объяснение — в окне «Как это б#&^ь работает?» с иллюстрацией.
122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
from __future__ import annotations
|
||
|
||
from PyQt6.QtCore import pyqtSignal
|
||
from PyQt6.QtGui import QFont
|
||
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
|
||
from qfluentwidgets import (
|
||
BodyLabel,
|
||
CaptionLabel,
|
||
CardWidget,
|
||
FluentIcon as FIF,
|
||
LineEdit,
|
||
PlainTextEdit,
|
||
PrimaryPushButton,
|
||
)
|
||
|
||
from .detail_page import DetailPage
|
||
|
||
|
||
class PresetEditWidget(DetailPage):
|
||
#: original name ("" for a new preset), name, description, content
|
||
save_requested = pyqtSignal(str, str, str, str)
|
||
|
||
def __init__(self, parent: QWidget | None = None):
|
||
super().__init__(
|
||
"Zapret",
|
||
"Редактирование пресета",
|
||
parent,
|
||
root_key="zapret",
|
||
page_key="preset-edit",
|
||
)
|
||
self._original_content = ""
|
||
self._original_name = ""
|
||
self._original_description = ""
|
||
|
||
root = self.content_layout
|
||
|
||
self.error_label = CaptionLabel("", self)
|
||
self.add_header_action(self.error_label)
|
||
self.save_btn = PrimaryPushButton(FIF.SAVE, "Сохранить", self)
|
||
self.save_btn.clicked.connect(self._on_save)
|
||
self.add_header_action(self.save_btn)
|
||
|
||
# Metadata card
|
||
meta_card = CardWidget(self)
|
||
meta_layout = QVBoxLayout(meta_card)
|
||
meta_layout.setContentsMargins(16, 12, 16, 12)
|
||
meta_layout.setSpacing(8)
|
||
|
||
name_row = QHBoxLayout()
|
||
name_row.addWidget(BodyLabel("Название:", meta_card))
|
||
self.name_edit = LineEdit(meta_card)
|
||
self.name_edit.setPlaceholderText("Имя пресета")
|
||
name_row.addWidget(self.name_edit, 1)
|
||
meta_layout.addLayout(name_row)
|
||
|
||
desc_row = QHBoxLayout()
|
||
desc_row.addWidget(BodyLabel("Описание:", meta_card))
|
||
self.desc_edit = LineEdit(meta_card)
|
||
self.desc_edit.setPlaceholderText("Краткое описание (необязательно)")
|
||
desc_row.addWidget(self.desc_edit, 1)
|
||
meta_layout.addLayout(desc_row)
|
||
|
||
self.meta_label = CaptionLabel("", meta_card)
|
||
meta_layout.addWidget(self.meta_label)
|
||
root.addWidget(meta_card)
|
||
|
||
# Content editor
|
||
self.editor = PlainTextEdit(self)
|
||
self.editor.setPlaceholderText("Аргументы winws2, по одному на строку.\nСтроки с # — комментарии.")
|
||
font = QFont("Consolas", 10)
|
||
font.setStyleHint(QFont.StyleHint.Monospace)
|
||
self.editor.setFont(font)
|
||
root.addWidget(self.editor, 1)
|
||
|
||
def set_preset(self, name: str, description: str, content: str,
|
||
created: str = "", modified: str = "") -> None:
|
||
"""Load a preset into the editor."""
|
||
self._original_name = name
|
||
self._original_description = description
|
||
self._original_content = content
|
||
|
||
self.error_label.setText("")
|
||
self.name_edit.setText(name)
|
||
self.desc_edit.setText(description)
|
||
self.editor.setPlainText(content)
|
||
self.set_page_label(name or "Новый пресет")
|
||
|
||
# Meta info
|
||
parts = []
|
||
if created:
|
||
parts.append(f"Создан: {created}")
|
||
if modified:
|
||
parts.append(f"Изменён: {modified}")
|
||
self.meta_label.setText(" | ".join(parts) if parts else "")
|
||
|
||
def is_dirty(self) -> bool:
|
||
"""Check if content was modified."""
|
||
return (self.editor.toPlainText() != self._original_content
|
||
or self.name_edit.text() != self._original_name
|
||
or self.desc_edit.text() != self._original_description)
|
||
|
||
def _on_save(self) -> None:
|
||
name = self.name_edit.text().strip()
|
||
if not name:
|
||
self.error_label.setText("Введите название")
|
||
return
|
||
if any(char in '\\/:*?"<>|' for char in name) or name.startswith("_"):
|
||
self.error_label.setText("Название не может содержать \\ / : * ? \" < > | и начинаться с _")
|
||
return
|
||
self.error_label.setText("")
|
||
desc = self.desc_edit.text().strip()
|
||
content = self.editor.toPlainText()
|
||
self.save_requested.emit(self._original_name, name, desc, content)
|
||
|
||
def mark_saved(self, name: str, description: str, content: str) -> None:
|
||
"""The file is written: this is the new clean state."""
|
||
self._original_name = name
|
||
self._original_description = description
|
||
self._original_content = content
|
||
|
||
def show_error(self, message: str) -> None:
|
||
self.error_label.setText(message)
|