v1.1.0 — chemin Blender configurable depuis l'UI

Refonte de PathResolver : auto-detection systeme (which/where + chemins courants),
persistance du chemin dans userData, validation du fichier.
Ajout d'un modal Bootstrap pour configurer le chemin manuellement.
Badge vert/rouge dans la navbar indiquant le statut Blender.
Le modal s'ouvre automatiquement si Blender n'est pas trouve au lancement.
Suppression de extraResources (plus de Blender embarque dans le build).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sorlinv
2026-02-25 16:44:04 +01:00
parent f31f5aa605
commit d5cb63a27b
8 changed files with 393 additions and 38 deletions

35
main.js
View File

@@ -5,6 +5,7 @@ const CameraParser = require("./src/main/CameraParser.js");
const QueueManager = require("./src/main/QueueManager.js");
const ConfigManager = require("./src/main/ConfigManager.js");
const UpdateManager = require("./src/main/UpdateManager.js");
const PathResolver = require("./src/main/PathResolver.js");
let obj_main_window = null;
let obj_queue_manager = null;
@@ -27,9 +28,12 @@ const create_window = () => {
obj_queue_manager = new QueueManager(obj_main_window);
PathResolver.load_saved_path();
UpdateManager.init(obj_main_window);
obj_main_window.webContents.on("did-finish-load", () => {
UpdateManager.check_for_updates();
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
});
};
@@ -159,6 +163,37 @@ ipcMain.handle("read-image", (event, str_image_path) => {
return fn_try_read(0);
});
ipcMain.handle("get-blender-path", () => {
return PathResolver.get_status();
});
ipcMain.handle("set-blender-path", (event, str_path) => {
let obj_result = PathResolver.set_blender_path(str_path);
if (obj_result.is_success) {
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
}
return obj_result;
});
ipcMain.handle("select-blender-exe", () => {
let str_exe_name = process.platform === "win32" ? "blender.exe" : "blender";
let list_filters = process.platform === "win32"
? [{ name: "Blender", extensions: ["exe"] }]
: [{ name: "Blender", extensions: ["*"] }];
return dialog.showOpenDialog(obj_main_window, {
title: "Selectionner l'executable Blender",
filters: list_filters,
properties: ["openFile"],
})
.then((obj_result) => {
if (obj_result.canceled || obj_result.filePaths.length === 0) {
return null;
}
return obj_result.filePaths[0];
});
});
ipcMain.handle("check-for-updates", () => {
return UpdateManager.check_for_updates();
});

View File

@@ -23,13 +23,6 @@
"src/**/*",
"version.json"
],
"extraResources": [
{
"from": "blender",
"to": "blender",
"filter": ["**/*"]
}
],
"linux": {
"target": "dir"
},

View File

@@ -67,6 +67,24 @@ contextBridge.exposeInMainWorld("api", {
});
},
get_blender_path: () => {
return ipcRenderer.invoke("get-blender-path");
},
set_blender_path: (str_path) => {
return ipcRenderer.invoke("set-blender-path", str_path);
},
select_blender_exe: () => {
return ipcRenderer.invoke("select-blender-exe");
},
on_blender_path_status: (fn_callback) => {
ipcRenderer.on("blender-path-status", (event, obj_data) => {
fn_callback(obj_data);
});
},
check_for_updates: () => {
return ipcRenderer.invoke("check-for-updates");
},

View File

