Added OS context menu for linux
This commit is contained in:
@@ -293,7 +293,6 @@ function getDesktopViewState(win) {
|
|||||||
|
|
||||||
function createMenuPopupWindow(parentWin) {
|
function createMenuPopupWindow(parentWin) {
|
||||||
const menuWin = new BrowserWindow({
|
const menuWin = new BrowserWindow({
|
||||||
parent: parentWin,
|
|
||||||
modal: false,
|
modal: false,
|
||||||
frame: false,
|
frame: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
@@ -301,7 +300,8 @@ function createMenuPopupWindow(parentWin) {
|
|||||||
show: false,
|
show: false,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
focusable: true,
|
focusable: false,
|
||||||
|
fullscreenable: false,
|
||||||
hasShadow: true,
|
hasShadow: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
@@ -314,6 +314,7 @@ function createMenuPopupWindow(parentWin) {
|
|||||||
|
|
||||||
menuWin.setMenu(null);
|
menuWin.setMenu(null);
|
||||||
try { menuWin.setAlwaysOnTop(true, 'pop-up-menu'); } catch {}
|
try { menuWin.setAlwaysOnTop(true, 'pop-up-menu'); } catch {}
|
||||||
|
try { menuWin.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } catch {}
|
||||||
|
|
||||||
const hideMenu = () => {
|
const hideMenu = () => {
|
||||||
if (!menuWin.isDestroyed()) menuWin.hide();
|
if (!menuWin.isDestroyed()) menuWin.hide();
|
||||||
@@ -334,20 +335,45 @@ function createMenuPopupWindow(parentWin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function positionMenuPopup(parentWin, menuWin, anchorRect) {
|
function positionMenuPopup(parentWin, menuWin, anchorRect) {
|
||||||
if (!parentWin || !menuWin || !anchorRect) return;
|
if (!parentWin || !menuWin) return;
|
||||||
const contentBounds = parentWin.getContentBounds();
|
|
||||||
const display = screen.getDisplayMatching(contentBounds);
|
|
||||||
const workArea = display?.workArea || contentBounds;
|
|
||||||
|
|
||||||
const width = MENU_POPUP_SIZE.width;
|
const width = MENU_POPUP_SIZE.width;
|
||||||
const height = MENU_POPUP_SIZE.height;
|
const height = MENU_POPUP_SIZE.height;
|
||||||
let x = Math.round(contentBounds.x + anchorRect.x + anchorRect.width - width);
|
|
||||||
let y = Math.round(contentBounds.y + anchorRect.y + anchorRect.height + 6);
|
|
||||||
|
|
||||||
if (x < workArea.x) x = workArea.x;
|
const parentBounds = parentWin.getBounds();
|
||||||
if (y < workArea.y) y = workArea.y;
|
const contentBounds = parentWin.getContentBounds();
|
||||||
if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width;
|
|
||||||
if (y + height > workArea.y + workArea.height) y = workArea.y + workArea.height - height;
|
const rect = anchorRect && Number.isFinite(anchorRect.x) && Number.isFinite(anchorRect.y)
|
||||||
|
? anchorRect
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
|
|
||||||
|
if (rect) {
|
||||||
|
x = Math.round(contentBounds.x + rect.x + rect.width - width);
|
||||||
|
y = Math.round(contentBounds.y + rect.y + rect.height + 6);
|
||||||
|
} else {
|
||||||
|
x = Math.round(parentBounds.x + parentBounds.width - width - 12);
|
||||||
|
y = Math.round(parentBounds.y + 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
const display = screen.getDisplayNearestPoint({ x, y });
|
||||||
|
const workArea = display?.workArea || { x: 0, y: 0, width: 1920, height: 1080 };
|
||||||
|
|
||||||
|
if (x < workArea.x) x = workArea.x + 6;
|
||||||
|
if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width - 6;
|
||||||
|
if (y < workArea.y) y = workArea.y + 6;
|
||||||
|
if (y + height > workArea.y + workArea.height) {
|
||||||
|
const aboveY = rect
|
||||||
|
? Math.round(contentBounds.y + rect.y - height - 6)
|
||||||
|
: Math.round(parentBounds.y + parentBounds.height - height - 12);
|
||||||
|
if (aboveY >= workArea.y) {
|
||||||
|
y = aboveY;
|
||||||
|
} else {
|
||||||
|
y = workArea.y + workArea.height - height - 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
menuWin.setBounds({ x, y, width, height }, false);
|
menuWin.setBounds({ x, y, width, height }, false);
|
||||||
}
|
}
|
||||||
@@ -2117,7 +2143,35 @@ ipcMain.handle('browserview-set-bounds', (event, bounds) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Overlay menu (to sit above BrowserView)
|
// Native popup menu for the burger button — renders above BrowserView on all platforms
|
||||||
|
ipcMain.handle('show-app-menu', (event, payload = {}) => {
|
||||||
|
try {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (!win) return;
|
||||||
|
const zoomFactor = win.webContents.getZoomFactor?.() || 1;
|
||||||
|
const zoomLabel = `${Math.round(zoomFactor * 100)}%`;
|
||||||
|
const template = [
|
||||||
|
{ label: 'Settings', click: () => win.webContents.send('menu-command', { cmd: 'open-settings' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: '\u{1F3AE} Big Picture Mode', click: () => win.webContents.send('menu-command', { cmd: 'big-picture' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Toggle Developer Tools', click: () => win.webContents.send('menu-command', { cmd: 'toggle-devtools' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: `Zoom: ${zoomLabel}`, enabled: false },
|
||||||
|
{ label: 'Zoom In', accelerator: 'CmdOrCtrl+=', click: () => win.webContents.send('menu-command', { cmd: 'zoom-in' }) },
|
||||||
|
{ label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', click: () => win.webContents.send('menu-command', { cmd: 'zoom-out' }) },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Hard Reload (Ignore Cache)', click: () => win.webContents.send('menu-command', { cmd: 'hard-reload' }) },
|
||||||
|
{ label: 'Reload Fresh (Add Cache-Buster)', click: () => win.webContents.send('menu-command', { cmd: 'fresh-reload' }) },
|
||||||
|
];
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
menu.popup({ window: win, x: payload.x, y: payload.y });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[show-app-menu] Error:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overlay menu (to sit above BrowserView) — kept for backwards compat but no longer primary
|
||||||
ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
|
ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
|
||||||
try {
|
try {
|
||||||
const parentWin = BrowserWindow.fromWebContents(event.sender);
|
const parentWin = BrowserWindow.fromWebContents(event.sender);
|
||||||
@@ -2134,7 +2188,24 @@ ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
positionMenuPopup(parentWin, menuWin, payload.anchorRect);
|
const anchorRect = payload?.anchorRect;
|
||||||
|
const anchorScreenPoint = payload?.anchorScreenPoint;
|
||||||
|
|
||||||
|
if (anchorScreenPoint && Number.isFinite(anchorScreenPoint.x) && Number.isFinite(anchorScreenPoint.y)) {
|
||||||
|
const width = MENU_POPUP_SIZE.width;
|
||||||
|
const height = MENU_POPUP_SIZE.height;
|
||||||
|
let x = Math.round(anchorScreenPoint.x - width);
|
||||||
|
let y = Math.round(anchorScreenPoint.y + 6);
|
||||||
|
const display = screen.getDisplayNearestPoint({ x, y });
|
||||||
|
const workArea = display?.workArea || { x: 0, y: 0, width: 1920, height: 1080 };
|
||||||
|
if (x < workArea.x) x = workArea.x + 6;
|
||||||
|
if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width - 6;
|
||||||
|
if (y < workArea.y) y = workArea.y + 6;
|
||||||
|
if (y + height > workArea.y + workArea.height) y = workArea.y + workArea.height - height - 6;
|
||||||
|
menuWin.setBounds({ x, y, width, height }, false);
|
||||||
|
} else {
|
||||||
|
positionMenuPopup(parentWin, menuWin, anchorRect);
|
||||||
|
}
|
||||||
|
|
||||||
const initPayload = { theme: payload.theme || null };
|
const initPayload = { theme: payload.theme || null };
|
||||||
const sendInit = () => {
|
const sendInit = () => {
|
||||||
@@ -2148,8 +2219,15 @@ ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof menuWin.showInactive === 'function') {
|
||||||
|
menuWin.showInactive();
|
||||||
|
} else {
|
||||||
menuWin.show();
|
menuWin.show();
|
||||||
menuWin.focus();
|
}
|
||||||
|
} catch {
|
||||||
|
menuWin.show();
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -58,7 +58,10 @@
|
|||||||
"icon": "assets/images/Logos/Nebula-Favicon.ico"
|
"icon": "assets/images/Logos/Nebula-Favicon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"icon": "assets/images/Logos/Nebula-Favicon.png"
|
"icon": "assets/images/Logos/Nebula-Favicon.png",
|
||||||
|
"target": [
|
||||||
|
"AppImage"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@
|
|||||||
<div class="menu-wrapper">
|
<div class="menu-wrapper">
|
||||||
<button id="menu-btn">☰</button>
|
<button id="menu-btn">☰</button>
|
||||||
<div id="menu-popup" class="hidden">
|
<div id="menu-popup" class="hidden">
|
||||||
<button onclick="openSettings()">Settings</button>
|
<button id="open-settings-btn">Settings</button>
|
||||||
<!-- You can add more options here -->
|
<!-- You can add more options here -->
|
||||||
<button id="bigpicture-btn" title="Launch Big Picture Mode (Controller/Steam Deck UI)">🎮 Big Picture Mode</button>
|
<button id="bigpicture-btn" title="Launch Big Picture Mode (Controller/Steam Deck UI)">🎮 Big Picture Mode</button>
|
||||||
<button id="devtools-btn" title="Open / Close Developer Tools">Toggle Developer Tools</button>
|
<button id="devtools-btn" title="Open / Close Developer Tools">Toggle Developer Tools</button>
|
||||||
|
|||||||
+65
-14
@@ -477,8 +477,7 @@ ipcRenderer.on('browserview-host-message', (payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Commands from the overlay menu window
|
// Commands from the overlay menu window
|
||||||
ipcRenderer.on('menu-command', (payload) => {
|
function runMenuCommand(cmd) {
|
||||||
const cmd = payload?.cmd;
|
|
||||||
if (!cmd) return;
|
if (!cmd) return;
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'open-settings':
|
case 'open-settings':
|
||||||
@@ -508,6 +507,45 @@ ipcRenderer.on('menu-command', (payload) => {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLinux = document.body.classList.contains('platform-linux');
|
||||||
|
|
||||||
|
function hideMainMenuPopup() {
|
||||||
|
if (!isLinux) {
|
||||||
|
// Windows/macOS: hide the popup window
|
||||||
|
ipcRenderer.send('menu-popup-hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNativeAppMenu() {
|
||||||
|
if (!menuBtn) return;
|
||||||
|
const rect = menuBtn.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (isLinux) {
|
||||||
|
// Linux: use native OS menu (renders above BrowserView reliably)
|
||||||
|
ipcRenderer.invoke('show-app-menu', {
|
||||||
|
x: Math.round(rect.right - 200),
|
||||||
|
y: Math.round(rect.bottom + 4)
|
||||||
|
}).catch(() => {});
|
||||||
|
} else {
|
||||||
|
// Windows/macOS: use the custom popup window
|
||||||
|
const theme = currentThemeColors ? { colors: currentThemeColors } : null;
|
||||||
|
ipcRenderer.send('menu-popup-toggle', {
|
||||||
|
anchorRect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
|
||||||
|
anchorScreenPoint: {
|
||||||
|
x: Math.round(window.screenX + rect.right),
|
||||||
|
y: Math.round(window.screenY + rect.bottom)
|
||||||
|
},
|
||||||
|
theme
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.on('menu-command', (payload) => {
|
||||||
|
const cmd = payload?.cmd;
|
||||||
|
runMenuCommand(cmd);
|
||||||
|
hideMainMenuPopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-open on download start is disabled by design now.
|
// Auto-open on download start is disabled by design now.
|
||||||
@@ -1145,18 +1183,12 @@ let ringSvgEl = null;
|
|||||||
// Open/close on button click; stop propagation so outside-click handler doesn't immediately close it
|
// Open/close on button click; stop propagation so outside-click handler doesn't immediately close it
|
||||||
menuBtn.addEventListener('click', (e) => {
|
menuBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!menuBtn) return;
|
showNativeAppMenu();
|
||||||
const rect = menuBtn.getBoundingClientRect();
|
|
||||||
const theme = currentThemeColors ? { colors: currentThemeColors } : null;
|
|
||||||
ipcRenderer.send('menu-popup-toggle', {
|
|
||||||
anchorRect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
|
|
||||||
theme
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on Escape key
|
// Close on Escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') ipcRenderer.send('menu-popup-hide');
|
if (e.key === 'Escape') hideMainMenuPopup();
|
||||||
if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
|
if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
|
||||||
hideDownloadsPopup();
|
hideDownloadsPopup();
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1199,7 @@ ipcRenderer.on('browserview-event', (payload) => {
|
|||||||
if (!payload || !payload.type) return;
|
if (!payload || !payload.type) return;
|
||||||
const { tabId, type } = payload;
|
const { tabId, type } = payload;
|
||||||
if (type === 'focus') {
|
if (type === 'focus') {
|
||||||
ipcRenderer.send('menu-popup-hide');
|
hideMainMenuPopup();
|
||||||
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) hideDownloadsPopup();
|
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) hideDownloadsPopup();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1276,14 +1308,33 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
bigPictureBtn.addEventListener('click', async () => {
|
bigPictureBtn.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
await window.bigPictureAPI.launch();
|
await window.bigPictureAPI.launch();
|
||||||
// Close the overlay menu
|
hideMainMenuPopup();
|
||||||
ipcRenderer.send('menu-popup-hide');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to launch Big Picture Mode:', e);
|
console.error('Failed to launch Big Picture Mode:', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (menuPopup) {
|
||||||
|
menuPopup.addEventListener('click', (e) => {
|
||||||
|
const button = e.target instanceof HTMLElement ? e.target.closest('button') : null;
|
||||||
|
if (!button) return;
|
||||||
|
const id = button.id;
|
||||||
|
if (!id) return;
|
||||||
|
if (id !== 'zoom-in-btn' && id !== 'zoom-out-btn') {
|
||||||
|
hideMainMenuPopup();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!menuPopup || menuPopup.classList.contains('hidden')) return;
|
||||||
|
if (menuWrapper && !menuWrapper.contains(e.target)) {
|
||||||
|
hideMainMenuPopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cache back/forward buttons for faster updates (no need to add listeners - already in HTML)
|
// Cache back/forward buttons for faster updates (no need to add listeners - already in HTML)
|
||||||
backBtnCached = document.querySelector('.nav-left button:nth-child(1)');
|
backBtnCached = document.querySelector('.nav-left button:nth-child(1)');
|
||||||
fwdBtnCached = document.querySelector('.nav-left button:nth-child(2)');
|
fwdBtnCached = document.querySelector('.nav-left button:nth-child(2)');
|
||||||
@@ -1462,7 +1513,7 @@ try {
|
|||||||
function attachCloseMenuOnInteract(el) {
|
function attachCloseMenuOnInteract(el) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const closeIfOpen = () => {
|
const closeIfOpen = () => {
|
||||||
ipcRenderer.send('menu-popup-hide');
|
hideMainMenuPopup();
|
||||||
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
|
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
|
||||||
hideDownloadsPopup();
|
hideDownloadsPopup();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user