Compare commits

..

8 Commits

Author SHA1 Message Date
5b7ef4c951 Configuration debug vscode 2026-03-20 18:42:04 +01:00
82679a5709 Style pour le bouton en travaux 2026-03-20 18:36:31 +01:00
93c37a905b Fix server handler on .exe, add hazard stripe button 2026-03-20 11:26:56 +01:00
945abae5f1 new tool for quitting app from child windows 2026-03-19 14:22:54 +01:00
720b004eca Refacto 2026-03-19 13:39:08 +01:00
54aa7a50b2 Refacto QSS, add comments 2026-03-19 12:38:15 +01:00
1125e5ee10 Refacto constants 2026-03-19 09:31:36 +01:00
5d2282def1 Fix discord user is connected with pypresence 2026-03-18 17:33:27 +01:00
16 changed files with 421 additions and 134 deletions

30
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Attach using Process Id",
"type": "debugpy",
"request": "attach",
"processId": "${command:pickProcess}"
},
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "Python Debugger: Attach",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
}
}
]
}

View File

@@ -8,7 +8,11 @@ a = Analysis(
('.\\styles\\styles.qss', 'styles'),
('.\\ui\\mainwindow_vertical_pager.ui', 'ui')
],
hiddenimports=[],
hiddenimports=[
"asyncio",
"pypresence",
"pypresence.baseclient",
],
hookspath=[],
hooksconfig={
"qt_plugins": ["platforms", "styles"]
@@ -107,8 +111,8 @@ a = Analysis(
# Concurrency non utilisée dans ton code
"multiprocessing",
"concurrent",
"asyncio",
#"concurrent",
#"asyncio",
# REPL / terminal
"readline",
@@ -218,6 +222,15 @@ a.binaries = [
if not any(u.lower() in name.lower() for u in unwanted_dlls)
]
# AJOUTE CECI ICI :
# On filtre la liste des fichiers de données (datas)
# On exclut tout ce qui se trouve dans le dossier 'translations' de PySide6
a.datas = [f for f in a.datas if "translations" not in f[0].lower()]
# Si tu veux aussi supprimer les traductions système de Qt (fichiers .qm)
a.datas = [f for f in a.datas if not f[0].endswith('.qm')]
pyz = PYZ(a.pure, a.zipped_data)
exe = EXE(
@@ -243,7 +256,7 @@ exe = EXE(
"msvcp*.dll",
],
runtime_tmpdir=None,
console=True,
console=False,
disable_windowed_traceback=True,
argv_emulation=False,
target_arch=None,

View File

@@ -7,7 +7,23 @@ from PySide6.QtGui import QColor
# ---------------------------------------------------------------------------
NO_STAFF = True
NO_WHITELIST = False
REDIRECT_URI = "http://localhost:5000/callback"
SCOPES = ["identify"]
CLIENT_ID = "1240007913175781508"
AUTENTICATION_SUCCESS_MESSAGE = """
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
</head>
<body>
<h1>Authentication réussie</h1>
<p>Vous pouvez maintenant fermer cette fenêtre et revenir au launcher de La Tanière.</p>
</body>
</html>
""".encode('utf-8')
# ---------------------------------------------------------------------------
# ENUMS
# ---------------------------------------------------------------------------

View File

@@ -3,7 +3,7 @@ from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
from config.config_manager import ConfigManager, VOLUME_KEY
from tools.constants import Resources
from config.constants import Resources
class AudioController:
# Encapsule toute la logique audio : lecture, volume, mute.

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPropertyAnimation, QEasingCurve
from PySide6.QtWidgets import QGraphicsDropShadowEffect
from tools.constants import Glow
from config.constants import Glow
class GlowAnimator:

View File

@@ -3,31 +3,23 @@ import webbrowser
import os
from urllib.parse import urlencode
from http.server import HTTPServer, BaseHTTPRequestHandler
from tools.get_server_token import GetServerTokenForDiscord
from fivemserver.get_server_token import GetServerTokenForDiscord
from config.constants import CLIENT_ID, REDIRECT_URI, SCOPES, AUTENTICATION_SUCCESS_MESSAGE
# Disable stderr output
os.environ['PYTHONWARNINGS'] = 'ignore'
REDIRECT_URI = "http://localhost:5000/callback"
SCOPES = ["identify"]
CLIENT_ID = "1240007913175781508"
AUTENTICATION_SUCCESS_MESSAGE = """
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
</head>
<body>
<h1>Authentication réussie</h1>
<p>Vous pouvez maintenant fermer cette fenêtre et revenir au launcher de La Tanière.</p>
</body>
</html>
""".encode('utf-8')
class OAuthCallbackHandler(BaseHTTPRequestHandler):
code: str | None = None
# Ajoute ceci pour empêcher le serveur d'écrire dans la console/stderr
def log_message(self, format, *args):
return # Ne fait rien, donc pas de blocage sur stdout/stderr
def do_GET(self):
"""
callback pour discord auth
"""
if "/callback" in self.path:
query = self.path.split("?")[1]
params = dict(p.split("=") for p in query.split("&"))
@@ -40,11 +32,19 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
# return discord application id (client id)
def get_discord_client_id() -> str:
"""
return discord application id
"""
return CLIENT_ID
# return discord user id
def get_discord_user_id() -> str:
# récupération des infos serveur lataupe
"""
Retourne l'id du compte discord de l'utilisateur via l'oauh discord.
"""
# récupération des infos serveur la tanière
# récupération d
session_id = GetServerTokenForDiscord.authenticate()
client_secret = GetServerTokenForDiscord.get_token(session_id)
@@ -59,9 +59,6 @@ def get_discord_user_id() -> str:
webbrowser.open(f"{auth_url}?{urlencode(params)}")
server = HTTPServer(("localhost", 5000), OAuthCallbackHandler)
# celle ligne cache le stderr output pour ne pas afficher l'url de callback dans la console
# valable en debug mode
os.dup2(os.open(os.devnull, os.O_WRONLY), 2)
server.handle_request()
if not OAuthCallbackHandler.code:

View File

@@ -0,0 +1,53 @@
import psutil
from pypresence import Presence
from fivemserver.get_server_token import GetServerTokenForDiscord
from config.constants import Urls
from discord.discord_oauth import CLIENT_ID
class DiscordToken:
"""
Décode le token discord
"""
@staticmethod
def decode_discord_token():
discord_token = GetServerTokenForDiscord.get_token(
GetServerTokenForDiscord.authenticate(Urls.API_URL.value)
)
return discord_token
class CheckDiscord:
@staticmethod
def isdiscordrunning() -> bool:
"""
Vérifie si Discord est en cours d'exécution sur l'ordinateur. (Vérifie aussi pour Linux)
"""
for process in psutil.process_iter(["name"]):
if (
process.info["name"].lower() == "discord.exe"
or process.info["name"].lower() == "discordcanary.exe"
or process.info["name"].lower() == "discord"
or process.info["name"].lower() == "discord canary"
):
return True
return False
@staticmethod
def isuserconnected() -> bool:
"""
Vérifie si l'utilisateur Discord est connecté.
ne vérifie pas le user id discord.
"""
rpc = Presence(CLIENT_ID)
try:
rpc.connect()
return True
except Exception as e:
return False
finally:
try:
rpc.close()
except Exception as e:
pass

View File

@@ -5,16 +5,16 @@ from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
API_URL = 'https://prod.la-taniere.fun:30121/'
from config.constants import Urls
class GetServerTokenForDiscord:
derived_key: bytes | None = None
@staticmethod
def authenticate(server = API_URL):
def authenticate(server = Urls.API_URL.value):
if server is None:
server = API_URL
server = Urls.API_URL.value
# ==========================
# Génération clé ECDH client
# ==========================
@@ -49,12 +49,12 @@ class GetServerTokenForDiscord:
return auth["session_id"]
@staticmethod
def get_token(session_id: bytes, server = API_URL):
def get_token(session_id: bytes, server = Urls.API_URL.value):
# ==========================
# DISCORD TOKEN
# ==========================
if server is None:
server = API_URL
server = Urls.API_URL.value
download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={
"x-session-id": session_id
}).json()
@@ -65,12 +65,12 @@ class GetServerTokenForDiscord:
aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
return aesgcm.decrypt(nonce, encrypted_data, None)
# @staticmethod
# def register_discord_user(user_id: str, server = API_URL) -> str:
# if server is None:
# server = API_URL
# registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
# "x-session-id": user_id
# }).json()
#
# return registeredId["discord_id"]
@staticmethod
def register_discord_user(user_id: str, server = Urls.API_URL.value) -> str:
if server is None:
server = Urls.API_URL.value
registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
"x-session-id": user_id
}).json()
return registeredId["discord_id"]

