Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0e7aa8f52 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmox-spice-client",
|
"name": "proxmox-spice-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "VDI SPICE Client f\u00fcr Proxmox",
|
"description": "VDI SPICE Client f\u00fcr Proxmox",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+65
-12
@@ -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 ----------
|
// ---------- SPICE helpers ----------
|
||||||
|
|
||||||
function buildVVFile(params, vmid) {
|
function buildVVFile(params, vmid) {
|
||||||
@@ -187,24 +209,55 @@ function buildVVFile(params, vmid) {
|
|||||||
return vvPath;
|
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) {
|
function launchRemoteViewer(vvPath) {
|
||||||
const candidates =
|
let candidates;
|
||||||
process.platform === 'win32'
|
if (process.platform === 'win32') {
|
||||||
? [
|
const customPath = store.get('viewerPath', null);
|
||||||
'remote-viewer',
|
candidates = [
|
||||||
path.join('C:', 'Program Files', 'VirtViewer', 'bin', 'remote-viewer.exe'),
|
...(customPath ? [customPath] : []),
|
||||||
path.join('C:', 'Program Files (x86)', 'VirtViewer', 'bin', 'remote-viewer.exe'),
|
'remote-viewer',
|
||||||
]
|
...findWindowsViewerCandidates(),
|
||||||
: ['remote-viewer', 'virt-viewer'];
|
];
|
||||||
|
} else {
|
||||||
|
candidates = ['remote-viewer', 'virt-viewer'];
|
||||||
|
}
|
||||||
|
|
||||||
function tryNext(i) {
|
function tryNext(i) {
|
||||||
if (i >= candidates.length) {
|
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(
|
dialog.showErrorBox(
|
||||||
'remote-viewer nicht gefunden',
|
'remote-viewer nicht gefunden',
|
||||||
'Bitte installiere virt-viewer:\n\n' +
|
'Bitte installiere virt-viewer:\n\n' + hint + '\n\nDanach bitte erneut verbinden.'
|
||||||
' Linux: sudo apt install virt-viewer\n' +
|
|
||||||
' Windows: https://virt-manager.org/download/\n\n' +
|
|
||||||
'Danach bitte erneut verbinden.'
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,10 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
connect: (vm) => ipcRenderer.invoke('proxmox:connect', vm),
|
connect: (vm) => ipcRenderer.invoke('proxmox:connect', vm),
|
||||||
logout: () => ipcRenderer.invoke('proxmox:logout'),
|
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'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -292,3 +292,59 @@ body.vms-page {
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
color: #3a4a60;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Virtuelle Maschinen</h2>
|
<h2>Virtuelle Maschinen</h2>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<button id="settingsBtn" class="btn-secondary hidden" title="Viewer-Pfad einstellen">⚙ Einstellungen</button>
|
||||||
<button id="refreshBtn" class="btn-secondary" title="Liste aktualisieren">↻ Aktualisieren</button>
|
<button id="refreshBtn" class="btn-secondary" title="Liste aktualisieren">↻ Aktualisieren</button>
|
||||||
<button id="logoutBtn" class="btn-secondary">Abmelden</button>
|
<button id="logoutBtn" class="btn-secondary">Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,6 +43,26 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings modal (Windows only — VirtViewer's install folder name changes per version) -->
|
||||||
|
<div id="settingsOverlay" class="modal-overlay hidden">
|
||||||
|
<div class="modal-card">
|
||||||
|
<h3>Viewer-Pfad</h3>
|
||||||
|
<p class="hint">
|
||||||
|
Wird VirtViewer (remote-viewer.exe) nicht automatisch gefunden, kannst du hier
|
||||||
|
den Pfad zur .exe manuell festlegen.
|
||||||
|
</p>
|
||||||
|
<div class="field">
|
||||||
|
<label>Aktueller Pfad</label>
|
||||||
|
<div id="viewerPathDisplay" class="path-display">automatisch erkannt</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="browseViewerBtn" class="btn-secondary">Durchsuchen…</button>
|
||||||
|
<button id="resetViewerBtn" class="btn-secondary">Zurücksetzen</button>
|
||||||
|
<button id="closeSettingsBtn" class="btn-primary modal-close">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="vms.js"></script>
|
<script src="vms.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ const emptyEl = document.getElementById('emptyState');
|
|||||||
const errorEl = document.getElementById('error');
|
const errorEl = document.getElementById('error');
|
||||||
const noticeEl = document.getElementById('notice');
|
const noticeEl = document.getElementById('notice');
|
||||||
const refreshBtn = document.getElementById('refreshBtn');
|
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 () => {
|
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||||||
await api.proxmox.logout();
|
await api.proxmox.logout();
|
||||||
@@ -13,6 +16,42 @@ document.getElementById('logoutBtn').addEventListener('click', async () => {
|
|||||||
|
|
||||||
refreshBtn.addEventListener('click', () => loadVMs());
|
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 ─────────────────────────────────────────
|
// ── Load on mount ─────────────────────────────────────────
|
||||||
|
|
||||||
async function loadVMs() {
|
async function loadVMs() {
|
||||||
|
|||||||
Reference in New Issue
Block a user