Compare commits
17 Commits
8a87fe38c8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eccea5b9d | |||
| 177f224760 | |||
| bad0cb43bf | |||
| 335e709bc6 | |||
| b52d37c14f | |||
| 36370c4b80 | |||
| 7ecd952f08 | |||
| 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'),
|
||||
('.\\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(
|
||||
|
||||
5
assets/no_whitelist.svg
Normal file
5
assets/no_whitelist.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path color="#000000" d="M7.992 0A2.008 2.008 0 0 0 6 2H5c-.657 0-1.178.06-1.617.225-.439.164-.79.461-.998.838-.415.752-.37 1.673-.385 2.931v5.012c.015 1.258-.03 2.179.385 2.932.208.376.56.673.998.838.439.164.96.224 1.617.224h3.762a4.5 4.5 0 0 1-.498-1H5c-.592 0-1.005-.063-1.265-.16-.26-.098-.372-.203-.473-.387C3.06 13.087 3.015 12.259 3 11V6c.015-1.259.06-2.087.262-2.453.101-.184.213-.29.473-.387C3.995 3.062 4.408 3 5 3v.999h6V3c.593 0 1.006.063 1.266.16.26.098.371.203.473.387.201.366.247 1.194.261 2.453v2.031a4.5 4.5 0 0 1 1 .233v-2.27c-.015-1.258.031-2.179-.385-2.932a1.88 1.88 0 0 0-.998-.837C12.179 2.06 11.657 2 11 2H9.996a2.008 2.008 0 0 0-1.992-2zm.01 1c.559 0 1 .442 1 1a.99.99 0 0 1-1 1 .982.982 0 0 1-.922-.61A1.01 1.01 0 0 1 7.003 2c0-.558.441-1 1-1zM5 6v1h6.012V6zm0 2v1h4.674a4.5 4.5 0 0 1 1.338-.74V8zm-.01 2v1h3v-1z" fill="gray" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal;marker:none" white-space="normal"/>
|
||||
<path class="error" color="#000000" d="M12.5 9A3.5 3.5 0 0 0 9 12.5a3.5 3.5 0 0 0 3.5 3.5 3.5 3.5 0 0 0 3.5-3.5A3.5 3.5 0 0 0 12.5 9zm-.5 1h1v1.168c0 .348-.016.667-.047.957-.03.29-.069.581-.115.875h-.666a12.898 12.898 0 0 1-.125-.875 9.146 9.146 0 0 1-.047-.957zm.5 4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#f22c42" overflow="visible" style="marker:none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -49,3 +49,10 @@ Execute `python install.py`
|
||||
Note: in case if you aren't virtual environment, the script stop.
|
||||
|
||||
Enjoy.
|
||||
|
||||
## Generate resource file
|
||||
|
||||
`rcc -g python .\resources.qrc -o .\src\resources_rc.py`
|
||||
|
||||
You need to give complete path of rcc:
|
||||
`H:\Qt\6.10.2\mingw_64\bin\rcc.exe -g python resources.qrc -o .\src\resources.py`
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>assets/no_whitelist.svg</file>
|
||||
<file>assets/closed-store-info.svg</file>
|
||||
<file>assets/letter-i-info.svg</file>
|
||||
<file>assets/open-store-info.svg</file>
|
||||
|
||||
56
src/config/constants.py
Normal file
56
src/config/constants.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
FIVEMURL = "fivem://connect/prod.la-taniere.fun"
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
class Resources(Enum):
|
||||
MP3 = ':/assets/the-beat-of-nature.mp3'
|
||||
FONT = ':/assets/Avocado-Cake-Demo.otf'
|
||||
|
||||
class Urls(Enum):
|
||||
DISCORD = 'https://discord.gg/A7eanmSkp2'
|
||||
INTRANET = 'https://la-taniere.fun/connexion/'
|
||||
API_URL = 'https://prod.la-taniere.fun:30121/'
|
||||
|
||||
class ApiEndPoints(Enum):
|
||||
QUEUE_STATUS = '/queue/status/'
|
||||
QUEUE_LEAVE = '/queue/leave'
|
||||
QUEUE_JOIN = '/queue/join'
|
||||
REGISTER_USER = '/api_v2/connection/register'
|
||||
|
||||
class Glow(Enum):
|
||||
COLOR = QColor(255, 140, 0, 255)
|
||||
BLUR_BASE = 15
|
||||
BLUR_PEAK = 70
|
||||
ANIM_DURATION = 1200
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DATACLASS
|
||||
# ---------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class PlayerServerInfo:
|
||||
is_staff: bool = False
|
||||
is_whitelist: bool = False
|
||||
session_id: str = None
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
113
src/discord/discord_oauth.py
Normal file
113
src/discord/discord_oauth.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import os
|
||||
import webbrowser
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
from config.constants import AUTENTICATION_SUCCESS_MESSAGE, CLIENT_ID, REDIRECT_URI, SCOPES
|
||||
from fivemserver.get_server_token import GetServerTokenForDiscord
|
||||
from tools.http_client import ApiError, http_get, http_post
|
||||
|
||||
# Disable stderr output
|
||||
os.environ['PYTHONWARNINGS'] = 'ignore'
|
||||
|
||||
OAUTH_AUTHORIZE_URL = "https://discord.com/api/oauth2/authorize"
|
||||
OAUTH_TOKEN_URL = "https://discord.com/api/oauth2/token"
|
||||
DISCORD_ME_URL = "https://discord.com/api/users/@me"
|
||||
LOCAL_CALLBACK_HOST = "localhost"
|
||||
LOCAL_CALLBACK_PORT = 5000
|
||||
|
||||
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
|
||||
"""
|
||||
parsed_url = urlparse(self.path)
|
||||
if parsed_url.path != "/callback":
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
OAuthCallbackHandler.code = query_params.get("code", [None])[0]
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(AUTENTICATION_SUCCESS_MESSAGE)
|
||||
|
||||
# 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() -> tuple[str, str]:
|
||||
"""
|
||||
Retourne l'id du compte discord de l'utilisateur via l'oauh discord.
|
||||
"""
|
||||
|
||||
# récupération des infos serveur la tanière
|
||||
session_id = GetServerTokenForDiscord.authenticate()
|
||||
client_secret = GetServerTokenForDiscord.get_token(session_id)
|
||||
|
||||
auth_url = "https://discord.com/api/oauth2/authorize"
|
||||
params = {
|
||||
"client_id": CLIENT_ID,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"response_type": "code",
|
||||
"scope": " ".join(SCOPES),
|
||||
}
|
||||
|
||||
webbrowser.open(f"{auth_url}?{urlencode(params)}")
|
||||
|
||||
server = HTTPServer((LOCAL_CALLBACK_HOST, LOCAL_CALLBACK_PORT), OAuthCallbackHandler)
|
||||
|
||||
try:
|
||||
server.handle_request()
|
||||
finally:
|
||||
server.server_close()
|
||||
|
||||
if not OAuthCallbackHandler.code:
|
||||
raise ApiError("OAuth échoué : aucun code de validation reçu.")
|
||||
|
||||
try:
|
||||
token = http_post(
|
||||
OAUTH_TOKEN_URL,
|
||||
data={
|
||||
"client_id": CLIENT_ID,
|
||||
"client_secret": client_secret,
|
||||
"grant_type": "authorization_code",
|
||||
"code": OAuthCallbackHandler.code,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
).json()
|
||||
except ValueError as exc:
|
||||
raise ApiError("OAuth échoué : réponse JSON invalide lors de la récupération du token.",
|
||||
url=OAUTH_TOKEN_URL) from exc
|
||||
|
||||
access_token = token.get("access_token")
|
||||
if not access_token:
|
||||
raise ApiError("OAuth échoué : access_token manquant.", url=OAUTH_TOKEN_URL)
|
||||
|
||||
try:
|
||||
user = http_get(
|
||||
DISCORD_ME_URL,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
).json()
|
||||
except ValueError as exc:
|
||||
raise ApiError("OAuth échoué : réponse JSON invalide lors de la récupération du profil Discord.",
|
||||
url=DISCORD_ME_URL) from exc
|
||||
|
||||
user_id = user.get("id")
|
||||
if not user_id:
|
||||
raise ApiError("OAuth échoué : id utilisateur manquant.", url=DISCORD_ME_URL)
|
||||
|
||||
return user_id, session_id
|
||||
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
|
||||
20
src/fivemserver/fivemlauncher.py
Normal file
20
src/fivemserver/fivemlauncher.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from config.constants import FIVEMURL
|
||||
|
||||
class FiveMLauncher:
|
||||
def __init__(self, fivem_path: str):
|
||||
self.fivem_path = os.path.expandvars(fivem_path)
|
||||
|
||||
@staticmethod
|
||||
def launch():
|
||||
"""
|
||||
if not os.path.exists(self.fivem_path):
|
||||
raise FileNotFoundError("❌ FiveM.exe introuvable")
|
||||
|
||||
subprocess.Popen(self.fivem_path, shell=True)
|
||||
"""
|
||||
|
||||
#subprocess.Popen(f"explorer {FIVEMURL}")
|
||||
subprocess.Popen(r'explorer fivem://connect/prod.la-taniere.fun')
|
||||
113
src/fivemserver/get_server_token.py
Normal file
113
src/fivemserver/get_server_token.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import base64
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
from config.constants import Urls
|
||||
from tools.http_client import http_post, ApiError
|
||||
|
||||
|
||||
class GetServerTokenForDiscord:
|
||||
derived_key: bytes | None = None
|
||||
|
||||
@staticmethod
|
||||
def authenticate(server: str | None = Urls.API_URL.value) -> str:
|
||||
if server is None:
|
||||
server = Urls.API_URL.value
|
||||
|
||||
# ==========================
|
||||
# Génération clé ECDH client
|
||||
# ==========================
|
||||
client_private = ec.generate_private_key(ec.SECP256R1())
|
||||
client_public = client_private.public_key()
|
||||
|
||||
client_pub_pem = client_public.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# AUTH
|
||||
# ==========================
|
||||
try:
|
||||
auth = http_post(
|
||||
f"{server}/api_v2/auth",
|
||||
json={"client_pub": base64.b64encode(client_pub_pem).decode()},
|
||||
).json()
|
||||
except ApiError:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise ApiError("Réponse JSON invalide lors de l'authentification.", url=f"{server}/api_v2/auth") from exc
|
||||
|
||||
server_pub_b64 = auth.get("server_pub")
|
||||
session_id = auth.get("session_id")
|
||||
if not server_pub_b64 or not session_id:
|
||||
raise ApiError("Réponse d'authentification invalide : données manquantes.", url=f"{server}/api_v2/auth")
|
||||
|
||||
server_pub = serialization.load_pem_public_key(base64.b64decode(server_pub_b64))
|
||||
shared_key = client_private.exchange(ec.ECDH(), server_pub)
|
||||
|
||||
GetServerTokenForDiscord.derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=None,
|
||||
info=b"fivem-private-server"
|
||||
).derive(shared_key)
|
||||
|
||||
return auth["session_id"]
|
||||
|
||||
@staticmethod
|
||||
def get_token(session_id: bytes, server: str | None = Urls.API_URL.value) -> bytes:
|
||||
# ==========================
|
||||
# DISCORD TOKEN
|
||||
# ==========================
|
||||
if server is None:
|
||||
server = Urls.API_URL.value
|
||||
|
||||
if GetServerTokenForDiscord.derived_key is None:
|
||||
raise ApiError("Clé dérivée manquante : appelez authenticate() avant get_token().")
|
||||
|
||||
try:
|
||||
download = http_post(
|
||||
f"{server}/api_v2/tkn_auth",
|
||||
headers={"x-session-id": session_id},
|
||||
).json()
|
||||
except ApiError:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise ApiError("Réponse JSON invalide lors de la récupération du token.",
|
||||
url=f"{server}/api_v2/tkn_auth") from exc
|
||||
|
||||
nonce_b64 = download.get("nonce")
|
||||
encrypted_data_b64 = download.get("data")
|
||||
if not nonce_b64 or not encrypted_data_b64:
|
||||
raise ApiError("Réponse de token invalide : nonce ou data manquant.", url=f"{server}/api_v2/tkn_auth")
|
||||
|
||||
nonce = base64.b64decode(nonce_b64)
|
||||
encrypted_data = base64.b64decode(encrypted_data_b64)
|
||||
|
||||
aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
|
||||
return aesgcm.decrypt(nonce, encrypted_data, None)
|
||||
|
||||
@staticmethod
|
||||
def register_discord_user(
|
||||
discord_user_id: str, session_id: str, server: str | None = Urls.API_URL.value) -> bool:
|
||||
if server is None:
|
||||
server = Urls.API_URL.value
|
||||
|
||||
try:
|
||||
registration_data = http_post(
|
||||
f"{server}/api_v2/connection/register",
|
||||
headers={"x-session-id": session_id},
|
||||
json={"discord_id": discord_user_id},
|
||||
).json()
|
||||
except ApiError:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise ApiError(
|
||||
"Réponse JSON invalide lors de l'enregistrement Discord.",
|
||||
url=f"{server}/api_v2/connection/register",
|
||||
) from exc
|
||||
|
||||
return bool(registration_data.get("success", False))
|
||||
57
src/fivemserver/queuemanager.py
Normal file
57
src/fivemserver/queuemanager.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# queue_manager.py — Aucune dépendance Qt
|
||||
import requests
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from config.constants import Urls, ApiEndPoints
|
||||
|
||||
class QueueManager:
|
||||
def __init__(self, user_id: str, on_update: Callable[[str], None]):
|
||||
self.user_id = user_id
|
||||
self.on_update = on_update # Callback pour envoyer les mises à jour
|
||||
self._running = True
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
|
||||
def join_queue(self) -> dict:
|
||||
res = requests.post(
|
||||
f"{Urls.API_URL.value}{ApiEndPoints.QUEUE_JOIN.value}",
|
||||
json={"uuid": self.user_id},
|
||||
verify=False
|
||||
)
|
||||
return res.json()
|
||||
|
||||
def check_status(self) -> dict:
|
||||
res = requests.get(f"{Urls.API_URL.value}{ApiEndPoints.QUEUE_STATUS.value}/{self.user_id}")
|
||||
return res.json()
|
||||
|
||||
def leave_queue(self):
|
||||
requests.post(
|
||||
f"{Urls.API_URL.value}{ApiEndPoints.QUEUE_LEAVE.value}",
|
||||
json={"uuid": self.user_id},
|
||||
verify=False
|
||||
)
|
||||
|
||||
def start(self):
|
||||
join = self.join_queue()
|
||||
# print(f"[QueueManager] join response: {join}") # ← Debug
|
||||
|
||||
if join.get("position") == 0: # Position 0 = slot libre
|
||||
self.on_update("ok")
|
||||
return
|
||||
|
||||
self.on_update(f"position:{join.get('position')}:{join.get('queueSize')}")
|
||||
|
||||
while self._running:
|
||||
time.sleep(5)
|
||||
if not self._running:
|
||||
break
|
||||
|
||||
status = self.check_status()
|
||||
|
||||
if status.get("position") == 0: # Position 0 = c'est le tour
|
||||
self.on_update("ready")
|
||||
return
|
||||
else:
|
||||
self.on_update(f"position:{status.get('position')}:{status.get('queueSize')}")
|
||||
22
src/fivemserver/whitelistmanager.py
Normal file
22
src/fivemserver/whitelistmanager.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from tools.http_client import ApiError, http_get
|
||||
|
||||
from config.constants import PlayerServerInfo
|
||||
|
||||
WHITELIST_URL_ENDPOINT = "iswhitelist/"
|
||||
|
||||
|
||||
class WhiteList:
|
||||
@staticmethod
|
||||
def check_whitelist(url: str, discord_user_id: str) -> None:
|
||||
try:
|
||||
api_data = http_get(f"{url}{WHITELIST_URL_ENDPOINT}{discord_user_id}").json()
|
||||
except ApiError:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise ApiError(
|
||||
"Réponse JSON invalide lors de la vérification whitelist.",
|
||||
url=f"{url}{WHITELIST_URL_ENDPOINT}{discord_user_id}",
|
||||
) from exc
|
||||
|
||||
PlayerServerInfo.is_whitelist = api_data.get("whitelisted", False)
|
||||
PlayerServerInfo.is_staff = api_data.get("isStaff", 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
|
||||
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
|
||||
@@ -60,7 +62,7 @@ if __name__ == "__main__":
|
||||
# 3. Garde-fou Discord
|
||||
if not CheckDiscord.isdiscordrunning():
|
||||
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"
|
||||
"Tu dois avoir démarré Discord et y être connecté pour utiliser l'application.\n\n"
|
||||
"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)
|
||||
stored_user_id = config.get_discord_user()
|
||||
if stored_user_id != "": # si pas encore d'id dans la config
|
||||
if not CheckDiscord.isuserconnected(stored_user_id):
|
||||
if stored_user_id != "" and not stored_user_id.isspace():
|
||||
if not CheckDiscord.isuserconnected():
|
||||
msg = CustomMessageBox(
|
||||
title="Launcher La Tanière: connexion Discord",
|
||||
title="La Tanière: connexion Discord",
|
||||
message="Tu n'est pas connecté à Discord\n\n"
|
||||
"Assure-toi que tu es connecté à Discord.\n\n"
|
||||
"Lorsque cela sera fait, relance le launcher.",
|
||||
|
||||
121
src/resources.py
121
src/resources.py
@@ -181151,6 +181151,119 @@ ill:#60C3AB;\x22 cx\
|
||||
=\x22225.067\x22 cy=\x224\
|
||||
43.733\x22 r=\x2211.73\
|
||||
3\x22/>\x0d\x0a</svg>\
|
||||
\x00\x00\x06\xe2\
|
||||
<\
|
||||
?xml version=\x221.\
|
||||
0\x22 encoding=\x22utf\
|
||||
-8\x22?><!-- Upload\
|
||||
ed to: SVG Repo,\
|
||||
www.svgrepo.com\
|
||||
, Generator: SVG\
|
||||
Repo Mixer Tool\
|
||||
s -->\x0a<svg width\
|
||||
=\x22800px\x22 height=\
|
||||
\x22800px\x22 viewBox=\
|
||||
\x220 0 16 16\x22 xmln\
|
||||
s=\x22http://www.w3\
|
||||
.org/2000/svg\x22>\x0d\
|
||||
\x0a <path color\
|
||||
=\x22#000000\x22 d=\x22M7\
|
||||
.992 0A2.008 2.0\
|
||||
08 0 0 0 6 2H5c-\
|
||||
.657 0-1.178.06-\
|
||||
1.617.225-.439.1\
|
||||
64-.79.461-.998.\
|
||||
838-.415.752-.37\
|
||||
1.673-.385 2.93\
|
||||
1v5.012c.015 1.2\
|
||||
58-.03 2.179.385\
|
||||
2.932.208.376.5\
|
||||
6.673.998.838.43\
|
||||
9.164.96.224 1.6\
|
||||
17.224h3.762a4.5\
|
||||
4.5 0 0 1-.498-\
|
||||
1H5c-.592 0-1.00\
|
||||
5-.063-1.265-.16\
|
||||
-.26-.098-.372-.\
|
||||
203-.473-.387C3.\
|
||||
06 13.087 3.015 \
|
||||
12.259 3 11V6c.0\
|
||||
15-1.259.06-2.08\
|
||||
7.262-2.453.101-\
|
||||
.184.213-.29.473\
|
||||
-.387C3.995 3.06\
|
||||
2 4.408 3 5 3v.9\
|
||||
99h6V3c.593 0 1.\
|
||||
006.063 1.266.16\
|
||||
.26.098.371.203.\
|
||||
473.387.201.366.\
|
||||
247 1.194.261 2.\
|
||||
453v2.031a4.5 4.\
|
||||
5 0 0 1 1 .233v-\
|
||||
2.27c-.015-1.258\
|
||||
.031-2.179-.385-\
|
||||
2.932a1.88 1.88 \
|
||||
0 0 0-.998-.837C\
|
||||
12.179 2.06 11.6\
|
||||
57 2 11 2H9.996a\
|
||||
2.008 2.008 0 0 \
|
||||
0-1.992-2zm.01 1\
|
||||
c.559 0 1 .442 1\
|
||||
1a.99.99 0 0 1-\
|
||||
1 1 .982.982 0 0\
|
||||
1-.922-.61A1.01\
|
||||
1.01 0 0 1 7.00\
|
||||
3 2c0-.558.441-1\
|
||||
1-1zM5 6v1h6.01\
|
||||
2V6zm0 2v1h4.674\
|
||||
a4.5 4.5 0 0 1 1\
|
||||
.338-.74V8zm-.01\
|
||||
2v1h3v-1z\x22 fill\
|
||||
=\x22gray\x22 font-fam\
|
||||
ily=\x22sans-serif\x22\
|
||||
font-weight=\x2240\
|
||||
0\x22 overflow=\x22vis\
|
||||
ible\x22 style=\x22lin\
|
||||
e-height:normal;\
|
||||
text-indent:0;te\
|
||||
xt-align:start;t\
|
||||
ext-decoration-l\
|
||||
ine:none;text-de\
|
||||
coration-style:s\
|
||||
olid;text-decora\
|
||||
tion-color:#0000\
|
||||
00;text-transfor\
|
||||
m:none;isolation\
|
||||
:auto;mix-blend-\
|
||||
mode:normal;mark\
|
||||
er:none\x22 white-s\
|
||||
pace=\x22normal\x22/>\x0d\
|
||||
\x0a <path class\
|
||||
=\x22error\x22 color=\x22\
|
||||
#000000\x22 d=\x22M12.\
|
||||
5 9A3.5 3.5 0 0 \
|
||||
0 9 12.5a3.5 3.5\
|
||||
0 0 0 3.5 3.5 3\
|
||||
.5 3.5 0 0 0 3.5\
|
||||
-3.5A3.5 3.5 0 0\
|
||||
0 12.5 9zm-.5 1\
|
||||
h1v1.168c0 .348-\
|
||||
.016.667-.047.95\
|
||||
7-.03.29-.069.58\
|
||||
1-.115.875h-.666\
|
||||
a12.898 12.898 0\
|
||||
0 1-.125-.875 9\
|
||||
.146 9.146 0 0 1\
|
||||
-.047-.957zm.5 4\
|
||||
a.5.5 0 0 1 .5.5\
|
||||
.5.5 0 0 1-.5.5.\
|
||||
5.5 0 0 1-.5-.5.\
|
||||
5.5 0 0 1 .5-.5z\
|
||||
\x22 fill=\x22#f22c42\x22\
|
||||
overflow=\x22visib\
|
||||
le\x22 style=\x22marke\
|
||||
r:none\x22/>\x0d\x0a</svg\
|
||||
>\
|
||||
"
|
||||
|
||||
qt_resource_name = b"\
|
||||
@@ -181217,12 +181330,16 @@ qt_resource_name = b"\
|
||||
\x0cA\xab\xe7\
|
||||
\x00c\
|
||||
\x00o\x00m\x00p\x00u\x00t\x00e\x00r\x00-\x00t\x00v\x00.\x00s\x00v\x00g\
|
||||
\x00\x10\
|
||||
\x0b[_\x07\
|
||||
\x00n\
|
||||
\x00o\x00_\x00w\x00h\x00i\x00t\x00e\x00l\x00i\x00s\x00t\x00.\x00s\x00v\x00g\
|
||||
"
|
||||
|
||||
qt_resource_struct = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x02\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x02\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x01\xba\x00\x00\x00\x00\x00\x01\x00+\xdd\xc1\
|
||||
\x00\x00\x01\x9c\x0ft\xac\xa3\
|
||||
@@ -181240,6 +181357,8 @@ qt_resource_struct = b"\
|
||||
\x00\x00\x01\x9c\xd8\xa1X\x0f\
|
||||
\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00(\xfb*\
|
||||
\x00\x00\x01\x9c\xd9\xa2\xfa\xf9\
|
||||
\x00\x00\x02\x0e\x00\x00\x00\x00\x00\x01\x00,7\xc6\
|
||||
\x00\x00\x01\x9d\x16B\x01\xc7\
|
||||
\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00)\x83\x02\
|
||||
\x00\x00\x01\x9c\xed\xb0@\xf2\
|
||||
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00,1\x15\
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NO_STAFF = True
|
||||
NO_DISCORD = True
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ENUMS
|
||||
# ---------------------------------------------------------------------------
|
||||
class Resources(Enum):
|
||||
MP3 = ":/assets/the-beat-of-nature.mp3"
|
||||
FONT = ":/assets/Avocado-Cake-Demo.otf"
|
||||
|
||||
class Urls(Enum):
|
||||
DISCORD = "https://discord.gg/A7eanmSkp2"
|
||||
INTRANET = "https://la-taniere.fun/connexion/"
|
||||
API_URL = 'https://prod.la-taniere.fun:30121/'
|
||||
|
||||
class Glow(Enum):
|
||||
COLOR = QColor(255, 140, 0, 255)
|
||||
BLUR_BASE = 15
|
||||
BLUR_PEAK = 70
|
||||
ANIM_DURATION = 1200
|
||||
@@ -1,87 +0,0 @@
|
||||
import requests
|
||||
import webbrowser
|
||||
import os
|
||||
from urllib.parse import urlencode
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from get_server_token import GetServerTokenForDiscord
|
||||
|
||||
# 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
|
||||
|
||||
def do_GET(self):
|
||||
if "/callback" in self.path:
|
||||
query = self.path.split("?")[1]
|
||||
params = dict(p.split("=") for p in query.split("&"))
|
||||
OAuthCallbackHandler.code = params.get("code")
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(AUTENTICATION_SUCCESS_MESSAGE)
|
||||
|
||||
# return discord application id (client id)
|
||||
def get_discord_client_id() -> str:
|
||||
return CLIENT_ID
|
||||
|
||||
# return discord user id
|
||||
def get_discord_user_id() -> str:
|
||||
# récupération des infos serveur lataupe
|
||||
session_id = GetServerTokenForDiscord.authenticate()
|
||||
client_secret = GetServerTokenForDiscord.get_token(session_id)
|
||||
|
||||
auth_url = "https://discord.com/api/oauth2/authorize"
|
||||
params = {
|
||||
"client_id": CLIENT_ID,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"response_type": "code",
|
||||
"scope": " ".join(SCOPES),
|
||||
}
|
||||
|
||||
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:
|
||||
raise RuntimeError("OAuth échoué")
|
||||
|
||||
token = requests.post(
|
||||
"https://discord.com/api/oauth2/token",
|
||||
data={
|
||||
"client_id": CLIENT_ID,
|
||||
"client_secret": client_secret,
|
||||
"grant_type": "authorization_code",
|
||||
"code": OAuthCallbackHandler.code,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
).json()
|
||||
|
||||
user = requests.get(
|
||||
"https://discord.com/api/users/@me",
|
||||
headers={"Authorization": f"Bearer {token['access_token']}"},
|
||||
).json()
|
||||
|
||||
return user["id"]
|
||||
@@ -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,76 +0,0 @@
|
||||
import base64
|
||||
import requests
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
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/'
|
||||
|
||||
class GetServerTokenForDiscord:
|
||||
derived_key: bytes | None = None
|
||||
|
||||
@staticmethod
|
||||
def authenticate(server = API_URL):
|
||||
|
||||
if server is None:
|
||||
server = API_URL
|
||||
# ==========================
|
||||
# Génération clé ECDH client
|
||||
# ==========================
|
||||
client_private = ec.generate_private_key(ec.SECP256R1())
|
||||
client_public = client_private.public_key()
|
||||
|
||||
client_pub_pem = client_public.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# AUTH
|
||||
# ==========================
|
||||
auth = requests.post(server + "/api_v2/auth", verify=False, json={
|
||||
"client_pub": base64.b64encode(client_pub_pem).decode()
|
||||
}).json()
|
||||
|
||||
server_pub = serialization.load_pem_public_key(
|
||||
base64.b64decode(auth["server_pub"])
|
||||
)
|
||||
|
||||
shared_key = client_private.exchange(ec.ECDH(), server_pub)
|
||||
|
||||
GetServerTokenForDiscord.derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=None,
|
||||
info=b"fivem-private-server"
|
||||
).derive(shared_key)
|
||||
|
||||
return auth["session_id"]
|
||||
|
||||
@staticmethod
|
||||
def get_token(session_id: bytes, server = API_URL):
|
||||
# ==========================
|
||||
# DISCORD TOKEN
|
||||
# ==========================
|
||||
if server is None:
|
||||
server = API_URL
|
||||
download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={
|
||||
"x-session-id": session_id
|
||||
}).json()
|
||||
|
||||
nonce = base64.b64decode(download["nonce"])
|
||||
encrypted_data = base64.b64decode(download["data"])
|
||||
|
||||
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"]
|
||||
73
src/tools/http_client.py
Normal file
73
src/tools/http_client.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Literal
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT = 15
|
||||
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ApiError(RuntimeError):
|
||||
def __init__(self, message: str, *, url: str | None = None, status_code: int | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.url = url
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def http_request(
|
||||
method: HttpMethod,
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
json: dict[str, Any] | None = None,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> requests.Response:
|
||||
try:
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=data,
|
||||
json=json,
|
||||
verify=False,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
except requests.RequestException as exc:
|
||||
status_code = getattr(getattr(exc, "response", None), "status_code", None)
|
||||
logger.warning("HTTP %s failed for %s (%s)", method, url, status_code or "no-status")
|
||||
raise ApiError(
|
||||
f"Erreur HTTP sur {method} {url}",
|
||||
url=url,
|
||||
status_code=status_code,
|
||||
) from exc
|
||||
|
||||
|
||||
def http_get(
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> requests.Response:
|
||||
return http_request("GET", url, headers=headers, params=params, timeout=timeout)
|
||||
|
||||
|
||||
def http_post(
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
json: dict[str, Any] | None = None,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> requests.Response:
|
||||
return http_request("POST", url, headers=headers, data=data, json=json, timeout=timeout)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
14
src/ui/error_dialog.py
Normal file
14
src/ui/error_dialog.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from ui.custom_message_box import CustomMessageBox
|
||||
|
||||
|
||||
def show_qt_error(parent: QWidget | None, title: str, message: str) -> None:
|
||||
msg = CustomMessageBox(
|
||||
title=title,
|
||||
message=message,
|
||||
icon_type=CustomMessageBox.WARNING,
|
||||
buttons=CustomMessageBox.OK,
|
||||
parent=parent,
|
||||
)
|
||||
msg.exec()
|
||||
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
|
||||
)
|
||||
@@ -6,12 +6,21 @@ from PySide6 import QtGui
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtUiTools import QUiLoader
|
||||
from PySide6.QtWidgets import QMainWindow, QSizePolicy
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
from config.config_manager import ConfigManager
|
||||
from config.constants import PlayerServerInfo, Urls
|
||||
from tools.http_client import ApiError
|
||||
from ui.error_dialog import show_qt_error
|
||||
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.constants import NO_DISCORD, NO_STAFF, Urls
|
||||
from discord import discord_oauth
|
||||
from fivemserver.whitelistmanager import WhiteList
|
||||
from fivemserver.fivemlauncher import FiveMLauncher
|
||||
from fivemserver.queuemanager import QueueManager
|
||||
from fivemserver.get_server_token import GetServerTokenForDiscord
|
||||
from fake_patch_notes import patch_note
|
||||
|
||||
# For Linux Wayland to authorize moving window
|
||||
@@ -19,10 +28,14 @@ if platform.startswith('linux'):
|
||||
environ["QT_QPA_PLATFORM"] = "xcb"
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
#update = Signal(str) # Reçoit les callbacks de QueueManager
|
||||
|
||||
def __init__(self, bundle_dir: str, config_manager: ConfigManager):
|
||||
super().__init__()
|
||||
|
||||
self.config = config_manager
|
||||
self.stored_user_id = self.config.get_discord_user()
|
||||
self.queue_thread = None
|
||||
|
||||
# UI
|
||||
self.ui = QUiLoader().load(f"{bundle_dir}/ui/mainwindow_vertical_pager.ui", self)
|
||||
@@ -30,8 +43,60 @@ class MainWindow(QMainWindow):
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
# Par défaut on affiche la page normal pour la connexion au serveur
|
||||
self.ui.stackedWidget.setCurrentIndex(0)
|
||||
# On cache par défaut les infos liste d'attente
|
||||
self.ui.queue_lbl.hide()
|
||||
self.ui.queue_position.hide()
|
||||
|
||||
|
||||
# Si l'id discord = "" ou des espace, alors on affiche la page comme quoi faut être connecté à discord.
|
||||
if self.stored_user_id == "" or self.stored_user_id.isspace():
|
||||
self.ui.stackedWidget.setCurrentIndex(1)
|
||||
else:
|
||||
try:
|
||||
# on vérifie si le joueur est whitelisté
|
||||
WhiteList.check_whitelist(Urls.API_URL.value, self.stored_user_id)
|
||||
except ApiError as exc:
|
||||
show_qt_error(self, "La Tanière", f"Impossible de vérifier la whitelist.\n\n{exc}")
|
||||
PlayerServerInfo.is_whitelist = False
|
||||
PlayerServerInfo.is_staff = False
|
||||
|
||||
# si on est whitelisté, on démarre la file d'attente
|
||||
if PlayerServerInfo.is_whitelist:
|
||||
self.start_queue()
|
||||
self.ui.queue_lbl.show()
|
||||
self.ui.queue_position.show()
|
||||
else:
|
||||
self.ui.stackedWidget.setCurrentIndex(2)
|
||||
|
||||
# Test bouton en contruction
|
||||
en_chantier = False
|
||||
# on set la css du bouton en fonction de la valeur de la variable en_chantier
|
||||
self.set_en_chantier(en_chantier)
|
||||
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:
|
||||
if not PlayerServerInfo.is_staff:
|
||||
self.ui.staff_btn.hide()
|
||||
layout = self.ui.verticalLayout_6
|
||||
# Trouver et modifier le spacer item
|
||||
@@ -41,12 +106,6 @@ class MainWindow(QMainWindow):
|
||||
item.spacerItem().changeSize(20, 15, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
layout.invalidate() # Forcer le recalcul du layout
|
||||
break
|
||||
# self.ui.spacer_substitution.hide()
|
||||
|
||||
if NO_DISCORD:
|
||||
self.ui.queue_lbl.hide()
|
||||
self.ui.queue_position.hide()
|
||||
self.ui.stackedWidget.setCurrentIndex(1)
|
||||
|
||||
self.ui.info_text.setMarkdown(patch_note)
|
||||
|
||||
@@ -57,8 +116,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._connect_signals()
|
||||
self._center_window()
|
||||
|
||||
self.show()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Setup
|
||||
# ------------------------------------------------------------------
|
||||
@@ -69,8 +130,8 @@ class MainWindow(QMainWindow):
|
||||
self.ui.connexion_btn.clicked.connect(self._on_connexion)
|
||||
self.ui.discord_btn.clicked.connect(self._on_discord)
|
||||
self.ui.intranet_btn.clicked.connect(self._on_intranet)
|
||||
|
||||
self.ui.discord_auth_btn.clicked.connect(self._on_discord_auth_btn)
|
||||
self.ui.no_whitelist_btn.clicked.connect(self.close)
|
||||
|
||||
def _center_window(self) -> None:
|
||||
self.adjustSize()
|
||||
@@ -87,7 +148,12 @@ class MainWindow(QMainWindow):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_connexion(self) -> None:
|
||||
pass # à implémenter
|
||||
try:
|
||||
session_id = GetServerTokenForDiscord.authenticate(Urls.API_URL.value)
|
||||
GetServerTokenForDiscord.register_discord_user(self.stored_user_id, session_id)
|
||||
FiveMLauncher.launch()
|
||||
except ApiError as exc:
|
||||
show_qt_error(self, "Connexion impossible", f"Erreur lors de la connexion.\n\n{exc}")
|
||||
|
||||
@staticmethod
|
||||
def _on_discord() -> None:
|
||||
@@ -98,8 +164,14 @@ class MainWindow(QMainWindow):
|
||||
self._glow.start()
|
||||
|
||||
def _on_discord_auth_btn(self) -> None:
|
||||
try:
|
||||
test = discord_oauth.get_discord_user_id()
|
||||
self.config.set_discord_user(test[0])
|
||||
PlayerServerInfo.session_id = test[1]
|
||||
self.config.save()
|
||||
self.ui.stackedWidget.setCurrentIndex(0)
|
||||
|
||||
except ApiError as exc:
|
||||
show_qt_error(self, "Connexion Discord", f"Impossible de récupérer ton compte Discord.\n\n{exc}")
|
||||
# ------------------------------------------------------------------
|
||||
# Mouse events → délégués au WindowDragger
|
||||
# ------------------------------------------------------------------
|
||||
@@ -121,5 +193,70 @@ class MainWindow(QMainWindow):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
if self.queue_thread and self.queue_thread.isRunning():
|
||||
self.queue_thread.stop()
|
||||
self.queue_thread.wait() # Attend que le thread se termine proprement
|
||||
self.config.save()
|
||||
super().closeEvent(event)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Change ui on runtime
|
||||
# ------------------------------------------------------------------
|
||||
def set_en_chantier(self, valeur: bool):
|
||||
self.en_chantier = valeur # ta variable Python
|
||||
self.ui.connexion_btn.setProperty("en_chantier", valeur) # propriété Qt
|
||||
self.ui.connexion_btn.style().unpolish(self.ui.connexion_btn)
|
||||
self.ui.connexion_btn.style().polish(self.ui.connexion_btn)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Queue managment
|
||||
# ------------------------------------------------------------------
|
||||
def start_queue(self):
|
||||
self.queue_thread = QueueThread(self.stored_user_id)
|
||||
self.queue_thread = QueueThread(self.stored_user_id, parent=self) # ← parent=self
|
||||
self.queue_thread.update.connect(self.handle_update)
|
||||
self.queue_thread.start()
|
||||
|
||||
# 🧪 TEMP - Simule une position en queue pour tester l'UI
|
||||
self.handle_update("position:3:10")
|
||||
|
||||
def handle_update(self, message: str):
|
||||
# print(f"[handle_update] reçu: {message}") # ← Debug
|
||||
if message == "ok":
|
||||
self.ui.queue_lbl.setVisible(False)
|
||||
self.ui.queue_position.setVisible(False)
|
||||
#self.ui.connexion_btn.setEnabled(True)
|
||||
#self.ui.connexion_btn.setText("Lancer FiveM")
|
||||
#self.launch_fivem()
|
||||
|
||||
elif message == "ready":
|
||||
self.ui.queue_lbl.setVisible(True)
|
||||
self.ui.queue_position.setVisible(False)
|
||||
self.ui.queue_lbl.setText("🚀 C'est votre tour !")
|
||||
self.launch_fivem()
|
||||
|
||||
elif message.startswith("position:"):
|
||||
_, pos, total = message.split(":")
|
||||
self.ui.queue_lbl.setVisible(True)
|
||||
self.ui.queue_position.setVisible(True)
|
||||
#self.ui.queue_position.setText(f"{pos} / {total}") <- si on veux le total de slots
|
||||
self.ui.queue_position.setText(f"{pos}")
|
||||
|
||||
def launch_fivem(self):
|
||||
pass
|
||||
|
||||
class QueueThread(QThread):
|
||||
update = Signal(str)
|
||||
|
||||
def __init__(self, user_id: str, parent=None): # ← parent=None
|
||||
super().__init__(parent) # ← passé à QThread
|
||||
self.manager = QueueManager(
|
||||
user_id=user_id,
|
||||
on_update=self.update.emit
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.manager.start()
|
||||
|
||||
def stop(self):
|
||||
self.manager.stop()
|
||||
|
||||
@@ -15,19 +15,17 @@ QFrame#logo_frame {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
QFrame#frame_2 QLabel {
|
||||
QFrame#frame_2 QLabel,
|
||||
QFrame#frame_6 QLabel {
|
||||
color: rgb(163, 177, 198);
|
||||
}
|
||||
|
||||
QFrame#frame_2 QLabel#discord_title_label {
|
||||
QFrame#frame_2 QLabel#discord_info_title_label,
|
||||
QFrame#frame_6 QLabel#whitelist_info_title_label {
|
||||
font-size: 28px;
|
||||
color: rgb(255, 255, 255) /* label enfant, obligé de définir la couleur car ne prend pas la général */
|
||||
}
|
||||
|
||||
QFrame#frame_2 QLabel#label_2 {
|
||||
/* font-family: 'sans serif'; */
|
||||
}
|
||||
|
||||
QLabel#maintitle_label {
|
||||
font-size: 38px;
|
||||
}
|
||||
@@ -45,7 +43,8 @@ QLabel#queue_lbl {
|
||||
|
||||
}
|
||||
|
||||
QPushButton#connexion_btn {
|
||||
/* ------------------------------------ BUTTONS --------------------------------------------------*/
|
||||
QPushButton#connexion_btn[en_chantier="false"] {
|
||||
/* Dégradé chaleureux : Orange vers Orange-Rouge */
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop: 0 #ff9d00,
|
||||
@@ -57,7 +56,7 @@ QPushButton#connexion_btn {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
QPushButton#connexion_btn:hover {
|
||||
QPushButton#connexion_btn[en_chantier="false"]:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop: 0 #ffb338,
|
||||
stop: 1 #ff7a29);
|
||||
@@ -66,7 +65,7 @@ QPushButton#connexion_btn:hover {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QPushButton#connexionBtn:pressed {
|
||||
QPushButton#connexion_btn[en_chantier="false"]:pressed {
|
||||
background: #cc5200;
|
||||
padding-top: 12px; /* Effet d'enfoncement */
|
||||
}
|
||||
@@ -74,29 +73,37 @@ QPushButton#connexionBtn:pressed {
|
||||
/* État normal - Rouge Corail Vibrant */
|
||||
QPushButton#staff_btn {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #FF4B2B, stop:1 #FF416C);
|
||||
color: white;
|
||||
stop: 0 #FF4B2B,
|
||||
stop: 1 #FF416C);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #d03522;
|
||||
padding: 5px 15px;
|
||||
color: white;
|
||||
/* padding: 5px 15px;*/
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
QPushButton#staff_btn:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #FF6046, stop:1 #FF527B);
|
||||
border: 1px solid #FF4B2B;
|
||||
stop: 0 #FF6046,
|
||||
stop: 1 #FF527B);
|
||||
/* border: 1px solid #FF4B2B;*/
|
||||
border: 1px solid #FFFFFF;
|
||||
/* Un léger halo autour du bouton */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QPushButton#staff_btn:pressed
|
||||
{
|
||||
background-color: #d03522;
|
||||
padding-top: 7px;
|
||||
padding-left: 17px;
|
||||
/* padding-top: 7px;*/
|
||||
/* padding-left: 17px;*/
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
QPushButton#discord_btn,
|
||||
QPushButton#discord_auth_btn,
|
||||
QPushButton#intranet_btn
|
||||
QPushButton#intranet_btn,
|
||||
QPushButton#no_whitelist_btn
|
||||
{
|
||||
background-color: rgba(32, 58, 67, 0.6); /* Bleu très sombre semi-transparent */
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
@@ -107,7 +114,8 @@ QPushButton#intranet_btn
|
||||
}
|
||||
|
||||
QPushButton#discord_btn:hover,
|
||||
QPushButton#discord_auth_btn:hover
|
||||
QPushButton#discord_auth_btn:hover,
|
||||
QPushButton#no_whitelist_btn:hover
|
||||
{
|
||||
background-color: rgba(88, 101, 242, 0.4); /* Fond bleu Discord translucide */
|
||||
border: 2px solid #7289da; /* Bordure plus épaisse et claire pour l'éclat */
|
||||
@@ -156,6 +164,23 @@ QPushButton#minimize_btn {
|
||||
padding-top: 0
|
||||
}
|
||||
|
||||
HazardButton#connexion_btn {
|
||||
color: #0A1A3A;
|
||||
/* color: #0D2A6B;*/
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
HazardButton#connexion_btn:hover {
|
||||
/* color: #ffffff;*/
|
||||
color: #0D2A6B;
|
||||
}
|
||||
|
||||
HazardButton#connexion_btn:pressed {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* ------------------------------------ Other --------------------------------------------------*/
|
||||
|
||||
QFrame#info_frame{
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0,
|
||||
@@ -209,3 +234,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;
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="queue_position">
|
||||
<property name="text">
|
||||
<string>20</string>
|
||||
<string>-</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
@@ -627,7 +627,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>0</height>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
@@ -640,39 +640,39 @@
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
<number>2</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="page">
|
||||
<widget class="QWidget" name="main_page">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>650</height>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>650</height>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>680</width>
|
||||
<height>650</height>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>650</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>680</width>
|
||||
<width>700</width>
|
||||
<height>650</height>
|
||||
</size>
|
||||
</property>
|
||||
@@ -907,6 +907,22 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="info_frame">
|
||||
<property name="sizePolicy">
|
||||
@@ -959,22 +975,6 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>170</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
@@ -982,20 +982,20 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>650</height>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>650</height>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<widget class="QFrame" name="frame_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</rect>
|
||||
@@ -1078,7 +1078,7 @@
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<widget class="QLabel" name="discord_info_icon">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
@@ -1103,7 +1103,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="discord_title_label">
|
||||
<widget class="QLabel" name="discord_info_title_label">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>180</width>
|
||||
@@ -1135,7 +1135,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_7">
|
||||
<widget class="QFrame" name="auth_discord_page_content">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
@@ -1322,6 +1322,357 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="whitelist">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<widget class="QFrame" name="frame_6">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>482</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="whitelist_page_title">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>292</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="whitelist_info_icon_labe">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../resources.qrc">:/assets/letter-i-info.svg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="whitelist_info_title_label">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>180</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Information</string>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_11">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>291</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_7">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>700</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="whitelist_info_text">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Tu n’es pas whitelisté sur le serveur.
|
||||
Inscris-toi sur Discord, puis relance le launcher.</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_11">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>30</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_9">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Shadow::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_12">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="no_whitelist_btn">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>380</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string> Compris !</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../resources.qrc">
|
||||
<normaloff>:/assets/no_whitelist.svg</normaloff>:/assets/no_whitelist.svg</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_13">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_12">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
Reference in New Issue
Block a user