425 lines
17 KiB
Python
425 lines
17 KiB
Python
import asyncio
|
||
from PySide6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame,
|
||
QStackedWidget, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect,
|
||
QGraphicsOpacityEffect, QMessageBox
|
||
)
|
||
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
|
||
from app.core.services.auth_service import get_user_role
|
||
from app.core.database import get_current_access_token
|
||
from app.core.localizer import localizer
|
||
|
||
class YobbleHomeView(QWidget):
|
||
def __init__(self, username: str):
|
||
super().__init__()
|
||
self.username = username
|
||
self.setWindowTitle(f"Yobble Home - {username}")
|
||
self.setMinimumSize(360, 640)
|
||
self.permission_cache = set()
|
||
|
||
# --- Основной макет ---
|
||
# Используем 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()
|
||
content_layout.addWidget(self.top_bar)
|
||
|
||
# 2. Центральная область контента
|
||
self.content_stack = QStackedWidget()
|
||
self.setup_content_pages()
|
||
content_layout.addWidget(self.content_stack, 1)
|
||
|
||
# 3. Нижняя панель навигации
|
||
self.bottom_bar = self.create_bottom_bar()
|
||
self.content_stack.setCurrentIndex(2)
|
||
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())
|
||
|
||
def create_top_bar(self):
|
||
"""Создает верхнюю панель с меню и заголовком."""
|
||
top_bar_widget = QWidget()
|
||
top_bar_widget.setObjectName("TopBar")
|
||
top_bar_layout = QHBoxLayout(top_bar_widget)
|
||
top_bar_layout.setContentsMargins(10, 5, 10, 5)
|
||
|
||
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("Чаты")
|
||
self.title_label.setObjectName("TitleLabel")
|
||
top_bar_layout.addWidget(self.title_label)
|
||
top_bar_layout.addStretch()
|
||
|
||
# Новые кнопки справа
|
||
self.search_button = QPushButton("🔍")
|
||
self.search_button.setObjectName("SearchButton")
|
||
self.search_button.setFocusPolicy(Qt.NoFocus)
|
||
self.search_button.setCursor(Qt.PointingHandCursor)
|
||
top_bar_layout.addWidget(self.search_button)
|
||
|
||
self.notification_button = QPushButton("🔔")
|
||
self.notification_button.setObjectName("NotificationButton")
|
||
self.notification_button.setFocusPolicy(Qt.NoFocus)
|
||
self.notification_button.setCursor(Qt.PointingHandCursor)
|
||
top_bar_layout.addWidget(self.notification_button)
|
||
|
||
return top_bar_widget
|
||
|
||
def create_bottom_bar(self):
|
||
"""Создает нижнюю панель навигации в стиле SwiftUI."""
|
||
bottom_bar_widget = QWidget()
|
||
bottom_bar_widget.setObjectName("BottomBar")
|
||
|
||
bottom_bar_layout = QHBoxLayout(bottom_bar_widget)
|
||
bottom_bar_layout.setContentsMargins(10, 0, 10, 0)
|
||
bottom_bar_layout.setSpacing(10)
|
||
|
||
btn_feed = self.create_tab_button("☰", "Лента", 0)
|
||
btn_music = self.create_tab_button("🎵", "Музыка", 1)
|
||
btn_create = self.create_create_button()
|
||
btn_chats = self.create_tab_button("💬", "Чаты", 2)
|
||
btn_profile = self.create_tab_button("👤", "Лицо", 3)
|
||
|
||
bottom_bar_layout.addWidget(btn_feed)
|
||
bottom_bar_layout.addWidget(btn_music)
|
||
bottom_bar_layout.addWidget(btn_create)
|
||
bottom_bar_layout.addWidget(btn_chats)
|
||
bottom_bar_layout.addWidget(btn_profile)
|
||
|
||
for btn in [btn_feed, btn_music, btn_chats, btn_profile]:
|
||
btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||
btn.setFocusPolicy(Qt.NoFocus)
|
||
|
||
btn_create.setFocusPolicy(Qt.NoFocus)
|
||
|
||
self.update_tab_selection(2)
|
||
return bottom_bar_widget
|
||
|
||
def create_tab_button(self, icon_text, text, index):
|
||
button = QPushButton()
|
||
button.setObjectName("TabButton")
|
||
button.setCursor(Qt.PointingHandCursor)
|
||
button.setFocusPolicy(Qt.NoFocus)
|
||
|
||
layout = QVBoxLayout(button)
|
||
layout.setContentsMargins(0, 5, 0, 5)
|
||
layout.setSpacing(4)
|
||
|
||
icon_label = QLabel(icon_text)
|
||
icon_label.setAlignment(Qt.AlignCenter)
|
||
icon_label.setObjectName("TabIcon")
|
||
|
||
text_label = QLabel(text)
|
||
text_label.setAlignment(Qt.AlignCenter)
|
||
text_label.setObjectName("TabText")
|
||
|
||
layout.addWidget(icon_label)
|
||
layout.addWidget(text_label)
|
||
|
||
button.setProperty("tab_index", index)
|
||
button.clicked.connect(lambda: self.on_tab_button_clicked(index))
|
||
return button
|
||
|
||
def create_create_button(self):
|
||
button = QPushButton("+")
|
||
button.setObjectName("CreateButton")
|
||
button.setFixedSize(56, 56)
|
||
button.setCursor(Qt.PointingHandCursor)
|
||
button.setFocusPolicy(Qt.NoFocus)
|
||
|
||
shadow = QGraphicsDropShadowEffect(self)
|
||
shadow.setBlurRadius(18)
|
||
shadow.setColor(QColor(0, 0, 0, 100))
|
||
shadow.setOffset(0, 3)
|
||
button.setGraphicsEffect(shadow)
|
||
|
||
return button
|
||
|
||
def setup_content_pages(self):
|
||
self.content_stack.addWidget(QLabel("Контент Ленты"))
|
||
self.content_stack.addWidget(QLabel("Контент Музыки"))
|
||
self.content_stack.addWidget(QLabel("Контент Чатов"))
|
||
self.content_stack.addWidget(QLabel("Контент Профиля"))
|
||
|
||
def on_tab_button_clicked(self, index):
|
||
"""Обрабатывает нажатие на кнопку вкладки, проверяя права доступа."""
|
||
|
||
required_permissions = {
|
||
0: "post.access", # Лента
|
||
1: "music.access" # Музыка
|
||
}
|
||
|
||
if index in required_permissions:
|
||
# Запускаем асинхронную проверку в существующем цикле событий
|
||
asyncio.create_task(self.check_permissions_and_switch(index, required_permissions[index]))
|
||
else:
|
||
# Для вкладок без специальных прав доступа
|
||
self.switch_tab(index)
|
||
|
||
async def check_permissions_and_switch(self, index, permission_code):
|
||
"""Асинхронно проверяет права и переключает вкладку."""
|
||
if permission_code in self.permission_cache:
|
||
self.switch_tab(index)
|
||
return
|
||
|
||
access_token = get_current_access_token()
|
||
if not access_token:
|
||
self.show_error_message(localizer.translate("Сессия не найдена. Пожалуйста, войдите снова."))
|
||
return
|
||
|
||
success, data = await get_user_role(access_token)
|
||
print("data", data)
|
||
|
||
if success and permission_code in data.get("user_permissions", []):
|
||
self.permission_cache.add(permission_code)
|
||
self.switch_tab(index)
|
||
else:
|
||
error_message = data if not success else localizer.translate("У вас нет прав для доступа к этому разделу.")
|
||
self.show_error_message(error_message)
|
||
|
||
def switch_tab(self, index):
|
||
"""Переключает на указанную вкладку."""
|
||
self.content_stack.setCurrentIndex(index)
|
||
self.update_tab_selection(index)
|
||
titles = ["Лента", "Музыка", "Чаты", "Лицо"]
|
||
self.title_label.setText(titles[index])
|
||
|
||
def show_error_message(self, message):
|
||
"""Показывает диалоговое окно с сообщением об ошибке."""
|
||
msg_box = QMessageBox(self)
|
||
msg_box.setIcon(QMessageBox.Warning)
|
||
msg_box.setText(message)
|
||
msg_box.setWindowTitle(localizer.translate("Ошибка доступа"))
|
||
msg_box.setStandardButtons(QMessageBox.Ok)
|
||
msg_box.exec()
|
||
|
||
def update_tab_selection(self, selected_index):
|
||
if not hasattr(self, 'bottom_bar'):
|
||
return
|
||
|
||
for button in self.bottom_bar.findChildren(QPushButton):
|
||
is_tab_button = button.property("tab_index") is not None
|
||
if is_tab_button:
|
||
button.setProperty("selected", button.property("tab_index") == selected_index)
|
||
button.style().unpolish(button)
|
||
button.style().polish(button)
|
||
|
||
def get_stylesheet(self):
|
||
"""Возвращает QSS стили для компонента в зависимости от темы."""
|
||
is_dark = theme_manager.is_dark()
|
||
|
||
# Цветовая палитра
|
||
bg_color = "#1c1c1e" if is_dark else "white"
|
||
bar_bg_color = "#2c2c2e" if is_dark else "#f8f8f8"
|
||
bar_border_color = "#3c3c3c" if is_dark else "#e7e7e7"
|
||
text_color = "#8e8e93" if is_dark else "#888888"
|
||
title_color = "white" if is_dark else "black"
|
||
active_color = "#0A84FF"
|
||
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"""
|
||
#content_widget {{
|
||
background-color: {bg_color};
|
||
}}
|
||
#Overlay {{
|
||
background-color: {overlay_color};
|
||
}}
|
||
|
||
/* Глобальные стили для кнопок */
|
||
QPushButton {{
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
}}
|
||
QPushButton:focus,
|
||
QPushButton:pressed,
|
||
QPushButton:checked {{
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
}}
|
||
|
||
/* Верхняя панель */
|
||
#TopBar {{
|
||
background-color: {top_bar_bg};
|
||
border-bottom: 1px solid {top_bar_border};
|
||
}}
|
||
#TopBar QPushButton {{
|
||
font-size: 22px;
|
||
border: none;
|
||
padding: 5px;
|
||
color: {title_color};
|
||
background: transparent;
|
||
}}
|
||
#TitleLabel {{
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: {title_color};
|
||
border: none;
|
||
outline: none;
|
||
background-color: transparent;
|
||
}}
|
||
|
||
/* Нижняя панель */
|
||
#BottomBar {{
|
||
background-color: {bar_bg_color};
|
||
border-top: 1px solid {bar_border_color};
|
||
padding-top: 5px;
|
||
padding-bottom: 15px;
|
||
}}
|
||
|
||
/* Кнопки вкладок */
|
||
#TabButton {{
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
padding: 5px;
|
||
}}
|
||
#TabButton:hover {{
|
||
background-color: {hover_color};
|
||
border-radius: 6px;
|
||
}}
|
||
#TabButton #TabIcon {{ color: {text_color}; }}
|
||
#TabButton #TabText {{ color: {text_color}; }}
|
||
|
||
#TabButton[selected="true"] #TabIcon,
|
||
#TabButton[selected="true"] #TabText {{
|
||
color: {active_color};
|
||
}}
|
||
#TabIcon, #TabText {{
|
||
border: none;
|
||
outline: none;
|
||
background-color: transparent;
|
||
}}
|
||
#TabIcon {{ font-size: 22px; }}
|
||
#TabText {{ font-size: 12px; }}
|
||
|
||
/* Центральная кнопка "Создать" */
|
||
#CreateButton {{
|
||
color: white;
|
||
font-size: 30px;
|
||
font-weight: 300;
|
||
border: none;
|
||
border-radius: 28px;
|
||
background-color: qlineargradient(
|
||
x1: 0, y1: 0, x2: 0, y2: 1,
|
||
stop: 0 #007AFF, stop: 1 #0056b3
|
||
);
|
||
margin-bottom: 20px;
|
||
}}
|
||
#CreateButton:hover {{
|
||
background-color: qlineargradient(
|
||
x1: 0, y1: 0, x2: 0, y2: 1,
|
||
stop: 0 #0088FF, stop: 1 #0066c3
|
||
);
|
||
}}
|
||
#CreateButton:pressed {{
|
||
background-color: qlineargradient(
|
||
x1: 0, y1: 0, x2: 0, y2: 1,
|
||
stop: 0 #0056b3, stop: 1 #004493
|
||
);
|
||
}}
|
||
"""
|