Добавлен новый интерфейс с навигационной панелью и страницами для управления моделями, настройками и генерацией изображений. Реализованы стили для новых компонентов и добавлены функции для загрузки шрифтов и иконок. Обновлен основной класс окна для поддержки новых страниц.
This commit is contained in:
1 parent
df447a1ecf
commit
ac32c55ab9
25 files changed
+1496
-476
No files matched your search
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "c:\\projects\\stable_api\\venv\\Scripts\\python.exe"
|
||||
}
|
||||
@@ -8,9 +8,10 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QComboBox, QSpinBox, QDoubleSpinBox, QProgressBar,
|
||||
QScrollArea, QTextEdit, QFileDialog, QMenuBar, QMenu)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QPixmap, QImage, QAction
|
||||
from PyQt6.QtGui import QPixmap, QImage, QAction, QFontDatabase, QFont
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
# Импортируем новые модули
|
||||
from modules.console_window import ConsoleWindow
|
||||
@@ -20,6 +21,13 @@ from modules.prompt_window import PromptWindow
|
||||
from modules.negative_prompt_window import NegativePromptWindow
|
||||
from modules.size_preset_manager import SizePresetManager
|
||||
from modules.settings_menu import SettingsMenu
|
||||
from modules.styles.base_window import BaseWindow
|
||||
from modules.styles.navigation import NavigationPanel
|
||||
from modules.styles.page import PageContainer
|
||||
from modules.pages.home_page import HomePage
|
||||
from modules.pages.models_page import ModelsPage
|
||||
from modules.pages.settings_page import SettingsPage
|
||||
from scripts.download_resources import download_font
|
||||
|
||||
class GenerationThread(QThread):
|
||||
progress_updated = pyqtSignal(float, str)
|
||||
@@ -61,497 +69,67 @@ class GenerationThread(QThread):
|
||||
def stop(self):
|
||||
self.is_generating = False
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
class MainWindow(BaseWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.api_url = "http://localhost:7860" # URL по умолчанию
|
||||
self.console_window = None
|
||||
self.prompt_window = None
|
||||
self.negative_prompt_window = None
|
||||
self.progress_timer = None
|
||||
|
||||
# Создаем меню бар
|
||||
self.create_menu_bar()
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Создание меню бара с настройками"""
|
||||
menubar = QMenuBar(self)
|
||||
menubar.setStyleSheet("""
|
||||
QMenuBar {
|
||||
background-color: #2b2b2b;
|
||||
color: #ffffff;
|
||||
border-bottom: 1px solid #3b3b3b;
|
||||
padding: 2px;
|
||||
}
|
||||
QMenuBar::item {
|
||||
background-color: transparent;
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
background-color: #3b3b3b;
|
||||
}
|
||||
QMenuBar::item:pressed {
|
||||
background-color: #404040;
|
||||
}
|
||||
""")
|
||||
|
||||
# Добавляем меню настроек
|
||||
settings_menu = SettingsMenu(self)
|
||||
menubar.addMenu(settings_menu)
|
||||
|
||||
self.setMenuBar(menubar)
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("Stable Diffusion UI")
|
||||
self.setStyleSheet("""
|
||||
QMainWindow {
|
||||
background-color: #2b2b2b;
|
||||
}
|
||||
QWidget {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
QLineEdit, QTextEdit, QComboBox, QSpinBox, QDoubleSpinBox {
|
||||
background-color: #3b3b3b;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
color: #ffffff;
|
||||
min-width: 100px;
|
||||
}
|
||||
QSpinBox::up-button, QDoubleSpinBox::up-button {
|
||||
background-color: #4a4a4a;
|
||||
border: none;
|
||||
border-left: 1px solid #555555;
|
||||
border-bottom: 1px solid #555555;
|
||||
border-top-right-radius: 4px;
|
||||
width: 20px;
|
||||
height: 12px;
|
||||
}
|
||||
QSpinBox::down-button, QDoubleSpinBox::down-button {
|
||||
background-color: #4a4a4a;
|
||||
border: none;
|
||||
border-left: 1px solid #555555;
|
||||
border-bottom-right-radius: 4px;
|
||||
width: 20px;
|
||||
height: 12px;
|
||||
}
|
||||
QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover,
|
||||
QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover {
|
||||
background-color: #5a5a5a;
|
||||
}
|
||||
QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed,
|
||||
QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed {
|
||||
background-color: #666666;
|
||||
}
|
||||
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {
|
||||
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOCA0TDQgOEgxMkw4IDRaIiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+);
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
}
|
||||
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {
|
||||
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOCAxMkwxMiA4SDRMOCAxMloiIGZpbGw9IiNmZmZmZmYiLz48L3N2Zz4=);
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #0d6efd;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #0b5ed7;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #0a58ca;
|
||||
}
|
||||
QProgressBar {
|
||||
border: 1px solid #555555;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
""")
|
||||
self.setMinimumSize(1200, 800)
|
||||
self.setup_ui()
|
||||
|
||||
# Создаем центральный виджет
|
||||
def create_horizontal_layout(self):
|
||||
"""Создает горизонтальный layout для главного окна"""
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
main_layout = QHBoxLayout(central_widget)
|
||||
layout = QHBoxLayout(central_widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
return layout
|
||||
|
||||
# Левая панель с настройками
|
||||
settings_widget = QWidget()
|
||||
settings_layout = QVBoxLayout(settings_widget)
|
||||
settings_layout.setSpacing(10)
|
||||
|
||||
# URL API
|
||||
api_layout = QHBoxLayout()
|
||||
api_label = QLabel("API URL:")
|
||||
self.api_url_input = QLineEdit(self.api_url)
|
||||
self.api_url_input.textChanged.connect(self.update_api_url)
|
||||
api_layout.addWidget(api_label)
|
||||
api_layout.addWidget(self.api_url_input)
|
||||
settings_layout.addLayout(api_layout)
|
||||
|
||||
# Промпт
|
||||
prompt_layout = QVBoxLayout()
|
||||
prompt_header = QHBoxLayout()
|
||||
prompt_label = QLabel("Промпт:")
|
||||
self.prompt_input = QTextEdit()
|
||||
self.prompt_input.setMinimumHeight(100)
|
||||
prompt_buttons = QHBoxLayout()
|
||||
self.prompt_button = QPushButton("Готовые промпты")
|
||||
self.prompt_button.clicked.connect(self.toggle_prompt_window)
|
||||
self.negative_prompt_button = QPushButton("Негативные промпты")
|
||||
self.negative_prompt_button.clicked.connect(self.toggle_negative_prompt_window)
|
||||
prompt_buttons.addWidget(self.prompt_button)
|
||||
prompt_buttons.addWidget(self.negative_prompt_button)
|
||||
prompt_header.addWidget(prompt_label)
|
||||
prompt_header.addLayout(prompt_buttons)
|
||||
prompt_layout.addLayout(prompt_header)
|
||||
prompt_layout.addWidget(self.prompt_input)
|
||||
settings_layout.addLayout(prompt_layout)
|
||||
|
||||
# Негативный промпт
|
||||
negative_prompt_layout = QVBoxLayout()
|
||||
negative_prompt_label = QLabel("Негативный промпт:")
|
||||
self.negative_prompt_input = QTextEdit()
|
||||
self.negative_prompt_input.setMinimumHeight(100)
|
||||
negative_prompt_layout.addWidget(negative_prompt_label)
|
||||
negative_prompt_layout.addWidget(self.negative_prompt_input)
|
||||
settings_layout.addLayout(negative_prompt_layout)
|
||||
|
||||
# Добавляем менеджер моделей
|
||||
self.model_manager = ModelManager(self.api_url)
|
||||
self.model_manager.model_changed.connect(self.on_model_changed)
|
||||
settings_layout.addWidget(self.model_manager)
|
||||
|
||||
# Добавляем менеджер сэмплеров
|
||||
self.sampler_manager = SamplerManager(self.api_url)
|
||||
settings_layout.addWidget(self.sampler_manager)
|
||||
|
||||
# Добавляем менеджер пресетов размеров
|
||||
self.size_preset_manager = SizePresetManager()
|
||||
self.size_preset_manager.size_changed.connect(self.on_size_preset_changed)
|
||||
settings_layout.addWidget(self.size_preset_manager)
|
||||
|
||||
# Параметры генерации
|
||||
params_layout = QVBoxLayout()
|
||||
def setup_ui(self):
|
||||
# Загружаем шрифт
|
||||
font_path = Path("resources/fonts/InterVariable.ttf")
|
||||
if not font_path.exists():
|
||||
download_font()
|
||||
font_id = QFontDatabase.addApplicationFont(str(font_path))
|
||||
if font_id != -1:
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||
app_font = QFont(font_family)
|
||||
QApplication.setFont(app_font)
|
||||
|
||||
# Размеры
|
||||
size_layout = QHBoxLayout()
|
||||
width_label = QLabel("Ширина:")
|
||||
self.width_input = QSpinBox()
|
||||
self.width_input.setRange(64, 2048)
|
||||
self.width_input.setValue(512)
|
||||
height_label = QLabel("Высота:")
|
||||
self.height_input = QSpinBox()
|
||||
self.height_input.setRange(64, 2048)
|
||||
self.height_input.setValue(512)
|
||||
size_layout.addWidget(width_label)
|
||||
size_layout.addWidget(self.width_input)
|
||||
size_layout.addWidget(height_label)
|
||||
size_layout.addWidget(self.height_input)
|
||||
params_layout.addLayout(size_layout)
|
||||
|
||||
# Сид
|
||||
seed_layout = QHBoxLayout()
|
||||
seed_label = QLabel("Сид:")
|
||||
self.seed_input = QSpinBox()
|
||||
self.seed_input.setRange(-1, 999999999)
|
||||
self.seed_input.setValue(-1)
|
||||
seed_layout.addWidget(seed_label)
|
||||
seed_layout.addWidget(self.seed_input)
|
||||
params_layout.addLayout(seed_layout)
|
||||
|
||||
# CFG Scale
|
||||
cfg_layout = QHBoxLayout()
|
||||
cfg_label = QLabel("CFG Scale:")
|
||||
self.cfg_input = QDoubleSpinBox()
|
||||
self.cfg_input.setRange(1.0, 30.0)
|
||||
self.cfg_input.setValue(7.0)
|
||||
cfg_layout.addWidget(cfg_label)
|
||||
cfg_layout.addWidget(self.cfg_input)
|
||||
params_layout.addLayout(cfg_layout)
|
||||
|
||||
# Шаги
|
||||
steps_layout = QHBoxLayout()
|
||||
steps_label = QLabel("Шаги:")
|
||||
self.steps_input = QSpinBox()
|
||||
self.steps_input.setRange(1, 150)
|
||||
self.steps_input.setValue(20)
|
||||
steps_layout.addWidget(steps_label)
|
||||
steps_layout.addWidget(self.steps_input)
|
||||
params_layout.addLayout(steps_layout)
|
||||
|
||||
settings_layout.addLayout(params_layout)
|
||||
|
||||
# Добавляем кнопку консоли
|
||||
self.console_button = QPushButton("Открыть консоль")
|
||||
self.console_button.clicked.connect(self.toggle_console)
|
||||
settings_layout.addWidget(self.console_button)
|
||||
|
||||
# Кнопка генерации
|
||||
self.generate_button = QPushButton("Сгенерировать")
|
||||
self.generate_button.clicked.connect(self.start_generation)
|
||||
settings_layout.addWidget(self.generate_button)
|
||||
|
||||
# Прогресс бар
|
||||
self.progress_bar = QProgressBar()
|
||||
settings_layout.addWidget(self.progress_bar)
|
||||
|
||||
# Добавляем левую панель в основной layout
|
||||
main_layout.addWidget(settings_widget, stretch=1)
|
||||
|
||||
# Правая панель для результатов
|
||||
results_widget = QWidget()
|
||||
results_layout = QVBoxLayout(results_widget)
|
||||
# Создаем горизонтальный layout
|
||||
layout = self.create_horizontal_layout()
|
||||
|
||||
# Область для отображения изображений
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setMinimumWidth(512)
|
||||
self.results_content = QWidget()
|
||||
self.results_content_layout = QVBoxLayout(self.results_content)
|
||||
scroll_area.setWidget(self.results_content)
|
||||
results_layout.addWidget(scroll_area)
|
||||
|
||||
# Добавляем правую панель в основной layout
|
||||
main_layout.addWidget(results_widget, stretch=2)
|
||||
|
||||
# Устанавливаем размер окна
|
||||
self.setMinimumSize(1200, 800)
|
||||
|
||||
def toggle_console(self):
|
||||
"""Открыть/закрыть окно консоли"""
|
||||
if self.console_window is None:
|
||||
self.console_window = ConsoleWindow(parent=self)
|
||||
self.console_window.console_closed.connect(self.on_console_closed)
|
||||
self.console_window.show()
|
||||
else:
|
||||
self.console_window.close()
|
||||
|
||||
def on_console_closed(self):
|
||||
"""Обработчик закрытия окна консоли"""
|
||||
self.console_window = None
|
||||
|
||||
def on_model_changed(self, model_name):
|
||||
"""Обработчик смены модели"""
|
||||
print(f"Модель изменена на: {model_name}")
|
||||
|
||||
def log_to_console(self, message):
|
||||
"""Логирование в консоль"""
|
||||
if self.console_window:
|
||||
self.console_window.log(message)
|
||||
print(message)
|
||||
|
||||
def toggle_prompt_window(self):
|
||||
"""Открыть/закрыть окно с промптами"""
|
||||
if self.prompt_window is None:
|
||||
self.prompt_window = PromptWindow(parent=self)
|
||||
self.prompt_window.prompt_selected.connect(self.add_prompt)
|
||||
self.prompt_window.show()
|
||||
else:
|
||||
self.prompt_window.close()
|
||||
|
||||
def toggle_negative_prompt_window(self):
|
||||
"""Открыть/закрыть окно с негативными промптами"""
|
||||
if self.negative_prompt_window is None:
|
||||
self.negative_prompt_window = NegativePromptWindow(parent=self)
|
||||
self.negative_prompt_window.prompt_selected.connect(self.add_negative_prompt)
|
||||
self.negative_prompt_window.show()
|
||||
else:
|
||||
self.negative_prompt_window.close()
|
||||
|
||||
def add_prompt(self, text):
|
||||
"""Добавить промпт в поле ввода"""
|
||||
current_text = self.prompt_input.toPlainText()
|
||||
if current_text:
|
||||
current_text += ", "
|
||||
self.prompt_input.setText(current_text + text)
|
||||
|
||||
def add_negative_prompt(self, text):
|
||||
"""Добавить негативный промпт в поле ввода"""
|
||||
current_text = self.negative_prompt_input.toPlainText()
|
||||
if current_text:
|
||||
current_text += ", "
|
||||
self.negative_prompt_input.setText(current_text + text)
|
||||
|
||||
def start_generation(self):
|
||||
self.generate_button.setEnabled(False)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
params = {
|
||||
"prompt": self.prompt_input.toPlainText(),
|
||||
"negative_prompt": self.negative_prompt_input.toPlainText(),
|
||||
"width": self.width_input.value(),
|
||||
"height": self.height_input.value(),
|
||||
"seed": self.seed_input.value(),
|
||||
"cfg_scale": self.cfg_input.value(),
|
||||
"steps": self.steps_input.value(),
|
||||
"sampler_name": self.sampler_manager.get_current_sampler(),
|
||||
"batch_size": 1
|
||||
}
|
||||
|
||||
self.log_to_console(f"Начало генерации с параметрами: {json.dumps(params, indent=2, ensure_ascii=False)}")
|
||||
# Добавляем навигационную панель
|
||||
self.nav_panel = NavigationPanel()
|
||||
layout.addWidget(self.nav_panel)
|
||||
|
||||
# Запускаем таймер для отслеживания прогресса
|
||||
self.progress_timer = QTimer()
|
||||
self.progress_timer.timeout.connect(self.check_progress)
|
||||
self.progress_timer.start(100) # Проверяем каждые 100 мс
|
||||
# Создаем контейнер для страниц
|
||||
self.page_container = PageContainer()
|
||||
layout.addWidget(self.page_container)
|
||||
|
||||
self.generation_thread = GenerationThread(self.api_url_input.text(), params)
|
||||
self.generation_thread.generation_complete.connect(self.handle_generation_complete)
|
||||
self.generation_thread.error_occurred.connect(self.handle_error)
|
||||
self.generation_thread.start()
|
||||
|
||||
def check_progress(self):
|
||||
"""Проверка прогресса генерации"""
|
||||
try:
|
||||
response = requests.get(f"{self.api_url_input.text()}/sdapi/v1/progress")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
progress = data.get('progress', 0)
|
||||
current_step = data.get('current_step', 0)
|
||||
total_steps = data.get('total_steps', 1)
|
||||
|
||||
percentage = int(progress * 100)
|
||||
self.progress_bar.setValue(percentage)
|
||||
self.log_to_console(f"Прогресс: {percentage}% - Шаг {current_step}/{total_steps}")
|
||||
|
||||
# Если генерация завершена, останавливаем таймер
|
||||
if progress >= 1.0:
|
||||
self.progress_timer.stop()
|
||||
self.progress_timer = None
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении прогресса: {str(e)}")
|
||||
|
||||
def handle_generation_complete(self, images):
|
||||
"""Обработка завершения генерации"""
|
||||
# Останавливаем таймер, если он ещё активен
|
||||
if self.progress_timer:
|
||||
self.progress_timer.stop()
|
||||
self.progress_timer = None
|
||||
|
||||
self.generate_button.setEnabled(True)
|
||||
self.progress_bar.setValue(100)
|
||||
self.log_to_console("Генерация завершена успешно")
|
||||
# Добавляем страницы
|
||||
self.home_page = HomePage()
|
||||
self.models_page = ModelsPage()
|
||||
self.settings_page = SettingsPage()
|
||||
|
||||
# Очищаем предыдущие результаты
|
||||
for i in reversed(range(self.results_content_layout.count())):
|
||||
self.results_content_layout.itemAt(i).widget().setParent(None)
|
||||
|
||||
for img_data in images:
|
||||
try:
|
||||
# Декодируем base64 в байты
|
||||
img_bytes = base64.b64decode(img_data)
|
||||
self.log_to_console(f"Размер полученных данных: {len(img_bytes)} байт")
|
||||
|
||||
# Создаем объект изображения
|
||||
img = Image.open(BytesIO(img_bytes))
|
||||
self.log_to_console(f"Размер изображения: {img.size}, формат: {img.format}")
|
||||
|
||||
# Конвертируем в RGB если нужно
|
||||
if img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Сохраняем во временный буфер как PNG
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
|
||||
# Создаем QPixmap
|
||||
pixmap = QPixmap()
|
||||
if not pixmap.loadFromData(buffer.getvalue()):
|
||||
raise Exception("Не удалось создать QPixmap из данных")
|
||||
|
||||
# Проверяем размер QPixmap
|
||||
if pixmap.isNull():
|
||||
raise Exception("Создан пустой QPixmap")
|
||||
|
||||
self.log_to_console(f"Размер QPixmap: {pixmap.width()}x{pixmap.height()}")
|
||||
|
||||
# Создаем и добавляем QLabel с изображением
|
||||
img_label = QLabel()
|
||||
img_label.setPixmap(pixmap)
|
||||
img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# Создаем кнопку сохранения
|
||||
save_button = QPushButton("Сохранить")
|
||||
save_button.clicked.connect(lambda checked, img=img: self.save_image(img))
|
||||
|
||||
# Создаем кнопку копирования в буфер обмена
|
||||
copy_button = QPushButton("Копировать")
|
||||
copy_button.clicked.connect(lambda checked, img=img: self.copy_to_clipboard(img))
|
||||
|
||||
# Создаем контейнер для изображения и кнопок
|
||||
container = QWidget()
|
||||
container_layout = QVBoxLayout(container)
|
||||
container_layout.addWidget(img_label)
|
||||
|
||||
# Создаем горизонтальный layout для кнопок
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.addWidget(save_button)
|
||||
buttons_layout.addWidget(copy_button)
|
||||
container_layout.addLayout(buttons_layout)
|
||||
|
||||
self.results_content_layout.addWidget(container)
|
||||
self.log_to_console("Изображение успешно добавлено в интерфейс")
|
||||
except Exception as e:
|
||||
error_msg = f"Ошибка отображения изображения: {str(e)}"
|
||||
self.log_to_console(error_msg)
|
||||
error_label = QLabel(error_msg)
|
||||
self.results_content_layout.addWidget(error_label)
|
||||
|
||||
def handle_error(self, error_message):
|
||||
"""Обработка ошибок"""
|
||||
self.generate_button.setEnabled(True)
|
||||
self.log_to_console(f"Ошибка: {error_message}")
|
||||
|
||||
def save_image(self, img):
|
||||
file_path, _ = QFileDialog.getSaveFileName(self, "Сохранить изображение", "", "PNG Files (*.png);;JPEG Files (*.jpg)")
|
||||
if file_path:
|
||||
img.save(file_path)
|
||||
|
||||
def copy_to_clipboard(self, img):
|
||||
"""Копирование изображения в буфер обмена"""
|
||||
# Конвертируем изображение в QPixmap
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(buffer.getvalue())
|
||||
self.page_container.add_page("home", self.home_page)
|
||||
self.page_container.add_page("models", self.models_page)
|
||||
self.page_container.add_page("settings", self.settings_page)
|
||||
|
||||
# Копируем в буфер обмена
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setPixmap(pixmap)
|
||||
# Подключаем сигнал смены страницы
|
||||
self.nav_panel.page_changed.connect(self.switch_page)
|
||||
|
||||
# Показываем уведомление
|
||||
self.log_to_console("Изображение скопировано в буфер обмена")
|
||||
# Показываем домашнюю страницу по умолчанию
|
||||
self.page_container.show_page("home")
|
||||
|
||||
def update_api_url(self):
|
||||
"""Обновление URL API во всех компонентах"""
|
||||
new_url = self.api_url_input.text()
|
||||
self.model_manager.update_api_url(new_url)
|
||||
self.sampler_manager.update_api_url(new_url)
|
||||
self.log_to_console(f"API URL обновлен: {new_url}")
|
||||
def switch_page(self, page_name):
|
||||
self.page_container.show_page(page_name)
|
||||
|
||||
def on_size_preset_changed(self, width, height):
|
||||
"""Обработчик изменения размера через пресет"""
|
||||
self.width_input.setValue(width)
|
||||
self.height_input.setValue(height)
|
||||
self.log_to_console(f"Размер изменен на: {width}x{height}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,195 @@
|
||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QLineEdit, QTextEdit,
|
||||
QSpinBox, QDoubleSpinBox, QComboBox)
|
||||
from PyQt6.QtCore import Qt
|
||||
from ..styles.page import BasePage
|
||||
from ..styles.theme import Colors, Spacing, Fonts
|
||||
from ..styles.theme_manager import ThemeManager
|
||||
|
||||
class HomePage(BasePage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_content()
|
||||
|
||||
def setup_content(self):
|
||||
"""Настройка содержимого страницы"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
spacing = self.theme_manager.current_theme.get_spacing()
|
||||
|
||||
# Заголовок
|
||||
header = QLabel("Генерация изображений")
|
||||
header.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
self.layout.addWidget(header)
|
||||
|
||||
# Промпт
|
||||
prompt_layout = QVBoxLayout()
|
||||
prompt_label = QLabel("Промпт:")
|
||||
prompt_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
""")
|
||||
self.prompt_input = QTextEdit()
|
||||
self.prompt_input.setPlaceholderText("Опишите изображение, которое хотите сгенерировать...")
|
||||
self.prompt_input.setMinimumHeight(100)
|
||||
|
||||
prompt_layout.addWidget(prompt_label)
|
||||
prompt_layout.addWidget(self.prompt_input)
|
||||
self.layout.addLayout(prompt_layout)
|
||||
|
||||
# Негативный промпт
|
||||
negative_prompt_layout = QVBoxLayout()
|
||||
negative_prompt_label = QLabel("Негативный промпт:")
|
||||
negative_prompt_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
""")
|
||||
self.negative_prompt_input = QTextEdit()
|
||||
self.negative_prompt_input.setPlaceholderText("Опишите, что не должно появиться на изображении...")
|
||||
self.negative_prompt_input.setMinimumHeight(100)
|
||||
|
||||
negative_prompt_layout.addWidget(negative_prompt_label)
|
||||
negative_prompt_layout.addWidget(self.negative_prompt_input)
|
||||
self.layout.addLayout(negative_prompt_layout)
|
||||
|
||||
# Параметры генерации
|
||||
params_layout = QHBoxLayout()
|
||||
|
||||
# Размер изображения
|
||||
size_layout = QVBoxLayout()
|
||||
size_label = QLabel("Размер:")
|
||||
size_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
""")
|
||||
self.size_combo = QComboBox()
|
||||
self.size_combo.addItems(["512x512", "768x768", "1024x1024"])
|
||||
size_layout.addWidget(size_label)
|
||||
size_layout.addWidget(self.size_combo)
|
||||
params_layout.addLayout(size_layout)
|
||||
|
||||
# Количество шагов
|
||||
steps_layout = QVBoxLayout()
|
||||
steps_label = QLabel("Шаги:")
|
||||
steps_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
""")
|
||||
self.steps_spin = QSpinBox()
|
||||
self.steps_spin.setRange(1, 150)
|
||||
self.steps_spin.setValue(20)
|
||||
steps_layout.addWidget(steps_label)
|
||||
steps_layout.addWidget(self.steps_spin)
|
||||
params_layout.addLayout(steps_layout)
|
||||
|
||||
# CFG Scale
|
||||
cfg_layout = QVBoxLayout()
|
||||
cfg_label = QLabel("CFG Scale:")
|
||||
cfg_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
""")
|
||||
self.cfg_spin = QDoubleSpinBox()
|
||||
self.cfg_spin.setRange(1.0, 30.0)
|
||||
self.cfg_spin.setValue(7.0)
|
||||
self.cfg_spin.setSingleStep(0.5)
|
||||
cfg_layout.addWidget(cfg_label)
|
||||
cfg_layout.addWidget(self.cfg_spin)
|
||||
params_layout.addLayout(cfg_layout)
|
||||
|
||||
# Сэмплер
|
||||
sampler_layout = QVBoxLayout()
|
||||
sampler_label = QLabel("Сэмплер:")
|
||||
sampler_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
""")
|
||||
self.sampler_combo = QComboBox()
|
||||
self.sampler_combo.addItems(["Euler a", "Euler", "DPM++ 2M Karras"])
|
||||
sampler_layout.addWidget(sampler_label)
|
||||
sampler_layout.addWidget(self.sampler_combo)
|
||||
params_layout.addLayout(sampler_layout)
|
||||
|
||||
# Добавляем параметры на страницу
|
||||
self.layout.addLayout(params_layout)
|
||||
|
||||
# Кнопки управления
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.setSpacing(spacing.MEDIUM)
|
||||
|
||||
self.generate_button = QPushButton("Сгенерировать")
|
||||
self.generate_button.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
min-width: 150px;
|
||||
background-color: {colors['primary']};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
}}
|
||||
""")
|
||||
|
||||
self.clear_button = QPushButton("Очистить")
|
||||
self.clear_button.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
min-width: 150px;
|
||||
background-color: transparent;
|
||||
color: {colors['primary']};
|
||||
border: 1px solid {colors['primary']};
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
color: white;
|
||||
border: none;
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
color: white;
|
||||
border: none;
|
||||
}}
|
||||
""")
|
||||
|
||||
buttons_layout.addWidget(self.generate_button)
|
||||
buttons_layout.addWidget(self.clear_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
self.layout.addLayout(buttons_layout)
|
||||
|
||||
# Растягиваем пространство
|
||||
self.layout.addStretch()
|
||||
@@ -0,0 +1,178 @@
|
||||
from PyQt6.QtWidgets import (QLabel, QPushButton, QHBoxLayout,
|
||||
QVBoxLayout, QLineEdit, QListWidget,
|
||||
QListWidgetItem, QFrame)
|
||||
from PyQt6.QtCore import Qt
|
||||
from modules.styles.page import BasePage
|
||||
from modules.styles.theme import Colors
|
||||
from modules.styles.theme_manager import ThemeManager
|
||||
|
||||
class ModelItem(QFrame):
|
||||
def __init__(self, model_name: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.model_name = model_name
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка UI элемента модели"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
spacing = self.theme_manager.current_theme.get_spacing()
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
# Название модели
|
||||
name_label = QLabel(self.model_name)
|
||||
name_label.setStyleSheet(f"color: {colors['text']};")
|
||||
layout.addWidget(name_label)
|
||||
|
||||
# Растягиваем пространство
|
||||
layout.addStretch()
|
||||
|
||||
# Кнопки управления
|
||||
select_btn = QPushButton("Выбрать")
|
||||
select_btn.setMinimumWidth(100)
|
||||
select_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['primary']};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
}}
|
||||
""")
|
||||
layout.addWidget(select_btn)
|
||||
|
||||
delete_btn = QPushButton("Удалить")
|
||||
delete_btn.setMinimumWidth(100)
|
||||
delete_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['error']};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
}}
|
||||
""")
|
||||
layout.addWidget(delete_btn)
|
||||
|
||||
# Стили
|
||||
self.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {colors['surface']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {spacing.TINY}px;
|
||||
}}
|
||||
""")
|
||||
|
||||
class ModelsPage(BasePage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_content()
|
||||
|
||||
def setup_content(self):
|
||||
"""Настройка содержимого страницы"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
spacing = self.theme_manager.current_theme.get_spacing()
|
||||
|
||||
# Заголовок
|
||||
header = QLabel("Модели")
|
||||
header.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
self.layout.addWidget(header)
|
||||
|
||||
# Поиск и добавление
|
||||
search_layout = QHBoxLayout()
|
||||
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("Поиск моделей...")
|
||||
self.search_input.setStyleSheet(f"""
|
||||
QLineEdit {{
|
||||
background-color: {colors['surface']};
|
||||
color: {colors['text']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px;
|
||||
}}
|
||||
|
||||
QLineEdit:focus {{
|
||||
border: 1px solid {colors['primary']};
|
||||
}}
|
||||
""")
|
||||
search_layout.addWidget(self.search_input)
|
||||
|
||||
self.add_btn = QPushButton("Добавить модель")
|
||||
self.add_btn.setMinimumWidth(150)
|
||||
self.add_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['primary']};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
}}
|
||||
""")
|
||||
search_layout.addWidget(self.add_btn)
|
||||
|
||||
self.layout.addLayout(search_layout)
|
||||
|
||||
# Список моделей
|
||||
self.models_list = QListWidget()
|
||||
self.models_list.setSpacing(8)
|
||||
self.models_list.setStyleSheet(f"""
|
||||
QListWidget {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}}
|
||||
|
||||
QListWidget::item {{
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}}
|
||||
""")
|
||||
|
||||
# Добавляем тестовые модели
|
||||
test_models = [
|
||||
"stable-diffusion-v1-5",
|
||||
"stable-diffusion-v2-1",
|
||||
"stable-diffusion-xl-base-1.0"
|
||||
]
|
||||
|
||||
for model in test_models:
|
||||
item = QListWidgetItem(self.models_list)
|
||||
model_widget = ModelItem(model)
|
||||
item.setSizeHint(model_widget.sizeHint())
|
||||
self.models_list.addItem(item)
|
||||
self.models_list.setItemWidget(item, model_widget)
|
||||
|
||||
self.layout.addWidget(self.models_list)
|
||||
@@ -0,0 +1,193 @@
|
||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QLineEdit, QComboBox,
|
||||
QSpinBox, QCheckBox, QFrame)
|
||||
from PyQt6.QtCore import Qt
|
||||
from ..styles.page import BasePage
|
||||
from ..styles.theme import Colors, Spacing, Fonts
|
||||
from ..styles.icons import IconManager, IconType
|
||||
from ..styles.theme_manager import ThemeManager
|
||||
|
||||
class SettingsSection(QFrame):
|
||||
def __init__(self, title: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.title = title
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка UI секции настроек"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
spacing = self.theme_manager.current_theme.get_spacing()
|
||||
|
||||
# Основной layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Заголовок секции
|
||||
title_label = QLabel(self.title)
|
||||
title_label.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Контейнер для содержимого
|
||||
self.content_layout = QVBoxLayout()
|
||||
self.content_layout.setSpacing(12)
|
||||
layout.addLayout(self.content_layout)
|
||||
|
||||
# Стили
|
||||
self.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {colors['surface']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {spacing.MEDIUM}px;
|
||||
}}
|
||||
""")
|
||||
|
||||
class SettingsPage(BasePage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_content()
|
||||
|
||||
def setup_content(self):
|
||||
"""Настройка содержимого страницы"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
spacing = self.theme_manager.current_theme.get_spacing()
|
||||
|
||||
# Заголовок
|
||||
header = QLabel("Настройки")
|
||||
header.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
""")
|
||||
self.layout.addWidget(header)
|
||||
|
||||
# API настройки
|
||||
api_section = SettingsSection("API")
|
||||
|
||||
api_url_layout = QVBoxLayout()
|
||||
api_url_label = QLabel("URL API:")
|
||||
api_url_label.setStyleSheet(f"color: {colors['text']};")
|
||||
self.api_url_input = QLineEdit()
|
||||
self.api_url_input.setPlaceholderText("http://localhost:7860")
|
||||
api_url_layout.addWidget(api_url_label)
|
||||
api_url_layout.addWidget(self.api_url_input)
|
||||
api_section.content_layout.addLayout(api_url_layout)
|
||||
|
||||
self.layout.addWidget(api_section)
|
||||
|
||||
# UI настройки
|
||||
ui_section = SettingsSection("Интерфейс")
|
||||
|
||||
# Тема
|
||||
theme_layout = QVBoxLayout()
|
||||
theme_label = QLabel("Тема:")
|
||||
theme_label.setStyleSheet(f"color: {colors['text']};")
|
||||
self.theme_combo = QComboBox()
|
||||
self.theme_combo.addItems(["Тёмная", "Светлая"])
|
||||
theme_layout.addWidget(theme_label)
|
||||
theme_layout.addWidget(self.theme_combo)
|
||||
ui_section.content_layout.addLayout(theme_layout)
|
||||
|
||||
# Размер шрифта
|
||||
font_size_layout = QVBoxLayout()
|
||||
font_size_label = QLabel("Размер шрифта:")
|
||||
font_size_label.setStyleSheet(f"color: {colors['text']};")
|
||||
self.font_size_spin = QSpinBox()
|
||||
self.font_size_spin.setRange(10, 24)
|
||||
self.font_size_spin.setValue(14)
|
||||
font_size_layout.addWidget(font_size_label)
|
||||
font_size_layout.addWidget(self.font_size_spin)
|
||||
ui_section.content_layout.addLayout(font_size_layout)
|
||||
|
||||
self.layout.addWidget(ui_section)
|
||||
|
||||
# Настройки генерации
|
||||
generation_section = SettingsSection("Генерация")
|
||||
|
||||
# Качество по умолчанию
|
||||
quality_layout = QVBoxLayout()
|
||||
quality_label = QLabel("Качество по умолчанию:")
|
||||
quality_label.setStyleSheet(f"color: {colors['text']};")
|
||||
self.quality_combo = QComboBox()
|
||||
self.quality_combo.addItems(["Низкое", "Среднее", "Высокое"])
|
||||
quality_layout.addWidget(quality_label)
|
||||
quality_layout.addWidget(self.quality_combo)
|
||||
generation_section.content_layout.addLayout(quality_layout)
|
||||
|
||||
# Размер по умолчанию
|
||||
size_layout = QVBoxLayout()
|
||||
size_label = QLabel("Размер по умолчанию:")
|
||||
size_label.setStyleSheet(f"color: {colors['text']};")
|
||||
self.size_combo = QComboBox()
|
||||
self.size_combo.addItems(["512x512", "768x768", "1024x1024"])
|
||||
size_layout.addWidget(size_label)
|
||||
size_layout.addWidget(self.size_combo)
|
||||
generation_section.content_layout.addLayout(size_layout)
|
||||
|
||||
self.layout.addWidget(generation_section)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.save_btn = QPushButton("Сохранить")
|
||||
self.save_btn.setMinimumWidth(150)
|
||||
self.save_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['primary']};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
}}
|
||||
""")
|
||||
buttons_layout.addWidget(self.save_btn)
|
||||
|
||||
self.reset_btn = QPushButton("Сбросить")
|
||||
self.reset_btn.setMinimumWidth(150)
|
||||
self.reset_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
color: {colors['primary']};
|
||||
border: 1px solid {colors['primary']};
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
color: white;
|
||||
border: none;
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
color: white;
|
||||
border: none;
|
||||
}}
|
||||
""")
|
||||
buttons_layout.addWidget(self.reset_btn)
|
||||
|
||||
buttons_layout.addStretch()
|
||||
|
||||
self.layout.addLayout(buttons_layout)
|
||||
|
||||
# Растягиваем пространство
|
||||
self.layout.addStretch()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
from PyQt6.QtWidgets import QMainWindow, QWidget, QVBoxLayout
|
||||
from PyQt6.QtCore import QEasingCurve, QPropertyAnimation, QRect
|
||||
from .theme_manager import ThemeManager
|
||||
|
||||
class BaseWindow(QMainWindow):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_window()
|
||||
|
||||
def setup_window(self):
|
||||
"""Базовая настройка окна"""
|
||||
# Создаем центральный виджет
|
||||
self.central_widget = QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
# Основной layout
|
||||
self.main_layout = QVBoxLayout(self.central_widget)
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
# Применяем тему
|
||||
self.theme_manager.apply_theme()
|
||||
|
||||
def animate_window(self, start_geometry: QRect, end_geometry: QRect, duration: int = 300):
|
||||
"""Анимация окна"""
|
||||
self.animation = QPropertyAnimation(self, b"geometry")
|
||||
self.animation.setDuration(duration)
|
||||
self.animation.setStartValue(start_geometry)
|
||||
self.animation.setEndValue(end_geometry)
|
||||
self.animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
self.animation.start()
|
||||
|
||||
def show_with_animation(self):
|
||||
"""Показать окно с анимацией"""
|
||||
# Сохраняем конечную геометрию
|
||||
end_geometry = self.geometry()
|
||||
|
||||
# Устанавливаем начальную геометрию (уменьшенную и прозрачную)
|
||||
start_geometry = QRect(
|
||||
end_geometry.x() + end_geometry.width() // 2,
|
||||
end_geometry.y() + end_geometry.height() // 2,
|
||||
0,
|
||||
0
|
||||
)
|
||||
|
||||
# Запускаем анимацию
|
||||
self.animate_window(start_geometry, end_geometry)
|
||||
super().show()
|
||||
|
||||
def hide_with_animation(self):
|
||||
"""Скрыть окно с анимацией"""
|
||||
# Сохраняем начальную геометрию
|
||||
start_geometry = self.geometry()
|
||||
|
||||
# Устанавливаем конечную геометрию (уменьшенную и прозрачную)
|
||||
end_geometry = QRect(
|
||||
start_geometry.x() + start_geometry.width() // 2,
|
||||
start_geometry.y() + start_geometry.height() // 2,
|
||||
0,
|
||||
0
|
||||
)
|
||||
|
||||
# Запускаем анимацию
|
||||
self.animation = QPropertyAnimation(self, b"geometry")
|
||||
self.animation.setDuration(300)
|
||||
self.animation.setStartValue(start_geometry)
|
||||
self.animation.setEndValue(end_geometry)
|
||||
self.animation.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
self.animation.finished.connect(super().hide)
|
||||
self.animation.start()
|
||||
|
||||
def toggle_theme(self):
|
||||
"""Переключение темы"""
|
||||
self.theme_manager.toggle_theme()
|
||||
@@ -0,0 +1,97 @@
|
||||
from enum import Enum, auto
|
||||
from PyQt6.QtGui import QIcon
|
||||
from pathlib import Path
|
||||
import requests
|
||||
import os
|
||||
|
||||
class IconType(Enum):
|
||||
# Навигация
|
||||
HOME = auto()
|
||||
SETTINGS = auto()
|
||||
MENU = "menu"
|
||||
BACK = "arrow-back"
|
||||
FORWARD = "arrow-forward"
|
||||
|
||||
# Действия
|
||||
ADD = "add"
|
||||
REMOVE = "remove"
|
||||
EDIT = "edit"
|
||||
DELETE = "delete"
|
||||
SAVE = "save"
|
||||
REFRESH = "refresh"
|
||||
SEARCH = "search"
|
||||
|
||||
# Медиа
|
||||
IMAGE = auto()
|
||||
VIDEO = "video"
|
||||
AUDIO = "audio"
|
||||
PLAY = "play"
|
||||
PAUSE = "pause"
|
||||
STOP = "stop"
|
||||
|
||||
# Статусы
|
||||
SUCCESS = "check-circle"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
INFO = "info"
|
||||
|
||||
# Интерфейс
|
||||
CLOSE = "close"
|
||||
MINIMIZE = "minimize"
|
||||
MAXIMIZE = "maximize"
|
||||
RESTORE = "restore"
|
||||
|
||||
# Тема
|
||||
LIGHT_MODE = "light-mode"
|
||||
DARK_MODE = "dark-mode"
|
||||
|
||||
class IconManager:
|
||||
_instance = None
|
||||
_icons = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(IconManager, cls).__new__(cls)
|
||||
cls._instance._download_icons()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def _download_icons(self):
|
||||
"""Загрузка иконок из интернета"""
|
||||
icons_dir = Path("resources/icons")
|
||||
icons_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
icons = {
|
||||
IconType.HOME: "https://api.iconify.design/mdi/home.svg",
|
||||
IconType.IMAGE: "https://api.iconify.design/mdi/image.svg",
|
||||
IconType.SETTINGS: "https://api.iconify.design/mdi/cog.svg"
|
||||
}
|
||||
|
||||
for icon_type, url in icons.items():
|
||||
icon_path = icons_dir / f"{icon_type.name.lower()}.svg"
|
||||
if not icon_path.exists():
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
with open(icon_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
except Exception as e:
|
||||
print(f"Ошибка при загрузке иконки {icon_type}: {e}")
|
||||
|
||||
self._icons[icon_type] = QIcon(str(icon_path))
|
||||
|
||||
def get_icon(self, icon_type: IconType) -> QIcon:
|
||||
"""Получение иконки по типу"""
|
||||
return self._icons.get(icon_type, QIcon())
|
||||
|
||||
def get_themed_icon(self, icon_type: IconType, theme: str) -> QIcon:
|
||||
"""Получение иконки с учетом темы"""
|
||||
icon_file = Path("resources/icons") / theme / f"{icon_type.value}.svg"
|
||||
if icon_file.exists():
|
||||
return QIcon(str(icon_file))
|
||||
return self.get_icon(icon_type)
|
||||
@@ -0,0 +1,170 @@
|
||||
/* Основные стили */
|
||||
QMainWindow {
|
||||
background: #1C1C1E;
|
||||
}
|
||||
|
||||
/* Навигационная панель */
|
||||
NavigationPanel {
|
||||
background-color: #2C2C2E;
|
||||
border-right: 1px solid #38383A;
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
NavigationButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
NavigationButton:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
NavigationButton:checked {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Страницы */
|
||||
BasePage {
|
||||
background-color: #1C1C1E;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Заголовки */
|
||||
QLabel[heading="true"] {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Поля ввода */
|
||||
QLineEdit, QTextEdit {
|
||||
background-color: #2C2C2E;
|
||||
border: 1px solid #38383A;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
QLineEdit:focus, QTextEdit:focus {
|
||||
border: 2px solid #007AFF;
|
||||
background-color: #1C1C1E;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
QPushButton {
|
||||
background-color: #007AFF;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
min-width: 100px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #5856D6;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #007AFF;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #38383A;
|
||||
color: #EBEBF5;
|
||||
}
|
||||
|
||||
/* Выпадающие списки */
|
||||
QComboBox {
|
||||
background-color: #2C2C2E;
|
||||
color: white;
|
||||
border: 1px solid #38383A;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
QComboBox::down-arrow {
|
||||
image: url(resources/icons/chevron-down.svg);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Полосы прокрутки */
|
||||
QScrollBar:vertical {
|
||||
background: #1C1C1E;
|
||||
width: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: #38383A;
|
||||
border-radius: 4px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Секции настроек */
|
||||
SettingsSection {
|
||||
background-color: #2C2C2E;
|
||||
border: 1px solid #38383A;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Списки */
|
||||
QListWidget {
|
||||
background-color: #2C2C2E;
|
||||
border: 1px solid #38383A;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
QListWidget::item {
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
QListWidget::item:selected {
|
||||
background-color: #007AFF;
|
||||
}
|
||||
|
||||
/* Прогресс бар */
|
||||
QProgressBar {
|
||||
border: 1px solid #38383A;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background-color: #2C2C2E;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #007AFF;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QFrame)
|
||||
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, pyqtSignal
|
||||
from .icons import IconManager, IconType
|
||||
from .theme import Theme, Colors, Spacing
|
||||
from .theme_manager import ThemeManager
|
||||
|
||||
class NavigationButton(QPushButton):
|
||||
def __init__(self, icon_type: IconType, text: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.icon_manager = IconManager.get_instance()
|
||||
self.icon_type = icon_type
|
||||
self.setup_ui(text)
|
||||
|
||||
def setup_ui(self, text: str):
|
||||
self.icon_manager = IconManager.get_instance()
|
||||
self.setIcon(self.icon_manager.get_icon(self.icon_type))
|
||||
self.setText(text)
|
||||
self.setCheckable(True)
|
||||
self.setFixedHeight(40)
|
||||
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
QPushButton:checked {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
|
||||
class NavigationPanel(QFrame):
|
||||
page_changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
self.setStyleSheet(f"""
|
||||
QFrame {{
|
||||
background-color: {colors['surface']};
|
||||
border-right: 1px solid {colors['border']};
|
||||
}}
|
||||
""")
|
||||
|
||||
# Основной layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(8, 16, 8, 16)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Заголовок
|
||||
header = QLabel("Stable Diffusion UI")
|
||||
header.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding: 16px;
|
||||
}}
|
||||
""")
|
||||
layout.addWidget(header)
|
||||
|
||||
# Кнопки навигации
|
||||
self.home_btn = NavigationButton(IconType.HOME, "Главная")
|
||||
self.models_btn = NavigationButton(IconType.IMAGE, "Модели")
|
||||
self.settings_btn = NavigationButton(IconType.SETTINGS, "Настройки")
|
||||
|
||||
# Добавляем кнопки
|
||||
layout.addWidget(self.home_btn)
|
||||
layout.addWidget(self.models_btn)
|
||||
layout.addWidget(self.settings_btn)
|
||||
|
||||
# Растягиваем пространство
|
||||
layout.addStretch()
|
||||
|
||||
# Подключаем сигналы
|
||||
self.home_btn.clicked.connect(lambda: self.change_page("home"))
|
||||
self.models_btn.clicked.connect(lambda: self.change_page("models"))
|
||||
self.settings_btn.clicked.connect(lambda: self.change_page("settings"))
|
||||
|
||||
# Устанавливаем фиксированную ширину
|
||||
self.setFixedWidth(250)
|
||||
|
||||
def change_page(self, page_name: str):
|
||||
"""Смена активной страницы"""
|
||||
# Отключаем все кнопки
|
||||
for btn in self.findChildren(NavigationButton):
|
||||
btn.setChecked(False)
|
||||
|
||||
# Включаем нужную кнопку
|
||||
if page_name == "home":
|
||||
self.home_btn.setChecked(True)
|
||||
elif page_name == "models":
|
||||
self.models_btn.setChecked(True)
|
||||
elif page_name == "settings":
|
||||
self.settings_btn.setChecked(True)
|
||||
|
||||
# Отправляем сигнал о смене страницы
|
||||
self.page_changed.emit(page_name)
|
||||
@@ -0,0 +1,103 @@
|
||||
from PyQt6.QtWidgets import QWidget, QStackedWidget, QVBoxLayout
|
||||
from PyQt6.QtCore import QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, Qt, QPoint
|
||||
from .theme import Theme, Colors
|
||||
from .theme_manager import ThemeManager
|
||||
|
||||
class BasePage(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Базовая настройка страницы"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
self.setStyleSheet(f"""
|
||||
QWidget {{
|
||||
background-color: {colors['background']};
|
||||
}}
|
||||
""")
|
||||
|
||||
# Основной layout
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(24, 24, 24, 24)
|
||||
self.layout.setSpacing(16)
|
||||
|
||||
def enter_animation(self):
|
||||
"""Анимация появления страницы"""
|
||||
# Создаем группу анимаций
|
||||
self.anim_group = QParallelAnimationGroup()
|
||||
|
||||
# Анимация прозрачности
|
||||
fade_anim = QPropertyAnimation(self, b"windowOpacity")
|
||||
fade_anim.setStartValue(0.0)
|
||||
fade_anim.setEndValue(1.0)
|
||||
fade_anim.setDuration(200)
|
||||
fade_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
# Анимация сдвига
|
||||
pos_anim = QPropertyAnimation(self, b"pos")
|
||||
start_pos = self.pos()
|
||||
pos_anim.setStartValue(start_pos + QPoint(20, 0))
|
||||
pos_anim.setEndValue(start_pos)
|
||||
pos_anim.setDuration(200)
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
# Добавляем анимации в группу
|
||||
self.anim_group.addAnimation(fade_anim)
|
||||
self.anim_group.addAnimation(pos_anim)
|
||||
|
||||
# Запускаем анимацию
|
||||
self.anim_group.start()
|
||||
|
||||
def leave_animation(self):
|
||||
"""Анимация исчезновения страницы"""
|
||||
# Создаем группу анимаций
|
||||
self.anim_group = QParallelAnimationGroup()
|
||||
|
||||
# Анимация прозрачности
|
||||
fade_anim = QPropertyAnimation(self, b"windowOpacity")
|
||||
fade_anim.setStartValue(1.0)
|
||||
fade_anim.setEndValue(0.0)
|
||||
fade_anim.setDuration(200)
|
||||
fade_anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
# Анимация сдвига
|
||||
pos_anim = QPropertyAnimation(self, b"pos")
|
||||
start_pos = self.pos()
|
||||
pos_anim.setStartValue(start_pos)
|
||||
pos_anim.setEndValue(start_pos - QPoint(20, 0))
|
||||
pos_anim.setDuration(200)
|
||||
pos_anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
# Добавляем анимации в группу
|
||||
self.anim_group.addAnimation(fade_anim)
|
||||
self.anim_group.addAnimation(pos_anim)
|
||||
|
||||
# Запускаем анимацию
|
||||
self.anim_group.start()
|
||||
|
||||
class PageContainer(QStackedWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.pages = {}
|
||||
|
||||
def add_page(self, name: str, page: BasePage):
|
||||
"""Добавление новой страницы"""
|
||||
self.pages[name] = page
|
||||
self.addWidget(page)
|
||||
|
||||
def show_page(self, name: str):
|
||||
"""Показ страницы по имени"""
|
||||
if name in self.pages:
|
||||
# Запускаем анимацию исчезновения текущей страницы
|
||||
current_page = self.currentWidget()
|
||||
if isinstance(current_page, BasePage):
|
||||
current_page.leave_animation()
|
||||
|
||||
# Показываем новую страницу
|
||||
new_page = self.pages[name]
|
||||
self.setCurrentWidget(new_page)
|
||||
|
||||
# Запускаем анимацию появления новой страницы
|
||||
new_page.enter_animation()
|
||||
@@ -0,0 +1,217 @@
|
||||
from enum import Enum, auto
|
||||
from typing import Dict
|
||||
|
||||
class Colors:
|
||||
DARK = {
|
||||
'background': '#1E1E1E',
|
||||
'surface': '#252526',
|
||||
'primary': '#007ACC',
|
||||
'secondary': '#3C3C3C',
|
||||
'text': '#FFFFFF',
|
||||
'text_secondary': '#CCCCCC',
|
||||
'border': '#3C3C3C',
|
||||
'hover': '#2A2D2E',
|
||||
'active': '#37373D',
|
||||
'error': '#F44336',
|
||||
'success': '#4CAF50',
|
||||
'warning': '#FFC107'
|
||||
}
|
||||
|
||||
LIGHT = {
|
||||
'background': '#FFFFFF',
|
||||
'surface': '#F3F3F3',
|
||||
'primary': '#007ACC',
|
||||
'secondary': '#E0E0E0',
|
||||
'text': '#000000',
|
||||
'text_secondary': '#666666',
|
||||
'border': '#CCCCCC',
|
||||
'hover': '#E8E8E8',
|
||||
'active': '#DADADA',
|
||||
'error': '#F44336',
|
||||
'success': '#4CAF50',
|
||||
'warning': '#FFC107'
|
||||
}
|
||||
|
||||
class Fonts:
|
||||
FAMILY = "Inter"
|
||||
SIZES = {
|
||||
"xs": "11px",
|
||||
"sm": "13px",
|
||||
"md": "14px",
|
||||
"lg": "16px",
|
||||
"xl": "18px"
|
||||
}
|
||||
WEIGHTS = {
|
||||
"normal": "400",
|
||||
"medium": "500",
|
||||
"semibold": "600",
|
||||
"bold": "700"
|
||||
}
|
||||
|
||||
class Spacing:
|
||||
TINY = 4
|
||||
SMALL = 8
|
||||
MEDIUM = 16
|
||||
LARGE = 24
|
||||
XLARGE = 32
|
||||
XXLARGE = 48
|
||||
|
||||
class Shadows:
|
||||
SMALL = {
|
||||
"dark": "0 2px 4px rgba(0, 0, 0, 0.3)",
|
||||
"light": "0 2px 4px rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
MEDIUM = {
|
||||
"dark": "0 4px 8px rgba(0, 0, 0, 0.4)",
|
||||
"light": "0 4px 8px rgba(0, 0, 0, 0.15)"
|
||||
}
|
||||
LARGE = {
|
||||
"dark": "0 8px 16px rgba(0, 0, 0, 0.5)",
|
||||
"light": "0 8px 16px rgba(0, 0, 0, 0.2)"
|
||||
}
|
||||
|
||||
class Theme(Enum):
|
||||
DARK = auto()
|
||||
LIGHT = auto()
|
||||
|
||||
def get_colors(self):
|
||||
if self == Theme.DARK:
|
||||
return Colors.DARK
|
||||
return Colors.LIGHT
|
||||
|
||||
def get_spacing(self):
|
||||
return Spacing
|
||||
|
||||
def get_fonts(self):
|
||||
return Fonts
|
||||
|
||||
def get_shadows(self):
|
||||
if self == Theme.DARK:
|
||||
return {k: v["dark"] for k, v in Shadows.__dict__.items() if isinstance(v, dict)}
|
||||
return {k: v["light"] for k, v in Shadows.__dict__.items() if isinstance(v, dict)}
|
||||
|
||||
class StyleSheet:
|
||||
@staticmethod
|
||||
def get_base_styles(theme: Theme) -> str:
|
||||
colors = theme.get_colors()
|
||||
spacing = theme.get_spacing()
|
||||
|
||||
return f"""
|
||||
QMainWindow {{
|
||||
background-color: {colors['background']};
|
||||
}}
|
||||
|
||||
QWidget {{
|
||||
color: {colors['text']};
|
||||
font-family: 'Inter';
|
||||
}}
|
||||
|
||||
QPushButton {{
|
||||
background-color: {colors['primary']};
|
||||
border: none;
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
|
||||
color: white;
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {colors['hover']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {colors['active']};
|
||||
}}
|
||||
|
||||
QLineEdit, QTextEdit {{
|
||||
background-color: {colors['surface']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px;
|
||||
color: {colors['text']};
|
||||
}}
|
||||
|
||||
QLineEdit:focus, QTextEdit:focus {{
|
||||
border: 1px solid {colors['primary']};
|
||||
}}
|
||||
|
||||
QComboBox {{
|
||||
background-color: {colors['surface']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {spacing.TINY}px;
|
||||
padding: {spacing.SMALL}px;
|
||||
color: {colors['text']};
|
||||
}}
|
||||
|
||||
QComboBox:hover {{
|
||||
border: 1px solid {colors['primary']};
|
||||
}}
|
||||
|
||||
QLabel {{
|
||||
color: {colors['text']};
|
||||
}}
|
||||
|
||||
QScrollBar:vertical {{
|
||||
border: none;
|
||||
background-color: {colors['surface']};
|
||||
width: 10px;
|
||||
margin: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:vertical {{
|
||||
background-color: {colors['secondary']};
|
||||
min-height: 20px;
|
||||
border-radius: 5px;
|
||||
}}
|
||||
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
||||
height: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar:horizontal {{
|
||||
border: none;
|
||||
background-color: {colors['surface']};
|
||||
height: 10px;
|
||||
margin: 0px;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:horizontal {{
|
||||
background-color: {colors['secondary']};
|
||||
min-width: 20px;
|
||||
border-radius: 5px;
|
||||
}}
|
||||
|
||||
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
|
||||
width: 0px;
|
||||
}}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_custom_styles(theme: Theme) -> Dict[str, str]:
|
||||
colors = theme.get_colors()
|
||||
return {
|
||||
"primary_button": f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['primary']};
|
||||
color: white;
|
||||
}}
|
||||
""",
|
||||
"secondary_button": f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
color: {colors['primary']};
|
||||
border: 1px solid {colors['primary']};
|
||||
}}
|
||||
""",
|
||||
"danger_button": f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['error']};
|
||||
color: white;
|
||||
}}
|
||||
""",
|
||||
"success_button": f"""
|
||||
QPushButton {{
|
||||
background-color: {colors['success']};
|
||||
color: white;
|
||||
}}
|
||||
"""
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Optional
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from .theme import Theme, StyleSheet
|
||||
|
||||
class ThemeManager:
|
||||
_instance: Optional['ThemeManager'] = None
|
||||
_current_theme = Theme.DARK
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(ThemeManager, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> 'ThemeManager':
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
@property
|
||||
def current_theme(self) -> Theme:
|
||||
return self._current_theme
|
||||
|
||||
def switch_theme(self, theme: Theme):
|
||||
"""Переключение темы приложения"""
|
||||
self._current_theme = theme
|
||||
self.apply_theme()
|
||||
|
||||
def toggle_theme(self):
|
||||
"""Переключение между светлой и темной темой"""
|
||||
new_theme = Theme.LIGHT if self._current_theme == Theme.DARK else Theme.DARK
|
||||
self.switch_theme(new_theme)
|
||||
|
||||
def apply_theme(self):
|
||||
"""Применение текущей темы ко всему приложению"""
|
||||
if not QApplication.instance():
|
||||
return
|
||||
|
||||
# Применяем базовые стили
|
||||
QApplication.instance().setStyleSheet(
|
||||
StyleSheet.get_base_styles(self._current_theme)
|
||||
)
|
||||
|
||||
def get_style(self, style_name: str) -> str:
|
||||
"""Получение конкретного стиля"""
|
||||
return StyleSheet.get_base_styles(self._current_theme)
|
||||
@@ -0,0 +1,2 @@
|
||||
PyQt6>=6.4.0
|
||||
requests>=2.31.0
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
import os
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
def download_font():
|
||||
# Создаем директории если они не существуют
|
||||
resources_dir = Path("resources")
|
||||
fonts_dir = resources_dir / "fonts"
|
||||
fonts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# URL для загрузки шрифта
|
||||
font_url = "https://github.com/rsms/inter/raw/master/docs/font-files/InterVariable.ttf"
|
||||
font_path = fonts_dir / "InterVariable.ttf"
|
||||
|
||||
# Загружаем шрифт если его еще нет
|
||||
if not font_path.exists():
|
||||
print("Загрузка шрифта Inter...")
|
||||
response = requests.get(font_url)
|
||||
if response.status_code == 200:
|
||||
with open(font_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
print(f"Шрифт успешно загружен в {font_path}")
|
||||
else:
|
||||
print(f"Ошибка при загрузке шрифта: {response.status_code}")
|
||||
|
||||
def download_icons():
|
||||
# Создаем директорию для иконок
|
||||
icons_dir = Path("resources/icons")
|
||||
icons_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Словарь с иконками и их SVG содержимым
|
||||
icons = {
|
||||
"home.svg": """<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>""",
|
||||
|
||||
"image.svg": """<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>""",
|
||||
|
||||
"settings.svg": """<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>"""
|
||||
}
|
||||
|
||||
# Сохраняем иконки
|
||||
for filename, svg_content in icons.items():
|
||||
icon_path = icons_dir / filename
|
||||
if not icon_path.exists():
|
||||
print(f"Создаем иконку {filename}...")
|
||||
with open(icon_path, "w", encoding="utf-8") as f:
|
||||
f.write(svg_content)
|
||||
print(f"Иконка {filename} создана")
|
||||
|
||||
if __name__ == "__main__":
|
||||
download_font()
|
||||
download_icons()
|
||||
Reference in new issue
Block a user