Compare commits
10 Commits
8a87fe38c8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b7ef4c951 | |||
| 82679a5709 | |||
| 93c37a905b | |||
| 945abae5f1 | |||
| 720b004eca | |||
| 54aa7a50b2 | |||
| 1125e5ee10 | |||
| 5d2282def1 | |||
| 968d88c18d | |||
| 8a3e487df6 |
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,7 +8,11 @@ a = Analysis(
|
|||||||
('.\\styles\\styles.qss', 'styles'),
|
('.\\styles\\styles.qss', 'styles'),
|
||||||
('.\\ui\\mainwindow_vertical_pager.ui', 'ui')
|
('.\\ui\\mainwindow_vertical_pager.ui', 'ui')
|
||||||
],
|
],
|
||||||
hiddenimports=[],
|
hiddenimports=[
|
||||||
|
"asyncio",
|
||||||
|
"pypresence",
|
||||||
|
"pypresence.baseclient",
|
||||||
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={
|
hooksconfig={
|
||||||
"qt_plugins": ["platforms", "styles"]
|
"qt_plugins": ["platforms", "styles"]
|
||||||
@@ -107,8 +111,8 @@ a = Analysis(
|
|||||||
|
|
||||||
# Concurrency non utilisée dans ton code
|
# Concurrency non utilisée dans ton code
|
||||||
"multiprocessing",
|
"multiprocessing",
|
||||||
"concurrent",
|
#"concurrent",
|
||||||
"asyncio",
|
#"asyncio",
|
||||||
|
|
||||||
# REPL / terminal
|
# REPL / terminal
|
||||||
"readline",
|
"readline",
|
||||||
@@ -218,6 +222,15 @@ a.binaries = [
|
|||||||
if not any(u.lower() in name.lower() for u in unwanted_dlls)
|
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)
|
pyz = PYZ(a.pure, a.zipped_data)
|
||||||
|
|
||||||
exe = EXE(
|
exe = EXE(
|
||||||
@@ -243,7 +256,7 @@ exe = EXE(
|
|||||||
"msvcp*.dll",
|
"msvcp*.dll",
|
||||||
],
|
],
|
||||||
runtime_tmpdir=None,
|
runtime_tmpdir=None,
|
||||||
console=True,
|
console=False,
|
||||||
disable_windowed_traceback=True,
|
disable_windowed_traceback=True,
|
||||||
argv_emulation=False,
|
argv_emulation=False,
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
|
|||||||
@@ -7,8 +7,23 @@ from PySide6.QtGui import QColor
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
NO_STAFF = True
|
NO_STAFF = True
|
||||||
NO_DISCORD = 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
|
# ENUMS
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -3,7 +3,7 @@ from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
|
|||||||
|
|
||||||
from config.config_manager import ConfigManager, VOLUME_KEY
|
from config.config_manager import ConfigManager, VOLUME_KEY
|
||||||
|
|
||||||
from tools.constants import Resources
|
from config.constants import Resources
|
||||||
|
|
||||||
class AudioController:
|
class AudioController:
|
||||||
# Encapsule toute la logique audio : lecture, volume, mute.
|
# Encapsule toute la logique audio : lecture, volume, mute.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from PySide6.QtCore import QPropertyAnimation, QEasingCurve
|
from PySide6.QtCore import QPropertyAnimation, QEasingCurve
|
||||||
from PySide6.QtWidgets import QGraphicsDropShadowEffect
|
from PySide6.QtWidgets import QGraphicsDropShadowEffect
|
||||||
|
|
||||||
from tools.constants import Glow
|
from config.constants import Glow
|
||||||
|
|
||||||
|
|
||||||
class GlowAnimator:
|
class GlowAnimator:
|
||||||
|
|||||||
@@ -3,31 +3,23 @@ import webbrowser
|
|||||||
import os
|
import os
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
from 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
|
# Disable stderr output
|
||||||
os.environ['PYTHONWARNINGS'] = 'ignore'
|
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):
|
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
||||||
code: str | None = None
|
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):
|
def do_GET(self):
|
||||||
|
"""
|
||||||
|
callback pour discord auth
|
||||||
|
"""
|
||||||
if "/callback" in self.path:
|
if "/callback" in self.path:
|
||||||
query = self.path.split("?")[1]
|
query = self.path.split("?")[1]
|
||||||
params = dict(p.split("=") for p in query.split("&"))
|
params = dict(p.split("=") for p in query.split("&"))
|
||||||
@@ -40,11 +32,19 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
# return discord application id (client id)
|
# return discord application id (client id)
|
||||||
def get_discord_client_id() -> str:
|
def get_discord_client_id() -> str:
|
||||||
|
"""
|
||||||
|
return discord application id
|
||||||
|
"""
|
||||||
return CLIENT_ID
|
return CLIENT_ID
|
||||||
|
|
||||||
# return discord user id
|
# return discord user id
|
||||||
def get_discord_user_id() -> str:
|
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()
|
session_id = GetServerTokenForDiscord.authenticate()
|
||||||
client_secret = GetServerTokenForDiscord.get_token(session_id)
|
client_secret = GetServerTokenForDiscord.get_token(session_id)
|
||||||
|
|
||||||
@@ -59,9 +59,6 @@ def get_discord_user_id() -> str:
|
|||||||
webbrowser.open(f"{auth_url}?{urlencode(params)}")
|
webbrowser.open(f"{auth_url}?{urlencode(params)}")
|
||||||
|
|
||||||
server = HTTPServer(("localhost", 5000), OAuthCallbackHandler)
|
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()
|
server.handle_request()
|
||||||
|
|
||||||
if not OAuthCallbackHandler.code:
|
if not OAuthCallbackHandler.code:
|
||||||
53
src/discord/discord_tools.py
Normal file
53
src/discord/discord_tools.py
Normal 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
|
||||||
@@ -5,16 +5,16 @@ from cryptography.hazmat.primitives.asymmetric import ec
|
|||||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
|
||||||
API_URL = 'https://prod.la-taniere.fun:30121/'
|
from config.constants import Urls
|
||||||
|
|
||||||
class GetServerTokenForDiscord:
|
class GetServerTokenForDiscord:
|
||||||
derived_key: bytes | None = None
|
derived_key: bytes | None = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def authenticate(server = API_URL):
|
def authenticate(server = Urls.API_URL.value):
|
||||||
|
|
||||||
if server is None:
|
if server is None:
|
||||||
server = API_URL
|
server = Urls.API_URL.value
|
||||||
# ==========================
|
# ==========================
|
||||||
# Génération clé ECDH client
|
# Génération clé ECDH client
|
||||||
# ==========================
|
# ==========================
|
||||||
@@ -49,12 +49,12 @@ class GetServerTokenForDiscord:
|
|||||||
return auth["session_id"]
|
return auth["session_id"]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_token(session_id: bytes, server = API_URL):
|
def get_token(session_id: bytes, server = Urls.API_URL.value):
|
||||||
# ==========================
|
# ==========================
|
||||||
# DISCORD TOKEN
|
# DISCORD TOKEN
|
||||||
# ==========================
|
# ==========================
|
||||||
if server is None:
|
if server is None:
|
||||||
server = API_URL
|
server = Urls.API_URL.value
|
||||||
download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={
|
download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={
|
||||||
"x-session-id": session_id
|
"x-session-id": session_id
|
||||||
}).json()
|
}).json()
|
||||||
@@ -65,12 +65,12 @@ class GetServerTokenForDiscord:
|
|||||||
aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
|
aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
|
||||||
return aesgcm.decrypt(nonce, encrypted_data, None)
|
return aesgcm.decrypt(nonce, encrypted_data, None)
|
||||||
|
|
||||||
# @staticmethod
|
@staticmethod
|
||||||
# def register_discord_user(user_id: str, server = API_URL) -> str:
|
def register_discord_user(user_id: str, server = Urls.API_URL.value) -> str:
|
||||||
# if server is None:
|
if server is None:
|
||||||
# server = API_URL
|
server = Urls.API_URL.value
|
||||||
# registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
|
registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
|
||||||
# "x-session-id": user_id
|
"x-session-id": user_id
|
||||||
# }).json()
|
}).json()
|
||||||
#
|
|
||||||
# return registeredId["discord_id"]
|
return registeredId["discord_id"]
|
||||||
23
src/fivemserver/whitelistmanager.py
Normal file
23
src/fivemserver/whitelistmanager.py
Normal 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
|
||||||
18
src/main.py
18
src/main.py
@@ -7,16 +7,18 @@ from PySide6.QtWidgets import QApplication
|
|||||||
|
|
||||||
# Imports pour la gestion de la configuration
|
# Imports pour la gestion de la configuration
|
||||||
from config.config_manager import ConfigManager
|
from config.config_manager import ConfigManager
|
||||||
|
from config.constants import Resources
|
||||||
|
|
||||||
# Imports pour la vérification Discord
|
# Imports pour la vérification Discord
|
||||||
from tools.discord_tools import CheckDiscord
|
from discord.discord_tools import CheckDiscord
|
||||||
from tools.custom_message_box import CustomMessageBox
|
|
||||||
|
# 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
|
# Ne pas supprimer ! Enregistre les ressources Qt
|
||||||
import resources # noqa: F401 - required to register Qt resources
|
import resources # noqa: F401 - required to register Qt resources
|
||||||
|
|
||||||
from ui.main_window import MainWindow
|
|
||||||
from tools.constants import Resources
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Bundle path resolution
|
# Bundle path resolution
|
||||||
@@ -60,7 +62,7 @@ if __name__ == "__main__":
|
|||||||
# 3. Garde-fou Discord
|
# 3. Garde-fou Discord
|
||||||
if not CheckDiscord.isdiscordrunning():
|
if not CheckDiscord.isdiscordrunning():
|
||||||
msg = CustomMessageBox(
|
msg = CustomMessageBox(
|
||||||
title="Launcher La Tanière: Discord non détecté",
|
title="La Tanière: Discord non détecté",
|
||||||
message="Discord ne semble pas lancé.\n\n"
|
message="Discord ne semble pas lancé.\n\n"
|
||||||
"Tu dois avoir démarré Discord et y être connecté pour utiliser l'application.\n\n"
|
"Tu dois avoir démarré Discord et y être connecté pour utiliser l'application.\n\n"
|
||||||
"Lorsque cela sera fait, relance le launcher.",
|
"Lorsque cela sera fait, relance le launcher.",
|
||||||
@@ -72,10 +74,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# On récupère l'ID stocké (sera "" si absent grâce au schéma)
|
# On récupère l'ID stocké (sera "" si absent grâce au schéma)
|
||||||
stored_user_id = config.get_discord_user()
|
stored_user_id = config.get_discord_user()
|
||||||
if stored_user_id != "": # si pas encore d'id dans la config
|
if stored_user_id != "" and not stored_user_id.isspace():
|
||||||
if not CheckDiscord.isuserconnected(stored_user_id):
|
if not CheckDiscord.isuserconnected():
|
||||||
msg = CustomMessageBox(
|
msg = CustomMessageBox(
|
||||||
title="Launcher La Tanière: connexion Discord",
|
title="La Tanière: connexion Discord",
|
||||||
message="Tu n'est pas connecté à Discord\n\n"
|
message="Tu n'est pas connecté à Discord\n\n"
|
||||||
"Assure-toi que tu es connecté à Discord.\n\n"
|
"Assure-toi que tu es connecté à Discord.\n\n"
|
||||||
"Lorsque cela sera fait, relance le launcher.",
|
"Lorsque cela sera fait, relance le launcher.",
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
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
|
# sys.executable est le chemin complet vers l'application .exe
|
||||||
return Path(sys.executable).parent.resolve()
|
return Path(sys.executable).parent.resolve()
|
||||||
return Path(__file__).resolve().parents[2]
|
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)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import sys
|
|
||||||
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout,
|
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout,
|
||||||
QLabel, QPushButton, QWidget, QGraphicsDropShadowEffect)
|
QLabel, QPushButton, QWidget, QGraphicsDropShadowEffect)
|
||||||
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
|
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
|
||||||
@@ -20,9 +19,6 @@ class CustomMessageBox(QDialog):
|
|||||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||||
self.setMinimumWidth(400)
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
color_main = "#101624"
|
|
||||||
color_accent = "#248277" if icon_type == self.INFO else "#cf5b16"
|
|
||||||
|
|
||||||
# --- ANIMATION DE FONDU ---
|
# --- ANIMATION DE FONDU ---
|
||||||
self.setWindowOpacity(0)
|
self.setWindowOpacity(0)
|
||||||
self.fade_anim = QPropertyAnimation(self, b"windowOpacity")
|
self.fade_anim = QPropertyAnimation(self, b"windowOpacity")
|
||||||
@@ -33,16 +29,10 @@ class CustomMessageBox(QDialog):
|
|||||||
|
|
||||||
# --- UI SETUP ---
|
# --- UI SETUP ---
|
||||||
self.container = QWidget(self)
|
self.container = QWidget(self)
|
||||||
self.container.setObjectName("MainContainer")
|
self.container.setObjectName("MsgBoxMainContainer")
|
||||||
self.container.setStyleSheet(f"""
|
# Utilisé dans le fichier QSS comme condition dynamique de style
|
||||||
QWidget#MainContainer {{
|
self.container.setProperty("iconType", icon_type)
|
||||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
self.container.setProperty("buttonsType", buttons)
|
||||||
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'; }}
|
|
||||||
""")
|
|
||||||
|
|
||||||
# LAYOUT PRINCIPAL DU CONTAINER (Marges à 0 pour coller le bouton au bord)
|
# LAYOUT PRINCIPAL DU CONTAINER (Marges à 0 pour coller le bouton au bord)
|
||||||
layout = QVBoxLayout(self.container)
|
layout = QVBoxLayout(self.container)
|
||||||
@@ -55,28 +45,13 @@ class CustomMessageBox(QDialog):
|
|||||||
title_bar_layout.setSpacing(0)
|
title_bar_layout.setSpacing(0)
|
||||||
|
|
||||||
title_label = QLabel(title.upper())
|
title_label = QLabel(title.upper())
|
||||||
title_label.setStyleSheet(
|
title_label.setObjectName("MsgBoxTitleLabel")
|
||||||
"font-weight: bold; font-size: 10px; color: rgba(255,255,255,0.7); letter-spacing: 1px;")
|
|
||||||
|
|
||||||
self.close_btn = QPushButton("✕")
|
self.close_btn = QPushButton("✕")
|
||||||
|
self.close_btn.setObjectName("MsgBoxCloseButton")
|
||||||
self.close_btn.setFixedSize(45, 35)
|
self.close_btn.setFixedSize(45, 35)
|
||||||
self.close_btn.clicked.connect(self.reject)
|
self.close_btn.clicked.connect(self.reject)
|
||||||
self.close_btn.setCursor(Qt.PointingHandCursor)
|
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.addWidget(title_label)
|
||||||
title_bar_layout.addStretch()
|
title_bar_layout.addStretch()
|
||||||
@@ -91,12 +66,12 @@ class CustomMessageBox(QDialog):
|
|||||||
# Contenu central (Icône + Message)
|
# Contenu central (Icône + Message)
|
||||||
content_layout = QHBoxLayout()
|
content_layout = QHBoxLayout()
|
||||||
icon_label = QLabel()
|
icon_label = QLabel()
|
||||||
|
icon_label.setObjectName("MsgBoxIconLabel")
|
||||||
icon_text = "ℹ️" if icon_type == self.INFO else "⚠️"
|
icon_text = "ℹ️" if icon_type == self.INFO else "⚠️"
|
||||||
icon_label.setText(icon_text)
|
icon_label.setText(icon_text)
|
||||||
icon_label.setStyleSheet("font-size: 35px; margin-right: 10px;")
|
|
||||||
|
|
||||||
msg_label = QLabel(message)
|
msg_label = QLabel(message)
|
||||||
msg_label.setStyleSheet("font-size: 14px; color: #f0f0f0;")
|
msg_label.setObjectName("MsgBoxMessageLabel")
|
||||||
msg_label.setWordWrap(True)
|
msg_label.setWordWrap(True)
|
||||||
|
|
||||||
content_layout.addWidget(icon_label)
|
content_layout.addWidget(icon_label)
|
||||||
@@ -108,24 +83,14 @@ class CustomMessageBox(QDialog):
|
|||||||
btn_layout.setSpacing(10)
|
btn_layout.setSpacing(10)
|
||||||
btn_layout.addStretch()
|
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:
|
if buttons == self.OK_CANCEL:
|
||||||
self.btn_cancel = QPushButton("ANNULER")
|
self.btn_cancel = QPushButton("ANNULER")
|
||||||
self.btn_cancel.setStyleSheet(style_btn_base)
|
self.btn_cancel.setObjectName("MsgBoxCancelButton")
|
||||||
self.btn_cancel.clicked.connect(self.reject)
|
self.btn_cancel.clicked.connect(self.reject)
|
||||||
btn_layout.addWidget(self.btn_cancel)
|
btn_layout.addWidget(self.btn_cancel)
|
||||||
|
|
||||||
self.btn_ok = QPushButton("COMPRIS")
|
self.btn_ok = QPushButton("COMPRIS")
|
||||||
style_btn_ok = style_btn_base.replace("#2a313d", "#248277")
|
self.btn_ok.setObjectName("MsgBoxOkButton")
|
||||||
self.btn_ok.setStyleSheet(style_btn_ok)
|
|
||||||
self.btn_ok.setCursor(Qt.PointingHandCursor)
|
self.btn_ok.setCursor(Qt.PointingHandCursor)
|
||||||
self.btn_ok.clicked.connect(self.accept)
|
self.btn_ok.clicked.connect(self.accept)
|
||||||
btn_layout.addWidget(self.btn_ok)
|
btn_layout.addWidget(self.btn_ok)
|
||||||
75
src/ui/hazard_stripes.py
Normal file
75
src/ui/hazard_stripes.py
Normal 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
|
||||||
|
)
|
||||||
@@ -8,10 +8,15 @@ from PySide6.QtUiTools import QUiLoader
|
|||||||
from PySide6.QtWidgets import QMainWindow, QSizePolicy
|
from PySide6.QtWidgets import QMainWindow, QSizePolicy
|
||||||
|
|
||||||
from config.config_manager import ConfigManager
|
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.audio_controller import AudioController
|
||||||
from controllers.glow_animator import GlowAnimator
|
from controllers.glow_animator import GlowAnimator
|
||||||
from controllers.window_dragger import WindowDragger
|
from controllers.window_dragger import WindowDragger
|
||||||
from tools.constants import NO_DISCORD, NO_STAFF, Urls
|
from discord import discord_oauth
|
||||||
|
from tools.utils import quit_application
|
||||||
|
|
||||||
from fake_patch_notes import patch_note
|
from fake_patch_notes import patch_note
|
||||||
|
|
||||||
# For Linux Wayland to authorize moving window
|
# For Linux Wayland to authorize moving window
|
||||||
@@ -30,6 +35,29 @@ class MainWindow(QMainWindow):
|
|||||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
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
|
# centrage vertical du bouton connexion
|
||||||
if NO_STAFF:
|
if NO_STAFF:
|
||||||
self.ui.staff_btn.hide()
|
self.ui.staff_btn.hide()
|
||||||
@@ -43,7 +71,7 @@ class MainWindow(QMainWindow):
|
|||||||
break
|
break
|
||||||
# self.ui.spacer_substitution.hide()
|
# self.ui.spacer_substitution.hide()
|
||||||
|
|
||||||
if NO_DISCORD:
|
if config_manager.get_discord_user() == "" or config_manager.get_discord_user().isspace():
|
||||||
self.ui.queue_lbl.hide()
|
self.ui.queue_lbl.hide()
|
||||||
self.ui.queue_position.hide()
|
self.ui.queue_position.hide()
|
||||||
self.ui.stackedWidget.setCurrentIndex(1)
|
self.ui.stackedWidget.setCurrentIndex(1)
|
||||||
@@ -59,6 +87,18 @@ class MainWindow(QMainWindow):
|
|||||||
self._center_window()
|
self._center_window()
|
||||||
self.show()
|
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
|
# Setup
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -98,6 +138,8 @@ class MainWindow(QMainWindow):
|
|||||||
self._glow.start()
|
self._glow.start()
|
||||||
|
|
||||||
def _on_discord_auth_btn(self) -> None:
|
def _on_discord_auth_btn(self) -> None:
|
||||||
|
self.config.set_discord_user(discord_oauth.get_discord_user_id())
|
||||||
|
self.config.save()
|
||||||
self.ui.stackedWidget.setCurrentIndex(0)
|
self.ui.stackedWidget.setCurrentIndex(0)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ QLabel#queue_lbl {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#connexion_btn {
|
QPushButton#connexion_btn[chantier="false"] {
|
||||||
/* Dégradé chaleureux : Orange vers Orange-Rouge */
|
/* Dégradé chaleureux : Orange vers Orange-Rouge */
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
stop: 0 #ff9d00,
|
stop: 0 #ff9d00,
|
||||||
@@ -57,7 +57,7 @@ QPushButton#connexion_btn {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#connexion_btn:hover {
|
QPushButton#connexion_btn[chantier="false"]:hover {
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
stop: 0 #ffb338,
|
stop: 0 #ffb338,
|
||||||
stop: 1 #ff7a29);
|
stop: 1 #ff7a29);
|
||||||
@@ -66,7 +66,7 @@ QPushButton#connexion_btn:hover {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#connexionBtn:pressed {
|
QPushButton#connexion_btn[chantier="false"]:pressed {
|
||||||
background: #cc5200;
|
background: #cc5200;
|
||||||
padding-top: 12px; /* Effet d'enfoncement */
|
padding-top: 12px; /* Effet d'enfoncement */
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,21 @@ QPushButton#staff_btn {
|
|||||||
padding: 5px 15px;
|
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 {
|
QPushButton#staff_btn:hover {
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||||
stop:0 #FF6046, stop:1 #FF527B);
|
stop:0 #FF6046, stop:1 #FF527B);
|
||||||
@@ -209,3 +224,92 @@ QSlider::handle:horizontal:pressed {
|
|||||||
background: #E65100;
|
background: #E65100;
|
||||||
border-radius: 8px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -640,7 +640,7 @@
|
|||||||
<enum>QFrame::Shape::NoFrame</enum>
|
<enum>QFrame::Shape::NoFrame</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="currentIndex">
|
<property name="currentIndex">
|
||||||
<number>1</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="page">
|
<widget class="QWidget" name="page">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
|
|||||||
Reference in New Issue
Block a user