feat: notification badge at tray

This commit is contained in:
Oleh Polisan 2024-06-21 16:02:39 +03:00
parent 1ddd93342e
commit c725352bef
29 changed files with 137 additions and 58 deletions

View file

@ -6,22 +6,27 @@
import { app, NativeImage, nativeImage } from "electron"; import { app, NativeImage, nativeImage } from "electron";
import { join } from "path"; import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { BADGE_DIR } from "shared/paths"; import { BADGE_DIR } from "shared/paths";
import { mainWin } from "./mainWindow";
const imgCache = new Map<number, NativeImage>(); const imgCache = new Map<number, NativeImage>();
function loadBadge(index: number) { export function loadBadge(index: number) {
const cached = imgCache.get(index); const cached = imgCache.get(index);
if (cached) return cached; if (cached) return cached;
const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.ico`)); const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.png`));
imgCache.set(index, img); imgCache.set(index, img);
return img; return img;
} }
let lastIndex: null | number = -1; let lastBadgeIndex: null | number = -1;
export var lastBadgeCount: number = -1;
export function setBadgeCount(count: number) { export function setBadgeCount(count: number) {
lastBadgeCount = count;
switch (process.platform) { switch (process.platform) {
case "linux": case "linux":
if (count === -1) count = 0; if (count === -1) count = 0;
@ -36,15 +41,17 @@ export function setBadgeCount(count: number) {
break; break;
case "win32": case "win32":
const [index, description] = getBadgeIndexAndDescription(count); const [index, description] = getBadgeIndexAndDescription(count);
if (lastIndex === index) break; if (lastBadgeIndex === index) break;
lastIndex = index; lastBadgeIndex = index;
// circular import shenanigans // circular import shenanigans
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow"); const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description); mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
break; break;
} }
mainWin.webContents.send(IpcEvents.SET_CURRENT_VOICE_TRAY_ICON);
} }
function getBadgeIndexAndDescription(count: number): [number | null, string] { function getBadgeIndexAndDescription(count: number): [number | null, string] {

View file

@ -28,9 +28,17 @@ import { IpcEvents } from "../shared/IpcEvents";
import { setBadgeCount } from "./appBadge"; import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart"; import { autoStart } from "./autoStart";
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants"; import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { mainWin, setTrayIcon } from "./mainWindow"; import { mainWin } from "./mainWindow";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { createTrayIcon, generateTrayIcons, getTrayIconFile, getTrayIconFileSync, pickTrayIcon } from "./tray"; import {
createTrayIcon,
generateTrayIcons,
getIconWithBadge,
getTrayIconFile,
getTrayIconFileSync,
pickTrayIcon,
setTrayIcon
} from "./tray";
import { handle, handleSync } from "./utils/ipcWrappers"; import { handle, handleSync } from "./utils/ipcWrappers";
import { PopoutWindows } from "./utils/popout"; import { PopoutWindows } from "./utils/popout";
import { isDeckGameMode, showGamePage } from "./utils/steamOS"; import { isDeckGameMode, showGamePage } from "./utils/steamOS";
@ -170,3 +178,4 @@ handle(IpcEvents.CREATE_TRAY_ICON_RESPONSE, (_, iconName, dataURL, isCustomIcon)
); );
handle(IpcEvents.GENERATE_TRAY_ICONS, () => generateTrayIcons()); handle(IpcEvents.GENERATE_TRAY_ICONS, () => generateTrayIcons());
handle(IpcEvents.SELECT_TRAY_ICON, async (_, iconName) => pickTrayIcon(iconName)); handle(IpcEvents.SELECT_TRAY_ICON, async (_, iconName) => pickTrayIcon(iconName));
handle(IpcEvents.GET_ICON_WITH_BADGE, async (_, dataURL) => getIconWithBadge(dataURL));

View file

@ -11,8 +11,6 @@ import {
dialog, dialog,
Menu, Menu,
MenuItemConstructorOptions, MenuItemConstructorOptions,
NativeImage,
nativeImage,
nativeTheme, nativeTheme,
screen, screen,
session, session,
@ -21,7 +19,7 @@ import {
import { rm } from "fs/promises"; import { rm } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { ICON_PATH, ICONS_DIR } from "shared/paths"; import { ICON_PATH } from "shared/paths";
import { isTruthy } from "shared/utils/guards"; import { isTruthy } from "shared/utils/guards";
import { once } from "shared/utils/once"; import { once } from "shared/utils/once";
import type { SettingsStore } from "shared/utils/SettingsStore"; import type { SettingsStore } from "shared/utils/SettingsStore";
@ -40,13 +38,13 @@ import {
} from "./constants"; } from "./constants";
import { Settings, State, VencordSettings } from "./settings"; import { Settings, State, VencordSettings } from "./settings";
import { createSplashWindow } from "./splash"; import { createSplashWindow } from "./splash";
import { generateTrayIcons, isCustomIcon, statusToSettingsKey } from "./tray"; import { setTrayIcon } from "./tray";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS"; import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader"; import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
let isQuitting = false; let isQuitting = false;
let tray: Tray; export let tray: Tray;
applyDeckKeyboardFix(); applyDeckKeyboardFix();
@ -127,14 +125,7 @@ function initTray(win: BrowserWindow) {
]); ]);
tray = new Tray(ICON_PATH); tray = new Tray(ICON_PATH);
try { setTrayIcon("icon");
if (Settings.store.trayMainOverride) tray.setImage(join(ICONS_DIR, "icon_custom.png"));
else tray.setImage(join(ICONS_DIR, "icon.png"));
} catch (error) {
console.log("Error while loading custom tray image. Recreating new ones.");
Settings.store.trayMainOverride = false;
generateTrayIcons();
}
tray.setToolTip("Vesktop"); tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu); tray.setContextMenu(trayMenu);
tray.on("click", onTrayClick); tray.on("click", onTrayClick);
@ -512,34 +503,3 @@ export async function createWindows() {
initArRPC(); initArRPC();
} }
export async function setTrayIcon(iconName: string) {
if (!tray || tray.isDestroyed()) return;
const Icons = new Set(["speaking", "muted", "deafened", "idle", "icon"]);
if (!Icons.has(iconName)) return;
try {
var trayImage: NativeImage;
if (isCustomIcon(iconName)) {
trayImage = nativeImage.createFromPath(join(ICONS_DIR, iconName + "_custom.png"));
if (trayImage.isEmpty()) {
const iconKey = statusToSettingsKey[iconName as keyof typeof statusToSettingsKey];
Settings.store[iconKey] = false;
generateTrayIcons();
return;
}
} else trayImage = nativeImage.createFromPath(join(ICONS_DIR, iconName + ".png"));
if (trayImage.isEmpty()) {
generateTrayIcons();
return;
}
if (process.platform === "darwin") {
trayImage = trayImage.resize({ width: 16, height: 16 });
}
tray.setImage(trayImage);
} catch (error) {
console.log("Error: ", error, "Regenerating tray icons.");
generateTrayIcons(); // TODO: pass here only one icon request
}
return;
}

