925 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			925 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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 PySide6.QtWidgets import QLineEdit
 | 
						||
 | 
						||
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.ui.views.chat_list_view import ChatListView
 | 
						||
from app.ui.views.search_results_view import SearchResultsView
 | 
						||
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
 | 
						||
from app.ui.views.chat_view import ChatView
 | 
						||
from app.core.services.chat_service import get_chat_history, send_private_message
 | 
						||
from app.core.models.chat_models import PrivateMessageSendRequest, MessageItem
 | 
						||
from uuid import UUID
 | 
						||
from app.core.services.search_service import search_by_query
 | 
						||
 | 
						||
class YobbleHomeView(QWidget):
 | 
						||
    REQUIRED_PERMISSIONS = {
 | 
						||
        0: "post.access",   # Лента
 | 
						||
        1: "music.access"   # Музыка
 | 
						||
    }
 | 
						||
 | 
						||
    def __init__(self, username: str, current_user_id: UUID):
 | 
						||
        super().__init__()
 | 
						||
        self.username = username
 | 
						||
        self.current_user_id = current_user_id
 | 
						||
        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
 | 
						||
        self.chat_view = None # Placeholder for the chat view
 | 
						||
 | 
						||
        # --- Основной макет ---
 | 
						||
        # Используем 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._setup_notification_widget()
 | 
						||
 | 
						||
        self.update_styles()
 | 
						||
        theme_manager.theme_changed.connect(self.update_styles)
 | 
						||
        # Previous content page to return to after search
 | 
						||
        self._prev_content_widget = None
 | 
						||
 | 
						||
        # Анти-спам/кулдауны для прав
 | 
						||
        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 _setup_notification_widget(self):
 | 
						||
        """Настраивает виджет для всплывающих уведомлений."""
 | 
						||
        self.notification_widget = QFrame(self)
 | 
						||
        self.notification_widget.setObjectName("NotificationWidget")
 | 
						||
        
 | 
						||
        # Тень
 | 
						||
        shadow = QGraphicsDropShadowEffect(self)
 | 
						||
        shadow.setBlurRadius(20)
 | 
						||
        shadow.setColor(QColor(0, 0, 0, 80))
 | 
						||
        shadow.setOffset(0, 3)
 | 
						||
        self.notification_widget.setGraphicsEffect(shadow)
 | 
						||
        self.notification_widget.hide()
 | 
						||
 | 
						||
        layout = QHBoxLayout(self.notification_widget)
 | 
						||
        layout.setContentsMargins(15, 10, 15, 10)
 | 
						||
        self.notification_label = QLabel()
 | 
						||
        self.notification_label.setObjectName("NotificationLabel")
 | 
						||
        layout.addWidget(self.notification_label)
 | 
						||
 | 
						||
        # Эффект прозрачности для анимации
 | 
						||
        self.notification_opacity_effect = QGraphicsOpacityEffect(self.notification_widget)
 | 
						||
        self.notification_widget.setGraphicsEffect(self.notification_opacity_effect)
 | 
						||
 | 
						||
        self.notification_animation = QPropertyAnimation(self.notification_opacity_effect, b"opacity")
 | 
						||
        self.notification_animation.setDuration(300) # мс на появление/исчезание
 | 
						||
 | 
						||
        self.notification_timer = QTimer(self)
 | 
						||
        self.notification_timer.setSingleShot(True)
 | 
						||
        self.notification_timer.timeout.connect(self.hide_notification)
 | 
						||
 | 
						||
    def show_notification(self, message, is_error=True, duration=3000):
 | 
						||
        """Показывает всплывающее уведомление."""
 | 
						||
        self.notification_label.setText(message)
 | 
						||
 | 
						||
        self.notification_widget.setProperty("is_error", is_error)
 | 
						||
 | 
						||
        self.notification_widget.style().unpolish(self.notification_widget)
 | 
						||
        self.notification_widget.style().polish(self.notification_widget)
 | 
						||
 | 
						||
        # Позиционирование
 | 
						||
        self.notification_widget.adjustSize()
 | 
						||
        x = (self.width() - self.notification_widget.width()) / 2
 | 
						||
        y = self.height() - self.notification_widget.height() - self.bottom_bar.height() - 15 # Отступ снизу
 | 
						||
        self.notification_widget.move(int(x), int(y))
 | 
						||
        
 | 
						||
        self.notification_widget.show()
 | 
						||
        self.notification_widget.raise_() # Поднять поверх всех
 | 
						||
 | 
						||
        # Анимация появления
 | 
						||
        self.notification_animation.stop()
 | 
						||
        self.notification_animation.setStartValue(0.0)
 | 
						||
        self.notification_animation.setEndValue(1.0)
 | 
						||
        self.notification_animation.start()
 | 
						||
 | 
						||
        # Таймер на скрытие
 | 
						||
        self.notification_timer.start(duration)
 | 
						||
 | 
						||
    def hide_notification(self):
 | 
						||
        """Плавно скрывает уведомление."""
 | 
						||
        self.notification_animation.stop()
 | 
						||
        self.notification_animation.setStartValue(self.notification_opacity_effect.opacity())
 | 
						||
        self.notification_animation.setEndValue(0.0)
 | 
						||
        self.notification_animation.start()
 | 
						||
        
 | 
						||
        # Прячем виджет только после завершения анимации
 | 
						||
        def on_finished():
 | 
						||
            self.notification_widget.hide()
 | 
						||
            # Отключаем, чтобы не вызывался многократно
 | 
						||
            try:
 | 
						||
                self.notification_animation.finished.disconnect(on_finished)
 | 
						||
            except (TypeError, RuntimeError):
 | 
						||
                pass # Уже отключен
 | 
						||
        
 | 
						||
        self.notification_animation.finished.connect(on_finished)
 | 
						||
 | 
						||
    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())
 | 
						||
 | 
						||
        if hasattr(self, 'notification_widget') and self.notification_widget.isVisible():
 | 
						||
            self.notification_widget.adjustSize()
 | 
						||
            x = (self.width() - self.notification_widget.width()) / 2
 | 
						||
            y = self.height() - self.notification_widget.height() - self.bottom_bar.height() - 15
 | 
						||
            self.notification_widget.move(int(x), int(y))
 | 
						||
 | 
						||
    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.back_button = QPushButton("‹")
 | 
						||
        self.back_button.setObjectName("BackButton")
 | 
						||
        self.back_button.setFocusPolicy(Qt.NoFocus)
 | 
						||
        self.back_button.setCursor(Qt.PointingHandCursor)
 | 
						||
        self.back_button.clicked.connect(self.close_chat_view)
 | 
						||
        self.back_button.hide()
 | 
						||
        top_bar_layout.addWidget(self.back_button)
 | 
						||
 | 
						||
        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_input = QLineEdit()
 | 
						||
        self.search_input.setObjectName("SearchInput")
 | 
						||
        self.search_input.setPlaceholderText("Поиск…")
 | 
						||
        self.search_input.hide()
 | 
						||
        top_bar_layout.addWidget(self.search_input, 1)
 | 
						||
        # Запуск поиска по Enter
 | 
						||
        self.search_input.returnPressed.connect(self.on_search_submit)
 | 
						||
 | 
						||
        # Новые кнопки справа
 | 
						||
        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.search_close_button = QPushButton("✕")
 | 
						||
        self.search_close_button.setObjectName("SearchCloseButton")
 | 
						||
        self.search_close_button.setFocusPolicy(Qt.NoFocus)
 | 
						||
        self.search_close_button.setCursor(Qt.PointingHandCursor)
 | 
						||
        self.search_close_button.hide()
 | 
						||
        top_bar_layout.addWidget(self.search_close_button)
 | 
						||
        self.search_close_button.clicked.connect(self.exit_search_mode)
 | 
						||
 | 
						||
        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)
 | 
						||
        # Иконки для кнопок (визуальная корректировка)
 | 
						||
        try:
 | 
						||
            self.search_button.setText("🔍")
 | 
						||
            self.notification_button.setText("🔔")
 | 
						||
        except Exception:
 | 
						||
            pass
 | 
						||
 | 
						||
        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.chat_list_view = ChatListView(current_user_id=self.current_user_id)
 | 
						||
        self.chat_list_view.chat_selected.connect(self.open_chat_view)
 | 
						||
        self.content_stack.addWidget(self.chat_list_view)
 | 
						||
 | 
						||
        # Профиль
 | 
						||
        self.content_stack.addWidget(QLabel("Контент Профиля"))
 | 
						||
 | 
						||
        # Placeholder для открытого чата
 | 
						||
        self.chat_view_container = QWidget()
 | 
						||
        self.content_stack.addWidget(self.chat_view_container)
 | 
						||
 | 
						||
        # Страница результатов поиска
 | 
						||
        self.search_results_view = SearchResultsView()
 | 
						||
        self.content_stack.addWidget(self.search_results_view)
 | 
						||
 | 
						||
    def update_chat_list(self, chat_data):
 | 
						||
        """
 | 
						||
        Слот для обновления списка чатов.
 | 
						||
        Получает данные из сигнала контроллера.
 | 
						||
        """
 | 
						||
        print(f"[UI] Получены данные для обновления чатов: {len(chat_data.items)} элементов.")
 | 
						||
        # Передаем данные в виджет списка чатов для отображения
 | 
						||
        self.chat_list_view.populate_chats(chat_data.items)
 | 
						||
 | 
						||
    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.")
 | 
						||
            # Поскольку мы уже в async-контексте, который будет запущен
 | 
						||
            # в основном потоке через QMetaObject.invokeMethod или сигнал,
 | 
						||
            # можно вызвать напрямую, но сигнал надежнее.
 | 
						||
            # Здесь прямой вызов может быть небезопасен, если async-задача
 | 
						||
            # выполняется в другом потоке. Но т.к. мы используем сигнал
 | 
						||
            # в контроллере, этот код не будет вызван.
 | 
						||
            return
 | 
						||
 | 
						||
        success, data = await get_user_role(access_token, self.username)
 | 
						||
        if success:
 | 
						||
            user_permissions = set(data.get("user_permissions", []))
 | 
						||
            self.permission_cache = user_permissions
 | 
						||
            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}")
 | 
						||
            raise ConnectionError(data)
 | 
						||
        return success, data
 | 
						||
 | 
						||
    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, self.username)
 | 
						||
        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 open_chat_view(self, chat_id: UUID):
 | 
						||
        """Открывает виджет чата, загружая его историю."""
 | 
						||
        print(f"Opening chat: {chat_id}")
 | 
						||
        asyncio.create_task(self.load_and_display_chat(chat_id))
 | 
						||
 | 
						||
    async def load_and_display_chat(self, chat_id: UUID):
 | 
						||
        """Асинхронно загружает историю и отображает чат."""
 | 
						||
        token = await get_current_access_token()
 | 
						||
        if not token:
 | 
						||
            self.show_notification("Ошибка: сессия не найдена.", is_error=True)
 | 
						||
            return
 | 
						||
 | 
						||
        self.show_notification("Загрузка истории...", is_error=False, duration=1000)
 | 
						||
        success, data = await get_chat_history(self.username, token, chat_id)
 | 
						||
 | 
						||
        if success:
 | 
						||
            if self.chat_view:
 | 
						||
                self.content_stack.removeWidget(self.chat_view)
 | 
						||
                self.chat_view.deleteLater()
 | 
						||
 | 
						||
            self.chat_view = ChatView(chat_id, self.current_user_id)
 | 
						||
            self.chat_view.populate_history(data.items)
 | 
						||
            # Подключаем сигнал отправки сообщения к нашему новому методу
 | 
						||
            self.chat_view.send_message_requested.connect(self.send_message)
 | 
						||
            
 | 
						||
            # Заменяем плейсхолдер на реальный виджет
 | 
						||
            self.content_stack.removeWidget(self.chat_view_container)
 | 
						||
            self.content_stack.addWidget(self.chat_view)
 | 
						||
            self.content_stack.setCurrentWidget(self.chat_view)
 | 
						||
            
 | 
						||
            self.bottom_bar.hide()
 | 
						||
            self.burger_menu_button.hide()
 | 
						||
            self.back_button.show()
 | 
						||
        else:
 | 
						||
            self.show_notification(f"Не удалось загрузить историю: {data}", is_error=True)
 | 
						||
 | 
						||
    def send_message(self, content: str):
 | 
						||
        """Слот для отправки сообщения. Запускает асинхронную задачу."""
 | 
						||
        if not self.chat_view:
 | 
						||
            return
 | 
						||
        
 | 
						||
        chat_id = self.chat_view.chat_id
 | 
						||
        payload = PrivateMessageSendRequest(
 | 
						||
            chat_id=chat_id,
 | 
						||
            content=content,
 | 
						||
            message_type=["text"]
 | 
						||
        )
 | 
						||
        asyncio.create_task(self.do_send_message(payload))
 | 
						||
 | 
						||
    async def do_send_message(self, payload: PrivateMessageSendRequest):
 | 
						||
        """Асинхронно отправляет сообщение и обновляет UI."""
 | 
						||
        token = await get_current_access_token()
 | 
						||
        if not token:
 | 
						||
            self.show_notification("Ошибка: сессия не найдена.", is_error=True)
 | 
						||
            return
 | 
						||
 | 
						||
        success, data = await send_private_message(self.username, token, payload)
 | 
						||
 | 
						||
        if success:
 | 
						||
            # В случае успеха, создаем объект сообщения и добавляем в чат
 | 
						||
            # Используем текущее время, т.к. сервер возвращает время с UTC
 | 
						||
            from datetime import datetime
 | 
						||
            
 | 
						||
            new_message = MessageItem(
 | 
						||
                message_id=data.message_id,
 | 
						||
                chat_id=data.chat_id,
 | 
						||
                sender_id=self.current_user_id,
 | 
						||
                content=payload.content,
 | 
						||
                message_type=['text'],
 | 
						||
                is_viewed=True, # Свои сообщения считаем просмотренными
 | 
						||
                created_at=datetime.now(),
 | 
						||
                forward_metadata=None,
 | 
						||
                sender_data=None,
 | 
						||
                media_link=None,
 | 
						||
                updated_at=None
 | 
						||
            )
 | 
						||
            self.chat_view.add_message(new_message)
 | 
						||
            self.chat_view.clear_input() # Очищаем поле ввода
 | 
						||
        else:
 | 
						||
            self.show_notification(f"Ошибка отправки: {data}", is_error=True)
 | 
						||
 | 
						||
    def close_chat_view(self):
 | 
						||
        """Закрывает виджет чата и возвращается к списку."""
 | 
						||
        self.content_stack.setCurrentWidget(self.chat_list_view)
 | 
						||
        self.bottom_bar.show()
 | 
						||
        self.burger_menu_button.show()
 | 
						||
        self.back_button.hide()
 | 
						||
        if self.chat_view:
 | 
						||
            self.content_stack.removeWidget(self.chat_view)
 | 
						||
            self.chat_view.deleteLater()
 | 
						||
            self.chat_view = None
 | 
						||
            # Возвращаем плейсхолдер
 | 
						||
            self.chat_view_container = QWidget()
 | 
						||
            self.content_stack.addWidget(self.chat_view_container)
 | 
						||
 | 
						||
    def show_error_message(self, message):
 | 
						||
        """Показывает всплывающее уведомление об ошибке."""
 | 
						||
        self.show_notification(message, is_error=True)
 | 
						||
 | 
						||
    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_search_click(self):
 | 
						||
        """Включает режим поиска: показывает поле и крестик."""
 | 
						||
        self.enter_search_mode()
 | 
						||
 | 
						||
    def enter_search_mode(self):
 | 
						||
        # Скрываем элементы, показываем поиск
 | 
						||
        self.title_label.hide()
 | 
						||
        self.search_button.hide()
 | 
						||
        self.notification_button.hide()
 | 
						||
        # Показываем поле ввода и кнопку закрытия
 | 
						||
        self.search_input.show()
 | 
						||
        self.search_close_button.show()
 | 
						||
        self.search_input.setFocus()
 | 
						||
 | 
						||
    def exit_search_mode(self):
 | 
						||
        # Прячем поле поиска, очищаем
 | 
						||
        self.search_input.clear()
 | 
						||
        self.search_input.hide()
 | 
						||
        self.search_close_button.hide()
 | 
						||
        # Возвращаем элементы
 | 
						||
        self.title_label.show()
 | 
						||
        self.search_button.show()
 | 
						||
        self.notification_button.show()
 | 
						||
        # Вернуть прежний экран, если он был сохранён
 | 
						||
        try:
 | 
						||
            if getattr(self, "_prev_content_widget", None) is not None:
 | 
						||
                self.content_stack.setCurrentWidget(self._prev_content_widget)
 | 
						||
        finally:
 | 
						||
            self._prev_content_widget = None
 | 
						||
 | 
						||
    def on_search_submit(self):
 | 
						||
        """Обработчик Enter в поле поиска."""
 | 
						||
        query = (self.search_input.text() or "").strip()
 | 
						||
        if len(query) < 1:
 | 
						||
            self.show_notification(localizer.translate("Введите минимум 1 символ"), is_error=True)
 | 
						||
            return
 | 
						||
        # Запускаем асинхронный поиск
 | 
						||
        asyncio.ensure_future(self._do_search(query))
 | 
						||
 | 
						||
    async def _do_search(self, query: str):
 | 
						||
        """Вызов серверного поиска и показ краткого результата."""
 | 
						||
        # Получаем текущие учётные данные
 | 
						||
        login = self.username
 | 
						||
        token = await get_current_access_token()
 | 
						||
        if not token:
 | 
						||
            self.show_notification(localizer.translate("Не найден токен авторизации"), is_error=True)
 | 
						||
            return
 | 
						||
 | 
						||
        ok, data_or_error = await search_by_query(login, token, query)
 | 
						||
        if not ok:
 | 
						||
            self.show_notification(str(data_or_error), is_error=True)
 | 
						||
            return
 | 
						||
 | 
						||
        data = data_or_error
 | 
						||
        users_cnt = len(getattr(data, 'users', []) or [])
 | 
						||
        groups_cnt = len(getattr(data, 'groups', []) or [])
 | 
						||
        channels_cnt = len(getattr(data, 'channels', []) or [])
 | 
						||
        messages_cnt = len(getattr(data, 'messages', []) or [])
 | 
						||
        # self.show_notification(localizer.translate(
 | 
						||
        #     f"Найдено: пользователи {users_cnt}, беседы {groups_cnt}, паблики {channels_cnt}, сообщения {messages_cnt}"
 | 
						||
        # ))
 | 
						||
 | 
						||
        # Показать результаты на отдельной странице
 | 
						||
        try:
 | 
						||
            # Сохраняем текущую страницу перед переходом к результатам
 | 
						||
            if getattr(self, "_prev_content_widget", None) is None:
 | 
						||
                self._prev_content_widget = self.content_stack.currentWidget()
 | 
						||
            self.search_results_view.populate(data)
 | 
						||
            self.content_stack.setCurrentWidget(self.search_results_view)
 | 
						||
        except Exception:
 | 
						||
            pass
 | 
						||
 | 
						||
    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};
 | 
						||
            }}
 | 
						||
 | 
						||
            /* --- Уведомления --- */
 | 
						||
            #NotificationWidget {{
 | 
						||
                border-radius: 12px;
 | 
						||
                background-color: {"#333" if is_dark else "#FFF"};
 | 
						||
                border: 1px solid {"#444" if is_dark else "#E0E0E0"};
 | 
						||
            }}
 | 
						||
            #NotificationWidget[is_error="true"] {{
 | 
						||
                background-color: {"#D32F2F" if is_dark else "#f44336"};
 | 
						||
                border: 1px solid {"#C62828" if is_dark else "#E53935"};
 | 
						||
            }}
 | 
						||
            #NotificationWidget[is_error="false"] {{
 | 
						||
                background-color: {"#388E3C" if is_dark else "#4CAF50"};
 | 
						||
                border: 1px solid {"#2E7D32" if is_dark else "#43A047"};
 | 
						||
            }}
 | 
						||
            #NotificationLabel {{
 | 
						||
                color: white;
 | 
						||
                font-size: 14px;
 | 
						||
                background: transparent;
 | 
						||
            }}
 | 
						||
            /* --- Конец Уведомлений --- */
 | 
						||
 | 
						||
            /* Глобально для кнопок */
 | 
						||
            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;
 | 
						||
            }}
 | 
						||
            #SearchButton:hover,
 | 
						||
            #SearchCloseButton:hover {{
 | 
						||
                background-color: {hover_bg};
 | 
						||
                border-radius: 6px;
 | 
						||
            }}
 | 
						||
            #SearchButton:pressed,
 | 
						||
            #SearchCloseButton:pressed {{
 | 
						||
                background-color: {pressed_bg};
 | 
						||
                border-radius: 6px;
 | 
						||
            }}
 | 
						||
            #BackButton {{
 | 
						||
                font-size: 28px;
 | 
						||
                font-weight: bold;
 | 
						||
            }}
 | 
						||
            #TitleLabel {{
 | 
						||
                font-size: 18px;
 | 
						||
                font-weight: bold;
 | 
						||
                color: {title_color};
 | 
						||
                border: none;
 | 
						||
                outline: none;
 | 
						||
                background-color: transparent;
 | 
						||
            }}
 | 
						||
            #SearchInput {{
 | 
						||
                border-radius: 8px;
 | 
						||
                padding: 6px 10px;
 | 
						||
                font-size: 14px;
 | 
						||
                color: {title_color};
 | 
						||
                background-color: {bar_bg_color};
 | 
						||
                border: 1px solid {top_bar_border};
 | 
						||
            }}
 | 
						||
            #SearchCloseButton {{
 | 
						||
                font-size: 18px;
 | 
						||
                padding: 5px;
 | 
						||
                color: {title_color};
 | 
						||
                background: 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
 | 
						||
                );
 | 
						||
            }}
 | 
						||
        """
 | 
						||
 |