diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f9d5951
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.pythonPath": "c:\\projects\\stable_api\\venv\\Scripts\\python.exe"
+}
\ No newline at end of file
diff --git a/main.py b/main.py
index 30f371f..99298a3 100644
--- a/main.py
+++ b/main.py
@@ -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())
\ No newline at end of file
+ sys.exit(app.exec())
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/modules/pages/__pycache__/home_page.cpython-310.pyc b/modules/pages/__pycache__/home_page.cpython-310.pyc
new file mode 100644
index 0000000..e10b6fc
Binary files /dev/null and b/modules/pages/__pycache__/home_page.cpython-310.pyc differ
diff --git a/modules/pages/__pycache__/models_page.cpython-310.pyc b/modules/pages/__pycache__/models_page.cpython-310.pyc
new file mode 100644
index 0000000..2935d0d
Binary files /dev/null and b/modules/pages/__pycache__/models_page.cpython-310.pyc differ
diff --git a/modules/pages/__pycache__/settings_page.cpython-310.pyc b/modules/pages/__pycache__/settings_page.cpython-310.pyc
new file mode 100644
index 0000000..c81c4dc
Binary files /dev/null and b/modules/pages/__pycache__/settings_page.cpython-310.pyc differ
diff --git a/modules/pages/home_page.py b/modules/pages/home_page.py
new file mode 100644
index 0000000..2c361ae
--- /dev/null
+++ b/modules/pages/home_page.py
@@ -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()
\ No newline at end of file
diff --git a/modules/pages/models_page.py b/modules/pages/models_page.py
new file mode 100644
index 0000000..8e06925
--- /dev/null
+++ b/modules/pages/models_page.py
@@ -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)
\ No newline at end of file
diff --git a/modules/pages/settings_page.py b/modules/pages/settings_page.py
new file mode 100644
index 0000000..f88419b
--- /dev/null
+++ b/modules/pages/settings_page.py
@@ -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()
\ No newline at end of file
diff --git a/modules/styles/__pycache__/base_window.cpython-310.pyc b/modules/styles/__pycache__/base_window.cpython-310.pyc
new file mode 100644
index 0000000..c940481
Binary files /dev/null and b/modules/styles/__pycache__/base_window.cpython-310.pyc differ
diff --git a/modules/styles/__pycache__/icons.cpython-310.pyc b/modules/styles/__pycache__/icons.cpython-310.pyc
new file mode 100644
index 0000000..3571374
Binary files /dev/null and b/modules/styles/__pycache__/icons.cpython-310.pyc differ
diff --git a/modules/styles/__pycache__/navigation.cpython-310.pyc b/modules/styles/__pycache__/navigation.cpython-310.pyc
new file mode 100644
index 0000000..4c8c562
Binary files /dev/null and b/modules/styles/__pycache__/navigation.cpython-310.pyc differ
diff --git a/modules/styles/__pycache__/page.cpython-310.pyc b/modules/styles/__pycache__/page.cpython-310.pyc
new file mode 100644
index 0000000..6ce58bb
Binary files /dev/null and b/modules/styles/__pycache__/page.cpython-310.pyc differ
diff --git a/modules/styles/__pycache__/theme.cpython-310.pyc b/modules/styles/__pycache__/theme.cpython-310.pyc
new file mode 100644
index 0000000..7e6f17a
Binary files /dev/null and b/modules/styles/__pycache__/theme.cpython-310.pyc differ
diff --git a/modules/styles/__pycache__/theme_manager.cpython-310.pyc b/modules/styles/__pycache__/theme_manager.cpython-310.pyc
new file mode 100644
index 0000000..ecfcbaf
Binary files /dev/null and b/modules/styles/__pycache__/theme_manager.cpython-310.pyc differ
diff --git a/modules/styles/base_window.py b/modules/styles/base_window.py
new file mode 100644
index 0000000..7d77e01
--- /dev/null
+++ b/modules/styles/base_window.py
@@ -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()
\ No newline at end of file
diff --git a/modules/styles/icons.py b/modules/styles/icons.py
new file mode 100644
index 0000000..74fafb4
--- /dev/null
+++ b/modules/styles/icons.py
@@ -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)
\ No newline at end of file
diff --git a/modules/styles/main.qss b/modules/styles/main.qss
new file mode 100644
index 0000000..a389e8f
--- /dev/null
+++ b/modules/styles/main.qss
@@ -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;
+}
\ No newline at end of file
diff --git a/modules/styles/navigation.py b/modules/styles/navigation.py
new file mode 100644
index 0000000..46abdcc
--- /dev/null
+++ b/modules/styles/navigation.py
@@ -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)
\ No newline at end of file
diff --git a/modules/styles/page.py b/modules/styles/page.py
new file mode 100644
index 0000000..1ab813a
--- /dev/null
+++ b/modules/styles/page.py
@@ -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()
\ No newline at end of file
diff --git a/modules/styles/theme.py b/modules/styles/theme.py
new file mode 100644
index 0000000..5973adb
--- /dev/null
+++ b/modules/styles/theme.py
@@ -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;
+ }}
+ """
+ }
\ No newline at end of file
diff --git a/modules/styles/theme_manager.py b/modules/styles/theme_manager.py
new file mode 100644
index 0000000..e2f08d3
--- /dev/null
+++ b/modules/styles/theme_manager.py
@@ -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)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..12a1b6b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+PyQt6>=6.4.0
+requests>=2.31.0
\ No newline at end of file
diff --git a/resources/fonts/InterVariable.ttf b/resources/fonts/InterVariable.ttf
new file mode 100644
index 0000000..4ab79e0
Binary files /dev/null and b/resources/fonts/InterVariable.ttf differ
diff --git a/scripts/__pycache__/download_resources.cpython-310.pyc b/scripts/__pycache__/download_resources.cpython-310.pyc
new file mode 100644
index 0000000..dd964a3
Binary files /dev/null and b/scripts/__pycache__/download_resources.cpython-310.pyc differ
diff --git a/scripts/download_resources.py b/scripts/download_resources.py
new file mode 100644
index 0000000..b08c0f6
--- /dev/null
+++ b/scripts/download_resources.py
@@ -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": """""",
+
+ "image.svg": """""",
+
+ "settings.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()
\ No newline at end of file