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) # stub handlers for actions self.profile_view.start_chat_clicked.connect(lambda u: self.show_notification("Скоро: написать сообщение")) self.profile_view.follow_clicked.connect(lambda u: self.show_notification("Скоро: подписка")) 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 ); }} """