desktop_app/app/ui/views/yobble_home_view.py
2025-10-04 03:06:09 +03:00

973 lines
42 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 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
);
}}
"""