feat: notification badge at tray
|
@ -6,22 +6,27 @@
|
|||
|
||||
import { app, NativeImage, nativeImage } from "electron";
|
||||
import { join } from "path";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { BADGE_DIR } from "shared/paths";
|
||||
|
||||
import { mainWin } from "./mainWindow";
|
||||
|
||||
const imgCache = new Map<number, NativeImage>();
|
||||
function loadBadge(index: number) {
|
||||
export function loadBadge(index: number) {
|
||||
const cached = imgCache.get(index);
|
||||
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);
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
let lastIndex: null | number = -1;
|
||||
let lastBadgeIndex: null | number = -1;
|
||||
export var lastBadgeCount: number = -1;
|
||||
|
||||
export function setBadgeCount(count: number) {
|
||||
lastBadgeCount = count;
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
if (count === -1) count = 0;
|
||||
|
@ -36,15 +41,17 @@ export function setBadgeCount(count: number) {
|
|||
break;
|
||||
case "win32":
|
||||
const [index, description] = getBadgeIndexAndDescription(count);
|
||||
if (lastIndex === index) break;
|
||||
if (lastBadgeIndex === index) break;
|
||||
|
||||
lastIndex = index;
|
||||
lastBadgeIndex = index;
|
||||
|
||||
// circular import shenanigans
|
||||
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
|
||||
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
|
||||
break;
|
||||
}
|
||||
|
||||
mainWin.webContents.send(IpcEvents.SET_CURRENT_VOICE_TRAY_ICON);
|
||||
}
|
||||
|
||||
function getBadgeIndexAndDescription(count: number): [number | null, string] {
|
||||
|
|
|
@ -28,9 +28,17 @@ import { IpcEvents } from "../shared/IpcEvents";
|
|||
import { setBadgeCount } from "./appBadge";
|
||||
import { autoStart } from "./autoStart";
|
||||
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 { createTrayIcon, generateTrayIcons, getTrayIconFile, getTrayIconFileSync, pickTrayIcon } from "./tray";
|
||||
import {
|
||||
createTrayIcon,
|
||||
generateTrayIcons,
|
||||
getIconWithBadge,
|
||||
getTrayIconFile,
|
||||
getTrayIconFileSync,
|
||||
pickTrayIcon,
|
||||
setTrayIcon
|
||||
} from "./tray";
|
||||
import { handle, handleSync } from "./utils/ipcWrappers";
|
||||
import { PopoutWindows } from "./utils/popout";
|
||||
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.SELECT_TRAY_ICON, async (_, iconName) => pickTrayIcon(iconName));
|
||||
handle(IpcEvents.GET_ICON_WITH_BADGE, async (_, dataURL) => getIconWithBadge(dataURL));
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
dialog,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
NativeImage,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
screen,
|
||||
session,
|
||||
|
@ -21,7 +19,7 @@ import {
|
|||
import { rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
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 { once } from "shared/utils/once";
|
||||
import type { SettingsStore } from "shared/utils/SettingsStore";
|
||||
|
@ -40,13 +38,13 @@ import {
|
|||
} from "./constants";
|
||||
import { Settings, State, VencordSettings } from "./settings";
|
||||
import { createSplashWindow } from "./splash";
|
||||
import { generateTrayIcons, isCustomIcon, statusToSettingsKey } from "./tray";
|
||||
import { setTrayIcon } from "./tray";
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
|
||||
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
|
||||
|
||||
let isQuitting = false;
|
||||
let tray: Tray;
|
||||
export let tray: Tray;
|
||||
|
||||
applyDeckKeyboardFix();
|
||||
|
||||
|
@ -127,14 +125,7 @@ function initTray(win: BrowserWindow) {
|
|||
]);
|
||||
|
||||
tray = new Tray(ICON_PATH);
|
||||
try {
|
||||
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();
|
||||
}
|
||||
setTrayIcon("icon");
|
||||
tray.setToolTip("Vesktop");
|
||||
tray.setContextMenu(trayMenu);
|
||||
tray.on("click", onTrayClick);
|
||||
|
@ -512,34 +503,3 @@ export async function createWindows() {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
*/
|
||||
|
||||
import { dialog, NativeImage, nativeImage } from "electron";
|
||||
import { copyFileSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
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";
|
||||
|
||||
export const statusToSettingsKey = {
|
||||
|
@ -27,6 +28,62 @@ export const isCustomIcon = (status: string) => {
|
|||
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) {
|
||||
const Icons = new Set(["speaking", "muted", "deafened", "idle"]);
|
||||
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
|
||||
iconDataURL = iconDataURL.replace(/^data:image\/png;base64,/, "");
|
||||
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 {
|
||||
writeFileSync(join(ICONS_DIR, iconName + ".png"), iconDataURL, "base64");
|
||||
}
|
||||
|
@ -76,7 +135,8 @@ export async function generateTrayIcons() {
|
|||
for (const icon of Icons) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -93,6 +153,11 @@ export async function pickTrayIcon(iconName: string) {
|
|||
// add .svg !!
|
||||
const image = nativeImage.createFromPath(dir);
|
||||
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;
|
||||
}
|
||||
|
||||
export async function getIconWithBadge(dataURL: string) {
|
||||
tray.setImage(nativeImage.createFromDataURL(dataURL));
|
||||
}
|
||||
|
|
|
@ -94,6 +94,12 @@ export const VesktopNative = {
|
|||
generateTrayIcons: () => invoke<void>(IpcEvents.GENERATE_TRAY_ICONS),
|
||||
setCurrentVoiceIcon: (listener: (...args: any[]) => void) => {
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(() => {
|
||||
setCurrentTrayIcon();
|
||||
});
|
||||
|
|
|
@ -59,5 +59,7 @@ export const enum IpcEvents {
|
|||
GENERATE_TRAY_ICONS = "VCD_GENERATE_TRAY_ICONS",
|
||||
SET_CURRENT_VOICE_TRAY_ICON = "VCD_SET_CURRENT_VOICE_ICON",
|
||||
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"
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/1.png
Normal file
After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/10.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/11.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/2.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/3.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/4.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/5.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/6.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/7.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/8.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/badges/9.png
Normal file
After Width: | Height: | Size: 1.6 KiB |