desktop_app/app/ui/views/yobble_home_view.py
2025-09-26 21:35:18 +03:00

509 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame,
QStackedWidget, QSizePolicy, QGraphicsDropShadowEffect,
QGraphicsOpacityEffect, QMessageBox
)
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
from PySide6.QtGui import QColor
from app.core.theme import theme_manager
from app.core.dialogs import show_themed_messagebox
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):
REQUIRED_PERMISSIONS = {
0: "post.access", # Лента
1: "music.access" # Музыка
}
def __init__(self, username: str):
super().__init__()
self.username = username
self.setWindowTitle(f"Yobble Home - {username}")
self.setMinimumSize(360, 640)
self.permission_cache = set()
self.permissions_preloaded = False
# --- Основной макет ---
# Используем 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.search_button.clicked.connect(self.handle_search_click)
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)
self.notification_button.clicked.connect(self.handle_notification_click)
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.feed_label = QLabel("Загрузка...")
self.feed_label.setAlignment(Qt.AlignCenter)
self.content_stack.addWidget(self.feed_label)
# Музыка
self.music_label = QLabel("Загрузка...")
self.music_label.setAlignment(Qt.AlignCenter)
self.content_stack.addWidget(self.music_label)
# Чаты
self.content_stack.addWidget(QLabel("Контент Чатов"))
# Профиль
self.content_stack.addWidget(QLabel("Контент Профиля"))
def on_tab_button_clicked(self, index):
"""Обрабатывает нажатие на кнопку вкладки, проверяя права доступа."""
if index in self.REQUIRED_PERMISSIONS:
# сразу переключаем на вкладку (там уже "Загрузка...")
self.switch_tab(index)
# запускаем асинхронную проверку
asyncio.create_task(
self.check_permissions_and_switch(index, self.REQUIRED_PERMISSIONS[index])
)
else:
# Для вкладок без специальных прав доступа
self.switch_tab(index)
async def preload_permissions(self):
"""Асинхронно предзагружает права доступа без UI."""
access_token = get_current_access_token()
if not access_token:
print("[Permissions] Preload failed: No access token.")
return
success, data = await get_user_role(access_token)
if success:
user_permissions = data.get("user_permissions", [])
for permission_code in self.REQUIRED_PERMISSIONS.values():
if permission_code in user_permissions:
self.permission_cache.add(permission_code)
self.permissions_preloaded = True
print(f"[Permissions] Preloaded. Cache: {self.permission_cache}")
else:
print(f"[Permissions] Preload failed: {data}")
async def check_permissions_and_switch(self, index, permission_code):
"""Асинхронно проверяет права и переключает вкладку."""
if permission_code in self.permission_cache:
self.show_real_content(index)
self.switch_tab(index)
return
# Если предзагрузка завершена, но прав в кеше нет → запрет
if self.permissions_preloaded:
self.show_denied(index)
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)
success, data = await get_user_role(access_token)
if success and permission_code in data.get("user_permissions", []):
self.permission_cache.add(permission_code)
self.show_real_content(index)
self.switch_tab(index)
else:
self.show_denied(index)
self.switch_tab(index)
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):
"""Показывает диалоговое окно с сообщением об ошибке."""
show_themed_messagebox(
self,
QMessageBox.Warning,
localizer.translate("Ошибка доступа"),
message
)
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 show_real_content(self, index):
if index == 0:
self.feed_label.setText("Контент Ленты")
elif index == 1:
self.music_label.setText("Контент Музыки")
def show_denied(self, index):
denied = QLabel("Доступ запрещен")
denied.setAlignment(Qt.AlignCenter)
denied.setStyleSheet("font-size: 18px; color: #8e8e93;")
if index == 0:
self.content_stack.removeWidget(self.feed_label)
self.feed_label = denied
self.content_stack.insertWidget(0, denied)
elif index == 1:
self.content_stack.removeWidget(self.music_label)
self.music_label = denied
self.content_stack.insertWidget(1, denied)
def handle_search_click(self):
"""Пустышка для кнопки поиска."""
show_themed_messagebox(
self,
QMessageBox.Information,
"Поиск",
"🔍 Функция поиска пока в разработке."
)
def handle_notification_click(self):
"""Пустышка для кнопки уведомлений."""
show_themed_messagebox(
self,
QMessageBox.Information,
"Уведомления",
"🔔 Центр уведомлений пока в разработке."
)
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
);
}}
"""