Info-Menü mit Benutzerhandbuch/Disclaimer-Dialog ergaenzen
Neues (i)-Symbol in der Titelleiste öffnet ein Dropdown mit den Einträgen "Benutzerhandbuch" und "Disclaimer". Klick auf einen Eintrag zeigt den jeweiligen Text als modalen, scrollbaren Dialog (marked wandelt die .md-Dateien zur Laufzeit im Main-Prozess in HTML um). Beide Dateien sind jetzt auch in build.files gelistet, damit sie im gepackten Release verfügbar sind. Claude-Session: https://claude.ai/code/session_01VeqoaAZDVSCU4QBtiYDC5C
This commit is contained in:
Generated
+15
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; script-src 'self';" />
|
||||
<title>Info</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="titlebar">
|
||||
<span class="title" id="title">Info</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-btn" id="btn-close" title="Schließen" aria-label="Schließen">✕</button>
|
||||
</header>
|
||||
|
||||
<main class="content" id="content"></main>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,188 @@
|
||||
/* Benutzerhandbuch-/Disclaimer-Fenster — gleiche Farbwelt wie das Widget,
|
||||
plus Basis-Typografie für aus Markdown gerendertes HTML. */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
--surface: #1e1e22;
|
||||
--surface-raised: rgba(255, 255, 255, 0.05);
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #c3c2b7;
|
||||
--text-muted: #898781;
|
||||
--hairline: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--accent: #3987e5;
|
||||
|
||||
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--font-mono: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── Titelleiste ─────────────────────────────────────────────────────── */
|
||||
|
||||
.titlebar {
|
||||
-webkit-app-region: drag;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 10px 9px 14px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
-webkit-app-region: no-drag;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Inhalt (aus Markdown gerendertes HTML) ─────────────────────────────── */
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px 22px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.content h1,
|
||||
.content h2,
|
||||
.content h3 {
|
||||
color: var(--text-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
margin: 24px 0 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content h3 {
|
||||
margin: 18px 0 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.content li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.content a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.content code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
background: var(--surface-raised);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
background: var(--surface-raised);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--hairline);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0 0 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.content th,
|
||||
.content td {
|
||||
border: 1px solid var(--hairline);
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.content th {
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell, Notification } = require('electron');
|
||||
const { marked } = require('marked');
|
||||
|
||||
const { Store, loadConfig } = require('./store');
|
||||
const { Collector } = require('./collector');
|
||||
@@ -30,6 +31,16 @@ const SETTINGS_HEIGHT = 660;
|
||||
const ERRORS_WIDTH = 420;
|
||||
const ERRORS_HEIGHT = 480;
|
||||
|
||||
const INFO_WIDTH = 560;
|
||||
const INFO_HEIGHT = 680;
|
||||
|
||||
/** Feste Whitelist — der Renderer kann nur diese beiden Schlüssel anfragen,
|
||||
* nie einen beliebigen Dateipfad. */
|
||||
const INFO_DOCS = {
|
||||
manual: { title: 'Benutzerhandbuch', file: path.join(ROOT, 'BENUTZERHANDBUCH.md') },
|
||||
disclaimer: { title: 'Disclaimer', file: path.join(ROOT, 'DISCLAIMER.md') },
|
||||
};
|
||||
|
||||
/** Wartezeit vor der einmaligen Update-Prüfung — verzögert, damit sie den
|
||||
* Start nicht verlangsamt und nicht mit dem ersten Poll konkurriert. */
|
||||
const UPDATE_CHECK_DELAY_MS = 60 * 1000;
|
||||
@@ -37,6 +48,7 @@ const UPDATE_CHECK_DELAY_MS = 60 * 1000;
|
||||
let win = null;
|
||||
let settingsWin = null;
|
||||
let errorsWin = null;
|
||||
let infoWin = null;
|
||||
let tray = null;
|
||||
let collector = null;
|
||||
let store = null;
|
||||
@@ -364,6 +376,76 @@ function openErrorLog() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Escaped Text für die Einbettung in HTML — nur für die Fehlermeldung unten. */
|
||||
function escapeHtml(text) {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzerhandbuch-/Disclaimer-Dialog — ein Fenster für beide Dokumente
|
||||
* (Singleton wie oben), Titel und Inhalt werden bei jedem Öffnen aktualisiert.
|
||||
* Modal relativ zum Hauptfenster, ausschließlich mit Schließen-Button.
|
||||
*/
|
||||
function openInfoDoc(kind) {
|
||||
const doc = INFO_DOCS[kind];
|
||||
if (!doc) return;
|
||||
|
||||
let html;
|
||||
try {
|
||||
const raw = fs.readFileSync(doc.file, 'utf8');
|
||||
html = marked.parse(raw);
|
||||
} catch (err) {
|
||||
html = `<p>Datei konnte nicht gelesen werden: ${escapeHtml(err.message)}</p>`;
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="info-wrap" id="info-wrap">
|
||||
<button class="icon-btn" id="btn-info" title="Info" aria-label="Info">ⓘ</button>
|
||||
<div class="info-menu" id="info-menu" hidden>
|
||||
<button class="info-menu-item" id="info-menu-manual">Benutzerhandbuch</button>
|
||||
<button class="info-menu-item" id="info-menu-disclaimer">Disclaimer</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="icon-btn" id="btn-hide" title="Ausblenden (über das Tray-Symbol zurückholen)">–</button>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -157,6 +157,47 @@ body {
|
||||
color: var(--status-update);
|
||||
}
|
||||
|
||||
.info-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 148px;
|
||||
padding: 4px;
|
||||
background: var(--surface-solid);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.info-menu-item {
|
||||
-webkit-app-region: no-drag;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-menu-item:hover {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Meter ───────────────────────────────────────────────────────────── */
|
||||
|
||||
.meters {
|
||||
|
||||
Reference in New Issue
Block a user