View File

@@ -0,0 +1,23 @@
import requests
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning
# Supress only InsecureRequestWarning
disable_warnings(InsecureRequestWarning)
WHITELIST_URL_ENDPOINT = f'iswhitelist/'
class WhiteList:
@staticmethod
def checkwhitelist(url, discord_user_id: str) -> bool:
print('🗒️ Vérification de la whitelist...')
response = requests.get(url + WHITELIST_URL_ENDPOINT + discord_user_id, verify=False)
api_data = response.json()
if api_data['whitelisted']:
print("👍 Vous êtes en whitelist")
return True
else:
print('🙅‍♂️ Désole mais vous n\'êtes pas whitelisté sur le serveur.')
return False

View File

@@ -7,16 +7,18 @@ from PySide6.QtWidgets import QApplication
# Imports pour la gestion de la configuration
from config.config_manager import ConfigManager
from config.constants import Resources
# Imports pour la vérification Discord
from tools.discord_tools import CheckDiscord
from tools.custom_message_box import CustomMessageBox
from discord.discord_tools import CheckDiscord
# Import pour la partie ui
from ui.custom_message_box import CustomMessageBox
from ui.main_window import MainWindow
# Ne pas supprimer ! Enregistre les ressources Qt
import resources # noqa: F401 - required to register Qt resources
from ui.main_window import MainWindow
from tools.constants import Resources
# ---------------------------------------------------------------------------
# Bundle path resolution
@@ -72,8 +74,8 @@ if __name__ == "__main__":
# On récupère l'ID stocké (sera "" si absent grâce au schéma)
stored_user_id = config.get_discord_user()
if stored_user_id != "" or not stored_user_id.isspace():
if not CheckDiscord.isuserconnected(stored_user_id):
if stored_user_id != "" and not stored_user_id.isspace():
if not CheckDiscord.isuserconnected():
msg = CustomMessageBox(
title="La Tanière: connexion Discord",
message="Tu n'est pas connecté à Discord\n\n"

View File

@@ -1,37 +0,0 @@
import psutil
from pypresence import Presence
from tools.get_server_token import GetServerTokenForDiscord
from tools.constants import Urls
class DiscordToken:
@staticmethod
def decode_discord_token():
discord_token = GetServerTokenForDiscord.get_token(GetServerTokenForDiscord.authenticate(Urls.API_URL.value))
return discord_token
class CheckDiscord:
@staticmethod
def isdiscordrunning() -> bool:
for process in psutil.process_iter(["name"]):
if (process.info["name"].lower() == "discord.exe" or
process.info["name"].lower() == "discordcanary.exe" or
process.info["name"].lower() == "discord" or
process.info["name"].lower() == "discord canary"):
return True
return False
@staticmethod
def isuserconnected(clientid: str) -> bool:
rpc = Presence(clientid)
try:
rpc.connect()
return True
except Exception:
return False
finally:
try:
rpc.close()
except Exception:
pass

View File

@@ -1,5 +1,6 @@
import sys
from pathlib import Path
from PySide6.QtWidgets import QApplication
PROJECT_ROOT = Path(__file__).resolve().parents[2]
@@ -17,3 +18,10 @@ def get_executable_dir() -> Path:
# sys.executable est le chemin complet vers l'application .exe
return Path(sys.executable).parent.resolve()
return Path(__file__).resolve().parents[2]
def quit_application(exit_code: int = 0) -> None:
app = QApplication.instance()
if app is not None:
app.closeAllWindows()
app.exit(exit_code)
sys.exit(exit_code)

View File

@@ -1,4 +1,3 @@
import sys
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QWidget, QGraphicsDropShadowEffect)
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
@@ -20,9 +19,6 @@ class CustomMessageBox(QDialog):
self.setAttribute(Qt.WA_TranslucentBackground)
self.setMinimumWidth(400)
color_main = "#101624"
color_accent = "#248277" if icon_type == self.INFO else "#cf5b16"
# --- ANIMATION DE FONDU ---
self.setWindowOpacity(0)
self.fade_anim = QPropertyAnimation(self, b"windowOpacity")
@@ -33,16 +29,10 @@ class CustomMessageBox(QDialog):
# --- UI SETUP ---
self.container = QWidget(self)
self.container.setObjectName("MainContainer")
self.container.setStyleSheet(f"""
QWidget#MainContainer {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 {color_main}, stop:1 {color_accent});
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}}
QLabel {{ color: white; font-family: 'Segoe UI'; }}
""")
self.container.setObjectName("MsgBoxMainContainer")
# Utilisé dans le fichier QSS comme condition dynamique de style
self.container.setProperty("iconType", icon_type)
self.container.setProperty("buttonsType", buttons)
# LAYOUT PRINCIPAL DU CONTAINER (Marges à 0 pour coller le bouton au bord)
layout = QVBoxLayout(self.container)
@@ -55,28 +45,13 @@ class CustomMessageBox(QDialog):
title_bar_layout.setSpacing(0)
title_label = QLabel(title.upper())
title_label.setStyleSheet(
"font-weight: bold; font-size: 10px; color: rgba(255,255,255,0.7); letter-spacing: 1px;")
title_label.setObjectName("MsgBoxTitleLabel")
self.close_btn = QPushButton("")
self.close_btn.setObjectName("MsgBoxCloseButton")
self.close_btn.setFixedSize(45, 35)
self.close_btn.clicked.connect(self.reject)
self.close_btn.setCursor(Qt.PointingHandCursor)
self.close_btn.setStyleSheet("""
QPushButton {
background: transparent;
color: white;
border: none;
font-size: 14px;
/* Rayon identique au container (15px) pour épouser parfaitement le coin */
border-top-right-radius: 15px;
border-bottom-left-radius: 10px;
}
QPushButton:hover {
background-color: #e74c3c;
color: white;
}
""")
title_bar_layout.addWidget(title_label)
title_bar_layout.addStretch()
@@ -91,12 +66,12 @@ class CustomMessageBox(QDialog):
# Contenu central (Icône + Message)
content_layout = QHBoxLayout()
icon_label = QLabel()
icon_label.setObjectName("MsgBoxIconLabel")
icon_text = "" if icon_type == self.INFO else "⚠️"
icon_label.setText(icon_text)
icon_label.setStyleSheet("font-size: 35px; margin-right: 10px;")
msg_label = QLabel(message)
msg_label.setStyleSheet("font-size: 14px; color: #f0f0f0;")
msg_label.setObjectName("MsgBoxMessageLabel")
msg_label.setWordWrap(True)
content_layout.addWidget(icon_label)
@@ -108,24 +83,14 @@ class CustomMessageBox(QDialog):
btn_layout.setSpacing(10)
btn_layout.addStretch()
style_btn_base = """
QPushButton {
background: #2a313d; border-radius: 6px; color: white;
padding: 8px 20px; font-weight: bold; font-size: 12px;
border: 1px solid rgba(255,255,255,0.05);
}
QPushButton:hover { background: #363d4a; border: 1px solid white; }
"""
if buttons == self.OK_CANCEL:
self.btn_cancel = QPushButton("ANNULER")
self.btn_cancel.setStyleSheet(style_btn_base)
self.btn_cancel.setObjectName("MsgBoxCancelButton")
self.btn_cancel.clicked.connect(self.reject)
btn_layout.addWidget(self.btn_cancel)
self.btn_ok = QPushButton("COMPRIS")
style_btn_ok = style_btn_base.replace("#2a313d", "#248277")
self.btn_ok.setStyleSheet(style_btn_ok)
self.btn_ok.setObjectName("MsgBoxOkButton")
self.btn_ok.setCursor(Qt.PointingHandCursor)
self.btn_ok.clicked.connect(self.accept)
btn_layout.addWidget(self.btn_ok)

