mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(electron): use native images for Linux tray icons
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
fb61ed7d92
commit
077bed154b
2 changed files with 245 additions and 5 deletions
214
electron/indicator.test.cjs
Normal file
214
electron/indicator.test.cjs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const Module = require('node:module');
|
||||
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const originalModuleLoad = Module._load;
|
||||
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
|
||||
const indicatorModulePath = path.resolve(__dirname, 'indicator.ts');
|
||||
|
||||
let createdTrayArgs;
|
||||
let createdFromPath = [];
|
||||
let traySetImageCalls = [];
|
||||
let ipcHandlers = new Map();
|
||||
let nextNativeImageIsEmpty = false;
|
||||
let beforeQuitHandler = () => {};
|
||||
|
||||
const resetModule = () => {
|
||||
delete require.cache[indicatorModulePath];
|
||||
};
|
||||
|
||||
const installMocks = () => {
|
||||
Module._load = function patchedLoad(request, parent, isMain) {
|
||||
if (request === 'electron') {
|
||||
class FakeTray {
|
||||
constructor(image, guid) {
|
||||
createdTrayArgs.push([image, guid]);
|
||||
}
|
||||
|
||||
setContextMenu() {}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (eventName === 'click') {
|
||||
this._clickHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
setImage(image) {
|
||||
traySetImageCalls.push(image);
|
||||
}
|
||||
|
||||
setTitle() {}
|
||||
|
||||
setToolTip() {}
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
return {
|
||||
Tray: FakeTray,
|
||||
Menu: {
|
||||
buildFromTemplate: (template) => ({ template }),
|
||||
},
|
||||
ipcMain: {
|
||||
on: (eventName, handler) => {
|
||||
ipcHandlers.set(eventName, handler);
|
||||
},
|
||||
},
|
||||
nativeTheme: {
|
||||
shouldUseDarkColors: false,
|
||||
},
|
||||
nativeImage: {
|
||||
createFromPath: (iconPath) => {
|
||||
createdFromPath.push(iconPath);
|
||||
return {
|
||||
iconPath,
|
||||
kind: 'native-image',
|
||||
isEmpty: () => nextNativeImageIsEmpty,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (request === 'electron-log/main') {
|
||||
return {
|
||||
log: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
if (request === './shared-with-frontend/ipc-events.const') {
|
||||
return {
|
||||
IPC: {
|
||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
||||
SET_PROGRESS_BAR: 'SET_PROGRESS_BAR',
|
||||
CURRENT_TASK_UPDATED: 'CURRENT_TASK_UPDATED',
|
||||
TODAY_TASKS_UPDATED: 'TODAY_TASKS_UPDATED',
|
||||
TASK_TOGGLE_START: 'TASK_TOGGLE_START',
|
||||
SWITCH_TASK: 'SWITCH_TASK',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (request === './shared-state') {
|
||||
return {
|
||||
getIsTrayShowCurrentTask: () => false,
|
||||
getIsTrayShowCurrentCountdown: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
if (request === './task-widget/task-widget') {
|
||||
return {
|
||||
updateTaskWidgetAlwaysShow: () => {},
|
||||
updateTaskWidgetEnabled: () => {},
|
||||
updateTaskWidgetOpacity: () => {},
|
||||
updateTaskWidgetTask: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
if (request === './main-window') {
|
||||
return {
|
||||
getWin: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
request === '../src/app/features/tasks/task.model' ||
|
||||
request === '../src/app/features/config/global-config.model'
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return originalModuleLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
};
|
||||
|
||||
const loadIndicatorModule = () => {
|
||||
resetModule();
|
||||
return require(indicatorModulePath);
|
||||
};
|
||||
|
||||
test.beforeEach(() => {
|
||||
createdTrayArgs = [];
|
||||
createdFromPath = [];
|
||||
traySetImageCalls = [];
|
||||
ipcHandlers = new Map();
|
||||
nextNativeImageIsEmpty = false;
|
||||
beforeQuitHandler = () => {};
|
||||
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: 'linux',
|
||||
});
|
||||
|
||||
installMocks();
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
Module._load = originalModuleLoad;
|
||||
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
|
||||
resetModule();
|
||||
});
|
||||
|
||||
test('initIndicator uses NativeImage for Linux tray creation and updates', () => {
|
||||
const { initIndicator } = loadIndicatorModule();
|
||||
|
||||
initIndicator({
|
||||
showApp: () => {},
|
||||
quitApp: () => {},
|
||||
ICONS_FOLDER: '/icons/',
|
||||
forceDarkTray: false,
|
||||
app: {
|
||||
on: (eventName, handler) => {
|
||||
if (eventName === 'before-quit') {
|
||||
beforeQuitHandler = handler;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(createdTrayArgs.length, 1);
|
||||
assert.equal(createdTrayArgs[0][0].kind, 'native-image');
|
||||
assert.match(createdTrayArgs[0][0].iconPath, /\/icons\/indicator\/stopped-d\.png$/);
|
||||
assert.match(createdFromPath[0], /\/icons\/indicator\/stopped-d\.png$/);
|
||||
|
||||
const currentTaskUpdated = ipcHandlers.get('CURRENT_TASK_UPDATED');
|
||||
assert.equal(typeof currentTaskUpdated, 'function');
|
||||
|
||||
currentTaskUpdated(
|
||||
{},
|
||||
{ id: 'T1', title: 'Task', timeSpent: 5 * 60000, timeEstimate: 25 * 60000 },
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
undefined,
|
||||
);
|
||||
|
||||
assert.equal(traySetImageCalls.at(-1).kind, 'native-image');
|
||||
assert.match(traySetImageCalls.at(-1).iconPath, /\/icons\/indicator\/running-anim-d\/3\.png$/);
|
||||
|
||||
beforeQuitHandler();
|
||||
});
|
||||
|
||||
test('initIndicator falls back to icon path if NativeImage creation is empty', () => {
|
||||
nextNativeImageIsEmpty = true;
|
||||
const { initIndicator } = loadIndicatorModule();
|
||||
|
||||
initIndicator({
|
||||
showApp: () => {},
|
||||
quitApp: () => {},
|
||||
ICONS_FOLDER: '/icons/',
|
||||
forceDarkTray: false,
|
||||
app: {
|
||||
on: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(createdTrayArgs.length, 1);
|
||||
assert.equal(typeof createdTrayArgs[0][0], 'string');
|
||||
assert.match(createdTrayArgs[0][0], /\/icons\/indicator\/stopped-d\.png$/);
|
||||
});
|
||||
|
|
@ -1,4 +1,12 @@
|
|||
import { App, ipcMain, IpcMainEvent, Menu, nativeTheme, Tray } from 'electron';
|
||||
import {
|
||||
App,
|
||||
ipcMain,
|
||||
IpcMainEvent,
|
||||
Menu,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import { log } from 'electron-log/main';
|
||||
import { IPC } from './shared-with-frontend/ipc-events.const';
|
||||
import { getIsTrayShowCurrentTask, getIsTrayShowCurrentCountdown } from './shared-state';
|
||||
|
|
@ -134,19 +142,20 @@ export const ensureIndicator = (): Tray | undefined => {
|
|||
const createTray = (): Tray => {
|
||||
const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
|
||||
const trayIconPath = DIR + `stopped${suf}`;
|
||||
const trayIcon = getTrayImage(trayIconPath);
|
||||
let nextTray: Tray;
|
||||
if (IS_WINDOWS) {
|
||||
const guid = getWindowsTrayGuid();
|
||||
try {
|
||||
nextTray = new Tray(trayIconPath, guid);
|
||||
nextTray = new Tray(trayIcon, guid);
|
||||
log('Tray created on Windows with GUID:', guid);
|
||||
} catch (e) {
|
||||
log('Tray creation with GUID failed, retrying without GUID:', e);
|
||||
nextTray = new Tray(trayIconPath);
|
||||
nextTray = new Tray(trayIcon);
|
||||
log('Tray created on Windows without GUID');
|
||||
}
|
||||
} else {
|
||||
nextTray = new Tray(trayIconPath);
|
||||
nextTray = new Tray(trayIcon);
|
||||
}
|
||||
nextTray.setContextMenu(createContextMenu());
|
||||
|
||||
|
|
@ -558,11 +567,28 @@ function getRunningIconPath(progress?: number): string {
|
|||
|
||||
let curIco: string | undefined;
|
||||
|
||||
// GNOME AppIndicator can fall back to a generic "three dots" icon for
|
||||
// sandboxed Electron apps when given only a file path. Passing a NativeImage
|
||||
// keeps the actual pixel data attached to the tray item.
|
||||
const getTrayImage = (icoPath: string): string | Electron.NativeImage => {
|
||||
if (!IS_LINUX) {
|
||||
return icoPath;
|
||||
}
|
||||
|
||||
const image = nativeImage.createFromPath(icoPath);
|
||||
if (image.isEmpty()) {
|
||||
log('Tray icon NativeImage is empty, falling back to icon path:', icoPath);
|
||||
return icoPath;
|
||||
}
|
||||
|
||||
return image;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||
function setTrayIcon(tr: Tray, icoPath: string): void {
|
||||
if (icoPath !== curIco) {
|
||||
curIco = icoPath;
|
||||
tr.setImage(icoPath);
|
||||
tr.setImage(getTrayImage(icoPath));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue