Upload via GUI (85 Dateien)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Eigenes Menüfenster für das Tray-Symbol.
|
||||
// Das native Electron-Menü zeichnet unter Windows keine Hover-Markierung; dieses kleine Fenster
|
||||
// (renderer/tray-menu.html) übernimmt Aussehen und Markierung selbst.
|
||||
|
||||
const { BrowserWindow, screen, nativeTheme, ipcMain } = require('electron');
|
||||
|
||||
const WIDTH = 286; // Fensterbreite inkl. 8 px Rand für den Schatten
|
||||
const DEFAULT_HEIGHT = 320;
|
||||
const MARGIN = 8;
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(value, Math.max(min, max)));
|
||||
}
|
||||
|
||||
// Position des Menüs: am Mauszeiger, innerhalb des sichtbaren Bereichs. Liegt der Zeiger in der
|
||||
// Taskleiste (unter/über dem Arbeitsbereich), sitzt das Menü direkt daneben.
|
||||
function computePosition(point, workArea, width, height) {
|
||||
const x = clamp(point.x - Math.round(width / 2), workArea.x + MARGIN, workArea.x + workArea.width - width - MARGIN);
|
||||
let y;
|
||||
if (point.y >= workArea.y + workArea.height) y = workArea.y + workArea.height - height - MARGIN;
|
||||
else if (point.y <= workArea.y) y = workArea.y + MARGIN;
|
||||
else y = clamp(point.y - Math.round(height / 2), workArea.y + MARGIN, workArea.y + workArea.height - height - MARGIN);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function createTrayPopup({ htmlPath, preloadPath, getModel, onAction }) {
|
||||
let win = null;
|
||||
let height = DEFAULT_HEIGHT;
|
||||
let shownAt = 0;
|
||||
let handlersRegistered = false;
|
||||
|
||||
const own = (event) => win && !win.isDestroyed() && event.sender === win.webContents;
|
||||
|
||||
function registerHandlers() {
|
||||
if (handlersRegistered) return;
|
||||
handlersRegistered = true;
|
||||
ipcMain.on('tray-menu:action', (event, id) => {
|
||||
if (!own(event)) return;
|
||||
hide();
|
||||
try { onAction(String(id)); } catch (e) { console.warn('[Tray] Aktion fehlgeschlagen:', e.message); }
|
||||
});
|
||||
ipcMain.on('tray-menu:close', (event) => { if (own(event)) hide(); });
|
||||
ipcMain.on('tray-menu:resize', (event, contentHeight) => {
|
||||
if (!own(event)) return;
|
||||
const h = Math.round(Number(contentHeight));
|
||||
if (!(h > 40 && h < 900)) return;
|
||||
height = h;
|
||||
if (win.isVisible()) {
|
||||
const { x, y } = computePosition(screen.getCursorScreenPoint(), screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).workArea, WIDTH, height);
|
||||
win.setBounds({ x, y, width: WIDTH, height });
|
||||
} else {
|
||||
win.setContentSize(WIDTH, height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureWindow() {
|
||||
if (win && !win.isDestroyed()) return win;
|
||||
registerHandlers();
|
||||
win = new BrowserWindow({
|
||||
width: WIDTH,
|
||||
height,
|
||||
show: false,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
movable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
skipTaskbar: true,
|
||||
alwaysOnTop: true,
|
||||
focusable: true,
|
||||
hasShadow: false,
|
||||
webPreferences: {
|
||||
preload: preloadPath,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
spellcheck: false
|
||||
}
|
||||
});
|
||||
win.setAlwaysOnTop(true, 'pop-up-menu');
|
||||
win.on('blur', () => { if (Date.now() - shownAt > 250) hide(); });
|
||||
win.on('closed', () => { win = null; });
|
||||
win.loadFile(htmlPath);
|
||||
return win;
|
||||
}
|
||||
|
||||
function push() {
|
||||
if (!win || win.isDestroyed()) return;
|
||||
win.webContents.send('tray-menu-state', { items: getModel(), dark: nativeTheme.shouldUseDarkColors });
|
||||
}
|
||||
|
||||
function show() {
|
||||
const w = ensureWindow();
|
||||
const send = () => {
|
||||
push();
|
||||
const point = screen.getCursorScreenPoint();
|
||||
const { x, y } = computePosition(point, screen.getDisplayNearestPoint(point).workArea, WIDTH, height);
|
||||
w.setBounds({ x, y, width: WIDTH, height });
|
||||
shownAt = Date.now();
|
||||
w.show();
|
||||
w.focus();
|
||||
};
|
||||
if (w.webContents.isLoading()) w.webContents.once('did-finish-load', send);
|
||||
else send();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (win && !win.isDestroyed() && win.isVisible()) win.hide();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (win && !win.isDestroyed() && win.isVisible()) push();
|
||||
}
|
||||
|
||||
// Lädt das versteckte Fenster im Voraus. Der erste Start des Renderers dauert mehrere Sekunden;
|
||||
// ohne Vorladen würde der erste Rechtsklick auf das Tray-Symbol entsprechend hängen.
|
||||
function warmUp() {
|
||||
ensureWindow();
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (win && !win.isDestroyed()) win.destroy();
|
||||
win = null;
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
warmUp,
|
||||
hide,
|
||||
refresh,
|
||||
destroy,
|
||||
isVisible: () => !!win && !win.isDestroyed() && win.isVisible(),
|
||||
getWindow: () => (win && !win.isDestroyed() ? win : null)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createTrayPopup, computePosition };
|
||||
Reference in New Issue
Block a user