diff --git a/.env.example b/.env.example
index 16bd4e2..03ef33d 100644
--- a/.env.example
+++ b/.env.example
@@ -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
\ No newline at end of file
diff --git a/app/core/license_manager.py b/app/core/license_manager.py
new file mode 100644
index 0000000..42f97f5
--- /dev/null
+++ b/app/core/license_manager.py
@@ -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")
+ }
\ No newline at end of file
diff --git a/app/core/main_manager.py b/app/core/main_manager.py
index 439b5c4..b6f9077 100644
--- a/app/core/main_manager.py
+++ b/app/core/main_manager.py
@@ -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
\ No newline at end of file
+ return self.update_manager
+
+ def get_license_manager(self) -> LicenseManager:
+ return self.license_manager
\ No newline at end of file
diff --git a/app/core/theme_manager.py b/app/core/theme_manager.py
index 6cd9f9f..ceb36d3 100644
--- a/app/core/theme_manager.py
+++ b/app/core/theme_manager.py
@@ -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")};
diff --git a/app/ui/main_window.py b/app/ui/main_window.py
index c380d74..6751649 100644
--- a/app/ui/main_window.py
+++ b/app/ui/main_window.py
@@ -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)
diff --git a/app/ui/widgets/tabs_widget.py b/app/ui/widgets/tabs_widget.py
index 6e3f9e8..6ad062f 100644
--- a/app/ui/widgets/tabs_widget.py
+++ b/app/ui/widgets/tabs_widget.py
@@ -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)
diff --git a/app/ui/windows/activation_window.py b/app/ui/windows/activation_window.py
new file mode 100644
index 0000000..54b07a0
--- /dev/null
+++ b/app/ui/windows/activation_window.py
@@ -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()
\ No newline at end of file
diff --git a/app/ui/windows/settings_window.py b/app/ui/windows/settings_window.py
index ede1bf2..34d4302 100644
--- a/app/ui/windows/settings_window.py
+++ b/app/ui/windows/settings_window.py
@@ -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
diff --git a/app/ui/windows/suggestion_window.py b/app/ui/windows/suggestion_window.py
index c418e2d..1852c0c 100644
--- a/app/ui/windows/suggestion_window.py
+++ b/app/ui/windows/suggestion_window.py
@@ -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"))
\ No newline at end of file
diff --git a/app/utils/licence.py b/app/utils/licence.py
new file mode 100644
index 0000000..88623eb
--- /dev/null
+++ b/app/utils/licence.py
@@ -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()
\ No newline at end of file
diff --git a/config.json b/config.json
index 960c68e..5998352 100644
--- a/config.json
+++ b/config.json
@@ -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"
+ ]
+ }
}
\ No newline at end of file
diff --git a/data/assets/license.svg b/data/assets/license.svg
new file mode 100644
index 0000000..d0241b2
--- /dev/null
+++ b/data/assets/license.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/data/assets/settings.svg b/data/assets/settings.svg
index b9b2280..3efc063 100644
--- a/data/assets/settings.svg
+++ b/data/assets/settings.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/data/lang/en.json b/data/lang/en.json
index 3e639a3..b49a508 100644
--- a/data/lang/en.json
+++ b/data/lang/en.json
@@ -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"
}
\ No newline at end of file
diff --git a/data/lang/fr.json b/data/lang/fr.json
index 3a1a70a..c556a5e 100644
--- a/data/lang/fr.json
+++ b/data/lang/fr.json
@@ -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"
}
\ No newline at end of file
diff --git a/main.py b/main.py
index ab47ef8..7d9f402 100644
--- a/main.py
+++ b/main.py
@@ -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"))