From 55f033ab376d97b356116941f9011480ddf04c21 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 26 Sep 2025 15:42:45 +0300 Subject: [PATCH] add burger menu --- app/core/theme.py | 5 + app/ui/views/side_menu_view.py | 334 +++++++++++++++++++++++++++++++ app/ui/views/yobble_home_view.py | 110 +++++++++- 3 files changed, 439 insertions(+), 10 deletions(-) create mode 100644 app/ui/views/side_menu_view.py diff --git a/app/core/theme.py b/app/core/theme.py index 69f0df6..f71b75a 100644 --- a/app/core/theme.py +++ b/app/core/theme.py @@ -20,5 +20,10 @@ class ThemeManager(QObject): self.settings.setValue("theme", theme) self.theme_changed.emit(self.theme) + def toggle_theme(self): + """Переключает тему между светлой и темной.""" + new_theme = "light" if self.is_dark() else "dark" + self.set_theme(new_theme) + # Глобальный экземпляр theme_manager = ThemeManager() diff --git a/app/ui/views/side_menu_view.py b/app/ui/views/side_menu_view.py new file mode 100644 index 0000000..bc2ba19 --- /dev/null +++ b/app/ui/views/side_menu_view.py @@ -0,0 +1,334 @@ +# app/ui/views/side_menu_view.py + +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, + QScrollArea, QSizePolicy, QGraphicsDropShadowEffect +) +from PySide6.QtCore import Qt, QSize, QPropertyAnimation, QEasingCurve, Property +from PySide6.QtGui import QIcon, QColor + +from app.core.theme import theme_manager + +class SideMenuButton(QPushButton): + """Кастомная кнопка для пунктов бокового меню.""" + def __init__(self, icon_path: str, text: str): + super().__init__() + self.setCursor(Qt.PointingHandCursor) + self.setObjectName("SideMenuButton") + + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 8, 10, 8) + layout.setSpacing(15) + + icon_label = QLabel(icon_path) # Используем текстовые иконки + icon_label.setObjectName("SideMenuButtonIcon") + icon_label.setFixedSize(24, 24) + icon_label.setAlignment(Qt.AlignCenter) + + text_label = QLabel(text) + text_label.setObjectName("SideMenuButtonText") + + layout.addWidget(icon_label) + layout.addWidget(text_label) + layout.addStretch() + +class SideMenuFooterButton(QPushButton): + """Кастомная кнопка для футера бокового меню.""" + def __init__(self, icon_path: str, text: str): + super().__init__() + self.setCursor(Qt.PointingHandCursor) + self.setObjectName("SideMenuFooterButton") + + layout = QVBoxLayout(self) + layout.setContentsMargins(5, 8, 5, 8) + layout.setSpacing(4) + + icon_label = QLabel(icon_path) + icon_label.setObjectName("SideMenuFooterIcon") + icon_label.setAlignment(Qt.AlignCenter) + + text_label = QLabel(text) + text_label.setObjectName("SideMenuFooterText") + text_label.setAlignment(Qt.AlignCenter) + + layout.addWidget(icon_label) + layout.addWidget(text_label) + + +class SideMenuView(QFrame): + """Виджет бокового меню.""" + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("SideMenuView") + self.setFixedWidth(280) + + # --- Основной макет --- + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # --- Компоненты --- + self.header = self._create_header() + self.account_list_container = self._create_account_list() + self.scroll_area = self._create_scroll_area() + self.footer = self._create_footer() + + main_layout.addWidget(self.header) + main_layout.addWidget(self.account_list_container) + main_layout.addWidget(self.scroll_area) + main_layout.addWidget(self.footer) + + # --- Анимация списка аккаунтов --- + self.account_list_container.setVisible(False) + self.account_list_animation = QPropertyAnimation(self.account_list_container, b"maximumHeight") + self.account_list_animation.setDuration(300) + self.account_list_animation.setEasingCurve(QEasingCurve.InOutQuart) + + # --- Состояние анимации --- + self.is_closing_animation = False + self.account_list_animation.finished.connect(self._on_animation_finished) + + self.update_styles() + theme_manager.theme_changed.connect(self.update_styles) + + def _create_header(self): + """Создает заголовок меню.""" + header_widget = QWidget() + header_layout = QVBoxLayout(header_widget) + header_layout.setContentsMargins(15, 50, 15, 10) + header_layout.setSpacing(10) + + # Верхняя часть: Аватар и кнопка темы + top_row_layout = QHBoxLayout() + + avatar_button = QPushButton("👤") + avatar_button.setObjectName("AvatarButton") + avatar_button.setFixedSize(60, 60) + + self.theme_toggle_button = QPushButton("☀️") + self.theme_toggle_button.setObjectName("ThemeToggleButton") + self.theme_toggle_button.setCursor(Qt.PointingHandCursor) + self.theme_toggle_button.clicked.connect(theme_manager.toggle_theme) + + top_row_layout.addWidget(avatar_button) + top_row_layout.addStretch() + top_row_layout.addWidget(self.theme_toggle_button) + + # Нижняя часть: Имя пользователя и кнопка раскрытия + self.account_toggle_button = QPushButton() + self.account_toggle_button.setObjectName("AccountToggleButton") + self.account_toggle_button.setCursor(Qt.PointingHandCursor) + self.account_toggle_button.clicked.connect(self.toggle_account_list) + + bottom_row_layout = QHBoxLayout(self.account_toggle_button) + + user_info_layout = QVBoxLayout() + user_info_layout.setSpacing(0) + user_info_layout.addWidget(QLabel("Your Name")) + user_info_layout.addWidget(QLabel("@yourusername")) + + self.expand_icon = QLabel("▼") + + bottom_row_layout.addLayout(user_info_layout) + bottom_row_layout.addStretch() + bottom_row_layout.addWidget(self.expand_icon) + + header_layout.addLayout(top_row_layout) + header_layout.addWidget(self.account_toggle_button) + + return header_widget + + def _create_account_list(self): + """Создает выпадающий список аккаунтов.""" + container = QWidget() + container.setObjectName("AccountListContainer") + layout = QVBoxLayout(container) + layout.setContentsMargins(15, 10, 15, 10) + layout.setSpacing(15) + + # Пример аккаунтов + accounts = [ + ("Your Name", "@yourusername", True), + ("Second Account", "@second", False) + ] + + for name, username, is_current in accounts: + # Здесь можно создать более сложный виджет для каждого аккаунта + label = QLabel(f"{name} ({username}) {'✔' if is_current else ''}") + layout.addWidget(label) + + return container + + def _create_scroll_area(self): + """Создает прокручиваемую область с пунктами меню.""" + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QFrame.NoFrame) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll_area.setObjectName("ScrollArea") + + content_widget = QWidget() + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(15, 10, 15, 10) + content_layout.setSpacing(5) + + # Секция 1 + content_layout.addWidget(SideMenuButton("👥", "People You May Like")) + content_layout.addWidget(SideMenuButton("⭐", "Fun Fest")) + content_layout.addWidget(SideMenuButton("💡", "Creator Center")) + + content_layout.addSpacing(10) + content_layout.addWidget(self._create_divider()) + content_layout.addSpacing(10) + + # Секция 2 + content_layout.addWidget(self._create_category_label("CATEGORY")) + content_layout.addWidget(SideMenuButton("📄", "Drafts")) + content_layout.addWidget(SideMenuButton("💬", "My Comments")) + + content_layout.addStretch() # Заполняет пространство внизу + scroll_area.setWidget(content_widget) + return scroll_area + + def _create_footer(self): + """Создает футер меню.""" + footer_widget = QWidget() + footer_widget.setObjectName("SideMenuFooter") + layout = QHBoxLayout(footer_widget) + layout.setContentsMargins(15, 10, 15, 20) + + layout.addWidget(SideMenuFooterButton("📱", "Scan")) + layout.addWidget(SideMenuFooterButton("❓", "Help")) + layout.addWidget(SideMenuFooterButton("⚙️", "Settings")) + + return footer_widget + + def _create_divider(self): + divider = QFrame() + divider.setFrameShape(QFrame.HLine) + divider.setObjectName("Divider") + return divider + + def _create_category_label(self, text): + label = QLabel(text) + label.setObjectName("CategoryLabel") + return label + + def _on_animation_finished(self): + """Срабатывает после завершения анимации.""" + if self.is_closing_animation: + self.account_list_container.setVisible(False) + + def toggle_account_list(self): + """Анимирует показ/скрытие списка аккаунтов.""" + self.account_list_animation.stop() + + if self.account_list_container.isVisible(): + # --- Закрытие --- + self.is_closing_animation = True + self.expand_icon.setText("▼") + end_height = 0 + else: + # --- Открытие --- + self.is_closing_animation = False + self.expand_icon.setText("▲") + self.account_list_container.setVisible(True) + end_height = self.account_list_container.sizeHint().height() + + self.account_list_animation.setStartValue(self.account_list_container.height()) + self.account_list_animation.setEndValue(end_height) + self.account_list_animation.start() + + def update_styles(self): + """Обновляет стили в зависимости от темы.""" + is_dark = theme_manager.is_dark() + self.theme_toggle_button.setText("🌙" if is_dark else "☀️") + self.setStyleSheet(self.get_stylesheet()) + + def get_stylesheet(self): + is_dark = theme_manager.is_dark() + + bg_color = "#1c1c1e" if is_dark else "#ffffff" + text_color = "#f2f2f7" if is_dark else "#000000" + secondary_text_color = "#8e8e93" if is_dark else "#888888" + divider_color = "#3c3c3c" if is_dark else "#e7e7e7" + button_hover_bg = "#2c2c2e" if is_dark else "#f0f0f0" + + return f""" + #SideMenuView {{ + background-color: {bg_color}; + }} + QPushButton {{ + border: none; + background: transparent; + text-align: left; + color: {text_color}; + }} + + /* --- Header --- */ + #AvatarButton {{ + font-size: 40px; + border-radius: 30px; + background-color: {divider_color}; + }} + #ThemeToggleButton {{ + font-size: 24px; + }} + #AccountToggleButton {{ + padding: 8px; + border-radius: 8px; + }} + #AccountToggleButton:hover {{ + background-color: {button_hover_bg}; + }} + #AccountToggleButton QLabel {{ + color: {text_color}; + }} + #AccountToggleButton > QLabel:first-child {{ /* Имя */ + font-weight: bold; + }} + #AccountToggleButton > QLabel:last-child {{ /* @username */ + color: {secondary_text_color}; + }} + + /* --- Account List --- */ + #AccountListContainer {{ + background-color: transparent; + color: {text_color}; + }} + + /* --- Menu Buttons --- */ + #SideMenuButton, #SideMenuFooterButton {{ + border-radius: 8px; + }} + #SideMenuButton:hover, #SideMenuFooterButton:hover {{ + background-color: {button_hover_bg}; + }} + #SideMenuButtonIcon, #SideMenuFooterIcon {{ + font-size: 18px; + color: {text_color}; + }} + #SideMenuButtonText, #SideMenuFooterText {{ + font-size: 14px; + color: {text_color}; + }} + #SideMenuFooterText {{ + font-size: 11px; + }} + + /* --- Other --- */ + #Divider {{ + background-color: {divider_color}; + border: none; + height: 1px; + }} + #CategoryLabel {{ + color: {secondary_text_color}; + font-size: 12px; + font-weight: bold; + padding: 5px 10px; + }} + #ScrollArea {{ + border: none; + }} + """ diff --git a/app/ui/views/yobble_home_view.py b/app/ui/views/yobble_home_view.py index f646af4..c63d8ec 100644 --- a/app/ui/views/yobble_home_view.py +++ b/app/ui/views/yobble_home_view.py @@ -1,41 +1,125 @@ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, - QStackedWidget, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect + QStackedWidget, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect, + QGraphicsOpacityEffect ) -from PySide6.QtCore import Qt, QSize +from PySide6.QtCore import Qt, QSize, QPropertyAnimation, QEasingCurve, Property from PySide6.QtGui import QIcon, QColor from app.core.theme import theme_manager +from app.ui.views.side_menu_view import SideMenuView class YobbleHomeView(QWidget): def __init__(self, username: str): super().__init__() self.username = username self.setWindowTitle(f"Yobble Home - {username}") + self.setMinimumSize(360, 640) # --- Основной макет --- - main_layout = QVBoxLayout(self) + # Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом + # Но на самом деле меню будет поверх контента + main_layout = QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) + # --- Виджет для основного контента --- + self.content_widget = QWidget() + content_layout = QVBoxLayout(self.content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(0) + # 1. Верхняя панель self.top_bar = self.create_top_bar() - main_layout.addWidget(self.top_bar) + content_layout.addWidget(self.top_bar) # 2. Центральная область контента self.content_stack = QStackedWidget() self.setup_content_pages() - main_layout.addWidget(self.content_stack, 1) + content_layout.addWidget(self.content_stack, 1) # 3. Нижняя панель навигации self.bottom_bar = self.create_bottom_bar() - main_layout.addWidget(self.bottom_bar) - - self.update_styles() # Применяем стили при инициализации + content_layout.addWidget(self.bottom_bar) - # Подключаемся к сигналу смены темы + main_layout.addWidget(self.content_widget) + + # --- Боковое меню и оверлей --- + self.setup_side_menu() + + self.update_styles() theme_manager.theme_changed.connect(self.update_styles) + def setup_side_menu(self): + """Настраивает боковое меню и оверлей.""" + # Оверлей для затемнения контента + self.overlay = QFrame(self) + self.overlay.setObjectName("Overlay") + self.overlay.hide() + self.overlay.mousePressEvent = lambda event: self.toggle_side_menu() + + # Эффект прозрачности для оверлея + self.opacity_effect = QGraphicsOpacityEffect(self.overlay) + self.overlay.setGraphicsEffect(self.opacity_effect) + + # Боковое меню + self.side_menu = SideMenuView(self) + self.side_menu.move(-self.side_menu.width(), 0) # Изначально скрыто за экраном + + # Анимация для меню + self.menu_animation = QPropertyAnimation(self.side_menu, b"pos") + self.menu_animation.setEasingCurve(QEasingCurve.InOutCubic) + self.menu_animation.setDuration(300) + + # Анимация для оверлея + self.opacity_animation = QPropertyAnimation(self.opacity_effect, b"opacity") + self.opacity_animation.setDuration(300) + + # --- Состояние анимации --- + self.is_menu_closing = False + self.opacity_animation.finished.connect(self._on_menu_animation_finished) + + def _on_menu_animation_finished(self): + """Срабатывает после завершения анимации оверлея.""" + if self.is_menu_closing: + self.overlay.hide() + + def toggle_side_menu(self): + """Показывает или скрывает боковое меню с анимацией.""" + # Останавливаем текущие анимации, чтобы избежать конфликтов + self.menu_animation.stop() + self.opacity_animation.stop() + + is_hidden = self.side_menu.pos().x() < 0 + + if is_hidden: + # --- Открытие --- + self.is_menu_closing = False + self.overlay.show() + self.menu_animation.setStartValue(self.side_menu.pos()) + self.menu_animation.setEndValue(self.side_menu.pos().__class__(0, 0)) + self.opacity_animation.setStartValue(self.opacity_effect.opacity()) + self.opacity_animation.setEndValue(0.5) + else: + # --- Закрытие --- + self.is_menu_closing = True + self.menu_animation.setStartValue(self.side_menu.pos()) + self.menu_animation.setEndValue(self.side_menu.pos().__class__(-self.side_menu.width(), 0)) + self.opacity_animation.setStartValue(self.opacity_effect.opacity()) + self.opacity_animation.setEndValue(0) + + self.menu_animation.start() + self.opacity_animation.start() + + + def resizeEvent(self, event): + """Обновляет геометрию оверлея и меню при изменении размера окна.""" + super().resizeEvent(event) + self.overlay.setGeometry(self.rect()) + if self.side_menu.pos().x() < 0: + self.side_menu.move(-self.side_menu.width(), 0) + self.side_menu.setFixedHeight(self.height()) + def update_styles(self): """Обновляет стили компонента при смене темы.""" self.setStyleSheet(self.get_stylesheet()) @@ -50,6 +134,8 @@ class YobbleHomeView(QWidget): self.burger_menu_button = QPushButton("☰") self.burger_menu_button.setObjectName("BurgerMenuButton") self.burger_menu_button.setFocusPolicy(Qt.NoFocus) + self.burger_menu_button.setCursor(Qt.PointingHandCursor) + self.burger_menu_button.clicked.connect(self.toggle_side_menu) # Подключаем сигнал top_bar_layout.addWidget(self.burger_menu_button) self.title_label = QLabel("Чаты") @@ -167,11 +253,15 @@ class YobbleHomeView(QWidget): top_bar_bg = "#2c2c2e" if is_dark else "#f5f5f5" top_bar_border = "#3c3c3c" if is_dark else "#e0e0e0" hover_color = "#d0d0d0" if not is_dark else "#444444" + overlay_color = "rgba(0, 0, 0, 0.5)" return f""" - YobbleHomeView {{ + #content_widget {{ background-color: {bg_color}; }} + #Overlay {{ + background-color: {overlay_color}; + }} /* Глобальные стили для кнопок */ QPushButton {{