Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbd2f676e2 | ||
|
|
ada5c05aa7 | ||
|
|
eae2c68f29 | ||
|
|
54d7d41eb6 | ||
|
|
88ad17164c | ||
|
|
40b9e29f80 | ||
|
|
b169e69b24 | ||
|
|
9ab59373df | ||
|
|
02d2e9ed1d | ||
|
|
b1c66f055a |
@@ -30,7 +30,9 @@
|
|||||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''Release creee: id={d.get\\(\"\"id\"\", \"\"ERREUR\"\"\\)} tag={d.get\\(\"\"tag_name\"\", \"\"?\"\"\\)}''\\); print\\(d.get\\(''message'',''''\\)\\) if ''message'' in d else None\")",
|
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''Release creee: id={d.get\\(\"\"id\"\", \"\"ERREUR\"\"\\)} tag={d.get\\(\"\"tag_name\"\", \"\"?\"\"\\)}''\\); print\\(d.get\\(''message'',''''\\)\\) if ''message'' in d else None\")",
|
||||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f'' OK: {d.get\\(\"\"name\"\",\"\"?\"\"\\)} \\({d.get\\(\"\"size\"\",0\\)//1024//1024}MB\\)''\\)\")",
|
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f'' OK: {d.get\\(\"\"name\"\",\"\"?\"\"\\)} \\({d.get\\(\"\"size\"\",0\\)//1024//1024}MB\\)''\\)\")",
|
||||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''OK: {d.get\\(\"\"name\"\",d.get\\(\"\"message\"\",\"\"?\"\"\\)\\)} size={d.get\\(\"\"size\"\",0\\)//1024//1024}MB''\\)\")",
|
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''OK: {d.get\\(\"\"name\"\",d.get\\(\"\"message\"\",\"\"?\"\"\\)\\)} size={d.get\\(\"\"size\"\",0\\)//1024//1024}MB''\\)\")",
|
||||||
"Bash(python3 -c \":*)"
|
"Bash(python3 -c \":*)",
|
||||||
|
"mcp__ide__getDiagnostics",
|
||||||
|
"Bash(ffmpeg:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
161
main.js
161
main.js
@@ -1,4 +1,4 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
|
const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, nativeImage } = require("electron");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const CameraParser = require("./src/main/CameraParser.js");
|
const CameraParser = require("./src/main/CameraParser.js");
|
||||||
@@ -6,12 +6,16 @@ const QueueManager = require("./src/main/QueueManager.js");
|
|||||||
const ConfigManager = require("./src/main/ConfigManager.js");
|
const ConfigManager = require("./src/main/ConfigManager.js");
|
||||||
const UpdateManager = require("./src/main/UpdateManager.js");
|
const UpdateManager = require("./src/main/UpdateManager.js");
|
||||||
const PathResolver = require("./src/main/PathResolver.js");
|
const PathResolver = require("./src/main/PathResolver.js");
|
||||||
|
const VideoGenerator = require("./src/main/VideoGenerator.js");
|
||||||
|
const EmailNotifier = require("./src/main/EmailNotifier.js");
|
||||||
|
|
||||||
let obj_main_window = null;
|
let obj_main_window = null;
|
||||||
let obj_queue_manager = null;
|
let obj_queue_manager = null;
|
||||||
let obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
|
let obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
|
||||||
|
let obj_email_config = { is_enabled: false, str_to: "" };
|
||||||
|
|
||||||
const STR_NOTIFICATION_CONFIG_FILE = "notification_config.json";
|
const STR_NOTIFICATION_CONFIG_FILE = "notification_config.json";
|
||||||
|
const STR_EMAIL_CONFIG_FILE = "email_config.json";
|
||||||
|
|
||||||
const _load_notification_config = () => {
|
const _load_notification_config = () => {
|
||||||
let str_config_path = path.join(app.getPath("userData"), STR_NOTIFICATION_CONFIG_FILE);
|
let str_config_path = path.join(app.getPath("userData"), STR_NOTIFICATION_CONFIG_FILE);
|
||||||
@@ -34,6 +38,28 @@ const _save_notification_config = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _load_email_config = () => {
|
||||||
|
let str_config_path = path.join(app.getPath("userData"), STR_EMAIL_CONFIG_FILE);
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(str_config_path)) {
|
||||||
|
let str_content = fs.readFileSync(str_config_path, "utf8");
|
||||||
|
obj_email_config = JSON.parse(str_content);
|
||||||
|
EmailNotifier.set_config(obj_email_config);
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
console.error("Erreur lecture config email :", obj_err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _save_email_config = () => {
|
||||||
|
let str_config_path = path.join(app.getPath("userData"), STR_EMAIL_CONFIG_FILE);
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(str_config_path, JSON.stringify(obj_email_config, null, 4), "utf8");
|
||||||
|
} catch (obj_err) {
|
||||||
|
console.error("Erreur sauvegarde config email :", obj_err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const create_window = () => {
|
const create_window = () => {
|
||||||
obj_main_window = new BrowserWindow({
|
obj_main_window = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1400,
|
||||||
@@ -53,13 +79,17 @@ const create_window = () => {
|
|||||||
obj_queue_manager = new QueueManager(obj_main_window);
|
obj_queue_manager = new QueueManager(obj_main_window);
|
||||||
|
|
||||||
PathResolver.load_saved_path();
|
PathResolver.load_saved_path();
|
||||||
|
PathResolver.load_saved_ffmpeg_path();
|
||||||
_load_notification_config();
|
_load_notification_config();
|
||||||
|
_load_email_config();
|
||||||
obj_queue_manager.set_notification_config(obj_notification_config);
|
obj_queue_manager.set_notification_config(obj_notification_config);
|
||||||
|
obj_queue_manager.set_email_notifier(EmailNotifier);
|
||||||
|
|
||||||
UpdateManager.init(obj_main_window);
|
UpdateManager.init(obj_main_window);
|
||||||
obj_main_window.webContents.on("did-finish-load", () => {
|
obj_main_window.webContents.on("did-finish-load", () => {
|
||||||
UpdateManager.check_for_updates();
|
UpdateManager.check_for_updates();
|
||||||
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
|
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
|
||||||
|
obj_main_window.webContents.send("ffmpeg-path-status", PathResolver.get_ffmpeg_status());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,6 +235,37 @@ ipcMain.handle("set-blender-path", (event, str_path) => {
|
|||||||
return obj_result;
|
return obj_result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("get-ffmpeg-path", () => {
|
||||||
|
return PathResolver.get_ffmpeg_status();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("set-ffmpeg-path", (event, str_path) => {
|
||||||
|
let obj_result = PathResolver.set_ffmpeg_path(str_path);
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
obj_main_window.webContents.send("ffmpeg-path-status", PathResolver.get_ffmpeg_status());
|
||||||
|
}
|
||||||
|
return obj_result;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("select-ffmpeg-exe", () => {
|
||||||
|
let str_exe_name = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
|
||||||
|
let list_filters = process.platform === "win32"
|
||||||
|
? [{ name: "FFmpeg", extensions: ["exe"] }]
|
||||||
|
: [{ name: "FFmpeg", extensions: ["*"] }];
|
||||||
|
|
||||||
|
return dialog.showOpenDialog(obj_main_window, {
|
||||||
|
title: "Selectionner l'executable FFmpeg",
|
||||||
|
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("select-blender-exe", () => {
|
ipcMain.handle("select-blender-exe", () => {
|
||||||
let str_exe_name = process.platform === "win32" ? "blender.exe" : "blender";
|
let str_exe_name = process.platform === "win32" ? "blender.exe" : "blender";
|
||||||
let list_filters = process.platform === "win32"
|
let list_filters = process.platform === "win32"
|
||||||
@@ -232,6 +293,15 @@ ipcMain.handle("apply-update", (event, str_tag_name) => {
|
|||||||
return UpdateManager.download_and_apply(str_tag_name);
|
return UpdateManager.download_and_apply(str_tag_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("generate-preview-video", (event, obj_params) => {
|
||||||
|
let fn_on_log = (str_msg) => {
|
||||||
|
if (obj_main_window && !obj_main_window.isDestroyed()) {
|
||||||
|
obj_main_window.webContents.send("log", "[ffmpeg] " + str_msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return VideoGenerator.generate(obj_params, fn_on_log);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("select-output-folder", () => {
|
ipcMain.handle("select-output-folder", () => {
|
||||||
return dialog.showOpenDialog(obj_main_window, {
|
return dialog.showOpenDialog(obj_main_window, {
|
||||||
title: "Selectionner le dossier de sortie",
|
title: "Selectionner le dossier de sortie",
|
||||||
@@ -245,6 +315,78 @@ ipcMain.handle("select-output-folder", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── File actions (context menu) ───────────────────────────────
|
||||||
|
|
||||||
|
ipcMain.handle("show-item-in-folder", (event, str_path) => {
|
||||||
|
shell.showItemInFolder(str_path);
|
||||||
|
return { is_success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("open-file-default", (event, str_path) => {
|
||||||
|
return shell.openPath(str_path)
|
||||||
|
.then((str_error) => {
|
||||||
|
if (str_error) {
|
||||||
|
return { is_success: false, str_error: str_error };
|
||||||
|
}
|
||||||
|
return { is_success: true };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("delete-rendered-file", (event, str_path) => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(str_path)) {
|
||||||
|
fs.unlinkSync(str_path);
|
||||||
|
return { is_success: true };
|
||||||
|
}
|
||||||
|
return { is_success: false, str_error: "Fichier introuvable" };
|
||||||
|
} catch (obj_err) {
|
||||||
|
return { is_success: false, str_error: obj_err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("delete-rendered-files", (event, list_paths) => {
|
||||||
|
let nb_deleted = 0;
|
||||||
|
let list_errors = [];
|
||||||
|
for (let str_p of list_paths) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(str_p)) {
|
||||||
|
fs.unlinkSync(str_p);
|
||||||
|
nb_deleted++;
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
list_errors.push(str_p + ": " + obj_err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { is_success: true, nb_deleted: nb_deleted, list_errors: list_errors };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("get-file-info", (event, str_path) => {
|
||||||
|
try {
|
||||||
|
let obj_stats = fs.statSync(str_path);
|
||||||
|
return {
|
||||||
|
is_success: true,
|
||||||
|
nb_size: obj_stats.size,
|
||||||
|
str_modified: obj_stats.mtime.toISOString(),
|
||||||
|
str_created: obj_stats.birthtime.toISOString(),
|
||||||
|
};
|
||||||
|
} catch (obj_err) {
|
||||||
|
return { is_success: false, str_error: obj_err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("copy-image-to-clipboard", (event, str_path) => {
|
||||||
|
try {
|
||||||
|
let obj_image = nativeImage.createFromPath(str_path);
|
||||||
|
if (obj_image.isEmpty()) {
|
||||||
|
return { is_success: false, str_error: "Image vide ou format non supporte" };
|
||||||
|
}
|
||||||
|
clipboard.writeImage(obj_image);
|
||||||
|
return { is_success: true };
|
||||||
|
} catch (obj_err) {
|
||||||
|
return { is_success: false, str_error: obj_err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Notification Config ───────────────────────────────────────
|
// ── Notification Config ───────────────────────────────────────
|
||||||
|
|
||||||
ipcMain.handle("get-notification-config", () => {
|
ipcMain.handle("get-notification-config", () => {
|
||||||
@@ -257,3 +399,20 @@ ipcMain.handle("set-notification-config", (event, obj_config) => {
|
|||||||
obj_queue_manager.set_notification_config(obj_notification_config);
|
obj_queue_manager.set_notification_config(obj_notification_config);
|
||||||
return { is_success: true };
|
return { is_success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Email Config ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
ipcMain.handle("get-email-config", () => {
|
||||||
|
return obj_email_config;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("set-email-config", (event, obj_config) => {
|
||||||
|
obj_email_config = obj_config;
|
||||||
|
_save_email_config();
|
||||||
|
EmailNotifier.set_config(obj_email_config);
|
||||||
|
return { is_success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("test-email", (event, obj_config) => {
|
||||||
|
return EmailNotifier.test(obj_config);
|
||||||
|
});
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-render-blender",
|
"name": "multi-render-blender",
|
||||||
"version": "1.3.0",
|
"version": "1.6.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "multi-render-blender",
|
"name": "multi-render-blender",
|
||||||
"version": "1.3.0",
|
"version": "1.6.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nodemailer": "^8.0.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^34.0.0",
|
"electron": "^34.0.0",
|
||||||
"electron-builder": "^25.1.8"
|
"electron-builder": "^25.1.8"
|
||||||
@@ -3958,6 +3961,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-render-blender",
|
"name": "multi-render-blender",
|
||||||
"version": "1.4.0",
|
"version": "1.6.3",
|
||||||
"description": "Application Electron pour piloter des rendus Blender multi-cameras",
|
"description": "Application Electron pour piloter des rendus Blender multi-cameras",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^34.0.0",
|
"electron": "^40.7.0",
|
||||||
"electron-builder": "^25.1.8"
|
"electron-builder": "^26.8.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.multirender.blender",
|
"appId": "com.multirender.blender",
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"main.js",
|
"main.js",
|
||||||
"preload.js",
|
"preload.js",
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"version.json"
|
"version.json",
|
||||||
|
"node_modules/nodemailer/**/*"
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": "dir"
|
"target": "dir"
|
||||||
@@ -31,5 +32,8 @@
|
|||||||
"target": "dir",
|
"target": "dir",
|
||||||
"signAndEditExecutable": false
|
"signAndEditExecutable": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nodemailer": "^8.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
preload.js
58
preload.js
@@ -41,6 +41,34 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
return ipcRenderer.invoke("read-image", str_path);
|
return ipcRenderer.invoke("read-image", str_path);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
show_item_in_folder: (str_path) => {
|
||||||
|
return ipcRenderer.invoke("show-item-in-folder", str_path);
|
||||||
|
},
|
||||||
|
|
||||||
|
open_file_default: (str_path) => {
|
||||||
|
return ipcRenderer.invoke("open-file-default", str_path);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_rendered_file: (str_path) => {
|
||||||
|
return ipcRenderer.invoke("delete-rendered-file", str_path);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_rendered_files: (list_paths) => {
|
||||||
|
return ipcRenderer.invoke("delete-rendered-files", list_paths);
|
||||||
|
},
|
||||||
|
|
||||||
|
get_file_info: (str_path) => {
|
||||||
|
return ipcRenderer.invoke("get-file-info", str_path);
|
||||||
|
},
|
||||||
|
|
||||||
|
copy_image_to_clipboard: (str_path) => {
|
||||||
|
return ipcRenderer.invoke("copy-image-to-clipboard", str_path);
|
||||||
|
},
|
||||||
|
|
||||||
|
generate_preview_video: (obj_params) => {
|
||||||
|
return ipcRenderer.invoke("generate-preview-video", obj_params);
|
||||||
|
},
|
||||||
|
|
||||||
on_render_progress: (fn_callback) => {
|
on_render_progress: (fn_callback) => {
|
||||||
ipcRenderer.on("render-progress", (event, obj_data) => {
|
ipcRenderer.on("render-progress", (event, obj_data) => {
|
||||||
fn_callback(obj_data);
|
fn_callback(obj_data);
|
||||||
@@ -89,6 +117,24 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_ffmpeg_path: () => {
|
||||||
|
return ipcRenderer.invoke("get-ffmpeg-path");
|
||||||
|
},
|
||||||
|
|
||||||
|
set_ffmpeg_path: (str_path) => {
|
||||||
|
return ipcRenderer.invoke("set-ffmpeg-path", str_path);
|
||||||
|
},
|
||||||
|
|
||||||
|
select_ffmpeg_exe: () => {
|
||||||
|
return ipcRenderer.invoke("select-ffmpeg-exe");
|
||||||
|
},
|
||||||
|
|
||||||
|
on_ffmpeg_path_status: (fn_callback) => {
|
||||||
|
ipcRenderer.on("ffmpeg-path-status", (event, obj_data) => {
|
||||||
|
fn_callback(obj_data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
get_notification_config: () => {
|
get_notification_config: () => {
|
||||||
return ipcRenderer.invoke("get-notification-config");
|
return ipcRenderer.invoke("get-notification-config");
|
||||||
},
|
},
|
||||||
@@ -97,6 +143,18 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
return ipcRenderer.invoke("set-notification-config", obj_config);
|
return ipcRenderer.invoke("set-notification-config", obj_config);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_email_config: () => {
|
||||||
|
return ipcRenderer.invoke("get-email-config");
|
||||||
|
},
|
||||||
|
|
||||||
|
set_email_config: (obj_config) => {
|
||||||
|
return ipcRenderer.invoke("set-email-config", obj_config);
|
||||||
|
},
|
||||||
|
|
||||||
|
test_email: (obj_config) => {
|
||||||
|
return ipcRenderer.invoke("test-email", obj_config);
|
||||||
|
},
|
||||||
|
|
||||||
check_for_updates: () => {
|
check_for_updates: () => {
|
||||||
return ipcRenderer.invoke("check-for-updates");
|
return ipcRenderer.invoke("check-for-updates");
|
||||||
},
|
},
|
||||||
|
|||||||
244
src/main/BlenderDaemon.js
Normal file
244
src/main/BlenderDaemon.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
const { spawn } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
const PathResolver = require("./PathResolver.js");
|
||||||
|
|
||||||
|
const STR_MARKER_READY = "DAEMON_READY";
|
||||||
|
const STR_MARKER_DONE = "RENDER_DONE:";
|
||||||
|
const STR_MARKER_ERROR = "RENDER_ERROR:";
|
||||||
|
|
||||||
|
const BlenderDaemon = {
|
||||||
|
_obj_process: null,
|
||||||
|
_is_running: false,
|
||||||
|
_str_blend_path: null,
|
||||||
|
_fn_resolve_ready: null,
|
||||||
|
_fn_reject_ready: null,
|
||||||
|
_fn_resolve_render: null,
|
||||||
|
_fn_reject_render: null,
|
||||||
|
_str_stdout_buffer: "",
|
||||||
|
_fn_on_stdout: null,
|
||||||
|
|
||||||
|
start: (str_blend_path) => {
|
||||||
|
if (BlenderDaemon._is_running) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_script_path = path.join(__dirname, "..", "python", "render_daemon.py");
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
BlenderDaemon._fn_resolve_ready = resolve;
|
||||||
|
BlenderDaemon._fn_reject_ready = reject;
|
||||||
|
BlenderDaemon._str_blend_path = str_blend_path;
|
||||||
|
BlenderDaemon._str_stdout_buffer = "";
|
||||||
|
|
||||||
|
let obj_process = spawn(PathResolver.get_blender_path(), [
|
||||||
|
"-b", str_blend_path,
|
||||||
|
"--python", str_script_path,
|
||||||
|
]);
|
||||||
|
|
||||||
|
BlenderDaemon._obj_process = obj_process;
|
||||||
|
|
||||||
|
obj_process.stdout.on("data", (obj_data) => {
|
||||||
|
BlenderDaemon._on_stdout_data(obj_data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
obj_process.stderr.on("data", (obj_data) => {
|
||||||
|
if (BlenderDaemon._fn_on_stdout) {
|
||||||
|
let str_text = obj_data.toString().trim();
|
||||||
|
if (str_text) {
|
||||||
|
BlenderDaemon._fn_on_stdout(str_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj_process.on("close", (nb_code) => {
|
||||||
|
BlenderDaemon._is_running = false;
|
||||||
|
BlenderDaemon._obj_process = null;
|
||||||
|
|
||||||
|
if (BlenderDaemon._fn_resolve_ready) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_ready;
|
||||||
|
BlenderDaemon._fn_resolve_ready = null;
|
||||||
|
BlenderDaemon._fn_reject_ready = null;
|
||||||
|
fn_reject(new Error("Blender a quitte avant DAEMON_READY (code " + nb_code + ")"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BlenderDaemon._fn_resolve_render) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_render;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
fn_reject({
|
||||||
|
str_message: "Le daemon Blender a crash (code " + nb_code + ")",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj_process.on("error", (obj_err) => {
|
||||||
|
BlenderDaemon._is_running = false;
|
||||||
|
BlenderDaemon._obj_process = null;
|
||||||
|
|
||||||
|
if (BlenderDaemon._fn_resolve_ready) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_ready;
|
||||||
|
BlenderDaemon._fn_resolve_ready = null;
|
||||||
|
BlenderDaemon._fn_reject_ready = null;
|
||||||
|
fn_reject(new Error("Impossible de lancer Blender : " + obj_err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BlenderDaemon._fn_resolve_render) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_render;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
fn_reject({
|
||||||
|
str_message: "Erreur process Blender : " + obj_err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render_frame: (obj_params) => {
|
||||||
|
if (!BlenderDaemon._is_running || !BlenderDaemon._obj_process) {
|
||||||
|
return Promise.reject({
|
||||||
|
str_message: "Le daemon Blender n'est pas demarre.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
BlenderDaemon._fn_resolve_render = resolve;
|
||||||
|
BlenderDaemon._fn_reject_render = reject;
|
||||||
|
BlenderDaemon._fn_on_stdout = obj_params.fn_on_stdout || null;
|
||||||
|
|
||||||
|
let obj_cmd = {
|
||||||
|
str_cmd: "render",
|
||||||
|
str_camera: obj_params.str_camera_name,
|
||||||
|
nb_frame: obj_params.nb_frame,
|
||||||
|
nb_resolution_x: obj_params.nb_resolution_x,
|
||||||
|
nb_resolution_y: obj_params.nb_resolution_y,
|
||||||
|
str_format: obj_params.str_format,
|
||||||
|
str_output_path: obj_params.str_output_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (obj_params.list_collections) {
|
||||||
|
obj_cmd.list_collections = obj_params.list_collections;
|
||||||
|
}
|
||||||
|
if (obj_params.obj_render_settings) {
|
||||||
|
obj_cmd.obj_render_settings = obj_params.obj_render_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_json = JSON.stringify(obj_cmd) + "\n";
|
||||||
|
BlenderDaemon._obj_process.stdin.write(str_json);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: () => {
|
||||||
|
if (!BlenderDaemon._obj_process) {
|
||||||
|
BlenderDaemon._is_running = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj_process_ref = BlenderDaemon._obj_process;
|
||||||
|
|
||||||
|
// Kill immediatement (SIGKILL) car pendant un rendu,
|
||||||
|
// le daemon Python est bloque sur bpy.ops.render.render()
|
||||||
|
// et ne lit pas stdin — le "quit" ne serait traite qu'apres le rendu.
|
||||||
|
try {
|
||||||
|
if (obj_process_ref && !obj_process_ref.killed) {
|
||||||
|
obj_process_ref.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
// process deja mort
|
||||||
|
}
|
||||||
|
|
||||||
|
BlenderDaemon._is_running = false;
|
||||||
|
BlenderDaemon._obj_process = null;
|
||||||
|
BlenderDaemon._fn_resolve_ready = null;
|
||||||
|
BlenderDaemon._fn_reject_ready = null;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
BlenderDaemon._fn_on_stdout = null;
|
||||||
|
BlenderDaemon._str_stdout_buffer = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
is_running: () => {
|
||||||
|
return BlenderDaemon._is_running;
|
||||||
|
},
|
||||||
|
|
||||||
|
_on_stdout_data: (str_data) => {
|
||||||
|
BlenderDaemon._str_stdout_buffer += str_data;
|
||||||
|
|
||||||
|
let list_lines = BlenderDaemon._str_stdout_buffer.split(/\r\n|\n|\r/);
|
||||||
|
BlenderDaemon._str_stdout_buffer = list_lines[list_lines.length - 1];
|
||||||
|
|
||||||
|
for (let nb_i = 0; nb_i < list_lines.length - 1; nb_i++) {
|
||||||
|
let str_line = list_lines[nb_i];
|
||||||
|
BlenderDaemon._handle_stdout_line(str_line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_handle_stdout_line: (str_line) => {
|
||||||
|
if (str_line === STR_MARKER_READY) {
|
||||||
|
BlenderDaemon._is_running = true;
|
||||||
|
if (BlenderDaemon._fn_resolve_ready) {
|
||||||
|
let fn_resolve = BlenderDaemon._fn_resolve_ready;
|
||||||
|
BlenderDaemon._fn_resolve_ready = null;
|
||||||
|
BlenderDaemon._fn_reject_ready = null;
|
||||||
|
fn_resolve();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_line.indexOf(STR_MARKER_DONE) === 0) {
|
||||||
|
let str_json = str_line.substring(STR_MARKER_DONE.length);
|
||||||
|
try {
|
||||||
|
let obj_result = JSON.parse(str_json);
|
||||||
|
if (BlenderDaemon._fn_resolve_render) {
|
||||||
|
let fn_resolve = BlenderDaemon._fn_resolve_render;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
BlenderDaemon._fn_on_stdout = null;
|
||||||
|
fn_resolve({
|
||||||
|
str_rendered_file: obj_result.str_file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
// JSON parse error sur RENDER_DONE — traiter comme erreur
|
||||||
|
if (BlenderDaemon._fn_reject_render) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_render;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
BlenderDaemon._fn_on_stdout = null;
|
||||||
|
fn_reject({ str_message: "JSON invalide dans RENDER_DONE" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_line.indexOf(STR_MARKER_ERROR) === 0) {
|
||||||
|
let str_json = str_line.substring(STR_MARKER_ERROR.length);
|
||||||
|
try {
|
||||||
|
let obj_result = JSON.parse(str_json);
|
||||||
|
if (BlenderDaemon._fn_reject_render) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_render;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
BlenderDaemon._fn_on_stdout = null;
|
||||||
|
fn_reject({ str_message: obj_result.str_error || "Erreur rendu" });
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
if (BlenderDaemon._fn_reject_render) {
|
||||||
|
let fn_reject = BlenderDaemon._fn_reject_render;
|
||||||
|
BlenderDaemon._fn_resolve_render = null;
|
||||||
|
BlenderDaemon._fn_reject_render = null;
|
||||||
|
BlenderDaemon._fn_on_stdout = null;
|
||||||
|
fn_reject({ str_message: str_json });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ligne standard Blender (progression, etc.) — forward au callback
|
||||||
|
if (BlenderDaemon._fn_on_stdout && str_line.trim()) {
|
||||||
|
BlenderDaemon._fn_on_stdout(str_line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = BlenderDaemon;
|
||||||
107
src/main/EmailNotifier.js
Normal file
107
src/main/EmailNotifier.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const nodemailer = require("nodemailer");
|
||||||
|
|
||||||
|
const _SMTP = {
|
||||||
|
str_host: "ssl0.ovh.net",
|
||||||
|
nb_port: 465,
|
||||||
|
str_user: "contact@sorlinv.fr",
|
||||||
|
_k: [0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x70, 0x11, 0x22, 0x33],
|
||||||
|
_d: [0x50, 0x13, 0x7f, 0x7e, 0x6c, 0x05, 0x48, 0x72, 0x11, 0x01],
|
||||||
|
};
|
||||||
|
|
||||||
|
const _decode = () => {
|
||||||
|
let list_r = [];
|
||||||
|
for (let nb_i = 0; nb_i < _SMTP._d.length; nb_i++) {
|
||||||
|
list_r.push(String.fromCharCode(_SMTP._d[nb_i] ^ _SMTP._k[nb_i]));
|
||||||
|
}
|
||||||
|
return list_r.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmailNotifier = {
|
||||||
|
_obj_config: null,
|
||||||
|
_obj_transporter: null,
|
||||||
|
|
||||||
|
set_config: (obj_config) => {
|
||||||
|
EmailNotifier._obj_config = obj_config;
|
||||||
|
EmailNotifier._obj_transporter = null;
|
||||||
|
|
||||||
|
if (!obj_config || !obj_config.is_enabled || !obj_config.str_to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailNotifier._obj_transporter = nodemailer.createTransport({
|
||||||
|
host: _SMTP.str_host,
|
||||||
|
port: _SMTP.nb_port,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: _SMTP.str_user,
|
||||||
|
pass: _decode(),
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
send: (str_subject, str_body) => {
|
||||||
|
if (!EmailNotifier._obj_transporter || !EmailNotifier._obj_config || !EmailNotifier._obj_config.is_enabled) {
|
||||||
|
return Promise.resolve({ is_success: false, str_error: "Email non configure" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj_mail = {
|
||||||
|
from: _SMTP.str_user,
|
||||||
|
to: EmailNotifier._obj_config.str_to,
|
||||||
|
subject: str_subject,
|
||||||
|
text: str_body,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
EmailNotifier._obj_transporter.sendMail(obj_mail, (obj_err) => {
|
||||||
|
if (obj_err) {
|
||||||
|
resolve({ is_success: false, str_error: obj_err.message });
|
||||||
|
} else {
|
||||||
|
resolve({ is_success: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
test: (obj_config) => {
|
||||||
|
let obj_transporter = nodemailer.createTransport({
|
||||||
|
host: _SMTP.str_host,
|
||||||
|
port: _SMTP.nb_port,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: _SMTP.str_user,
|
||||||
|
pass: _decode(),
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let str_to = obj_config.str_to || "";
|
||||||
|
if (!str_to) {
|
||||||
|
return Promise.resolve({ is_success: false, str_error: "Aucun destinataire" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj_mail = {
|
||||||
|
from: _SMTP.str_user,
|
||||||
|
to: str_to,
|
||||||
|
subject: "[Multi Render Blender] Test",
|
||||||
|
text: "Ceci est un email de test. La configuration fonctionne correctement.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
obj_transporter.sendMail(obj_mail, (obj_err) => {
|
||||||
|
obj_transporter.close();
|
||||||
|
if (obj_err) {
|
||||||
|
resolve({ is_success: false, str_error: obj_err.message });
|
||||||
|
} else {
|
||||||
|
resolve({ is_success: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = EmailNotifier;
|
||||||
@@ -5,10 +5,13 @@ const { execFileSync } = require("child_process");
|
|||||||
|
|
||||||
const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender";
|
const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender";
|
||||||
const STR_CONFIG_FILE = "blender_path.json";
|
const STR_CONFIG_FILE = "blender_path.json";
|
||||||
|
const STR_FFMPEG_CONFIG_FILE = "ffmpeg_path.json";
|
||||||
|
|
||||||
const PathResolver = {
|
const PathResolver = {
|
||||||
_str_blender_path: null,
|
_str_blender_path: null,
|
||||||
_is_found: false,
|
_is_found: false,
|
||||||
|
_str_ffmpeg_path: null,
|
||||||
|
_is_ffmpeg_found: false,
|
||||||
|
|
||||||
load_saved_path: () => {
|
load_saved_path: () => {
|
||||||
let str_config_path = PathResolver._get_config_path();
|
let str_config_path = PathResolver._get_config_path();
|
||||||
@@ -85,12 +88,112 @@ const PathResolver = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── FFmpeg ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
load_saved_ffmpeg_path: () => {
|
||||||
|
let str_config_path = PathResolver._get_ffmpeg_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_ffmpeg_path = obj_data.str_path;
|
||||||
|
PathResolver._is_ffmpeg_found = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
console.error("PathResolver: impossible de lire la config ffmpeg :", obj_err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_detected = PathResolver._auto_detect_ffmpeg();
|
||||||
|
if (str_detected) {
|
||||||
|
PathResolver._str_ffmpeg_path = str_detected;
|
||||||
|
PathResolver._is_ffmpeg_found = true;
|
||||||
|
} else {
|
||||||
|
PathResolver._str_ffmpeg_path = "ffmpeg";
|
||||||
|
PathResolver._is_ffmpeg_found = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get_ffmpeg_path: () => {
|
||||||
|
if (!PathResolver._str_ffmpeg_path) {
|
||||||
|
PathResolver.load_saved_ffmpeg_path();
|
||||||
|
}
|
||||||
|
return PathResolver._str_ffmpeg_path;
|
||||||
|
},
|
||||||
|
|
||||||
|
set_ffmpeg_path: (str_path) => {
|
||||||
|
if (!str_path || !fs.existsSync(str_path)) {
|
||||||
|
return { is_success: false, str_error: "Fichier introuvable : " + str_path };
|
||||||
|
}
|
||||||
|
|
||||||
|
PathResolver._str_ffmpeg_path = str_path;
|
||||||
|
PathResolver._is_ffmpeg_found = true;
|
||||||
|
|
||||||
|
let str_config_path = PathResolver._get_ffmpeg_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 ffmpeg :", obj_err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { is_success: true, str_path: str_path };
|
||||||
|
},
|
||||||
|
|
||||||
|
get_ffmpeg_status: () => {
|
||||||
|
return {
|
||||||
|
str_path: PathResolver._str_ffmpeg_path || "ffmpeg",
|
||||||
|
is_found: PathResolver._is_ffmpeg_found,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_auto_detect_ffmpeg: () => {
|
||||||
|
let str_cmd = process.platform === "win32" ? "where" : "which";
|
||||||
|
let str_exe = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let str_result = execFileSync(str_cmd, [str_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) {
|
||||||
|
// not in PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
let LIST_PATHS = [
|
||||||
|
"/usr/bin/ffmpeg",
|
||||||
|
"/usr/local/bin/ffmpeg",
|
||||||
|
"/snap/bin/ffmpeg",
|
||||||
|
];
|
||||||
|
for (let str_p of LIST_PATHS) {
|
||||||
|
if (fs.existsSync(str_p)) {
|
||||||
|
return str_p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
// ── Private ──────────────────────────────────────────────
|
// ── Private ──────────────────────────────────────────────
|
||||||
|
|
||||||
_get_config_path: () => {
|
_get_config_path: () => {
|
||||||
return path.join(app.getPath("userData"), STR_CONFIG_FILE);
|
return path.join(app.getPath("userData"), STR_CONFIG_FILE);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_get_ffmpeg_config_path: () => {
|
||||||
|
return path.join(app.getPath("userData"), STR_FFMPEG_CONFIG_FILE);
|
||||||
|
},
|
||||||
|
|
||||||
_auto_detect_linux: () => {
|
_auto_detect_linux: () => {
|
||||||
let LIST_PATHS = [
|
let LIST_PATHS = [
|
||||||
"/usr/bin/blender",
|
"/usr/bin/blender",
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
const BlenderProcess = require("./BlenderProcess.js");
|
const BlenderProcess = require("./BlenderProcess.js");
|
||||||
|
const BlenderDaemon = require("./BlenderDaemon.js");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
const { Notification } = require("electron");
|
const { Notification } = require("electron");
|
||||||
|
|
||||||
const STR_STATUS_IDLE = "idle";
|
const STR_STATUS_IDLE = "idle";
|
||||||
const STR_STATUS_RUNNING = "running";
|
const STR_STATUS_RUNNING = "running";
|
||||||
const STR_STATUS_PAUSED = "paused";
|
const STR_STATUS_PAUSED = "paused";
|
||||||
const STR_PLACEHOLDER_CONTENT = "RENDERING";
|
const NB_PLACEHOLDER_MAX_SIZE = 512;
|
||||||
const NB_PLACEHOLDER_MAX_SIZE = 64;
|
|
||||||
|
|
||||||
class QueueManager {
|
class QueueManager {
|
||||||
constructor(obj_window) {
|
constructor(obj_window) {
|
||||||
@@ -19,12 +20,21 @@ class QueueManager {
|
|||||||
this.nb_last_render_ms = 0;
|
this.nb_last_render_ms = 0;
|
||||||
this.str_last_image_path = null;
|
this.str_last_image_path = null;
|
||||||
this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
|
this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
|
||||||
|
this.str_hostname = os.hostname();
|
||||||
|
this.nb_total_render_ms = 0;
|
||||||
|
this.nb_completed_renders = 0;
|
||||||
|
this.map_remote_avgs = {};
|
||||||
|
this.obj_email_notifier = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_notification_config(obj_config) {
|
set_notification_config(obj_config) {
|
||||||
this.obj_notification_config = obj_config || { is_notify_each_image: false, is_notify_all_done: true };
|
this.obj_notification_config = obj_config || { is_notify_each_image: false, is_notify_all_done: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_email_notifier(obj_notifier) {
|
||||||
|
this.obj_email_notifier = obj_notifier;
|
||||||
|
}
|
||||||
|
|
||||||
start(obj_config) {
|
start(obj_config) {
|
||||||
if (this.str_status === STR_STATUS_PAUSED) {
|
if (this.str_status === STR_STATUS_PAUSED) {
|
||||||
this.str_status = STR_STATUS_RUNNING;
|
this.str_status = STR_STATUS_RUNNING;
|
||||||
@@ -38,13 +48,31 @@ class QueueManager {
|
|||||||
this.str_status = STR_STATUS_RUNNING;
|
this.str_status = STR_STATUS_RUNNING;
|
||||||
this.nb_last_render_ms = 0;
|
this.nb_last_render_ms = 0;
|
||||||
this.str_last_image_path = null;
|
this.str_last_image_path = null;
|
||||||
|
this.nb_total_render_ms = 0;
|
||||||
|
this.nb_completed_renders = 0;
|
||||||
|
this.map_remote_avgs = {};
|
||||||
this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite";
|
this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite";
|
||||||
this.list_collections = obj_config.list_collections || [];
|
this.list_collections = obj_config.list_collections || [];
|
||||||
this.obj_render_settings = obj_config.obj_render_settings || null;
|
this.obj_render_settings = obj_config.obj_render_settings || null;
|
||||||
|
|
||||||
|
if (this.str_overwrite_mode === "skip") {
|
||||||
|
this._prescan_existing();
|
||||||
|
}
|
||||||
|
|
||||||
this._send_log("File de rendu construite : " + this.list_queue.length + " elements.");
|
this._send_log("File de rendu construite : " + this.list_queue.length + " elements.");
|
||||||
this._send_progress();
|
this._send_progress();
|
||||||
this._process_next();
|
|
||||||
|
this._send_log("Demarrage du daemon Blender...");
|
||||||
|
BlenderDaemon.start(obj_config.str_blend_file)
|
||||||
|
.then(() => {
|
||||||
|
this._send_log("Daemon Blender pret.");
|
||||||
|
this._process_next();
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
this._send_log("ERREUR demarrage daemon : " + (obj_err.message || String(obj_err)));
|
||||||
|
this.str_status = STR_STATUS_IDLE;
|
||||||
|
this._send_progress();
|
||||||
|
});
|
||||||
|
|
||||||
return Promise.resolve({ is_success: true, nb_total: this.list_queue.length });
|
return Promise.resolve({ is_success: true, nb_total: this.list_queue.length });
|
||||||
}
|
}
|
||||||
@@ -59,17 +87,33 @@ class QueueManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
let str_file_to_delete = null;
|
||||||
|
|
||||||
|
if (this.nb_current_index < this.list_queue.length) {
|
||||||
|
let obj_item = this.list_queue[this.nb_current_index];
|
||||||
|
if (obj_item.str_status === "rendering") {
|
||||||
|
obj_item.str_status = "stopped";
|
||||||
|
str_file_to_delete = obj_item.str_expected_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.str_status = STR_STATUS_IDLE;
|
this.str_status = STR_STATUS_IDLE;
|
||||||
|
|
||||||
|
BlenderDaemon.stop();
|
||||||
|
|
||||||
if (this.obj_current_process) {
|
if (this.obj_current_process) {
|
||||||
this.obj_current_process.kill("SIGTERM");
|
this.obj_current_process.kill("SIGTERM");
|
||||||
this.obj_current_process = null;
|
this.obj_current_process = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.nb_current_index < this.list_queue.length) {
|
if (str_file_to_delete) {
|
||||||
let obj_item = this.list_queue[this.nb_current_index];
|
try {
|
||||||
if (obj_item.str_status === "rendering") {
|
if (fs.existsSync(str_file_to_delete)) {
|
||||||
obj_item.str_status = "stopped";
|
fs.unlinkSync(str_file_to_delete);
|
||||||
|
this._send_log("Fichier partiel supprime : " + path.basename(str_file_to_delete));
|
||||||
|
}
|
||||||
|
} catch (obj_del_err) {
|
||||||
|
this._send_log("Erreur suppression fichier partiel : " + obj_del_err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,15 +233,62 @@ class QueueManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_prescan_existing() {
|
||||||
|
let nb_skipped = 0;
|
||||||
|
let nb_remote = 0;
|
||||||
|
|
||||||
|
for (let obj_item of this.list_queue) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(obj_item.str_expected_file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let nb_size = fs.statSync(obj_item.str_expected_file).size;
|
||||||
|
if (nb_size === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
|
||||||
|
obj_item.str_status = "skipped";
|
||||||
|
try {
|
||||||
|
obj_item.str_done_date = fs.statSync(obj_item.str_expected_file).mtime.toISOString();
|
||||||
|
} catch (obj_date_err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
nb_skipped++;
|
||||||
|
} else {
|
||||||
|
obj_item.str_status = "rendering_remote";
|
||||||
|
this._read_placeholder(obj_item);
|
||||||
|
nb_remote++;
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
// Fichier inaccessible, on ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nb_skipped > 0 || nb_remote > 0) {
|
||||||
|
this._send_log("Pre-scan : " + nb_skipped + " fichier(s) existant(s), " + nb_remote + " en cours sur d'autres machines.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_process_next() {
|
_process_next() {
|
||||||
if (this.str_status !== STR_STATUS_RUNNING) {
|
if (this.str_status !== STR_STATUS_RUNNING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch skip : boucle iterative pour eviter un stack overflow recursif
|
this._check_remote_completions();
|
||||||
|
|
||||||
|
// Batch skip : sauter les items deja marques par le prescan ou nouvellement detectes
|
||||||
let nb_skip_count = 0;
|
let nb_skip_count = 0;
|
||||||
while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") {
|
while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") {
|
||||||
let obj_check = this.list_queue[this.nb_current_index];
|
let obj_check = this.list_queue[this.nb_current_index];
|
||||||
|
|
||||||
|
// Item deja marque par le prescan
|
||||||
|
if (obj_check.str_status === "skipped" || obj_check.str_status === "rendering_remote") {
|
||||||
|
this.nb_current_index++;
|
||||||
|
nb_skip_count++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verification fichier pour les items non pre-scannes (apparus entre-temps)
|
||||||
let is_exists = false;
|
let is_exists = false;
|
||||||
let nb_size = 0;
|
let nb_size = 0;
|
||||||
try {
|
try {
|
||||||
@@ -215,7 +306,18 @@ class QueueManager {
|
|||||||
if (nb_size === 0) {
|
if (nb_size === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
obj_check.str_status = "skipped";
|
|
||||||
|
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
|
||||||
|
obj_check.str_status = "skipped";
|
||||||
|
try {
|
||||||
|
obj_check.str_done_date = fs.statSync(obj_check.str_expected_file).mtime.toISOString();
|
||||||
|
} catch (obj_date_err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj_check.str_status = "rendering_remote";
|
||||||
|
this._read_placeholder(obj_check);
|
||||||
|
}
|
||||||
this.nb_current_index++;
|
this.nb_current_index++;
|
||||||
nb_skip_count++;
|
nb_skip_count++;
|
||||||
}
|
}
|
||||||
@@ -227,6 +329,7 @@ class QueueManager {
|
|||||||
|
|
||||||
if (this.nb_current_index >= this.list_queue.length) {
|
if (this.nb_current_index >= this.list_queue.length) {
|
||||||
this.str_status = STR_STATUS_IDLE;
|
this.str_status = STR_STATUS_IDLE;
|
||||||
|
BlenderDaemon.stop();
|
||||||
this._send_log("Tous les rendus sont termines !");
|
this._send_log("Tous les rendus sont termines !");
|
||||||
this._send_event("render-complete", { is_all_done: true });
|
this._send_event("render-complete", { is_all_done: true });
|
||||||
if (this.obj_notification_config.is_notify_all_done) {
|
if (this.obj_notification_config.is_notify_all_done) {
|
||||||
@@ -242,13 +345,26 @@ class QueueManager {
|
|||||||
try {
|
try {
|
||||||
if (fs.existsSync(obj_item.str_expected_file)) {
|
if (fs.existsSync(obj_item.str_expected_file)) {
|
||||||
let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size;
|
let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size;
|
||||||
if (nb_recheck_size > 0) {
|
if (nb_recheck_size > NB_PLACEHOLDER_MAX_SIZE) {
|
||||||
obj_item.str_status = "skipped";
|
obj_item.str_status = "skipped";
|
||||||
|
try {
|
||||||
|
obj_item.str_done_date = fs.statSync(obj_item.str_expected_file).mtime.toISOString();
|
||||||
|
} catch (obj_d_err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)");
|
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)");
|
||||||
this.nb_current_index++;
|
this.nb_current_index++;
|
||||||
this._send_progress();
|
this._send_progress();
|
||||||
this._process_next();
|
this._process_next();
|
||||||
return;
|
return;
|
||||||
|
} else if (nb_recheck_size > 0) {
|
||||||
|
obj_item.str_status = "rendering_remote";
|
||||||
|
this._read_placeholder(obj_item);
|
||||||
|
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (en cours par " + (obj_item.str_remote_hostname || "?") + ")");
|
||||||
|
this.nb_current_index++;
|
||||||
|
this._send_progress();
|
||||||
|
this._process_next();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (obj_recheck_err) {
|
} catch (obj_recheck_err) {
|
||||||
@@ -264,7 +380,7 @@ class QueueManager {
|
|||||||
if (!fs.existsSync(str_dir)) {
|
if (!fs.existsSync(str_dir)) {
|
||||||
fs.mkdirSync(str_dir, { recursive: true });
|
fs.mkdirSync(str_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
fs.writeFileSync(obj_item.str_expected_file, STR_PLACEHOLDER_CONTENT);
|
fs.writeFileSync(obj_item.str_expected_file, this._get_placeholder_content());
|
||||||
} catch (obj_file_err) {
|
} catch (obj_file_err) {
|
||||||
this._send_log("ERREUR creation placeholder : " + obj_file_err.message);
|
this._send_log("ERREUR creation placeholder : " + obj_file_err.message);
|
||||||
}
|
}
|
||||||
@@ -275,22 +391,18 @@ class QueueManager {
|
|||||||
|
|
||||||
let nb_start = Date.now();
|
let nb_start = Date.now();
|
||||||
|
|
||||||
BlenderProcess.render_frame({
|
BlenderDaemon.render_frame({
|
||||||
str_blend_path: obj_item.str_blend_path,
|
|
||||||
str_camera_name: obj_item.str_camera_name,
|
str_camera_name: obj_item.str_camera_name,
|
||||||
nb_frame: obj_item.nb_frame,
|
nb_frame: obj_item.nb_frame,
|
||||||
nb_resolution_x: obj_item.nb_resolution_x,
|
nb_resolution_x: obj_item.nb_resolution_x,
|
||||||
nb_resolution_y: obj_item.nb_resolution_y,
|
nb_resolution_y: obj_item.nb_resolution_y,
|
||||||
str_format: obj_item.str_format,
|
str_format: obj_item.str_format,
|
||||||
str_output_path: obj_item.str_output_path,
|
str_output_path: obj_item.str_expected_file,
|
||||||
list_collections: this.list_collections,
|
list_collections: this.list_collections,
|
||||||
obj_render_settings: this.obj_render_settings,
|
obj_render_settings: this.obj_render_settings,
|
||||||
fn_on_stdout: (str_data) => {
|
fn_on_stdout: (str_data) => {
|
||||||
this._send_log(str_data.trim());
|
this._send_log(str_data.trim());
|
||||||
},
|
},
|
||||||
fn_on_process: (obj_process) => {
|
|
||||||
this.obj_current_process = obj_process;
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then((obj_result) => {
|
.then((obj_result) => {
|
||||||
this.obj_current_process = null;
|
this.obj_current_process = null;
|
||||||
@@ -301,6 +413,9 @@ class QueueManager {
|
|||||||
|
|
||||||
obj_item.str_status = "done";
|
obj_item.str_status = "done";
|
||||||
this.nb_last_render_ms = Date.now() - nb_start;
|
this.nb_last_render_ms = Date.now() - nb_start;
|
||||||
|
this.nb_total_render_ms += this.nb_last_render_ms;
|
||||||
|
this.nb_completed_renders++;
|
||||||
|
obj_item.str_done_date = new Date().toISOString();
|
||||||
|
|
||||||
let str_image = obj_result.str_rendered_file || obj_item.str_expected_file;
|
let str_image = obj_result.str_rendered_file || obj_item.str_expected_file;
|
||||||
this.str_last_image_path = str_image;
|
this.str_last_image_path = str_image;
|
||||||
@@ -347,7 +462,33 @@ class QueueManager {
|
|||||||
|
|
||||||
this.nb_current_index++;
|
this.nb_current_index++;
|
||||||
this._send_progress();
|
this._send_progress();
|
||||||
this._process_next();
|
|
||||||
|
let is_gpu_error = str_err_msg.indexOf("CUDA") !== -1
|
||||||
|
|| str_err_msg.indexOf("GPU memory") !== -1
|
||||||
|
|| str_err_msg.indexOf("out of memory") !== -1
|
||||||
|
|| str_err_msg.indexOf("OpenCL") !== -1
|
||||||
|
|| str_err_msg.indexOf("HIP") !== -1;
|
||||||
|
|
||||||
|
if (!BlenderDaemon.is_running() || is_gpu_error) {
|
||||||
|
if (is_gpu_error) {
|
||||||
|
this._send_log("Erreur GPU detectee, redemarrage du daemon pour contexte CUDA neuf...");
|
||||||
|
BlenderDaemon.stop();
|
||||||
|
} else {
|
||||||
|
this._send_log("Daemon crash detecte, redemarrage...");
|
||||||
|
}
|
||||||
|
BlenderDaemon.start(obj_item.str_blend_path)
|
||||||
|
.then(() => {
|
||||||
|
this._send_log("Daemon redemarre.");
|
||||||
|
this._process_next();
|
||||||
|
})
|
||||||
|
.catch((obj_restart_err) => {
|
||||||
|
this._send_log("ERREUR redemarrage daemon : " + (obj_restart_err.message || String(obj_restart_err)));
|
||||||
|
this.str_status = STR_STATUS_IDLE;
|
||||||
|
this._send_progress();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._process_next();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,17 +497,66 @@ class QueueManager {
|
|||||||
let list_skipped = [];
|
let list_skipped = [];
|
||||||
let list_stopped = [];
|
let list_stopped = [];
|
||||||
let list_skipped_paths = [];
|
let list_skipped_paths = [];
|
||||||
|
let list_rendering_remote = [];
|
||||||
|
let list_item_results = [];
|
||||||
|
|
||||||
|
let nb_avg = this.nb_completed_renders > 0
|
||||||
|
? Math.round(this.nb_total_render_ms / this.nb_completed_renders)
|
||||||
|
: 0;
|
||||||
|
|
||||||
for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) {
|
for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) {
|
||||||
if (this.list_queue[nb_i].str_status === "skipped") {
|
let obj_q = this.list_queue[nb_i];
|
||||||
|
if (obj_q.str_status === "skipped") {
|
||||||
list_skipped.push(nb_i);
|
list_skipped.push(nb_i);
|
||||||
list_skipped_paths.push({ nb_index: nb_i, str_path: this.list_queue[nb_i].str_expected_file });
|
list_skipped_paths.push({ nb_index: nb_i, str_path: obj_q.str_expected_file });
|
||||||
} else if (this.list_queue[nb_i].str_status === "stopped") {
|
list_item_results.push({
|
||||||
|
nb_index: nb_i,
|
||||||
|
str_type: "done",
|
||||||
|
str_date: obj_q.str_done_date || null,
|
||||||
|
str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y,
|
||||||
|
});
|
||||||
|
} else if (obj_q.str_status === "stopped") {
|
||||||
list_stopped.push(nb_i);
|
list_stopped.push(nb_i);
|
||||||
|
} else if (obj_q.str_status === "rendering_remote") {
|
||||||
|
list_rendering_remote.push(nb_i);
|
||||||
|
list_item_results.push({
|
||||||
|
nb_index: nb_i,
|
||||||
|
str_type: "rendering_remote",
|
||||||
|
str_remote_hostname: obj_q.str_remote_hostname || "?",
|
||||||
|
nb_remote_avg_ms: obj_q.nb_remote_avg_ms || 0,
|
||||||
|
});
|
||||||
|
} else if (obj_q.str_status === "done") {
|
||||||
|
list_item_results.push({
|
||||||
|
nb_index: nb_i,
|
||||||
|
str_type: "done",
|
||||||
|
str_date: obj_q.str_done_date || null,
|
||||||
|
str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let list_machine_avgs = [];
|
||||||
|
if (nb_avg > 0) {
|
||||||
|
list_machine_avgs.push({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg });
|
||||||
|
}
|
||||||
|
for (let str_h of Object.keys(this.map_remote_avgs)) {
|
||||||
|
if (this.map_remote_avgs[str_h] > 0) {
|
||||||
|
list_machine_avgs.push({ str_hostname: str_h, nb_avg_ms: this.map_remote_avgs[str_h] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nb_remaining = 0;
|
||||||
|
for (let nb_r = this.nb_current_index; nb_r < this.list_queue.length; nb_r++) {
|
||||||
|
let obj_r = this.list_queue[nb_r];
|
||||||
|
if (obj_r.str_status === "pending" || obj_r.str_status === "rendering") {
|
||||||
|
nb_remaining++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._send_event("render-progress", {
|
this._send_event("render-progress", {
|
||||||
nb_current: this.nb_current_index,
|
nb_current: this.nb_current_index,
|
||||||
nb_total: this.list_queue.length,
|
nb_total: this.list_queue.length,
|
||||||
|
nb_remaining: nb_remaining,
|
||||||
str_camera: obj_item.str_camera_name || "-",
|
str_camera: obj_item.str_camera_name || "-",
|
||||||
nb_frame: obj_item.nb_frame || 0,
|
nb_frame: obj_item.nb_frame || 0,
|
||||||
str_status: this.str_status,
|
str_status: this.str_status,
|
||||||
@@ -375,9 +565,57 @@ class QueueManager {
|
|||||||
list_skipped: list_skipped,
|
list_skipped: list_skipped,
|
||||||
list_stopped: list_stopped,
|
list_stopped: list_stopped,
|
||||||
list_skipped_paths: list_skipped_paths,
|
list_skipped_paths: list_skipped_paths,
|
||||||
|
list_rendering_remote: list_rendering_remote,
|
||||||
|
list_item_results: list_item_results,
|
||||||
|
str_hostname: this.str_hostname,
|
||||||
|
nb_avg_render_ms: nb_avg,
|
||||||
|
list_machine_avgs: list_machine_avgs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_get_placeholder_content() {
|
||||||
|
let nb_avg = this.nb_completed_renders > 0
|
||||||
|
? Math.round(this.nb_total_render_ms / this.nb_completed_renders)
|
||||||
|
: 0;
|
||||||
|
return JSON.stringify({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg });
|
||||||
|
}
|
||||||
|
|
||||||
|
_read_placeholder(obj_item) {
|
||||||
|
try {
|
||||||
|
let str_content = fs.readFileSync(obj_item.str_expected_file, "utf-8");
|
||||||
|
let obj_data = JSON.parse(str_content);
|
||||||
|
obj_item.str_remote_hostname = obj_data.str_hostname || "?";
|
||||||
|
obj_item.nb_remote_avg_ms = obj_data.nb_avg_ms || 0;
|
||||||
|
if (obj_data.str_hostname && obj_data.nb_avg_ms > 0) {
|
||||||
|
this.map_remote_avgs[obj_data.str_hostname] = obj_data.nb_avg_ms;
|
||||||
|
}
|
||||||
|
} catch (obj_parse_err) {
|
||||||
|
obj_item.str_remote_hostname = "?";
|
||||||
|
obj_item.nb_remote_avg_ms = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_check_remote_completions() {
|
||||||
|
for (let obj_q of this.list_queue) {
|
||||||
|
if (obj_q.str_status !== "rendering_remote") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(obj_q.str_expected_file)) {
|
||||||
|
let nb_size = fs.statSync(obj_q.str_expected_file).size;
|
||||||
|
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
|
||||||
|
obj_q.str_status = "skipped";
|
||||||
|
obj_q.str_done_date = fs.statSync(obj_q.str_expected_file).mtime.toISOString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj_q.str_status = "pending";
|
||||||
|
}
|
||||||
|
} catch (obj_check_err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_send_log(str_message) {
|
_send_log(str_message) {
|
||||||
this._send_event("log", str_message);
|
this._send_event("log", str_message);
|
||||||
}
|
}
|
||||||
@@ -393,6 +631,20 @@ class QueueManager {
|
|||||||
let obj_notif = new Notification({ title: str_title, body: str_body });
|
let obj_notif = new Notification({ title: str_title, body: str_body });
|
||||||
obj_notif.show();
|
obj_notif.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.obj_email_notifier) {
|
||||||
|
this.obj_email_notifier.send("[Multi Render] " + str_title, str_body)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
this._send_log("Email envoye : " + str_title);
|
||||||
|
} else if (obj_result.str_error && obj_result.str_error !== "Email non configure") {
|
||||||
|
this._send_log("Erreur email : " + obj_result.str_error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
this._send_log("Erreur email : " + obj_err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
342
src/main/VideoGenerator.js
Normal file
342
src/main/VideoGenerator.js
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
const { spawn } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const PathResolver = require("./PathResolver.js");
|
||||||
|
|
||||||
|
const STR_TEMP_DIR = path.join(os.tmpdir(), "multi_render_blender_preview");
|
||||||
|
const NB_PLACEHOLDER_MAX_SIZE = 512;
|
||||||
|
|
||||||
|
const VideoGenerator = {
|
||||||
|
_obj_process: null,
|
||||||
|
|
||||||
|
generate: (obj_params, fn_on_log) => {
|
||||||
|
let str_output_path = obj_params.str_output_path;
|
||||||
|
let list_cameras = obj_params.list_cameras;
|
||||||
|
let nb_fps = obj_params.nb_fps || 24;
|
||||||
|
let str_output_mode = obj_params.str_output_mode || "subfolder";
|
||||||
|
let str_frame_prefix = obj_params.str_frame_prefix !== undefined ? obj_params.str_frame_prefix : "f_";
|
||||||
|
let nb_frame_padding = obj_params.nb_frame_padding || 5;
|
||||||
|
|
||||||
|
if (!fs.existsSync(STR_TEMP_DIR)) {
|
||||||
|
fs.mkdirSync(STR_TEMP_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_video_path = path.join(STR_TEMP_DIR, "preview.mp4");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(str_video_path)) {
|
||||||
|
fs.unlinkSync(str_video_path);
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
let list_camera_data = VideoGenerator._scan_rendered_files(
|
||||||
|
str_output_path, list_cameras, str_output_mode, str_frame_prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
if (list_camera_data.length === 0) {
|
||||||
|
return Promise.reject(new Error("Aucune image rendue trouvee."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let nb_width = list_camera_data[0].nb_resolution_x;
|
||||||
|
let nb_height = list_camera_data[0].nb_resolution_y;
|
||||||
|
if (nb_width % 2 !== 0) {
|
||||||
|
nb_width++;
|
||||||
|
}
|
||||||
|
if (nb_height % 2 !== 0) {
|
||||||
|
nb_height++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list_segments = [];
|
||||||
|
let nb_segment_index = 0;
|
||||||
|
|
||||||
|
let fn_generate_segments = () => {
|
||||||
|
if (nb_segment_index >= list_camera_data.length) {
|
||||||
|
return VideoGenerator._concat_segments(list_segments, str_video_path, fn_on_log);
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj_cam_data = list_camera_data[nb_segment_index];
|
||||||
|
let str_title_path = path.join(STR_TEMP_DIR, "title_" + nb_segment_index + ".mp4");
|
||||||
|
let str_frames_path = path.join(STR_TEMP_DIR, "frames_" + nb_segment_index + ".mp4");
|
||||||
|
|
||||||
|
fn_on_log("Titre : " + obj_cam_data.str_name);
|
||||||
|
|
||||||
|
return VideoGenerator._generate_title_card(
|
||||||
|
str_title_path, obj_cam_data, nb_width, nb_height, nb_fps
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
fn_on_log("Sequence : " + obj_cam_data.str_name + " (" + obj_cam_data.list_frame_numbers.length + " images)");
|
||||||
|
return VideoGenerator._generate_frames_video(
|
||||||
|
str_frames_path, obj_cam_data, nb_width, nb_height, nb_fps
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
list_segments.push(str_title_path);
|
||||||
|
list_segments.push(str_frames_path);
|
||||||
|
nb_segment_index++;
|
||||||
|
return fn_generate_segments();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return fn_generate_segments()
|
||||||
|
.then(() => {
|
||||||
|
for (let str_seg of list_segments) {
|
||||||
|
try { fs.unlinkSync(str_seg); } catch (obj_e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
let str_concat_file = path.join(STR_TEMP_DIR, "concat.txt");
|
||||||
|
try { if (fs.existsSync(str_concat_file)) { fs.unlinkSync(str_concat_file); } } catch (obj_e) { /* ignore */ }
|
||||||
|
let str_frames_list = path.join(STR_TEMP_DIR, "frames_list.txt");
|
||||||
|
try { if (fs.existsSync(str_frames_list)) { fs.unlinkSync(str_frames_list); } } catch (obj_e) { /* ignore */ }
|
||||||
|
|
||||||
|
return { str_video_path: str_video_path };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_extract_frame_number: (str_filepath, str_file_pattern) => {
|
||||||
|
let str_basename = path.basename(str_filepath);
|
||||||
|
let str_after = str_basename.substring(str_file_pattern.length);
|
||||||
|
let str_num = str_after.replace(/\.[^.]+$/, "");
|
||||||
|
let nb_frame = parseInt(str_num, 10);
|
||||||
|
return isNaN(nb_frame) ? -1 : nb_frame;
|
||||||
|
},
|
||||||
|
|
||||||
|
_scan_rendered_files: (str_output_path, list_cameras, str_output_mode, str_frame_prefix) => {
|
||||||
|
let list_camera_data = [];
|
||||||
|
|
||||||
|
for (let obj_cam of list_cameras) {
|
||||||
|
if (!obj_cam.is_enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_cam_dir = "";
|
||||||
|
let str_file_pattern = "";
|
||||||
|
|
||||||
|
if (str_output_mode === "prefix") {
|
||||||
|
str_cam_dir = str_output_path;
|
||||||
|
str_file_pattern = obj_cam.str_name + "_" + str_frame_prefix;
|
||||||
|
} else if (str_output_mode === "both") {
|
||||||
|
str_cam_dir = path.join(str_output_path, obj_cam.str_name);
|
||||||
|
str_file_pattern = obj_cam.str_name + "_" + str_frame_prefix;
|
||||||
|
} else {
|
||||||
|
str_cam_dir = path.join(str_output_path, obj_cam.str_name);
|
||||||
|
str_file_pattern = str_frame_prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(str_cam_dir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list_files = fs.readdirSync(str_cam_dir);
|
||||||
|
let map_frame_to_file = {};
|
||||||
|
|
||||||
|
for (let str_file of list_files) {
|
||||||
|
if (!str_file.startsWith(str_file_pattern)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let str_full = path.join(str_cam_dir, str_file);
|
||||||
|
try {
|
||||||
|
let nb_size = fs.statSync(str_full).size;
|
||||||
|
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
|
||||||
|
let nb_frame = VideoGenerator._extract_frame_number(str_full, str_file_pattern);
|
||||||
|
if (nb_frame >= 0) {
|
||||||
|
map_frame_to_file[nb_frame] = str_full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (obj_err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let list_frame_numbers = Object.keys(map_frame_to_file).map(Number);
|
||||||
|
list_frame_numbers.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
if (list_frame_numbers.length > 0) {
|
||||||
|
list_camera_data.push({
|
||||||
|
str_name: obj_cam.str_name,
|
||||||
|
nb_frame_start: obj_cam.nb_frame_start,
|
||||||
|
nb_frame_end: obj_cam.nb_frame_end,
|
||||||
|
nb_resolution_x: obj_cam.nb_resolution_x,
|
||||||
|
nb_resolution_y: obj_cam.nb_resolution_y,
|
||||||
|
map_frame_to_file: map_frame_to_file,
|
||||||
|
list_frame_numbers: list_frame_numbers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list_camera_data;
|
||||||
|
},
|
||||||
|
|
||||||
|
_find_font: () => {
|
||||||
|
let list_paths = [
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||||
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\arial.ttf",
|
||||||
|
"/System/Library/Fonts/Helvetica.ttc",
|
||||||
|
];
|
||||||
|
for (let str_p of list_paths) {
|
||||||
|
if (fs.existsSync(str_p)) {
|
||||||
|
return str_p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_escape_drawtext: (str_text) => {
|
||||||
|
return str_text
|
||||||
|
.replace(/\\/g, "\\\\")
|
||||||
|
.replace(/'/g, "\u2019")
|
||||||
|
.replace(/:/g, "\\:")
|
||||||
|
.replace(/;/g, "\\;")
|
||||||
|
.replace(/\[/g, "\\[")
|
||||||
|
.replace(/\]/g, "\\]");
|
||||||
|
},
|
||||||
|
|
||||||
|
_generate_title_card: (str_output, obj_cam_data, nb_width, nb_height, nb_fps) => {
|
||||||
|
let str_font = VideoGenerator._find_font();
|
||||||
|
let str_font_prefix = str_font ? "fontfile=" + str_font.replace(/:/g, "\\:") + ":" : "";
|
||||||
|
|
||||||
|
let str_cam_name = VideoGenerator._escape_drawtext(obj_cam_data.str_name);
|
||||||
|
let str_frame_range = VideoGenerator._escape_drawtext(
|
||||||
|
"Frames " + obj_cam_data.nb_frame_start + " - " + obj_cam_data.nb_frame_end
|
||||||
|
);
|
||||||
|
let str_resolution = VideoGenerator._escape_drawtext(
|
||||||
|
obj_cam_data.nb_resolution_x + " x " + obj_cam_data.nb_resolution_y
|
||||||
|
);
|
||||||
|
let str_nb_images = VideoGenerator._escape_drawtext(
|
||||||
|
obj_cam_data.list_frame_numbers.length + " images rendues"
|
||||||
|
);
|
||||||
|
|
||||||
|
let str_filter = "drawtext=" + str_font_prefix + "text='" + str_cam_name
|
||||||
|
+ "':fontcolor=white:fontsize=60:x=(w-tw)/2:y=(h/2)-80"
|
||||||
|
+ ",drawtext=" + str_font_prefix + "text='" + str_frame_range
|
||||||
|
+ "':fontcolor=0xaaaaaa:fontsize=36:x=(w-tw)/2:y=(h/2)"
|
||||||
|
+ ",drawtext=" + str_font_prefix + "text='" + str_resolution
|
||||||
|
+ "':fontcolor=0x888888:fontsize=28:x=(w-tw)/2:y=(h/2)+60"
|
||||||
|
+ ",drawtext=" + str_font_prefix + "text='" + str_nb_images
|
||||||
|
+ "':fontcolor=0x888888:fontsize=28:x=(w-tw)/2:y=(h/2)+100";
|
||||||
|
|
||||||
|
let list_args = [
|
||||||
|
"-f", "lavfi",
|
||||||
|
"-i", "color=c=0x1a1a2e:s=" + nb_width + "x" + nb_height + ":d=1:r=" + nb_fps,
|
||||||
|
"-vf", str_filter,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-t", "1",
|
||||||
|
"-y",
|
||||||
|
str_output,
|
||||||
|
];
|
||||||
|
|
||||||
|
return VideoGenerator._run_ffmpeg(list_args);
|
||||||
|
},
|
||||||
|
|
||||||
|
_generate_frames_video: (str_output, obj_cam_data, nb_width, nb_height, nb_fps) => {
|
||||||
|
let str_concat_path = path.join(STR_TEMP_DIR, "frames_list.txt");
|
||||||
|
let nb_frame_duration = 1 / nb_fps;
|
||||||
|
let str_content = "";
|
||||||
|
|
||||||
|
let list_frame_numbers = obj_cam_data.list_frame_numbers;
|
||||||
|
let map_frame_to_file = obj_cam_data.map_frame_to_file;
|
||||||
|
let nb_first = list_frame_numbers[0];
|
||||||
|
let nb_last = list_frame_numbers[list_frame_numbers.length - 1];
|
||||||
|
|
||||||
|
let str_last_file = map_frame_to_file[nb_first];
|
||||||
|
for (let nb_f = nb_first; nb_f <= nb_last; nb_f++) {
|
||||||
|
if (map_frame_to_file[nb_f]) {
|
||||||
|
str_last_file = map_frame_to_file[nb_f];
|
||||||
|
}
|
||||||
|
let str_safe = str_last_file.replace(/'/g, "'\\''");
|
||||||
|
str_content += "file '" + str_safe + "'\n";
|
||||||
|
str_content += "duration " + nb_frame_duration.toFixed(6) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_safe_last = str_last_file.replace(/'/g, "'\\''");
|
||||||
|
str_content += "file '" + str_safe_last + "'\n";
|
||||||
|
|
||||||
|
fs.writeFileSync(str_concat_path, str_content, "utf8");
|
||||||
|
|
||||||
|
let str_scale = "scale=" + nb_width + ":" + nb_height
|
||||||
|
+ ":force_original_aspect_ratio=decrease"
|
||||||
|
+ ",pad=" + nb_width + ":" + nb_height + ":(ow-iw)/2:(oh-ih)/2:color=black";
|
||||||
|
|
||||||
|
let list_args = [
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", str_concat_path,
|
||||||
|
"-vf", str_scale,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-r", String(nb_fps),
|
||||||
|
"-y",
|
||||||
|
str_output,
|
||||||
|
];
|
||||||
|
|
||||||
|
return VideoGenerator._run_ffmpeg(list_args);
|
||||||
|
},
|
||||||
|
|
||||||
|
_concat_segments: (list_segments, str_output, fn_on_log) => {
|
||||||
|
let str_concat_path = path.join(STR_TEMP_DIR, "concat.txt");
|
||||||
|
let str_content = "";
|
||||||
|
|
||||||
|
for (let str_seg of list_segments) {
|
||||||
|
let str_safe = str_seg.replace(/'/g, "'\\''");
|
||||||
|
str_content += "file '" + str_safe + "'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(str_concat_path, str_content, "utf8");
|
||||||
|
fn_on_log("Concatenation des segments...");
|
||||||
|
|
||||||
|
let list_args = [
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", str_concat_path,
|
||||||
|
"-c", "copy",
|
||||||
|
"-y",
|
||||||
|
str_output,
|
||||||
|
];
|
||||||
|
|
||||||
|
return VideoGenerator._run_ffmpeg(list_args);
|
||||||
|
},
|
||||||
|
|
||||||
|
_run_ffmpeg: (list_args) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let obj_process = spawn(PathResolver.get_ffmpeg_path(), list_args);
|
||||||
|
let str_stderr = "";
|
||||||
|
|
||||||
|
VideoGenerator._obj_process = obj_process;
|
||||||
|
|
||||||
|
obj_process.stderr.on("data", (obj_data) => {
|
||||||
|
str_stderr += obj_data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
obj_process.on("close", (nb_code) => {
|
||||||
|
VideoGenerator._obj_process = null;
|
||||||
|
if (nb_code !== 0) {
|
||||||
|
reject(new Error("ffmpeg erreur (code " + nb_code + "): " + str_stderr.slice(-500)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
obj_process.on("error", (obj_err) => {
|
||||||
|
VideoGenerator._obj_process = null;
|
||||||
|
reject(new Error("Impossible de lancer ffmpeg : " + obj_err.message));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel: () => {
|
||||||
|
if (VideoGenerator._obj_process) {
|
||||||
|
try { VideoGenerator._obj_process.kill("SIGTERM"); } catch (obj_err) { /* ignore */ }
|
||||||
|
VideoGenerator._obj_process = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = VideoGenerator;
|
||||||
142
src/python/render_daemon.py
Normal file
142
src/python/render_daemon.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"""Daemon de rendu persistant pour Blender.
|
||||||
|
|
||||||
|
Ce script est execute par Blender en mode background (-b) et reste actif.
|
||||||
|
Il lit des commandes JSON depuis stdin et execute les rendus demandes.
|
||||||
|
Le fichier .blend est charge une seule fois au demarrage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="[render_daemon] %(message)s")
|
||||||
|
obj_logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_render_settings(obj_scene: object, obj_settings: dict) -> None:
|
||||||
|
"""Applique les parametres de rendu a la scene."""
|
||||||
|
if not obj_settings:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "str_engine" in obj_settings:
|
||||||
|
obj_scene.render.engine = obj_settings["str_engine"]
|
||||||
|
|
||||||
|
if "nb_resolution_percentage" in obj_settings:
|
||||||
|
obj_scene.render.resolution_percentage = obj_settings["nb_resolution_percentage"]
|
||||||
|
|
||||||
|
if "is_film_transparent" in obj_settings:
|
||||||
|
obj_scene.render.film_transparent = obj_settings["is_film_transparent"]
|
||||||
|
|
||||||
|
str_engine: str = obj_scene.render.engine
|
||||||
|
|
||||||
|
if str_engine == "CYCLES":
|
||||||
|
if "nb_samples" in obj_settings:
|
||||||
|
obj_scene.cycles.samples = obj_settings["nb_samples"]
|
||||||
|
if "str_device" in obj_settings:
|
||||||
|
obj_scene.cycles.device = obj_settings["str_device"]
|
||||||
|
if "is_denoise" in obj_settings:
|
||||||
|
obj_scene.cycles.use_denoising = obj_settings["is_denoise"]
|
||||||
|
elif str_engine in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
|
||||||
|
if "nb_samples" in obj_settings:
|
||||||
|
try:
|
||||||
|
obj_scene.eevee.taa_render_samples = obj_settings["nb_samples"]
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
obj_scene.eevee.samples = obj_settings["nb_samples"]
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def apply_collections(list_collections: list) -> None:
|
||||||
|
"""Applique la visibilite des collections pour le rendu."""
|
||||||
|
if not list_collections:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _set_layer_collection(obj_lc: object, str_name: str, is_val: bool) -> None:
|
||||||
|
"""Parcourt recursivement les layer collections pour exclure/inclure."""
|
||||||
|
for obj_child in obj_lc.children:
|
||||||
|
if obj_child.name == str_name:
|
||||||
|
obj_child.exclude = is_val
|
||||||
|
return
|
||||||
|
_set_layer_collection(obj_child, str_name, is_val)
|
||||||
|
|
||||||
|
obj_view_layer_collection = bpy.context.view_layer.layer_collection
|
||||||
|
|
||||||
|
for obj_col in list_collections:
|
||||||
|
str_name: str = obj_col["str_name"]
|
||||||
|
is_hide: bool = obj_col["is_hide_render"]
|
||||||
|
bpy.data.collections[str_name].hide_render = is_hide
|
||||||
|
_set_layer_collection(obj_view_layer_collection, str_name, is_hide)
|
||||||
|
|
||||||
|
|
||||||
|
def process_render(obj_cmd: dict) -> None:
|
||||||
|
"""Execute le rendu d'une frame selon la commande recue."""
|
||||||
|
obj_scene = bpy.context.scene
|
||||||
|
|
||||||
|
str_camera: str = obj_cmd["str_camera"]
|
||||||
|
nb_frame: int = obj_cmd["nb_frame"]
|
||||||
|
nb_resolution_x: int = obj_cmd["nb_resolution_x"]
|
||||||
|
nb_resolution_y: int = obj_cmd["nb_resolution_y"]
|
||||||
|
str_format: str = obj_cmd["str_format"]
|
||||||
|
str_output_path: str = obj_cmd["str_output_path"]
|
||||||
|
|
||||||
|
obj_scene.camera = bpy.data.objects[str_camera]
|
||||||
|
obj_scene.render.resolution_x = nb_resolution_x
|
||||||
|
obj_scene.render.resolution_y = nb_resolution_y
|
||||||
|
obj_scene.render.image_settings.file_format = str_format
|
||||||
|
obj_scene.frame_set(nb_frame)
|
||||||
|
|
||||||
|
str_dir: str = os.path.dirname(str_output_path)
|
||||||
|
if str_dir and not os.path.exists(str_dir):
|
||||||
|
os.makedirs(str_dir, exist_ok=True)
|
||||||
|
|
||||||
|
obj_scene.render.filepath = str_output_path
|
||||||
|
|
||||||
|
apply_render_settings(obj_scene, obj_cmd.get("obj_render_settings"))
|
||||||
|
apply_collections(obj_cmd.get("list_collections"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
str_result: str = json.dumps({"str_file": str_output_path})
|
||||||
|
sys.stdout.write("RENDER_DONE:" + str_result + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as obj_err:
|
||||||
|
str_error: str = json.dumps({"str_error": str(obj_err)})
|
||||||
|
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
obj_logger.error("Erreur rendu : %s", str(obj_err))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Boucle principale du daemon de rendu."""
|
||||||
|
sys.stdout.write("DAEMON_READY\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
for str_line in sys.stdin:
|
||||||
|
str_line = str_line.strip()
|
||||||
|
if not str_line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj_cmd: dict = json.loads(str_line)
|
||||||
|
except json.JSONDecodeError as obj_err:
|
||||||
|
str_error: str = json.dumps({"str_error": "JSON invalide : " + str(obj_err)})
|
||||||
|
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
continue
|
||||||
|
|
||||||
|
str_cmd: str = obj_cmd.get("str_cmd", "")
|
||||||
|
|
||||||
|
if str_cmd == "quit":
|
||||||
|
break
|
||||||
|
elif str_cmd == "render":
|
||||||
|
process_render(obj_cmd)
|
||||||
|
else:
|
||||||
|
str_error = json.dumps({"str_error": "Commande inconnue : " + str_cmd})
|
||||||
|
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<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:;">
|
<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:; media-src 'self' file: data:;">
|
||||||
<title>Multi Render Blender</title>
|
<title>Multi Render Blender</title>
|
||||||
|
|
||||||
<!-- Bootstrap 5 -->
|
<!-- Bootstrap 5 -->
|
||||||
@@ -229,8 +229,9 @@
|
|||||||
<small id="label_progress_status">En attente</small>
|
<small id="label_progress_status">En attente</small>
|
||||||
<small id="label_progress_count">0 / 0</small>
|
<small id="label_progress_count">0 / 0</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress bg-secondary" style="height: 8px;">
|
<div class="progress bg-secondary position-relative" style="height: 20px;">
|
||||||
<div id="bar_progress" class="progress-bar bg-primary" role="progressbar" style="width: 0%;"></div>
|
<div id="bar_progress" class="progress-bar bg-primary" role="progressbar" style="width: 0%;"></div>
|
||||||
|
<small id="label_progress_time" class="position-absolute w-100 text-center" style="line-height: 20px; font-size: 0.7rem; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.9);"></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
@@ -264,8 +265,24 @@
|
|||||||
<div class="col-6 d-flex flex-column">
|
<div class="col-6 d-flex flex-column">
|
||||||
<!-- Preview -->
|
<!-- Preview -->
|
||||||
<div class="card bg-dark border-secondary flex-grow-1">
|
<div class="card bg-dark border-secondary flex-grow-1">
|
||||||
<div class="card-header border-secondary">
|
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||||
<i class="mdi mdi-image-outline me-1"></i>Preview
|
<span><i class="mdi mdi-image-outline me-1"></i>Preview</span>
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<div id="container_ffmpeg_progress" class="d-none d-flex align-items-center gap-1">
|
||||||
|
<div class="progress bg-secondary position-relative" style="height: 14px; width: 120px;">
|
||||||
|
<div id="bar_ffmpeg_progress" class="progress-bar progress-bar-striped progress-bar-animated bg-info" role="progressbar" style="width: 100%;"></div>
|
||||||
|
<small id="label_ffmpeg_time" class="position-absolute w-100 text-center" style="line-height: 14px; font-size: 0.6rem; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.9);"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select id="select_video_fps" class="form-select form-select-sm bg-dark text-light border-secondary py-0" style="width: 70px; font-size: 0.7rem;">
|
||||||
|
<option value="24">24 fps</option>
|
||||||
|
<option value="30" selected>30 fps</option>
|
||||||
|
<option value="60">60 fps</option>
|
||||||
|
</select>
|
||||||
|
<button id="btn_generate_video" class="btn btn-sm btn-outline-info py-0 px-1" title="Generer une video preview (ffmpeg)">
|
||||||
|
<i class="mdi mdi-video-outline"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-2 text-center" id="container_preview">
|
<div class="card-body p-2 text-center" id="container_preview">
|
||||||
<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 200px;">
|
<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 200px;">
|
||||||
@@ -296,6 +313,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Preview Modal -->
|
||||||
|
<div class="modal fade" id="modal_video_preview" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
|
<div class="modal-content bg-dark border-secondary">
|
||||||
|
<div class="modal-header border-secondary py-2">
|
||||||
|
<h6 class="modal-title text-light mb-0">
|
||||||
|
<i class="mdi mdi-video-outline me-1"></i>Preview Video
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0 text-center bg-black">
|
||||||
|
<video id="video_preview_player" controls style="max-width: 100%; max-height: 75vh;"></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bootstrap JS -->
|
<!-- Bootstrap JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
@@ -310,6 +344,7 @@
|
|||||||
<script src="scripts/ProgressBar.js"></script>
|
<script src="scripts/ProgressBar.js"></script>
|
||||||
<script src="scripts/UpdateBanner.js"></script>
|
<script src="scripts/UpdateBanner.js"></script>
|
||||||
<script src="scripts/BlenderPath.js"></script>
|
<script src="scripts/BlenderPath.js"></script>
|
||||||
|
<script src="scripts/FfmpegPath.js"></script>
|
||||||
<script src="scripts/NotificationConfig.js"></script>
|
<script src="scripts/NotificationConfig.js"></script>
|
||||||
<script src="scripts/App.js"></script>
|
<script src="scripts/App.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const App = {
|
|||||||
ProgressBar.init();
|
ProgressBar.init();
|
||||||
UpdateBanner.init();
|
UpdateBanner.init();
|
||||||
BlenderPath.init();
|
BlenderPath.init();
|
||||||
|
FfmpegPath.init();
|
||||||
NotificationConfig.init();
|
NotificationConfig.init();
|
||||||
|
|
||||||
App._bind_events();
|
App._bind_events();
|
||||||
|
|||||||
@@ -36,9 +36,14 @@ const BlenderPath = {
|
|||||||
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
|
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="modal-body">' +
|
'<div class="modal-body">' +
|
||||||
'<p class="text-light-emphasis mb-3" style="font-size: 0.85rem;">' +
|
'<p class="text-light-emphasis mb-2" style="font-size: 0.85rem;">' +
|
||||||
'Selectionnez l\'executable Blender sur votre machine.' +
|
'Selectionnez l\'executable Blender sur votre machine.' +
|
||||||
'</p>' +
|
'</p>' +
|
||||||
|
'<p class="mb-3" style="font-size: 0.8rem;">' +
|
||||||
|
'<a href="https://www.blender.org/download/" target="_blank" class="text-info">' +
|
||||||
|
'<i class="mdi mdi-download me-1"></i>Telecharger Blender' +
|
||||||
|
'</a>' +
|
||||||
|
'</p>' +
|
||||||
'<div class="input-group input-group-sm mb-3">' +
|
'<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>' +
|
'<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">' +
|
'<button id="btn_browse_blender" class="btn btn-outline-primary" type="button">' +
|
||||||
|
|||||||
198
src/renderer/scripts/FfmpegPath.js
Normal file
198
src/renderer/scripts/FfmpegPath.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
const FfmpegPath = {
|
||||||
|
str_current_path: null,
|
||||||
|
is_found: false,
|
||||||
|
obj_modal: null,
|
||||||
|
|
||||||
|
init: () => {
|
||||||
|
FfmpegPath._create_badge();
|
||||||
|
FfmpegPath._create_modal();
|
||||||
|
FfmpegPath._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_ffmpeg_status";
|
||||||
|
obj_badge.className = "btn btn-sm btn-outline-secondary";
|
||||||
|
obj_badge.title = "Chemin FFmpeg";
|
||||||
|
obj_badge.innerHTML = '<i class="mdi mdi-video-outline"></i>';
|
||||||
|
obj_nav_right.insertBefore(obj_badge, obj_nav_right.firstChild);
|
||||||
|
|
||||||
|
obj_badge.addEventListener("click", () => {
|
||||||
|
FfmpegPath._open_modal();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_create_modal: () => {
|
||||||
|
let obj_modal_el = document.createElement("div");
|
||||||
|
obj_modal_el.id = "modal_ffmpeg_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-video-outline me-2"></i>Chemin FFmpeg</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-2" style="font-size: 0.85rem;">' +
|
||||||
|
'Selectionnez l\'executable FFmpeg sur votre machine.' +
|
||||||
|
'</p>' +
|
||||||
|
'<p class="mb-3" style="font-size: 0.8rem;">' +
|
||||||
|
'<a href="https://ffmpeg.org/download.html" target="_blank" class="text-info">' +
|
||||||
|
'<i class="mdi mdi-download me-1"></i>Telecharger FFmpeg' +
|
||||||
|
'</a>' +
|
||||||
|
'</p>' +
|
||||||
|
'<div class="input-group input-group-sm mb-3">' +
|
||||||
|
'<input type="text" id="input_ffmpeg_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun chemin configure" readonly>' +
|
||||||
|
'<button id="btn_browse_ffmpeg" 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_ffmpeg" class="btn btn-sm btn-outline-secondary">' +
|
||||||
|
'<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement' +
|
||||||
|
'</button>' +
|
||||||
|
'<span id="label_ffmpeg_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_ffmpeg" 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);
|
||||||
|
FfmpegPath.obj_modal = new bootstrap.Modal(obj_modal_el);
|
||||||
|
},
|
||||||
|
|
||||||
|
_bind_events: () => {
|
||||||
|
let obj_btn_browse = document.getElementById("btn_browse_ffmpeg");
|
||||||
|
obj_btn_browse.addEventListener("click", () => {
|
||||||
|
FfmpegPath._browse();
|
||||||
|
});
|
||||||
|
|
||||||
|
let obj_btn_detect = document.getElementById("btn_detect_ffmpeg");
|
||||||
|
obj_btn_detect.addEventListener("click", () => {
|
||||||
|
FfmpegPath._detect();
|
||||||
|
});
|
||||||
|
|
||||||
|
let obj_btn_validate = document.getElementById("btn_validate_ffmpeg");
|
||||||
|
obj_btn_validate.addEventListener("click", () => {
|
||||||
|
FfmpegPath._validate();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.api.on_ffmpeg_path_status((obj_data) => {
|
||||||
|
FfmpegPath.str_current_path = obj_data.str_path;
|
||||||
|
FfmpegPath.is_found = obj_data.is_found;
|
||||||
|
FfmpegPath._update_badge();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_open_modal: () => {
|
||||||
|
let obj_input = document.getElementById("input_ffmpeg_path");
|
||||||
|
obj_input.value = FfmpegPath.is_found ? FfmpegPath.str_current_path : "";
|
||||||
|
FfmpegPath._update_modal_status(FfmpegPath.is_found, FfmpegPath.str_current_path);
|
||||||
|
FfmpegPath.obj_modal.show();
|
||||||
|
|
||||||
|
if (!FfmpegPath.is_found) {
|
||||||
|
FfmpegPath._detect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_browse: () => {
|
||||||
|
window.api.select_ffmpeg_exe()
|
||||||
|
.then((str_path) => {
|
||||||
|
if (!str_path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let obj_input = document.getElementById("input_ffmpeg_path");
|
||||||
|
obj_input.value = str_path;
|
||||||
|
FfmpegPath._update_modal_status(true, str_path);
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur selection FFmpeg : " + obj_err.message);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_detect: () => {
|
||||||
|
let obj_btn = document.getElementById("btn_detect_ffmpeg");
|
||||||
|
obj_btn.disabled = true;
|
||||||
|
obj_btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Detection...';
|
||||||
|
|
||||||
|
window.api.get_ffmpeg_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_ffmpeg_path");
|
||||||
|
obj_input.value = obj_data.str_path;
|
||||||
|
FfmpegPath._update_modal_status(true, obj_data.str_path);
|
||||||
|
} else {
|
||||||
|
FfmpegPath._update_modal_status(false, null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
obj_btn.disabled = false;
|
||||||
|
obj_btn.innerHTML = '<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement';
|
||||||
|
FfmpegPath._update_modal_status(false, null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_validate: () => {
|
||||||
|
let str_path = document.getElementById("input_ffmpeg_path").value;
|
||||||
|
if (!str_path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.api.set_ffmpeg_path(str_path)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
FfmpegPath.str_current_path = obj_result.str_path;
|
||||||
|
FfmpegPath.is_found = true;
|
||||||
|
FfmpegPath._update_badge();
|
||||||
|
FfmpegPath.obj_modal.hide();
|
||||||
|
ConsoleLog.add("Chemin FFmpeg configure : " + obj_result.str_path);
|
||||||
|
} else {
|
||||||
|
FfmpegPath._update_modal_status(false, null);
|
||||||
|
ConsoleLog.add("Chemin FFmpeg invalide : " + (obj_result.str_error || ""));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur configuration FFmpeg : " + obj_err.message);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_update_badge: () => {
|
||||||
|
let obj_badge = document.getElementById("btn_ffmpeg_status");
|
||||||
|
if (FfmpegPath.is_found) {
|
||||||
|
obj_badge.className = "btn btn-sm btn-outline-success";
|
||||||
|
obj_badge.title = "FFmpeg : " + FfmpegPath.str_current_path;
|
||||||
|
} else {
|
||||||
|
obj_badge.className = "btn btn-sm btn-outline-danger";
|
||||||
|
obj_badge.title = "FFmpeg non trouve";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_update_modal_status: (is_valid, str_path) => {
|
||||||
|
let obj_label = document.getElementById("label_ffmpeg_status");
|
||||||
|
let obj_btn_validate = document.getElementById("btn_validate_ffmpeg");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const NotificationConfig = {
|
const NotificationConfig = {
|
||||||
is_notify_each_image: false,
|
is_notify_each_image: false,
|
||||||
is_notify_all_done: true,
|
is_notify_all_done: true,
|
||||||
|
obj_email_config: null,
|
||||||
obj_modal: null,
|
obj_modal: null,
|
||||||
|
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -8,6 +9,7 @@ const NotificationConfig = {
|
|||||||
NotificationConfig._create_modal();
|
NotificationConfig._create_modal();
|
||||||
NotificationConfig._bind_events();
|
NotificationConfig._bind_events();
|
||||||
NotificationConfig._load_config();
|
NotificationConfig._load_config();
|
||||||
|
NotificationConfig._load_email_config();
|
||||||
},
|
},
|
||||||
|
|
||||||
_create_badge: () => {
|
_create_badge: () => {
|
||||||
@@ -35,28 +37,55 @@ const NotificationConfig = {
|
|||||||
obj_modal_el.className = "modal fade";
|
obj_modal_el.className = "modal fade";
|
||||||
obj_modal_el.tabIndex = -1;
|
obj_modal_el.tabIndex = -1;
|
||||||
obj_modal_el.innerHTML =
|
obj_modal_el.innerHTML =
|
||||||
'<div class="modal-dialog modal-dialog-centered modal-sm">' +
|
'<div class="modal-dialog modal-dialog-centered">' +
|
||||||
'<div class="modal-content bg-dark text-light border-secondary">' +
|
'<div class="modal-content bg-dark text-light border-secondary">' +
|
||||||
'<div class="modal-header border-secondary">' +
|
'<div class="modal-header border-secondary">' +
|
||||||
'<h6 class="modal-title"><i class="mdi mdi-bell-outline me-2"></i>Notifications</h6>' +
|
'<h6 class="modal-title"><i class="mdi mdi-bell-outline me-2"></i>Notifications</h6>' +
|
||||||
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
|
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="modal-body">' +
|
'<div class="modal-body">' +
|
||||||
'<p class="text-light-emphasis mb-3" style="font-size: 0.85rem;">' +
|
|
||||||
'Configurez les notifications systeme.' +
|
'<p class="text-light-emphasis mb-2" style="font-size: 0.8rem; font-weight: 600;">Notifications systeme</p>' +
|
||||||
'</p>' +
|
|
||||||
'<div class="form-check mb-2">' +
|
'<div class="form-check mb-2">' +
|
||||||
'<input class="form-check-input" type="checkbox" id="check_notify_each_image">' +
|
'<input class="form-check-input" type="checkbox" id="check_notify_each_image">' +
|
||||||
'<label class="form-check-label" for="check_notify_each_image">' +
|
'<label class="form-check-label" for="check_notify_each_image">' +
|
||||||
'A chaque image rendue' +
|
'A chaque image rendue' +
|
||||||
'</label>' +
|
'</label>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="form-check">' +
|
'<div class="form-check mb-3">' +
|
||||||
'<input class="form-check-input" type="checkbox" id="check_notify_all_done" checked>' +
|
'<input class="form-check-input" type="checkbox" id="check_notify_all_done" checked>' +
|
||||||
'<label class="form-check-label" for="check_notify_all_done">' +
|
'<label class="form-check-label" for="check_notify_all_done">' +
|
||||||
'Quand tous les rendus sont termines' +
|
'Quand tous les rendus sont termines' +
|
||||||
'</label>' +
|
'</label>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|
||||||
|
'<hr class="border-secondary">' +
|
||||||
|
|
||||||
|
'<div class="d-flex justify-content-between align-items-center mb-2">' +
|
||||||
|
'<p class="text-light-emphasis mb-0" style="font-size: 0.8rem; font-weight: 600;">' +
|
||||||
|
'<i class="mdi mdi-email-outline me-1"></i>Notification par email' +
|
||||||
|
'</p>' +
|
||||||
|
'<div class="form-check form-switch mb-0">' +
|
||||||
|
'<input class="form-check-input" type="checkbox" id="check_email_enabled">' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
|
'<div id="container_email_fields">' +
|
||||||
|
'<p class="text-light-emphasis mb-2" style="font-size: 0.75rem;">' +
|
||||||
|
'Un email sera envoye lors des notifications. Separez les adresses par une virgule.' +
|
||||||
|
'</p>' +
|
||||||
|
'<div class="mb-2">' +
|
||||||
|
'<label class="form-label form-label-sm mb-0">Destinataire(s)</label>' +
|
||||||
|
'<input type="text" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_email_to" placeholder="email@example.com, autre@example.com">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="d-flex justify-content-between align-items-center">' +
|
||||||
|
'<button id="btn_test_email" class="btn btn-sm btn-outline-info">' +
|
||||||
|
'<i class="mdi mdi-email-send-outline me-1"></i>Envoyer un test' +
|
||||||
|
'</button>' +
|
||||||
|
'<span id="label_email_test_status" class="badge d-none"></span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="modal-footer border-secondary">' +
|
'<div class="modal-footer border-secondary">' +
|
||||||
'<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Annuler</button>' +
|
'<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Annuler</button>' +
|
||||||
@@ -76,11 +105,46 @@ const NotificationConfig = {
|
|||||||
obj_btn_save.addEventListener("click", () => {
|
obj_btn_save.addEventListener("click", () => {
|
||||||
NotificationConfig._save_config();
|
NotificationConfig._save_config();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let obj_check_email = document.getElementById("check_email_enabled");
|
||||||
|
obj_check_email.addEventListener("change", () => {
|
||||||
|
NotificationConfig._toggle_email_fields();
|
||||||
|
});
|
||||||
|
|
||||||
|
let obj_btn_test = document.getElementById("btn_test_email");
|
||||||
|
obj_btn_test.addEventListener("click", () => {
|
||||||
|
NotificationConfig._test_email();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_toggle_email_fields: () => {
|
||||||
|
let is_enabled = document.getElementById("check_email_enabled").checked;
|
||||||
|
let obj_fields = document.getElementById("container_email_fields");
|
||||||
|
let list_inputs = obj_fields.querySelectorAll("input, button");
|
||||||
|
for (let obj_input of list_inputs) {
|
||||||
|
obj_input.disabled = !is_enabled;
|
||||||
|
}
|
||||||
|
obj_fields.style.opacity = is_enabled ? "1" : "0.4";
|
||||||
},
|
},
|
||||||
|
|
||||||
_open_modal: () => {
|
_open_modal: () => {
|
||||||
document.getElementById("check_notify_each_image").checked = NotificationConfig.is_notify_each_image;
|
document.getElementById("check_notify_each_image").checked = NotificationConfig.is_notify_each_image;
|
||||||
document.getElementById("check_notify_all_done").checked = NotificationConfig.is_notify_all_done;
|
document.getElementById("check_notify_all_done").checked = NotificationConfig.is_notify_all_done;
|
||||||
|
|
||||||
|
let obj_email = NotificationConfig.obj_email_config;
|
||||||
|
if (obj_email) {
|
||||||
|
document.getElementById("check_email_enabled").checked = obj_email.is_enabled || false;
|
||||||
|
document.getElementById("input_email_to").value = obj_email.str_to || "";
|
||||||
|
} else {
|
||||||
|
document.getElementById("check_email_enabled").checked = false;
|
||||||
|
document.getElementById("input_email_to").value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationConfig._toggle_email_fields();
|
||||||
|
|
||||||
|
let obj_status = document.getElementById("label_email_test_status");
|
||||||
|
obj_status.classList.add("d-none");
|
||||||
|
|
||||||
NotificationConfig.obj_modal.show();
|
NotificationConfig.obj_modal.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -98,16 +162,39 @@ const NotificationConfig = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_load_email_config: () => {
|
||||||
|
window.api.get_email_config()
|
||||||
|
.then((obj_config) => {
|
||||||
|
if (obj_config) {
|
||||||
|
NotificationConfig.obj_email_config = obj_config;
|
||||||
|
NotificationConfig._update_badge();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Config non trouvee
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_save_config: () => {
|
_save_config: () => {
|
||||||
NotificationConfig.is_notify_each_image = document.getElementById("check_notify_each_image").checked;
|
NotificationConfig.is_notify_each_image = document.getElementById("check_notify_each_image").checked;
|
||||||
NotificationConfig.is_notify_all_done = document.getElementById("check_notify_all_done").checked;
|
NotificationConfig.is_notify_all_done = document.getElementById("check_notify_all_done").checked;
|
||||||
|
|
||||||
let obj_config = {
|
let obj_notif_config = {
|
||||||
is_notify_each_image: NotificationConfig.is_notify_each_image,
|
is_notify_each_image: NotificationConfig.is_notify_each_image,
|
||||||
is_notify_all_done: NotificationConfig.is_notify_all_done,
|
is_notify_all_done: NotificationConfig.is_notify_all_done,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.api.set_notification_config(obj_config)
|
let obj_email_config = {
|
||||||
|
is_enabled: document.getElementById("check_email_enabled").checked,
|
||||||
|
str_to: document.getElementById("input_email_to").value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationConfig.obj_email_config = obj_email_config;
|
||||||
|
|
||||||
|
window.api.set_notification_config(obj_notif_config)
|
||||||
|
.then(() => {
|
||||||
|
return window.api.set_email_config(obj_email_config);
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
NotificationConfig._update_badge();
|
NotificationConfig._update_badge();
|
||||||
NotificationConfig.obj_modal.hide();
|
NotificationConfig.obj_modal.hide();
|
||||||
@@ -118,12 +205,47 @@ const NotificationConfig = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_test_email: () => {
|
||||||
|
let obj_btn = document.getElementById("btn_test_email");
|
||||||
|
let obj_status = document.getElementById("label_email_test_status");
|
||||||
|
obj_btn.disabled = true;
|
||||||
|
obj_btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Envoi...';
|
||||||
|
obj_status.classList.add("d-none");
|
||||||
|
|
||||||
|
let obj_config = {
|
||||||
|
str_to: document.getElementById("input_email_to").value.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
window.api.test_email(obj_config)
|
||||||
|
.then((obj_result) => {
|
||||||
|
obj_btn.disabled = false;
|
||||||
|
obj_btn.innerHTML = '<i class="mdi mdi-email-send-outline me-1"></i>Envoyer un test';
|
||||||
|
obj_status.classList.remove("d-none");
|
||||||
|
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
obj_status.className = "badge bg-success";
|
||||||
|
obj_status.innerHTML = '<i class="mdi mdi-check-circle me-1"></i>Envoye';
|
||||||
|
} else {
|
||||||
|
obj_status.className = "badge bg-danger";
|
||||||
|
obj_status.textContent = obj_result.str_error || "Erreur";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
obj_btn.disabled = false;
|
||||||
|
obj_btn.innerHTML = '<i class="mdi mdi-email-send-outline me-1"></i>Envoyer un test';
|
||||||
|
obj_status.classList.remove("d-none");
|
||||||
|
obj_status.className = "badge bg-danger";
|
||||||
|
obj_status.textContent = obj_err.message || "Erreur";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_update_badge: () => {
|
_update_badge: () => {
|
||||||
let obj_badge = document.getElementById("btn_notification_config");
|
let obj_badge = document.getElementById("btn_notification_config");
|
||||||
let is_any_active = NotificationConfig.is_notify_each_image || NotificationConfig.is_notify_all_done;
|
let is_any_active = NotificationConfig.is_notify_each_image || NotificationConfig.is_notify_all_done;
|
||||||
if (is_any_active) {
|
let is_email = NotificationConfig.obj_email_config && NotificationConfig.obj_email_config.is_enabled;
|
||||||
|
if (is_any_active || is_email) {
|
||||||
obj_badge.className = "btn btn-sm btn-outline-success";
|
obj_badge.className = "btn btn-sm btn-outline-success";
|
||||||
obj_badge.title = "Notifications activees";
|
obj_badge.title = "Notifications activees" + (is_email ? " + email" : "");
|
||||||
} else {
|
} else {
|
||||||
obj_badge.className = "btn btn-sm btn-outline-secondary";
|
obj_badge.className = "btn btn-sm btn-outline-secondary";
|
||||||
obj_badge.title = "Notifications desactivees";
|
obj_badge.title = "Notifications desactivees";
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ const PreviewPanel = {
|
|||||||
window.api.on_preview_update((str_image_path) => {
|
window.api.on_preview_update((str_image_path) => {
|
||||||
PreviewPanel.show_image(str_image_path);
|
PreviewPanel.show_image(str_image_path);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let obj_btn_video = document.getElementById("btn_generate_video");
|
||||||
|
obj_btn_video.addEventListener("click", () => {
|
||||||
|
PreviewPanel._generate_video();
|
||||||
|
});
|
||||||
|
|
||||||
|
let obj_modal = document.getElementById("modal_video_preview");
|
||||||
|
obj_modal.addEventListener("hidden.bs.modal", () => {
|
||||||
|
let obj_video = document.getElementById("video_preview_player");
|
||||||
|
obj_video.pause();
|
||||||
|
obj_video.src = "";
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
show_image: (str_image_path) => {
|
show_image: (str_image_path) => {
|
||||||
@@ -55,4 +67,88 @@ const PreviewPanel = {
|
|||||||
+ "Aucun rendu disponible"
|
+ "Aucun rendu disponible"
|
||||||
+ "</div></div>";
|
+ "</div></div>";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_generate_video: () => {
|
||||||
|
if (!App.str_output_path) {
|
||||||
|
ConsoleLog.add("Veuillez selectionner un dossier de sortie.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list_cameras = CameraList.list_cameras;
|
||||||
|
let is_has_enabled = false;
|
||||||
|
for (let obj_cam of list_cameras) {
|
||||||
|
if (obj_cam.is_enabled) {
|
||||||
|
is_has_enabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_has_enabled) {
|
||||||
|
ConsoleLog.add("Aucune camera activee.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj_btn = document.getElementById("btn_generate_video");
|
||||||
|
obj_btn.disabled = true;
|
||||||
|
obj_btn.innerHTML = '<i class="mdi mdi-loading mdi-spin"></i>';
|
||||||
|
ConsoleLog.add("Generation de la video preview...");
|
||||||
|
|
||||||
|
let obj_ffmpeg_container = document.getElementById("container_ffmpeg_progress");
|
||||||
|
let obj_ffmpeg_label = document.getElementById("label_ffmpeg_time");
|
||||||
|
obj_ffmpeg_container.classList.remove("d-none");
|
||||||
|
obj_ffmpeg_label.textContent = "0s";
|
||||||
|
|
||||||
|
let nb_start_ms = Date.now();
|
||||||
|
let nb_timer = setInterval(() => {
|
||||||
|
let nb_elapsed = Math.round((Date.now() - nb_start_ms) / 1000);
|
||||||
|
obj_ffmpeg_label.textContent = PreviewPanel._format_elapsed(nb_elapsed);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||||
|
let str_frame_prefix = document.getElementById("input_frame_prefix").value || "";
|
||||||
|
let nb_frame_padding = parseInt(document.getElementById("input_frame_padding").value, 10) || 5;
|
||||||
|
|
||||||
|
let nb_fps = parseInt(document.getElementById("select_video_fps").value, 10) || 30;
|
||||||
|
|
||||||
|
let obj_params = {
|
||||||
|
str_output_path: App.str_output_path,
|
||||||
|
list_cameras: list_cameras,
|
||||||
|
nb_fps: nb_fps,
|
||||||
|
str_output_mode: str_output_mode,
|
||||||
|
str_frame_prefix: str_frame_prefix,
|
||||||
|
nb_frame_padding: nb_frame_padding,
|
||||||
|
};
|
||||||
|
|
||||||
|
window.api.generate_preview_video(obj_params)
|
||||||
|
.then((obj_result) => {
|
||||||
|
clearInterval(nb_timer);
|
||||||
|
obj_ffmpeg_container.classList.add("d-none");
|
||||||
|
obj_btn.disabled = false;
|
||||||
|
obj_btn.innerHTML = '<i class="mdi mdi-video-outline"></i>';
|
||||||
|
ConsoleLog.add("Video generee : " + obj_result.str_video_path);
|
||||||
|
|
||||||
|
let obj_video = document.getElementById("video_preview_player");
|
||||||
|
obj_video.src = "file://" + obj_result.str_video_path;
|
||||||
|
obj_video.load();
|
||||||
|
|
||||||
|
let obj_modal = new bootstrap.Modal(document.getElementById("modal_video_preview"));
|
||||||
|
obj_modal.show();
|
||||||
|
obj_video.play();
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
clearInterval(nb_timer);
|
||||||
|
obj_ffmpeg_container.classList.add("d-none");
|
||||||
|
obj_btn.disabled = false;
|
||||||
|
obj_btn.innerHTML = '<i class="mdi mdi-video-outline"></i>';
|
||||||
|
ConsoleLog.add("Erreur generation video : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_format_elapsed: (nb_seconds) => {
|
||||||
|
let nb_m = Math.floor(nb_seconds / 60);
|
||||||
|
let nb_s = nb_seconds % 60;
|
||||||
|
if (nb_m > 0) {
|
||||||
|
return nb_m + "m " + nb_s + "s";
|
||||||
|
}
|
||||||
|
return nb_s + "s";
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const ProgressBar = {
|
|||||||
let str_camera = obj_data.str_camera || "-";
|
let str_camera = obj_data.str_camera || "-";
|
||||||
let nb_frame = obj_data.nb_frame || 0;
|
let nb_frame = obj_data.nb_frame || 0;
|
||||||
let str_status = obj_data.str_status || "idle";
|
let str_status = obj_data.str_status || "idle";
|
||||||
|
let nb_avg_render_ms = obj_data.nb_avg_render_ms || 0;
|
||||||
|
let nb_remaining = obj_data.nb_remaining || 0;
|
||||||
|
|
||||||
let nb_percent = 0;
|
let nb_percent = 0;
|
||||||
if (nb_total > 0) {
|
if (nb_total > 0) {
|
||||||
@@ -40,7 +42,21 @@ const ProgressBar = {
|
|||||||
let obj_frame_label = document.getElementById("label_current_frame");
|
let obj_frame_label = document.getElementById("label_current_frame");
|
||||||
obj_frame_label.textContent = nb_frame !== null && nb_frame !== undefined ? String(nb_frame) : "-";
|
obj_frame_label.textContent = nb_frame !== null && nb_frame !== undefined ? String(nb_frame) : "-";
|
||||||
|
|
||||||
RenderQueue.update_progress(nb_current, obj_data.nb_last_render_ms || 0, obj_data.str_last_image_path || null, obj_data.list_skipped || [], obj_data.list_stopped || [], obj_data.list_skipped_paths || []);
|
let obj_time_label = document.getElementById("label_progress_time");
|
||||||
|
if (nb_avg_render_ms > 0 && nb_remaining > 0 && str_status === "running") {
|
||||||
|
let nb_remaining_ms = nb_avg_render_ms * nb_remaining;
|
||||||
|
let str_remaining = ProgressBar._format_duration(nb_remaining_ms);
|
||||||
|
let obj_eta = new Date(Date.now() + nb_remaining_ms);
|
||||||
|
let str_eta_hours = String(obj_eta.getHours()).padStart(2, "0");
|
||||||
|
let str_eta_minutes = String(obj_eta.getMinutes()).padStart(2, "0");
|
||||||
|
obj_time_label.textContent = "Restant : " + str_remaining + " — Fin : " + str_eta_hours + ":" + str_eta_minutes;
|
||||||
|
} else if (str_status === "idle" && nb_current >= nb_total && nb_total > 0) {
|
||||||
|
obj_time_label.textContent = "Termine";
|
||||||
|
} else {
|
||||||
|
obj_time_label.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderQueue.update_progress(obj_data);
|
||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
@@ -50,5 +66,30 @@ const ProgressBar = {
|
|||||||
document.getElementById("label_progress_status").textContent = "En attente";
|
document.getElementById("label_progress_status").textContent = "En attente";
|
||||||
document.getElementById("label_current_camera").textContent = "-";
|
document.getElementById("label_current_camera").textContent = "-";
|
||||||
document.getElementById("label_current_frame").textContent = "-";
|
document.getElementById("label_current_frame").textContent = "-";
|
||||||
|
document.getElementById("label_progress_time").textContent = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
_format_duration: (nb_ms) => {
|
||||||
|
let nb_total_seconds = Math.ceil(nb_ms / 1000);
|
||||||
|
let nb_days = Math.floor(nb_total_seconds / 86400);
|
||||||
|
nb_total_seconds %= 86400;
|
||||||
|
let nb_hours = Math.floor(nb_total_seconds / 3600);
|
||||||
|
nb_total_seconds %= 3600;
|
||||||
|
let nb_minutes = Math.floor(nb_total_seconds / 60);
|
||||||
|
let nb_seconds = nb_total_seconds % 60;
|
||||||
|
|
||||||
|
let str_result = "";
|
||||||
|
if (nb_days > 0) {
|
||||||
|
str_result += nb_days + "j ";
|
||||||
|
}
|
||||||
|
if (nb_hours > 0 || nb_days > 0) {
|
||||||
|
str_result += nb_hours + "h ";
|
||||||
|
}
|
||||||
|
if (nb_minutes > 0 || nb_hours > 0 || nb_days > 0) {
|
||||||
|
str_result += nb_minutes + "m ";
|
||||||
|
}
|
||||||
|
str_result += nb_seconds + "s";
|
||||||
|
|
||||||
|
return str_result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ const RenderQueue = {
|
|||||||
nb_total_render_ms: 0,
|
nb_total_render_ms: 0,
|
||||||
nb_completed_renders: 0,
|
nb_completed_renders: 0,
|
||||||
nb_last_current: 0,
|
nb_last_current: 0,
|
||||||
|
list_machine_avgs: [],
|
||||||
|
nb_local_avg_ms: 0,
|
||||||
|
str_hostname: "",
|
||||||
|
_obj_context_menu: null,
|
||||||
|
_nb_context_index: -1,
|
||||||
|
|
||||||
init: () => {
|
init: () => {
|
||||||
// Initialized on demand
|
RenderQueue._create_context_menu();
|
||||||
},
|
},
|
||||||
|
|
||||||
build_display: (str_mode, list_cameras) => {
|
build_display: (str_mode, list_cameras) => {
|
||||||
@@ -13,6 +18,9 @@ const RenderQueue = {
|
|||||||
RenderQueue.nb_total_render_ms = 0;
|
RenderQueue.nb_total_render_ms = 0;
|
||||||
RenderQueue.nb_completed_renders = 0;
|
RenderQueue.nb_completed_renders = 0;
|
||||||
RenderQueue.nb_last_current = 0;
|
RenderQueue.nb_last_current = 0;
|
||||||
|
RenderQueue.list_machine_avgs = [];
|
||||||
|
RenderQueue.nb_local_avg_ms = 0;
|
||||||
|
RenderQueue.str_hostname = "";
|
||||||
|
|
||||||
let list_enabled = [];
|
let list_enabled = [];
|
||||||
for (let obj_cam of list_cameras) {
|
for (let obj_cam of list_cameras) {
|
||||||
@@ -32,6 +40,7 @@ const RenderQueue = {
|
|||||||
str_image_path: null,
|
str_image_path: null,
|
||||||
obj_dom_el: null,
|
obj_dom_el: null,
|
||||||
obj_dom_icon: null,
|
obj_dom_icon: null,
|
||||||
|
obj_dom_result: null,
|
||||||
str_dom_status: null,
|
str_dom_status: null,
|
||||||
is_click_bound: false,
|
is_click_bound: false,
|
||||||
});
|
});
|
||||||
@@ -60,6 +69,7 @@ const RenderQueue = {
|
|||||||
str_image_path: null,
|
str_image_path: null,
|
||||||
obj_dom_el: null,
|
obj_dom_el: null,
|
||||||
obj_dom_icon: null,
|
obj_dom_icon: null,
|
||||||
|
obj_dom_result: null,
|
||||||
str_dom_status: null,
|
str_dom_status: null,
|
||||||
is_click_bound: false,
|
is_click_bound: false,
|
||||||
});
|
});
|
||||||
@@ -101,12 +111,20 @@ const RenderQueue = {
|
|||||||
obj_frame.classList.add("text-light-emphasis");
|
obj_frame.classList.add("text-light-emphasis");
|
||||||
obj_frame.textContent = "F" + obj_item.nb_frame;
|
obj_frame.textContent = "F" + obj_item.nb_frame;
|
||||||
|
|
||||||
|
let obj_result = document.createElement("small");
|
||||||
|
obj_result.classList.add("text-light-emphasis", "queue-item-result");
|
||||||
|
obj_result.style.minWidth = "140px";
|
||||||
|
obj_result.style.textAlign = "right";
|
||||||
|
obj_result.style.fontSize = "0.75em";
|
||||||
|
|
||||||
obj_el.appendChild(obj_icon);
|
obj_el.appendChild(obj_icon);
|
||||||
obj_el.appendChild(obj_name);
|
obj_el.appendChild(obj_name);
|
||||||
obj_el.appendChild(obj_frame);
|
obj_el.appendChild(obj_frame);
|
||||||
|
obj_el.appendChild(obj_result);
|
||||||
|
|
||||||
obj_item.obj_dom_el = obj_el;
|
obj_item.obj_dom_el = obj_el;
|
||||||
obj_item.obj_dom_icon = obj_icon;
|
obj_item.obj_dom_icon = obj_icon;
|
||||||
|
obj_item.obj_dom_result = obj_result;
|
||||||
obj_item.str_dom_status = "pending";
|
obj_item.str_dom_status = "pending";
|
||||||
|
|
||||||
obj_fragment.appendChild(obj_el);
|
obj_fragment.appendChild(obj_el);
|
||||||
@@ -115,7 +133,21 @@ const RenderQueue = {
|
|||||||
obj_container.appendChild(obj_fragment);
|
obj_container.appendChild(obj_fragment);
|
||||||
},
|
},
|
||||||
|
|
||||||
update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped, list_stopped, list_skipped_paths) => {
|
update_progress: (obj_data) => {
|
||||||
|
let nb_current = obj_data.nb_current || 0;
|
||||||
|
let nb_last_render_ms = obj_data.nb_last_render_ms || 0;
|
||||||
|
let str_last_image_path = obj_data.str_last_image_path || null;
|
||||||
|
let list_skipped = obj_data.list_skipped || [];
|
||||||
|
let list_stopped = obj_data.list_stopped || [];
|
||||||
|
let list_skipped_paths = obj_data.list_skipped_paths || [];
|
||||||
|
let list_rendering_remote = obj_data.list_rendering_remote || [];
|
||||||
|
let list_item_results = obj_data.list_item_results || [];
|
||||||
|
let list_machine_avgs = obj_data.list_machine_avgs || [];
|
||||||
|
|
||||||
|
RenderQueue.str_hostname = obj_data.str_hostname || "";
|
||||||
|
RenderQueue.nb_local_avg_ms = obj_data.nb_avg_render_ms || 0;
|
||||||
|
RenderQueue.list_machine_avgs = list_machine_avgs;
|
||||||
|
|
||||||
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
|
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
|
||||||
RenderQueue.nb_total_render_ms += nb_last_render_ms;
|
RenderQueue.nb_total_render_ms += nb_last_render_ms;
|
||||||
RenderQueue.nb_completed_renders++;
|
RenderQueue.nb_completed_renders++;
|
||||||
@@ -134,9 +166,17 @@ const RenderQueue = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build result map for quick lookup
|
||||||
|
let map_results = {};
|
||||||
|
for (let obj_r of list_item_results) {
|
||||||
|
map_results[obj_r.nb_index] = obj_r;
|
||||||
|
}
|
||||||
|
|
||||||
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
|
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
|
||||||
if (list_stopped && list_stopped.indexOf(nb_i) !== -1) {
|
if (list_stopped && list_stopped.indexOf(nb_i) !== -1) {
|
||||||
RenderQueue.list_items[nb_i].str_status = "stopped";
|
RenderQueue.list_items[nb_i].str_status = "stopped";
|
||||||
|
} else if (list_rendering_remote && list_rendering_remote.indexOf(nb_i) !== -1) {
|
||||||
|
RenderQueue.list_items[nb_i].str_status = "rendering_remote";
|
||||||
} else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
|
} else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
|
||||||
RenderQueue.list_items[nb_i].str_status = "skipped";
|
RenderQueue.list_items[nb_i].str_status = "skipped";
|
||||||
} else if (nb_i < nb_current) {
|
} else if (nb_i < nb_current) {
|
||||||
@@ -146,6 +186,11 @@ const RenderQueue = {
|
|||||||
} else {
|
} else {
|
||||||
RenderQueue.list_items[nb_i].str_status = "pending";
|
RenderQueue.list_items[nb_i].str_status = "pending";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store result info on item
|
||||||
|
if (map_results[nb_i]) {
|
||||||
|
RenderQueue.list_items[nb_i].obj_result = map_results[nb_i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderQueue._update_time_display();
|
RenderQueue._update_time_display();
|
||||||
@@ -163,7 +208,9 @@ const RenderQueue = {
|
|||||||
let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped")
|
let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped")
|
||||||
&& obj_item.str_image_path && !obj_item.is_click_bound;
|
&& obj_item.str_image_path && !obj_item.is_click_bound;
|
||||||
|
|
||||||
if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) {
|
let is_status_changed = obj_item.str_status !== obj_item.str_dom_status;
|
||||||
|
|
||||||
|
if (!is_status_changed && !is_needs_click) {
|
||||||
if (obj_item.str_status === "rendering") {
|
if (obj_item.str_status === "rendering") {
|
||||||
obj_rendering_el = obj_item.obj_dom_el;
|
obj_rendering_el = obj_item.obj_dom_el;
|
||||||
}
|
}
|
||||||
@@ -189,10 +236,18 @@ const RenderQueue = {
|
|||||||
} else if (obj_item.str_status === "stopped") {
|
} else if (obj_item.str_status === "stopped") {
|
||||||
str_icon = "mdi-stop-circle";
|
str_icon = "mdi-stop-circle";
|
||||||
str_color = "text-warning";
|
str_color = "text-warning";
|
||||||
|
} else if (obj_item.str_status === "rendering_remote") {
|
||||||
|
str_icon = "mdi-desktop-classic";
|
||||||
|
str_color = "text-warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
|
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
|
||||||
|
|
||||||
|
// Update result column
|
||||||
|
if (obj_item.obj_dom_result) {
|
||||||
|
RenderQueue._update_result_cell(obj_item);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_needs_click) {
|
if (is_needs_click) {
|
||||||
obj_item.obj_dom_el.classList.add("queue-item-clickable");
|
obj_item.obj_dom_el.classList.add("queue-item-clickable");
|
||||||
let str_path = obj_item.str_image_path;
|
let str_path = obj_item.str_image_path;
|
||||||
@@ -210,6 +265,45 @@ const RenderQueue = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_update_result_cell: (obj_item) => {
|
||||||
|
let obj_r = obj_item.obj_result;
|
||||||
|
if (!obj_r) {
|
||||||
|
obj_item.obj_dom_result.textContent = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj_r.str_type === "done") {
|
||||||
|
let str_date = "";
|
||||||
|
if (obj_r.str_date) {
|
||||||
|
let obj_d = new Date(obj_r.str_date);
|
||||||
|
let str_day = String(obj_d.getDate()).padStart(2, "0");
|
||||||
|
let str_month = String(obj_d.getMonth() + 1).padStart(2, "0");
|
||||||
|
let str_hours = String(obj_d.getHours()).padStart(2, "0");
|
||||||
|
let str_minutes = String(obj_d.getMinutes()).padStart(2, "0");
|
||||||
|
str_date = str_day + "/" + str_month + " " + str_hours + ":" + str_minutes;
|
||||||
|
}
|
||||||
|
let str_res = obj_r.str_resolution || "";
|
||||||
|
obj_item.obj_dom_result.textContent = str_date + (str_date && str_res ? " | " : "") + str_res;
|
||||||
|
obj_item.obj_dom_result.className = "text-success queue-item-result";
|
||||||
|
obj_item.obj_dom_result.style.minWidth = "140px";
|
||||||
|
obj_item.obj_dom_result.style.textAlign = "right";
|
||||||
|
obj_item.obj_dom_result.style.fontSize = "0.75em";
|
||||||
|
} else if (obj_r.str_type === "rendering_remote") {
|
||||||
|
let str_host = obj_r.str_remote_hostname || "?";
|
||||||
|
let str_avg = "";
|
||||||
|
if (obj_r.nb_remote_avg_ms > 0) {
|
||||||
|
str_avg = " ~" + RenderQueue._format_duration(obj_r.nb_remote_avg_ms) + "/i";
|
||||||
|
}
|
||||||
|
obj_item.obj_dom_result.textContent = str_host + str_avg;
|
||||||
|
obj_item.obj_dom_result.className = "text-warning queue-item-result";
|
||||||
|
obj_item.obj_dom_result.style.minWidth = "140px";
|
||||||
|
obj_item.obj_dom_result.style.textAlign = "right";
|
||||||
|
obj_item.obj_dom_result.style.fontSize = "0.75em";
|
||||||
|
} else {
|
||||||
|
obj_item.obj_dom_result.textContent = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mark_existing: (list_existing) => {
|
mark_existing: (list_existing) => {
|
||||||
for (let obj_existing of list_existing) {
|
for (let obj_existing of list_existing) {
|
||||||
if (obj_existing.nb_index < RenderQueue.list_items.length) {
|
if (obj_existing.nb_index < RenderQueue.list_items.length) {
|
||||||
@@ -226,22 +320,55 @@ const RenderQueue = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RenderQueue.nb_completed_renders === 0) {
|
let nb_local_avg = RenderQueue.nb_local_avg_ms;
|
||||||
|
if (nb_local_avg === 0 && RenderQueue.nb_completed_renders > 0) {
|
||||||
|
nb_local_avg = Math.round(RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nb_local_avg === 0) {
|
||||||
obj_label.innerHTML = "";
|
obj_label.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nb_avg_ms = RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders;
|
|
||||||
let nb_remaining_count = 0;
|
let nb_remaining_count = 0;
|
||||||
for (let obj_item of RenderQueue.list_items) {
|
for (let obj_item of RenderQueue.list_items) {
|
||||||
if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped") {
|
if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped" && obj_item.str_status !== "rendering_remote") {
|
||||||
nb_remaining_count++;
|
nb_remaining_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let nb_remaining_ms = nb_avg_ms * nb_remaining_count;
|
// Part 1 : temps moyen machine actuelle /i
|
||||||
obj_label.innerHTML = '<i class="mdi mdi-clock-outline me-1"></i>'
|
let str_local_avg = RenderQueue._format_duration(nb_local_avg) + "/i";
|
||||||
+ RenderQueue._format_duration(nb_remaining_ms);
|
|
||||||
|
// Part 2 : temps restant si machine seule
|
||||||
|
let nb_remaining_solo_ms = nb_local_avg * nb_remaining_count;
|
||||||
|
let str_remaining_solo = RenderQueue._format_duration(nb_remaining_solo_ms);
|
||||||
|
|
||||||
|
// Part 3 : temps restant multi-machines
|
||||||
|
let list_avgs = RenderQueue.list_machine_avgs;
|
||||||
|
let nb_machines_with_data = 0;
|
||||||
|
let nb_sum_avgs = 0;
|
||||||
|
for (let obj_m of list_avgs) {
|
||||||
|
if (obj_m.nb_avg_ms > 0) {
|
||||||
|
nb_sum_avgs += obj_m.nb_avg_ms;
|
||||||
|
nb_machines_with_data++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_remaining_multi = "";
|
||||||
|
if (nb_machines_with_data > 1) {
|
||||||
|
let nb_avg_of_avgs = nb_sum_avgs / nb_machines_with_data;
|
||||||
|
let nb_remaining_multi_ms = (nb_avg_of_avgs * nb_remaining_count) / nb_machines_with_data;
|
||||||
|
str_remaining_multi = RenderQueue._format_duration(nb_remaining_multi_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_display = '<i class="mdi mdi-clock-outline me-1"></i>'
|
||||||
|
+ str_local_avg + " | " + str_remaining_solo;
|
||||||
|
if (str_remaining_multi) {
|
||||||
|
str_display += " | " + str_remaining_multi;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj_label.innerHTML = str_display;
|
||||||
},
|
},
|
||||||
|
|
||||||
_format_duration: (nb_ms) => {
|
_format_duration: (nb_ms) => {
|
||||||
@@ -267,4 +394,262 @@ const RenderQueue = {
|
|||||||
|
|
||||||
return str_result;
|
return str_result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Context menu ─────────────────────────────────────────
|
||||||
|
|
||||||
|
_create_context_menu: () => {
|
||||||
|
let obj_menu = document.createElement("div");
|
||||||
|
obj_menu.id = "context_menu_queue";
|
||||||
|
obj_menu.className = "context-menu d-none";
|
||||||
|
obj_menu.innerHTML =
|
||||||
|
'<div class="context-menu-item" data-action="preview"><i class="mdi mdi-eye-outline me-2"></i>Voir dans le preview</div>' +
|
||||||
|
'<div class="context-menu-item" data-action="open-folder"><i class="mdi mdi-folder-open-outline me-2"></i>Ouvrir dans l\'explorateur</div>' +
|
||||||
|
'<div class="context-menu-item" data-action="open-default"><i class="mdi mdi-open-in-new me-2"></i>Ouvrir (app par defaut)</div>' +
|
||||||
|
'<div class="context-menu-separator"></div>' +
|
||||||
|
'<div class="context-menu-item" data-action="copy-path"><i class="mdi mdi-content-copy me-2"></i>Copier le chemin</div>' +
|
||||||
|
'<div class="context-menu-item" data-action="copy-image"><i class="mdi mdi-image-outline me-2"></i>Copier l\'image</div>' +
|
||||||
|
'<div class="context-menu-separator"></div>' +
|
||||||
|
'<div class="context-menu-item" data-action="file-info"><i class="mdi mdi-information-outline me-2"></i>Infos fichier</div>' +
|
||||||
|
'<div class="context-menu-separator"></div>' +
|
||||||
|
'<div class="context-menu-item" data-action="delete"><i class="mdi mdi-delete-outline me-2"></i>Supprimer l\'image</div>' +
|
||||||
|
'<div class="context-menu-item context-menu-danger" data-action="delete-from-here"><i class="mdi mdi-delete-sweep-outline me-2"></i>Supprimer depuis ici</div>';
|
||||||
|
|
||||||
|
document.body.appendChild(obj_menu);
|
||||||
|
RenderQueue._obj_context_menu = obj_menu;
|
||||||
|
|
||||||
|
obj_menu.addEventListener("click", (obj_event) => {
|
||||||
|
let obj_target = obj_event.target.closest(".context-menu-item");
|
||||||
|
if (!obj_target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let str_action = obj_target.dataset.action;
|
||||||
|
RenderQueue._hide_context_menu();
|
||||||
|
RenderQueue._handle_context_action(str_action);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", () => {
|
||||||
|
RenderQueue._hide_context_menu();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (obj_event) => {
|
||||||
|
if (obj_event.key === "Escape") {
|
||||||
|
RenderQueue._hide_context_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let obj_container = document.getElementById("container_render_queue");
|
||||||
|
obj_container.addEventListener("contextmenu", (obj_event) => {
|
||||||
|
let obj_el = obj_event.target.closest(".list-group-item");
|
||||||
|
if (!obj_el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nb_index = -1;
|
||||||
|
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
|
||||||
|
if (RenderQueue.list_items[nb_i].obj_dom_el === obj_el) {
|
||||||
|
nb_index = nb_i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nb_index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj_item = RenderQueue.list_items[nb_index];
|
||||||
|
if (!obj_item.str_image_path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj_event.preventDefault();
|
||||||
|
RenderQueue._nb_context_index = nb_index;
|
||||||
|
RenderQueue._show_context_menu(obj_event.clientX, obj_event.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
obj_container.addEventListener("scroll", () => {
|
||||||
|
RenderQueue._hide_context_menu();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_show_context_menu: (nb_x, nb_y) => {
|
||||||
|
let obj_menu = RenderQueue._obj_context_menu;
|
||||||
|
obj_menu.classList.remove("d-none");
|
||||||
|
obj_menu.style.left = nb_x + "px";
|
||||||
|
obj_menu.style.top = nb_y + "px";
|
||||||
|
|
||||||
|
let obj_rect = obj_menu.getBoundingClientRect();
|
||||||
|
if (obj_rect.right > window.innerWidth) {
|
||||||
|
obj_menu.style.left = (nb_x - obj_rect.width) + "px";
|
||||||
|
}
|
||||||
|
if (obj_rect.bottom > window.innerHeight) {
|
||||||
|
obj_menu.style.top = (nb_y - obj_rect.height) + "px";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_hide_context_menu: () => {
|
||||||
|
if (RenderQueue._obj_context_menu) {
|
||||||
|
RenderQueue._obj_context_menu.classList.add("d-none");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_handle_context_action: (str_action) => {
|
||||||
|
let nb_index = RenderQueue._nb_context_index;
|
||||||
|
if (nb_index < 0 || nb_index >= RenderQueue.list_items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let obj_item = RenderQueue.list_items[nb_index];
|
||||||
|
let str_path = obj_item.str_image_path;
|
||||||
|
|
||||||
|
if (!str_path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_action === "preview") {
|
||||||
|
PreviewPanel.show_image(str_path);
|
||||||
|
} else if (str_action === "open-folder") {
|
||||||
|
window.api.show_item_in_folder(str_path)
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur ouverture dossier : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
} else if (str_action === "open-default") {
|
||||||
|
window.api.open_file_default(str_path)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (!obj_result.is_success) {
|
||||||
|
ConsoleLog.add("Erreur ouverture : " + obj_result.str_error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur ouverture : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
} else if (str_action === "copy-path") {
|
||||||
|
navigator.clipboard.writeText(str_path)
|
||||||
|
.then(() => {
|
||||||
|
ConsoleLog.add("Chemin copie : " + str_path);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ConsoleLog.add("Erreur copie chemin.");
|
||||||
|
});
|
||||||
|
} else if (str_action === "copy-image") {
|
||||||
|
window.api.copy_image_to_clipboard(str_path)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
ConsoleLog.add("Image copiee dans le presse-papier.");
|
||||||
|
} else {
|
||||||
|
ConsoleLog.add("Erreur copie image : " + obj_result.str_error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur copie image : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
} else if (str_action === "file-info") {
|
||||||
|
window.api.get_file_info(str_path)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
let nb_size_kb = Math.round(obj_result.nb_size / 1024);
|
||||||
|
let str_size = nb_size_kb > 1024
|
||||||
|
? (nb_size_kb / 1024).toFixed(1) + " Mo"
|
||||||
|
: nb_size_kb + " Ko";
|
||||||
|
let obj_date = new Date(obj_result.str_modified);
|
||||||
|
let str_date = String(obj_date.getDate()).padStart(2, "0") + "/"
|
||||||
|
+ String(obj_date.getMonth() + 1).padStart(2, "0") + "/"
|
||||||
|
+ obj_date.getFullYear() + " "
|
||||||
|
+ String(obj_date.getHours()).padStart(2, "0") + ":"
|
||||||
|
+ String(obj_date.getMinutes()).padStart(2, "0") + ":"
|
||||||
|
+ String(obj_date.getSeconds()).padStart(2, "0");
|
||||||
|
ConsoleLog.add("Infos : " + str_path);
|
||||||
|
ConsoleLog.add(" Taille : " + str_size + " — Modifie : " + str_date);
|
||||||
|
} else {
|
||||||
|
ConsoleLog.add("Erreur infos : " + obj_result.str_error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur infos : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
} else if (str_action === "delete") {
|
||||||
|
RenderQueue._delete_single(nb_index);
|
||||||
|
} else if (str_action === "delete-from-here") {
|
||||||
|
RenderQueue._delete_from_here(nb_index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_delete_single: (nb_index) => {
|
||||||
|
let obj_item = RenderQueue.list_items[nb_index];
|
||||||
|
let str_path = obj_item.str_image_path;
|
||||||
|
|
||||||
|
window.api.delete_rendered_file(str_path)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
obj_item.str_image_path = null;
|
||||||
|
obj_item.str_status = "deleted";
|
||||||
|
obj_item.is_click_bound = false;
|
||||||
|
RenderQueue._update_deleted_item(obj_item);
|
||||||
|
ConsoleLog.add("Supprime : " + str_path);
|
||||||
|
} else {
|
||||||
|
ConsoleLog.add("Erreur suppression : " + obj_result.str_error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur suppression : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_delete_from_here: (nb_index) => {
|
||||||
|
let list_paths = [];
|
||||||
|
for (let nb_i = nb_index; nb_i < RenderQueue.list_items.length; nb_i++) {
|
||||||
|
let obj_it = RenderQueue.list_items[nb_i];
|
||||||
|
if (obj_it.str_image_path) {
|
||||||
|
list_paths.push(obj_it.str_image_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list_paths.length === 0) {
|
||||||
|
ConsoleLog.add("Aucun fichier a supprimer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_confirm = confirm("Supprimer " + list_paths.length + " fichier(s) depuis cet element ?\n\nCette action est irreversible.");
|
||||||
|
if (!is_confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.api.delete_rendered_files(list_paths)
|
||||||
|
.then((obj_result) => {
|
||||||
|
if (obj_result.is_success) {
|
||||||
|
for (let nb_i = nb_index; nb_i < RenderQueue.list_items.length; nb_i++) {
|
||||||
|
let obj_it = RenderQueue.list_items[nb_i];
|
||||||
|
if (obj_it.str_image_path) {
|
||||||
|
obj_it.str_image_path = null;
|
||||||
|
obj_it.str_status = "deleted";
|
||||||
|
obj_it.is_click_bound = false;
|
||||||
|
RenderQueue._update_deleted_item(obj_it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConsoleLog.add(obj_result.nb_deleted + " fichier(s) supprime(s).");
|
||||||
|
if (obj_result.list_errors.length > 0) {
|
||||||
|
for (let str_err of obj_result.list_errors) {
|
||||||
|
ConsoleLog.add("Erreur : " + str_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((obj_err) => {
|
||||||
|
ConsoleLog.add("Erreur suppression : " + (obj_err.message || obj_err));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_update_deleted_item: (obj_item) => {
|
||||||
|
if (!obj_item.obj_dom_el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
obj_item.obj_dom_icon.className = "mdi mdi-file-remove-outline text-muted";
|
||||||
|
obj_item.obj_dom_el.classList.remove("queue-item-clickable");
|
||||||
|
if (obj_item.obj_dom_result) {
|
||||||
|
obj_item.obj_dom_result.textContent = "supprime";
|
||||||
|
obj_item.obj_dom_result.className = "text-muted queue-item-result";
|
||||||
|
obj_item.obj_dom_result.style.minWidth = "140px";
|
||||||
|
obj_item.obj_dom_result.style.textAlign = "right";
|
||||||
|
obj_item.obj_dom_result.style.fontSize = "0.75em";
|
||||||
|
}
|
||||||
|
obj_item.str_dom_status = "deleted";
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -246,6 +246,47 @@ body.has-update-banner .container-fluid {
|
|||||||
background-color: #6c757d;
|
background-color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Context menu ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 220px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #e1e4e8;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background-color: rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-danger {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-danger:hover {
|
||||||
|
background-color: rgba(248, 113, 113, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-separator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #444;
|
||||||
|
margin: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Badge ──────────────────────────────────────────────────── */
|
/* ── Badge ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"str_version": "1.4.0"
|
"str_version": "1.6.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user