desktop_app/app/ui/views/yobble_home_view.py
2025-09-27 04:01:52 +03:00

579 lines
23 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
import time
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame,
QStackedWidget, QSizePolicy, QGraphicsDropShadowEffect,
QGraphicsOpacityEffect, QMessageBox
)
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QTimer
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
self.permissions_preloaded = False
self.permissions_preloaded_last = 0.0
# --- Основной макет ---
# Используем 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.update_tab_selection(2)
# self.content_stack.setCurrentIndex(2)
self.switch_tab(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)
# Анти-спам/кулдауны для прав
self._permission_checking = set() # индекс вкладки, где сейчас идёт проверка
self._denied_recently = {} # {index: bool} — активен кулдаун после "Доступ запрещен"
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 = 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)
# важно для стабильного hover
button.setAttribute(Qt.WA_Hover, True)
button.setMouseTracking(True)
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")
# ключ: делаем детей «прозрачными» для мыши
icon_label.setAttribute(Qt.WA_TransparentForMouseEvents, True)
text_label.setAttribute(Qt.WA_TransparentForMouseEvents, True)
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(localizer.translate("Загрузка..."))
self.feed_label.setAlignment(Qt.AlignCenter)
self.content_stack.addWidget(self.feed_label)
# Музыка
self.music_label = QLabel(localizer.translate("Загрузка..."))
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)
if self.permissions_preloaded and self.preload_permissions_first==False:
now = time.time()
elapsed = now - self.permissions_preloaded_last if self.permissions_preloaded_last else float("inf")
# если прошло больше 30 секунд или системное время ушло назад → протухло
if elapsed >= 30 or elapsed < 0:
self.permissions_preloaded = False
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 = await 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 = set(data.get("user_permissions", []))
# Загружаем ВСЕ права пользователя в кэш
self.permission_cache = user_permissions
print("self.permission_cache", self.permission_cache)
# for permission_code in self.REQUIRED_PERMISSIONS.values():
# if permission_code in user_permissions:
# self.permission_cache.add(permission_code)
self.permissions_preloaded_last = time.time()
self.permissions_preloaded = True
self.preload_permissions_first = True
print(f"[Permissions] Preloaded. Cache: {self.permission_cache}")
else:
print(f"[Permissions] Preload failed: {data}")
# def update_current_tab_content(self):
# """Обновляет контент текущей активной вкладки после предзагрузки прав."""
# current_index = self.content_stack.currentIndex()
# # Проверяем, требует ли текущая вкладка прав доступа
# if current_index in self.REQUIRED_PERMISSIONS:
# permission_code = self.REQUIRED_PERMISSIONS[current_index]
# if permission_code in self.permission_cache:
# self.show_real_content(current_index)
# else:
# self.show_denied(current_index)
async def check_permissions_and_switch(self, index, permission_code):
"""Асинхронно проверяет права и переключает вкладку."""
self.preload_permissions_first = False
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 = await get_current_access_token()
if not access_token:
self.show_error_message(localizer.translate("Сессия не найдена. Пожалуйста, войдите снова."))
return
success, data = await get_user_role(access_token)
if success and permission_code in data.get("user_permissions", []):
# self.permission_cache.add(permission_code)
user_permissions = set(data.get("user_permissions", []))
self.permission_cache.update(user_permissions)
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)
button.update()
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(localizer.translate("Доступ запрещен"))
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 "#777777"
title_color = "white" if is_dark else "black"
top_bar_bg = "#2c2c2e" if is_dark else "#f5f5f5"
top_bar_border = "#3c3c3c" if is_dark else "#e0e0e0"
overlay_color = "rgba(0, 0, 0, 0.5)"
# Акцент и производные
active_hex = "#0A84FF"
active_rgb = "10, 132, 255" # для rgba()
# Hover — нейтральный, чтобы не спорил с выбранным
hover_bg = "rgba(0, 0, 0, 0.06)" if not is_dark else "rgba(255, 255, 255, 0.07)"
# Pressed — одинаково в темах
pressed_bg = f"rgba({active_rgb}, 0.36)"
# Selected — РАЗНЫЕ для светлой и тёмной тем
selected_bg = f"rgba({active_rgb}, 0.16)" if not is_dark else f"rgba({active_rgb}, 0.28)"
selected_border = f"rgba({active_rgb}, 0.24)" if not is_dark else f"rgba({active_rgb}, 0.42)"
return f"""
#content_widget {{
background-color: {bg_color};
}}
#Overlay {{
background-color: {overlay_color};
}}
/* Глобально для кнопок */
QPushButton {{
background: transparent;
border: none;
outline: none;
}}
QPushButton:focus,
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 {{
padding: 6px 8px;
border-radius: 10px;
border: 1px solid transparent; /* чтобы при selection не прыгала высота */
}}
#TabButton:hover {{
background-color: {hover_bg};
}}
#TabButton:pressed {{
background-color: {pressed_bg};
padding-top: 8px;
padding-bottom: 4px;
}}
/* Иконка/текст по умолчанию */
#TabButton #TabIcon {{ color: {text_color}; }}
#TabButton #TabText {{ color: {text_color}; }}
/* ВЫБРАННАЯ вкладка — разные тона для light/dark */
#TabButton[selected="true"] {{
background-color: {selected_bg};
border: 1px solid {selected_border};
}}
#TabButton[selected="true"] #TabIcon,
#TabButton[selected="true"] #TabText {{
color: {active_hex};
font-weight: 600;
}}
#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
);
}}
"""