v1.0.0 — release avec auto-update Gitea

Ajout du systeme de mise a jour automatique :
- UpdateManager (main) : verifie les tags Gitea, telecharge et applique les MAJ
- UpdateBanner (renderer) : banniere UI avec progression et retry
- IPC channels : check-for-updates, apply-update, update-available, update-progress, update-error
- Desactivation asar pour permettre le remplacement des sources
- version.json comme source de verite pour la version locale

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sorlinv
2026-02-25 15:58:02 +01:00
parent b556cce88c
commit f31f5aa605
16 changed files with 766 additions and 54 deletions

View File

@@ -67,17 +67,33 @@
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_subfolder" value="subfolder" checked>
<label class="form-check-label" for="radio_output_subfolder">
Sous-dossier par camera
Sous-dossier
</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_prefix" value="prefix">
<label class="form-check-label" for="radio_output_prefix">
Nom camera dans le fichier
Prefixe
</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_both" value="both">
<label class="form-check-label" for="radio_output_both">
Les deux
</label>
</div>
<div class="mt-2 d-flex gap-2 align-items-end">
<div>
<label class="form-label form-label-sm mb-1">Prefixe frame</label>
<input type="text" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_prefix" value="f_" style="max-width: 160px;">
</div>
<div>
<label class="form-label form-label-sm mb-1">Padding</label>
<input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_padding" value="5" min="1" max="10" style="max-width: 80px;">
</div>
</div>
<div class="mt-1">
<small id="label_output_example" class="text-light-emphasis">Ex: /sortie/<strong>Camera.001</strong>/frame_00001.png</small>
<small id="label_output_example" class="text-light-emphasis">Ex: /sortie/<strong>Camera.001</strong>/f_00001.png</small>
</div>
</div>
</div>
@@ -244,6 +260,7 @@
<script src="scripts/RenderQueue.js"></script>
<script src="scripts/PreviewPanel.js"></script>
<script src="scripts/ProgressBar.js"></script>
<script src="scripts/UpdateBanner.js"></script>
<script src="scripts/App.js"></script>
</body>
</html>

View File

