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

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