from PyQt6.QtCore import QSettings 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: ObserverManager, theme_manager: ThemeManager) -> None: self.observer_manager: ObserverManager = observer_manager self.theme_manager: ThemeManager = theme_manager # Hardcoded fallback settings in case files are missing self.fallback_settings: Dict[str, Any] = { "theme": "dark", "lang": "en", "window_size": {"width": 1000, "height": 600}, "maximized": False } self.fallback_config: Dict[str, Any] = { "app_name": "Application", "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() # 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) -> 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[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: """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) -> None: """Set the window maximized state""" try: self.settings.setValue("maximized", maximized) except Exception as e: logger.error(f"Error setting maximized state: {e}")