75
src/ui/hazard_stripes.py Normal file
View File

@@ -0,0 +1,75 @@
# ui/hazard_stripes.py
from PySide6.QtWidgets import QPushButton, QStyleOptionButton, QStyle
from PySide6.QtGui import QPainter, QColor, QPainterPath, QPen, QPolygon
from PySide6.QtCore import Qt, QPoint
class HazardButton(QPushButton):
def __init__(self, parent=None):
super().__init__(parent)
self._hazard = False
def set_hazard(self, enabled: bool):
self._hazard = enabled
self.update()
def paintEvent(self, event):
if not hasattr(self, '_hazard'):
self.__dict__['_hazard'] = False
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
r = self.rect()
radius = 4
path = QPainterPath()
path.addRoundedRect(r, radius, radius)
p.setClipPath(path)
if self._hazard:
p.fillRect(r, QColor("#FFD700"))
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor("#000000"))
stripe_width = 20
stripe_gap = 30
period = stripe_width + stripe_gap
diag = r.width() + r.height()
for x in range(-diag, diag * 2, period):
stripe = QPolygon([
QPoint(x, r.bottom() + 10),
QPoint(x + stripe_width, r.bottom() + 10),
QPoint(x + stripe_width + r.height() + 10, r.top() - 10),
QPoint(x + r.height() + 10, r.top() - 10),
])
p.drawPolygon(stripe)
# ↓ Fond semi-transparent derrière le texte
text_bg = QColor(255, 215, 0, 230) # noir à 63% d'opacité
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(text_bg)
bg_rect = r.adjusted(60, 8, -60, -8) # marges internes
p.drawRoundedRect(bg_rect, 4, 4)
else:
p.fillRect(r, QColor("#FFD700"))
p.setClipping(False)
p.setPen(QPen(QColor("#000000"), 2))
p.setBrush(Qt.BrushStyle.NoBrush)
p.drawRoundedRect(r.adjusted(1, 1, -1, -1), radius, radius)
p.setClipping(False)
opt = QStyleOptionButton()
self.initStyleOption(opt)
opt.palette.setColor(
opt.palette.ColorRole.ButtonText,
self.palette().color(self.palette().ColorRole.ButtonText)
)
self.style().drawControl(
QStyle.ControlElement.CE_PushButtonLabel, opt, p, self
)

