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

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

+3
View File
@@ -0,0 +1,3 @@
{
"python.pythonPath": "c:\\projects\\stable_api\\venv\\Scripts\\python.exe"
}
+54 -476
View File
@@ -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.
+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)
+2
View File
@@ -0,0 +1,2 @@
PyQt6>=6.4.0
requests>=2.31.0
Binary file not shown.
+51
View File
@@ -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()