Compare commits

..

No commits in common. "e8427ceb5d9c42726045610d846bc4077ba5b443" and "35ad95b8f7a5bbab300779a791d0cdb477dab049" have entirely different histories.

7 changed files with 223 additions and 226 deletions

View File

@ -63,7 +63,7 @@ class MainController(QStackedWidget):
print("[Sync] Запускаем предзагрузку прав доступа...") print("[Sync] Запускаем предзагрузку прав доступа...")
try: try:
# Запускаем асинхронную функцию в текущем потоке # Запускаем асинхронную функцию в текущем потоке
asyncio.run(self.yobble_home_view.preload_permissions()) # TODO добавить из sqlite asyncio.run(self.yobble_home_view.preload_permissions())
print("[Sync] Предзагрузка прав доступа завершена.") print("[Sync] Предзагрузка прав доступа завершена.")
except Exception as e: except Exception as e:
print(f"[Sync] Ошибка во время предзагрузки прав: {e}") print(f"[Sync] Ошибка во время предзагрузки прав: {e}")

View File

@ -1,5 +1,4 @@
import sqlite3 import sqlite3
import aiosqlite
import os import os
from datetime import datetime from datetime import datetime
@ -50,37 +49,18 @@ def init_db():
conn.commit() conn.commit()
conn.close() conn.close()
def add_session(login, access_token, refresh_token, update_existing=False): def add_session(login, access_token, refresh_token):
"""Добавляет новую сессию или обновляет существующую.""" """Добавляет новую сессию или обновляет существующую."""
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
# REPLACE INTO - удобный способ для вставки или обновления
if update_existing:
# Обновляем существующую сессию по access_token
cursor.execute('''
UPDATE sessions
SET access_token = ?, refresh_token = ?, created_at = ?
WHERE access_token = ?
''', (access_token, refresh_token, datetime.now(), access_token))
else:
# Вставляем новую или заменяем существующую по логину
cursor.execute(''' cursor.execute('''
INSERT OR REPLACE INTO sessions (login, access_token, refresh_token, created_at) INSERT OR REPLACE INTO sessions (login, access_token, refresh_token, created_at)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (login, access_token, refresh_token, datetime.now())) ''', (login, access_token, refresh_token, datetime.now()))
conn.commit() conn.commit()
conn.close() conn.close()
def logout(access_token: str):
"""Удаляет сессию по токену доступа."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM sessions WHERE access_token = ?', (access_token,))
conn.commit()
conn.close()
def get_session(login: str): def get_session(login: str):
"""Получает сессию по логину.""" """Получает сессию по логину."""
conn = get_connection() conn = get_connection()
@ -99,6 +79,13 @@ def get_all_sessions():
conn.close() conn.close()
return sessions return sessions
def delete_session(login: str):
"""Удаляет сессию по логину."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM sessions WHERE login = ?', (login,))
conn.commit()
conn.close()
def get_last_login(): def get_last_login():
"""Получает логин последнего вошедшего пользователя.""" """Получает логин последнего вошедшего пользователя."""
@ -117,26 +104,14 @@ def set_last_login(login: str):
conn.commit() conn.commit()
conn.close() conn.close()
# def get_current_access_token(): def get_current_access_token():
# """ """
# Получает access_token для последней активной сессии. Получает access_token для последней активной сессии.
# :return: access_token или None, если сессия не найдена. :return: access_token или None, если сессия не найдена.
# """ """
# last_login = get_last_login() last_login = get_last_login()
# if not last_login: if not last_login:
# return None
# session = get_session(last_login)
# return session['access_token'] if session else None
async def get_current_access_token():
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute("SELECT value FROM app_state WHERE key = 'last_login'") as cur:
row = await cur.fetchone()
if not row:
return None return None
last_login = row[0]
async with db.execute('SELECT access_token FROM sessions WHERE login = ?', (last_login,)) as cur: session = get_session(last_login)
row = await cur.fetchone() return session['access_token'] if session else None
return row[0] if row else None

View File

@ -1,7 +1,7 @@
import httpx import httpx
import asyncio import asyncio
from app.core import config from app.core import config
from app.core.database import add_session, logout, get_session from app.core.database import add_session
from app.core.localizer import localizer from app.core.localizer import localizer
async def login(login, password): async def login(login, password):
@ -91,52 +91,12 @@ async def register(login, password, invite=None):
return False, f"{localizer.translate('Произошла ошибка')}: {e}" return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def refresh_token(access_token: str, refresh_token: str): async def get_user_role(access_token: str):
"""
Обновляет токен доступа, используя токен обновления.
:param access_token: Истекший токен доступа
:param refresh_token: Токен обновления
:return: Кортеж (успех: bool, данные: dict | str)
"""
url = f"{config.BASE_URL}/v1/auth/token/refresh"
payload = {"access_token": access_token, "refresh_token": refresh_token}
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.post(url, json=payload)
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
token_data = data["data"]
# Обновляем сессию с новыми токенами
add_session(
login=None, # Логин не требуется для обновления
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"],
update_existing=True
)
return True, token_data
else:
return False, data.get("detail", "Unknown error")
elif response.status_code == 401:
return False, "Refresh token is invalid or expired"
else:
return False, f"Server error: {response.status_code}"
except httpx.RequestError as e:
return False, f"Network error: {e}"
except Exception as e:
return False, f"An error occurred: {e}"
async def get_user_role(access_token: str, login: str):
""" """
Получает роль и права пользователя по токену доступа. Получает роль и права пользователя по токену доступа.
В случае истечения срока действия токена, пытается его обновить.
:param access_token: Токен доступа пользователя
:return: Кортеж (успех: bool, данные: UserRoleData | str)
""" """
url = f"{config.BASE_URL}/v1/user/role" url = f"{config.BASE_URL}/v1/user/role"
headers = {"Authorization": f"Bearer {access_token}"} headers = {"Authorization": f"Bearer {access_token}"}
@ -147,34 +107,32 @@ async def get_user_role(access_token: str, login: str):
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
return (True, data['data']) if data.get("status") == "fine" else (False, data.get("detail", "Unknown error")) if data.get("status") == "fine":
# Здесь можно добавить валидацию через Pydantic, если необходимо
# from app.core.models.user_models import UserRoleData
# user_role_data = UserRoleData(**data['data'])
return True, data['data']
else:
return False, data.get("detail", localizer.translate("Неизвестная ошибка ответа"))
elif response.status_code == 401: elif response.status_code == 401:
# Токен истек, пытаемся обновить return False, localizer.translate("Токен недействителен или истек")
session = get_session(login) elif response.status_code == 404:
if not session or not session['refresh_token']: return False, localizer.translate("Пользователь не найден")
return False, "No refresh token found"
refresh_success, refresh_data = await refresh_token(access_token, session['refresh_token'])
if refresh_success:
# Повторяем запрос с новым токеном
new_access_token = refresh_data['access_token']
headers["Authorization"] = f"Bearer {new_access_token}"
response = await client.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
return (True, data['data']) if data.get("status") == "fine" else (False, "Failed to get role after refresh")
# Если обновление не удалось, выходим из системы
logout(access_token)
return False, "Session expired, please log in again"
else: else:
return False, f"Server error: {response.status_code}" return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
except httpx.RequestError as e: except httpx.RequestError as e:
return False, f"Network error: {e}" return False, f"{localizer.translate('Ошибка сети')}: {e}"
except Exception as e: except Exception as e:
return False, f"An error occurred: {e}" return False, f"{localizer.translate('Произошла ошибка')}: {e}"
# Пример использования (для тестирования)
async def main():
# Замените на реальные данные для теста
success, message = await login("testuser", "testpassword")
print(f"Результат входа: {success}, Сообщение: {message}")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -30,9 +30,6 @@
"Регистрация в данный момент отключена.": "Registration is currently disabled.", "Регистрация в данный момент отключена.": "Registration is currently disabled.",
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Data validation failed. Check username and password length.", "Данные не прошли валидацию. Проверьте длину логина и пароля.": "Data validation failed. Check username and password length.",
"Загрузка...": "Загрузка...",
"Доступ запрещен": "Access denied",
"Login must be between 3 and 32 characters long": "Login must be between 3 and 32 characters long", "Login must be between 3 and 32 characters long": "Login must be between 3 and 32 characters long",
"Login must not contain whitespace characters": "Login must not contain whitespace characters", "Login must not contain whitespace characters": "Login must not contain whitespace characters",
"Login must not start with an underscore": "Login must not start with an underscore", "Login must not start with an underscore": "Login must not start with an underscore",

View File

@ -30,9 +30,6 @@
"Регистрация в данный момент отключена.": "Регистрация в данный момент отключена.", "Регистрация в данный момент отключена.": "Регистрация в данный момент отключена.",
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Данные не прошли валидацию. Проверьте длину логина и пароля.", "Данные не прошли валидацию. Проверьте длину логина и пароля.": "Данные не прошли валидацию. Проверьте длину логина и пароля.",
"Загрузка...": "Loading...",
"Доступ запрещен": "Доступ запрещен",
"Login must be between 3 and 32 characters long": "Логин должен быть от 3 до 32 символов", "Login must be between 3 and 32 characters long": "Логин должен быть от 3 до 32 символов",
"Login must not contain whitespace characters": "Логин не должен содержать пробелы", "Login must not contain whitespace characters": "Логин не должен содержать пробелы",
"Login must not start with an underscore": "Логин не должен начинаться с символа подчёркивания", "Login must not start with an underscore": "Логин не должен начинаться с символа подчёркивания",

View File

@ -1,11 +1,10 @@
import asyncio import asyncio
import time
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame,
QStackedWidget, QSizePolicy, QGraphicsDropShadowEffect, QStackedWidget, QSizePolicy, QGraphicsDropShadowEffect,
QGraphicsOpacityEffect, QMessageBox QGraphicsOpacityEffect, QMessageBox
) )
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QTimer from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
from PySide6.QtGui import QColor from PySide6.QtGui import QColor
from app.core.theme import theme_manager from app.core.theme import theme_manager
@ -28,8 +27,6 @@ class YobbleHomeView(QWidget):
self.setMinimumSize(360, 640) self.setMinimumSize(360, 640)
self.permission_cache = set() self.permission_cache = set()
self.permissions_preloaded = False self.permissions_preloaded = False
self.permissions_preloaded = False
self.permissions_preloaded_last = 0.0
# --- Основной макет --- # --- Основной макет ---
# Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом # Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом
@ -55,9 +52,7 @@ class YobbleHomeView(QWidget):
# 3. Нижняя панель навигации # 3. Нижняя панель навигации
self.bottom_bar = self.create_bottom_bar() self.bottom_bar = self.create_bottom_bar()
# self.update_tab_selection(2) self.content_stack.setCurrentIndex(2)
# self.content_stack.setCurrentIndex(2)
self.switch_tab(2)
content_layout.addWidget(self.bottom_bar) content_layout.addWidget(self.bottom_bar)
main_layout.addWidget(self.content_widget) main_layout.addWidget(self.content_widget)
@ -68,11 +63,6 @@ class YobbleHomeView(QWidget):
self.update_styles() self.update_styles()
theme_manager.theme_changed.connect(self.update_styles) theme_manager.theme_changed.connect(self.update_styles)
# Анти-спам/кулдауны для прав
self._permission_checking = set() # индекс вкладки, где сейчас идёт проверка
self._denied_recently = {} # {index: bool} — активен кулдаун после "Доступ запрещен"
def setup_side_menu(self): def setup_side_menu(self):
"""Настраивает боковое меню и оверлей.""" """Настраивает боковое меню и оверлей."""
# Оверлей для затемнения контента # Оверлей для затемнения контента
@ -162,7 +152,6 @@ class YobbleHomeView(QWidget):
top_bar_layout.addWidget(self.burger_menu_button) top_bar_layout.addWidget(self.burger_menu_button)
self.title_label = QLabel("Чаты") self.title_label = QLabel("Чаты")
#self.title_label = QLabel()
self.title_label.setObjectName("TitleLabel") self.title_label.setObjectName("TitleLabel")
top_bar_layout.addWidget(self.title_label) top_bar_layout.addWidget(self.title_label)
top_bar_layout.addStretch() top_bar_layout.addStretch()
@ -211,7 +200,7 @@ class YobbleHomeView(QWidget):
btn_create.setFocusPolicy(Qt.NoFocus) btn_create.setFocusPolicy(Qt.NoFocus)
#self.update_tab_selection(2) self.update_tab_selection(2)
return bottom_bar_widget return bottom_bar_widget
def create_tab_button(self, icon_text, text, index): def create_tab_button(self, icon_text, text, index):
@ -264,12 +253,12 @@ class YobbleHomeView(QWidget):
def setup_content_pages(self): def setup_content_pages(self):
# Лента # Лента
self.feed_label = QLabel(localizer.translate("Загрузка...")) self.feed_label = QLabel("Загрузка...")
self.feed_label.setAlignment(Qt.AlignCenter) self.feed_label.setAlignment(Qt.AlignCenter)
self.content_stack.addWidget(self.feed_label) self.content_stack.addWidget(self.feed_label)
# Музыка # Музыка
self.music_label = QLabel(localizer.translate("Загрузка...")) self.music_label = QLabel("Загрузка...")
self.music_label.setAlignment(Qt.AlignCenter) self.music_label.setAlignment(Qt.AlignCenter)
self.content_stack.addWidget(self.music_label) self.content_stack.addWidget(self.music_label)
@ -283,64 +272,35 @@ class YobbleHomeView(QWidget):
"""Обрабатывает нажатие на кнопку вкладки, проверяя права доступа.""" """Обрабатывает нажатие на кнопку вкладки, проверяя права доступа."""
if index in self.REQUIRED_PERMISSIONS: if index in self.REQUIRED_PERMISSIONS:
# сразу переключаем на вкладку (там уже "Загрузка...") # сразу переключаем на вкладку (там уже "Загрузка...")
#self.switch_tab(index) 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( asyncio.create_task(
self.check_permissions_and_switch(index, self.REQUIRED_PERMISSIONS[index]) self.check_permissions_and_switch(index, self.REQUIRED_PERMISSIONS[index])
) )
else: else:
# Для вкладок без специальных прав доступа
self.switch_tab(index) self.switch_tab(index)
async def preload_permissions(self): async def preload_permissions(self):
"""Асинхронно предзагружает права доступа без UI.""" """Асинхронно предзагружает права доступа без UI."""
access_token = await get_current_access_token() access_token = get_current_access_token()
if not access_token: if not access_token:
print("[Permissions] Preload failed: No access token.") print("[Permissions] Preload failed: No access token.")
return return
success, data = await get_user_role(access_token, self.username) success, data = await get_user_role(access_token)
if success: if success:
user_permissions = set(data.get("user_permissions", [])) user_permissions = data.get("user_permissions", [])
for permission_code in self.REQUIRED_PERMISSIONS.values():
# Загружаем ВСЕ права пользователя в кэш if permission_code in user_permissions:
self.permission_cache = user_permissions self.permission_cache.add(permission_code)
print("self.permission_cache", self.permission_cache)
# for permission_code in self.REQUIRED_PERMISSIONS.values():
# if permission_code in user_permissions:
# self.permission_cache.add(permission_code)
self.permissions_preloaded_last = time.time()
self.permissions_preloaded = True self.permissions_preloaded = True
self.preload_permissions_first = True
print(f"[Permissions] Preloaded. Cache: {self.permission_cache}") print(f"[Permissions] Preloaded. Cache: {self.permission_cache}")
else: else:
print(f"[Permissions] Preload failed: {data}") print(f"[Permissions] Preload failed: {data}")
# def update_current_tab_content(self):
# """Обновляет контент текущей активной вкладки после предзагрузки прав."""
# current_index = self.content_stack.currentIndex()
# # Проверяем, требует ли текущая вкладка прав доступа
# if current_index in self.REQUIRED_PERMISSIONS:
# permission_code = self.REQUIRED_PERMISSIONS[current_index]
# if permission_code in self.permission_cache:
# self.show_real_content(current_index)
# else:
# self.show_denied(current_index)
async def check_permissions_and_switch(self, index, permission_code): async def check_permissions_and_switch(self, index, permission_code):
"""Асинхронно проверяет права и переключает вкладку.""" """Асинхронно проверяет права и переключает вкладку."""
self.preload_permissions_first = False
if permission_code in self.permission_cache: if permission_code in self.permission_cache:
self.show_real_content(index) self.show_real_content(index)
self.switch_tab(index) self.switch_tab(index)
@ -353,16 +313,17 @@ class YobbleHomeView(QWidget):
return return
# Иначе делаем запрос # Иначе делаем запрос
access_token = await get_current_access_token() access_token = get_current_access_token()
if not access_token: if not access_token:
self.show_error_message(localizer.translate("Сессия не найдена. Пожалуйста, войдите снова.")) self.show_error_message(localizer.translate("Сессия не найдена. Пожалуйста, войдите снова."))
return return
success, data = await get_user_role(access_token, self.username) success, data = await get_user_role(access_token)
print("data", data)
success, data = await get_user_role(access_token)
if success and permission_code in data.get("user_permissions", []): if success and permission_code in data.get("user_permissions", []):
# self.permission_cache.add(permission_code) 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.show_real_content(index)
self.switch_tab(index) self.switch_tab(index)
else: else:
@ -395,7 +356,6 @@ class YobbleHomeView(QWidget):
button.setProperty("selected", button.property("tab_index") == selected_index) button.setProperty("selected", button.property("tab_index") == selected_index)
button.style().unpolish(button) button.style().unpolish(button)
button.style().polish(button) button.style().polish(button)
button.update()
def show_real_content(self, index): def show_real_content(self, index):
if index == 0: if index == 0:
@ -404,7 +364,7 @@ class YobbleHomeView(QWidget):
self.music_label.setText("Контент Музыки") self.music_label.setText("Контент Музыки")
def show_denied(self, index): def show_denied(self, index):
denied = QLabel(localizer.translate("Доступ запрещен")) denied = QLabel("Доступ запрещен")
denied.setAlignment(Qt.AlignCenter) denied.setAlignment(Qt.AlignCenter)
denied.setStyleSheet("font-size: 18px; color: #8e8e93;") denied.setStyleSheet("font-size: 18px; color: #8e8e93;")
@ -439,29 +399,18 @@ class YobbleHomeView(QWidget):
"""Возвращает QSS стили для компонента в зависимости от темы.""" """Возвращает QSS стили для компонента в зависимости от темы."""
is_dark = theme_manager.is_dark() is_dark = theme_manager.is_dark()
# Базовая палитра # Цветовая палитра
bg_color = "#1c1c1e" if is_dark else "white" bg_color = "#1c1c1e" if is_dark else "white"
bar_bg_color = "#2c2c2e" if is_dark else "#f8f8f8" bar_bg_color = "#2c2c2e" if is_dark else "#f8f8f8"
bar_border_color = "#3c3c3c" if is_dark else "#e7e7e7" bar_border_color = "#3c3c3c" if is_dark else "#e7e7e7"
text_color = "#8e8e93" if is_dark else "#777777" text_color = "#8e8e93" if is_dark else "#888888"
title_color = "white" if is_dark else "black" title_color = "white" if is_dark else "black"
active_color = "#0A84FF"
top_bar_bg = "#2c2c2e" if is_dark else "#f5f5f5" top_bar_bg = "#2c2c2e" if is_dark else "#f5f5f5"
top_bar_border = "#3c3c3c" if is_dark else "#e0e0e0" top_bar_border = "#3c3c3c" if is_dark else "#e0e0e0"
hover_color = "#d0d0d0" if not is_dark else "#444444"
overlay_color = "rgba(0, 0, 0, 0.5)" 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""" return f"""
#content_widget {{ #content_widget {{
background-color: {bg_color}; background-color: {bg_color};
@ -470,13 +419,14 @@ class YobbleHomeView(QWidget):
background-color: {overlay_color}; background-color: {overlay_color};
}} }}
/* Глобально для кнопок */ /* Глобальные стили для кнопок */
QPushButton {{ QPushButton {{
background: transparent; background: transparent;
border: none; border: none;
outline: none; outline: none;
}} }}
QPushButton:focus, QPushButton:focus,
QPushButton:pressed,
QPushButton:checked {{ QPushButton:checked {{
background: transparent; background: transparent;
border: none; border: none;
@ -514,34 +464,29 @@ class YobbleHomeView(QWidget):
/* Кнопки вкладок */ /* Кнопки вкладок */
#TabButton {{ #TabButton {{
padding: 6px 8px; background: transparent;
border-radius: 10px; border: none;
border: 1px solid transparent; /* чтобы при selection не прыгала высота */ outline: none;
padding: 5px;
}} }}
#TabButton:hover {{ #TabButton:hover {{
background-color: {hover_bg}; background-color: {hover_color};
border-radius: 6px;
}} }}
#TabButton:pressed {{ #TabButton:pressed {{
background-color: {pressed_bg}; background-color: {active_color}22; /* активный цвет с прозрачностью */
padding-top: 8px; border-radius: 6px;
padding-bottom: 4px;
}} }}
/* Иконка/текст по умолчанию */
#TabButton #TabIcon {{ color: {text_color}; }} #TabButton #TabIcon {{ color: {text_color}; }}
#TabButton #TabText {{ color: {text_color}; }} #TabButton #TabText {{ color: {text_color}; }}
/* ВЫБРАННАЯ вкладка разные тона для light/dark */
#TabButton[selected="true"] {{ #TabButton[selected="true"] {{
background-color: {selected_bg}; background-color: {active_color}22;
border: 1px solid {selected_border};
}} }}
#TabButton[selected="true"] #TabIcon, #TabButton[selected="true"] #TabIcon,
#TabButton[selected="true"] #TabText {{ #TabButton[selected="true"] #TabText {{
color: {active_hex}; color: {active_color};
font-weight: 600;
}} }}
#TabIcon, #TabText {{ #TabIcon, #TabText {{
border: none; border: none;
outline: none; outline: none;
@ -576,3 +521,130 @@ class YobbleHomeView(QWidget):
); );
}} }}
""" """
# 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 "#888888"
# title_color = "white" if is_dark else "black"
# active_color = "#0A84FF"
# top_bar_bg = "#2c2c2e" if is_dark else "#f5f5f5"
# top_bar_border = "#3c3c3c" if is_dark else "#e0e0e0"
# hover_color = "#3a3a3c" if is_dark else "#e0e0e0"
# overlay_color = "rgba(0, 0, 0, 0.5)"
# return f"""
# #content_widget {{
# background-color: {bg_color};
# }}
# #Overlay {{
# background-color: {overlay_color};
# }}
# /* Глобальные стили для кнопок */
# QPushButton {{
# background: transparent;
# border: none;
# outline: none;
# transition: background-color 200ms ease, transform 150ms ease;
# }}
# QPushButton:focus,
# QPushButton:checked {{
# background: transparent;
# border: none;
# outline: none;
# }}
# /* Верхняя панель */
# #TopBar {{
# background-color: {top_bar_bg};
# border-bottom: 1px solid {top_bar_border};
# }}
# #TopBar QPushButton {{
# font-size: 22px;
# border: none;
# padding: 5px;
# color: {title_color};
# background: transparent;
# }}
# #TitleLabel {{
# font-size: 18px;
# font-weight: bold;
# color: {title_color};
# border: none;
# outline: none;
# background-color: transparent;
# }}
# /* Нижняя панель */
# #BottomBar {{
# background-color: {bar_bg_color};
# border-top: 1px solid {bar_border_color};
# padding-top: 5px;
# padding-bottom: 15px;
# }}
# /* Кнопки вкладок */
# #TabButton {{
# border-radius: 6px;
# padding: 6px;
# }}
# /* Hover (не выбранная вкладка) */
# #TabButton:hover[selected="false"] {{
# background-color: {hover_color};
# }}
# /* Pressed (удержание) */
# #TabButton:pressed {{
# background-color: {active_color}44;
# transform: scale(0.95);
# }}
# /* Selected */
# #TabButton[selected="true"] {{
# background-color: {active_color}22;
# }}
# #TabButton[selected="true"] #TabIcon,
# #TabButton[selected="true"] #TabText {{
# color: {active_color};
# font-weight: 600;
# }}
# #TabIcon, #TabText {{
# border: none;
# outline: none;
# background-color: transparent;
# }}
# #TabIcon {{ font-size: 22px; color: {text_color}; }}
# #TabText {{ font-size: 12px; color: {text_color}; }}
# /* Центральная кнопка "Создать" */
# #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
# );
# transform: scale(0.95);
# }}
# """

View File

@ -8,5 +8,3 @@ common-lib @ git+https://githlam.com/messenger/common_lib.git@main
httpx[http2]==0.28.1 httpx[http2]==0.28.1
asyncio==4.0.0 asyncio==4.0.0
qasync==0.28.0 qasync==0.28.0
logging==0.4.9.6
aiosqlite==0.21.0