zapret-kvn/xray_fluent/ui/zapret_page.py
2026-03-26 23:39:55 +03:00

330 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Zapret (DPI bypass) management page — preset table + editor subpage."""
from __future__ import annotations
from pathlib import Path
from typing import cast
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QBrush, QColor, QCursor
from PyQt6.QtWidgets import (
QAbstractItemView, QFileDialog, QHBoxLayout, QHeaderView,
QStackedWidget, QTableWidgetItem, QVBoxLayout, QWidget,
)
from qfluentwidgets import (
BodyLabel,
CaptionLabel,
CardWidget,
FluentIcon as FIF,
IndeterminateProgressBar,
PrimaryToolButton,
SubtitleLabel,
TableWidget,
TransparentToolButton,
VerticalSeparator,
)
from qfluentwidgets import RoundMenu, Action
from ..zapret_manager import PresetInfo, ZapretManager
from .preset_edit_widget import PresetEditWidget
class ZapretPage(QWidget):
start_requested = pyqtSignal(str) # preset name
stop_requested = pyqtSignal()
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self.setObjectName("zapret")
self._presets: list[PresetInfo] = []
self._running = False
self._active_preset = ""
# ── Stack: page 0 = list, page 1 = editor ──
self._stack = QStackedWidget(self)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
outer.addWidget(self._stack)
# ══════════════ Page 0: Preset list ══════════════
list_page = QWidget()
root = QVBoxLayout(list_page)
root.setContentsMargins(24, 20, 24, 20)
root.setSpacing(12)
root.addWidget(SubtitleLabel("Обход блокировок (zapret)", list_page))
# Info card
info_card = CardWidget(list_page)
info_lay = QVBoxLayout(info_card)
info_lay.setContentsMargins(16, 12, 16, 12)
info_lay.setSpacing(4)
info_lay.addWidget(BodyLabel(
"Zapret (winws2) — инструмент обхода DPI-блокировок.\n"
"Выберите пресет и нажмите «Запустить». Работает независимо от VPN/прокси.",
info_card,
))
info_lay.addWidget(CaptionLabel(
"Требуются права администратора. Файлы zapret должны находиться в папке zapret/ рядом с приложением.",
info_card,
))
root.addWidget(info_card)
# Toolbar
toolbar = QHBoxLayout()
toolbar.setSpacing(4)
self.add_btn = PrimaryToolButton(FIF.ADD, list_page)
self.add_btn.setToolTip("Создать новый пресет")
toolbar.addWidget(self.add_btn)
self.import_btn = TransparentToolButton(FIF.FOLDER, list_page)
self.import_btn.setToolTip("Импорт из файла")
toolbar.addWidget(self.import_btn)
toolbar.addWidget(VerticalSeparator(list_page))
self.delete_btn = TransparentToolButton(FIF.DELETE, list_page)
self.delete_btn.setToolTip("Удалить пресет")
toolbar.addWidget(self.delete_btn)
self.refresh_btn = TransparentToolButton(FIF.SYNC, list_page)
self.refresh_btn.setToolTip("Обновить список")
toolbar.addWidget(self.refresh_btn)
toolbar.addStretch()
toolbar.addWidget(VerticalSeparator(list_page))
self.start_btn = TransparentToolButton(FIF.PLAY_SOLID, list_page)
self.start_btn.setToolTip("Запустить выбранный пресет")
toolbar.addWidget(self.start_btn)
self.stop_btn = TransparentToolButton(FIF.PAUSE_BOLD, list_page)
self.stop_btn.setToolTip("Остановить zapret")
self.stop_btn.setEnabled(False)
toolbar.addWidget(self.stop_btn)
root.addLayout(toolbar)
# Status bar
status_card = CardWidget(list_page)
status_lay = QHBoxLayout(status_card)
status_lay.setContentsMargins(16, 8, 16, 8)
self.status_label = BodyLabel("Остановлен", status_card)
status_lay.addWidget(self.status_label, 1)
self.progress = IndeterminateProgressBar(status_card)
self.progress.setFixedHeight(3)
self.progress.hide()
status_lay.addWidget(self.progress)
root.addWidget(status_card)
# Table
self.table = TableWidget(list_page)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["Имя", "Описание", "Аргументов", "Изменён"])
v_header = cast(QHeaderView, self.table.verticalHeader())
v_header.setVisible(False)
h_header = cast(QHeaderView, self.table.horizontalHeader())
h_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
h_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
h_header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
h_header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
root.addWidget(self.table, 1)
self._stack.addWidget(list_page)
# ══════════════ Page 1: Editor ══════════════
self._editor = PresetEditWidget(self)
self._stack.addWidget(self._editor)
# ── Signals ──
self.add_btn.clicked.connect(self._on_create)
self.import_btn.clicked.connect(self._on_import)
self.delete_btn.clicked.connect(self._on_delete)
self.refresh_btn.clicked.connect(self.refresh_presets)
self.start_btn.clicked.connect(self._on_start)
self.stop_btn.clicked.connect(self._on_stop)
self.table.doubleClicked.connect(self._on_double_click)
self.table.customContextMenuRequested.connect(self._on_context_menu)
self._editor.back_requested.connect(self._show_list)
self._editor.save_requested.connect(self._on_save_preset)
# ── Public API ──
def set_presets(self, infos: list[PresetInfo], selected: str = "") -> None:
self._presets = list(infos)
self._reload_table(selected)
def set_running(self, running: bool, preset_name: str = "") -> None:
self._running = running
if running:
self._active_preset = preset_name
self.status_label.setText(f"Работает: {preset_name}")
self.status_label.setStyleSheet("color: #4CAF50;")
self.progress.show()
self.progress.start()
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
else:
self._active_preset = ""
self.status_label.setText("Остановлен")
self.status_label.setStyleSheet("")
self.progress.stop()
self.progress.hide()
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self._reload_table()
def set_error(self, message: str) -> None:
self.set_running(False)
self.status_label.setText(f"Ошибка: {message}")
self.status_label.setStyleSheet("color: #F44336;")
def current_preset(self) -> str:
row = self.table.currentRow()
if 0 <= row < len(self._presets):
return self._presets[row].name
return ""
def refresh_presets(self) -> None:
selected = self.current_preset()
self._presets = ZapretManager.list_preset_infos()
self._reload_table(selected)
# ── Table ──
def _reload_table(self, select_name: str = "") -> None:
self.table.setUpdatesEnabled(False)
self.table.blockSignals(True)
self.table.setRowCount(len(self._presets))
select_row = -1
for row, p in enumerate(self._presets):
name_item = QTableWidgetItem(p.name)
desc_item = QTableWidgetItem(p.description)
args_item = QTableWidgetItem(str(p.arg_count))
mod_item = QTableWidgetItem(self._format_date(p.modified))
# Highlight active preset
if self._running and p.name == self._active_preset:
green = QBrush(QColor(76, 175, 80))
for item in (name_item, desc_item, args_item, mod_item):
item.setForeground(green)
self.table.setItem(row, 0, name_item)
self.table.setItem(row, 1, desc_item)
self.table.setItem(row, 2, args_item)
self.table.setItem(row, 3, mod_item)
if p.name == select_name:
select_row = row
self.table.blockSignals(False)
self.table.setUpdatesEnabled(True)
if select_row >= 0:
self.table.selectRow(select_row)
@staticmethod
def _format_date(iso: str) -> str:
if not iso:
return ""
try:
from datetime import datetime
dt = datetime.fromisoformat(iso)
return dt.strftime("%Y-%m-%d %H:%M")
except (ValueError, TypeError):
return iso
# ── Handlers ──
def _on_start(self) -> None:
name = self.current_preset()
if name:
self.start_requested.emit(name)
def _on_stop(self) -> None:
self.stop_requested.emit()
def _on_double_click(self, index) -> None:
row = index.row()
if 0 <= row < len(self._presets):
self._open_editor(self._presets[row])
def _on_create(self) -> None:
self._editor.set_preset("", "", "")
self._stack.setCurrentIndex(1)
def _on_import(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self, "Импорт пресета", "", "Текстовые файлы (*.txt);;Все файлы (*)"
)
if path:
info = ZapretManager.import_preset(Path(path))
if info:
self.refresh_presets()
self._reload_table(info.name)
def _on_delete(self) -> None:
name = self.current_preset()
if not name:
return
from qfluentwidgets import MessageBox
box = MessageBox("Удаление пресета", f"Удалить «{name}»?", self.window())
box.yesButton.setText("Удалить")
box.cancelButton.setText("Отмена")
if box.exec():
if self._running and name == self._active_preset:
self.stop_requested.emit()
ZapretManager.delete_preset(name)
self.refresh_presets()
def _on_save_preset(self, name: str, description: str, content: str) -> None:
ZapretManager.save_preset(name, content, description)
self.refresh_presets()
self._reload_table(name)
self._show_list()
def _open_editor(self, info: PresetInfo) -> None:
content = ZapretManager.read_preset(info.name)
self._editor.set_preset(info.name, info.description, content,
info.created, info.modified)
self._stack.setCurrentIndex(1)
def _show_list(self) -> None:
self._stack.setCurrentIndex(0)
def _on_context_menu(self, pos) -> None:
item = self.table.itemAt(pos)
if item is None:
return
row = item.row()
if row < 0 or row >= len(self._presets):
return
preset = self._presets[row]
self.table.selectRow(row)
menu = RoundMenu(parent=self)
edit_action = Action("Редактировать", self)
edit_action.triggered.connect(lambda: self._open_editor(preset))
menu.addAction(edit_action)
start_action = Action("Запустить", self)
start_action.triggered.connect(lambda: self.start_requested.emit(preset.name))
menu.addAction(start_action)
menu.addSeparator()
delete_action = Action("Удалить", self)
delete_action.triggered.connect(self._on_delete)
menu.addAction(delete_action)
menu.exec(QCursor.pos())