Windows support: cross-built NSIS installer

- toFileURL and path-basename handling made Windows-safe (backslashes,
  drive letters) so media/model loading works there
- Blender discovery scans Program Files\Blender Foundation; ffmpeg
  candidates include common Windows paths
- build config: win/nsis target (x64, custom install dir), build/icon.ico
  generated from the 1024 png, dist:win script
Cross-compiled from macOS: dist/DJ Visualizer Setup 1.6.0.exe (95MB) with
scripts unpacked and models/MediaPipe bundled. Untested on real Windows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-31 14:44:47 +02:00
1 parent a869d5b969
commit 7414dbe2ec
5 files changed
+38 -9

No files matched your search

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

+11 -1
View File
@@ -114,6 +114,15 @@ async function ensureMicAccess() {
function blenderPath() { function blenderPath() {
const cands = ['/Applications/Blender.app/Contents/MacOS/Blender', const cands = ['/Applications/Blender.app/Contents/MacOS/Blender',
'/opt/homebrew/bin/blender', '/usr/local/bin/blender']; '/opt/homebrew/bin/blender', '/usr/local/bin/blender'];
if (process.platform === 'win32') {
// Blender installs versioned folders under Program Files
for (const root of ['C:\\Program Files\\Blender Foundation', 'C:\\Program Files (x86)\\Blender Foundation']) {
try {
for (const d of fs.readdirSync(root))
cands.push(path.join(root, d, 'blender.exe'));
} catch (e) { /* root missing */ }
}
}
for (const c of cands) if (fs.existsSync(c)) return c; for (const c of cands) if (fs.existsSync(c)) return c;
return null; return null;
} }
@@ -209,7 +218,8 @@ function ffmpegPath() {
const cands = []; const cands = [];
try { cands.push(require('ffmpeg-static')); } catch (e) { /* optional */ } try { cands.push(require('ffmpeg-static')); } catch (e) { /* optional */ }
cands.push(process.env.FFMPEG, 'ffmpeg', cands.push(process.env.FFMPEG, 'ffmpeg',
'/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/usr/bin/ffmpeg'); '/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/usr/bin/ffmpeg',
'C:\\ffmpeg\\bin\\ffmpeg.exe', 'C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe');
for (const c of cands) { for (const c of cands) {
if (!c) continue; if (!c) continue;
if (c === 'ffmpeg' || fs.existsSync(c)) return c; if (c === 'ffmpeg' || fs.existsSync(c)) return c;
+17 -1
View File
@@ -10,7 +10,8 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"dist": "electron-builder --mac" "dist": "electron-builder --mac",
"dist:win": "electron-builder --win"
}, },
"build": { "build": {
"appId": "com.djvisualizer.app", "appId": "com.djvisualizer.app",
@@ -22,6 +23,21 @@
"icon": "build/icon.icns", "icon": "build/icon.icns",
"identity": null "identity": null
}, },
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"icon": "build/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"files": [ "files": [
"main.js", "main.js",
"preload.js", "preload.js",
+4 -4
View File
@@ -250,8 +250,8 @@ function glbLabel(name) {
: 'Il modello ruota a tempo di musica nella famiglia “Modello 3D”.'; : 'Il modello ruota a tempo di musica nella famiglia “Modello 3D”.';
} }
if (glbPath) { if (glbPath) {
glbLabel(glbPath.split('/').pop()); glbLabel(glbPath.split(/[\\/]/).pop());
if (!glbModels.some(m => m.path === glbPath)) addGlbModel(glbPath.split('/').pop(), glbPath); if (!glbModels.some(m => m.path === glbPath)) addGlbModel(glbPath.split(/[\\/]/).pop(), glbPath);
} }
renderGlbModels(); renderGlbModels();
// every shader family can be a backdrop too (fluid/interactive/3D excluded: // every shader family can be a backdrop too (fluid/interactive/3D excluded:
@@ -292,7 +292,7 @@ $('#anim-input').addEventListener('change', async (e) => {
if (res && res.ok) { if (res && res.ok) {
if (!glbAnimFiles.includes(res.path)) glbAnimFiles.push(res.path); if (!glbAnimFiles.includes(res.path)) glbAnimFiles.push(res.path);
send({ type: 'animAdd', path: res.path }); send({ type: 'animAdd', path: res.path });
glbLabel(glbPath ? glbPath.split('/').pop() : ''); glbLabel(glbPath ? glbPath.split(/[\\/]/).pop() : '');
} else { } else {
$('#glb-name').textContent = '⚠️ ' + ((res && res.error) || 'conversione fallita'); $('#glb-name').textContent = '⚠️ ' + ((res && res.error) || 'conversione fallita');
} }
@@ -784,7 +784,7 @@ function refreshScenePreview(i) {
advanceCues(playCur || 0); advanceCues(playCur || 0);
} }
const baseName = (p) => p.split('/').pop(); const baseName = (p) => p.split(/[\\/]/).pop();
function probePaths(paths) { function probePaths(paths) {
const todo = paths.filter(p => p && !(p in durations)); const todo = paths.filter(p => p && !(p in durations));
+6 -3
View File
@@ -17,9 +17,12 @@ try {
const audio = new AudioEngine(); const audio = new AudioEngine();
let speed = 1.0; let speed = 1.0;
// Convert an absolute filesystem path into a file:// URL the renderer can load. // Convert an absolute filesystem path into a file:// URL the renderer can
// load (Windows: backslashes -> slashes, drive letter needs a leading /).
function toFileURL(p) { function toFileURL(p) {
return encodeURI('file://' + p).replace(/#/g, '%23').replace(/\?/g, '%3F'); let n = String(p).replace(/\\/g, '/');
if (n[0] !== '/') n = '/' + n;
return encodeURI('file://' + n).replace(/#/g, '%23').replace(/\?/g, '%3F');
} }
// ---------------------------------------------------------------- overlays // ---------------------------------------------------------------- overlays
@@ -341,7 +344,7 @@ djv.onControl(async (m) => {
const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const ok = viz.setModelData(buf); const ok = viz.setModelData(buf);
if (ok === false) throw new Error((viz.model3d && viz.model3d.loadError) || 'GLB non valido'); if (ok === false) throw new Error((viz.model3d && viz.model3d.loadError) || 'GLB non valido');
djv.report({ type: 'glbLoaded', name: m.path.split('/').pop() }); djv.report({ type: 'glbLoaded', name: m.path.split(/[\\/]/).pop() });
reportClips(); reportClips();
hideHint(); hideHint();
} catch (e) { djv.report({ type: 'error', message: 'GLB: ' + e.message }); } } catch (e) { djv.report({ type: 'error', message: 'GLB: ' + e.message }); }