973 lines
42 KiB
Python
973 lines
42 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.ui.views.profile_view import ProfileView
|
||
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.on_back_clicked)
|
||
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)
|
||
self.search_results_view.result_selected.connect(self.on_search_result_selected)
|
||
|
||
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):
|
||
# Скрываем элементы, показываем поиск
|
||
if getattr(self, "_prev_content_widget", None) is None:
|
||
# Запоминаем активную страницу до любого перехода по результатам
|
||
try:
|
||
self._prev_content_widget = self.content_stack.currentWidget()
|
||
except Exception:
|
||
self._prev_content_widget = None
|
||
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 [])
|
||
print("data", data)
|
||
# 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 on_search_result_selected(self, payload: dict):
|
||
t = (payload or {}).get("type")
|
||
if t == "user":
|
||
user = (payload or {}).get("user") or {}
|
||
return self.open_profile_view(user)
|
||
# TODO: handle group/channel/message
|
||
|
||
def open_profile_view(self, user: dict):
|
||
try:
|
||
self.profile_view = ProfileView(user)
|
||
self.bottom_bar.hide()
|
||
self.burger_menu_button.hide()
|
||
self.back_button.show()
|
||
self.content_stack.addWidget(self.profile_view)
|
||
self.content_stack.setCurrentWidget(self.profile_view)
|
||
except Exception as e:
|
||
self.show_notification(str(e), is_error=True)
|
||
|
||
def close_profile_view(self):
|
||
self.content_stack.setCurrentWidget(self.search_results_view)
|
||
self.bottom_bar.show()
|
||
self.burger_menu_button.show()
|
||
self.back_button.hide()
|
||
if hasattr(self, 'profile_view') and self.profile_view:
|
||
try:
|
||
self.content_stack.removeWidget(self.profile_view)
|
||
except Exception:
|
||
pass
|
||
self.profile_view.deleteLater()
|
||
self.profile_view = None
|
||
|
||
def handle_notification_click(self):
|
||
"""Пустышка для кнопки уведомлений."""
|
||
show_themed_messagebox(
|
||
self,
|
||
QMessageBox.Information,
|
||
"Уведомления",
|
||
"🔔 Центр уведомлений пока в разработке."
|
||
)
|
||
|
||
def on_back_clicked(self):
|
||
if getattr(self, 'profile_view', None):
|
||
self.close_profile_view()
|
||
return
|
||
if getattr(self, 'chat_view', None):
|
||
self.close_chat_view()
|
||
return
|
||
|
||
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
|
||
);
|
||
}}
|
||
"""
|
||
|