alert_manager and improvements

This commit is contained in:
Louis Mazin 2025-08-19 18:03:29 +02:00
parent 508ba8461a
commit dd43bf5abf
14 changed files with 358 additions and 161 deletions

View File

@ -59,7 +59,7 @@ exe = EXE(
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=True, console=False,
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,
target_arch=None, target_arch=None,

20
app/core/alert_manager.py Normal file
View File

@ -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)

View File

@ -2,6 +2,7 @@ from json import JSONDecodeError, load
from os import listdir, path from os import listdir, path
from typing import Dict from typing import Dict
import app.utils.paths as paths import app.utils.paths as paths
from app.core.settings_manager import SettingsManager
class LanguageManager: class LanguageManager:
""" """
@ -9,9 +10,9 @@ class LanguageManager:
Charge les fichiers JSON de langue dans data/lang/, permet Charge les fichiers JSON de langue dans data/lang/, permet
de changer la langue courante, et récupérer des textes traduits. 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.translations: Dict[str, Dict[str, str]] = {}
self.settings_manager = settings_manager self.settings_manager: SettingsManager = settings_manager
self.load_all_translations() self.load_all_translations()
@ -19,12 +20,12 @@ class LanguageManager:
""" """
Charge tous les fichiers JSON dans data/lang/ comme dictionnaires. 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): for filename in listdir(lang_dir):
if filename.endswith(".json"): if filename.endswith(".json"):
lang_code = filename[:-5] # strip .json lang_code: str = filename[:-5] # strip .json
file_path = path.join(lang_dir, filename) file_path: str = path.join(lang_dir, filename)
try: try:
with open(file_path, "r", encoding="utf-8") as f: with open(file_path, "r", encoding="utf-8") as f:
self.translations[lang_code] = load(f) self.translations[lang_code] = load(f)

View File

@ -2,31 +2,40 @@ from app.core.observer_manager import ObserverManager, NotificationType
from app.core.language_manager import LanguageManager from app.core.language_manager import LanguageManager
from app.core.theme_manager import ThemeManager from app.core.theme_manager import ThemeManager
from app.core.settings_manager import SettingsManager from app.core.settings_manager import SettingsManager
from app.core.alert_manager import AlertManager
from typing import Optional
class MainManager: class MainManager:
_instance = None _instance: Optional['MainManager'] = None
def __init__(self):
def __init__(self) -> None:
if MainManager._instance is not None: if MainManager._instance is not None:
raise Exception("This class is a singleton!") raise Exception("This class is a singleton!")
else: else:
MainManager._instance = self MainManager._instance = self
self.observer_manager = ObserverManager() self.observer_manager: ObserverManager = ObserverManager()
self.theme_manager = ThemeManager() self.theme_manager: ThemeManager = ThemeManager()
self.settings_manager = SettingsManager(self.observer_manager,self.theme_manager) self.settings_manager: SettingsManager = SettingsManager(self.observer_manager, self.theme_manager)
self.language_manager = LanguageManager(self.settings_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 @classmethod
def get_instance(cls): def get_instance(cls) -> 'MainManager':
if cls._instance is None: if cls._instance is None:
cls._instance = cls() cls._instance = cls()
return cls._instance 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

View File

@ -1,65 +1,210 @@
from PyQt6.QtCore import QSettings 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 from os import path
import app.utils.paths as paths import app.utils.paths as paths
import json import json
import logging
from typing import Dict, Any, Union
# Configure logging
logger: logging.Logger = logging.getLogger(__name__)
class SettingsManager: class SettingsManager:
""" """
Gestion des paramètres utilisateurs avec sauvegarde persistante via QSettings. Gestion des paramètres utilisateurs avec sauvegarde persistante via QSettings.
Notifie les changements via ObserverManager. Notifie les changements via ObserverManager.
""" """
def __init__(self, observer_manager, theme_manager): def __init__(self, observer_manager: ObserverManager, theme_manager: ThemeManager) -> None:
self.observer_manager = observer_manager self.observer_manager: ObserverManager = observer_manager
self.theme_manager = theme_manager self.theme_manager: ThemeManager = theme_manager
# Load default settings from JSON file # Hardcoded fallback settings in case files are missing
defaults_path = path.join(paths.get_data_dir(), "others", "defaults_settings.json") self.fallback_settings: Dict[str, Any] = {
with open(defaults_path, 'r', encoding='utf-8') as f: "theme": "dark",
self.default_settings = json.load(f) "lang": "en",
"window_size": {"width": 1000, "height": 600},
with open(paths.resource_path("config.json"), 'r', encoding='utf-8') as f: "maximized": False
self.config = json.load(f) }
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 # Config
def get_config(self, key: str): def get_config(self, key: str) -> Any:
return self.config.get(key, None) """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 # Theme
def get_theme(self) -> str: 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: def set_theme(self, mode: str) -> None:
if mode != self.get_theme(): """Set the application theme"""
self.settings.setValue("theme", mode) try:
self.theme_manager.set_theme(mode) if mode != self.get_theme():
self.observer_manager.notify(NotificationType.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 # Language
def get_language(self) -> str: 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: def set_language(self, lang_code: str) -> None:
if lang_code != self.get_language(): """Set the application language"""
self.settings.setValue("lang", lang_code) try:
self.observer_manager.notify(NotificationType.LANGUAGE) 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 # Window size and maximized
def get_window_size(self) -> dict: def get_window_size(self) -> Dict[str, int]:
return self.settings.value("window_size", self.default_settings.get("window_size")) """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: def set_window_size(self, width: int, height: int) -> None:
current_size = self.get_window_size() """Set the window size"""
if current_size["width"] != width or current_size["height"] != height: try:
new_size = {"width": width, "height": height} # Validate input values
self.settings.setValue("window_size", new_size) 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: 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): def set_maximized(self, maximized: bool) -> None:
self.settings.setValue("maximized", maximized) """Set the window maximized state"""
try:
self.settings.setValue("maximized", maximized)
except Exception as e:
logger.error(f"Error setting maximized state: {e}")

