alert_manager and improvements
This commit is contained in:
parent
508ba8461a
commit
dd43bf5abf
@ -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
20
app/core/alert_manager.py
Normal 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)
|
@ -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)
|
||||
|
@ -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
|
@ -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()
|
||||
|
||||
# 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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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):
|
||||
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}")
|
@ -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:
|
||||
|
@ -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())
|
@ -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"))
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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)
|
||||
|
@ -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."
|
||||
}
|
@ -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."
|
||||
}
|
@ -2,5 +2,5 @@
|
||||
"theme": "dark",
|
||||
"lang": "fr",
|
||||
"window_size": {"width": 1000, "height": 600},
|
||||
"maximize": true
|
||||
"maximized": true
|
||||
}
|
15
main.py
15
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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user