license possibility

This commit is contained in:
Louis Mazin 2025-10-22 18:34:02 +02:00
parent dbc6361027
commit 4ee7f50d65
16 changed files with 771 additions and 34 deletions

View File

@ -6,3 +6,8 @@ EMAIL_ADDRESS=your_email@gmail.com
EMAIL_PASSWORD=your_app_password
EMAIL_SMTP_SERVER=smtp.gmail.com
EMAIL_SMTP_PORT=587
# Licensing configuration
LICENSE_API_URL=https://your-server.com/api/licenses
PURCHASE_URL=https://your-website.com/buy
LICENSE_API_KEY=your_license_api_key

220
app/core/license_manager.py Normal file
View File

@ -0,0 +1,220 @@
import hashlib
import platform
import uuid
import requests
from datetime import datetime
from typing import Optional, Dict
from PyQt6.QtCore import QObject, pyqtSignal
import os
from dotenv import load_dotenv
import app.utils.paths as paths
# Load environment variables
load_dotenv(paths.resource_path(".env"))
class LicenseManager(QObject):
"""Gestionnaire de licences avec liaison matérielle"""
license_activated = pyqtSignal(str) # Signal émis lors de l'activation
license_expired = pyqtSignal()
def __init__(self, settings_manager):
super().__init__()
self.settings_manager = settings_manager
self.licensing_enabled = settings_manager.get_config("enable_licensing")
# Si le système de licence est désactivé, initialiser en mode gratuit
if not self.licensing_enabled:
self.hardware_id = None
self.license_key = ""
self.license_data = None
return
# Charger l'URL de l'API depuis .env
self.api_url = os.getenv("LICENSE_API_URL")
if not self.api_url:
raise ValueError("LICENSE_API_URL non définie dans le fichier .env")
self.hardware_id = self._get_hardware_id()
# Charger la licence sauvegardée
self.license_key = settings_manager.settings.value("license_key", "")
self.license_data = None
if self.license_key:
self._load_license_data()
def _get_hardware_id(self) -> str:
"""Génère un identifiant unique basé sur le matériel"""
# Combine plusieurs éléments matériels pour un ID unique
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
for elements in range(0, 2*6, 2)][::-1])
system = platform.system()
machine = platform.machine()
processor = platform.processor()
# Créer un hash unique
unique_string = f"{mac}{system}{machine}{processor}"
return hashlib.sha256(unique_string.encode()).hexdigest()
def get_hardware_id(self) -> str:
"""Retourne le hardware ID pour l'afficher à l'utilisateur"""
return self.hardware_id
def activate_license(self, license_key: str) -> Dict:
"""
Active une licence avec le serveur
Returns:
dict: {"success": bool, "message": str, "data": dict}
"""
try:
response = requests.post(
f"{self.api_url}/activate",
json={
"license_key": license_key,
"hardware_id": self.hardware_id,
"app_version": self.settings_manager.get_config("app_version"),
"platform": platform.system()
},
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get("success"):
# Sauvegarder la licence
self.license_key = license_key
self.license_data = data.get("license_data", {})
self.settings_manager.settings.setValue("license_key", license_key)
self.settings_manager.settings.setValue("license_data", self.license_data)
self.license_activated.emit(license_key)
return {
"success": True,
"message": "Licence activée avec succès",
"data": self.license_data
}
else:
return {
"success": False,
"message": data.get("message", "Erreur d'activation"),
"data": None
}
except requests.exceptions.RequestException as e:
return {
"success": False,
"message": f"Erreur de connexion au serveur: {str(e)}",
"data": None
}
def verify_license(self) -> bool:
"""Vérifie la validité de la licence avec le serveur"""
if not self.license_key:
return False
try:
response = requests.post(
f"{self.api_url}/verify",
json={
"license_key": self.license_key,
"hardware_id": self.hardware_id
},
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get("valid"):
self.license_data = data.get("license_data", {})
self.settings_manager.settings.setValue("license_data", self.license_data)
return True
else:
return False
except requests.exceptions.RequestException:
# En cas d'erreur réseau, utiliser les données en cache
return self._verify_offline()
def _verify_offline(self) -> bool:
"""Vérification hors ligne basique"""
if not self.license_key or not self.license_data:
return False
# Vérifier que le hardware_id correspond
if self.license_data.get("hardware_id") != self.hardware_id:
return False
# Vérifier la date d'expiration si applicable
expires_at = self.license_data.get("expires_at")
if expires_at:
expiry_date = datetime.fromisoformat(expires_at)
if datetime.now() > expiry_date:
self.license_expired.emit()
return False
return True
def _load_license_data(self):
"""Charge les données de licence depuis les settings"""
self.license_data = self.settings_manager.settings.value("license_data", {})
def is_activated(self) -> bool:
"""Vérifie si l'application est activée"""
# Si le système de licence est désactivé, toujours retourner False
if not self.licensing_enabled:
return False
return bool(self.license_key) and self.verify_license()
def get_license_type(self) -> str:
"""Retourne le type de licence (free, premium, etc.)"""
# Si le système de licence est désactivé, retourner "free"
if not self.licensing_enabled:
return "free"
if not self.license_data:
return "free"
return self.license_data.get("type", "free")
def is_feature_available(self, feature_id: str) -> bool:
"""
Vérifie si une fonctionnalité est disponible
Args:
feature_id: Identifiant de la fonctionnalité (ex: "advanced_export")
"""
# Si le système de licence est désactivé, toutes les fonctionnalités sont disponibles
if not self.licensing_enabled:
return True
# Si pas de licence, vérifier dans la config des features gratuites
if not self.is_activated():
free_features = self.settings_manager.get_config("features_by_license").get("free", [])
return feature_id in free_features
# Vérifier les features autorisées par la licence
license_type = self.get_license_type()
features_by_type = self.settings_manager.get_config("features_by_license")
allowed_features = features_by_type.get(license_type, [])
return feature_id in allowed_features or feature_id in self.settings_manager.get_config("free_features")
def get_license_info(self) -> Dict:
"""Retourne les informations de la licence"""
if not self.license_data:
return {
"type": "free",
"status": "inactive",
"expires_at": None
}
return {
"type": self.get_license_type(),
"status": "active" if self.is_activated() else "inactive",
"expires_at": self.license_data.get("expires_at"),
"email": self.license_data.get("email"),
"activated_at": self.license_data.get("activated_at")
}

View File

@ -4,6 +4,8 @@ from app.core.theme_manager import ThemeManager
from app.core.settings_manager import SettingsManager
from app.core.alert_manager import AlertManager
from app.core.update_manager import UpdateManager
from app.core.license_manager import LicenseManager
from typing import Optional
class MainManager:
@ -20,7 +22,7 @@ class MainManager:
self.language_manager: LanguageManager = LanguageManager(self.settings_manager)
self.alert_manager: AlertManager = AlertManager(self.language_manager, self.theme_manager)
self.update_manager: UpdateManager = UpdateManager(self.settings_manager, self.language_manager, self.alert_manager)
self.license_manager: LicenseManager = LicenseManager(self.settings_manager)
@classmethod
def get_instance(cls) -> 'MainManager':
if cls._instance is None:
@ -43,4 +45,7 @@ class MainManager:
return self.alert_manager
def get_update_manager(self) -> UpdateManager:
return self.update_manager
return self.update_manager
def get_license_manager(self) -> LicenseManager:
return self.license_manager

View File

@ -66,9 +66,17 @@ class ThemeManager:
border-radius: 3px;
}}
QTextEdit {{
border: 1px solid {self.current_theme.get_color("border_color")};
border: 2px solid {self.current_theme.get_color("border_color")};
border-radius: 8px;
padding: 10px;
padding: 5px;
font-size: 14px;
background-color: {self.current_theme.get_color("background_secondary_color")};
color: {self.current_theme.get_color("text_color")};
}}
QLineEdit {{
border: 2px solid {self.current_theme.get_color("border_color")};
border-radius: 8px;
padding: 5px;
font-size: 14px;
background-color: {self.current_theme.get_color("background_secondary_color")};
color: {self.current_theme.get_color("text_color")};

View File

@ -1,10 +1,11 @@
from PyQt6.QtWidgets import QApplication, QMainWindow
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
from PyQt6.QtGui import QResizeEvent, QCloseEvent
from PyQt6.QtCore import QSize, QEvent
from app.core.main_manager import MainManager, NotificationType
from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide, TabSide
from app.ui.windows.settings_window import SettingsWindow
from app.ui.windows.suggestion_window import SuggestionWindow
from app.ui.windows.activation_window import ActivationWindow
import app.utils.paths as paths, shutil
from typing import Optional
@ -31,18 +32,24 @@ class MainWindow(QMainWindow):
self.current_size: QSize = QSize(window_size["width"], window_size["height"])
self.previous_size: QSize = QSize(window_size["width"], window_size["height"])
# Configuration des tailles de police de référence
self.base_width = 600 # Largeur de référence
self.base_height = 450 # Hauteur de référence
# Cache pour stocker les font-sizes de base de chaque widget
self._base_font_sizes = {}
# UI elements
self.side_menu: TabsWidget
self.settings_window: SettingsWindow
self.suggestion_window: SuggestionWindow
self.setMinimumSize(600, 400)
self.setMinimumSize(600, 450)
# Initialiser l'UI immédiatement (sera fait pendant le splash)
self.setup_ui()
# Différer l'application des paramètres de fenêtre jusqu'à l'affichage réel
# (cela évite des bugs de taille pendant le préchargement)
self._window_state_applied = False
def showEvent(self, event):
@ -80,6 +87,92 @@ class MainWindow(QMainWindow):
if not self.isMaximized() and not self.is_maximizing:
self.previous_size = self.current_size
self.current_size = self.size()
# Ajuster dynamiquement les font-sizes
self.adjust_all_font_sizes()
def adjust_all_font_sizes(self):
"""Ajuste dynamiquement les font-sizes de tous les labels dans toutes les tabs"""
# Calculer le ratio basé sur la largeur ET la hauteur actuelle
current_width = self.width()
current_height = self.height()
# Calculer les ratios séparément
width_ratio = current_width / self.base_width
height_ratio = current_height / self.base_height
# Prendre le ratio le plus petit pour éviter que le texte dépasse
ratio = min(width_ratio, height_ratio)
# Récupérer tous les widgets des tabs
all_widgets = []
if hasattr(self, 'side_menu'):
all_widgets = self.side_menu.widgets
# Parcourir tous les widgets et ajuster leurs labels
for widget in all_widgets:
if widget:
self._adjust_widget_labels(widget, ratio)
def _adjust_widget_labels(self, widget, ratio):
"""Ajuste récursivement tous les QLabel, QPushButton, QLineEdit, QTextEdit et QComboBox d'un widget"""
from PyQt6.QtWidgets import QPushButton, QLineEdit, QTextEdit, QComboBox
# Types de widgets à ajuster
widget_types = [QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox]
font_size_dict = self.extract_base_font_size()
for widget_type in widget_types:
for child in widget.findChildren(widget_type):
# Obtenir l'identifiant unique du widget
widget_id = id(child)
# Si c'est la première fois qu'on voit ce widget, extraire sa font-size de base
if widget_id not in self._base_font_sizes:
base_size = font_size_dict.get(child.__class__.__name__, 14)
self._base_font_sizes[widget_id] = base_size
else:
base_size = self._base_font_sizes[widget_id]
# Calculer la nouvelle taille
new_size = int(base_size * ratio)
# Appliquer le style (en préservant les autres styles existants)
current_style = child.styleSheet()
# Retirer l'ancienne font-size si elle existe
style_parts = [s.strip() for s in current_style.split(';') if s.strip()]
style_parts = [s for s in style_parts if not s.startswith('font-size')]
# Ajouter la nouvelle font-size
style_parts.append(f'font-size: {new_size}px')
new_style = '; '.join(style_parts)
child.setStyleSheet(new_style)
def extract_base_font_size(self) -> dict:
"""Extrait la font-size de base d'un widget depuis son stylesheet"""
base_font_sizes = {}
try:
style = self.theme_manager.get_sheet()
# Chercher "font-size: XXpx" dans le style, puis chercher à quel widget cela correspond
lines = style.splitlines()
component = None
for line in lines:
line = line.strip()
if line.startswith("font-size:"):
size_part = line.split(":")[1].strip().rstrip(";")
if size_part.endswith("px"):
size_value = int(size_part[:-2])
base_font_sizes[component] = size_value
elif line.startswith("Q"):
component = line.split("{")[0].strip()
return base_font_sizes
except Exception:
# En cas d'erreur, retourner une valeur par défaut
return {}
def closeEvent(self, event: QCloseEvent) -> None:
"""Handle application close event"""
@ -105,6 +198,11 @@ class MainWindow(QMainWindow):
self.settings_window = SettingsWindow(self)
self.side_menu.add_widget(self.settings_window, "", paths.get_asset_svg_path("settings"), position=ButtonPosition.CENTER)
# Ajouter la tab d'activation uniquement si le système de licence est activé
if self.settings_manager.get_config("enable_licensing"):
self.activation_window = ActivationWindow(self)
self.side_menu.add_widget(self.activation_window, "", paths.get_asset_svg_path("license"), position=ButtonPosition.END)
self.setCentralWidget(self.side_menu)

View File

@ -1,6 +1,6 @@
from PyQt6.QtWidgets import QLayout, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStackedWidget, QSizePolicy, QSpacerItem
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import QSize
from PyQt6.QtCore import QSize, Qt
from enum import Enum
from pathlib import Path
import hashlib
@ -204,6 +204,9 @@ class TabsWidget(QWidget):
# Make button square with specified ratio
self._style_square_button(button)
# Configurer le widget pour qu'il soit responsive
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Add to collections first
widget_index = len(self.widgets)
self.buttons.append(button)

View File

@ -0,0 +1,241 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QFrame, QSizePolicy)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from app.core.main_manager import MainManager, NotificationType
from app.ui.widgets.loading_bar import LoadingBar
import webbrowser
import os
from dotenv import load_dotenv
import app.utils.paths as paths
# Load environment variables
load_dotenv(paths.resource_path(".env"))
class ActivationThread(QThread):
"""Thread pour l'activation afin de ne pas bloquer l'UI"""
finished = pyqtSignal(dict)
progress = pyqtSignal(int)
def __init__(self, license_manager, license_key):
super().__init__()
self.license_manager = license_manager
self.license_key = license_key
def run(self):
self.progress.emit(30)
result = self.license_manager.activate_license(self.license_key)
self.progress.emit(100)
self.finished.emit(result)
class ActivationWindow(QWidget):
"""Fenêtre d'activation de licence modernisée"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_manager = MainManager.get_instance()
self.license_manager = self.main_manager.get_license_manager()
self.language_manager = self.main_manager.get_language_manager()
self.theme_manager = self.main_manager.get_theme_manager()
self.alert_manager = self.main_manager.get_alert_manager()
self.observer_manager = self.main_manager.get_observer_manager()
self.settings_manager = self.main_manager.get_settings_manager()
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
self.observer_manager.subscribe(NotificationType.THEME, self.update_theme)
self.activation_thread = None
self.setup_ui()
self.update_license_status()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# === Section titre ===
self.title_label = QLabel(self.language_manager.get_text("activate_license"))
self.title_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.title_label.setWordWrap(True)
layout.addWidget(self.title_label)
# === Spacer flexible ===
layout.addStretch(1)
# === Section clé de licence ===
key_layout = QHBoxLayout()
self.key_title = QLabel(self.language_manager.get_text("license_key_section"))
self.key_title.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
key_layout.addWidget(self.key_title)
self.key_input = QLineEdit()
self.key_input.setPlaceholderText("XXXX-XXXX-XXXX-XXXX-XXXX")
self.key_input.textChanged.connect(self.format_license_key)
self.key_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
key_layout.addWidget(self.key_input)
layout.addLayout(key_layout)
# === Barre de progression ===
self.progress_bar = LoadingBar(self.language_manager.get_text("loading"))
self.progress_bar.setVisible(False)
self.progress_bar.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
layout.addWidget(self.progress_bar)
# === Spacer flexible ===
layout.addStretch(1)
# === Section statut ===
self.status_label = QLabel()
self.status_label.setWordWrap(True)
self.status_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
layout.addWidget(self.status_label)
# === Spacer flexible ===
layout.addStretch(1)
# === Boutons d'action ===
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
self.activate_btn = QPushButton(self.language_manager.get_text("activate"))
self.activate_btn.clicked.connect(self.activate_license)
self.activate_btn.setDefault(True)
self.activate_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
button_layout.addWidget(self.activate_btn)
self.buy_btn = QPushButton(self.language_manager.get_text("buy_license"))
self.buy_btn.clicked.connect(self.open_purchase_page)
self.buy_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
button_layout.addWidget(self.buy_btn)
self.compare_btn = QPushButton(self.language_manager.get_text("compare_versions"))
self.compare_btn.clicked.connect(self.show_features_comparison)
self.compare_btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
button_layout.addWidget(self.compare_btn)
layout.addLayout(button_layout)
def show_features_comparison(self):
"""Affiche la comparaison des versions dans une alerte"""
comparison_text = f"{self.language_manager.get_text('comparaisons')}\n\n"
for version in self.settings_manager.get_config("features_by_license").keys():
comparison_text += f"{self.language_manager.get_text(version+'_version')}:\n"
features = self.settings_manager.get_config("features_by_license").get(version, [])
for feature in features:
feature_desc = self.language_manager.get_text(feature)
comparison_text += f"{feature_desc}\n"
comparison_text += "\n"
self.alert_manager.show_info(comparison_text, parent=self)
def update_license_status(self):
"""Met à jour l'affichage du statut de la licence"""
if self.license_manager.is_activated():
license_info = self.license_manager.get_license_info()
license_type = license_info.get("type", "free").upper()
status_text = f"{self.language_manager.get_text('license_active')}\n"
status_text += f"{self.language_manager.get_text('license_type')}: {license_type}\n"
if license_info.get("email"):
status_text += f"{self.language_manager.get_text('license_email')}: {license_info['email']}\n"
if license_info.get("expires_at"):
status_text += f"{self.language_manager.get_text('license_expires')}: {license_info['expires_at']}"
self.status_label.setText(status_text)
self.key_input.setEnabled(False)
self.activate_btn.setEnabled(False)
else:
status_text = f"{self.language_manager.get_text('license_free_mode')}"
self.status_label.setText(status_text)
def format_license_key(self, text):
"""Formate automatiquement la clé de licence (XXXX-XXXX-...)"""
text = text.replace("-", "").upper()
formatted = "-".join([text[i:i+4] for i in range(0, len(text), 4)])
if len(formatted) > 24:
formatted = formatted[:24]
self.key_input.blockSignals(True)
self.key_input.setText(formatted)
self.key_input.blockSignals(False)
self.key_input.setCursorPosition(len(formatted))
def activate_license(self):
"""Lance l'activation de la licence"""
license_key = self.key_input.text().replace("-", "")
if len(license_key) < 16:
self.alert_manager.show_error("invalid_license_key")
return
# Disable inputs during activation
self.activate_btn.setEnabled(False)
self.key_input.setEnabled(False)
self.buy_btn.setEnabled(False)
# Show progress bar with initial message
self.progress_bar.set_label(self.language_manager.get_text("loading"))
self.progress_bar.set_progress(0)
self.progress_bar.setVisible(True)
# Start activation thread
self.activation_thread = ActivationThread(self.license_manager, license_key)
self.activation_thread.finished.connect(self.on_activation_finished)
self.activation_thread.progress.connect(self.on_activation_progress)
self.activation_thread.start()
def on_activation_progress(self, value):
"""Update progress bar during activation"""
self.progress_bar.set_progress(value)
def on_activation_finished(self, result):
"""Callback quand l'activation est terminée"""
# Hide progress bar
self.progress_bar.setVisible(False)
# Re-enable inputs
self.activate_btn.setEnabled(True)
self.key_input.setEnabled(True)
self.buy_btn.setEnabled(True)
if result["success"]:
# Show success message using AlertManager
success_msg = result['message']
if result.get("data"):
success_msg += f"\n\n{self.language_manager.get_text('license_type')}: {result['data'].get('type', 'N/A')}"
success_msg += f"\n{self.language_manager.get_text('license_email')}: {result['data'].get('email', 'N/A')}"
self.alert_manager.show_info(success_msg, parent=self)
self.update_license_status()
# Clear the input field
self.key_input.clear()
else:
# Show error message using AlertManager
self.alert_manager.show_info(f"{result['message']}", parent=self)
def open_purchase_page(self):
"""Ouvre la page d'achat dans le navigateur"""
purchase_url = os.getenv("PURCHASE_URL")
if purchase_url:
webbrowser.open(purchase_url)
else:
self.alert_manager.show_error("PURCHASE_URL non définie dans .env", parent=self)
def update_language(self):
"""Met à jour tous les textes selon la nouvelle langue"""
self.title_label.setText(self.language_manager.get_text("activate_license"))
self.activate_btn.setText(self.language_manager.get_text("activate"))
self.buy_btn.setText(self.language_manager.get_text("buy_license"))
self.compare_btn.setText(self.language_manager.get_text("compare_versions"))
self.progress_bar.set_label(self.language_manager.get_text("loading"))
self.key_title.setText(self.language_manager.get_text("license_key_section"))
self.update_license_status()
def update_theme(self):
"""Met à jour le style selon le nouveau thème"""
self.update_license_status()

View File

@ -1,4 +1,4 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QLabel, QHBoxLayout
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QLabel, QHBoxLayout, QSizePolicy
from PyQt6.QtCore import Qt
from app.core.main_manager import MainManager, NotificationType
from typing import Optional
@ -26,39 +26,43 @@ class SettingsWindow(QWidget):
def setup_ui(self) -> None:
layout: QVBoxLayout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
layout.addStretch()
layout.addStretch(1)
self.language_layout = QHBoxLayout()
# Paramètres de langue
self.languageLabel = QLabel(self.language_manager.get_text("language"),self)
self.languageLabel.setFixedWidth(120) # Largeur fixe pour l'alignement
self.languageLabel.setMinimumWidth(100)
self.languageLabel.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.language_layout.addWidget(self.languageLabel)
self.languageCombo = self.createLanguageSelector()
self.languageCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.language_layout.addWidget(self.languageCombo)
layout.addLayout(self.language_layout)
layout.addStretch()
layout.addStretch(1)
# Paramètres de thème
self.theme_layout = QHBoxLayout()
self.themeLabel = QLabel(self.language_manager.get_text("theme"), self)
self.themeLabel.setFixedWidth(120) # Même largeur fixe pour l'alignement
self.themeLabel.setMinimumWidth(100)
self.themeLabel.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.theme_layout.addWidget(self.themeLabel)
self.themeCombo = self.createThemeSelector()
self.themeCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.theme_layout.addWidget(self.themeCombo)
layout.addLayout(self.theme_layout)
layout.addStretch()
layout.addStretch(1)
def createLanguageSelector(self) -> QComboBox:
combo: QComboBox = QComboBox()
# Ajouter toutes les langues disponibles

View File

@ -1,4 +1,4 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLabel, QHBoxLayout
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLabel, QHBoxLayout, QSizePolicy
from PyQt6.QtCore import Qt, QThread, pyqtSignal
import smtplib, os
from email.mime.text import MIMEText
@ -62,6 +62,7 @@ class SuggestionWindow(QWidget):
self.language_manager = self.main_manager.get_language_manager()
self.settings_manager = self.main_manager.get_settings_manager()
self.alert_manager = self.main_manager.get_alert_manager()
self.license_manager = self.main_manager.get_license_manager()
self.observer_manager = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
@ -78,16 +79,16 @@ class SuggestionWindow(QWidget):
# Title
self.title_label: QLabel = QLabel(self.language_manager.get_text("suggestion_text"), self)
self.title_label.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;")
self.title_label.setWordWrap(True) # Permet le retour à la ligne automatique
self.title_label.setSizePolicy(self.title_label.sizePolicy().horizontalPolicy(),
self.title_label.sizePolicy().verticalPolicy())
self.title_label.setWordWrap(True)
self.title_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
layout.addWidget(self.title_label)
# Text area for suggestion
self.text_edit: QTextEdit = QTextEdit(self)
self.text_edit.setPlaceholderText(self.language_manager.get_text("suggestion_placeholder"))
layout.addWidget(self.text_edit)
self.text_edit.setMinimumHeight(150)
self.text_edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
layout.addWidget(self.text_edit, 1)
# Button layout
button_layout: QHBoxLayout = QHBoxLayout()
@ -99,7 +100,7 @@ class SuggestionWindow(QWidget):
button_layout.addWidget(self.send_button)
layout.addLayout(button_layout)
def send_suggestion(self) -> None:
message: str = self.text_edit.toPlainText().strip()
@ -111,9 +112,14 @@ class SuggestionWindow(QWidget):
self.send_button.setEnabled(False)
self.send_button.setText(self.language_manager.get_text("sending"))
# Create subject with app name
subject: str = f"Suggestion pour {self.settings_manager.get_config('app_name')}"
content = self.settings_manager.get_config('app_name').replace(' ', '_')
# Ajouter le préfixe "PRIORITAIRE" uniquement si le système de licence est activé ET que l'utilisateur a le support prioritaire
if self.settings_manager.get_config("enable_licensing") and self.license_manager.is_feature_available("priority_support"):
subject = "PRIORITAIRE - "+content
else:
subject = "Suggestion pour "+content
# Create and start email sender thread
self.email_sender = EmailSender(subject, message)
self.email_sender.success.connect(self.on_email_sent)
@ -134,4 +140,4 @@ class SuggestionWindow(QWidget):
def update_language(self) -> None:
self.title_label.setText(self.language_manager.get_text("suggestion_text"))
self.text_edit.setPlaceholderText(self.language_manager.get_text("suggestion_placeholder"))
self.send_button.setText(self.language_manager.get_text("send_suggestion"))
self.send_button.setText(self.language_manager.get_text("send_suggestion"))

66
app/utils/licence.py Normal file
View File

@ -0,0 +1,66 @@
from functools import wraps
from PyQt6.QtWidgets import QMessageBox
from app.core.main_manager import MainManager
def require_license(feature_id: str, show_upgrade_dialog: bool = True):
"""
Décorateur pour protéger une fonctionnalité premium
Usage:
@require_license("advanced_export")
def export_to_excel(self):
# Code de la fonctionnalité premium
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
main_manager = MainManager.get_instance()
license_manager = main_manager.get_license_manager()
settings_manager = main_manager.get_settings_manager()
# Si le système de licence est désactivé, autoriser toutes les fonctionnalités
if not settings_manager.get_config("enable_licensing"):
return func(*args, **kwargs)
if not license_manager.is_feature_available(feature_id):
if show_upgrade_dialog:
show_upgrade_message(feature_id)
return None
return func(*args, **kwargs)
return wrapper
return decorator
def show_upgrade_message(feature_id: str):
"""Affiche un message invitant à acheter la version premium"""
main_manager = MainManager.get_instance()
language_manager = main_manager.get_language_manager()
settings_manager = main_manager.get_settings_manager()
# Si le système de licence est désactivé, ne rien afficher
if not settings_manager.get_config("enable_licensing"):
return
feature_name = settings_manager.get_config("feature_descriptions", {}).get(
feature_id, feature_id
)
msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle(language_manager.get_text("premium_feature"))
msg.setText(language_manager.get_text("premium_feature_message").format(feature=feature_name))
msg.setInformativeText(language_manager.get_text("upgrade_prompt"))
buy_btn = msg.addButton(language_manager.get_text("buy_now"), QMessageBox.ButtonRole.AcceptRole)
activate_btn = msg.addButton(language_manager.get_text("activate_license"), QMessageBox.ButtonRole.ActionRole)
msg.addButton(language_manager.get_text("cancel"), QMessageBox.ButtonRole.RejectRole)
msg.exec()
if msg.clickedButton() == buy_btn:
import webbrowser
webbrowser.open(settings_manager.get_config("purchase_url"))
elif msg.clickedButton() == activate_btn:
from app.ui.windows.activation_window import ActivationWindow
activation_window = ActivationWindow()
activation_window.exec()

View File

@ -1,10 +1,24 @@
{
"app_name": "Applications",
"app_name": "Application",
"app_os": "Windows",
"app_version": "1.0.0",
"architecture": "x64",
"icon_path": "data/assets/icon.ico",
"splash_image": "splash",
"main_script": "main.py",
"git_repo": "https://gitea.louismazin.ovh/LouisMazin/PythonApplicationTemplate"
"git_repo": "https://gitea.louismazin.ovh/LouisMazin/PythonApplicationTemplate",
"enable_licensing": true,
"features_by_license": {
"free": [
"basic_features"
],
"premium": [
"basic_features",
"priority_support"
],
"enterprise": [
"basic_features",
"priority_support"
]
}
}

4
data/assets/license.svg Normal file
View File

@ -0,0 +1,4 @@
<ns0:svg width="800px" height="800px" viewBox="0 0 24 24" xmlns:ns0="http://www.w3.org/2000/svg">
<ns0:path fill-rule="evenodd" clip-rule="evenodd" d="M15.6807 14.5869C19.1708 14.5869 22 11.7692 22 8.29344C22 4.81767 19.1708 2 15.6807 2C12.1907 2 9.3615 4.81767 9.3615 8.29344C9.3615 9.90338 10.0963 11.0743 10.0963 11.0743L2.45441 18.6849C2.1115 19.0264 1.63143 19.9143 2.45441 20.7339L3.33616 21.6121C3.67905 21.9048 4.54119 22.3146 5.2466 21.6121L6.27531 20.5876C7.30403 21.6121 8.4797 21.0267 8.92058 20.4412C9.65538 19.4167 8.77362 18.3922 8.77362 18.3922L9.06754 18.0995C10.4783 19.5045 11.7128 18.6849 12.1537 18.0995C12.8885 17.075 12.1537 16.0505 12.1537 16.0505C11.8598 15.465 11.272 15.465 12.0067 14.7333L12.8885 13.8551C13.5939 14.4405 15.0439 14.5869 15.6807 14.5869Z M17.8853 8.29353C17.8853 9.50601 16.8984 10.4889 15.681 10.4889C14.4635 10.4889 13.4766 9.50601 13.4766 8.29353C13.4766 7.08105 14.4635 6.09814 15.681 6.09814C16.8984 6.09814 17.8853 7.08105 17.8853 8.29353Z" fill="#D1D1D6"/>
</ns0:svg>

View File

@ -1 +1,3 @@
<ns0:svg xmlns:ns0="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="100px" height="100px"><ns0:path d="M 22.205078 2 A 1.0001 1.0001 0 0 0 21.21875 2.8378906 L 20.246094 8.7929688 C 19.076509 9.1331971 17.961243 9.5922728 16.910156 10.164062 L 11.996094 6.6542969 A 1.0001 1.0001 0 0 0 10.708984 6.7597656 L 6.8183594 10.646484 A 1.0001 1.0001 0 0 0 6.7070312 11.927734 L 10.164062 16.873047 C 9.583454 17.930271 9.1142098 19.051824 8.765625 20.232422 L 2.8359375 21.21875 A 1.0001 1.0001 0 0 0 2.0019531 22.205078 L 2.0019531 27.705078 A 1.0001 1.0001 0 0 0 2.8261719 28.691406 L 8.7597656 29.742188 C 9.1064607 30.920739 9.5727226 32.043065 10.154297 33.101562 L 6.6542969 37.998047 A 1.0001 1.0001 0 0 0 6.7597656 39.285156 L 10.648438 43.175781 A 1.0001 1.0001 0 0 0 11.927734 43.289062 L 16.882812 39.820312 C 17.936999 40.39548 19.054994 40.857928 20.228516 41.201172 L 21.21875 47.164062 A 1.0001 1.0001 0 0 0 22.205078 48 L 27.705078 48 A 1.0001 1.0001 0 0 0 28.691406 47.173828 L 29.751953 41.1875 C 30.920633 40.838997 32.033372 40.369697 33.082031 39.791016 L 38.070312 43.291016 A 1.0001 1.0001 0 0 0 39.351562 43.179688 L 43.240234 39.287109 A 1.0001 1.0001 0 0 0 43.34375 37.996094 L 39.787109 33.058594 C 40.355783 32.014958 40.813915 30.908875 41.154297 29.748047 L 47.171875 28.693359 A 1.0001 1.0001 0 0 0 47.998047 27.707031 L 47.998047 22.207031 A 1.0001 1.0001 0 0 0 47.160156 21.220703 L 41.152344 20.238281 C 40.80968 19.078827 40.350281 17.974723 39.78125 16.931641 L 43.289062 11.933594 A 1.0001 1.0001 0 0 0 43.177734 10.652344 L 39.287109 6.7636719 A 1.0001 1.0001 0 0 0 37.996094 6.6601562 L 33.072266 10.201172 C 32.023186 9.6248101 30.909713 9.1579916 29.738281 8.8125 L 28.691406 2.828125 A 1.0001 1.0001 0 0 0 27.705078 2 L 22.205078 2 z M 23.056641 4 L 26.865234 4 L 27.861328 9.6855469 A 1.0001 1.0001 0 0 0 28.603516 10.484375 C 30.066026 10.848832 31.439607 11.426549 32.693359 12.185547 A 1.0001 1.0001 0 0 0 33.794922 12.142578 L 38.474609 8.7792969 L 41.167969 11.472656 L 37.835938 16.220703 A 1.0001 1.0001 0 0 0 37.796875 17.310547 C 38.548366 18.561471 39.118333 19.926379 39.482422 21.380859 A 1.0001 1.0001 0 0 0 40.291016 22.125 L 45.998047 23.058594 L 45.998047 26.867188 L 40.279297 27.871094 A 1.0001 1.0001 0 0 0 39.482422 28.617188 C 39.122545 30.069817 38.552234 31.434687 37.800781 32.685547 A 1.0001 1.0001 0 0 0 37.845703 33.785156 L 41.224609 38.474609 L 38.53125 41.169922 L 33.791016 37.84375 A 1.0001 1.0001 0 0 0 32.697266 37.808594 C 31.44975 38.567585 30.074755 39.148028 28.617188 39.517578 A 1.0001 1.0001 0 0 0 27.876953 40.3125 L 26.867188 46 L 23.052734 46 L 22.111328 40.337891 A 1.0001 1.0001 0 0 0 21.365234 39.53125 C 19.90185 39.170557 18.522094 38.59371 17.259766 37.835938 A 1.0001 1.0001 0 0 0 16.171875 37.875 L 11.46875 41.169922 L 8.7734375 38.470703 L 12.097656 33.824219 A 1.0001 1.0001 0 0 0 12.138672 32.724609 C 11.372652 31.458855 10.793319 30.079213 10.427734 28.609375 A 1.0001 1.0001 0 0 0 9.6328125 27.867188 L 4.0019531 26.867188 L 4.0019531 23.052734 L 9.6289062 22.117188 A 1.0001 1.0001 0 0 0 10.435547 21.373047 C 10.804273 19.898143 11.383325 18.518729 12.146484 17.255859 A 1.0001 1.0001 0 0 0 12.111328 16.164062 L 8.8261719 11.46875 L 11.523438 8.7734375 L 16.185547 12.105469 A 1.0001 1.0001 0 0 0 17.28125 12.148438 C 18.536908 11.394293 19.919867 10.822081 21.384766 10.462891 A 1.0001 1.0001 0 0 0 22.132812 9.6523438 L 23.056641 4 z M 25 17 C 20.593567 17 17 20.593567 17 25 C 17 29.406433 20.593567 33 25 33 C 29.406433 33 33 29.406433 33 25 C 33 20.593567 29.406433 17 25 17 z M 25 19 C 28.325553 19 31 21.674447 31 25 C 31 28.325553 28.325553 31 25 31 C 21.674447 31 19 28.325553 19 25 C 19 21.674447 21.674447 19 25 19 z" fill="#D1D1D6"/></ns0:svg>
<ns0:svg xmlns:ns0="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="100px" height="100px">
<ns0:path d="M 22.205078 2 A 1.0001 1.0001 0 0 0 21.21875 2.8378906 L 20.246094 8.7929688 C 19.076509 9.1331971 17.961243 9.5922728 16.910156 10.164062 L 11.996094 6.6542969 A 1.0001 1.0001 0 0 0 10.708984 6.7597656 L 6.8183594 10.646484 A 1.0001 1.0001 0 0 0 6.7070312 11.927734 L 10.164062 16.873047 C 9.583454 17.930271 9.1142098 19.051824 8.765625 20.232422 L 2.8359375 21.21875 A 1.0001 1.0001 0 0 0 2.0019531 22.205078 L 2.0019531 27.705078 A 1.0001 1.0001 0 0 0 2.8261719 28.691406 L 8.7597656 29.742188 C 9.1064607 30.920739 9.5727226 32.043065 10.154297 33.101562 L 6.6542969 37.998047 A 1.0001 1.0001 0 0 0 6.7597656 39.285156 L 10.648438 43.175781 A 1.0001 1.0001 0 0 0 11.927734 43.289062 L 16.882812 39.820312 C 17.936999 40.39548 19.054994 40.857928 20.228516 41.201172 L 21.21875 47.164062 A 1.0001 1.0001 0 0 0 22.205078 48 L 27.705078 48 A 1.0001 1.0001 0 0 0 28.691406 47.173828 L 29.751953 41.1875 C 30.920633 40.838997 32.033372 40.369697 33.082031 39.791016 L 38.070312 43.291016 A 1.0001 1.0001 0 0 0 39.351562 43.179688 L 43.240234 39.287109 A 1.0001 1.0001 0 0 0 43.34375 37.996094 L 39.787109 33.058594 C 40.355783 32.014958 40.813915 30.908875 41.154297 29.748047 L 47.171875 28.693359 A 1.0001 1.0001 0 0 0 47.998047 27.707031 L 47.998047 22.207031 A 1.0001 1.0001 0 0 0 47.160156 21.220703 L 41.152344 20.238281 C 40.80968 19.078827 40.350281 17.974723 39.78125 16.931641 L 43.289062 11.933594 A 1.0001 1.0001 0 0 0 43.177734 10.652344 L 39.287109 6.7636719 A 1.0001 1.0001 0 0 0 37.996094 6.6601562 L 33.072266 10.201172 C 32.023186 9.6248101 30.909713 9.1579916 29.738281 8.8125 L 28.691406 2.828125 A 1.0001 1.0001 0 0 0 27.705078 2 L 22.205078 2 z M 23.056641 4 L 26.865234 4 L 27.861328 9.6855469 A 1.0001 1.0001 0 0 0 28.603516 10.484375 C 30.066026 10.848832 31.439607 11.426549 32.693359 12.185547 A 1.0001 1.0001 0 0 0 33.794922 12.142578 L 38.474609 8.7792969 L 41.167969 11.472656 L 37.835938 16.220703 A 1.0001 1.0001 0 0 0 37.796875 17.310547 C 38.548366 18.561471 39.118333 19.926379 39.482422 21.380859 A 1.0001 1.0001 0 0 0 40.291016 22.125 L 45.998047 23.058594 L 45.998047 26.867188 L 40.279297 27.871094 A 1.0001 1.0001 0 0 0 39.482422 28.617188 C 39.122545 30.069817 38.552234 31.434687 37.800781 32.685547 A 1.0001 1.0001 0 0 0 37.845703 33.785156 L 41.224609 38.474609 L 38.53125 41.169922 L 33.791016 37.84375 A 1.0001 1.0001 0 0 0 32.697266 37.808594 C 31.44975 38.567585 30.074755 39.148028 28.617188 39.517578 A 1.0001 1.0001 0 0 0 27.876953 40.3125 L 26.867188 46 L 23.052734 46 L 22.111328 40.337891 A 1.0001 1.0001 0 0 0 21.365234 39.53125 C 19.90185 39.170557 18.522094 38.59371 17.259766 37.835938 A 1.0001 1.0001 0 0 0 16.171875 37.875 L 11.46875 41.169922 L 8.7734375 38.470703 L 12.097656 33.824219 A 1.0001 1.0001 0 0 0 12.138672 32.724609 C 11.372652 31.458855 10.793319 30.079213 10.427734 28.609375 A 1.0001 1.0001 0 0 0 9.6328125 27.867188 L 4.0019531 26.867188 L 4.0019531 23.052734 L 9.6289062 22.117188 A 1.0001 1.0001 0 0 0 10.435547 21.373047 C 10.804273 19.898143 11.383325 18.518729 12.146484 17.255859 A 1.0001 1.0001 0 0 0 12.111328 16.164062 L 8.8261719 11.46875 L 11.523438 8.7734375 L 16.185547 12.105469 A 1.0001 1.0001 0 0 0 17.28125 12.148438 C 18.536908 11.394293 19.919867 10.822081 21.384766 10.462891 A 1.0001 1.0001 0 0 0 22.132812 9.6523438 L 23.056641 4 z M 25 17 C 20.593567 17 17 20.593567 17 25 C 17 29.406433 20.593567 33 25 33 C 29.406433 33 33 29.406433 33 25 C 33 20.593567 29.406433 17 25 17 z M 25 19 C 28.325553 19 31 21.674447 31 25 C 31 28.325553 28.325553 31 25 31 C 21.674447 31 19 28.325553 19 25 C 19 21.674447 21.674447 19 25 19 z" fill="#D1D1D6"/>
</ns0:svg>

View File

@ -9,7 +9,7 @@
"confirmation": "Confirmation",
"information": "Information",
"close": "Close",
"suggestion_text": "Do you have a question or an idea to improve this application? Send me a message!",
"suggestion_text": "Do you have a question or an idea to improve my software? Send me a message!",
"suggestion_placeholder": "Type your message here...",
"send_suggestion": "Send",
"sending": "Sending...",
@ -30,7 +30,34 @@
"update_details": "Update Details",
"update_aborted": "Update aborted by user",
"loading": "Loading...",
"verifying_license": "Verifying license...",
"checking_updates": "Checking for updates...",
"initializing": "Initializing interface...",
"loading_complete": "Loading complete"
"loading_complete": "Loading complete",
"activate_license": "Activate your license to unlock all features",
"hardware_id_info": "This unique identifier is tied to your hardware. Share it with support if needed.",
"hardware_id_copied": "Hardware ID copied to clipboard!",
"license_key_section": "License Key :",
"enter_license_key": "Enter your license key",
"activate": "Activate",
"buy_license": "Buy License",
"comparaisons": "Version Comparison",
"free_version": "Free Version",
"premium_version": "Premium Version",
"enterprise_version": "Enterprise Version",
"basic_features": "Basic features",
"priority_support": "Priority support",
"license_active": "License active",
"license_type": "Type",
"license_email": "Email",
"license_expires": "Expires on",
"license_free_mode": "Free mode - Activate a license for more features",
"invalid_license_key": "Invalid license key. It must contain at least 16 characters.",
"premium_feature": "Premium Feature",
"premium_feature_message": "The feature '{feature}' requires a Premium or Enterprise license.",
"upgrade_prompt": "Would you like to upgrade your license?",
"buy_now": "Buy Now",
"cancel": "Cancel",
"activation_required": "Activation is required to continue.",
"compare_versions": "Compare Versions"
}

View File

@ -9,7 +9,7 @@
"confirmation": "Confirmation",
"information": "Information",
"close": "Fermer",
"suggestion_text": "Vous avez une question ou une idée pour améliorer cette application ? Envoyez-moi un message !",
"suggestion_text": "Vous avez une question ou une idée pour améliorer mon logiciel ? Envoyez-moi un message !",
"suggestion_placeholder": "Tapez votre message ici...",
"send_suggestion": "Envoyer",
"sending": "Envoi...",
@ -30,7 +30,34 @@
"update_details": "Détails de la mise à jour",
"update_aborted": "Mise à jour annulée par l'utilisateur",
"loading": "Chargement...",
"verifying_license": "Vérification de la licence...",
"checking_updates": "Vérification des mises à jour...",
"initializing": "Initialisation de l'interface...",
"loading_complete": "Chargement terminé"
"loading_complete": "Chargement terminé",
"activate_license": "Activez votre licence pour débloquer toutes les fonctionnalités",
"hardware_id_info": "Cet identifiant unique est lié à votre matériel. Partagez-le avec le support si nécessaire.",
"hardware_id_copied": "ID matériel copié dans le presse-papier !",
"license_key_section": "Clé de licence :",
"enter_license_key": "Entrez votre clé de licence",
"activate": "Activer",
"buy_license": "Acheter une licence",
"comparaisons": "Comparaison des versions",
"free_version": "Version Gratuite",
"premium_version": "Version Premium",
"enterprise_version": "Version Enterprise",
"basic_features": "Fonctionnalités de base",
"priority_support": "Support prioritaire",
"license_active": "Licence active",
"license_type": "Type",
"license_email": "Email",
"license_expires": "Expire le",
"license_free_mode": "Mode gratuit - Activez une licence pour plus de fonctionnalités",
"invalid_license_key": "Clé de licence invalide. Elle doit contenir au moins 16 caractères.",
"premium_feature": "Fonctionnalité Premium",
"premium_feature_message": "La fonctionnalité '{feature}' nécessite une licence Premium ou Enterprise.",
"upgrade_prompt": "Souhaitez-vous mettre à niveau votre licence ?",
"buy_now": "Acheter maintenant",
"cancel": "Annuler",
"activation_required": "L'activation est requise pour continuer.",
"compare_versions": "Comparer les versions"
}

View File

@ -25,6 +25,13 @@ def preload_application(progress_callback, splash=None):
main_manager = MainManager.get_instance()
language_manager = main_manager.get_language_manager()
update_manager = main_manager.get_update_manager()
license_manager = main_manager.get_license_manager()
settings_manager = main_manager.get_settings_manager()
# Vérifier la licence uniquement si le système est activé
if settings_manager.get_config("enable_licensing"):
progress_callback(language_manager.get_text("verifying_license"))
license_manager.verify_license()
progress_callback(language_manager.get_text("checking_updates"))
@ -32,7 +39,7 @@ def preload_application(progress_callback, splash=None):
return False
progress_callback(language_manager.get_text("initializing"))
preloaded_window = MainWindow()
progress_callback(language_manager.get_text("loading_complete"))