View file

@ -5,13 +5,14 @@
*/ */
import { dialog, NativeImage, nativeImage } from "electron"; import { dialog, NativeImage, nativeImage } from "electron";
import { copyFileSync, mkdirSync, writeFileSync } from "fs"; import { mkdirSync, writeFileSync } from "fs";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { ICONS_DIR, STATIC_DIR } from "shared/paths"; import { ICONS_DIR, STATIC_DIR } from "shared/paths";
import { mainWin } from "./mainWindow"; import { lastBadgeCount, loadBadge } from "./appBadge";
import { mainWin, tray } from "./mainWindow";
import { Settings } from "./settings"; import { Settings } from "./settings";
export const statusToSettingsKey = { export const statusToSettingsKey = {
@ -27,6 +28,62 @@ export const isCustomIcon = (status: string) => {
return Settings.store[settingKey]; return Settings.store[settingKey];
}; };
export async function setTrayIcon(iconName: string) {
if (!tray || tray.isDestroyed()) return;
const Icons = new Set(["speaking", "muted", "deafened", "idle", "icon"]);
if (!Icons.has(iconName)) return;
// if need to set main icon then check whether there is need of notif badge
if (iconName === "icon" && lastBadgeCount && lastBadgeCount > 0) {
var trayImage: NativeImage;
if (isCustomIcon("icon")) {
console.log("setting badge and CUSTOM icon");
trayImage = nativeImage.createFromPath(join(ICONS_DIR, "icon_custom.png"));
} else {
console.log("setting badge and stock icon");
trayImage = nativeImage.createFromPath(join(ICONS_DIR, "icon.png"));
}
const badgeImg = loadBadge(lastBadgeCount);
// and send IPC call to renderer to add to image
mainWin.webContents.send(IpcEvents.ADD_BADGE_TO_ICON, trayImage.toDataURL(), badgeImg.toDataURL());
return;
}
try {
var trayImage: NativeImage;
if (isCustomIcon(iconName)) {
trayImage = nativeImage.createFromPath(join(ICONS_DIR, iconName + "_custom.png"));
if (trayImage.isEmpty()) {
const iconKey = statusToSettingsKey[iconName as keyof typeof statusToSettingsKey];
Settings.store[iconKey] = false;
generateTrayIcons();
return;
}
} else trayImage = nativeImage.createFromPath(join(ICONS_DIR, iconName + ".png"));
if (trayImage.isEmpty()) {
generateTrayIcons();
return;
}
if (process.platform === "darwin") {
trayImage = trayImage.resize({ width: 16, height: 16 });
}
tray.setImage(trayImage);
} catch (error) {
console.log("Error: ", error, "Regenerating tray icons.");
generateTrayIcons(); // TODO: pass here only one icon request
}
return;
}
export async function setTrayIconWithBadge(iconDataURL: string) {
var trayImage = nativeImage.createFromDataURL(iconDataURL);
if (process.platform === "darwin") {
trayImage = trayImage.resize({ width: 16, height: 16 });
}
tray.setImage(trayImage);
}
export async function getTrayIconFile(iconName: string) { export async function getTrayIconFile(iconName: string) {
const Icons = new Set(["speaking", "muted", "deafened", "idle"]); const Icons = new Set(["speaking", "muted", "deafened", "idle"]);
if (!Icons.has(iconName)) { if (!Icons.has(iconName)) {
@ -62,7 +119,9 @@ export async function createTrayIcon(iconName: string, iconDataURL: string, isCu
// primarily called from renderer using CREATE_TRAY_ICON_RESPONSE IPC call // primarily called from renderer using CREATE_TRAY_ICON_RESPONSE IPC call
iconDataURL = iconDataURL.replace(/^data:image\/png;base64,/, ""); iconDataURL = iconDataURL.replace(/^data:image\/png;base64,/, "");
if (isCustomIcon) { if (isCustomIcon) {
writeFileSync(join(ICONS_DIR, iconName + "_custom.png"), iconDataURL, "base64"); const img = nativeImage.createFromDataURL(iconDataURL).resize({ width: 128, height: 128 });
// writeFileSync(join(ICONS_DIR, iconName + "_custom.png"), img.toDataURL(), "base64");
writeFileSync(join(ICONS_DIR, iconName + "_custom.png"), img.toPNG());
} else { } else {
writeFileSync(join(ICONS_DIR, iconName + ".png"), iconDataURL, "base64"); writeFileSync(join(ICONS_DIR, iconName + ".png"), iconDataURL, "base64");
} }
@ -76,7 +135,8 @@ export async function generateTrayIcons() {
for (const icon of Icons) { for (const icon of Icons) {
mainWin.webContents.send(IpcEvents.CREATE_TRAY_ICON_REQUEST, icon); mainWin.webContents.send(IpcEvents.CREATE_TRAY_ICON_REQUEST, icon);
} }
copyFileSync(join(STATIC_DIR, "icon.png"), join(ICONS_DIR, "icon.png")); const img = nativeImage.createFromPath(join(STATIC_DIR, "icon.png")).resize({ width: 128, height: 128 });
writeFileSync(join(ICONS_DIR, "icon.png"), img.toPNG());
mainWin.webContents.send(IpcEvents.SET_CURRENT_VOICE_TRAY_ICON); mainWin.webContents.send(IpcEvents.SET_CURRENT_VOICE_TRAY_ICON);
} }
@ -93,6 +153,11 @@ export async function pickTrayIcon(iconName: string) {
// add .svg !! // add .svg !!
const image = nativeImage.createFromPath(dir); const image = nativeImage.createFromPath(dir);
if (image.isEmpty()) return "invalid"; if (image.isEmpty()) return "invalid";
copyFileSync(dir, join(ICONS_DIR, iconName + "_custom.png")); const img = nativeImage.createFromPath(dir).resize({ width: 128, height: 128 });
writeFileSync(join(ICONS_DIR, iconName + "_custom.png"), img.toPNG());
return dir; return dir;
} }
export async function getIconWithBadge(dataURL: string) {
tray.setImage(nativeImage.createFromDataURL(dataURL));
}

View file

@ -94,6 +94,12 @@ export const VesktopNative = {
generateTrayIcons: () => invoke<void>(IpcEvents.GENERATE_TRAY_ICONS), generateTrayIcons: () => invoke<void>(IpcEvents.GENERATE_TRAY_ICONS),
setCurrentVoiceIcon: (listener: (...args: any[]) => void) => { setCurrentVoiceIcon: (listener: (...args: any[]) => void) => {
ipcRenderer.on(IpcEvents.SET_CURRENT_VOICE_TRAY_ICON, listener); ipcRenderer.on(IpcEvents.SET_CURRENT_VOICE_TRAY_ICON, listener);
} },
addBadgeToIcon: (listener: (iconDataURL: string, badgeDataURL: string) => void) => {
ipcRenderer.on(IpcEvents.ADD_BADGE_TO_ICON, (_, iconDataURL: string, badgeDataURL: string) =>
listener(iconDataURL, badgeDataURL)
);
},
returnIconWithBadge: (dataURL: string) => invoke<void>(IpcEvents.GET_ICON_WITH_BADGE, dataURL)
} }
}; };

View file

@ -58,6 +58,36 @@ VesktopNative.tray.createIconRequest(async (iconName: string) => {
} }
}); });
VesktopNative.tray.addBadgeToIcon(async (iconDataURL: string, badgeDataURL: string) => {
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const img = new Image();
img.width = 128;
img.height = 128;
img.onload = () => {
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0);
const iconImg = new Image();
iconImg.width = 64;
iconImg.height = 64;
iconImg.onload = () => {
ctx.drawImage(iconImg, 64, 0, 64, 64);
VesktopNative.tray.returnIconWithBadge(canvas.toDataURL());
};
iconImg.src = badgeDataURL;
}
};
img.src = iconDataURL;
});
VesktopNative.tray.setCurrentVoiceIcon(() => { VesktopNative.tray.setCurrentVoiceIcon(() => {
setCurrentTrayIcon(); setCurrentTrayIcon();
}); });

View file

@ -59,5 +59,7 @@ export const enum IpcEvents {
GENERATE_TRAY_ICONS = "VCD_GENERATE_TRAY_ICONS", GENERATE_TRAY_ICONS = "VCD_GENERATE_TRAY_ICONS",
SET_CURRENT_VOICE_TRAY_ICON = "VCD_SET_CURRENT_VOICE_ICON", SET_CURRENT_VOICE_TRAY_ICON = "VCD_SET_CURRENT_VOICE_ICON",
GET_SYSTEM_ACCENT_COLOR = "VCD_GET_ACCENT_COLOR", GET_SYSTEM_ACCENT_COLOR = "VCD_GET_ACCENT_COLOR",
SELECT_TRAY_ICON = "VCD_SELECT_TRAY_ICON" SELECT_TRAY_ICON = "VCD_SELECT_TRAY_ICON",
ADD_BADGE_TO_ICON = "VCD_ADD_BADGE_TO_ICON",
GET_ICON_WITH_BADGE = "VCD_GET_ICON_WITH_BADGE"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
static/badges/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB