- Add startup binary version checking for dragonxd/xmrig - Display daemon version in UI - Add idle mining thread count adjustment - Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard - Add paste button to import private key dialog with address validation - Add z-address generation UI feedback (loading indicator) - Add option to delete blockchain data while preserving wallet.dat - Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus) - Fix Windows RPC auth: trim \r from config values, add .cookie fallback - Fix connection status message during block index loading - Improve application shutdown to prevent lingering background process
806 lines
28 KiB
C++
806 lines
28 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "skin_manager.h"
|
|
#include "ui_schema.h"
|
|
#include "../../util/platform.h"
|
|
#include "../../resources/embedded_resources.h"
|
|
#include "../theme.h"
|
|
#include "../material/color_theme.h"
|
|
#include "../effects/theme_effects.h"
|
|
#include "../effects/imgui_acrylic.h"
|
|
|
|
#include <toml++/toml.hpp>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include "../../util/logger.h"
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace schema {
|
|
|
|
// ============================================================================
|
|
// Singleton
|
|
// ============================================================================
|
|
|
|
SkinManager& SkinManager::instance() {
|
|
static SkinManager s;
|
|
return s;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Directory helpers
|
|
// ============================================================================
|
|
|
|
std::string SkinManager::getBundledSkinsDirectory() {
|
|
// Bundled skins live in res/themes/ next to the executable
|
|
fs::path exe_dir = util::getExecutableDirectory();
|
|
fs::path themes_dir = exe_dir / "res" / "themes";
|
|
|
|
if (fs::exists(themes_dir)) {
|
|
// Update any stale overlay themes from embedded versions
|
|
int updated = resources::updateBundledThemes(themes_dir.string());
|
|
if (updated > 0)
|
|
DEBUG_LOGF("[SkinManager] Updated %d stale theme(s) in %s\n",
|
|
updated, themes_dir.string().c_str());
|
|
return themes_dir.string();
|
|
}
|
|
|
|
// Fallback: current working directory
|
|
themes_dir = fs::current_path() / "res" / "themes";
|
|
if (fs::exists(themes_dir)) {
|
|
return themes_dir.string();
|
|
}
|
|
|
|
// No on-disk themes dir found (single-file Windows distribution).
|
|
// Extract embedded overlay themes to the config directory.
|
|
fs::path configDir = util::Platform::getObsidianDragonDir();
|
|
fs::path extractedDir = configDir / "bundled-themes";
|
|
int extracted = resources::extractBundledThemes(extractedDir.string());
|
|
if (extracted > 0) {
|
|
DEBUG_LOGF("[SkinManager] Extracted %d embedded bundled themes to %s\n",
|
|
extracted, extractedDir.string().c_str());
|
|
}
|
|
if (fs::exists(extractedDir)) {
|
|
return extractedDir.string();
|
|
}
|
|
|
|
return (exe_dir / "res" / "themes").string();
|
|
}
|
|
|
|
std::string SkinManager::getUserSkinsDirectory() {
|
|
// User themes in ObsidianDragon config directory — folder-based
|
|
fs::path configDir = util::Platform::getObsidianDragonDir();
|
|
return (configDir / "themes").string();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Scan for skins
|
|
// ============================================================================
|
|
|
|
void SkinManager::scanDirectory(const std::string& dir, bool bundled) {
|
|
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
|
DEBUG_LOGF("[SkinManager] Directory does not exist: %s\n", dir.c_str());
|
|
return;
|
|
}
|
|
|
|
DEBUG_LOGF("[SkinManager] Scanning %s directory: %s\n", bundled ? "bundled" : "user", dir.c_str());
|
|
|
|
if (bundled) {
|
|
// Bundled skins: all .toml files in res/themes/ except ui.toml
|
|
// (ui.toml is the base theme and is always loaded as "dragonx")
|
|
for (const auto& entry : fs::directory_iterator(dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
|
|
fs::path p = entry.path();
|
|
if (p.extension() != ".toml") continue;
|
|
|
|
std::string stem = p.stem().string();
|
|
|
|
// Skip ui.toml - it's the base theme handled separately as "dragonx"
|
|
if (stem == "ui") continue;
|
|
|
|
// Try to parse and extract metadata
|
|
toml::table root;
|
|
try {
|
|
root = toml::parse_file(p.string());
|
|
} catch (...) {
|
|
DEBUG_LOGF("[SkinManager] Skipping '%s': invalid TOML\n", p.filename().string().c_str());
|
|
continue;
|
|
}
|
|
|
|
auto* theme = root["theme"].as_table();
|
|
if (!theme) {
|
|
DEBUG_LOGF("[SkinManager] Skipping '%s': no [theme] section\n", p.filename().string().c_str());
|
|
continue;
|
|
}
|
|
|
|
SkinInfo info;
|
|
info.path = p.string();
|
|
info.bundled = true;
|
|
|
|
// ID = filename stem (e.g. "dark" from dark.toml)
|
|
info.id = stem;
|
|
|
|
if (auto name = (*theme)["name"].value<std::string>()) {
|
|
info.name = *name;
|
|
} else {
|
|
info.name = info.id;
|
|
}
|
|
|
|
if (auto author = (*theme)["author"].value<std::string>()) {
|
|
info.author = *author;
|
|
}
|
|
|
|
if (auto dark = (*theme)["dark"].value<bool>()) {
|
|
info.dark = *dark;
|
|
}
|
|
|
|
// Resolve image paths from theme.images (bundled: res/img/)
|
|
fs::path imgDir = p.parent_path().parent_path() / "img";
|
|
std::string bgFilename;
|
|
std::string logoFilename;
|
|
|
|
if (auto* images = (*theme)["images"].as_table()) {
|
|
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
|
bgFilename = *bg;
|
|
}
|
|
if (auto logo = (*images)["logo"].value<std::string>()) {
|
|
logoFilename = *logo;
|
|
}
|
|
}
|
|
|
|
if (!bgFilename.empty()) {
|
|
fs::path bgPath = imgDir / bgFilename;
|
|
if (fs::exists(bgPath)) {
|
|
info.backgroundImagePath = bgPath.string();
|
|
}
|
|
}
|
|
|
|
if (!logoFilename.empty()) {
|
|
fs::path logoImgPath = imgDir / logoFilename;
|
|
if (fs::exists(logoImgPath)) {
|
|
info.logoPath = logoImgPath.string();
|
|
}
|
|
}
|
|
|
|
skins_.push_back(std::move(info));
|
|
}
|
|
} else {
|
|
// User themes: each subfolder must contain a theme.toml
|
|
for (const auto& entry : fs::directory_iterator(dir)) {
|
|
if (!entry.is_directory()) continue;
|
|
|
|
fs::path themeDir = entry.path();
|
|
fs::path themeToml = themeDir / "theme.toml";
|
|
|
|
if (!fs::exists(themeToml)) {
|
|
DEBUG_LOGF("[SkinManager] Skipping folder '%s': no theme.toml found\n",
|
|
themeDir.filename().string().c_str());
|
|
continue;
|
|
}
|
|
|
|
DEBUG_LOGF("[SkinManager] Found theme folder: %s (theme.toml exists)\n", themeDir.filename().string().c_str());
|
|
|
|
// Validate the theme file
|
|
auto validation = validateSkinFile(themeToml.string());
|
|
|
|
// Parse metadata even from invalid themes (so they show in the list)
|
|
toml::table root;
|
|
try {
|
|
root = toml::parse_file(themeToml.string());
|
|
} catch (...) {
|
|
// Still add as invalid
|
|
SkinInfo info;
|
|
info.id = themeDir.filename().string();
|
|
info.name = info.id;
|
|
info.path = themeToml.string();
|
|
info.directory = themeDir.string();
|
|
info.bundled = false;
|
|
info.valid = false;
|
|
info.validationError = "Invalid TOML";
|
|
skins_.push_back(std::move(info));
|
|
continue;
|
|
}
|
|
|
|
SkinInfo info;
|
|
info.id = themeDir.filename().string();
|
|
info.path = themeToml.string();
|
|
info.directory = themeDir.string();
|
|
info.bundled = false;
|
|
info.valid = validation.valid;
|
|
info.validationError = validation.error;
|
|
|
|
// Extract metadata from theme section
|
|
if (auto* theme = root["theme"].as_table()) {
|
|
|
|
if (auto name = (*theme)["name"].value<std::string>()) {
|
|
info.name = *name;
|
|
} else {
|
|
info.name = info.id;
|
|
}
|
|
|
|
if (auto author = (*theme)["author"].value<std::string>()) {
|
|
info.author = *author;
|
|
}
|
|
|
|
if (auto dark = (*theme)["dark"].value<bool>()) {
|
|
info.dark = *dark;
|
|
}
|
|
|
|
// Resolve image paths (from TOML)
|
|
fs::path imgDir = themeDir / "img";
|
|
std::string bgFilename;
|
|
std::string logoFilename;
|
|
|
|
if (auto* images = (*theme)["images"].as_table()) {
|
|
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
|
bgFilename = *bg;
|
|
}
|
|
if (auto logo = (*images)["logo"].value<std::string>()) {
|
|
logoFilename = *logo;
|
|
}
|
|
}
|
|
|
|
// Check if image files exist
|
|
if (!bgFilename.empty()) {
|
|
fs::path bgPath = imgDir / bgFilename;
|
|
if (fs::exists(bgPath)) {
|
|
info.backgroundImagePath = bgPath.string();
|
|
}
|
|
}
|
|
|
|
if (!logoFilename.empty()) {
|
|
fs::path logoImgPath = imgDir / logoFilename;
|
|
if (fs::exists(logoImgPath)) {
|
|
info.logoPath = logoImgPath.string();
|
|
}
|
|
}
|
|
} else {
|
|
info.name = info.id;
|
|
}
|
|
|
|
skins_.push_back(std::move(info));
|
|
}
|
|
|
|
// Also scan for loose .toml files (unified format with [theme.palette])
|
|
for (const auto& entry : fs::directory_iterator(dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
|
|
fs::path p = entry.path();
|
|
if (p.extension() != ".toml") continue;
|
|
|
|
toml::table root;
|
|
try {
|
|
root = toml::parse_file(p.string());
|
|
} catch (...) {
|
|
DEBUG_LOGF("[SkinManager] Skipping '%s': invalid TOML\n", p.filename().string().c_str());
|
|
continue;
|
|
}
|
|
|
|
SkinInfo info;
|
|
info.path = p.string();
|
|
info.id = p.stem().string();
|
|
info.bundled = false;
|
|
|
|
// Check for unified format ([theme] with [theme.palette])
|
|
auto* theme = root["theme"].as_table();
|
|
if (theme) {
|
|
if (auto name = (*theme)["name"].value<std::string>()) {
|
|
info.name = *name;
|
|
} else {
|
|
info.name = info.id;
|
|
}
|
|
if (auto author = (*theme)["author"].value<std::string>()) {
|
|
info.author = *author;
|
|
}
|
|
if (auto dark = (*theme)["dark"].value<bool>()) {
|
|
info.dark = *dark;
|
|
}
|
|
|
|
auto validation = validateSkinFile(p.string());
|
|
info.valid = validation.valid;
|
|
info.validationError = validation.error;
|
|
|
|
// Resolve image paths (look in same directory as the .toml file)
|
|
fs::path imgDir = p.parent_path();
|
|
std::string bgFilename;
|
|
std::string logoFilename;
|
|
|
|
if (auto* images = (*theme)["images"].as_table()) {
|
|
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
|
bgFilename = *bg;
|
|
}
|
|
if (auto logo = (*images)["logo"].value<std::string>()) {
|
|
logoFilename = *logo;
|
|
}
|
|
}
|
|
|
|
if (!bgFilename.empty()) {
|
|
fs::path bgPath = imgDir / bgFilename;
|
|
if (fs::exists(bgPath)) {
|
|
info.backgroundImagePath = bgPath.string();
|
|
}
|
|
}
|
|
if (!logoFilename.empty()) {
|
|
fs::path logoImgPath = imgDir / logoFilename;
|
|
if (fs::exists(logoImgPath)) {
|
|
info.logoPath = logoImgPath.string();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
DEBUG_LOGF("[SkinManager] Skipping '%s': unrecognized TOML format\n",
|
|
p.filename().string().c_str());
|
|
continue;
|
|
}
|
|
|
|
skins_.push_back(std::move(info));
|
|
}
|
|
}
|
|
}
|
|
|
|
void SkinManager::refresh() {
|
|
skins_.clear();
|
|
|
|
// Scan bundled skins (res/ directory)
|
|
scanDirectory(getBundledSkinsDirectory(), true);
|
|
|
|
// Scan user skins
|
|
std::string userDir = getUserSkinsDirectory();
|
|
if (fs::exists(userDir)) {
|
|
scanDirectory(userDir, false);
|
|
}
|
|
|
|
// Ensure the base "dragonx" theme always appears (it's ui.toml, the main theme).
|
|
// Other bundled themes are discovered automatically from res/themes/*.toml.
|
|
{
|
|
bool found = false;
|
|
for (const auto& s : skins_) {
|
|
if (s.id == "dragonx") { found = true; break; }
|
|
}
|
|
if (!found) {
|
|
SkinInfo info;
|
|
info.id = "dragonx";
|
|
info.name = "DragonX";
|
|
info.author = "The Hush Developers";
|
|
info.dark = true;
|
|
info.bundled = true;
|
|
info.valid = true;
|
|
// Try to set path to ui.toml if it exists
|
|
fs::path uiPath = fs::path(getBundledSkinsDirectory()) / "ui.toml";
|
|
if (fs::exists(uiPath)) {
|
|
info.path = uiPath.string();
|
|
}
|
|
skins_.push_back(std::move(info));
|
|
DEBUG_LOGF("[SkinManager] Injected base theme: dragonx\n");
|
|
}
|
|
}
|
|
|
|
// Sort: "dragonx" first, then bundled grouped by mode (dark then light), then user
|
|
std::sort(skins_.begin(), skins_.end(), [](const SkinInfo& a, const SkinInfo& b) {
|
|
// DragonX always first
|
|
if (a.id == "dragonx") return true;
|
|
if (b.id == "dragonx") return false;
|
|
|
|
// Bundled before user
|
|
if (a.bundled != b.bundled) return a.bundled;
|
|
|
|
// Group: dark themes first, then light themes
|
|
if (a.dark != b.dark) return a.dark;
|
|
|
|
// Alphabetical by name within each group
|
|
return a.name < b.name;
|
|
});
|
|
|
|
DEBUG_LOGF("[SkinManager] Found %zu skins\n", skins_.size());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Find
|
|
// ============================================================================
|
|
|
|
const SkinManager::SkinInfo* SkinManager::findById(const std::string& id) const {
|
|
for (const auto& skin : skins_) {
|
|
if (skin.id == id) return &skin;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Validation
|
|
// ============================================================================
|
|
|
|
SkinManager::ValidationResult SkinManager::validateSkinFile(const std::string& path) {
|
|
ValidationResult result;
|
|
|
|
// 1. Must be valid TOML
|
|
toml::table root;
|
|
try {
|
|
root = toml::parse_file(path);
|
|
} catch (const toml::parse_error& e) {
|
|
result.error = std::string("Invalid TOML: ") + e.what();
|
|
return result;
|
|
}
|
|
|
|
// 3. Must contain "theme" table
|
|
auto* theme = root["theme"].as_table();
|
|
if (!theme) {
|
|
result.error = "Missing or invalid 'theme' section";
|
|
return result;
|
|
}
|
|
|
|
// 4. theme.name must be a non-empty string
|
|
auto name = (*theme)["name"].value<std::string>();
|
|
if (!name || name->empty()) {
|
|
result.error = "theme.name must be a non-empty string";
|
|
return result;
|
|
}
|
|
|
|
// 5. theme.palette must exist with at least --primary and --background
|
|
auto* palette = (*theme)["palette"].as_table();
|
|
if (!palette) {
|
|
result.error = "Missing theme.palette table";
|
|
return result;
|
|
}
|
|
|
|
if (!palette->contains("--primary")) {
|
|
result.error = "Palette missing required '--primary' color";
|
|
return result;
|
|
}
|
|
if (!palette->contains("--background")) {
|
|
result.error = "Palette missing required '--background' color";
|
|
return result;
|
|
}
|
|
|
|
// 6. If globals exists, must be a table
|
|
if (root.contains("globals") && !root["globals"].is_table()) {
|
|
result.error = "'globals' must be a table";
|
|
return result;
|
|
}
|
|
|
|
result.valid = true;
|
|
return result;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Import
|
|
// ============================================================================
|
|
|
|
bool SkinManager::importSkin(const std::string& sourcePath) {
|
|
fs::path srcPath(sourcePath);
|
|
std::string userDir = getUserSkinsDirectory();
|
|
|
|
try {
|
|
fs::create_directories(userDir);
|
|
} catch (const fs::filesystem_error& e) {
|
|
DEBUG_LOGF("[SkinManager] Failed to create themes directory: %s\n", e.what());
|
|
return false;
|
|
}
|
|
|
|
if (fs::is_directory(srcPath)) {
|
|
// Import a theme folder — copy entire folder into themes/
|
|
fs::path themeToml = srcPath / "theme.toml";
|
|
if (!fs::exists(themeToml)) {
|
|
DEBUG_LOGF("[SkinManager] Import folder has no theme.toml: %s\n", sourcePath.c_str());
|
|
return false;
|
|
}
|
|
|
|
auto validation = validateSkinFile(themeToml.string());
|
|
if (!validation.valid) {
|
|
DEBUG_LOGF("[SkinManager] Import validation failed: %s\n", validation.error.c_str());
|
|
return false;
|
|
}
|
|
|
|
fs::path destDir = fs::path(userDir) / srcPath.filename();
|
|
try {
|
|
fs::copy(srcPath, destDir, fs::copy_options::recursive | fs::copy_options::overwrite_existing);
|
|
} catch (const fs::filesystem_error& e) {
|
|
DEBUG_LOGF("[SkinManager] Failed to copy theme folder: %s\n", e.what());
|
|
return false;
|
|
}
|
|
|
|
DEBUG_LOGF("[SkinManager] Imported theme folder: %s → %s\n", sourcePath.c_str(), destDir.string().c_str());
|
|
} else {
|
|
// Import a single .toml file — create a folder for it
|
|
auto validation = validateSkinFile(sourcePath);
|
|
if (!validation.valid) {
|
|
DEBUG_LOGF("[SkinManager] Import validation failed: %s\n", validation.error.c_str());
|
|
return false;
|
|
}
|
|
|
|
std::string folderName = srcPath.stem().string();
|
|
fs::path destDir = fs::path(userDir) / folderName;
|
|
try {
|
|
fs::create_directories(destDir);
|
|
fs::copy_file(srcPath, destDir / "theme.toml", fs::copy_options::overwrite_existing);
|
|
} catch (const fs::filesystem_error& e) {
|
|
DEBUG_LOGF("[SkinManager] Failed to import skin file: %s\n", e.what());
|
|
return false;
|
|
}
|
|
|
|
DEBUG_LOGF("[SkinManager] Imported skin file as folder: %s → %s\n", sourcePath.c_str(), destDir.string().c_str());
|
|
}
|
|
|
|
refresh();
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Remove
|
|
// ============================================================================
|
|
|
|
bool SkinManager::removeSkin(const std::string& id) {
|
|
const SkinInfo* skin = findById(id);
|
|
if (!skin) {
|
|
DEBUG_LOGF("[SkinManager] Skin not found: %s\n", id.c_str());
|
|
return false;
|
|
}
|
|
|
|
if (skin->bundled) {
|
|
DEBUG_LOGF("[SkinManager] Cannot remove bundled skin: %s\n", id.c_str());
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (!skin->directory.empty() && fs::is_directory(skin->directory)) {
|
|
// Folder-based theme — remove the entire directory
|
|
fs::remove_all(skin->directory);
|
|
} else {
|
|
// Legacy flat file
|
|
fs::remove(skin->path);
|
|
}
|
|
} catch (const fs::filesystem_error& e) {
|
|
DEBUG_LOGF("[SkinManager] Failed to remove skin: %s\n", e.what());
|
|
return false;
|
|
}
|
|
|
|
DEBUG_LOGF("[SkinManager] Removed skin: %s\n", id.c_str());
|
|
|
|
// If we removed the active skin, fall back to default
|
|
if (activeSkinId_ == id) {
|
|
setActiveSkin("dragonx");
|
|
}
|
|
|
|
refresh();
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Activate skin
|
|
// ============================================================================
|
|
|
|
bool SkinManager::setActiveSkin(const std::string& id) {
|
|
const SkinInfo* skin = findById(id);
|
|
if (!skin) {
|
|
DEBUG_LOGF("[SkinManager] Skin not found: %s\n", id.c_str());
|
|
return false;
|
|
}
|
|
|
|
if (!skin->valid) {
|
|
DEBUG_LOGF("[SkinManager] Skin is invalid: %s (%s)\n", id.c_str(), skin->validationError.c_str());
|
|
return false;
|
|
}
|
|
|
|
bool loaded = false;
|
|
|
|
// For skin files: always reload base layout first, then merge visual
|
|
// overlay on top. This ensures overlays only change palette + backdrop
|
|
// while inheriting all layout values from ui.toml.
|
|
if (!skin->path.empty()) {
|
|
auto& schema = UISchema::instance();
|
|
std::string basePath = schema.basePath();
|
|
|
|
if (!basePath.empty() || schema.hasEmbeddedBase()) {
|
|
if (!basePath.empty() && basePath == skin->path) {
|
|
// Switching back to the base theme: full reload, clear overlay
|
|
schema.reloadBase();
|
|
schema.reapplyColorsToImGui();
|
|
loaded = true;
|
|
} else {
|
|
// Switching to a non-base skin: reload base then merge overlay
|
|
if (schema.reloadBase()) {
|
|
if (schema.mergeOverlayFromFile(skin->path)) {
|
|
schema.reapplyColorsToImGui();
|
|
loaded = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: no base path or embedded data, full load of skin file
|
|
if (!loaded && schema.loadFromFile(skin->path)) {
|
|
schema.reapplyColorsToImGui();
|
|
loaded = true;
|
|
}
|
|
} else if (!id.empty()) {
|
|
// Skin with no path (e.g., "dragonx" on Windows with embedded ui.toml):
|
|
// just reload the base to restore the default theme
|
|
auto& schema = UISchema::instance();
|
|
if (schema.hasEmbeddedBase() || !schema.basePath().empty()) {
|
|
schema.reloadBase();
|
|
schema.reapplyColorsToImGui();
|
|
loaded = true;
|
|
}
|
|
}
|
|
|
|
// Fall back to built-in C++ themes (works even without theme files)
|
|
if (!loaded) {
|
|
if (!SetThemeById(id)) {
|
|
DEBUG_LOGF("[SkinManager] Failed to load skin: %s\n", id.c_str());
|
|
return false;
|
|
}
|
|
DEBUG_LOGF("[SkinManager] Loaded via built-in theme fallback: %s\n", id.c_str());
|
|
loaded = true;
|
|
}
|
|
|
|
activeSkinId_ = id;
|
|
DEBUG_LOGF("[SkinManager] Activated skin: %s (%s)\n", id.c_str(), skin->name.c_str());
|
|
|
|
// Reload theme visual effects config from the new skin's [effects] section
|
|
effects::ThemeEffects::instance().loadFromTheme();
|
|
|
|
// Resolve image paths from UISchema (which parsed [theme] images from the TOML).
|
|
// The UISchema stores relative filenames (e.g. "backgrounds/texture/drgx_bg.png");
|
|
// resolve them to absolute paths using the theme's directory structure.
|
|
{
|
|
// Use a mutable reference to update the SkinInfo
|
|
SkinInfo* mutableSkin = nullptr;
|
|
for (auto& s : skins_) {
|
|
if (s.id == id) { mutableSkin = &s; break; }
|
|
}
|
|
|
|
fs::path imgDir = resolveImgDir(skin);
|
|
resolveAndFireCallback(mutableSkin, imgDir);
|
|
}
|
|
|
|
// Force acrylic to re-capture the background with new theme colors/images.
|
|
// This must happen AFTER images are reloaded so the next frame renders the
|
|
// updated background before capture.
|
|
effects::ImGuiAcrylic::InvalidateCapture();
|
|
|
|
return true;
|
|
}
|
|
|
|
void SkinManager::resolveAndReloadImages(const std::string& skinId, const std::string& tomlPath) {
|
|
// Find the skin and update its image paths from the current UISchema values
|
|
SkinInfo* skin = nullptr;
|
|
for (auto& s : skins_) {
|
|
if (s.id == skinId) {
|
|
skin = &s;
|
|
break;
|
|
}
|
|
}
|
|
|
|
fs::path imgDir = resolveImgDir(skin);
|
|
resolveAndFireCallback(skin, imgDir);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Gradient mode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void SkinManager::setGradientMode(bool enabled) {
|
|
if (gradientMode_ == enabled) return;
|
|
gradientMode_ = enabled;
|
|
|
|
// Re-resolve + reload for the currently active skin
|
|
const SkinInfo* skin = findById(activeSkinId_);
|
|
if (!skin) return;
|
|
|
|
SkinInfo* mutableSkin = nullptr;
|
|
for (auto& s : skins_) {
|
|
if (s.id == activeSkinId_) { mutableSkin = &s; break; }
|
|
}
|
|
|
|
fs::path imgDir = resolveImgDir(skin);
|
|
resolveAndFireCallback(mutableSkin, imgDir);
|
|
|
|
effects::ImGuiAcrylic::InvalidateCapture();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fs::path SkinManager::resolveImgDir(const SkinInfo* skin) const {
|
|
if (!skin) {
|
|
return fs::path(getBundledSkinsDirectory()).parent_path() / "img";
|
|
}
|
|
if (skin->bundled) {
|
|
fs::path themeDir = skin->path.empty()
|
|
? fs::path(getBundledSkinsDirectory())
|
|
: fs::path(skin->path).parent_path();
|
|
return themeDir.parent_path() / "img";
|
|
}
|
|
if (!skin->directory.empty()) {
|
|
return fs::path(skin->directory) / "img";
|
|
}
|
|
if (!skin->path.empty()) {
|
|
return fs::path(skin->path).parent_path();
|
|
}
|
|
return fs::path(getBundledSkinsDirectory()).parent_path() / "img";
|
|
}
|
|
|
|
std::string SkinManager::resolveGradientBg(const std::string& bgFilename,
|
|
const fs::path& imgDir,
|
|
bool isDark) const {
|
|
// Given bgFilename like "backgrounds/texture/drgx_bg.png",
|
|
// look for "backgrounds/gradient/gradient_drgx_bg.png".
|
|
fs::path bgRel(bgFilename);
|
|
std::string stem = bgRel.stem().string(); // "drgx_bg"
|
|
std::string ext = bgRel.extension().string(); // ".png"
|
|
|
|
// Build the gradient candidate: backgrounds/gradient/gradient_<stem><ext>
|
|
std::string gradientRel = "backgrounds/gradient/gradient_" + stem + ext;
|
|
fs::path gradientPath = imgDir / gradientRel;
|
|
|
|
if (fs::exists(gradientPath)) {
|
|
return gradientPath.string();
|
|
}
|
|
|
|
// Fallback: dark_gradient.png or light_gradient.png
|
|
std::string fallbackName = isDark ? "dark_gradient.png" : "light_gradient.png";
|
|
std::string fallbackRel = "backgrounds/gradient/" + fallbackName;
|
|
fs::path fallbackPath = imgDir / fallbackRel;
|
|
|
|
if (fs::exists(fallbackPath)) {
|
|
return fallbackPath.string();
|
|
}
|
|
|
|
// Last resort: pass the relative gradient filename for embedded lookup (Windows)
|
|
return gradientRel;
|
|
}
|
|
|
|
void SkinManager::resolveAndFireCallback(SkinInfo* skin, const fs::path& imgDir) {
|
|
auto& schema = UISchema::instance();
|
|
std::string bgFilename = schema.backgroundImagePath();
|
|
std::string logoFilename = schema.logoImagePath();
|
|
|
|
std::string resolvedBg;
|
|
std::string resolvedLogo;
|
|
|
|
if (!bgFilename.empty()) {
|
|
if (gradientMode_) {
|
|
resolvedBg = resolveGradientBg(bgFilename, imgDir, schema.isDarkTheme());
|
|
} else {
|
|
fs::path bgPath = imgDir / bgFilename;
|
|
if (fs::exists(bgPath)) {
|
|
resolvedBg = bgPath.string();
|
|
} else {
|
|
resolvedBg = bgFilename;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!logoFilename.empty()) {
|
|
fs::path logoPath = imgDir / logoFilename;
|
|
if (fs::exists(logoPath)) {
|
|
resolvedLogo = logoPath.string();
|
|
} else {
|
|
resolvedLogo = logoFilename;
|
|
}
|
|
}
|
|
|
|
if (skin) {
|
|
skin->backgroundImagePath = resolvedBg;
|
|
skin->logoPath = resolvedLogo;
|
|
}
|
|
|
|
DEBUG_LOGF("[SkinManager] Resolved images (gradient=%s): bg='%s', logo='%s'\n",
|
|
gradientMode_ ? "on" : "off", resolvedBg.c_str(), resolvedLogo.c_str());
|
|
|
|
if (imageReloadCb_) {
|
|
imageReloadCb_(resolvedBg, resolvedLogo);
|
|
}
|
|
}
|
|
|
|
} // namespace schema
|
|
} // namespace ui
|
|
} // namespace dragonx
|