@@ -9,6 +9,7 @@ const App = {
RenderQueue.init();
PreviewPanel.init();
ProgressBar.init();
UpdateBanner.init();
App._bind_events();
App._bind_render_events();
@@ -54,8 +55,14 @@ const App = {
let obj_radio_subfolder = document.getElementById("radio_output_subfolder");
let obj_radio_prefix = document.getElementById("radio_output_prefix");
let obj_radio_both = document.getElementById("radio_output_both");
let obj_input_frame_prefix = document.getElementById("input_frame_prefix");
let obj_input_frame_padding = document.getElementById("input_frame_padding");
obj_radio_subfolder.addEventListener("change", () => { App._update_output_example(); });
obj_radio_prefix.addEventListener("change", () => { App._update_output_example(); });
obj_radio_both.addEventListener("change", () => { App._update_output_example(); });
obj_input_frame_prefix.addEventListener("input", () => { App._update_output_example(); });
obj_input_frame_padding.addEventListener("input", () => { App._update_output_example(); });
},
_bind_render_events: () => {
@@ -73,11 +80,16 @@ const App = {
_update_output_example: () => {
let str_mode = document.querySelector('input[name="output_mode"]:checked').value;
let str_prefix = document.getElementById("input_frame_prefix").value || "";
let nb_padding = parseInt(document.getElementById("input_frame_padding").value, 10) || 5;
let str_padded = String(1).padStart(nb_padding, "0");
let obj_label = document.getElementById("label_output_example");
if (str_mode === "subfolder") {
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>/frame_00001.png';
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>/' + str_prefix + str_padded + '.png';
} else if (str_mode === "prefix") {
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>_' + str_prefix + str_padded + '.png';
} else {
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>_frame_00001.png';
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>/<strong>Camera.001</strong>_' + str_prefix + str_padded + '.png';
}
},
@@ -101,16 +113,16 @@ const App = {
return window.api.get_cameras(str_path);
})
.then((list_cameras) => {
.then((obj_result) => {
obj_btn_blend.disabled = false;
if (!list_cameras) {
if (!obj_result) {
return;
}
CameraList.set_cameras(list_cameras);
CameraList.set_cameras(obj_result.list_cameras, obj_result.obj_scene);
CameraConfig.clear();
ConsoleLog.add(list_cameras.length + " camera(s) trouvee(s).");
ConsoleLog.add(obj_result.list_cameras.length + " camera(s) trouvee(s).");
App._update_start_button();
})
.catch((obj_err) => {
@@ -158,11 +170,15 @@ const App = {
let str_overwrite_mode = document.querySelector('input[name="overwrite_mode"]:checked').value;
let list_cameras = CameraList.list_cameras;
let str_frame_prefix = document.getElementById("input_frame_prefix").value || "";
let nb_frame_padding = parseInt(document.getElementById("input_frame_padding").value, 10) || 5;
let obj_config = {
str_blend_file: App.str_blend_path,
str_render_mode: str_mode,
str_output_mode: str_output_mode,
str_overwrite_mode: str_overwrite_mode,
str_frame_prefix: str_frame_prefix,
nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path,
list_cameras: list_cameras,
};
@@ -205,11 +221,15 @@ const App = {
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
let str_overwrite_mode = document.querySelector('input[name="overwrite_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 obj_config = {
str_blend_file: App.str_blend_path,
str_render_mode: str_mode,
str_output_mode: str_output_mode,
str_overwrite_mode: str_overwrite_mode,
str_frame_prefix: str_frame_prefix,
nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path,
list_cameras: CameraList.list_cameras,
};
@@ -245,6 +265,8 @@ const App = {
if (obj_config.str_output_mode === "prefix") {
document.getElementById("radio_output_prefix").checked = true;
} else if (obj_config.str_output_mode === "both") {
document.getElementById("radio_output_both").checked = true;
} else {
document.getElementById("radio_output_subfolder").checked = true;
}
@@ -255,6 +277,13 @@ const App = {
document.getElementById("radio_overwrite").checked = true;
}
if (obj_config.str_frame_prefix !== undefined) {
document.getElementById("input_frame_prefix").value = obj_config.str_frame_prefix;
}
if (obj_config.nb_frame_padding !== undefined) {
document.getElementById("input_frame_padding").value = obj_config.nb_frame_padding;
}
App._update_output_example();
if (obj_config.list_cameras && obj_config.list_cameras.length > 0) {

View File

@@ -32,6 +32,10 @@ const CameraConfig = {
+ ' <label class="form-label form-label-sm">Frame end</label>'
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_end" value="' + obj_camera.nb_frame_end + '" min="0">'
+ " </div>"
+ ' <div class="col-6">'
+ ' <label class="form-label form-label-sm">Frame step</label>'
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_step" value="' + obj_camera.nb_frame_step + '" min="1">'
+ " </div>"
+ ' <div class="col-12">'
+ ' <label class="form-label form-label-sm">Format</label>'
+ ' <select class="form-select form-select-sm bg-dark text-light border-secondary" id="select_format">'
@@ -69,8 +73,12 @@ const CameraConfig = {
obj_cam.nb_resolution_x = parseInt(document.getElementById("input_res_x").value, 10) || 1920;
obj_cam.nb_resolution_y = parseInt(document.getElementById("input_res_y").value, 10) || 1080;
obj_cam.nb_frame_start = parseInt(document.getElementById("input_frame_start").value, 10) || 1;
obj_cam.nb_frame_end = parseInt(document.getElementById("input_frame_end").value, 10) || 250;
let nb_parsed_start = parseInt(document.getElementById("input_frame_start").value, 10);
obj_cam.nb_frame_start = isNaN(nb_parsed_start) ? 1 : nb_parsed_start;
let nb_parsed_end = parseInt(document.getElementById("input_frame_end").value, 10);
obj_cam.nb_frame_end = isNaN(nb_parsed_end) ? 250 : nb_parsed_end;
let nb_parsed_step = parseInt(document.getElementById("input_frame_step").value, 10);
obj_cam.nb_frame_step = isNaN(nb_parsed_step) || nb_parsed_step < 1 ? 1 : nb_parsed_step;
obj_cam.str_format = document.getElementById("select_format").value;
ConsoleLog.add("Config appliquee pour " + obj_cam.str_name);

View File

@@ -7,17 +7,24 @@ const CameraList = {
CameraList.fn_on_select = fn_on_select;
},
set_cameras: (list_names) => {
set_cameras: (list_names, obj_scene) => {
CameraList.list_cameras = [];
let nb_res_x = (obj_scene && obj_scene.nb_resolution_x) || 1920;
let nb_res_y = (obj_scene && obj_scene.nb_resolution_y) || 1080;
let nb_start = obj_scene && obj_scene.nb_frame_start !== undefined ? obj_scene.nb_frame_start : 1;
let nb_end = (obj_scene && obj_scene.nb_frame_end) || 250;
let nb_step = (obj_scene && obj_scene.nb_frame_step) || 1;
for (let str_name of list_names) {
CameraList.list_cameras.push({
str_name: str_name,
is_enabled: true,
nb_resolution_x: 1920,
nb_resolution_y: 1080,
nb_frame_start: 1,
nb_frame_end: 250,
nb_resolution_x: nb_res_x,
nb_resolution_y: nb_res_y,
nb_frame_start: nb_start,
nb_frame_end: nb_end,
nb_frame_step: nb_step,
str_format: "PNG",
});
}

View File

@@ -38,7 +38,7 @@ const ProgressBar = {
obj_camera_label.textContent = str_camera;
let obj_frame_label = document.getElementById("label_current_frame");
obj_frame_label.textContent = nb_frame > 0 ? 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 || []);
},

View File

@@ -23,7 +23,8 @@ const RenderQueue = {
if (str_mode === "camera_by_camera") {
for (let obj_cam of list_enabled) {
for (let nb_frame = obj_cam.nb_frame_start; nb_frame <= obj_cam.nb_frame_end; nb_frame++) {
let nb_step = obj_cam.nb_frame_step || 1;
for (let nb_frame = obj_cam.nb_frame_start; nb_frame <= obj_cam.nb_frame_end; nb_frame += nb_step) {
RenderQueue.list_items.push({
str_camera: obj_cam.str_name,
nb_frame: nb_frame,
@@ -50,7 +51,8 @@ const RenderQueue = {
for (let nb_frame = nb_min; nb_frame <= nb_max; nb_frame++) {
for (let obj_cam of list_enabled) {
if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end) {
let nb_cam_step = obj_cam.nb_frame_step || 1;
if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end && (nb_frame - obj_cam.nb_frame_start) % nb_cam_step === 0) {
RenderQueue.list_items.push({
str_camera: obj_cam.str_name,
nb_frame: nb_frame,

View File

@@ -0,0 +1,132 @@
const UpdateBanner = {
obj_banner: null,
str_pending_tag: null,
init: () => {
UpdateBanner._create_dom();
UpdateBanner._bind_events();
},
_create_dom: () => {
let obj_banner = document.createElement("div");
obj_banner.id = "update_banner";
obj_banner.className = "update-banner d-none";
obj_banner.innerHTML =
'<div class="d-flex align-items-center justify-content-between px-3 py-2">' +
'<span class="update-banner-text">' +
'<i class="mdi mdi-download-circle-outline me-1"></i>' +
'<span id="update_banner_message">Mise a jour disponible</span>' +
'</span>' +
'<div class="d-flex align-items-center gap-2">' +
'<button id="btn_update_install" class="btn btn-sm btn-light">' +
'<i class="mdi mdi-download me-1"></i>Installer' +
'</button>' +
'<button id="btn_update_close" class="btn btn-sm btn-outline-light border-0">' +
'<i class="mdi mdi-close"></i>' +
'</button>' +
'</div>' +
'</div>';
let obj_nav = document.querySelector("nav.navbar");
obj_nav.parentNode.insertBefore(obj_banner, obj_nav.nextSibling);
UpdateBanner.obj_banner = obj_banner;
},
_bind_events: () => {
let obj_btn_install = document.getElementById("btn_update_install");
obj_btn_install.addEventListener("click", () => {
UpdateBanner._on_install_click();
});
let obj_btn_close = document.getElementById("btn_update_close");
obj_btn_close.addEventListener("click", () => {
UpdateBanner._hide();
});
window.api.on_update_available((obj_data) => {
UpdateBanner.str_pending_tag = obj_data.str_tag_name;
UpdateBanner._show("Version " + obj_data.str_version + " disponible");
});
window.api.on_update_progress((obj_data) => {
UpdateBanner._show_progress(obj_data.str_step, obj_data.nb_percent);
});
window.api.on_update_error((obj_data) => {
UpdateBanner.str_pending_tag = obj_data.str_tag_name || UpdateBanner.str_pending_tag;
UpdateBanner._show_error(obj_data.str_message);
});
},
_show: (str_message) => {
let obj_message = document.getElementById("update_banner_message");
obj_message.textContent = str_message;
let obj_btn_install = document.getElementById("btn_update_install");
obj_btn_install.innerHTML = '<i class="mdi mdi-download me-1"></i>Installer';
obj_btn_install.disabled = false;
obj_btn_install.className = "btn btn-sm btn-light";
let obj_btn_close = document.getElementById("btn_update_close");
obj_btn_close.classList.remove("d-none");
UpdateBanner.obj_banner.classList.remove("d-none", "update-banner-error");
document.body.classList.add("has-update-banner");
},
_hide: () => {
UpdateBanner.obj_banner.classList.add("d-none");
document.body.classList.remove("has-update-banner");
},
_on_install_click: () => {
if (!UpdateBanner.str_pending_tag) {
return;
}
let obj_btn_install = document.getElementById("btn_update_install");
obj_btn_install.disabled = true;
obj_btn_install.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>0%';
let obj_btn_close = document.getElementById("btn_update_close");
obj_btn_close.classList.add("d-none");
window.api.apply_update(UpdateBanner.str_pending_tag)
.catch(() => {});
},
_show_progress: (str_step, nb_percent) => {
let obj_btn_install = document.getElementById("btn_update_install");
let obj_message = document.getElementById("update_banner_message");
let str_label = "Mise a jour";
if (str_step === "downloading") {
str_label = "Telechargement";
} else if (str_step === "extracting") {
str_label = "Extraction";
} else if (str_step === "installing") {
str_label = "Installation";
} else if (str_step === "restarting") {
str_label = "Redemarrage";
}
obj_message.textContent = str_label + "...";
obj_btn_install.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>' + nb_percent + "%";
obj_btn_install.disabled = true;
},
_show_error: (str_message) => {
let obj_message = document.getElementById("update_banner_message");
obj_message.textContent = "Erreur : " + str_message;
UpdateBanner.obj_banner.classList.add("update-banner-error");
let obj_btn_install = document.getElementById("btn_update_install");
obj_btn_install.innerHTML = '<i class="mdi mdi-refresh me-1"></i>Reessayer';
obj_btn_install.disabled = false;
obj_btn_install.className = "btn btn-sm btn-outline-light";
let obj_btn_close = document.getElementById("btn_update_close");
obj_btn_close.classList.remove("d-none");
},
};

View File

@@ -12,6 +12,29 @@ body {
overflow: hidden;
}
body.has-update-banner .container-fluid {
height: calc(100vh - 56px - 42px);
}
/* ── Update Banner ─────────────────────────────────────────── */
.update-banner {
background-color: #1a4d2e;
border-bottom: 1px solid #2d6b42;
color: #d4edda;
font-size: 0.8rem;
}
.update-banner.update-banner-error {
background-color: #4d1a1a;
border-bottom-color: #6b2d2d;
color: #f5c6cb;
}
.update-banner-text {
font-weight: 500;
}
.row {
height: 100%;
}