View File

@@ -8,11 +8,14 @@ from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QMainWindow, QSizePolicy
from config.config_manager import ConfigManager
from config.constants import NO_STAFF, Urls, NO_WHITELIST
from ui.custom_message_box import CustomMessageBox
from ui.hazard_stripes import HazardButton
from controllers.audio_controller import AudioController
from controllers.glow_animator import GlowAnimator
from controllers.window_dragger import WindowDragger
from tools import discord_oauth
from tools.constants import NO_STAFF, Urls
from discord import discord_oauth
from tools.utils import quit_application
from fake_patch_notes import patch_note
@@ -32,6 +35,29 @@ class MainWindow(QMainWindow):
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# Test bouton en contruction
en_chantier = True
if en_chantier:
old_btn = self.ui.connexion_btn
parent_layout = self.ui.verticalLayout_6 # layout direct du bouton dans le .ui
index = parent_layout.indexOf(old_btn)
new_btn = HazardButton(old_btn.parentWidget())
new_btn.setObjectName("connexion_btn")
new_btn.setText("EN MAINTENANCE")
new_btn.setIcon(old_btn.icon())
new_btn.setIconSize(old_btn.iconSize())
new_btn.setMinimumSize(old_btn.minimumSize())
new_btn.set_hazard(True)
parent_layout.takeAt(index)
old_btn.deleteLater()
parent_layout.insertWidget(index, new_btn)
self.ui.connexion_btn = new_btn
self.ui.connexion_btn.clicked.connect(self._on_connexion)
# centrage vertical du bouton connexion
if NO_STAFF:
self.ui.staff_btn.hide()
@@ -61,6 +87,18 @@ class MainWindow(QMainWindow):
self._center_window()
self.show()
if NO_WHITELIST:
msg = CustomMessageBox(
title="La Tanière: Non whitelisté",
message="\n\nTu n'est pas whitelisté sur le serveur\n\n"
"Assure-toi de te faire whitelister.\n\n"
"Lorsque cela sera fait, relance le launcher.",
icon_type=CustomMessageBox.WARNING,
buttons=CustomMessageBox.OK
)
msg.exec()
quit_application()
# ------------------------------------------------------------------
# Setup
# ------------------------------------------------------------------

