#!.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", "fr", "ar"] 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.changeLangEn_Qact = QAction(QIcon(":icoLang"), "English", self) self.changeLangFr_Qact = QAction(QIcon(":icoLang"), "Français", self) self.changeLangAr_Qact = QAction(QIcon(":icoLang"), "العربية", self) self.changeLangDe_Qact = QAction(QIcon(":icoLang"), "Deutsch", self) self.changeLangIt_Qact = QAction(QIcon(":icoLang"), "Italiano", self) self.changeLangEs_Qact = QAction(QIcon(":icoLang"), "Espagnol", 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"