View File

@ -1,32 +1,35 @@
import app.utils.paths as paths import app.utils.paths as paths
import os, json import os, json
from typing import List, Dict, Any, Optional
class Theme: class Theme:
def __init__(self, name: str, colors: dict): def __init__(self, name: str, colors: Dict[str, str]) -> None:
self.name = name self.name: str = name
self.colors = colors self.colors: Dict[str, str] = colors
def get_color(self, element: str) -> str: def get_color(self, element: str) -> str:
return self.colors.get(element, "#FFFFFF") return self.colors.get(element, "#FFFFFF")
class ThemeManager: class ThemeManager:
def __init__(self): def __init__(self) -> None:
theme_path = os.path.join(paths.get_data_dir(), "themes") theme_path: str = os.path.join(paths.get_data_dir(), "themes")
self.themes = [] self.themes: List[Theme] = []
for theme_file in os.listdir(theme_path): for theme_file in os.listdir(theme_path):
if theme_file.endswith(".json"): if theme_file.endswith(".json"):
with open(os.path.join(theme_path, theme_file), 'r', encoding='utf-8') as f: with open(os.path.join(theme_path, theme_file), 'r', encoding='utf-8') as f:
theme_data = json.load(f) theme_data: Dict[str, Any] = json.load(f)
theme = Theme(theme_data["theme_name"], theme_data["colors"]) theme: Theme = Theme(theme_data["theme_name"], theme_data["colors"])
self.themes.append(theme) self.themes.append(theme)
self.current_theme = self.themes[0] self.current_theme: Theme = self.themes[0]
def set_theme(self, theme: str) -> None: def set_theme(self, theme: str) -> None:
if theme != self.current_theme.name: 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 return self.current_theme
def get_sheet(self) -> str: def get_sheet(self) -> str:

View File

