Compare commits
No commits in common. "99029541d3d9baebaef6f9bb9eceda884981dcf3" and "e8427ceb5d9c42726045610d846bc4077ba5b443" have entirely different histories.
99029541d3
...
e8427ceb5d
@ -1,23 +1,17 @@
|
|||||||
from PySide6.QtWidgets import QStackedWidget
|
from PySide6.QtWidgets import QStackedWidget
|
||||||
from PySide6.QtCore import Signal
|
|
||||||
|
|
||||||
from app.ui.views.login_view import LoginView
|
from app.ui.views.login_view import LoginView
|
||||||
from app.ui.views.yobble_home_view import YobbleHomeView
|
from app.ui.views.yobble_home_view import YobbleHomeView # <--- Изменено
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import time
|
import time # эмуляция задержки от сервера
|
||||||
import asyncio
|
import asyncio
|
||||||
from app.core.database import get_last_login, get_session, set_last_login
|
from app.core.database import get_last_login, get_session, set_last_login
|
||||||
|
|
||||||
class MainController(QStackedWidget):
|
class MainController(QStackedWidget):
|
||||||
# Сигнал для показа уведомлений из любого потока
|
|
||||||
# (message, is_error)
|
|
||||||
notification_requested = Signal(str, bool)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.login_view: Optional[LoginView] = None
|
self.login_view: Optional[LoginView] = None
|
||||||
self.yobble_home_view: Optional[YobbleHomeView] = None
|
self.yobble_home_view: Optional[YobbleHomeView] = None # <--- Изменено
|
||||||
|
|
||||||
self.init_app()
|
self.init_app()
|
||||||
|
|
||||||
@ -47,33 +41,29 @@ class MainController(QStackedWidget):
|
|||||||
self.removeWidget(self.login_view)
|
self.removeWidget(self.login_view)
|
||||||
self.login_view = None
|
self.login_view = None
|
||||||
|
|
||||||
|
# Отображаем новый главный экран
|
||||||
self.yobble_home_view = YobbleHomeView(username=username)
|
self.yobble_home_view = YobbleHomeView(username=username)
|
||||||
|
|
||||||
# Подключаем сигнал к слоту в YobbleHomeView
|
|
||||||
self.notification_requested.connect(self.yobble_home_view.show_notification)
|
|
||||||
|
|
||||||
self.addWidget(self.yobble_home_view)
|
self.addWidget(self.yobble_home_view)
|
||||||
self.setCurrentWidget(self.yobble_home_view)
|
self.setCurrentWidget(self.yobble_home_view)
|
||||||
self.yobble_home_view.show()
|
self.yobble_home_view.show()
|
||||||
|
|
||||||
|
# 🔹 Оставляем фоновое обновление на будущее
|
||||||
Thread(target=self.update_data_from_server, args=(username,), daemon=True).start()
|
Thread(target=self.update_data_from_server, args=(username,), daemon=True).start()
|
||||||
|
|
||||||
def update_data_from_server(self, username: str):
|
def update_data_from_server(self, username: str):
|
||||||
"""
|
"""
|
||||||
В фоновом режиме обновляет данные с сервера.
|
В фоновом режиме обновляет данные с сервера.
|
||||||
|
Эмулирует задержку и предзагружает права доступа.
|
||||||
"""
|
"""
|
||||||
|
# Эмуляция запроса
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
print(f"[Sync] Обновляем данные для пользователя: {username}")
|
print(f"[Sync] Обновляем данные для пользователя: {username}")
|
||||||
|
|
||||||
if self.yobble_home_view:
|
if self.yobble_home_view:
|
||||||
print("[Sync] Запускаем предзагрузку прав доступа...")
|
print("[Sync] Запускаем предзагрузку прав доступа...")
|
||||||
try:
|
try:
|
||||||
# `preload_permissions` теперь возвращает кортеж (успех, данные)
|
# Запускаем асинхронную функцию в текущем потоке
|
||||||
# asyncio.run() выполняет async функцию и возвращает её результат
|
asyncio.run(self.yobble_home_view.preload_permissions()) # TODO добавить из sqlite
|
||||||
asyncio.run(self.yobble_home_view.preload_permissions())
|
print("[Sync] Предзагрузка прав доступа завершена.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = f"Ошибка предзагрузки: {e}"
|
print(f"[Sync] Ошибка во время предзагрузки прав: {e}")
|
||||||
print(f"[Sync] {error_message}")
|
|
||||||
# Отправляем сигнал вместо прямого вызова
|
|
||||||
self.notification_requested.emit(error_message, True)
|
|
||||||
|
|||||||
@ -119,18 +119,18 @@ async def refresh_token(access_token: str, refresh_token: str):
|
|||||||
)
|
)
|
||||||
return True, token_data
|
return True, token_data
|
||||||
else:
|
else:
|
||||||
return False, data.get("detail", localizer.translate("Unknown error"))
|
return False, data.get("detail", "Unknown error")
|
||||||
|
|
||||||
elif response.status_code == 401:
|
elif response.status_code == 401:
|
||||||
return False, localizer.translate("Refresh token is invalid or expired")
|
return False, "Refresh token is invalid or expired"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return False, f"{localizer.translate('Server error')}: {response.status_code}"
|
return False, f"Server error: {response.status_code}"
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
return False, f"{localizer.translate('Network error')}: {e}"
|
return False, f"Network error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"{localizer.translate('An error occurred')}: {e}"
|
return False, f"An error occurred: {e}"
|
||||||
|
|
||||||
|
|
||||||
async def get_user_role(access_token: str, login: str):
|
async def get_user_role(access_token: str, login: str):
|
||||||
@ -147,13 +147,13 @@ 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", localizer.translate("Unknown error")))
|
return (True, data['data']) if data.get("status") == "fine" else (False, data.get("detail", "Unknown error"))
|
||||||
|
|
||||||
elif response.status_code == 401:
|
elif response.status_code == 401:
|
||||||
# Токен истек, пытаемся обновить
|
# Токен истек, пытаемся обновить
|
||||||
session = get_session(login)
|
session = get_session(login)
|
||||||
if not session or not session['refresh_token']:
|
if not session or not session['refresh_token']:
|
||||||
return False, localizer.translate("No refresh token found")
|
return False, "No refresh token found"
|
||||||
|
|
||||||
refresh_success, refresh_data = await refresh_token(access_token, session['refresh_token'])
|
refresh_success, refresh_data = await refresh_token(access_token, session['refresh_token'])
|
||||||
|
|
||||||
@ -165,16 +165,16 @@ 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, localizer.translate("Failed to get role after refresh"))
|
return (True, data['data']) if data.get("status") == "fine" else (False, "Failed to get role after refresh")
|
||||||
|
|
||||||
# Если обновление не удалось, выходим из системы
|
# Если обновление не удалось, выходим из системы
|
||||||
logout(access_token)
|
logout(access_token)
|
||||||
return False, localizer.translate("Session expired, please log in again")
|
return False, "Session expired, please log in again"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return False, f"{localizer.translate('Server error')}: {response.status_code}"
|
return False, f"Server error: {response.status_code}"
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
return False, f"{localizer.translate('Network error')}: {e}"
|
return False, f"Network error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"{localizer.translate('An error occurred')}: {e}"
|
return False, f"An error occurred: {e}"
|
||||||
|
|||||||
@ -30,20 +30,9 @@
|
|||||||
"Регистрация в данный момент отключена.": "Registration is currently disabled.",
|
"Регистрация в данный момент отключена.": "Registration is currently disabled.",
|
||||||
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Data validation failed. Check username and password length.",
|
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Data validation failed. Check username and password length.",
|
||||||
|
|
||||||
"Загрузка...": "Loading...",
|
"Загрузка...": "Загрузка...",
|
||||||
"Доступ запрещен": "Access denied",
|
"Доступ запрещен": "Access denied",
|
||||||
|
|
||||||
"Ошибка предзагрузки": "Preload error",
|
|
||||||
"Unknown error": "Unknown error",
|
|
||||||
"Refresh token is invalid or expired": "Refresh token is invalid or expired",
|
|
||||||
"Server error": "Server error",
|
|
||||||
"Network error": "Network error",
|
|
||||||
"An error occurred": "An error occurred",
|
|
||||||
"No refresh token found": "No refresh token found",
|
|
||||||
"Failed to get role after refresh": "Failed to get role after refresh",
|
|
||||||
"Session expired, please log in again": "Session expired, please log in again",
|
|
||||||
|
|
||||||
|
|
||||||
"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",
|
||||||
|
|||||||
@ -30,19 +30,9 @@
|
|||||||
"Регистрация в данный момент отключена.": "Регистрация в данный момент отключена.",
|
"Регистрация в данный момент отключена.": "Регистрация в данный момент отключена.",
|
||||||
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Данные не прошли валидацию. Проверьте длину логина и пароля.",
|
"Данные не прошли валидацию. Проверьте длину логина и пароля.": "Данные не прошли валидацию. Проверьте длину логина и пароля.",
|
||||||
|
|
||||||
"Загрузка...": "Загрузка...",
|
"Загрузка...": "Loading...",
|
||||||
"Доступ запрещен": "Доступ запрещен",
|
"Доступ запрещен": "Доступ запрещен",
|
||||||
|
|
||||||
"Ошибка предзагрузки": "Ошибка предзагрузки",
|
|
||||||
"Unknown error": "Неизвестная ошибка",
|
|
||||||
"Refresh token is invalid or expired": "Токен обновления недействителен или просрочен",
|
|
||||||
"Server error": "Ошибка сервера",
|
|
||||||
"Network error": "Ошибка сети",
|
|
||||||
"An error occurred": "Произошла ошибка",
|
|
||||||
"No refresh token found": "Токен обновления не найден",
|
|
||||||
"Failed to get role after refresh": "Не удалось получить роль после обновления",
|
|
||||||
"Session expired, please log in again": "Сессия истекла, пожалуйста, войдите снова",
|
|
||||||
|
|
||||||
"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": "Логин не должен начинаться с символа подчёркивания",
|
||||||
|
|||||||
@ -64,7 +64,6 @@ class YobbleHomeView(QWidget):
|
|||||||
|
|
||||||
# --- Боковое меню и оверлей ---
|
# --- Боковое меню и оверлей ---
|
||||||
self.setup_side_menu()
|
self.setup_side_menu()
|
||||||
self._setup_notification_widget()
|
|
||||||
|
|
||||||
self.update_styles()
|
self.update_styles()
|
||||||
theme_manager.theme_changed.connect(self.update_styles)
|
theme_manager.theme_changed.connect(self.update_styles)
|
||||||
@ -108,81 +107,6 @@ class YobbleHomeView(QWidget):
|
|||||||
if self.is_menu_closing:
|
if self.is_menu_closing:
|
||||||
self.overlay.hide()
|
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):
|
def toggle_side_menu(self):
|
||||||
"""Показывает или скрывает боковое меню с анимацией."""
|
"""Показывает или скрывает боковое меню с анимацией."""
|
||||||
# Останавливаем текущие анимации, чтобы избежать конфликтов
|
# Останавливаем текущие анимации, чтобы избежать конфликтов
|
||||||
@ -219,12 +143,6 @@ class YobbleHomeView(QWidget):
|
|||||||
self.side_menu.move(-self.side_menu.width(), 0)
|
self.side_menu.move(-self.side_menu.width(), 0)
|
||||||
self.side_menu.setFixedHeight(self.height())
|
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):
|
def update_styles(self):
|
||||||
"""Обновляет стили компонента при смене темы."""
|
"""Обновляет стили компонента при смене темы."""
|
||||||
self.setStyleSheet(self.get_stylesheet())
|
self.setStyleSheet(self.get_stylesheet())
|
||||||
@ -264,16 +182,6 @@ class YobbleHomeView(QWidget):
|
|||||||
top_bar_layout.addWidget(self.notification_button)
|
top_bar_layout.addWidget(self.notification_button)
|
||||||
self.notification_button.clicked.connect(self.handle_notification_click)
|
self.notification_button.clicked.connect(self.handle_notification_click)
|
||||||
|
|
||||||
# --- Временные кнопки для теста ---
|
|
||||||
# self.test_ok_button = QPushButton("Test OK")
|
|
||||||
# self.test_ok_button.clicked.connect(lambda: self.show_notification("Операция прошла успешно", is_error=False))
|
|
||||||
# top_bar_layout.addWidget(self.test_ok_button)
|
|
||||||
|
|
||||||
# self.test_err_button = QPushButton("Test Error")
|
|
||||||
# self.test_err_button.clicked.connect(lambda: self.show_notification("Произошла ошибка при обновлении", is_error=True))
|
|
||||||
# top_bar_layout.addWidget(self.test_err_button)
|
|
||||||
# --- Конец временного кода ---
|
|
||||||
|
|
||||||
return top_bar_widget
|
return top_bar_widget
|
||||||
|
|
||||||
def create_bottom_bar(self):
|
def create_bottom_bar(self):
|
||||||
@ -395,28 +303,27 @@ class YobbleHomeView(QWidget):
|
|||||||
access_token = await get_current_access_token()
|
access_token = await 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.")
|
||||||
# Поскольку мы уже в async-контексте, который будет запущен
|
|
||||||
# в основном потоке через QMetaObject.invokeMethod или сигнал,
|
|
||||||
# можно вызвать напрямую, но сигнал надежнее.
|
|
||||||
# Здесь прямой вызов может быть небезопасен, если async-задача
|
|
||||||
# выполняется в другом потоке. Но т.к. мы используем сигнал
|
|
||||||
# в контроллере, этот код не будет вызван.
|
|
||||||
return
|
return
|
||||||
|
|
||||||
success, data = await get_user_role(access_token, self.username)
|
success, data = await get_user_role(access_token, self.username)
|
||||||
if success:
|
if success:
|
||||||
user_permissions = set(data.get("user_permissions", []))
|
user_permissions = set(data.get("user_permissions", []))
|
||||||
|
|
||||||
|
# Загружаем ВСЕ права пользователя в кэш
|
||||||
self.permission_cache = 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_last = time.time()
|
||||||
self.permissions_preloaded = True
|
self.permissions_preloaded = True
|
||||||
self.preload_permissions_first = 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}")
|
||||||
raise ConnectionError(data)
|
|
||||||
return success, data
|
|
||||||
|
|
||||||
# def update_current_tab_content(self):
|
# def update_current_tab_content(self):
|
||||||
# """Обновляет контент текущей активной вкладки после предзагрузки прав."""
|
# """Обновляет контент текущей активной вкладки после предзагрузки прав."""
|
||||||
@ -470,8 +377,13 @@ class YobbleHomeView(QWidget):
|
|||||||
self.title_label.setText(titles[index])
|
self.title_label.setText(titles[index])
|
||||||
|
|
||||||
def show_error_message(self, message):
|
def show_error_message(self, message):
|
||||||
"""Показывает всплывающее уведомление об ошибке."""
|
"""Показывает диалоговое окно с сообщением об ошибке."""
|
||||||
self.show_notification(message, is_error=True)
|
show_themed_messagebox(
|
||||||
|
self,
|
||||||
|
QMessageBox.Warning,
|
||||||
|
localizer.translate("Ошибка доступа"),
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
def update_tab_selection(self, selected_index):
|
def update_tab_selection(self, selected_index):
|
||||||
if not hasattr(self, 'bottom_bar'):
|
if not hasattr(self, 'bottom_bar'):
|
||||||
@ -558,27 +470,6 @@ class YobbleHomeView(QWidget):
|
|||||||
background-color: {overlay_color};
|
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 {{
|
QPushButton {{
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
Reference in New Issue
Block a user