View File

@@ -45,7 +45,7 @@ QLabel#queue_lbl {
}
QPushButton#connexion_btn {
QPushButton#connexion_btn[chantier="false"] {
/* Dégradé chaleureux : Orange vers Orange-Rouge */
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop: 0 #ff9d00,
@@ -57,7 +57,7 @@ QPushButton#connexion_btn {
padding: 10px;
}
QPushButton#connexion_btn:hover {
QPushButton#connexion_btn[chantier="false"]:hover {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop: 0 #ffb338,
stop: 1 #ff7a29);
@@ -66,7 +66,7 @@ QPushButton#connexion_btn:hover {
outline: none;
}
QPushButton#connexionBtn:pressed {
QPushButton#connexion_btn[chantier="false"]:pressed {
background: #cc5200;
padding-top: 12px; /* Effet d'enfoncement */
}
@@ -81,6 +81,21 @@ QPushButton#staff_btn {
padding: 5px 15px;
}
HazardButton#connexion_btn {
color: #0A1A3A;
/* color: #0D2A6B;*/
font-weight: bold;
}
HazardButton#connexion_btn:hover {
/* color: #ffffff;*/
color: #0D2A6B;
}
HazardButton#connexion_btn:pressed {
color: #333333;
}
QPushButton#staff_btn:hover {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #FF6046, stop:1 #FF527B);
@@ -209,3 +224,92 @@ QSlider::handle:horizontal:pressed {
background: #E65100;
border-radius: 8px;
}
/* ----------------------------------------------
Custom Message Box
----------------------------------------------*/
QWidget#MsgBoxMainContainer {
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
QWidget#MsgBoxMainContainer[iconType="info"] {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 #101624,
stop: 1 #248277
);
}
QWidget#MsgBoxMainContainer[iconType="warning"] {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 #101624,
stop: 1 #cf5b16
);
}
QWidget#MsgBoxMainContainer QLabel,
QWidget#MsgBoxMainContainer QPushButton
{
color: white;
/*font-size: 18px;
/* font-family: 'Segoe UI';*/
}
QPushButton#MsgBoxOkButton,
QPushButton#MsgBoxCancelButton {
border-radius: 6px;
/* color: white;*/
padding: 8px 20px;
font-weight: bold;
font-size: 12px;
border: 1px solid rgba(255,255,255,0.05);
}
QPushButton#MsgBoxOkButton {
background: #248277;
}
QPushButton#MsgBoxCancelButton {
background: #2a313d;
}
QPushButton#MsgBoxOkButton:hover,
QPushButton#MsgBoxCancelButton:hover{
background: #363d4a;
border: 1px solid white;
}
QPushButton#MsgBoxCloseButton {
background: transparent;
color: white;
border: none;
font-size: 14px;
/* Rayon identique au container (15px) pour épouser parfaitement le coin */
border-top-right-radius: 15px;
border-bottom-left-radius: 10px;
}
QPushButton#MsgBoxCloseButton:hover {
background-color: #e74c3c;
color: white;
}
QLabel#MsgBoxTitleLabel {
font-weight: bold;
font-size: 14px;
color: rgba(255,255,255,0.7);
letter-spacing: 1px;
}
QLabel#MsgBoxMessageLabel {
font-size: 14px;
color: #f0f0f0;
}
QLabel#MsgBoxIconLabel {
font-size: 35px;
margin-right: 10px;
}