Воркед
This commit is contained in:
1 parent
ac32c55ab9
commit
ee94d8e529
1138 files changed
+55771
-77901
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+40
-35
@@ -5,20 +5,26 @@ import requests
|
||||
import os
|
||||
|
||||
class IconType(Enum):
|
||||
# Навигация
|
||||
"""Типы иконок"""
|
||||
HOME = auto()
|
||||
MODELS = auto()
|
||||
SETTINGS = auto()
|
||||
GENERATE = auto()
|
||||
ADD = auto()
|
||||
DELETE = auto()
|
||||
REFRESH = auto()
|
||||
SAVE = auto()
|
||||
CLOSE = auto()
|
||||
SUCCESS = auto()
|
||||
|
||||
# Навигация
|
||||
MENU = "menu"
|
||||
BACK = "arrow-back"
|
||||
FORWARD = "arrow-forward"
|
||||
|
||||
# Действия
|
||||
ADD = "add"
|
||||
REMOVE = "remove"
|
||||
EDIT = "edit"
|
||||
DELETE = "delete"
|
||||
SAVE = "save"
|
||||
REFRESH = "refresh"
|
||||
SEARCH = "search"
|
||||
|
||||
# Медиа
|
||||
@@ -30,13 +36,11 @@ class IconType(Enum):
|
||||
STOP = "stop"
|
||||
|
||||
# Статусы
|
||||
SUCCESS = "check-circle"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
INFO = "info"
|
||||
|
||||
# Интерфейс
|
||||
CLOSE = "close"
|
||||
MINIMIZE = "minimize"
|
||||
MAXIMIZE = "maximize"
|
||||
RESTORE = "restore"
|
||||
@@ -46,48 +50,49 @@ class IconType(Enum):
|
||||
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
|
||||
def __init__(self):
|
||||
self.icons = {}
|
||||
self.load_icons()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Получить экземпляр менеджера иконок (Singleton)"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
cls._instance = IconManager()
|
||||
return cls._instance
|
||||
|
||||
def _download_icons(self):
|
||||
"""Загрузка иконок из интернета"""
|
||||
def load_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"
|
||||
# Сопоставление типов иконок с файлами
|
||||
icon_files = {
|
||||
IconType.HOME: "home.svg",
|
||||
IconType.MODELS: "models.svg",
|
||||
IconType.SETTINGS: "settings.svg",
|
||||
IconType.GENERATE: "generate.svg",
|
||||
IconType.ADD: "add.svg",
|
||||
IconType.DELETE: "delete.svg",
|
||||
IconType.REFRESH: "refresh.svg",
|
||||
IconType.SAVE: "save.svg",
|
||||
IconType.CLOSE: "close.svg",
|
||||
IconType.SUCCESS: "success.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))
|
||||
# Загружаем иконки
|
||||
for icon_type, filename in icon_files.items():
|
||||
icon_path = icons_dir / filename
|
||||
if icon_path.exists():
|
||||
self.icons[icon_type] = QIcon(str(icon_path))
|
||||
else:
|
||||
print(f"Иконка не найдена: {icon_path}")
|
||||
|
||||
def get_icon(self, icon_type: IconType) -> QIcon:
|
||||
"""Получение иконки по типу"""
|
||||
return self._icons.get(icon_type, QIcon())
|
||||
"""Получить иконку по типу"""
|
||||
return self.icons.get(icon_type, QIcon())
|
||||
|
||||
def get_themed_icon(self, icon_type: IconType, theme: str) -> QIcon:
|
||||
"""Получение иконки с учетом темы"""
|
||||
|
||||
+45
-18
@@ -2,15 +2,17 @@ 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
|
||||
from .style_constants import StyleConstants
|
||||
|
||||
class BasePage(QWidget):
|
||||
"""Базовый класс для страниц"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.theme_manager = ThemeManager.get_instance()
|
||||
self.setup_ui()
|
||||
self.setup_base_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Базовая настройка страницы"""
|
||||
def setup_base_ui(self):
|
||||
"""Настройка базового UI"""
|
||||
colors = self.theme_manager.current_theme.get_colors()
|
||||
self.setStyleSheet(f"""
|
||||
QWidget {{
|
||||
@@ -18,11 +20,19 @@ class BasePage(QWidget):
|
||||
}}
|
||||
""")
|
||||
|
||||
# Основной layout
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(24, 24, 24, 24)
|
||||
self.layout.setSpacing(16)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.setSpacing(0)
|
||||
|
||||
# Создаем анимации
|
||||
self.enter_animation = QPropertyAnimation(self, b"pos")
|
||||
self.enter_animation.setDuration(250)
|
||||
self.enter_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
self.exit_animation = QPropertyAnimation(self, b"pos")
|
||||
self.exit_animation.setDuration(250)
|
||||
self.exit_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
def enter_animation(self):
|
||||
"""Анимация появления страницы"""
|
||||
# Создаем группу анимаций
|
||||
@@ -78,26 +88,43 @@ class BasePage(QWidget):
|
||||
self.anim_group.start()
|
||||
|
||||
class PageContainer(QStackedWidget):
|
||||
"""Контейнер для страниц с анимацией переходов"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.pages = {}
|
||||
self.current_page = None
|
||||
|
||||
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()
|
||||
"""Показать страницу с анимацией"""
|
||||
if name not in self.pages:
|
||||
return
|
||||
|
||||
# Показываем новую страницу
|
||||
new_page = self.pages[name]
|
||||
self.setCurrentWidget(new_page)
|
||||
next_page = self.pages[name]
|
||||
|
||||
if self.current_page == next_page:
|
||||
return
|
||||
|
||||
# Запускаем анимацию появления новой страницы
|
||||
new_page.enter_animation()
|
||||
# Получаем размеры виджета
|
||||
width = self.width()
|
||||
|
||||
# Настраиваем анимации
|
||||
if self.current_page:
|
||||
# Анимация выхода текущей страницы
|
||||
self.current_page.exit_animation.setStartValue(QPoint(0, 0))
|
||||
self.current_page.exit_animation.setEndValue(QPoint(-width, 0))
|
||||
self.current_page.exit_animation.start()
|
||||
|
||||
# Анимация входа следующей страницы
|
||||
next_page.move(width, 0)
|
||||
next_page.enter_animation.setStartValue(QPoint(width, 0))
|
||||
next_page.enter_animation.setEndValue(QPoint(0, 0))
|
||||
next_page.enter_animation.start()
|
||||
|
||||
# Показываем страницу
|
||||
self.setCurrentWidget(next_page)
|
||||
self.current_page = next_page
|
||||
@@ -0,0 +1,181 @@
|
||||
from typing import Dict
|
||||
|
||||
class StyleConstants:
|
||||
# Цветовая схема
|
||||
COLORS = {
|
||||
'primary': '#7E57C2', # Основной цвет
|
||||
'secondary': '#B39DDB', # Вторичный цвет
|
||||
'accent': '#536DFE', # Акцентный цвет
|
||||
'success': '#66BB6A', # Цвет успеха
|
||||
'warning': '#FFA726', # Цвет предупреждения
|
||||
'error': '#EF5350', # Цвет ошибки
|
||||
|
||||
# Светлая тема
|
||||
'light': {
|
||||
'background': 'qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #FFFFFF, stop:1 #F5F5F5)',
|
||||
'surface': '#FFFFFF',
|
||||
'text': '#212121',
|
||||
'text_secondary': '#757575',
|
||||
'border': '#E0E0E0',
|
||||
'hover': '#F5F5F5',
|
||||
'pressed': '#EEEEEE'
|
||||
},
|
||||
|
||||
# Темная тема
|
||||
'dark': {
|
||||
'background': 'qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1A1A1A, stop:1 #2D2D2D)',
|
||||
'surface': '#2D2D2D',
|
||||
'text': '#FFFFFF',
|
||||
'text_secondary': '#B0B0B0',
|
||||
'border': '#404040',
|
||||
'hover': '#353535',
|
||||
'pressed': '#404040'
|
||||
}
|
||||
}
|
||||
|
||||
# Размеры и отступы (в пикселях)
|
||||
SPACING = {
|
||||
'xxs': 4,
|
||||
'xs': 8,
|
||||
'sm': 12,
|
||||
'md': 16,
|
||||
'lg': 24,
|
||||
'xl': 32,
|
||||
'xxl': 48
|
||||
}
|
||||
|
||||
# Радиусы скругления (в пикселях)
|
||||
RADIUS = {
|
||||
'sm': 4,
|
||||
'md': 8,
|
||||
'lg': 12,
|
||||
'xl': 16,
|
||||
'pill': 9999
|
||||
}
|
||||
|
||||
# Тени
|
||||
SHADOWS = {
|
||||
'sm': '0 2px 4px rgba(0,0,0,0.1)',
|
||||
'md': '0 4px 8px rgba(0,0,0,0.1)',
|
||||
'lg': '0 8px 16px rgba(0,0,0,0.1)'
|
||||
}
|
||||
|
||||
# Анимации
|
||||
ANIMATIONS = {
|
||||
'duration': {
|
||||
'fast': 150,
|
||||
'normal': 250,
|
||||
'slow': 350
|
||||
},
|
||||
'easing': 'cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
}
|
||||
|
||||
# Шрифты
|
||||
FONTS = {
|
||||
'family': {
|
||||
'primary': 'Inter',
|
||||
'secondary': 'Roboto'
|
||||
},
|
||||
'size': {
|
||||
'xs': '12px',
|
||||
'sm': '14px',
|
||||
'md': '16px',
|
||||
'lg': '18px',
|
||||
'xl': '24px',
|
||||
'xxl': '32px'
|
||||
},
|
||||
'weight': {
|
||||
'regular': '400',
|
||||
'medium': '500',
|
||||
'semibold': '600',
|
||||
'bold': '700'
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_base_styles(cls, theme: str = 'light') -> str:
|
||||
"""Возвращает базовые стили для приложения"""
|
||||
colors = cls.COLORS[theme]
|
||||
|
||||
return f"""
|
||||
/* Основные стили */
|
||||
QMainWindow {{
|
||||
background: {colors['background']};
|
||||
}}
|
||||
|
||||
QWidget {{
|
||||
color: {colors['text']};
|
||||
font-family: {cls.FONTS['family']['primary']};
|
||||
font-size: {cls.FONTS['size']['md']};
|
||||
}}
|
||||
|
||||
/* Кнопки */
|
||||
QPushButton {{
|
||||
background-color: {cls.COLORS['primary']};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: {cls.RADIUS['md']}px;
|
||||
padding: {cls.SPACING['sm']}px {cls.SPACING['md']}px;
|
||||
font-weight: {cls.FONTS['weight']['medium']};
|
||||
}}
|
||||
|
||||
QPushButton:hover {{
|
||||
background-color: {cls.COLORS['secondary']};
|
||||
}}
|
||||
|
||||
QPushButton:pressed {{
|
||||
background-color: {cls.COLORS['accent']};
|
||||
}}
|
||||
|
||||
/* Поля ввода */
|
||||
QLineEdit, QTextEdit {{
|
||||
background-color: {colors['surface']};
|
||||
color: {colors['text']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {cls.RADIUS['md']}px;
|
||||
padding: {cls.SPACING['sm']}px;
|
||||
selection-background-color: {cls.COLORS['secondary']};
|
||||
}}
|
||||
|
||||
QLineEdit:focus, QTextEdit:focus {{
|
||||
border-color: {cls.COLORS['primary']};
|
||||
}}
|
||||
|
||||
/* Выпадающие списки */
|
||||
QComboBox {{
|
||||
background-color: {colors['surface']};
|
||||
color: {colors['text']};
|
||||
border: 1px solid {colors['border']};
|
||||
border-radius: {cls.RADIUS['md']}px;
|
||||
padding: {cls.SPACING['sm']}px;
|
||||
}}
|
||||
|
||||
QComboBox:hover {{
|
||||
border-color: {cls.COLORS['primary']};
|
||||
}}
|
||||
|
||||
/* Полосы прокрутки */
|
||||
QScrollBar:vertical {{
|
||||
background-color: {colors['surface']};
|
||||
width: 12px;
|
||||
margin: 0;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:vertical {{
|
||||
background-color: {cls.COLORS['secondary']};
|
||||
min-height: 20px;
|
||||
border-radius: {cls.RADIUS['sm']}px;
|
||||
}}
|
||||
|
||||
QScrollBar:horizontal {{
|
||||
background-color: {colors['surface']};
|
||||
height: 12px;
|
||||
margin: 0;
|
||||
}}
|
||||
|
||||
QScrollBar::handle:horizontal {{
|
||||
background-color: {cls.COLORS['secondary']};
|
||||
min-width: 20px;
|
||||
border-radius: {cls.RADIUS['sm']}px;
|
||||
}}
|
||||
"""
|
||||
Reference in new issue
Block a user