@@ -1,49 +1,156 @@
const path = require("path");
const fs = require("fs");
const { app } = require("electron");
const { execFileSync } = require("child_process");
const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender";
const STR_CONFIG_FILE = "blender_path.json";
const PathResolver = {
_str_blender_path: null,
_is_found: false,
get_blender_path: () => {
if (PathResolver._str_blender_path) {
return PathResolver._str_blender_path;
load_saved_path: () => {
let str_config_path = PathResolver._get_config_path();
try {
if (fs.existsSync(str_config_path)) {
let str_content = fs.readFileSync(str_config_path, "utf8");
let obj_data = JSON.parse(str_content);
if (obj_data.str_path && fs.existsSync(obj_data.str_path)) {
PathResolver._str_blender_path = obj_data.str_path;
PathResolver._is_found = true;
return;
}
}
} catch (obj_err) {
console.error("PathResolver: impossible de lire la config :", obj_err.message);
}
// Mode package : resources/blender/
let str_resources_dir = path.join(process.resourcesPath, "blender");
let str_found = PathResolver._find_in_dir(str_resources_dir);
if (str_found) {
PathResolver._str_blender_path = str_found;
return str_found;
}
// Mode dev : racine projet/blender/
let str_dev_dir = path.join(__dirname, "..", "..", "blender");
str_found = PathResolver._find_in_dir(str_dev_dir);
if (str_found) {
PathResolver._str_blender_path = str_found;
return str_found;
}
// Fallback : PATH systeme
let str_detected = PathResolver.auto_detect();
if (str_detected) {
PathResolver._str_blender_path = str_detected;
PathResolver._is_found = true;
} else {
PathResolver._str_blender_path = "blender";
return "blender";
PathResolver._is_found = false;
}
},
_find_in_dir: (str_dir) => {
if (!fs.existsSync(str_dir)) {
return null;
get_blender_path: () => {
if (!PathResolver._str_blender_path) {
PathResolver.load_saved_path();
}
return PathResolver._str_blender_path;
},
is_found: () => {
return PathResolver._is_found;
},
set_blender_path: (str_path) => {
if (!str_path || !fs.existsSync(str_path)) {
return { is_success: false, str_error: "Fichier introuvable : " + str_path };
}
let list_entries = fs.readdirSync(str_dir);
for (let str_entry of list_entries) {
let str_exe = path.join(str_dir, str_entry, STR_EXE_NAME);
PathResolver._str_blender_path = str_path;
PathResolver._is_found = true;
let str_config_path = PathResolver._get_config_path();
try {
let str_dir = path.dirname(str_config_path);
if (!fs.existsSync(str_dir)) {
fs.mkdirSync(str_dir, { recursive: true });
}
fs.writeFileSync(str_config_path, JSON.stringify({ str_path: str_path }, null, 4), "utf8");
} catch (obj_err) {
console.error("PathResolver: impossible de sauvegarder :", obj_err.message);
}
return { is_success: true, str_path: str_path };
},
auto_detect: () => {
if (process.platform === "win32") {
return PathResolver._auto_detect_windows();
}
return PathResolver._auto_detect_linux();
},
get_status: () => {
return {
str_path: PathResolver._str_blender_path || "blender",
is_found: PathResolver._is_found,
};
},
// ── Private ──────────────────────────────────────────────
_get_config_path: () => {
return path.join(app.getPath("userData"), STR_CONFIG_FILE);
},
_auto_detect_linux: () => {
let LIST_PATHS = [
"/usr/bin/blender",
"/snap/bin/blender",
"/usr/local/bin/blender",
"/opt/blender/blender",
];
try {
let str_result = execFileSync("which", ["blender"], { encoding: "utf8", timeout: 5000 }).trim();
if (str_result && fs.existsSync(str_result)) {
return str_result;
}
} catch (obj_err) {
// which not found or blender not in PATH
}
for (let str_path of LIST_PATHS) {
if (fs.existsSync(str_path)) {
return str_path;
}
}
return null;
},
_auto_detect_windows: () => {
try {
let str_result = execFileSync("where", ["blender.exe"], { encoding: "utf8", timeout: 5000 }).trim();
let str_first = str_result.split("\n")[0].trim();
if (str_first && fs.existsSync(str_first)) {
return str_first;
}
} catch (obj_err) {
// where not found or blender not in PATH
}
let LIST_BASES = [
process.env.PROGRAMFILES || "C:\\Program Files",
process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)",
];
for (let str_base of LIST_BASES) {
let str_foundation = path.join(str_base, "Blender Foundation");
if (!fs.existsSync(str_foundation)) {
continue;
}
try {
let list_dirs = fs.readdirSync(str_foundation);
for (let str_dir of list_dirs) {
let str_exe = path.join(str_foundation, str_dir, STR_EXE_NAME);
if (fs.existsSync(str_exe)) {
return str_exe;
}
}
} catch (obj_err) {
// permission denied or similar
}
}
return null;
},

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:;">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:;">
<title>Multi Render Blender</title>
<!-- Bootstrap 5 -->
@@ -253,6 +253,9 @@
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Scripts -->
<script src="scripts/ConsoleLog.js"></script>
<script src="scripts/CameraList.js"></script>
@@ -261,6 +264,7 @@
<script src="scripts/PreviewPanel.js"></script>
<script src="scripts/ProgressBar.js"></script>
<script src="scripts/UpdateBanner.js"></script>
<script src="scripts/BlenderPath.js"></script>
<script src="scripts/App.js"></script>
</body>
</html>

View File

@@ -10,6 +10,7 @@ const App = {
PreviewPanel.init();
ProgressBar.init();
UpdateBanner.init();
BlenderPath.init();
App._bind_events();
App._bind_render_events();

View File

@@ -0,0 +1,197 @@
const BlenderPath = {
str_current_path: null,
is_found: false,
obj_modal: null,
init: () => {
BlenderPath._create_badge();
BlenderPath._create_modal();
BlenderPath._bind_events();
},
_create_badge: () => {
let obj_nav_right = document.querySelector("nav .d-flex.gap-2");
let obj_badge = document.createElement("button");
obj_badge.id = "btn_blender_status";
obj_badge.className = "btn btn-sm btn-outline-secondary";
obj_badge.title = "Chemin Blender";
obj_badge.innerHTML = '<i class="mdi mdi-blender-software"></i>';
obj_nav_right.insertBefore(obj_badge, obj_nav_right.firstChild);
obj_badge.addEventListener("click", () => {
BlenderPath._open_modal();
});
},
_create_modal: () => {
let obj_modal_el = document.createElement("div");
obj_modal_el.id = "modal_blender_path";
obj_modal_el.className = "modal fade";
obj_modal_el.tabIndex = -1;
obj_modal_el.innerHTML =
'<div class="modal-dialog modal-dialog-centered">' +
'<div class="modal-content bg-dark text-light border-secondary">' +
'<div class="modal-header border-secondary">' +
'<h6 class="modal-title"><i class="mdi mdi-blender-software me-2"></i>Chemin Blender</h6>' +
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
'</div>' +
'<div class="modal-body">' +
'<p class="text-light-emphasis mb-3" style="font-size: 0.85rem;">' +
'Selectionnez l\'executable Blender sur votre machine.' +
'</p>' +
'<div class="input-group input-group-sm mb-3">' +
'<input type="text" id="input_blender_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun chemin configure" readonly>' +
'<button id="btn_browse_blender" class="btn btn-outline-primary" type="button">' +
'<i class="mdi mdi-folder-search-outline"></i> Parcourir' +
'</button>' +
'</div>' +
'<div class="d-flex justify-content-between align-items-center">' +
'<button id="btn_detect_blender" class="btn btn-sm btn-outline-secondary">' +
'<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement' +
'</button>' +
'<span id="label_blender_status" class="badge bg-danger">' +
'<i class="mdi mdi-close-circle me-1"></i>Non trouve' +
'</span>' +
'</div>' +
'</div>' +
'<div class="modal-footer border-secondary">' +
'<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Annuler</button>' +
'<button id="btn_validate_blender" type="button" class="btn btn-sm btn-primary" disabled>' +
'<i class="mdi mdi-check me-1"></i>Valider' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(obj_modal_el);
BlenderPath.obj_modal = new bootstrap.Modal(obj_modal_el);
},
_bind_events: () => {
let obj_btn_browse = document.getElementById("btn_browse_blender");
obj_btn_browse.addEventListener("click", () => {
BlenderPath._browse();
});
let obj_btn_detect = document.getElementById("btn_detect_blender");
obj_btn_detect.addEventListener("click", () => {
BlenderPath._detect();
});
let obj_btn_validate = document.getElementById("btn_validate_blender");
obj_btn_validate.addEventListener("click", () => {
BlenderPath._validate();
});
window.api.on_blender_path_status((obj_data) => {
BlenderPath.str_current_path = obj_data.str_path;
BlenderPath.is_found = obj_data.is_found;
BlenderPath._update_badge();
if (!obj_data.is_found) {
BlenderPath._open_modal();
}
});
},
_open_modal: () => {
let obj_input = document.getElementById("input_blender_path");
obj_input.value = BlenderPath.is_found ? BlenderPath.str_current_path : "";
BlenderPath._update_modal_status(BlenderPath.is_found, BlenderPath.str_current_path);
BlenderPath.obj_modal.show();
if (!BlenderPath.is_found) {
BlenderPath._detect();
}
},
_browse: () => {
window.api.select_blender_exe()
.then((str_path) => {
if (!str_path) {
return;
}
let obj_input = document.getElementById("input_blender_path");
obj_input.value = str_path;
BlenderPath._update_modal_status(true, str_path);
})
.catch((obj_err) => {
ConsoleLog.add("Erreur selection Blender : " + obj_err.message);
});
},
_detect: () => {
let obj_btn = document.getElementById("btn_detect_blender");
obj_btn.disabled = true;
obj_btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Detection...';
window.api.get_blender_path()
.then((obj_data) => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement';
if (obj_data.is_found) {
let obj_input = document.getElementById("input_blender_path");
obj_input.value = obj_data.str_path;
BlenderPath._update_modal_status(true, obj_data.str_path);
} else {
BlenderPath._update_modal_status(false, null);
}
})
.catch(() => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement';
BlenderPath._update_modal_status(false, null);
});
},
_validate: () => {
let str_path = document.getElementById("input_blender_path").value;
if (!str_path) {
return;
}
window.api.set_blender_path(str_path)
.then((obj_result) => {
if (obj_result.is_success) {
BlenderPath.str_current_path = obj_result.str_path;
BlenderPath.is_found = true;
BlenderPath._update_badge();
BlenderPath.obj_modal.hide();
ConsoleLog.add("Chemin Blender configure : " + obj_result.str_path);
} else {
BlenderPath._update_modal_status(false, null);
ConsoleLog.add("Chemin Blender invalide : " + (obj_result.str_error || ""));
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur configuration Blender : " + obj_err.message);
});
},
_update_badge: () => {
let obj_badge = document.getElementById("btn_blender_status");
if (BlenderPath.is_found) {
obj_badge.className = "btn btn-sm btn-outline-success";
obj_badge.title = "Blender : " + BlenderPath.str_current_path;
} else {
obj_badge.className = "btn btn-sm btn-outline-danger";
obj_badge.title = "Blender non trouve";
}
},
_update_modal_status: (is_valid, str_path) => {
let obj_label = document.getElementById("label_blender_status");
let obj_btn_validate = document.getElementById("btn_validate_blender");
if (is_valid && str_path) {
obj_label.className = "badge bg-success";
obj_label.innerHTML = '<i class="mdi mdi-check-circle me-1"></i>Trouve';
obj_btn_validate.disabled = false;
} else {
obj_label.className = "badge bg-danger";
obj_label.innerHTML = '<i class="mdi mdi-close-circle me-1"></i>Non trouve';
obj_btn_validate.disabled = true;
}
},
};

View File

@@ -1,3 +1,3 @@
{
"str_version": "1.0.0"
"str_version": "1.1.0"
}