feat(branding): Netralax rebrand + Discord-style taskbar unread badge
App productName becomes Netralax (driving exe name and window title);
existing installs keep their %APPDATA%\ChatApp profile via an explicit
app.setPath('userData', appData/ChatApp) so no Login/Sounds/Secret store
data is lost.
The Windows taskbar overlay now renders a red bubble with the actual
unread count (Discord parity) instead of just a static red dot. Renderer
paints a 64×64 PNG via canvas — full-bleed red circle, white bold count
with a "99+" cap, no outer ring — and passes the data URL through the
existing setTrayUnread IPC. Main decodes via nativeImage and applies it
as the BrowserWindow overlay icon. Falls back to the static dot if the
renderer canvas pipeline is unavailable.
Also: app.setName('Netralax') + setAppUserModelId('cloud.netralax.desktop')
for Windows taskbar grouping and notification source attribution, and
release.mjs now reads productName dynamically from package.json so the
artifact lookup stays correct after the rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,23 @@ const __dirnameSafe = path.dirname(__filenameSafe);
|
||||
const DEV_URL = 'http://localhost:1420';
|
||||
const WINDOW_STATE_FILE = 'window-state.json';
|
||||
|
||||
// App branding. productName in package.json drives the packaged exe name
|
||||
// (Netralax.exe) and electron-builder installer title. setName + the
|
||||
// AppUserModelId below cover the live process: window title fallback,
|
||||
// Windows taskbar grouping, notification source attribution.
|
||||
app.setName('Netralax');
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId('cloud.netralax.desktop');
|
||||
}
|
||||
|
||||
// Pin userData to %APPDATA%\ChatApp regardless of productName so existing
|
||||
// installs keep their profile, sounds, secrets, SQLite. Electron's default
|
||||
// is %APPDATA%\<productName>, which after the Netralax rename would point
|
||||
// at an empty fresh dir — same painful migration as the Tauri → Electron
|
||||
// cut. Anchored to `appData` (the platform-AppData root) so productName
|
||||
// changes can't drag it.
|
||||
app.setPath('userData', path.join(app.getPath('appData'), 'ChatApp'));
|
||||
|
||||
// Run dev side-by-side with the installed packaged build by isolating the
|
||||
// renderer profile / secret-store / SQLite / IndexedDB / localStorage in
|
||||
// a separate userData dir. Without this both share `%APPDATA%\ChatApp`,
|
||||
@@ -74,7 +91,7 @@ async function createWindow(): Promise<BrowserWindow> {
|
||||
const state = await loadState(WINDOW_STATE_FILE);
|
||||
|
||||
const win = new BrowserWindow({
|
||||
title: app.isPackaged ? 'ChatApp' : 'ChatApp (Dev)',
|
||||
title: app.isPackaged ? 'Netralax' : 'Netralax (Dev)',
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
...(state.x !== undefined ? { x: state.x } : {}),
|
||||
|
||||
@@ -55,7 +55,7 @@ function buildOverlay(): NativeImage {
|
||||
export function register(mainWindow: BrowserWindow): void {
|
||||
const icon = loadTrayIcon();
|
||||
trayRef = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon);
|
||||
trayRef.setToolTip('ChatApp');
|
||||
trayRef.setToolTip('Netralax');
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
@@ -83,20 +83,31 @@ export function register(mainWindow: BrowserWindow): void {
|
||||
else mainWindow.show();
|
||||
});
|
||||
|
||||
ipcMain.handle(CHANNELS.TRAY_UNREAD, async (_evt, count: number): Promise<void> => {
|
||||
const n = Math.max(0, Math.floor(Number(count) || 0));
|
||||
if (trayRef && !trayRef.isDestroyed()) {
|
||||
trayRef.setToolTip(n > 0 ? `ChatApp — ${n} unread` : 'ChatApp');
|
||||
}
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
if (process.platform === 'win32') {
|
||||
if (n > 0) {
|
||||
mainWindow.setOverlayIcon(buildOverlay(), `${n} unread`);
|
||||
} else {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
ipcMain.handle(
|
||||
CHANNELS.TRAY_UNREAD,
|
||||
async (_evt, count: number, badgeDataUrl?: string | null): Promise<void> => {
|
||||
const n = Math.max(0, Math.floor(Number(count) || 0));
|
||||
if (trayRef && !trayRef.isDestroyed()) {
|
||||
trayRef.setToolTip(n > 0 ? `Netralax — ${n} ungelesen` : 'Netralax');
|
||||
}
|
||||
}
|
||||
});
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
if (process.platform !== 'win32') return;
|
||||
if (n <= 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
return;
|
||||
}
|
||||
// Discord-style: prefer the renderer-painted badge (red circle with
|
||||
// the actual unread number). Fall back to the static red dot only if
|
||||
// the renderer didn't supply one or decoding failed — keeps the
|
||||
// visual indicator alive even when the canvas pipeline is unavailable.
|
||||
let overlay: NativeImage | null = null;
|
||||
if (typeof badgeDataUrl === 'string' && badgeDataUrl.startsWith('data:image/')) {
|
||||
const decoded = nativeImage.createFromDataURL(badgeDataUrl);
|
||||
if (!decoded.isEmpty()) overlay = decoded;
|
||||
}
|
||||
mainWindow.setOverlayIcon(overlay ?? buildOverlay(), `${n} ungelesen`);
|
||||
},
|
||||
);
|
||||
|
||||
app.on('before-quit', () => {
|
||||
try {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ export interface ElectronAPI {
|
||||
notify: (args: NotifyArgs) => Promise<void>;
|
||||
getNotificationPermission: () => Promise<'granted' | 'denied' | 'default'>;
|
||||
|
||||
setTrayUnread: (count: number) => Promise<void>;
|
||||
setTrayUnread: (count: number, badgeDataUrl?: string | null) => Promise<void>;
|
||||
|
||||
secureStoreOpen: (args: SecureStoreOpenArgs) => Promise<SecureStoreHandle>;
|
||||
secureStoreGet: (handle: string, key: string) => Promise<string | null>;
|
||||
|
||||
@@ -94,7 +94,12 @@ const api = {
|
||||
ipcRenderer.invoke(CHANNELS.NOTIFY_PERMISSION),
|
||||
|
||||
// Tray -------------------------------------------------------------------
|
||||
setTrayUnread: (count: number): Promise<void> => ipcRenderer.invoke(CHANNELS.TRAY_UNREAD, count),
|
||||
// `badgeDataUrl` (optional): renderer-painted PNG (data:image/png;base64)
|
||||
// that main applies as the Windows taskbar overlay icon. We render in the
|
||||
// renderer because main has no Canvas2D; passing a finished image avoids
|
||||
// bundling a native canvas backend just for a 32×32 badge.
|
||||
setTrayUnread: (count: number, badgeDataUrl?: string | null): Promise<void> =>
|
||||
ipcRenderer.invoke(CHANNELS.TRAY_UNREAD, count, badgeDataUrl ?? null),
|
||||
|
||||
// Secure store -----------------------------------------------------------
|
||||
secureStoreOpen: (args: SecureStoreOpenArgs): Promise<SecureStoreHandle> =>
|
||||
|
||||
Reference in New Issue
Block a user