@ -1,17 +1,18 @@
from PyQt6.QtWidgets import QApplication, QMainWindow from PyQt6.QtWidgets import QApplication, QMainWindow
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon, QResizeEvent, QCloseEvent
from PyQt6.QtCore import QSize from PyQt6.QtCore import QSize, QEvent
from app.core.main_manager import MainManager, NotificationType from app.core.main_manager import MainManager, NotificationType
from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide from app.ui.widgets.tabs_widget import TabsWidget, MenuDirection, ButtonPosition, BorderSide
from app.ui.windows.settings_window import SettingsWindow from app.ui.windows.settings_window import SettingsWindow
from app.ui.windows.suggestion_window import SuggestionWindow from app.ui.windows.suggestion_window import SuggestionWindow
import app.utils.paths as paths import app.utils.paths as paths
from typing import Optional
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self) -> None:
super().__init__() 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.language_manager = self.main_manager.get_language_manager()
self.theme_manager = self.main_manager.get_theme_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 = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.THEME, self.update_theme) 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() # UI elements
size = app.primaryScreen().size() 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.settings_manager.minScreenSize = min(size.height(),size.width())
self.setMinimumSize(600, 400) 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.setup_ui()
self.apply_saved_window_state() 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""" """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.current_size = QSize(window_size["width"], window_size["height"])
self.previous_size = QSize(window_size["width"], window_size["height"]) self.previous_size = QSize(window_size["width"], window_size["height"])
self.resize(self.current_size) self.resize(self.current_size)
@ -43,7 +47,7 @@ class MainWindow(QMainWindow):
self.is_maximizing = True self.is_maximizing = True
self.showMaximized() self.showMaximized()
def changeEvent(self, event): def changeEvent(self, event: QEvent) -> None:
"""Handle window state changes""" """Handle window state changes"""
super().changeEvent(event) super().changeEvent(event)
if event.type() == event.Type.WindowStateChange: if event.type() == event.Type.WindowStateChange:
@ -56,13 +60,13 @@ class MainWindow(QMainWindow):
self.current_size = self.previous_size self.current_size = self.previous_size
self.settings_manager.set_maximized(self.isMaximized()) 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 # Ne pas sauvegarder la taille si on est en train de maximiser
if not self.isMaximized() and not self.is_maximizing: if not self.isMaximized() and not self.is_maximizing:
self.previous_size = self.current_size self.previous_size = self.current_size
self.current_size = self.size() self.current_size = self.size()
def closeEvent(self, event): def closeEvent(self, event: QCloseEvent) -> None:
"""Handle application close event""" """Handle application close event"""
super().closeEvent(event) super().closeEvent(event)
# si la difference de taille est plus grande que 10 pixels, enregistrer previoussize # si la difference de taille est plus grande que 10 pixels, enregistrer previoussize
@ -73,7 +77,7 @@ class MainWindow(QMainWindow):
self.current_size.height() 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.side_menu = TabsWidget(self,MenuDirection.HORIZONTAL,70,None,10,1,BorderSide.TOP)
self.settings_window = SettingsWindow(self) self.settings_window = SettingsWindow(self)
@ -84,5 +88,5 @@ class MainWindow(QMainWindow):
self.setCentralWidget(self.side_menu) self.setCentralWidget(self.side_menu)
def update_theme(self): def update_theme(self) -> None:
self.setStyleSheet(self.theme_manager.get_sheet()) self.setStyleSheet(self.theme_manager.get_sheet())

View File

@ -1,21 +1,30 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QLabel, QHBoxLayout from PyQt6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QLabel, QHBoxLayout
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from app.core.main_manager import MainManager, NotificationType from app.core.main_manager import MainManager, NotificationType
from typing import Optional
class SettingsWindow(QWidget): class SettingsWindow(QWidget):
def __init__(self, parent=None): def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent) 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.language_manager = self.main_manager.get_language_manager()
self.settings_manager = self.main_manager.get_settings_manager() self.settings_manager = self.main_manager.get_settings_manager()
self.observer_manager = self.main_manager.get_observer_manager() self.observer_manager = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language) 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() self.setup_ui()
def setup_ui(self): def setup_ui(self) -> None:
layout = QVBoxLayout(self) layout: QVBoxLayout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.setSpacing(20) layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20) layout.setContentsMargins(20, 20, 20, 20)
@ -32,7 +41,6 @@ class SettingsWindow(QWidget):
layout.addLayout(self.language_layout) layout.addLayout(self.language_layout)
# Paramètres de thème # Paramètres de thème
self.theme_layout = QHBoxLayout() self.theme_layout = QHBoxLayout()
self.themeLabel = QLabel(self.language_manager.get_text("theme"), self) self.themeLabel = QLabel(self.language_manager.get_text("theme"), self)
@ -46,8 +54,8 @@ class SettingsWindow(QWidget):
layout.addStretch() layout.addStretch()
def createLanguageSelector(self): def createLanguageSelector(self) -> QComboBox:
combo = QComboBox() combo: QComboBox = QComboBox()
# Ajouter toutes les langues disponibles # Ajouter toutes les langues disponibles
for langCode, langData in self.language_manager.translations.items(): for langCode, langData in self.language_manager.translations.items():
combo.addItem(langData["lang_name"], langCode) combo.addItem(langData["lang_name"], langCode)
@ -59,8 +67,8 @@ class SettingsWindow(QWidget):
return combo return combo
def createThemeSelector(self): def createThemeSelector(self) -> QComboBox:
combo = QComboBox() combo: QComboBox = QComboBox()
# Ajouter les options de thème # Ajouter les options de thème
combo.addItem(self.language_manager.get_text("light_theme"), "light") combo.addItem(self.language_manager.get_text("light_theme"), "light")
@ -73,14 +81,14 @@ class SettingsWindow(QWidget):
return combo return combo
def change_language(self, index): def change_language(self, index: int) -> None:
self.settings_manager.set_language(self.languageCombo.itemData(index)) self.settings_manager.set_language(self.languageCombo.itemData(index))
def change_theme(self, index): def change_theme(self, index: int) -> None:
theme = self.themeCombo.itemData(index) theme: str = self.themeCombo.itemData(index)
self.settings_manager.set_theme(theme) 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.languageLabel.setText(self.language_manager.get_text("language"))
self.themeLabel.setText(self.language_manager.get_text("theme")) self.themeLabel.setText(self.language_manager.get_text("theme"))

