From dd43bf5abf0cc04ec9ae808769a267e7d2e58bfb Mon Sep 17 00:00:00 2001 From: Louis Mazin Date: Tue, 19 Aug 2025 18:03:29 +0200 Subject: [PATCH] alert_manager and improvements --- BUILD.spec | 2 +- app/core/alert_manager.py | 20 +++ app/core/language_manager.py | 11 +- app/core/main_manager.py | 41 +++--- app/core/settings_manager.py | 211 +++++++++++++++++++++++----- app/core/theme_manager.py | 25 ++-- app/ui/main_window.py | 40 +++--- app/ui/windows/settings_window.py | 34 +++-- app/ui/windows/suggestion_window.py | 81 +++++------ app/utils/paths.py | 31 ++-- data/lang/en.json | 3 +- data/lang/fr.json | 3 +- data/others/defaults_settings.json | 2 +- main.py | 15 +- 14 files changed, 358 insertions(+), 161 deletions(-) create mode 100644 app/core/alert_manager.py diff --git a/BUILD.spec b/BUILD.spec index 97a672a..602f831 100644 --- a/BUILD.spec +++ b/BUILD.spec @@ -59,7 +59,7 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=True, + console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/app/core/alert_manager.py b/app/core/alert_manager.py new file mode 100644 index 0000000..ef5dde2 --- /dev/null +++ b/app/core/alert_manager.py @@ -0,0 +1,20 @@ +from PyQt6.QtWidgets import QMessageBox + +class AlertManager: + + def __init__(self, language_manager, theme_manager) -> None: + self.language_manager = language_manager + self.theme_manager = theme_manager + + def show_success(self, success_key: str, parent=None) -> None: + success_title = self.language_manager.get_text("success") + success_text = self.language_manager.get_text(success_key) + + QMessageBox.information(parent, success_title, success_text) + + def show_error(self, error_key: str, parent=None) -> None: + + error_title = self.language_manager.get_text("error") + error_text = self.language_manager.get_text(error_key) + + QMessageBox.critical(parent, error_title, error_text) diff --git a/app/core/language_manager.py b/app/core/language_manager.py index 0f8aeca..fe689ce 100644 --- a/app/core/language_manager.py +++ b/app/core/language_manager.py @@ -2,6 +2,7 @@ from json import JSONDecodeError, load from os import listdir, path from typing import Dict import app.utils.paths as paths +from app.core.settings_manager import SettingsManager class LanguageManager: """ @@ -9,9 +10,9 @@ class LanguageManager: Charge les fichiers JSON de langue dans data/lang/, permet de changer la langue courante, et récupérer des textes traduits. """ - def __init__(self, settings_manager) -> None: + def __init__(self, settings_manager: SettingsManager) -> None: self.translations: Dict[str, Dict[str, str]] = {} - self.settings_manager = settings_manager + self.settings_manager: SettingsManager = settings_manager self.load_all_translations() @@ -19,12 +20,12 @@ class LanguageManager: """ Charge tous les fichiers JSON dans data/lang/ comme dictionnaires. """ - lang_dir = paths.get_lang_path() + lang_dir: str = paths.get_lang_path() for filename in listdir(lang_dir): if filename.endswith(".json"): - lang_code = filename[:-5] # strip .json - file_path = path.join(lang_dir, filename) + lang_code: str = filename[:-5] # strip .json + file_path: str = path.join(lang_dir, filename) try: with open(file_path, "r", encoding="utf-8") as f: self.translations[lang_code] = load(f) diff --git a/app/core/main_manager.py b/app/core/main_manager.py index 9f1e987..da39d9e 100644 --- a/app/core/main_manager.py +++ b/app/core/main_manager.py @@ -2,31 +2,40 @@ from app.core.observer_manager import ObserverManager, NotificationType from app.core.language_manager import LanguageManager from app.core.theme_manager import ThemeManager from app.core.settings_manager import SettingsManager +from app.core.alert_manager import AlertManager +from typing import Optional class MainManager: - _instance = None - def __init__(self): + _instance: Optional['MainManager'] = None + + def __init__(self) -> None: if MainManager._instance is not None: raise Exception("This class is a singleton!") else: MainManager._instance = self - self.observer_manager = ObserverManager() - self.theme_manager = ThemeManager() - self.settings_manager = SettingsManager(self.observer_manager,self.theme_manager) - self.language_manager = LanguageManager(self.settings_manager) + self.observer_manager: ObserverManager = ObserverManager() + self.theme_manager: ThemeManager = ThemeManager() + self.settings_manager: SettingsManager = SettingsManager(self.observer_manager, self.theme_manager) + self.language_manager: LanguageManager = LanguageManager(self.settings_manager) + self.alert_manager: AlertManager = AlertManager(self.language_manager, self.theme_manager) - def get_observer_manager(self): - return self.observer_manager - def get_theme_manager(self): - return self.theme_manager - def get_settings_manager(self): - return self.settings_manager - def get_language_manager(self): - return self.language_manager - @classmethod - def get_instance(cls): + def get_instance(cls) -> 'MainManager': if cls._instance is None: cls._instance = cls() return cls._instance + + def get_observer_manager(self) -> ObserverManager: + return self.observer_manager + + def get_theme_manager(self) -> ThemeManager: + return self.theme_manager + + def get_settings_manager(self) -> SettingsManager: + return self.settings_manager + + def get_language_manager(self) -> LanguageManager: + return self.language_manager + def get_alert_manager(self) -> AlertManager: + return self.alert_manager \ No newline at end of file diff --git a/app/core/settings_manager.py b/app/core/settings_manager.py index 3c72d1f..0fc789e 100644 --- a/app/core/settings_manager.py +++ b/app/core/settings_manager.py @@ -1,65 +1,210 @@ from PyQt6.QtCore import QSettings -from app.core.observer_manager import NotificationType +from app.core.observer_manager import ObserverManager, NotificationType +from app.core.theme_manager import ThemeManager from os import path import app.utils.paths as paths import json +import logging +from typing import Dict, Any, Union + +# Configure logging +logger: logging.Logger = logging.getLogger(__name__) class SettingsManager: """ Gestion des paramètres utilisateurs avec sauvegarde persistante via QSettings. Notifie les changements via ObserverManager. """ - def __init__(self, observer_manager, theme_manager): - self.observer_manager = observer_manager - self.theme_manager = theme_manager + def __init__(self, observer_manager: ObserverManager, theme_manager: ThemeManager) -> None: + self.observer_manager: ObserverManager = observer_manager + self.theme_manager: ThemeManager = theme_manager - # Load default settings from JSON file - defaults_path = path.join(paths.get_data_dir(), "others", "defaults_settings.json") - with open(defaults_path, 'r', encoding='utf-8') as f: - self.default_settings = json.load(f) - - with open(paths.resource_path("config.json"), 'r', encoding='utf-8') as f: - self.config = json.load(f) + # Hardcoded fallback settings in case files are missing + self.fallback_settings: Dict[str, Any] = { + "theme": "dark", + "lang": "en", + "window_size": {"width": 1000, "height": 600}, + "maximized": False + } + + self.fallback_config: Dict[str, Any] = { + "app_name": "Application", + "python_version": "3.11.7", + "app_os": "Windows", + "app_version": "1.0.0", + "architecture": "x64", + "icon_path": "data/assets/icon.ico", + "main_script": "main.py", + "git_repo": "" + } + + # Load default settings + self.default_settings: Dict[str, Any] = self._load_default_settings() + # Load config + self.config: Dict[str, Any] = self._load_config() + + # Initialize QSettings with error handling + self.settings: QSettings = self._initialize_qsettings() - self.settings = QSettings(path.join(paths.get_user_data_dir(self.get_config("app_name")), self.get_config("app_name") + ".ini"), QSettings.Format.IniFormat) + # Set initial theme + try: + self.theme_manager.set_theme(self.get_theme()) + except Exception as e: + logger.error(f"Error setting initial theme: {e}") + self.theme_manager.set_theme(self.fallback_settings["theme"]) - self.theme_manager.set_theme(self.get_theme()) + def _load_default_settings(self) -> Dict[str, Any]: + """Load default settings with error handling""" + try: + defaults_path = path.join(paths.get_data_dir(), "others", "defaults_settings.json") + if path.exists(defaults_path): + with open(defaults_path, 'r', encoding='utf-8') as f: + settings = json.load(f) + # Validate required keys + for key in self.fallback_settings.keys(): + if key not in settings: + logger.warning(f"Missing key '{key}' in defaults_settings.json, using fallback") + settings[key] = self.fallback_settings[key] + return settings + else: + logger.warning(f"defaults_settings.json not found, using fallback settings") + return self.fallback_settings.copy() + except (json.JSONDecodeError, FileNotFoundError, KeyError, UnicodeDecodeError) as e: + logger.error(f"Error loading default settings: {e}") + return self.fallback_settings.copy() + except Exception as e: + logger.error(f"Unexpected error loading default settings: {e}") + return self.fallback_settings.copy() + + def _load_config(self) -> Dict[str, Any]: + """Load config.json with error handling""" + try: + config_path = paths.resource_path("config.json") + if path.exists(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + # Validate required keys + for key in self.fallback_config.keys(): + if key not in config: + logger.warning(f"Missing key '{key}' in config.json, using fallback") + config[key] = self.fallback_config[key] + return config + else: + logger.warning("config.json not found, using fallback config") + return self.fallback_config.copy() + except (json.JSONDecodeError, FileNotFoundError, KeyError, UnicodeDecodeError) as e: + logger.error(f"Error loading config: {e}") + return self.fallback_config.copy() + except Exception as e: + logger.error(f"Unexpected error loading config: {e}") + return self.fallback_config.copy() + + def _initialize_qsettings(self) -> QSettings: + """Initialize QSettings with error handling""" + try: + app_name = self.get_config("app_name") + user_data_dir = paths.get_user_data_dir(app_name) + settings_path = path.join(user_data_dir, f"{app_name}.ini") + return QSettings(settings_path, QSettings.Format.IniFormat) + except Exception as e: + logger.error(f"Error initializing QSettings: {e}") + # Fallback to memory-only settings + return QSettings() # Config - def get_config(self, key: str): - return self.config.get(key, None) + def get_config(self, key: str) -> Any: + """Get configuration value by key""" + try: + return self.config.get(key, self.fallback_config.get(key)) + except Exception as e: + logger.error(f"Error getting config key '{key}': {e}") + return self.fallback_config.get(key) # Theme def get_theme(self) -> str: - return self.settings.value("theme", self.default_settings.get("theme")) + """Get the current theme""" + try: + return self.settings.value("theme", self.default_settings.get("theme")) + except Exception as e: + logger.error(f"Error getting theme: {e}") + return self.fallback_settings["theme"] def set_theme(self, mode: str) -> None: - if mode != self.get_theme(): - self.settings.setValue("theme", mode) - self.theme_manager.set_theme(mode) - self.observer_manager.notify(NotificationType.THEME) + """Set the application theme""" + try: + if mode != self.get_theme(): + self.settings.setValue("theme", mode) + self.theme_manager.set_theme(mode) + self.observer_manager.notify(NotificationType.THEME) + except Exception as e: + logger.error(f"Error setting theme: {e}") # Language def get_language(self) -> str: - return self.settings.value("lang", self.default_settings.get("lang")) + """Get the current language""" + try: + return self.settings.value("lang", self.default_settings.get("lang")) + except Exception as e: + logger.error(f"Error getting language: {e}") + return self.fallback_settings["lang"] def set_language(self, lang_code: str) -> None: - if lang_code != self.get_language(): - self.settings.setValue("lang", lang_code) - self.observer_manager.notify(NotificationType.LANGUAGE) + """Set the application language""" + try: + if lang_code != self.get_language(): + self.settings.setValue("lang", lang_code) + self.observer_manager.notify(NotificationType.LANGUAGE) + except Exception as e: + logger.error(f"Error setting language: {e}") # Window size and maximized - def get_window_size(self) -> dict: - return self.settings.value("window_size", self.default_settings.get("window_size")) + def get_window_size(self) -> Dict[str, int]: + """Get the window size""" + try: + size = self.settings.value("window_size", self.default_settings.get("window_size")) + # Validate window size values + if isinstance(size, dict) and "width" in size and "height" in size: + width = int(size["width"]) if size["width"] > 0 else self.fallback_settings["window_size"]["width"] + height = int(size["height"]) if size["height"] > 0 else self.fallback_settings["window_size"]["height"] + return {"width": width, "height": height} + else: + return self.fallback_settings["window_size"].copy() + except Exception as e: + logger.error(f"Error getting window size: {e}") + return self.fallback_settings["window_size"].copy() def set_window_size(self, width: int, height: int) -> None: - current_size = self.get_window_size() - if current_size["width"] != width or current_size["height"] != height: - new_size = {"width": width, "height": height} - self.settings.setValue("window_size", new_size) + """Set the window size""" + try: + # Validate input values + if width <= 0 or height <= 0: + logger.warning(f"Invalid window size: {width}x{height}") + return + + current_size = self.get_window_size() + if current_size["width"] != width or current_size["height"] != height: + new_size = {"width": width, "height": height} + self.settings.setValue("window_size", new_size) + except Exception as e: + logger.error(f"Error setting window size: {e}") def get_maximized(self) -> bool: - return self.settings.value("maximized", self.default_settings.get("maximized")) == "true" + """Check if the window is maximized""" + try: + value = self.settings.value("maximized", self.default_settings.get("maximized")) + if isinstance(value, str): + return value.lower() == "true" + elif isinstance(value, bool): + return value + else: + return self.fallback_settings["maximized"] + except Exception as e: + logger.error(f"Error getting maximized state: {e}") + return self.fallback_settings["maximized"] - def set_maximized(self, maximized: bool): - self.settings.setValue("maximized", maximized) \ No newline at end of file + def set_maximized(self, maximized: bool) -> None: + """Set the window maximized state""" + try: + self.settings.setValue("maximized", maximized) + except Exception as e: + logger.error(f"Error setting maximized state: {e}") \ No newline at end of file diff --git a/app/core/theme_manager.py b/app/core/theme_manager.py index 94c89b7..a753c30 100644 --- a/app/core/theme_manager.py +++ b/app/core/theme_manager.py @@ -1,32 +1,35 @@ import app.utils.paths as paths import os, json +from typing import List, Dict, Any, Optional class Theme: - def __init__(self, name: str, colors: dict): - self.name = name - self.colors = colors + def __init__(self, name: str, colors: Dict[str, str]) -> None: + self.name: str = name + self.colors: Dict[str, str] = colors def get_color(self, element: str) -> str: return self.colors.get(element, "#FFFFFF") class ThemeManager: - def __init__(self): - theme_path = os.path.join(paths.get_data_dir(), "themes") - self.themes = [] + def __init__(self) -> None: + theme_path: str = os.path.join(paths.get_data_dir(), "themes") + self.themes: List[Theme] = [] for theme_file in os.listdir(theme_path): if theme_file.endswith(".json"): with open(os.path.join(theme_path, theme_file), 'r', encoding='utf-8') as f: - theme_data = json.load(f) - theme = Theme(theme_data["theme_name"], theme_data["colors"]) + theme_data: Dict[str, Any] = json.load(f) + theme: Theme = Theme(theme_data["theme_name"], theme_data["colors"]) self.themes.append(theme) - self.current_theme = self.themes[0] + self.current_theme: Theme = self.themes[0] def set_theme(self, theme: str) -> None: if theme != self.current_theme.name: - self.current_theme = next((t for t in self.themes if t.name == theme), self.current_theme) + found_theme: Optional[Theme] = next((t for t in self.themes if t.name == theme), None) + if found_theme: + self.current_theme = found_theme - def get_theme(self) -> str: + def get_theme(self) -> Theme: return self.current_theme def get_sheet(self) -> str: diff --git a/app/ui/main_window.py b/app/ui/main_window.py index d530487..6aae7b4 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -1,17 +1,18 @@ from PyQt6.QtWidgets import QApplication, QMainWindow -from PyQt6.QtGui import QIcon -from PyQt6.QtCore import QSize +from PyQt6.QtGui import QIcon, 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 from app.ui.windows.settings_window import SettingsWindow from app.ui.windows.suggestion_window import SuggestionWindow import app.utils.paths as paths +from typing import Optional class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.main_manager = MainManager.get_instance() + self.main_manager: MainManager = MainManager.get_instance() self.language_manager = self.main_manager.get_language_manager() self.theme_manager = self.main_manager.get_theme_manager() @@ -19,23 +20,26 @@ class MainWindow(QMainWindow): self.observer_manager = self.main_manager.get_observer_manager() self.observer_manager.subscribe(NotificationType.THEME, self.update_theme) - self.is_maximizing = False + self.is_maximizing: bool = False + self.current_size: QSize + self.previous_size: QSize - app = QApplication.instance() - size = app.primaryScreen().size() + # UI elements + self.side_menu: TabsWidget + self.settings_window: SettingsWindow + self.suggestion_window: SuggestionWindow + + app: Optional[QApplication] = QApplication.instance() + size: QSize = app.primaryScreen().size() self.settings_manager.minScreenSize = min(size.height(),size.width()) self.setMinimumSize(600, 400) - self.setWindowTitle(self.settings_manager.get_config("app_name")) - self.setStyleSheet(self.theme_manager.get_sheet()) - self.setWindowIcon(QIcon(paths.get_asset_path("icon"))) - self.setup_ui() self.apply_saved_window_state() - def apply_saved_window_state(self): + def apply_saved_window_state(self) -> None: """Apply saved window size and maximized state""" - window_size = self.settings_manager.get_window_size() + window_size: dict = self.settings_manager.get_window_size() self.current_size = QSize(window_size["width"], window_size["height"]) self.previous_size = QSize(window_size["width"], window_size["height"]) self.resize(self.current_size) @@ -43,7 +47,7 @@ class MainWindow(QMainWindow): self.is_maximizing = True self.showMaximized() - def changeEvent(self, event): + def changeEvent(self, event: QEvent) -> None: """Handle window state changes""" super().changeEvent(event) if event.type() == event.Type.WindowStateChange: @@ -56,13 +60,13 @@ class MainWindow(QMainWindow): self.current_size = self.previous_size self.settings_manager.set_maximized(self.isMaximized()) - def resizeEvent(self, a0): + def resizeEvent(self, a0: QResizeEvent) -> None: # Ne pas sauvegarder la taille si on est en train de maximiser if not self.isMaximized() and not self.is_maximizing: self.previous_size = self.current_size self.current_size = self.size() - def closeEvent(self, event): + def closeEvent(self, event: QCloseEvent) -> None: """Handle application close event""" super().closeEvent(event) # si la difference de taille est plus grande que 10 pixels, enregistrer previoussize @@ -73,7 +77,7 @@ class MainWindow(QMainWindow): self.current_size.height() ) - def setup_ui(self): + def setup_ui(self) -> None: self.side_menu = TabsWidget(self,MenuDirection.HORIZONTAL,70,None,10,1,BorderSide.TOP) self.settings_window = SettingsWindow(self) @@ -84,5 +88,5 @@ class MainWindow(QMainWindow): self.setCentralWidget(self.side_menu) - def update_theme(self): + def update_theme(self) -> None: self.setStyleSheet(self.theme_manager.get_sheet()) \ No newline at end of file diff --git a/app/ui/windows/settings_window.py b/app/ui/windows/settings_window.py index b2bad14..c2cd628 100644 --- a/app/ui/windows/settings_window.py +++ b/app/ui/windows/settings_window.py @@ -1,21 +1,30 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QLabel, QHBoxLayout from PyQt6.QtCore import Qt from app.core.main_manager import MainManager, NotificationType +from typing import Optional class SettingsWindow(QWidget): - def __init__(self, parent=None): + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) - self.main_manager = MainManager.get_instance() + self.main_manager: MainManager = MainManager.get_instance() self.language_manager = self.main_manager.get_language_manager() self.settings_manager = self.main_manager.get_settings_manager() self.observer_manager = self.main_manager.get_observer_manager() self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language) + # Type hints for UI elements + self.language_layout: QHBoxLayout + self.languageLabel: QLabel + self.languageCombo: QComboBox + self.theme_layout: QHBoxLayout + self.themeLabel: QLabel + self.themeCombo: QComboBox + self.setup_ui() - def setup_ui(self): - layout = QVBoxLayout(self) + def setup_ui(self) -> None: + layout: QVBoxLayout = QVBoxLayout(self) layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) @@ -32,7 +41,6 @@ class SettingsWindow(QWidget): layout.addLayout(self.language_layout) # Paramètres de thème - self.theme_layout = QHBoxLayout() self.themeLabel = QLabel(self.language_manager.get_text("theme"), self) @@ -46,8 +54,8 @@ class SettingsWindow(QWidget): layout.addStretch() - def createLanguageSelector(self): - combo = QComboBox() + def createLanguageSelector(self) -> QComboBox: + combo: QComboBox = QComboBox() # Ajouter toutes les langues disponibles for langCode, langData in self.language_manager.translations.items(): combo.addItem(langData["lang_name"], langCode) @@ -59,8 +67,8 @@ class SettingsWindow(QWidget): return combo - def createThemeSelector(self): - combo = QComboBox() + def createThemeSelector(self) -> QComboBox: + combo: QComboBox = QComboBox() # Ajouter les options de thème combo.addItem(self.language_manager.get_text("light_theme"), "light") @@ -73,14 +81,14 @@ class SettingsWindow(QWidget): return combo - def change_language(self, index): + def change_language(self, index: int) -> None: self.settings_manager.set_language(self.languageCombo.itemData(index)) - def change_theme(self, index): - theme = self.themeCombo.itemData(index) + def change_theme(self, index: int) -> None: + theme: str = self.themeCombo.itemData(index) self.settings_manager.set_theme(theme) - def update_language(self): + def update_language(self) -> None: self.languageLabel.setText(self.language_manager.get_text("language")) self.themeLabel.setText(self.language_manager.get_text("theme")) diff --git a/app/ui/windows/suggestion_window.py b/app/ui/windows/suggestion_window.py index fc8d7dc..e7057f6 100644 --- a/app/ui/windows/suggestion_window.py +++ b/app/ui/windows/suggestion_window.py @@ -1,12 +1,11 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLabel, QMessageBox, QHBoxLayout from PyQt6.QtCore import Qt, QThread, pyqtSignal -import time -import smtplib -import os +import smtplib, os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from dotenv import load_dotenv from app.core.main_manager import MainManager, NotificationType +from typing import Optional # Load environment variables from .env file load_dotenv() @@ -15,25 +14,25 @@ class EmailSender(QThread): success = pyqtSignal() error = pyqtSignal(str) - def __init__(self, subject, message): + def __init__(self, subject: str, message: str) -> None: super().__init__() - self.subject = subject - self.message = message + self.subject: str = subject + self.message: str = message - def run(self): + def run(self) -> None: try: # Get email configuration from environment variables - email = os.getenv('EMAIL_ADDRESS') - password = os.getenv('EMAIL_PASSWORD') - smtp_server = os.getenv('EMAIL_SMTP_SERVER', 'smtp.gmail.com') - smtp_port = int(os.getenv('EMAIL_SMTP_PORT', '587')) + email: Optional[str] = os.getenv('EMAIL_ADDRESS') + password: Optional[str] = os.getenv('EMAIL_PASSWORD') + smtp_server: str = os.getenv('EMAIL_SMTP_SERVER', 'smtp.gmail.com') + smtp_port: int = int(os.getenv('EMAIL_SMTP_PORT', '587')) if not email or not password: self.error.emit("password") return # Create message - msg = MIMEMultipart() + msg: MIMEMultipart = MIMEMultipart() msg['From'] = email msg['To'] = email msg['Subject'] = self.subject @@ -42,64 +41,69 @@ class EmailSender(QThread): msg.attach(MIMEText(self.message, 'plain')) # Create SMTP session - server = smtplib.SMTP(smtp_server, smtp_port) + server: smtplib.SMTP = smtplib.SMTP(smtp_server, smtp_port) server.starttls() # Enable TLS encryption # Login with app password server.login(email, password) # Send email - text = msg.as_string() + text: str = msg.as_string() server.sendmail(email, email, text) server.quit() - self.success.emit() + except smtplib.SMTPAuthenticationError: + self.error.emit("email_credentials_error") except Exception: - self.error.emit("") + self.error.emit("suggestion_send_error") class SuggestionWindow(QWidget): - def __init__(self, parent=None): + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) - self.main_manager = MainManager.get_instance() + self.main_manager: MainManager = MainManager.get_instance() 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.observer_manager = self.main_manager.get_observer_manager() self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language) + self.email_sender: Optional[EmailSender] = None + self.setup_ui() - def setup_ui(self): - layout = QVBoxLayout(self) + def setup_ui(self) -> None: + layout: QVBoxLayout = QVBoxLayout(self) layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) # Title - self.title_label = QLabel(self.language_manager.get_text("suggestion_text"), self) + 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;") layout.addWidget(self.title_label) # Text area for suggestion - self.text_edit = QTextEdit(self) + self.text_edit: QTextEdit = QTextEdit(self) self.text_edit.setPlaceholderText(self.language_manager.get_text("suggestion_placeholder")) self.text_edit.setMinimumHeight(200) layout.addWidget(self.text_edit) # Button layout - button_layout = QHBoxLayout() + button_layout: QHBoxLayout = QHBoxLayout() button_layout.addStretch() # Send button - self.send_button = QPushButton(self.language_manager.get_text("send_suggestion"), self) + self.send_button: QPushButton = QPushButton(self.language_manager.get_text("send_suggestion"), self) self.send_button.clicked.connect(self.send_suggestion) button_layout.addWidget(self.send_button) layout.addLayout(button_layout) layout.addStretch() - def send_suggestion(self): - message = self.text_edit.toPlainText().strip() + def send_suggestion(self) -> None: + message: str = self.text_edit.toPlainText().strip() - if not message: + if len(message)<15: + self.alert_manager.show_error("suggestion_too_short") return # Disable send button during sending @@ -107,7 +111,7 @@ class SuggestionWindow(QWidget): self.send_button.setText(self.language_manager.get_text("sending")) # Create subject with app name - subject = f"Suggestion for {self.settings_manager.get_config('app_name')}" + subject: str = f"Suggestion for {self.settings_manager.get_config('app_name')}" # Create and start email sender thread self.email_sender = EmailSender(subject, message) @@ -115,27 +119,18 @@ class SuggestionWindow(QWidget): self.email_sender.error.connect(self.on_email_error) self.email_sender.start() - def on_email_sent(self): + def on_email_sent(self) -> None: self.send_button.setEnabled(True) self.send_button.setText(self.language_manager.get_text("send_suggestion")) - - QMessageBox.information(self, - self.language_manager.get_text("success"), - self.language_manager.get_text("suggestion_sent_success")) + self.alert_manager.show_success("suggestion_sent_success") self.text_edit.clear() - def on_email_error(self, error): + def on_email_error(self, error: str) -> None: self.send_button.setEnabled(True) self.send_button.setText(self.language_manager.get_text("send_suggestion")) - if error == "password": - message = self.language_manager.get_text("email_credentials_error") - else: - message = self.language_manager.get_text("suggestion_send_error") - QMessageBox.critical(self, - self.language_manager.get_text("error"), - message) + self.alert_manager.show_error(error) - def update_language(self): + 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")) diff --git a/app/utils/paths.py b/app/utils/paths.py index d5f777b..aba4dbf 100644 --- a/app/utils/paths.py +++ b/app/utils/paths.py @@ -2,46 +2,47 @@ from os import path, getenv, mkdir import sys from platform import system from pathlib import Path +from typing import Optional -def resource_path(relative_path: str) -> Path: +def resource_path(relative_path: str) -> str: """ Get absolute path to resource, works for dev and for PyInstaller bundle. PyInstaller stores bundled files in _MEIPASS folder. """ try: - base_path = Path(sys._MEIPASS) # PyInstaller temp folder + base_path: Path = Path(sys._MEIPASS) # PyInstaller temp folder except AttributeError: - base_path = Path(__file__).parent.parent.parent # Dev environment: source/ folder + base_path: Path = Path(__file__).parent.parent.parent # Dev environment: source/ folder return path.join(base_path, relative_path) -def get_data_dir() -> Path: +def get_data_dir() -> str: return resource_path("data") -def get_lang_path() -> Path: +def get_lang_path() -> str: return path.join(get_data_dir(), "lang") -def get_asset_path(asset: str) -> Path: +def get_asset_path(asset: str) -> str: return path.join(get_data_dir(), "assets", f"{asset}.png") -def get_asset_svg_path(asset: str) -> Path: +def get_asset_svg_path(asset: str) -> str: return path.join(get_data_dir(), "assets", f"{asset}.svg") -def get_user_data_dir(app_name: str) -> Path: - home = Path.home() - os = system() +def get_user_data_dir(app_name: str) -> str: + home: Path = Path.home() + os: str = system() if os == "Windows": - appdata = getenv('APPDATA') + appdata: Optional[str] = getenv('APPDATA') if appdata: - user_data_path = path.join(Path(appdata), app_name) + user_data_path: str = path.join(Path(appdata), app_name) else: - user_data_path = path.join(home, "AppData", "Roaming", app_name) + user_data_path: str = path.join(home, "AppData", "Roaming", app_name) elif os == "Darwin": - user_data_path = path.join(home, "Library", "Application Support", app_name) + user_data_path: str = path.join(home, "Library", "Application Support", app_name) else: - user_data_path = path.join(home, ".local", "share", app_name) + user_data_path: str = path.join(home, ".local", "share", app_name) if not path.exists(user_data_path): mkdir(user_data_path) diff --git a/data/lang/en.json b/data/lang/en.json index 1f76faa..63e98cb 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -15,5 +15,6 @@ "error": "Error", "suggestion_sent_success": "Your message has been sent successfully!", "suggestion_send_error": "Error sending message. Try again later.", - "email_credentials_error": "Email credentials not configured. Please set your email and password in the .env file." + "email_credentials_error": "Email credentials not or bad configured. Please set your email and password in the .env file.", + "suggestion_too_short": "The message must be at least 15 characters long." } \ No newline at end of file diff --git a/data/lang/fr.json b/data/lang/fr.json index 8677160..53bc011 100644 --- a/data/lang/fr.json +++ b/data/lang/fr.json @@ -15,5 +15,6 @@ "error": "Erreur", "suggestion_sent_success": "Votre message a été envoyé avec succès !", "suggestion_send_error": "Erreur lors de l'envoi du message. Essayez à nouveau plus tard.", - "email_credentials_error": "Identifiants de messagerie non configurés. Veuillez définir votre email et mot de passe dans le fichier .env." + "email_credentials_error": "Identifiants de messagerie non ou mal configurés. Veuillez définir votre email et mot de passe dans le fichier .env.", + "suggestion_too_short": "Le message doit contenir au moins 15 caractères." } \ No newline at end of file diff --git a/data/others/defaults_settings.json b/data/others/defaults_settings.json index 639890e..f963074 100644 --- a/data/others/defaults_settings.json +++ b/data/others/defaults_settings.json @@ -2,5 +2,5 @@ "theme": "dark", "lang": "fr", "window_size": {"width": 1000, "height": 600}, - "maximize": true + "maximized": true } \ No newline at end of file diff --git a/main.py b/main.py index 9a881ec..e6605b9 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,20 @@ import sys +import app.utils.paths as paths from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon from app.ui.main_window import MainWindow +from app.core.main_manager import MainManager +def main() -> int: -def main(): - app = QApplication(sys.argv) + main_manager: MainManager = MainManager.get_instance() + theme_manager = main_manager.get_theme_manager() + settings_manager = main_manager.get_settings_manager() - window = MainWindow() + app: QApplication = QApplication(sys.argv) + app.setStyleSheet(theme_manager.get_sheet()) + app.setApplicationName(settings_manager.get_config("app_name")) + app.setWindowIcon(QIcon(paths.get_asset_path("icon"))) + window: MainWindow = MainWindow() window.show() return app.exec()