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_exclude=[],
runtime_tmpdir=None,
console=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
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 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)

View File

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

View File

@ -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)
# 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
}
with open(paths.resource_path("config.json"), 'r', encoding='utf-8') as f:
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": ""
}
self.settings = QSettings(path.join(paths.get_user_data_dir(self.get_config("app_name")), self.get_config("app_name") + ".ini"), QSettings.Format.IniFormat)
# Load default settings
self.default_settings: Dict[str, Any] = self._load_default_settings()
# Load config
self.config: Dict[str, Any] = self._load_config()
self.theme_manager.set_theme(self.get_theme())
# Initialize QSettings with error handling
self.settings: QSettings = self._initialize_qsettings()
# 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"])
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)
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}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."
}

View File

@ -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."
}

View File

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

15
main.py
View File

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