// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include namespace dragonx { namespace ui { namespace schema { /** * @brief Manages bundled and user-installed skins (unified TOML files) * * Responsibilities: * - Enumerate bundled skins from res/themes/ directory (any .toml except ui.toml) * - Enumerate user themes from ~/.config/ObsidianDragon/themes//theme.toml * - Import / remove user skins * - Validate skin TOML structure before import * - Track active skin ID in settings */ class SkinManager { public: /** * @brief Metadata about an available skin */ struct SkinInfo { std::string id; ///< Unique identifier (folder name or filename stem) std::string name; ///< Display name from theme.name std::string author; ///< Author from theme.author std::string path; ///< Full filesystem path to the TOML file std::string directory; ///< Folder containing theme.toml (empty for bundled flat files) std::string backgroundImagePath; ///< Resolved path to background image override (empty = use default) std::string logoPath; ///< Resolved path to logo image override (empty = use default) bool dark = true; ///< Dark mode flag from theme.dark bool bundled = true; ///< true = shipped with app, false = user-installed bool valid = true; ///< true if theme.toml passed validation std::string validationError; ///< Error message if !valid }; /** * @brief Result of a skin validation */ struct ValidationResult { bool valid = false; std::string error; ///< Error message if !valid }; /** * @brief Get the singleton instance */ static SkinManager& instance(); /** * @brief Scan for available skins (bundled + user) * * Re-scans both the bundled res/ directory and the user skins directory. * Call this on startup and after import/remove operations. */ void refresh(); /** * @brief Get the list of available skins * @return Sorted list: bundled skins first (with "dragonx" at top), then user skins */ const std::vector& available() const { return skins_; } /** * @brief Find a skin by ID * @return Pointer to SkinInfo, or nullptr if not found */ const SkinInfo* findById(const std::string& id) const; /** * @brief Validate a skin TOML file * @param path Path to the TOML file * @return Validation result with error message if invalid * * Validation rules: * 1. File must be valid TOML * 2. Must contain [theme] table * 3. theme.name must be a non-empty string * 4. theme.palette must be a table with at least --primary and --background * 5. If [globals] exists, it must be a table */ static ValidationResult validateSkinFile(const std::string& path); /** * @brief Import a skin file into the user skins directory * @param sourcePath Path to the source TOML file * @return true if imported successfully * * Validates the file first. Copies to user skins directory. * Calls refresh() on success. */ bool importSkin(const std::string& sourcePath); /** * @brief Remove a user-installed skin * @param id Skin ID to remove * @return true if removed successfully * * Cannot remove bundled skins. Calls refresh() on success. */ bool removeSkin(const std::string& id); /** * @brief Apply a skin by ID * @param id Skin ID to activate * @return true if skin was found and loaded * * Loads the skin file into UISchema and applies ImGui colors. */ bool setActiveSkin(const std::string& id); /** * @brief Get the currently active skin ID */ const std::string& activeSkinId() const { return activeSkinId_; } /** * @brief Get the bundled skins directory path */ static std::string getBundledSkinsDirectory(); /** * @brief Get the user skins directory path */ static std::string getUserSkinsDirectory(); /** * @brief Set callback invoked after skin changes (for image reloading) * @param cb Callback receiving backgroundImagePath and logoPath (empty = use default) */ void setImageReloadCallback(std::function cb) { imageReloadCb_ = std::move(cb); } /** * @brief Re-resolve image paths from UISchema and trigger reload callback * * Called by UISchema hot-reload when [theme.images] values change. * Updates the active SkinInfo's image paths and fires imageReloadCb_. * * @param skinId Active skin ID to update * @param tomlPath Path to the TOML file whose images section changed */ void resolveAndReloadImages(const std::string& skinId, const std::string& tomlPath); /** * @brief Enable/disable gradient background mode * * When enabled, theme backgrounds are replaced with their gradient * variants (e.g. "gradient_drgx_bg.png" instead of "drgx_bg.png"). * Falls back to dark_gradient.png or light_gradient.png when no * theme-specific gradient exists. */ void setGradientMode(bool enabled); bool isGradientMode() const { return gradientMode_; } private: SkinManager() = default; ~SkinManager() = default; SkinManager(const SkinManager&) = delete; SkinManager& operator=(const SkinManager&) = delete; void scanDirectory(const std::string& dir, bool bundled); /// Resolve the image directory for a given skin std::filesystem::path resolveImgDir(const SkinInfo* skin) const; /// Given an original bg relative path and img dir, resolve the gradient variant std::string resolveGradientBg(const std::string& bgFilename, const std::filesystem::path& imgDir, bool isDark) const; /// Common helper: resolve bg (optionally gradient) and logo, fire callback void resolveAndFireCallback(SkinInfo* skin, const std::filesystem::path& imgDir); std::vector skins_; std::string activeSkinId_ = "dragonx"; bool gradientMode_ = false; std::function imageReloadCb_; }; } // namespace schema } // namespace ui } // namespace dragonx