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