diff --git a/package-lock.json b/package-lock.json index 47c8295..d30fa51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "claude-live-dashboard", "version": "1.2.1", "license": "MIT", + "dependencies": { + "marked": "^18.0.11" + }, "devDependencies": { "electron": "^43.2.0", "electron-builder": "^25.1.8" @@ -3048,6 +3051,18 @@ "node": ">=12" } }, + "node_modules/marked": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.11.tgz", + "integrity": "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 6f605b8..a655dc8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "src/**/*", "assets/**/*", "config.json", + "BENUTZERHANDBUCH.md", + "DISCLAIMER.md", "!**/*.map" ], "win": { @@ -97,5 +99,8 @@ }, "artifactName": "${productName}-${version}-${arch}.${ext}" } + }, + "dependencies": { + "marked": "^18.0.11" } } diff --git a/src/info-preload.js b/src/info-preload.js new file mode 100644 index 0000000..0574d89 --- /dev/null +++ b/src/info-preload.js @@ -0,0 +1,14 @@ +'use strict'; + +/** Brücke für den Benutzerhandbuch-/Disclaimer-Dialog. Bewusst getrennt vom Widget-Preload. */ + +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('infoBridge', { + /** Registriert den Empfänger für Titel/Inhalt, gesendet bei jedem Öffnen. */ + onContent(callback) { + ipcRenderer.on('info:content', (_event, data) => callback(data)); + }, + /** Schließt das Fenster. */ + close: () => ipcRenderer.send('info:close'), +}); diff --git a/src/info/app.js b/src/info/app.js new file mode 100644 index 0000000..06c8091 --- /dev/null +++ b/src/info/app.js @@ -0,0 +1,27 @@ +'use strict'; + +/** + * Benutzerhandbuch-/Disclaimer-Dialog. Zeigt nur an, was der Main-Prozess + * schickt — Datei lesen und Markdown→HTML wandeln passiert dort (siehe + * `openInfoDoc()` in `src/main/index.js`). + */ + +const $ = (id) => document.getElementById(id); + +const el = { + title: $('title'), + content: $('content'), + close: $('btn-close'), +}; + +window.infoBridge.onContent(({ title, html }) => { + document.title = title; + el.title.textContent = title; + el.content.innerHTML = html; + el.content.scrollTop = 0; +}); + +el.close.addEventListener('click', () => window.infoBridge.close()); +document.addEventListener('keydown', (ev) => { + if (ev.key === 'Escape') window.infoBridge.close(); +}); diff --git a/src/info/index.html b/src/info/index.html new file mode 100644 index 0000000..6a548c9 --- /dev/null +++ b/src/info/index.html @@ -0,0 +1,20 @@ + + +
+ + +Datei konnte nicht gelesen werden: ${escapeHtml(err.message)}
`; + } + const send = () => infoWin.webContents.send('info:content', { title: doc.title, html }); + + if (infoWin && !infoWin.isDestroyed()) { + infoWin.show(); + infoWin.focus(); + send(); + return; + } + + const target = win && !win.isDestroyed() + ? screen.getDisplayMatching(win.getBounds()) + : screen.getPrimaryDisplay(); + const area = target.workArea; + + infoWin = new BrowserWindow({ + width: INFO_WIDTH, + height: INFO_HEIGHT, + x: Math.round(area.x + (area.width - INFO_WIDTH) / 2), + y: Math.round(area.y + Math.max(0, (area.height - INFO_HEIGHT) / 2)), + minWidth: 420, + minHeight: 360, + frame: false, + resizable: true, + skipTaskbar: false, + modal: true, + parent: win && !win.isDestroyed() ? win : undefined, + alwaysOnTop: true, + show: false, + backgroundColor: '#1e1e22', + icon: path.join(ROOT, 'assets', 'icon.png'), + webPreferences: { + preload: path.join(__dirname, '..', 'info-preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }); + + infoWin.loadFile(path.join(__dirname, '..', 'info', 'index.html')); + infoWin.webContents.once('did-finish-load', send); + infoWin.once('ready-to-show', () => infoWin.show()); + infoWin.on('closed', () => { + infoWin = null; + }); +} + /** * Übernimmt geänderte Einstellungen ohne Neustart und schreibt sie in die * Datei. Unbekannte Felder und die `_`-Kommentare bleiben erhalten, weil die @@ -419,6 +501,9 @@ function registerIpc() { const info = collector?.state?.update; if (info?.available && info.htmlUrl) shell.openExternal(info.htmlUrl); }); + ipcMain.on('open-manual', () => openInfoDoc('manual')); + ipcMain.on('open-disclaimer', () => openInfoDoc('disclaimer')); + ipcMain.on('info:close', () => infoWin && infoWin.close()); ipcMain.handle('settings:load', () => ({ config, diff --git a/src/preload.js b/src/preload.js index cb4904a..260eba1 100644 --- a/src/preload.js +++ b/src/preload.js @@ -26,4 +26,8 @@ contextBridge.exposeInMainWorld('dashboard', { openErrorLog: () => ipcRenderer.send('open-error-log'), /** Öffnet die Release-Seite eines gefundenen neuen Updates im Browser. */ openReleasePage: () => ipcRenderer.send('open-release-page'), + /** Öffnet den Benutzerhandbuch-Dialog. */ + openManual: () => ipcRenderer.send('open-manual'), + /** Öffnet den Disclaimer-Dialog. */ + openDisclaimer: () => ipcRenderer.send('open-disclaimer'), }); diff --git a/src/renderer/app.js b/src/renderer/app.js index 9ddda2d..99d2738 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -48,6 +48,11 @@ const el = { staleBadge: $('stale-badge'), btnEndpointStatus: $('btn-endpoint-status'), btnUpdateAvailable: $('btn-update-available'), + infoWrap: $('info-wrap'), + btnInfo: $('btn-info'), + infoMenu: $('info-menu'), + infoMenuManual: $('info-menu-manual'), + infoMenuDisclaimer: $('info-menu-disclaimer'), btnHide: $('btn-hide'), btnSettings: $('btn-settings'), btnExpand: $('btn-expand'), @@ -475,6 +480,28 @@ el.btnSettings.addEventListener('click', () => window.dashboard.openSettings()); el.btnEndpointStatus.addEventListener('click', () => window.dashboard.openErrorLog()); el.btnUpdateAvailable.addEventListener('click', () => window.dashboard.openReleasePage()); +el.btnInfo.addEventListener('click', () => { + el.infoMenu.hidden = !el.infoMenu.hidden; +}); +document.addEventListener('click', (ev) => { + if (!el.infoMenu.hidden && !el.infoWrap.contains(ev.target)) { + el.infoMenu.hidden = true; + } +}); +document.addEventListener('keydown', (ev) => { + if (ev.key === 'Escape' && !el.infoMenu.hidden) { + el.infoMenu.hidden = true; + } +}); +el.infoMenuManual.addEventListener('click', () => { + el.infoMenu.hidden = true; + window.dashboard.openManual(); +}); +el.infoMenuDisclaimer.addEventListener('click', () => { + el.infoMenu.hidden = true; + window.dashboard.openDisclaimer(); +}); + window.dashboard.onState(render); // Reihenfolge: erst die Ansicht auf die wiederhergestellte Fenstergröße diff --git a/src/renderer/index.html b/src/renderer/index.html index 3c7bfa1..2c62954 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -30,6 +30,13 @@ /> +