feat: notification badge at tray
|  | @ -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] { | ||||||
|  |  | ||||||
|  | @ -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)); | ||||||
|  |  | ||||||
|  | @ -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; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -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)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -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(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
| } | } | ||||||
|  |  | ||||||
| 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 | 
 Oleh Polisan
						Oleh Polisan