KhadhroonySRLPv4/application.py
2024-12-09 14:58:10 +01:00

1729 lines
74 KiB
Python
Executable File

#!.venv/bin/python
"""Khadhroony SRLPv4 main script"""
import argparse
import base64
import gettext
import json
import locale
import os
import platform
import portalocker
import random
import re
import rc_doc
import rc_icons
import rc_images
import sys
import yaml
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from datetime import datetime, timezone
from loguru import logger
from PySide6 import __version__ as pyside6version
from PySide6.QtCore import (
QAbstractTableModel,
QEasingCurve,
QEvent,
QMargins,
QModelIndex,
QObject,
QPropertyAnimation,
Qt,
QTimer,
Signal
)
from PySide6.QtGui import (
QAction,
QColor,
QCursor,
QIcon,
QKeyEvent,
QLinearGradient,
QPainter,
QPixmap,
QFont
)
from PySide6.QtPdf import QPdfDocument
from PySide6.QtPdfWidgets import QPdfView
from PySide6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QFormLayout,
QGraphicsOpacityEffect,
QGridLayout,
QLabel,
QLineEdit,
QMainWindow,
QMenu,
QMessageBox,
QSplashScreen,
QPushButton,
QSystemTrayIcon,
QTableView,
QTabWidget,
QToolBar,
QVBoxLayout,
QWidget,
QAbstractItemView,
QHeaderView
)
from solders.keypair import Keypair
from typing import Any, IO
APP_NAME: str = "KhadhroonySRLPv4"
"""Application Name"""
APP_ABOUT_NAME: str = "Khadhroony Sol RLPv4 Trading App"
"""Application Full Name"""
APP_FULL_NAME: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading App"
"""Application Full Name"""
APP_DESC: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application"
"""Application Version"""
APP_VERSION_INFO = ('0', '0', '2')
APP_VERSION = '.'.join(APP_VERSION_INFO)
"""Application Name + Version"""
APP_NAME_VERSION = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application @ " + APP_VERSION
"""Application Langs"""
APP_LANGS = ["en"]
APP_RTL_LANGS = [
"ae", # avestique
"aeb", # arabe tunisien
"aec", # arabe saïdi
"ar", # arabe
"arb", # arabe standard moderne
"arc", # araméen
"arq", # arabe algérien
"ary", # arabe marocain
"arz", # arabe égyptien
"ayl", # arabe libyen
"ckb", # sorani
"dv", # divéhi (maldivien)
"fa", # persan
"glk", # guilaki
"he", # hébreu
"khw", # khowar
"mzn", # mazandarani
"ota", # turc ottoman
"otk", # vieux turc
"pnb", # pendjabi occidental
"ps", # pachto
"syr", # syriaque
"tmr", # judéo-araméen babylonien
"ug", # ouïghour
"ur" # ourdou
]
"""Application Salt for wallet encrypting"""
APP_SALT = APP_NAME + " @ " + APP_VERSION_INFO[0] + "." + APP_VERSION_INFO[1]
YAML_CONFIG_FILE = "KhadhroonySRLPv4.yaml"
DEFAULT_YAML_CONFIG = {
"lastFile": "",
"lastFiles": [],
"defaultLang": "en",
"lastWidth": 1024,
"lastHeight": 768,
"lastMaximized": True
}
WALLET_FILE_EXT = "kew"
WALLET_FILE_DESC = "Khadhroony Encrypted Wallet"
WALLETS_FOLDER = "./wallets"
LOCALES_DIR = os.path.join(os.path.dirname(__file__), "locale")
_ = gettext.gettext
is_rtl = False
def load_yaml_app_config():
"""Load the configuration file or create a default one."""
if not os.path.exists(YAML_CONFIG_FILE):
logger.debug(f"{YAML_CONFIG_FILE} not found, creating new one")
with open(YAML_CONFIG_FILE, "w") as f:
yaml.dump(DEFAULT_YAML_CONFIG, f)
return DEFAULT_YAML_CONFIG
with open(YAML_CONFIG_FILE, "r") as f:
data = yaml.safe_load(f)
logger.debug(f"{YAML_CONFIG_FILE} safe loaded")
# Validation et complétion des données
for key, val in DEFAULT_YAML_CONFIG.items():
if key not in data:
logger.debug(f"{key} not found")
data[key] = val
elif not isinstance(data[key], type(val)):
logger.debug(f"{key} wrong type : wanted {type(val)}, found {type(data[key])}")
data[key] = val
return data
def save_yaml_app_config(cfg):
"""Save the configuration to the file."""
with open(YAML_CONFIG_FILE, "w") as f:
yaml.dump(cfg, f)
def create_wallets_folder():
"""Vérifie si le répertoire wallets existe et le créé dans le cas contraire."""
if not os.path.exists(WALLETS_FOLDER):
os.makedirs(WALLETS_FOLDER)
def derive_key(password: str, salt: bytes) -> bytes:
"""
Derive a symmetric key from the given password and salt.
:param password: The password to derive the key from.
:param salt: A random salt for the key derivation.
:return: The derived key.
"""
kdf = PBKDF2HMAC(
algorithm = hashes.SHA256(),
length = 32,
salt = salt,
iterations = 100_000,
backend = default_backend(),
)
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
def encrypt_string(password: str, salt: bytes, plaintext: str) -> str:
"""
Encrypts a string using a password.
:param password: The password to derive the encryption key.
:param salt: A salt for the key derivation.
:param plaintext: The string to encrypt.
:return: A tuple containing the salt and the encrypted string.
"""
key = derive_key(password, salt) # Derive the encryption key
fernet = Fernet(key)
encrypted_text = fernet.encrypt(plaintext.encode())
return encrypted_text.decode()
def center_on_screen(widget: QWidget):
"""Centre la fenêtre sur le moniteur actif (celui où le curseur est présent)."""
screen = QApplication.screenAt(QCursor().pos()) # Obtenir le moniteur actif
if not screen:
screen = QApplication.primaryScreen() # Par défaut, utiliser l'écran principal
screen_geometry = screen.availableGeometry() # Dimensions de l'écran
widget_geometry = widget.frameGeometry()
# Calcul des coordonnées pour centrer la fenêtre
x = screen_geometry.x() + (screen_geometry.width() - widget_geometry.width()) // 2
y = screen_geometry.y() + (screen_geometry.height() - widget_geometry.height()) // 2
widget.move(x, y)
def decrypt_string(password: str, salt: bytes, encrypted_text: bytes | str) -> str:
"""
Decrypts an encrypted string using the given password and salt.
:param password: The password to derive the decryption key.
:param salt: The salt used during encryption.
:param encrypted_text: The encrypted string to decrypt.
:return: The decrypted string.
"""
enc_text = encrypted_text
if isinstance(encrypted_text, str):
enc_text = encrypted_text.encode()
key = derive_key(password, salt) # Derive the decryption key
fernet = Fernet(key)
return fernet.decrypt(enc_text).decode()
def get_system_language() -> str:
"""Get the system language."""
global is_rtl
# sys_locale = locale.getdefaultlocale()[0]
sys_locale = locale.getlocale()[0]
default_lang = 'en'
if sys_locale:
lang_split = sys_locale.split("_")[0] # Extract language code
if lang_split in APP_LANGS:
if lang_split in APP_RTL_LANGS:
is_rtl = True
else:
is_rtl = False
return lang_split
is_rtl = False
return default_lang
def get_lang_or_fallback(lang: str) -> str:
global is_rtl
if lang in APP_LANGS:
if lang in APP_RTL_LANGS:
is_rtl = True
else:
is_rtl = False
return lang
return get_system_language()
def set_app_lang_code(lang_code):
"""Set the application language."""
global _
global is_rtl
try:
lang = gettext.translation("app", localedir = LOCALES_DIR, languages = [lang_code])
lang.install()
_ = lang.gettext
if lang_code in APP_RTL_LANGS:
is_rtl = True
else:
is_rtl = False
except FileNotFoundError:
# Fallback to default language (English)
gettext.install("app", localedir = LOCALES_DIR, names = ["en"])
_ = gettext.gettext
is_rtl = False
def get_qt_dir() -> Qt.LayoutDirection:
global is_rtl
return Qt.LayoutDirection.RightToLeft if is_rtl else Qt.LayoutDirection.LeftToRight
def get_qt_align_center() -> Qt.AlignmentFlag:
return Qt.AlignmentFlag.AlignCenter
def get_qt_align_vchl(rtl:bool = False) -> Qt.AlignmentFlag:
if rtl:
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight
else:
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
def get_qt_align_vchr(rtl:bool = False) -> Qt.AlignmentFlag:
if rtl:
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
else:
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight
def get_qt_align_hcenter() -> Qt.AlignmentFlag:
return Qt.AlignmentFlag.AlignHCenter
def get_qt_align_hl(rtl:bool = False) -> Qt.AlignmentFlag:
if rtl:
return Qt.AlignmentFlag.AlignRight
else:
return Qt.AlignmentFlag.AlignLeft
def get_qt_align_hr(rtl:bool = False) -> Qt.AlignmentFlag:
if rtl:
return Qt.AlignmentFlag.AlignLeft
else:
return Qt.AlignmentFlag.AlignRight
class LockedFile:
"""
Une classe pour gérer les fichiers avec verrouillage exclusif.
Utilise portalocker pour éviter les accès simultanés.
"""
def __init__(self, file_path, mode = "r"):
"""
Initialise un Verrou.
:param file_path: Chemin vers le fichier à verrouiller.
:param mode: Mode d'ouverture du fichier ('r', 'w', 'a', etc.).
"""
self._file = None
self._filePath = file_path
self._mode = mode
def __enter__(self) -> IO[Any]:
"""
Ouvre et verrouille le fichier.
"""
self._file = open(file = self._filePath, mode = self._mode)
# Application d'un verrou exclusif
portalocker.lock(self._file, portalocker.LOCK_EX)
return self._file
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Libère le verrou et ferme le fichier.
"""
try:
portalocker.unlock(self._file)
finally:
self._file.close()
class SplashScreen(QSplashScreen):
"""SplashScreen class."""
def __init__(self, fade_in_duration = 1500, display_duration = 2000, fade_out_duration = 1500):
super().__init__(QPixmap(":imgSplash"))
# logger.info(f"SplashScreen {Consts.APP_FULL_NAME} ({Consts.APP_VERSION})")
self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # Supprimer les boutons de fermeture
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# Appliquer l'effet d'opacité pour gérer les transitions
self._opacityEffect = QGraphicsOpacityEffect(self)
self.setGraphicsEffect(self._opacityEffect)
# Animation Fade-in
self._fadeIn = QPropertyAnimation(self._opacityEffect, b"opacity")
self._fadeIn.setDuration(fade_in_duration)
self._fadeIn.setStartValue(0)
self._fadeIn.setEndValue(1)
self._fadeIn.setEasingCurve(QEasingCurve.Type.InOutQuad)
# Animation Fade-out
self.fadeOut = QPropertyAnimation(self._opacityEffect, b"opacity")
self.fadeOut.setDuration(fade_out_duration)
self.fadeOut.setStartValue(1)
self.fadeOut.setEndValue(0)
self.fadeOut.setEasingCurve(QEasingCurve.Type.InOutQuad)
# Planification de l'animation fade-out après la durée
QTimer.singleShot(display_duration, self.start_fade_out)
# Lancer l'animation fade-in
self._fadeIn.start()
def start_fade_out(self):
"""Lancer l'animation fade-out et fermer le splash screen à la fin."""
self.fadeOut.start()
self.fadeOut.finished.connect(self.close)
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
class AboutWindow(QWidget):
def __init__(self, bg_txt:str, parent = None):
super().__init__(parent)
self.setWindowTitle(_("About %(app)s") % {'app': APP_NAME_VERSION})
self.setWindowIcon(QIcon(":imgIcon"))
self.setFixedSize(900, 600)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
self._bg_Qpix = QPixmap(":imgAbout")
# Texte centré placé avant le bouton de fermeture pour ne pas empêcher le click de celui-là
self._about_Qlbl = QLabel(self)
self._about_Qlbl.setText(bg_txt)
self._about_Qlbl.setAlignment(Qt.AlignmentFlag.AlignCenter) # Centrer le texte
self._about_Qlbl.setStyleSheet("font-size: 20px; color: white;") # Style du texte
self._about_Qlbl.setWordWrap(True) # Activer le retour à la ligne
self._about_Qlbl.setGeometry(0, 0, self.width(), self.height()) # Occuper toute la fenêtre
# Bouton de fermeture de la fenetre
self._exit_Qpbtn = QPushButton(self)
self._exit_Qpbtn.setIcon(QIcon(":icoQuit"))
self._exit_Qpbtn.setFixedSize(24, 24)
self._exit_Qpbtn.setGeometry(self.width() - 44, 20, 24, 24) # Position et taille
self._exit_Qpbtn.clicked.connect(self.close)
self._exit_Qpbtn.move(self.width() - self._exit_Qpbtn.width() - 20, 20)
# self.outsideClickFilter = OutsideCloseClickFilter(self)
# QApplication.instance().installEventFilter(self.outsideClickFilter)
def closeEvent(self, event) -> None:
# QApplication.instance().removeEventFilter(self.outsideClickFilter)
super().closeEvent(event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
# Redimensionner l'image pour qu'elle corresponde à la taille de la fenêtre
scaled_image = self._bg_Qpix.scaled(
self.size(),
Qt.AspectRatioMode.IgnoreAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
painter.drawPixmap(0, 0, scaled_image)
painter.setOpacity(0.7)
# Ajouter un effet de dégradé
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0, QColor(0, 0, 0, 0)) # Transparent en haut
gradient.setColorAt(1, QColor(0, 0, 0, 128)) # Sombre en bas
painter.fillRect(self.rect(), gradient)
def event(self, event):
"""Surveiller les événements de la fenêtre."""
if event.type() == QEvent.Type.WindowDeactivate:
self.close()
return super().event(event)
def keyPressEvent(self, event: QKeyEvent):
"""Gérer les raccourcis clavier pour fermer la fenêtre."""
if event.key() in {Qt.Key.Key_Escape, Qt.Key.Key_Space, Qt.Key.Key_Enter, Qt.Key.Key_Return}:
print("Fenêtre fermée avec une touche simple.")
self.close()
elif event.key() == Qt.Key.Key_F4 and event.modifiers() & Qt.KeyboardModifier.ControlModifier:
print("Fenêtre fermée avec Ctrl+F4.")
self.close()
else:
# Si une autre touche est pressée, laisser le comportement par défaut
super().keyPressEvent(event)
class HelpWindow(QWidget):
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
def __init__(self, pdf_doc:str, parent = None):
super().__init__(parent)
self.setWindowTitle(_("Help - %(app)s") % {'app': APP_NAME_VERSION})
# self.setWindowIcon(QIcon(":/res/images/favicon.ico"))
self.setWindowIcon(QIcon(":imgIcon"))
self.setFixedSize(900, 600)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.MSWindowsFixedSizeDialogHint)
# Créer le document PDF et le visualiseur PDF
self._pdfDoc = QPdfDocument(self)
self._pdfView = QPdfView()
self._pdfView.setDocument(self._pdfDoc)
# Configurer le widget central pour afficher le PDF
self._main_Qvlo = QVBoxLayout()
self.setLayout(self._main_Qvlo)
self._main_Qvlo.setSpacing(0)
self._main_Qvlo.setContentsMargins(QMargins(0, 0, 0, 0))
self._main_Qvlo.addWidget(self._pdfView)
# self._pdfDoc.load(":/res/help.pdf")
self._pdfDoc.load(pdf_doc)
if self._pdfDoc.status() != QPdfDocument.Status.Ready:
self.close()
else:
# Afficher la première page du PDF
self._pdfView.setPageMode(QPdfView.PageMode.MultiPage)
self._pdfView.setZoomMode(QPdfView.ZoomMode.FitToWidth)
self._pdfView.setPageSpacing(0)
self._pdfView.setDocumentMargins(QMargins(0, 0, 0, 0))
class LogEmitter(QObject):
"""
Objet émettant un signal pour envoyer les logs vers l'interface graphique.
"""
newLogSignal = Signal(dict) # Le log est transmis sous forme de dictionnaire
class LogHandler:
"""
Gestionnaire personnalisé pour diriger les logs de loguru vers un QTableView.
"""
def __init__(self, emitter: LogEmitter):
self._logEmitter = emitter
def __call__(self, message: str):
"""
Appelé par loguru pour chaque message de log.
"""
parsed_log = self.parse_log_message(message)
if parsed_log:
self._logEmitter.newLogSignal.emit(parsed_log)
@staticmethod
def parse_log_message(message: str):
"""
Parse un message de log loguru au format attendu (timestamp, LEVEL, message).
"""
parts = message.split(" -|- ", 2) # On attend le format "{time} -|- {level} -|- {message}"
if len(parts) == 3:
return {
"timestamp": parts[0], # Timestamp
"level": parts[1], # Niveau du log
"message": parts[2] # Message
}
return None
class LogsTableModel(QAbstractTableModel):
"""
Modèle pour gérer l'affichage des logs dans un QTableView.
"""
def __init__(self, display_lines: int = 100):
super().__init__()
self._dataList = [] # Stocke les logs
self._maxLines = display_lines # Nombre maximum de lignes
self._headers = [_("Timestamp"), _("Level"), _("Message")]
self._sortColumn = 0
self._sortOrder = Qt.SortOrder.AscendingOrder
self.sort(self._sortColumn, self._sortOrder)
def rowCount(self, parent = QModelIndex()):
return len(self._dataList)
def columnCount(self, parent = QModelIndex()):
return len(self._headers)
def set_headers(self, new_headers: list):
self._headers = new_headers
self.headerDataChanged.emit(Qt.Orientation.Horizontal, 0, len(self._headers) - 1)
self.layoutChanged.emit()
def headerData(self, section:int, orientation:Qt.Orientation, role:Qt.ItemDataRole = Qt.ItemDataRole.DisplayRole) -> Any:
"""
Définit les en-têtes des colonnes.
"""
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
return self._headers[section]
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
if not index.isValid():
return None
elif role == Qt.ItemDataRole.DisplayRole:
log = self._dataList[index.row()]
if index.column() == 0: # Timestamp
return log["timestamp"]
elif index.column() == 1: # Level
return log["level"]
elif index.column() == 2: # Message
return log["message"]
elif role == Qt.ItemDataRole.BackgroundRole and index.column() == 1: # Level
log = self._dataList[index.row()]
if log["level"] == 'DEBUG':
return QColor(Qt.GlobalColor.cyan)
elif log["level"] == 'INFO':
return QColor(Qt.GlobalColor.white)
elif log["level"] == 'SUCCESS':
return QColor(Qt.GlobalColor.green)
elif log["level"] == 'WARNING':
return QColor(Qt.GlobalColor.yellow)
elif log["level"] == 'ERROR':
return QColor(Qt.GlobalColor.red)
elif log["level"] == 'CRITICAL':
return QColor(Qt.GlobalColor.darkRed)
elif role == Qt.ItemDataRole.ForegroundRole and index.column() == 1: # Level
log = self._dataList[index.row()]
if log["level"] == 'DEBUG':
return QColor(Qt.GlobalColor.darkGray)
elif log["level"] == 'INFO':
return QColor(Qt.GlobalColor.black)
elif log["level"] == 'SUCCESS':
return QColor(Qt.GlobalColor.darkGray)
elif log["level"] == 'WARNING':
return QColor(Qt.GlobalColor.lightGray)
elif log["level"] == 'ERROR':
return QColor(Qt.GlobalColor.darkGray)
elif log["level"] == 'CRITICAL':
return QColor(Qt.GlobalColor.lightGray)
elif role == Qt.ItemDataRole.FontRole:
if index.column() == 1: # Level
# log = self._dataList[index.row()]
font = QFont("Helvetica")
font.setBold(True)
return font
return None
elif role == Qt.ItemDataRole.TextAlignmentRole:
global is_rtl
if index.column() == 0:
return get_qt_align_vchr()
elif index.column() == 1:
return get_qt_align_center()
elif index.column() == 2:
return get_qt_align_vchl()
return None
def sort(self, column: int, order: Qt.SortOrder = Qt.SortOrder.DescendingOrder):
self._sortColumn = column
self._sortOrder = order
# Sorting logic
if column == 0: # Sorting by "timestamp"
self._dataList.sort(key = lambda log: log["timestamp"], reverse = (order == Qt.SortOrder.AscendingOrder))
elif column == 1: # Sorting by "level"
self._dataList.sort(key = lambda log: log["level"], reverse = (order == Qt.SortOrder.AscendingOrder))
elif column == 2: # Sorting by "message"
self._dataList.sort(key = lambda log: log["message"], reverse = (order == Qt.SortOrder.AscendingOrder))
# Notify view that data has changed after sorting
self.layoutChanged.emit()
def add_data(self, log: dict):
"""
Ajoute un log au modèle tout en respectant la limite maximale des lignes.
"""
self._dataList.append(log)
while len(self._dataList) > self._maxLines:
self._dataList.pop(0) # Supprime les anciennes lignes
self.sort(self._sortColumn, self._sortOrder)
self.layoutChanged.emit()
def set_data(self, data: list):
self._dataList = data
self.sort(self._sortColumn, self._sortOrder)
self.layoutChanged.emit()
class LogViewerWidget(QWidget):
"""
Widget principal contenant un QTableView pour afficher les logs.
"""
def __init__(self, max_display_lines: int = 100, parent = None):
super().__init__(parent)
# Layout principal
self._main_Qvlo = QVBoxLayout(self)
self.setLayout(self._main_Qvlo)
self._main_Qvlo.setContentsMargins(0, 0, 0, 0)
# TableView
self._logs_Qtvmb = QTableView(self)
self._main_Qvlo.addWidget(self._logs_Qtvmb)
# Model
self._logs_lQtm = LogsTableModel(max_display_lines)
self._logs_Qtvmb.setModel(self._logs_lQtm)
# selection de ligne entiere
self._logs_Qtvmb.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# désactiver tentative d'edition
self._logs_Qtvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Lecture seule
# alterner les couleurs
self._logs_Qtvmb.setAlternatingRowColors(True)
# remplir le background
self._logs_Qtvmb.setAutoFillBackground(True)
# activer le trie
self._logs_Qtvmb.setSortingEnabled(True)
# Configuration des tailles des colonnes
self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Première colonne
self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Deuxième colonne
self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Troisième colonne
self._logs_Qtvmb.setTextElideMode(Qt.TextElideMode.ElideNone)
self._logs_Qtvmb.setWordWrap(False)
# Émetteur de signaux pour les logs
self._logEmitter = LogEmitter()
self._logEmitter.newLogSignal.connect(self.add_log) # Connexion du signal
# Ajouter un gestionnaire personnalisé pour loguru
log_handler = LogHandler(self._logEmitter)
# logger.remove() # Supprime les sorties par défaut
logger.add(log_handler, format = "{time:YYYY-MM-DD HH:mm:ss.SSS} -|- {level} -|- {message}", level = "INFO")
# update display
self.update_display()
def add_log(self, log: dict):
"""
Ajoute un log au modèle et effectue le défilement automatique.
"""
self._logs_lQtm.add_data(log)
def update_display(self):
self._logs_lQtm.set_headers([_("Timestamp"), _("Level"), _("Message")])
self.setLayoutDirection(get_qt_dir())
class Wallet:
"""Class Wallet can raise ValueError """
WrongWalletName = 1
"""'walletName' wrong type (must not be empty string)"""
WrongWalletId = 2
"""'walletId' wrong type (must not be empty string)"""
WrongWalletPass = 3
"""'walletPass' wrong type (must not be empty string)"""
WrongWalletType = 4
"""'walletType' must be equal string 'solana'"""
JSONDecodeError = 5
"""Cannot decode Json"""
def __init__(self,
wallet_name: str,
wallet_pub_key: str,
wallet_priv_key: str,
dt_crea: datetime = None,
dt_update: datetime = None,
main_sol_balance: float = 0,
main_tokens: list = None,
dev_sol_balance: float = 0,
dev_tokens: list = None,
wallet_type: str = 'solana'):
# Vérification des paramètres obligatoires
if not isinstance(wallet_name, str) or not wallet_name.strip():
raise ValueError(Wallet.WrongWalletName)
if not isinstance(wallet_pub_key, str) or not wallet_pub_key.strip():
raise ValueError(Wallet.WrongWalletId)
if not isinstance(wallet_priv_key, str) or not wallet_priv_key.strip():
raise ValueError(Wallet.WrongWalletPass)
if wallet_type != "solana":
raise ValueError(Wallet.WrongWalletType)
# Attribuer les valeurs obligatoires
self.walletType = wallet_type
self.walletName = wallet_name
self.walletPubKey = wallet_pub_key
self.walletPrivKey = wallet_priv_key
# Gérer les valeurs par défaut des dates
self.dtCrea = dt_crea if isinstance(dt_crea, datetime) else datetime.now(timezone.utc)
self.dtUpdate = dt_update if isinstance(dt_update, datetime) else datetime.now(timezone.utc)
# Gérer les balances et les mettre à zéro si elles sont négatives
self.mainSolBalance = max(main_sol_balance, 0)
self.devSolBalance = max(dev_sol_balance, 0)
# Initialiser les listes de tokens si elles ne sont pas fournies
self.mainTokens = main_tokens if main_tokens is not None else []
self.devTokens = dev_tokens if dev_tokens is not None else []
def to_json(self) -> str:
"""Convertir l'instance en JSON"""
return json.dumps({
"walletType": self.walletType,
"walletName": self.walletName,
"walletPubKey": self.walletPubKey,
"walletPrivKey": self.walletPrivKey,
"dtCrea": self.dtCrea.isoformat(),
"dtUpdate": self.dtUpdate.isoformat(),
"mainSolBalance": self.mainSolBalance,
"mainTokens": self.mainTokens,
"devSolBalance": self.devSolBalance,
"devTokens": self.devTokens
})
@staticmethod
def parse_datetime(dt_str):
"""Parse une chaîne de date en datetime, retourne l'heure actuelle en UTC en cas d'erreur."""
if isinstance(dt_str, str):
try:
return datetime.fromisoformat(dt_str)
except ValueError:
logger.warning("Format de date invalide, remplacement par l'heure actuelle.")
return datetime.now(timezone.utc)
@staticmethod
def from_json(json_string: str):
"""Créer une instance Wallet à partir d'une chaîne JSON"""
try:
data = json.loads(json_string)
# Vérification des paramètres obligatoires
if 'walletName' not in data or not isinstance(data["walletName"], str) or not data["walletName"].strip():
raise ValueError(Wallet.WrongWalletName)
if 'walletPubKey' not in data or not isinstance(data["walletPubKey"], str) or not data["walletPubKey"].strip():
raise ValueError(Wallet.WrongWalletId)
if 'walletPrivKey' not in data or not isinstance(data["walletPrivKey"], str) or not data["walletPrivKey"].strip():
raise ValueError(Wallet.WrongWalletPass)
if 'walletType' not in data or data["walletType"] != "solana":
raise ValueError(Wallet.WrongWalletType)
wallet_name = data["walletName"]
wallet_pub_key = data["walletPubKey"]
wallet_priv_key = data["walletPrivKey"]
wallet_type = data["walletType"]
# Récupérer et valider les dates
dt_crea = Wallet.parse_datetime(data.get("dtCrea"))
dt_update = Wallet.parse_datetime(data.get("dtUpdate"))
# Récupérer et valider les balances, les réinitialiser à zéro si elles sont négatives
main_sol_balance = max(data.get("mainSolBalance", 0), 0)
dev_sol_balance = max(data.get("devSolBalance", 0), 0)
# Récupérer les listes de tokens, initialiser à liste vide si absentes
main_tokens = data.get("mainTokens", [])
dev_tokens = data.get("devTokens", [])
# Créer et retourner l'instance Wallet
return Wallet(wallet_name, wallet_pub_key, wallet_priv_key, dt_crea, dt_update, main_sol_balance, main_tokens, dev_sol_balance, dev_tokens, wallet_type)
except json.JSONDecodeError:
raise ValueError(Wallet.JSONDecodeError)
class WalletTokensTableModel(QAbstractTableModel):
def __init__(self, parent = None):
super().__init__(parent)
self._headers = [_("Token Id"), _("Buy Price"), _("Buy Time"), _("Action")]
self._dataList = []
def set_headers(self, new_headers: list):
self._headers = new_headers
self.headerDataChanged.emit(Qt.Orientation.Horizontal, 0, 3)
self.layoutChanged.emit()
def rowCount(self, parent = QModelIndex()):
return len(self._dataList)
def columnCount(self, parent = QModelIndex()):
return len(self._headers)
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
if not index.isValid():
return None
elif role == Qt.ItemDataRole.DisplayRole:
token = self._dataList[index.row()]
if index.column() == 0: # Timestamp
return token["token_id"]
elif index.column() == 1: # Level
return token["buy_price"]
elif index.column() == 2: # Message
return token["buy_time"]
elif index.column() == 3: # Action
return ""
elif role == Qt.ItemDataRole.TextAlignmentRole:
global is_rtl
if index.column() == 0:
return get_qt_align_vchr()
elif index.column() == 1:
return get_qt_align_center()
elif index.column() == 2:
return get_qt_align_vchl(not is_rtl)
elif role == Qt.ItemDataRole.DecorationRole and index.column() == 0:
return QIcon(":icoWallet")
return None
def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
return self._headers[section]
return None
def set_data(self, data:list):
self._dataList = data
self.layoutChanged.emit()
class WalletWidget(QWidget):
def __init__(self, parent = None):
super().__init__(parent)
self.setWindowIcon(QIcon(":icoWallet"))
self.wallet = None
self.fileKey = None
self.filePath = None
self._main_Qglo = QGridLayout(self)
self.setLayout(self._main_Qglo)
# Line 1: Wallet Name
self._walletName_Qlbl = QLabel()
self.walletName_Qled = QLineEdit()
self._saveWallet_Qpbtn = QPushButton()
self._init_line1()
# Line 2: Wallet PubKey
self._walletPubKey_Qlbl = QLabel()
self.walletPubKey_Qled = QLineEdit()
self._copyToClipboard_Qpbtn = QPushButton()
self._init_line2()
# Line 3: Balances
self._realBalance_Qlbl = QLabel()
self.realBalance_Qled = QLineEdit()
self._demoBalance_Qlbl = QLabel()
self.demoBalance_Qled = QLineEdit()
self._init_line3()
# Line 4: Tokens Label
self._tokens_Qlbl = QLabel()
self._init_line4()
# # Line 5: TableView
self._tokens_qtbvmb = QTableView(self)
self._tokens_wtQtm = WalletTokensTableModel(self)
self._init_tokens_qtbvmb()
self.update_display()
self.populate_table()
def set_wallet(self, wallet: Wallet, file_pass: str, file_path:str):
self.save_wallet()
self.fileKey = file_pass
self.filePath = file_path
self.wallet = wallet
self.update_wallet_display()
def update_wallet_display(self):
if self.wallet is not None:
self.walletPubKey_Qled.setText(self.wallet.walletPubKey)
self.walletName_Qled.setText(self.wallet.walletName)
self.realBalance_Qled.setText(f"{self.wallet.mainSolBalance:.9f}")
self.demoBalance_Qled.setText(f"{self.wallet.devSolBalance:.9f}")
else:
self.walletPubKey_Qled.setText("")
self.walletName_Qled.setText("")
self.realBalance_Qled.setText(f"{0:.9f}")
self.demoBalance_Qled.setText(f"{0:.9f}")
def save_wallet(self):
if self.wallet is not None and self.fileKey is not None and self.filePath is not None:
encrypted_data = encrypt_string(self.fileKey, APP_SALT.encode(), self.wallet.to_json())
with LockedFile(self.filePath, "w+b") as file:
file.seek(0)
file.truncate()
file.write(encrypted_data.encode())
def _init_line1(self):
self.walletName_Qled.setAlignment(get_qt_align_center())
self._saveWallet_Qpbtn.setIcon(QIcon(":icoSave"))
self._saveWallet_Qpbtn.setMaximumWidth(50)
self._saveWallet_Qpbtn.clicked.connect(self.save_wallet)
self._main_Qglo.addWidget(self._walletName_Qlbl, 0, 0, 1, 2)
self._main_Qglo.addWidget(self.walletName_Qled, 0, 2, 1, 9)
self._main_Qglo.addWidget(self._saveWallet_Qpbtn, 0, 11, 1, 1)
def _init_line2(self):
self.walletPubKey_Qled.setAlignment(get_qt_align_center())
self.walletPubKey_Qled.setText("XXXXXXXXXXXX")
self.walletPubKey_Qled.setReadOnly(True)
self._copyToClipboard_Qpbtn.setIcon(QIcon(":icoCopy"))
self._copyToClipboard_Qpbtn.setMaximumWidth(50)
self._copyToClipboard_Qpbtn.clicked.connect(self.copy_wallet_priv_key_to_clipboard)
self._main_Qglo.addWidget(self._walletPubKey_Qlbl, 1, 0, 1, 2)
self._main_Qglo.addWidget(self.walletPubKey_Qled, 1, 2, 1, 9)
self._main_Qglo.addWidget(self._copyToClipboard_Qpbtn, 1, 11, 1, 1)
def _init_line3(self):
self.realBalance_Qled.setAlignment(get_qt_align_vchr())
self.realBalance_Qled.setText("10.000000000")
# self.realBalance_Qled.setMaximumWidth(300)
self.realBalance_Qled.setReadOnly(True)
self.demoBalance_Qled.setAlignment(get_qt_align_vchr())
self.demoBalance_Qled.setText("10.000000000")
# self.demoBalance_Qled.setMaximumWidth(300)
self.demoBalance_Qled.setReadOnly(True)
self._main_Qglo.addWidget(self._realBalance_Qlbl, 2, 0, 1, 2)
self._main_Qglo.addWidget(self.realBalance_Qled, 2, 2, 1, 3)
self._main_Qglo.addWidget(self._demoBalance_Qlbl, 2, 7, 1, 2)
self._main_Qglo.addWidget(self.demoBalance_Qled, 2, 9, 1, 3)
def _init_line4(self):
self._tokens_Qlbl.setAlignment(get_qt_align_center())
self._main_Qglo.addWidget(self._tokens_Qlbl, 3, 0, 1, 12)
def _init_tokens_qtbvmb(self):
self._tokens_qtbvmb.setModel(self._tokens_wtQtm)
# selection de ligne entiere
self._tokens_qtbvmb.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# desactiver tentative d'edition
self._tokens_qtbvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Lecture seule
# alterner les couleurs
self._tokens_qtbvmb.setAlternatingRowColors(True)
# remplir le background
self._tokens_qtbvmb.setAutoFillBackground(True)
# activer le trie
self._tokens_qtbvmb.setSortingEnabled(True)
# Configuration des tailles des colonnes
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Première colonne
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Deuxième colonne
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive) # Troisième colonne
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Quatrième colonne
self._tokens_qtbvmb.setTextElideMode(Qt.TextElideMode.ElideNone)
self._tokens_qtbvmb.setWordWrap(False)
self._main_Qglo.addWidget(self._tokens_qtbvmb, 4, 0, 1, 12)
def update_display(self):
global is_rtl
self.setLayoutDirection(get_qt_dir())
self._walletName_Qlbl.setText(_("Wallet Name:"))
self._walletName_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
self._walletPubKey_Qlbl.setText(_("Wallet PubKey:"))
self._walletPubKey_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
self._realBalance_Qlbl.setText(_("Balance Real:"))
self._realBalance_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
self._demoBalance_Qlbl.setText(_("Balance Demo:"))
self._demoBalance_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
self._tokens_Qlbl.setText(_("My Tokens:"))
self._tokens_wtQtm.set_headers([_("Token Id"), _("Buy Price"), _("Buy Time"), _("Action")])
def populate_table(self):
data = []
for x in range(10):
token_id = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k = 3))
buy_price = f"{random.uniform(0.000000001, 1.000000000):.9f}"
buy_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
action = _("Sell") # Placeholder for button-like behavior
token = {
"token_id": token_id, # token_id
"buy_price": buy_price, # Niveau du log
"buy_time": buy_time, # Message
"action": action # Message
}
data.append(token)
self._tokens_wtQtm.set_data(data)
def copy_wallet_priv_key_to_clipboard(self):
"""Copier le texte de QLineEdit dans le presse-papiers."""
clipboard = QApplication.clipboard()
clipboard.setText(self.walletPubKey_Qled.text())
logger.info(_("walletId copied to clipboard"))
class SimpleMsgBox(QMessageBox):
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
def __init__(self, window_title: str, window_text: str, ico: QMessageBox.Icon, parent = None):
super().__init__(parent)
self.setLayoutDirection(get_qt_dir())
self.setOption(QMessageBox.Option.DontUseNativeDialog, True)
self.setWindowIcon(QIcon(":imgIcon"))
self.setWindowTitle(window_title)
self.setText(window_text)
self.setIcon(ico)
class NewWalletNameAndPassDialog(QDialog):
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
def __init__(self, parent = None):
super().__init__(parent)
self.setLayoutDirection(get_qt_dir())
self.setWindowIcon(QIcon(":imgIcon"))
self.setWindowTitle(_("Wallet Name and EncryptPass"))
main_qflo = QFormLayout()
self.setLayout(main_qflo)
self._walletName_Qle = QLineEdit(self)
self._walletEncyptPass_Qle = QLineEdit(self)
self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password)
main_qflo.addRow(_("Wallet Name:"), self._walletName_Qle)
main_qflo.addRow(_("Wallet EncryptPass:"), self._walletEncyptPass_Qle)
validate_qpbtn = QPushButton(_("Validate"))
cancel_qpbtn = QPushButton(_("Cancel"))
button_qdbbox = QDialogButtonBox(self)
button_qdbbox.addButton(validate_qpbtn, QDialogButtonBox.ButtonRole.AcceptRole)
button_qdbbox.addButton(cancel_qpbtn, QDialogButtonBox.ButtonRole.RejectRole)
# Connecter les signaux des boutons
validate_qpbtn.clicked.connect(self.validate_and_accept)
cancel_qpbtn.clicked.connect(self.reject)
main_qflo.addWidget(button_qdbbox)
self.walletName = None
self.walletEncyptPass = None
def validate_and_accept(self):
# Validation du champ 1
if len(self._walletName_Qle.text().strip()) < 2:
msg_qmbox = SimpleMsgBox(_("Error"), _("The Wallet Name must have a minimum of 2 characters."), QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
return
# Validation du champ 2 (mot de passe)
if not re.match(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$", self._walletEncyptPass_Qle.text()):
msg_qmbox = SimpleMsgBox(_("Error"), _("The Wallet EncryptPass must have a minimum of 1 lower case character, 1 upper case character, 1 numeric character, 1 special character (@$!%*?&), and must have a minimum of 8 characters length."), QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
return
self.walletName = self._walletName_Qle.text().strip()
self.walletEncyptPass = self._walletEncyptPass_Qle.text()
# Si les deux champs sont valides, on accepte la saisie
self.accept()
class NewWalletFileDialog(QFileDialog):
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
def __init__(self, parent = None):
super().__init__(parent)
self.setOption(QFileDialog.Option.DontUseNativeDialog, True) # Empêche d'écraser un fichier existant
self.setWindowTitle(_("Save Content to New Wallet File"))
self.setWindowIcon(QIcon(":imgIcon"))
self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) # Mode "Enregistrer sous"
self.setFileMode(QFileDialog.FileMode.AnyFile) # Permet de saisir un nouveau fichier
self.setOption(QFileDialog.Option.DontConfirmOverwrite, True)
self.setLabelText(QFileDialog.DialogLabel.LookIn, _("Wallets location:"))
self.setLabelText(QFileDialog.DialogLabel.FileName, _("File name:"))
self.setLabelText(QFileDialog.DialogLabel.FileType, "File type:")
self.setLabelText(QFileDialog.DialogLabel.Accept, _("Validate"))
self.setLabelText(QFileDialog.DialogLabel.Reject, _("Cancel"))
self.setDirectory(WALLETS_FOLDER)
self.setDefaultSuffix(WALLET_FILE_EXT)
self.setNameFilter(_("%(desc)s (*.%(ext)s)") % {"desc": WALLET_FILE_DESC, "ext": WALLET_FILE_EXT})
# self.setMinimumSize(dialog.width(), dialog.height())
class LoadWalletPassDialog(QDialog):
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
def __init__(self, file_name: str, parent = None):
super().__init__(parent)
self.setLayoutDirection(get_qt_dir())
self.setWindowIcon(QIcon(":imgIcon"))
self.setWindowTitle(_("Wallet EncryptPass for file '%(fileName)s'") % {"fileName": file_name})
main_qflo = QFormLayout()
self.setLayout(main_qflo)
self._walletEncyptPass_Qle = QLineEdit(self)
self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password)
main_qflo.addRow(_("Wallet EncryptPass:"), self._walletEncyptPass_Qle)
validate_qpbtn = QPushButton(_("Validate"))
cancel_qpbtn = QPushButton(_("Cancel"))
button_qdbbox = QDialogButtonBox(self)
button_qdbbox.addButton(validate_qpbtn, QDialogButtonBox.ButtonRole.AcceptRole)
button_qdbbox.addButton(cancel_qpbtn, QDialogButtonBox.ButtonRole.RejectRole)
# Connecter les signaux des boutons
validate_qpbtn.clicked.connect(self.validate_and_accept)
cancel_qpbtn.clicked.connect(self.reject)
main_qflo.addWidget(button_qdbbox)
self.walletEncyptPass = None
def validate_and_accept(self):
# Validation du champ 1
if len(self._walletEncyptPass_Qle.text().strip()) < 1:
msg_qmbox = SimpleMsgBox(_("Error"), _("You must enter a valid Password."), QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
return
self.walletEncyptPass = self._walletEncyptPass_Qle.text()
# Si les deux champs sont valides, on accepte la saisie
self.accept()
class LoadWalletFileDialog(QFileDialog):
def showEvent(self, event) -> None:
super().showEvent(event)
center_on_screen(self)
def __init__(self, parent = None):
super().__init__(parent)
self.setOption(QFileDialog.Option.DontUseNativeDialog, True) # Empêche d'écraser un fichier existant
self.setWindowTitle(_("Load Wallet File"))
self.setWindowIcon(QIcon(":imgIcon"))
self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) # Mode "Ouvrir sous"
self.setFileMode(QFileDialog.FileMode.ExistingFile) # Permet de saisir un fichier existant
self.setDirectory(WALLETS_FOLDER)
self.setDefaultSuffix(WALLET_FILE_EXT)
self.setLabelText(QFileDialog.DialogLabel.LookIn, _("Wallets location:"))
self.setLabelText(QFileDialog.DialogLabel.FileName, _("File name:"))
self.setLabelText(QFileDialog.DialogLabel.FileType, "File type:")
self.setLabelText(QFileDialog.DialogLabel.Accept, _("Validate"))
self.setLabelText(QFileDialog.DialogLabel.Reject, _("Cancel"))
self.setNameFilter(_("%(desc)s (*.%(ext)s)") % {"desc": WALLET_FILE_DESC, "ext": WALLET_FILE_EXT})
# self.setMinimumSize(dialog.width(), dialog.height())
class MainWindow(QMainWindow):
def closeEvent(self, event: QEvent):
"""Intercepter la fermeture pour masquer la fenêtre au lieu de quitter."""
self.hide()
event.ignore()
def showEvent(self, event) -> None:
self.showApp_Qact.setDisabled(True)
self.hideApp_Qact.setDisabled(False)
super().showEvent(event)
if self.mustCenter:
self.mustCenter = False
center_on_screen(self)
def hideEvent(self, event) -> None:
self.showApp_Qact.setDisabled(False)
self.hideApp_Qact.setDisabled(True)
self.mustCenter = True
super().hideEvent(event)
def __init__(self, cfg:dict = DEFAULT_YAML_CONFIG):
super().__init__()
self.config = cfg
self.currentLang = get_lang_or_fallback(self.config['defaultLang'])
self.setWindowIcon(QIcon(":imgIcon"))
self.setWindowTitle(APP_NAME_VERSION)
self.setMinimumSize(1024, 768)
self.resize(max(self.config.get('lastWidth'), 1024), max(self.config.get('lastHeight'), 768))
self.mustCenter = True
# Main Menubar
self.main_Qmnub = self.menuBar()
# File Menu
self.file_Qmnu = QMenu()
self.newWallet_Qact = QAction(self)
self.newWallet_Qact.setIcon(QIcon(":icoWalletNew"))
self.openWallet_Qact = QAction(self)
self.openWallet_Qact.setIcon(QIcon(":icoWalletOpen"))
self.filesHistory_Qmnu = QMenu()
self.filesHistory_Qmnu.setIcon(QIcon(":icoLastFiles"))
self.noFileHistory_Qact = QAction(self)
self.noFileHistory_Qact.setIcon(QIcon(":icoNoHistory"))
self.noFileHistory_Qact.setDisabled(True)
self.hideApp_Qact = QAction(self)
self.hideApp_Qact.setIcon(QIcon(":icoTrayHide"))
self.closeApp_Qact = QAction(self)
self.closeApp_Qact.setIcon(QIcon(":icoQuit"))
self.forceCloseApp_Qact = QAction(self)
self.forceCloseApp_Qact.setIcon(QIcon(":icoForceQuit"))
# finalize initialisation of File Menu
self._init_file_qmnu()
# Langs Menu
self.langs_Qmnu = QMenu()
self.changeLangAr_Qact = QAction(QIcon(":icoLang"), "العربية", self)
self.changeLangDe_Qact = QAction(QIcon(":icoLang"), "Deutsch", self)
self.changeLangEn_Qact = QAction(QIcon(":icoLang"), "English", self)
self.changeLangEs_Qact = QAction(QIcon(":icoLang"), "Español", self)
self.changeLangFr_Qact = QAction(QIcon(":icoLang"), "Français", self)
self.changeLangHe_Qact = QAction(QIcon(":icoLang"), "עברית", self)
self.changeLangHi_Qact = QAction(QIcon(":icoLang"), "हिन्दी", self)
self.changeLangIt_Qact = QAction(QIcon(":icoLang"), "Italiano", self)
self.changeLangJa_Qact = QAction(QIcon(":icoLang"), "日本語", self)
self.changeLangKo_Qact = QAction(QIcon(":icoLang"), "한국어", self)
self.changeLangPt_Qact = QAction(QIcon(":icoLang"), "Português", self)
self.changeLangRu_Qact = QAction(QIcon(":icoLang"), "Русский", self)
self.changeLangZh_Qact = QAction(QIcon(":icoLang"), "中文", self)
# finalize initialisation of Langs Menu
self._init_langs_qmnu()
# Help Menu
self.help_Qmnu = QMenu()
self.help_Qact = QAction(self)
self.help_Qact.setIcon(QIcon(":icoHelp"))
self.about_Qact = QAction(self)
self.about_Qact.setIcon(QIcon(":icoAbout"))
self._init_help_qmnu()
# set texts and titles for Main Menubar
# Main Toolbar
self.main_Qtb = QToolBar()
self._init_main_toolbar()
# Initialisation de l'icône du System Tray
self.tray_Qsti = QSystemTrayIcon(QIcon(":imgIcon"), self)
# self.trayIcon.setToolTip(f"{APP_FULL_NAME} ({APP_VERSION})") # don't work on linux ???
# Menu du trayIcon
self.tray_Qmnu = QMenu()
self.showApp_Qact = QAction(self)
self._init_tray_qmnu()
# Central widget: QTabWidget
self.main_Qtabw = QTabWidget(self)
self.setCentralWidget(self.main_Qtabw)
self.token_qw = QWidget()
self.main_Qtabw.addTab(self.token_qw, QIcon(":icoTokens"), _("Tokens:"))
self.token_qw.setLayout(QVBoxLayout())
self.token_qw.layout().addWidget(QTableView())
self.wallet_Qw = WalletWidget(self)
self.main_Qtabw.addTab(self.wallet_Qw, QIcon(":icoWallet"), _("Wallet: %(walletName)s") % {"walletName": ""})
self.logViewer_Qw = LogViewerWidget(1000, self)
self.main_Qtabw.addTab(self.logViewer_Qw, QIcon(":icoLogs"), _("Logs:"))
self.update_display()
logger.success(_("Init of %(appName)s") % {"appName": APP_ABOUT_NAME})
def first_load(self):
# logger.debug(type(config))
configLastMaximized = self.config.get('lastMaximized', True)
if configLastMaximized:
self.showMaximized()
else:
self.show()
lastFile = self.config["lastFile"]
if lastFile is None or lastFile == "":
self._init_new_or_load_wallet()
elif not os.path.exists(lastFile):
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' does not exists. Please choose a new file.") % {"fileName": lastFile}, QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
self._init_new_or_load_wallet()
else:
self._ask_load_wallet_pass(lastFile, True)
def _quit_app_func(self):
msg_qmbox = SimpleMsgBox(_("Do you really want to Quit?"), _("If you accept, all the files will be saved\nand the application will be closed."), QMessageBox.Icon.Question, self)
yes_qpbtn = msg_qmbox.addButton(_("Yes"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole) # no_qpbtn = msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
if msg_qmbox.clickedButton() == yes_qpbtn:
self.config['lastMaximized'] = self.isMaximized()
# if self.isMaximized():
# self.showNormal()
# TODO: save size of non maximized window
self.config['lastHeight'] = self.height()
self.config['lastWidth'] = self.width()
# saving self.currentLang in config memory
self.config['defaultLang'] = self.currentLang
# save memory config to file
save_yaml_app_config(self.config)
self.wallet_Qw.save_wallet()
# close instance
QApplication.instance().quit()
def _force_quit_app_func(self):
msg_qmbox = SimpleMsgBox(_("Do you really want to Quit?"), _("If you accept, nothing will be saved\nand the application will be closed."), QMessageBox.Icon.Critical, self)
yes_qpbtn = msg_qmbox.addButton(_("Yes"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole) # no_qpbtn = msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
if msg_qmbox.clickedButton() == yes_qpbtn:
QApplication.instance().quit()
def _change_lang(self, lang_code, lang_name):
self.currentLang = get_lang_or_fallback(lang_code)
set_app_lang_code(self.currentLang)
logger.info(_("Switching to language %(lang)s.") % {"lang": lang_name})
self.update_display()
def _init_tray_qmnu(self):
self.tray_Qsti.setContextMenu(self.tray_Qmnu)
self.tray_Qmnu.addAction(self.hideApp_Qact)
self.showApp_Qact.setIcon(QIcon(":icoTrayShow"))
self.showApp_Qact.setDisabled(True)
self.showApp_Qact.triggered.connect(self.show)
self.tray_Qmnu.addAction(self.showApp_Qact)
self.tray_Qmnu.addAction(self.closeApp_Qact)
self.tray_Qmnu.addAction(self.forceCloseApp_Qact)
self.tray_Qsti.show()
def _show_about_window(self):
"""Affiche une boîte de dialogue 'À propos'."""
bg_txt = f"<h3>{APP_FULL_NAME}: {APP_VERSION}<br>PYTHON: {platform.python_version()}<br>Qt: {pyside6version}<br>OS: {platform.system()}</h3>"
about_window = AboutWindow(bg_txt, self)
about_window.show()
def _show_help_window(self):
"""Affiche une boîte de dialogue 'Aide'."""
help_window = HelpWindow(":pdfHelp", self)
help_window.show()
def _init_main_toolbar(self):
# Création de la barre d'outils
self.addToolBar(self.main_Qtb)
self.main_Qtb.setMovable(False)
favicon_toolbar_qlbl = QLabel()
favicon_toolbar_qlbl.setPixmap(QPixmap(":imgFavicon").scaled(24, 24, Qt.AspectRatioMode.KeepAspectRatio))
self.main_Qtb.addWidget(favicon_toolbar_qlbl)
self.main_Qtb.addSeparator()
self.main_Qtb.addAction(self.openWallet_Qact)
self.main_Qtb.addAction(self.newWallet_Qact)
self.main_Qtb.addSeparator()
# self.toolbarPcp.addAction(self.actionOpenWsMainNetConnexion)
def _init_new_or_load_wallet(self):
msg_qmbox = SimpleMsgBox(_("Action Required"), _("To use the application, you have to load a wallet or to create a new one."), QMessageBox.Icon.Critical, self)
load_qpbtn = msg_qmbox.addButton(_("Load Wallet"), QMessageBox.ButtonRole.ActionRole)
create_qpbtn = msg_qmbox.addButton(_("New Wallet"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.addButton(_("Quit"), QMessageBox.ButtonRole.RejectRole) # quit_qpbtn = msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
if msg_qmbox.clickedButton() == load_qpbtn:
create_wallets_folder()
self._ask_load_wallet_filepath(True)
elif msg_qmbox.clickedButton() == create_qpbtn:
create_wallets_folder()
self._ask_new_wallet_name_and_pass(True)
else:
self.close()
QApplication.instance().quit()
return
def _create_new_wallet(self):
create_wallets_folder()
self._ask_new_wallet_name_and_pass()
def _ask_new_wallet_name_and_pass(self, return_to_prev: bool = False):
wallet_dialog = NewWalletNameAndPassDialog(self)
if wallet_dialog.exec():
new_wallet_name = wallet_dialog.walletName
new_wallet_pass = wallet_dialog.walletEncyptPass
self._ask_new_wallet_filepath(new_wallet_name, new_wallet_pass, return_to_prev)
elif return_to_prev:
self._init_new_or_load_wallet()
def _ask_new_wallet_filepath(self, new_wallet_name: str, new_wallet_pass: str, return_to_prev: bool = False):
saved_file = False
while True:
saved_file = False
# Ouvre le QFileDialog pour choisir un emplacement et un nom de fichier
dialog = NewWalletFileDialog(self)
if dialog.exec():
file_path = dialog.selectedFiles()[0]
# Ajoute l'extension .kew si elle est manquante
if not file_path.endswith("." + WALLET_FILE_EXT):
file_path += "." + WALLET_FILE_EXT
file_name = os.path.basename(file_path)
# Vérifie si le fichier existe déjà
if os.path.exists(file_path):
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' already exists. Please choose a new file.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
continue # Relance le dialogue pour forcer un nouveau fichier
# Enregistre le contenu dans le nouveau fichier
try:
keypair = Keypair()
wallet = Wallet(wallet_name = new_wallet_name, wallet_pub_key = str(keypair.pubkey()), wallet_priv_key = str(keypair.secret()))
encrypted_data = encrypt_string(new_wallet_pass, APP_SALT.encode(), wallet.to_json())
with LockedFile(file_path, "w+b") as file:
file.write(encrypted_data.encode())
file_path = os.path.realpath(file_path)
if file_path not in self.config["lastFiles"]:
self.config["lastFiles"].append(file_path)
while len(self.config["lastFiles"]) > 5:
self.config["lastFiles"].pop(0)
self._refresh_files_history_qmnu()
self.config["lastFile"] = file_path
self.wallet_Qw.set_wallet(wallet, new_wallet_pass, file_path)
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": wallet.walletName})
saved_file = True
break # Quitte la boucle après avoir sauvegardé
except Exception as e:
logger.error(e)
msg_qmbox = SimpleMsgBox(_("Error"), _("An error occurred while saving the file '%(fileName)s'.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
break # Quitte la boucle en cas d'erreur
else:
break # L'utilisateur a annulé.
if saved_file:
return
if return_to_prev:
self._ask_new_wallet_name_and_pass(return_to_prev)
def _load_wallet(self):
self._ask_load_wallet_filepath()
def _ask_load_wallet_filepath(self, return_to_prev: bool = False):
loaded_file = False
file_path = None
while True:
dialog = LoadWalletFileDialog(self)
if dialog.exec():
file_path = dialog.selectedFiles()[0]
file_name = os.path.basename(file_path)
if not os.path.exists(file_path):
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' does not exists. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
elif self.config["lastFile"] is None or file_path != self.config["lastFile"] or return_to_prev:
loaded_file = True
else:
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' is already loaded. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
continue # Relance le dialogue pour forcer un nouveau fichier
if loaded_file:
break
# Relance le dialogue pour forcer un nouveau fichier
continue
else:
break # L'utilisateur a annulé.
if loaded_file:
self._ask_load_wallet_pass(file_path, return_to_prev)
return
if return_to_prev:
self._init_new_or_load_wallet()
def _ask_load_wallet_pass(self, file_path: str, return_to_prev: bool = False):
loaded_file = False
wallet = None
wallet_pass = None
while not loaded_file:
file_name = os.path.basename(file_path)
wallet_dialog = LoadWalletPassDialog(file_name, self)
if wallet_dialog.exec():
wallet_pass = wallet_dialog.walletEncyptPass
try:
with LockedFile(file_path, "rb") as file:
enc_data = file.read()
wallet = Wallet.from_json(decrypt_string(wallet_pass, APP_SALT.encode(), enc_data.decode()))
loaded_file = True
break
except portalocker.exceptions.LockException as e:
logger.error(_("LockException for Wallet file %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' seems to be locked. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
loaded_file = False
break
except InvalidToken as e:
logger.error(_("Wrong password for Wallet file %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
msg_qmbox = SimpleMsgBox(_("Error"), _("The password you provided for file '%(fileName)s' is incorrect. Please try again.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
loaded_file = False
continue
except json.JSONDecodeError as e:
logger.error(_("Cannot decode json in Wallet file %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
msg_qmbox = SimpleMsgBox(_("Error"), _("The file '%(fileName)s' seems to be corrupted. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
loaded_file = False
break
except ValueError as e:
logger.error(_("Wrong values in %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
msg_qmbox = SimpleMsgBox(_("Error"), _("The file '%(fileName)s' seems to have wrong values. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
msg_qmbox.exec()
loaded_file = False
break
else:
break
if loaded_file:
file_path = os.path.realpath(file_path)
if file_path not in self.config["lastFiles"]:
self.config["lastFiles"].append(file_path)
while len(self.config["lastFiles"]) > 5:
self.config["lastFiles"].pop(0)
self._refresh_files_history_qmnu()
self.config["lastFile"] = file_path
self.wallet_Qw.set_wallet(wallet, wallet_pass, file_path)
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": wallet.walletName})
return
if return_to_prev:
self._ask_load_wallet_filepath(return_to_prev)
def _init_file_qmnu(self):
"""Initialize File Menu."""
# add File Menu to Main MenuBar
self.main_Qmnub.addMenu(self.file_Qmnu)
# add action "New Wallet" to File Menu
self.file_Qmnu.addAction(self.newWallet_Qact)
# set action target
self.newWallet_Qact.triggered.connect(self._create_new_wallet)
# add action "Open Wallet" to File Menu
self.file_Qmnu.addAction(self.openWallet_Qact)
# set action target
self.openWallet_Qact.triggered.connect(self._load_wallet)
self.file_Qmnu.addSeparator()
# add files
self.file_Qmnu.addMenu(self.filesHistory_Qmnu)
self._refresh_files_history_qmnu()
self.file_Qmnu.addSeparator()
# add action "Hide application" to File Menu
self.file_Qmnu.addAction(self.hideApp_Qact)
# set action target
self.hideApp_Qact.triggered.connect(self.hide)
# add action "Close application" to File Menu
self.file_Qmnu.addAction(self.closeApp_Qact)
# set action target
self.closeApp_Qact.triggered.connect(self._quit_app_func)
# add action "Close without saving" to File Menu
self.file_Qmnu.addAction(self.forceCloseApp_Qact)
# set action target
self.forceCloseApp_Qact.triggered.connect(self._force_quit_app_func)
def _refresh_files_history_qmnu(self):
self.filesHistory_Qmnu.clear()
for file_path in self.config.get('lastFiles', []):
if not os.path.exists(file_path):
self.config['lastFiles'].remove(file_path)
else:
file_name = os.path.basename(file_path)
file_open_qact = QAction(QIcon(":icoWallet"), file_name, self)
self.filesHistory_Qmnu.addAction(file_open_qact)
# TODO: Add trigger connect
else:
self.filesHistory_Qmnu.addAction(self.noFileHistory_Qact)
def _init_langs_qmnu(self):
"""Initialize Langs Menu."""
self.main_Qmnub.addMenu(self.langs_Qmnu)
# Ar
self.langs_Qmnu.addAction(self.changeLangAr_Qact)
self.changeLangAr_Qact.triggered.connect(lambda: self._change_lang("ar", self.changeLangAr_Qact.text()))
self.changeLangAr_Qact.setDisabled(True)
# De
self.langs_Qmnu.addAction(self.changeLangDe_Qact)
self.changeLangDe_Qact.triggered.connect(lambda: self._change_lang("de", self.changeLangDe_Qact.text()))
self.changeLangDe_Qact.setDisabled(True)
# En
self.langs_Qmnu.addAction(self.changeLangEn_Qact)
self.changeLangEn_Qact.triggered.connect(lambda: self._change_lang("en", self.changeLangEn_Qact.text()))
# self.changeLangEn_Qact.setDisabled(True)
# Es
self.langs_Qmnu.addAction(self.changeLangEs_Qact)
self.changeLangEs_Qact.triggered.connect(lambda: self._change_lang("es", self.changeLangEs_Qact.text()))
self.changeLangEs_Qact.setDisabled(True)
# Fr
self.langs_Qmnu.addAction(self.changeLangFr_Qact)
self.changeLangFr_Qact.triggered.connect(lambda: self._change_lang("fr", self.changeLangFr_Qact.text()))
self.changeLangFr_Qact.setDisabled(True)
# He
self.langs_Qmnu.addAction(self.changeLangHe_Qact)
self.changeLangHe_Qact.triggered.connect(lambda: self._change_lang("he", self.changeLangHe_Qact.text()))
self.changeLangHe_Qact.setDisabled(True)
# Hi
self.langs_Qmnu.addAction(self.changeLangHi_Qact)
self.changeLangHi_Qact.triggered.connect(lambda: self._change_lang("hi", self.changeLangHi_Qact.text()))
self.changeLangHi_Qact.setDisabled(True)
# It
self.langs_Qmnu.addAction(self.changeLangIt_Qact)
self.changeLangIt_Qact.triggered.connect(lambda: self._change_lang("it", self.changeLangIt_Qact.text()))
self.changeLangIt_Qact.setDisabled(True)
# Ja
self.langs_Qmnu.addAction(self.changeLangJa_Qact)
self.changeLangJa_Qact.triggered.connect(lambda: self._change_lang("ja", self.changeLangJa_Qact.text()))
self.changeLangJa_Qact.setDisabled(True)
# Ko
self.langs_Qmnu.addAction(self.changeLangKo_Qact)
self.changeLangKo_Qact.triggered.connect(lambda: self._change_lang("ko", self.changeLangKo_Qact.text()))
self.changeLangKo_Qact.setDisabled(True)
# Pt
self.langs_Qmnu.addAction(self.changeLangPt_Qact)
self.changeLangPt_Qact.triggered.connect(lambda: self._change_lang("pt", self.changeLangPt_Qact.text()))
self.changeLangPt_Qact.setDisabled(True)
# Ru
self.langs_Qmnu.addAction(self.changeLangRu_Qact)
self.changeLangRu_Qact.triggered.connect(lambda: self._change_lang("ru", self.changeLangRu_Qact.text()))
self.changeLangRu_Qact.setDisabled(True)
# Zh
self.langs_Qmnu.addAction(self.changeLangZh_Qact)
self.changeLangZh_Qact.triggered.connect(lambda: self._change_lang("zh", self.changeLangZh_Qact.text()))
self.changeLangZh_Qact.setDisabled(True)
def _init_help_qmnu(self):
"""Initialize Help menu."""
# add Help Menu to Main MenuBar
self.main_Qmnub.addMenu(self.help_Qmnu)
# add action "Help Content" to Help Menu
self.help_Qmnu.addAction(self.help_Qact)
# set action target
self.help_Qact.triggered.connect(self._show_help_window)
# add action "About Khadhroony SRLPv4" to Help Menu
self.help_Qmnu.addAction(self.about_Qact)
# set action target
self.about_Qact.triggered.connect(self._show_about_window)
def update_display(self):
"""refresh translations of texts in main menubar when language change."""
self.file_Qmnu.setTitle(_("File"))
self.newWallet_Qact.setText(_("New Wallet"))
self.openWallet_Qact.setText(_("Open Wallet"))
self.filesHistory_Qmnu.setTitle(_("Last Files"))
self.noFileHistory_Qact.setText(_("No file found"))
self.hideApp_Qact.setText(_("Hide application"))
self.closeApp_Qact.setText(_("Close application"))
self.forceCloseApp_Qact.setText(_("Close without saving"))
self.langs_Qmnu.setTitle(_("Languages"))
self.help_Qmnu.setTitle(_("Help"))
self.help_Qact.setText(_("Help Content"))
self.about_Qact.setText(_("About %(app)s") % {"app":APP_NAME})
self.showApp_Qact.setText(_("Show application"))
self.main_Qtabw.setTabText(0, _("Tokens:"))
if self.wallet_Qw.wallet is None:
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": ""})
else:
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": self.wallet_Qw.wallet.walletName})
self.main_Qtabw.setTabText(2, _("Log Display:"))
self.wallet_Qw.update_display()
self.logViewer_Qw.update_display()
self.setLayoutDirection(get_qt_dir())
if __name__ == "__main__":
logger.remove() # Supprime les sorties par défaut
logger.add(sys.stderr, level = 'TRACE', format = '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{line: <4}</cyan> - <level>{message}</level>', filter = None, colorize = None, serialize = False, backtrace = True, diagnose = True, enqueue = False, context = None, catch = True)
logger.add("logs/KhadhroonyRaydium4.{time}.log", format = "{time:YYYY-MM-DD HH:mm:ss.SSS} - {level} - {line: <4}: {message}", level = "DEBUG")
app_lang = get_lang_or_fallback(get_system_language())
set_app_lang_code(app_lang)
gettext.bindtextdomain('argparse', 'locale')
gettext.textdomain('argparse')
parser = argparse.ArgumentParser(
description = "===~-<=<~~> No Khadhrawy <$<~>$> No Hamroony <~~>=>-~===\n\n" + APP_DESC + "\n" + _("Launch without options to show main window"),
formatter_class = argparse.RawTextHelpFormatter,
epilog = "===~-<=<~~> No Khadhrawy <$<~>$> No Hamroony <~~>=>-~==="
)
# parser.add_argument("-v", "--version", action="store_true", help="show version and exit.")
parser.add_argument('-v', '--version', action = 'version', version = "{prog} (v. {version})".format(prog = APP_NAME, version = APP_VERSION), help = _("show program version and exit"))
args = parser.parse_args()
# creation du répertoire de wallets
create_wallets_folder()
# chargement configuration de base / historique / langue / display etc ...
config = load_yaml_app_config()
configLocale = config["defaultLang"]
app_lang = get_lang_or_fallback(configLocale)
set_app_lang_code(app_lang)
config['defaultLang'] = app_lang
app = QApplication(sys.argv)
# chargement du SplashScreen
splash_screen = SplashScreen()
splash_screen.show()
# Planifier l'affichage de la fenêtre principale après le splash screen
main_window = MainWindow(config)
splash_screen.fadeOut.finished.connect(main_window.first_load)
sys.exit(app.exec())