Compare commits
8 Commits
968d88c18d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b7ef4c951 | |||
| 82679a5709 | |||
| 93c37a905b | |||
| 945abae5f1 | |||
| 720b004eca | |||
| 54aa7a50b2 | |||
| 1125e5ee10 | |||
| 5d2282def1 |
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(
|
||||
@@ -243,7 +256,7 @@ exe = EXE(
|
||||
"msvcp*.dll",
|
||||
],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=True,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
@@ -7,7 +7,23 @@ from PySide6.QtGui import QColor
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NO_STAFF = True
|
||||
NO_WHITELIST = False
|
||||
|
||||
REDIRECT_URI = "http://localhost:5000/callback"
|
||||
SCOPES = ["identify"]
|
||||
CLIENT_ID = "1240007913175781508"
|
||||
AUTENTICATION_SUCCESS_MESSAGE = """
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Authentication réussie</h1>
|
||||
<p>Vous pouvez maintenant fermer cette fenêtre et revenir au launcher de La Tanière.</p>
|
||||
</body>
|
||||
</html>
|
||||
""".encode('utf-8')
|
||||
# ---------------------------------------------------------------------------
|
||||
# ENUMS
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -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:
|
||||
|
||||
@@ -3,31 +3,23 @@ import webbrowser
|
||||
import os
|
||||
from urllib.parse import urlencode
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from tools.get_server_token import GetServerTokenForDiscord
|
||||
from fivemserver.get_server_token import GetServerTokenForDiscord
|
||||
from config.constants import CLIENT_ID, REDIRECT_URI, SCOPES, AUTENTICATION_SUCCESS_MESSAGE
|
||||
|
||||
# Disable stderr output
|
||||
os.environ['PYTHONWARNINGS'] = 'ignore'
|
||||
|
||||
REDIRECT_URI = "http://localhost:5000/callback"
|
||||
SCOPES = ["identify"]
|
||||
CLIENT_ID = "1240007913175781508"
|
||||
AUTENTICATION_SUCCESS_MESSAGE = """
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Authentication réussie</h1>
|
||||
<p>Vous pouvez maintenant fermer cette fenêtre et revenir au launcher de La Tanière.</p>
|
||||
</body>
|
||||
</html>
|
||||
""".encode('utf-8')
|
||||
|
||||
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
||||
code: str | None = None
|
||||
|
||||
# Ajoute ceci pour empêcher le serveur d'écrire dans la console/stderr
|
||||
def log_message(self, format, *args):
|
||||
return # Ne fait rien, donc pas de blocage sur stdout/stderr
|
||||
|
||||
def do_GET(self):
|
||||
"""
|
||||
callback pour discord auth
|
||||
"""
|
||||
if "/callback" in self.path:
|
||||
query = self.path.split("?")[1]
|
||||
params = dict(p.split("=") for p in query.split("&"))
|
||||
@@ -40,11 +32,19 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
||||
|
||||
# return discord application id (client id)
|
||||
def get_discord_client_id() -> str:
|
||||
"""
|
||||
return discord application id
|
||||
"""
|
||||
return CLIENT_ID
|
||||
|
||||
# return discord user id
|
||||
def get_discord_user_id() -> str:
|
||||
# récupération des infos serveur lataupe
|
||||
"""
|
||||
Retourne l'id du compte discord de l'utilisateur via l'oauh discord.
|
||||
"""
|
||||
|
||||
# récupération des infos serveur la tanière
|
||||
# récupération d
|
||||
session_id = GetServerTokenForDiscord.authenticate()
|
||||
client_secret = GetServerTokenForDiscord.get_token(session_id)
|
||||
|
||||
@@ -59,9 +59,6 @@ def get_discord_user_id() -> str:
|
||||
webbrowser.open(f"{auth_url}?{urlencode(params)}")
|
||||
|
||||
server = HTTPServer(("localhost", 5000), OAuthCallbackHandler)
|
||||
# celle ligne cache le stderr output pour ne pas afficher l'url de callback dans la console
|
||||
# valable en debug mode
|
||||
os.dup2(os.open(os.devnull, os.O_WRONLY), 2)
|
||||
server.handle_request()
|
||||
|
||||
if not OAuthCallbackHandler.code:
|
||||
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.ciphers.aead import AESGCM
|
||||
|
||||
API_URL = 'https://prod.la-taniere.fun:30121/'
|
||||
from config.constants import Urls
|
||||
|
||||
class GetServerTokenForDiscord:
|
||||
derived_key: bytes | None = None
|
||||
|
||||
@staticmethod
|
||||
def authenticate(server = API_URL):
|
||||
def authenticate(server = Urls.API_URL.value):
|
||||
|
||||
if server is None:
|
||||
server = API_URL
|
||||
server = Urls.API_URL.value
|
||||
# ==========================
|
||||
# Génération clé ECDH client
|
||||
# ==========================
|
||||
@@ -49,12 +49,12 @@ class GetServerTokenForDiscord:
|
||||
return auth["session_id"]
|
||||
|
||||
@staticmethod
|
||||
def get_token(session_id: bytes, server = API_URL):
|
||||
def get_token(session_id: bytes, server = Urls.API_URL.value):
|
||||
# ==========================
|
||||
# DISCORD TOKEN
|
||||
# ==========================
|
||||
if server is None:
|
||||
server = API_URL
|
||||
server = Urls.API_URL.value
|
||||
download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={
|
||||
"x-session-id": session_id
|
||||
}).json()
|
||||
@@ -65,12 +65,12 @@ class GetServerTokenForDiscord:
|
||||
aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
|
||||
return aesgcm.decrypt(nonce, encrypted_data, None)
|
||||
|
||||
# @staticmethod
|
||||
# def register_discord_user(user_id: str, server = API_URL) -> str:
|
||||
# if server is None:
|
||||
# server = API_URL
|
||||
# registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
|
||||
# "x-session-id": user_id
|
||||
# }).json()
|
||||
#
|
||||
# return registeredId["discord_id"]
|
||||
@staticmethod
|
||||
def register_discord_user(user_id: str, server = Urls.API_URL.value) -> str:
|
||||
if server is None:
|
||||
server = Urls.API_URL.value
|
||||
registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
|
||||
"x-session-id": user_id
|
||||
}).json()
|
||||
|
||||
return registeredId["discord_id"]
|
||||
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
|
||||
14
src/main.py
14
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
|
||||
@@ -72,8 +74,8 @@ if __name__ == "__main__":
|
||||
|
||||
# On récupère l'ID stocké (sera "" si absent grâce au schéma)
|
||||
stored_user_id = config.get_discord_user()
|
||||
if stored_user_id != "" or not stored_user_id.isspace():
|
||||
if not CheckDiscord.isuserconnected(stored_user_id):
|
||||
if stored_user_id != "" and not stored_user_id.isspace():
|
||||
if not CheckDiscord.isuserconnected():
|
||||
msg = CustomMessageBox(
|
||||
title="La Tanière: connexion Discord",
|
||||
message="Tu n'est pas connecté à Discord\n\n"
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
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,11 +8,14 @@ from PySide6.QtUiTools import QUiLoader
|
||||
from PySide6.QtWidgets import QMainWindow, QSizePolicy
|
||||
|
||||
from config.config_manager import ConfigManager
|
||||
from config.constants import NO_STAFF, Urls, NO_WHITELIST
|
||||
from ui.custom_message_box import CustomMessageBox
|
||||
from ui.hazard_stripes import HazardButton
|
||||
from controllers.audio_controller import AudioController
|
||||
from controllers.glow_animator import GlowAnimator
|
||||
from controllers.window_dragger import WindowDragger
|
||||
from tools import discord_oauth
|
||||
from tools.constants import NO_STAFF, Urls
|
||||
from discord import discord_oauth
|
||||
from tools.utils import quit_application
|
||||
|
||||
from fake_patch_notes import patch_note
|
||||
|
||||
@@ -32,6 +35,29 @@ class MainWindow(QMainWindow):
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
# Test bouton en contruction
|
||||
en_chantier = True
|
||||
if en_chantier:
|
||||
old_btn = self.ui.connexion_btn
|
||||
parent_layout = self.ui.verticalLayout_6 # layout direct du bouton dans le .ui
|
||||
|
||||
index = parent_layout.indexOf(old_btn)
|
||||
|
||||
new_btn = HazardButton(old_btn.parentWidget())
|
||||
new_btn.setObjectName("connexion_btn")
|
||||
new_btn.setText("EN MAINTENANCE")
|
||||
new_btn.setIcon(old_btn.icon())
|
||||
new_btn.setIconSize(old_btn.iconSize())
|
||||
new_btn.setMinimumSize(old_btn.minimumSize())
|
||||
new_btn.set_hazard(True)
|
||||
|
||||
parent_layout.takeAt(index)
|
||||
old_btn.deleteLater()
|
||||
parent_layout.insertWidget(index, new_btn)
|
||||
|
||||
self.ui.connexion_btn = new_btn
|
||||
self.ui.connexion_btn.clicked.connect(self._on_connexion)
|
||||
|
||||
# centrage vertical du bouton connexion
|
||||
if NO_STAFF:
|
||||
self.ui.staff_btn.hide()
|
||||
@@ -61,6 +87,18 @@ class MainWindow(QMainWindow):
|
||||
self._center_window()
|
||||
self.show()
|
||||
|
||||
if NO_WHITELIST:
|
||||
msg = CustomMessageBox(
|
||||
title="La Tanière: Non whitelisté",
|
||||
message="\n\nTu n'est pas whitelisté sur le serveur\n\n"
|
||||
"Assure-toi de te faire whitelister.\n\n"
|
||||
"Lorsque cela sera fait, relance le launcher.",
|
||||
icon_type=CustomMessageBox.WARNING,
|
||||
buttons=CustomMessageBox.OK
|
||||
)
|
||||
msg.exec()
|
||||
quit_application()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Setup
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -45,7 +45,7 @@ QLabel#queue_lbl {
|
||||
|
||||
}
|
||||
|
||||
QPushButton#connexion_btn {
|
||||
QPushButton#connexion_btn[chantier="false"] {
|
||||
/* Dégradé chaleureux : Orange vers Orange-Rouge */
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop: 0 #ff9d00,
|
||||
@@ -57,7 +57,7 @@ QPushButton#connexion_btn {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
QPushButton#connexion_btn:hover {
|
||||
QPushButton#connexion_btn[chantier="false"]:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop: 0 #ffb338,
|
||||
stop: 1 #ff7a29);
|
||||
@@ -66,7 +66,7 @@ QPushButton#connexion_btn:hover {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QPushButton#connexionBtn:pressed {
|
||||
QPushButton#connexion_btn[chantier="false"]:pressed {
|
||||
background: #cc5200;
|
||||
padding-top: 12px; /* Effet d'enfoncement */
|
||||
}
|
||||
@@ -81,6 +81,21 @@ QPushButton#staff_btn {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
HazardButton#connexion_btn {
|
||||
color: #0A1A3A;
|
||||
/* color: #0D2A6B;*/
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
HazardButton#connexion_btn:hover {
|
||||
/* color: #ffffff;*/
|
||||
color: #0D2A6B;
|
||||
}
|
||||
|
||||
HazardButton#connexion_btn:pressed {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
QPushButton#staff_btn:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 #FF6046, stop:1 #FF527B);
|
||||
@@ -209,3 +224,92 @@ QSlider::handle:horizontal:pressed {
|
||||
background: #E65100;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------
|
||||
Custom Message Box
|
||||
----------------------------------------------*/
|
||||
|
||||
QWidget#MsgBoxMainContainer {
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
QWidget#MsgBoxMainContainer[iconType="info"] {
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 0, x2: 1, y2: 1,
|
||||
stop: 0 #101624,
|
||||
stop: 1 #248277
|
||||
);
|
||||
}
|
||||
|
||||
QWidget#MsgBoxMainContainer[iconType="warning"] {
|
||||
background: qlineargradient(
|
||||
x1: 0, y1: 0, x2: 1, y2: 1,
|
||||
stop: 0 #101624,
|
||||
stop: 1 #cf5b16
|
||||
);
|
||||
}
|
||||
|
||||
QWidget#MsgBoxMainContainer QLabel,
|
||||
QWidget#MsgBoxMainContainer QPushButton
|
||||
{
|
||||
color: white;
|
||||
/*font-size: 18px;
|
||||
/* font-family: 'Segoe UI';*/
|
||||
}
|
||||
|
||||
QPushButton#MsgBoxOkButton,
|
||||
QPushButton#MsgBoxCancelButton {
|
||||
border-radius: 6px;
|
||||
/* color: white;*/
|
||||
padding: 8px 20px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
QPushButton#MsgBoxOkButton {
|
||||
background: #248277;
|
||||
}
|
||||
|
||||
QPushButton#MsgBoxCancelButton {
|
||||
background: #2a313d;
|
||||
}
|
||||
|
||||
QPushButton#MsgBoxOkButton:hover,
|
||||
QPushButton#MsgBoxCancelButton:hover{
|
||||
background: #363d4a;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
QPushButton#MsgBoxCloseButton {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
/* Rayon identique au container (15px) pour épouser parfaitement le coin */
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
QPushButton#MsgBoxCloseButton:hover {
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
QLabel#MsgBoxTitleLabel {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
QLabel#MsgBoxMessageLabel {
|
||||
font-size: 14px;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
QLabel#MsgBoxIconLabel {
|
||||
font-size: 35px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user