View File

@ -1,12 +1,11 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLabel, QMessageBox, QHBoxLayout from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QPushButton, QLabel, QMessageBox, QHBoxLayout
from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtCore import Qt, QThread, pyqtSignal
import time import smtplib, os
import smtplib
import os
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv from dotenv import load_dotenv
from app.core.main_manager import MainManager, NotificationType from app.core.main_manager import MainManager, NotificationType
from typing import Optional
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
@ -15,25 +14,25 @@ class EmailSender(QThread):
success = pyqtSignal() success = pyqtSignal()
error = pyqtSignal(str) error = pyqtSignal(str)
def __init__(self, subject, message): def __init__(self, subject: str, message: str) -> None:
super().__init__() super().__init__()
self.subject = subject self.subject: str = subject
self.message = message self.message: str = message
def run(self): def run(self) -> None:
try: try:
# Get email configuration from environment variables # Get email configuration from environment variables
email = os.getenv('EMAIL_ADDRESS') email: Optional[str] = os.getenv('EMAIL_ADDRESS')
password = os.getenv('EMAIL_PASSWORD') password: Optional[str] = os.getenv('EMAIL_PASSWORD')
smtp_server = os.getenv('EMAIL_SMTP_SERVER', 'smtp.gmail.com') smtp_server: str = os.getenv('EMAIL_SMTP_SERVER', 'smtp.gmail.com')
smtp_port = int(os.getenv('EMAIL_SMTP_PORT', '587')) smtp_port: int = int(os.getenv('EMAIL_SMTP_PORT', '587'))
if not email or not password: if not email or not password:
self.error.emit("password") self.error.emit("password")
return return
# Create message # Create message
msg = MIMEMultipart() msg: MIMEMultipart = MIMEMultipart()
msg['From'] = email msg['From'] = email
msg['To'] = email msg['To'] = email
msg['Subject'] = self.subject msg['Subject'] = self.subject
@ -42,64 +41,69 @@ class EmailSender(QThread):
msg.attach(MIMEText(self.message, 'plain')) msg.attach(MIMEText(self.message, 'plain'))
# Create SMTP session # Create SMTP session
server = smtplib.SMTP(smtp_server, smtp_port) server: smtplib.SMTP = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # Enable TLS encryption server.starttls() # Enable TLS encryption
# Login with app password # Login with app password
server.login(email, password) server.login(email, password)
# Send email # Send email
text = msg.as_string() text: str = msg.as_string()
server.sendmail(email, email, text) server.sendmail(email, email, text)
server.quit() server.quit()
self.success.emit() self.success.emit()
except smtplib.SMTPAuthenticationError:
self.error.emit("email_credentials_error")
except Exception: except Exception:
self.error.emit("") self.error.emit("suggestion_send_error")
class SuggestionWindow(QWidget): class SuggestionWindow(QWidget):
def __init__(self, parent=None): def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent) 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.language_manager = self.main_manager.get_language_manager()
self.settings_manager = self.main_manager.get_settings_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 = self.main_manager.get_observer_manager()
self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language) self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_language)
self.email_sender: Optional[EmailSender] = None
self.setup_ui() self.setup_ui()
def setup_ui(self): def setup_ui(self) -> None:
layout = QVBoxLayout(self) layout: QVBoxLayout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.setSpacing(20) layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20) layout.setContentsMargins(20, 20, 20, 20)
# Title # 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;") self.title_label.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;")
layout.addWidget(self.title_label) layout.addWidget(self.title_label)
# Text area for suggestion # 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.setPlaceholderText(self.language_manager.get_text("suggestion_placeholder"))
self.text_edit.setMinimumHeight(200) self.text_edit.setMinimumHeight(200)
layout.addWidget(self.text_edit) layout.addWidget(self.text_edit)
# Button layout # Button layout
button_layout = QHBoxLayout() button_layout: QHBoxLayout = QHBoxLayout()
button_layout.addStretch() button_layout.addStretch()
# Send button # 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) self.send_button.clicked.connect(self.send_suggestion)
button_layout.addWidget(self.send_button) button_layout.addWidget(self.send_button)
layout.addLayout(button_layout) layout.addLayout(button_layout)
layout.addStretch() layout.addStretch()
def send_suggestion(self): def send_suggestion(self) -> None:
message = self.text_edit.toPlainText().strip() message: str = self.text_edit.toPlainText().strip()
if not message: if len(message)<15:
self.alert_manager.show_error("suggestion_too_short")
return return
# Disable send button during sending # Disable send button during sending
@ -107,7 +111,7 @@ class SuggestionWindow(QWidget):
self.send_button.setText(self.language_manager.get_text("sending")) self.send_button.setText(self.language_manager.get_text("sending"))
# Create subject with app name # 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 # Create and start email sender thread
self.email_sender = EmailSender(subject, message) 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.error.connect(self.on_email_error)
self.email_sender.start() self.email_sender.start()
def on_email_sent(self): def on_email_sent(self) -> None:
self.send_button.setEnabled(True) self.send_button.setEnabled(True)
self.send_button.setText(self.language_manager.get_text("send_suggestion")) self.send_button.setText(self.language_manager.get_text("send_suggestion"))
self.alert_manager.show_success("suggestion_sent_success")
QMessageBox.information(self,
self.language_manager.get_text("success"),
self.language_manager.get_text("suggestion_sent_success"))
self.text_edit.clear() 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.setEnabled(True)
self.send_button.setText(self.language_manager.get_text("send_suggestion")) self.send_button.setText(self.language_manager.get_text("send_suggestion"))
if error == "password": self.alert_manager.show_error(error)
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)
def update_language(self): def update_language(self) -> None:
self.title_label.setText(self.language_manager.get_text("suggestion_text")) self.title_label.setText(self.language_manager.get_text("suggestion_text"))
self.text_edit.setPlaceholderText(self.language_manager.get_text("suggestion_placeholder")) 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"))

