Compare commits
2 Commits
35ad95b8f7
...
e8427ceb5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8427ceb5d | ||
|
|
ddb95ae852 |
@ -63,7 +63,7 @@ class MainController(QStackedWidget):
|
||||
print("[Sync] Запускаем предзагрузку прав доступа...")
|
||||
try:
|
||||
# Запускаем асинхронную функцию в текущем потоке
|
||||
asyncio.run(self.yobble_home_view.preload_permissions())
|
||||
asyncio.run(self.yobble_home_view.preload_permissions()) # TODO добавить из sqlite
|
||||
print("[Sync] Предзагрузка прав доступа завершена.")
|
||||
except Exception as e:
|
||||
print(f"[Sync] Ошибка во время предзагрузки прав: {e}")
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import sqlite3
|
||||
import aiosqlite
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
@ -49,18 +50,37 @@ def init_db():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def add_session(login, access_token, refresh_token):
|
||||
def add_session(login, access_token, refresh_token, update_existing=False):
|
||||
"""Добавляет новую сессию или обновляет существующую."""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
# REPLACE INTO - удобный способ для вставки или обновления
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO sessions (login, access_token, refresh_token, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (login, access_token, refresh_token, datetime.now()))
|
||||
|
||||
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('''
|
||||
INSERT OR REPLACE INTO sessions (login, access_token, refresh_token, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (login, access_token, refresh_token, datetime.now()))
|
||||
|
||||
conn.commit()
|
||||
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):
|
||||
"""Получает сессию по логину."""
|
||||
conn = get_connection()
|
||||
@ -79,13 +99,6 @@ def get_all_sessions():
|
||||
conn.close()
|
||||
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():
|
||||
"""Получает логин последнего вошедшего пользователя."""
|
||||
@ -104,14 +117,26 @@ def set_last_login(login: str):
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_current_access_token():
|
||||
"""
|
||||
Получает access_token для последней активной сессии.
|
||||
:return: access_token или None, если сессия не найдена.
|
||||
"""
|
||||
last_login = get_last_login()
|
||||
if not last_login:
|
||||
return None
|
||||
# def get_current_access_token():
|
||||
# """
|
||||
# Получает access_token для последней активной сессии.
|
||||
# :return: access_token или None, если сессия не найдена.
|
||||
# """
|
||||
# last_login = get_last_login()
|
||||
# if not last_login:
|
||||
# return None
|
||||
|
||||
session = get_session(last_login)
|
||||
return session['access_token'] if session else 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
|
||||
last_login = row[0]
|
||||
async with db.execute('SELECT access_token FROM sessions WHERE login = ?', (last_login,)) as cur:
|
||||
row = await cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import httpx
|
||||
import asyncio
|
||||
from app.core import config
|
||||
from app.core.database import add_session
|
||||
from app.core.database import add_session, logout, get_session
|
||||
from app.core.localizer import localizer
|
||||
|
||||
async def login(login, password):
|
||||
@ -91,12 +91,52 @@ async def register(login, password, invite=None):
|
||||
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
|
||||
|
||||
|
||||
async def get_user_role(access_token: str):
|
||||
async def refresh_token(access_token: str, refresh_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"
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
@ -107,32 +147,34 @@ async def get_user_role(access_token: str):
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
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("Неизвестная ошибка ответа"))
|
||||
return (True, data['data']) if data.get("status") == "fine" else (False, data.get("detail", "Unknown error"))
|
||||
|
||||
elif response.status_code == 401:
|
||||
return False, localizer.translate("Токен недействителен или истек")
|
||||
elif response.status_code == 404:
|
||||
return False, localizer.translate("Пользователь не найден")
|
||||
# Токен истек, пытаемся обновить
|
||||
session = get_session(login)
|
||||
if not session or not session['refresh_token']:
|
||||
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:
|
||||
return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}"
|
||||
return False, f"Server error: {response.status_code}"
|
||||
|
||||
except httpx.RequestError as e:
|
||||
return False, f"{localizer.translate('Ошибка сети')}: {e}"
|
||||
return False, f"Network error: {e}"
|
||||
except Exception as 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())
|
||||
return False, f"An error occurred: {e}"
|
||||
|
||||
@ -30,6 +30,9 @@
|
||||
"Регистрация в данный момент отключена.": "Registration is currently disabled.",
|
||||
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "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 not contain whitespace characters": "Login must not contain whitespace characters",
|
||||
"Login must not start with an underscore": "Login must not start with an underscore",
|
||||
|
||||
@ -30,6 +30,9 @@
|
||||
"Регистрация в данный момент отключена.": "Регистрация в данный момент отключена.",
|
||||
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Данные не прошли валидацию. Проверьте длину логина и пароля.",
|
||||
|
||||
"Загрузка...": "Loading...",
|
||||
"Доступ запрещен": "Доступ запрещен",
|
||||
|
||||
"Login must be between 3 and 32 characters long": "Логин должен быть от 3 до 32 символов",
|
||||
"Login must not contain whitespace characters": "Логин не должен содержать пробелы",
|
||||
"Login must not start with an underscore": "Логин не должен начинаться с символа подчёркивания",
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
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
|
||||
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QTimer
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
from app.core.theme import theme_manager
|
||||
@ -27,6 +28,8 @@ class YobbleHomeView(QWidget):
|
||||
self.setMinimumSize(360, 640)
|
||||
self.permission_cache = set()
|
||||
self.permissions_preloaded = False
|
||||
self.permissions_preloaded = False
|
||||
self.permissions_preloaded_last = 0.0
|
||||
|
||||
# --- Основной макет ---
|
||||
# Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом
|
||||
@ -52,7 +55,9 @@ class YobbleHomeView(QWidget):
|
||||
|
||||
# 3. Нижняя панель навигации
|
||||
self.bottom_bar = self.create_bottom_bar()
|
||||
self.content_stack.setCurrentIndex(2)
|
||||
# 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)
|
||||
@ -63,6 +68,11 @@ class YobbleHomeView(QWidget):
|
||||
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):
|
||||
"""Настраивает боковое меню и оверлей."""
|
||||
# Оверлей для затемнения контента
|
||||
@ -152,6 +162,7 @@ class YobbleHomeView(QWidget):
|
||||
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()
|
||||
@ -200,7 +211,7 @@ class YobbleHomeView(QWidget):
|
||||
|
||||
btn_create.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
self.update_tab_selection(2)
|
||||
#self.update_tab_selection(2)
|
||||
return bottom_bar_widget
|
||||
|
||||
def create_tab_button(self, icon_text, text, index):
|
||||
@ -253,12 +264,12 @@ class YobbleHomeView(QWidget):
|
||||
|
||||
def setup_content_pages(self):
|
||||
# Лента
|
||||
self.feed_label = QLabel("Загрузка...")
|
||||
self.feed_label = QLabel(localizer.translate("Загрузка..."))
|
||||
self.feed_label.setAlignment(Qt.AlignCenter)
|
||||
self.content_stack.addWidget(self.feed_label)
|
||||
|
||||
# Музыка
|
||||
self.music_label = QLabel("Загрузка...")
|
||||
self.music_label = QLabel(localizer.translate("Загрузка..."))
|
||||
self.music_label.setAlignment(Qt.AlignCenter)
|
||||
self.content_stack.addWidget(self.music_label)
|
||||
|
||||
@ -272,35 +283,64 @@ class YobbleHomeView(QWidget):
|
||||
"""Обрабатывает нажатие на кнопку вкладки, проверяя права доступа."""
|
||||
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(
|
||||
self.check_permissions_and_switch(index, self.REQUIRED_PERMISSIONS[index])
|
||||
)
|
||||
else:
|
||||
# Для вкладок без специальных прав доступа
|
||||
self.switch_tab(index)
|
||||
|
||||
async def preload_permissions(self):
|
||||
"""Асинхронно предзагружает права доступа без UI."""
|
||||
access_token = get_current_access_token()
|
||||
access_token = await get_current_access_token()
|
||||
if not access_token:
|
||||
print("[Permissions] Preload failed: No access token.")
|
||||
return
|
||||
|
||||
success, data = await get_user_role(access_token)
|
||||
success, data = await get_user_role(access_token, self.username)
|
||||
if success:
|
||||
user_permissions = data.get("user_permissions", [])
|
||||
for permission_code in self.REQUIRED_PERMISSIONS.values():
|
||||
if permission_code in user_permissions:
|
||||
self.permission_cache.add(permission_code)
|
||||
user_permissions = set(data.get("user_permissions", []))
|
||||
|
||||
# Загружаем ВСЕ права пользователя в кэш
|
||||
self.permission_cache = user_permissions
|
||||
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.preload_permissions_first = True
|
||||
|
||||
print(f"[Permissions] Preloaded. Cache: {self.permission_cache}")
|
||||
else:
|
||||
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):
|
||||
"""Асинхронно проверяет права и переключает вкладку."""
|
||||
self.preload_permissions_first = False
|
||||
if permission_code in self.permission_cache:
|
||||
self.show_real_content(index)
|
||||
self.switch_tab(index)
|
||||
@ -313,17 +353,16 @@ class YobbleHomeView(QWidget):
|
||||
return
|
||||
|
||||
# Иначе делаем запрос
|
||||
access_token = get_current_access_token()
|
||||
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)
|
||||
print("data", data)
|
||||
|
||||
success, data = await get_user_role(access_token)
|
||||
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)
|
||||
# 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:
|
||||
@ -356,6 +395,7 @@ class YobbleHomeView(QWidget):
|
||||
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:
|
||||
@ -364,7 +404,7 @@ class YobbleHomeView(QWidget):
|
||||
self.music_label.setText("Контент Музыки")
|
||||
|
||||
def show_denied(self, index):
|
||||
denied = QLabel("Доступ запрещен")
|
||||
denied = QLabel(localizer.translate("Доступ запрещен"))
|
||||
denied.setAlignment(Qt.AlignCenter)
|
||||
denied.setStyleSheet("font-size: 18px; color: #8e8e93;")
|
||||
|
||||
@ -399,17 +439,28 @@ class YobbleHomeView(QWidget):
|
||||
"""Возвращает 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 = "#d0d0d0" if not is_dark else "#444444"
|
||||
overlay_color = "rgba(0, 0, 0, 0.5)"
|
||||
# Базовая палитра
|
||||
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 {{
|
||||
@ -419,14 +470,13 @@ class YobbleHomeView(QWidget):
|
||||
background-color: {overlay_color};
|
||||
}}
|
||||
|
||||
/* Глобальные стили для кнопок */
|
||||
/* Глобально для кнопок */
|
||||
QPushButton {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}}
|
||||
QPushButton:focus,
|
||||
QPushButton:pressed,
|
||||
QPushButton:checked {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
@ -464,29 +514,34 @@ class YobbleHomeView(QWidget):
|
||||
|
||||
/* Кнопки вкладок */
|
||||
#TabButton {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent; /* чтобы при selection не прыгала высота */
|
||||
}}
|
||||
#TabButton:hover {{
|
||||
background-color: {hover_color};
|
||||
border-radius: 6px;
|
||||
background-color: {hover_bg};
|
||||
}}
|
||||
#TabButton:pressed {{
|
||||
background-color: {active_color}22; /* активный цвет с прозрачностью */
|
||||
border-radius: 6px;
|
||||
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: {active_color}22;
|
||||
background-color: {selected_bg};
|
||||
border: 1px solid {selected_border};
|
||||
}}
|
||||
#TabButton[selected="true"] #TabIcon,
|
||||
#TabButton[selected="true"] #TabText {{
|
||||
color: {active_color};
|
||||
color: {active_hex};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
#TabIcon, #TabText {{
|
||||
border: none;
|
||||
outline: none;
|
||||
@ -521,130 +576,3 @@ 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);
|
||||
# }}
|
||||
# """
|
||||
|
||||
@ -8,3 +8,5 @@ common-lib @ git+https://githlam.com/messenger/common_lib.git@main
|
||||
httpx[http2]==0.28.1
|
||||
asyncio==4.0.0
|
||||
qasync==0.28.0
|
||||
logging==0.4.9.6
|
||||
aiosqlite==0.21.0
|
||||
|
||||
Reference in New Issue
Block a user