From a0e7aa8f52fdfa1389107de988bd6a0c38fe23de Mon Sep 17 00:00:00 2001 From: ARIA Date: Mon, 6 Jul 2026 10:02:53 +0200 Subject: [PATCH] Fix VirtViewer detection on Windows + manual path override VirtViewer's installer folder name includes the version number (e.g. "VirtViewer v11.0-256"), so the previous hardcoded path never matched a real install. Now scans Program Files for any VirtViewer* folder, and adds a Settings dialog (Windows only) to pick the remote-viewer.exe path manually as a fallback. --- package.json | 2 +- src/main.js | 77 ++++++++++++++++++++++++++++++++++------- src/preload.js | 6 ++++ src/renderer/styles.css | 56 ++++++++++++++++++++++++++++++ src/renderer/vms.html | 21 +++++++++++ src/renderer/vms.js | 39 +++++++++++++++++++++ 6 files changed, 188 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 34ea340..4d7a382 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxmox-spice-client", - "version": "1.0.0", + "version": "1.0.1", "description": "VDI SPICE Client f\u00fcr Proxmox", "main": "src/main.js", "scripts": { diff --git a/src/main.js b/src/main.js index 4a82036..79553e0 100644 --- a/src/main.js +++ b/src/main.js @@ -155,6 +155,28 @@ ipcMain.handle('proxmox:connect', async (_e, { node, vmid }) => { } }); +// ---------- IPC: settings ---------- + +ipcMain.handle('settings:platform', () => process.platform); + +ipcMain.handle('settings:getViewerPath', () => store.get('viewerPath', null)); + +ipcMain.handle('settings:clearViewerPath', () => { + store.delete('viewerPath'); +}); + +ipcMain.handle('settings:browseViewerPath', async () => { + const result = await dialog.showOpenDialog(mainWindow, { + title: 'remote-viewer.exe auswählen', + properties: ['openFile'], + filters: [{ name: 'remote-viewer', extensions: ['exe'] }], + }); + if (result.canceled || result.filePaths.length === 0) return null; + const chosen = result.filePaths[0]; + store.set('viewerPath', chosen); + return chosen; +}); + // ---------- SPICE helpers ---------- function buildVVFile(params, vmid) { @@ -187,24 +209,55 @@ function buildVVFile(params, vmid) { return vvPath; } +// VirtViewer's Windows installer names its folder after the bundled version +// (e.g. "VirtViewer v11.0-256"), so a fixed path breaks on every version bump. +// Scan the two Program Files dirs for any "VirtViewer*" folder instead. +function findWindowsViewerCandidates() { + const roots = [ + process.env['ProgramFiles'] || 'C:\\Program Files', + process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', + ]; + const found = []; + for (const root of roots) { + let entries; + try { + entries = fs.readdirSync(root, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (!entry.isDirectory() || !/^virtviewer/i.test(entry.name)) continue; + const exe = path.join(root, entry.name, 'bin', 'remote-viewer.exe'); + if (fs.existsSync(exe)) found.push(exe); + } + } + return found; +} + function launchRemoteViewer(vvPath) { - const candidates = - process.platform === 'win32' - ? [ - 'remote-viewer', - path.join('C:', 'Program Files', 'VirtViewer', 'bin', 'remote-viewer.exe'), - path.join('C:', 'Program Files (x86)', 'VirtViewer', 'bin', 'remote-viewer.exe'), - ] - : ['remote-viewer', 'virt-viewer']; + let candidates; + if (process.platform === 'win32') { + const customPath = store.get('viewerPath', null); + candidates = [ + ...(customPath ? [customPath] : []), + 'remote-viewer', + ...findWindowsViewerCandidates(), + ]; + } else { + candidates = ['remote-viewer', 'virt-viewer']; + } function tryNext(i) { if (i >= candidates.length) { + const hint = + process.platform === 'win32' + ? 'Windows: https://virt-manager.org/download/\n\n' + + 'Falls VirtViewer an einem nicht-standardmäßigen Ort installiert ist, ' + + 'trage den Pfad unter „Einstellungen → Viewer-Pfad" ein.' + : 'Linux: sudo apt install virt-viewer'; dialog.showErrorBox( 'remote-viewer nicht gefunden', - 'Bitte installiere virt-viewer:\n\n' + - ' Linux: sudo apt install virt-viewer\n' + - ' Windows: https://virt-manager.org/download/\n\n' + - 'Danach bitte erneut verbinden.' + 'Bitte installiere virt-viewer:\n\n' + hint + '\n\nDanach bitte erneut verbinden.' ); return; } diff --git a/src/preload.js b/src/preload.js index f615c69..1f6114f 100644 --- a/src/preload.js +++ b/src/preload.js @@ -12,4 +12,10 @@ contextBridge.exposeInMainWorld('api', { connect: (vm) => ipcRenderer.invoke('proxmox:connect', vm), logout: () => ipcRenderer.invoke('proxmox:logout'), }, + settings: { + platform: () => ipcRenderer.invoke('settings:platform'), + getViewerPath: () => ipcRenderer.invoke('settings:getViewerPath'), + browseViewerPath: () => ipcRenderer.invoke('settings:browseViewerPath'), + clearViewerPath: () => ipcRenderer.invoke('settings:clearViewerPath'), + }, }); diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 2ecf417..ccc54d3 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -292,3 +292,59 @@ body.vms-page { margin-top: 0.5rem; color: #3a4a60; } + +/* ── Settings modal ───────────────────────────────────────── */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(5, 8, 16, 0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.modal-card { + width: 420px; + max-width: 90vw; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 14px; + padding: 1.6rem 1.7rem; + box-shadow: 0 24px 60px rgba(0,0,0,0.55); +} + +.modal-card h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.6rem; +} + +.modal-card .hint { + font-size: 0.8rem; + color: var(--muted); + margin-bottom: 1.1rem; + line-height: 1.5; +} + +.path-display { + padding: 0.6rem 0.85rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 7px; + font-size: 0.82rem; + color: var(--text); + word-break: break-all; +} + +.modal-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1.4rem; +} + +.modal-actions .modal-close { + width: auto; + padding: 0.42rem 1rem; +} diff --git a/src/renderer/vms.html b/src/renderer/vms.html index fd85796..c2d2097 100644 --- a/src/renderer/vms.html +++ b/src/renderer/vms.html @@ -14,6 +14,7 @@ + + + diff --git a/src/renderer/vms.js b/src/renderer/vms.js index e8e77fc..a83e19d 100644 --- a/src/renderer/vms.js +++ b/src/renderer/vms.js @@ -6,6 +6,9 @@ const emptyEl = document.getElementById('emptyState'); const errorEl = document.getElementById('error'); const noticeEl = document.getElementById('notice'); const refreshBtn = document.getElementById('refreshBtn'); +const settingsBtn = document.getElementById('settingsBtn'); +const settingsOverlay = document.getElementById('settingsOverlay'); +const viewerPathDisplay = document.getElementById('viewerPathDisplay'); document.getElementById('logoutBtn').addEventListener('click', async () => { await api.proxmox.logout(); @@ -13,6 +16,42 @@ document.getElementById('logoutBtn').addEventListener('click', async () => { refreshBtn.addEventListener('click', () => loadVMs()); +// ── Settings (Windows only — VirtViewer's install folder name carries the +// version number, so auto-detection can miss non-standard installs) ── + +initSettings(); + +async function initSettings() { + const platform = await api.settings.platform(); + if (platform !== 'win32') return; + + settingsBtn.classList.remove('hidden'); + settingsBtn.addEventListener('click', openSettings); + document.getElementById('closeSettingsBtn').addEventListener('click', closeSettings); + document.getElementById('browseViewerBtn').addEventListener('click', async () => { + const chosen = await api.settings.browseViewerPath(); + if (chosen) await refreshViewerPathDisplay(); + }); + document.getElementById('resetViewerBtn').addEventListener('click', async () => { + await api.settings.clearViewerPath(); + await refreshViewerPathDisplay(); + }); +} + +async function openSettings() { + await refreshViewerPathDisplay(); + settingsOverlay.classList.remove('hidden'); +} + +function closeSettings() { + settingsOverlay.classList.add('hidden'); +} + +async function refreshViewerPathDisplay() { + const viewerPath = await api.settings.getViewerPath(); + viewerPathDisplay.textContent = viewerPath || 'automatisch erkannt'; +} + // ── Load on mount ───────────────────────────────────────── async function loadVMs() {