View File

@ -2,46 +2,47 @@ from os import path, getenv, mkdir
import sys import sys
from platform import system from platform import system
from pathlib import Path 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. Get absolute path to resource, works for dev and for PyInstaller bundle.
PyInstaller stores bundled files in _MEIPASS folder. PyInstaller stores bundled files in _MEIPASS folder.
""" """
try: try:
base_path = Path(sys._MEIPASS) # PyInstaller temp folder base_path: Path = Path(sys._MEIPASS) # PyInstaller temp folder
except AttributeError: 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) return path.join(base_path, relative_path)
def get_data_dir() -> Path: def get_data_dir() -> str:
return resource_path("data") return resource_path("data")
def get_lang_path() -> Path: def get_lang_path() -> str:
return path.join(get_data_dir(), "lang") 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") 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") return path.join(get_data_dir(), "assets", f"{asset}.svg")
def get_user_data_dir(app_name: str) -> Path: def get_user_data_dir(app_name: str) -> str:
home = Path.home() home: Path = Path.home()
os = system() os: str = system()
if os == "Windows": if os == "Windows":
appdata = getenv('APPDATA') appdata: Optional[str] = getenv('APPDATA')
if appdata: if appdata:
user_data_path = path.join(Path(appdata), app_name) user_data_path: str = path.join(Path(appdata), app_name)
else: 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": 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: 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): if not path.exists(user_data_path):
mkdir(user_data_path) mkdir(user_data_path)

View File

@ -15,5 +15,6 @@
"error": "Error", "error": "Error",
"suggestion_sent_success": "Your message has been sent successfully!", "suggestion_sent_success": "Your message has been sent successfully!",
"suggestion_send_error": "Error sending message. Try again later.", "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."
} }

View File

@ -15,5 +15,6 @@
"error": "Erreur", "error": "Erreur",
"suggestion_sent_success": "Votre message a été envoyé avec succès !", "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.", "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."
} }

View File

@ -2,5 +2,5 @@
"theme": "dark", "theme": "dark",
"lang": "fr", "lang": "fr",
"window_size": {"width": 1000, "height": 600}, "window_size": {"width": 1000, "height": 600},
"maximize": true "maximized": true
} }

15
main.py
View File

@ -1,11 +1,20 @@
import sys import sys
import app.utils.paths as paths
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QIcon
from app.ui.main_window import MainWindow from app.ui.main_window import MainWindow
from app.core.main_manager import MainManager
def main() -> int:
def main(): main_manager: MainManager = MainManager.get_instance()
app = QApplication(sys.argv) 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() window.show()
return app.exec() return app.exec()