Добавлен новый интерфейс с навигационной панелью и страницами для управления моделями, настройками и генерацией изображений. Реализованы стили для новых компонентов и добавлены функции для загрузки шрифтов и иконок. Обновлен основной класс окна для поддержки новых страниц.

This commit is contained in:
kromskii2 committed 2025-03-15 09:10:31 +07:00
1 parent df447a1ecf
commit ac32c55ab9
25 files changed
+1496 -476

No files matched your search

Binary file not shown.
+195
View File
@@ -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()
+178
View File
@@ -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)
+193
View File
@@ -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.
+75
View File
@@ -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()
+97
View File
@@ -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)
+170
View File
@@ -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;
}
+112
View File
@@ -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)
+103
View File
@@ -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()
+217
View File
@@ -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;
}}
"""
}
+46
View File
@@ -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)