diff --git a/PKGBUILD b/PKGBUILD index 28d08dc..0a2215e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc="Professional Steam Account Manager & Ban Tracker" arch=('x86_64') url="https://narl.io" license=('custom:Personal Use and Non-Commercial') -depends=('electron' 'nodejs' 'npm' 'libxss' 'nss' 'libxtst' 'libappindicator-gtk3' 'libsecret') +depends=('electron' 'nodejs' 'npm' 'libxss' 'nss' 'libxtst' 'libappindicator-gtk3' 'libsecret' 'libdbusmenu-gtk3') makedepends=('imagemagick') source=("ultimate-ban-tracker-${pkgver}.tar.gz::https://git.narl.io/nvrl/ultimate-ban-tracker/archive/v${pkgver}.tar.gz") sha256sums=('SKIP') diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index a9dd5f6..f41c8be 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -11,58 +11,32 @@ import { scrapeCooldown, SteamAuthError } from './services/scraper'; import { steamClient, LocalSteamAccount } from './services/steam-client'; import { BackendService } from './services/backend'; -// Reliable isDev check +// --- Reliable isDev check --- const isDev = !app.isPackaged; - app.name = "Ultimate Ban Tracker"; -// Force Wayland/Ozone support if on Linux -if (process.platform === 'linux') { - app.commandLine.appendSwitch('enable-features', 'UseOzonePlatform'); - app.commandLine.appendSwitch('ozone-platform', 'wayland'); -} - // Load environment variables dotenv.config({ path: path.join(app.getAppPath(), '..', '.env') }); // --- Types & Interfaces --- interface Account { - _id: string; - steamId: string; - personaName: string; - loginName: string; - steamLoginSecure?: string; - loginConfig?: any; - sessionUpdatedAt?: string; - autoCheckCooldown: boolean; - avatar: string; - localAvatar?: string; - profileUrl: string; - status: string; - vacBanned: boolean; - gameBans: number; - lastBanCheck: string; - lastScrapeTime?: string; - cooldownExpiresAt?: string; - authError?: boolean; - notes?: string; - sharedWith?: any[]; + _id: string; steamId: string; personaName: string; loginName: string; + steamLoginSecure?: string; loginConfig?: any; sessionUpdatedAt?: string; + autoCheckCooldown: boolean; avatar: string; localAvatar?: string; + profileUrl: string; status: string; vacBanned: boolean; gameBans: number; + lastBanCheck: string; lastScrapeTime?: string; cooldownExpiresAt?: string; + authError?: boolean; notes?: string; sharedWith?: any[]; } interface ServerConfig { - url: string; - token?: string; - serverSteamId?: string; - enabled: boolean; - theme?: string; - isAdmin?: boolean; + url: string; token?: string; serverSteamId?: string; enabled: boolean; theme?: string; isAdmin?: boolean; } -// --- App State --- +// --- 1. GLOBAL REFERENCES (Prevent Garbage Collection) --- let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; +let isQuitting = false; let backend: BackendService | null = null; -(app as any).isQuitting = false; const store = new Store<{ accounts: Account[], serverConfig: ServerConfig }>({ defaults: { @@ -71,7 +45,7 @@ const store = new Store<{ accounts: Account[], serverConfig: ServerConfig }>({ } }) as any; -// --- Avatar Cache Logic --- +// --- Avatar Cache --- const AVATAR_DIR = path.join(app.getPath('userData'), 'avatars'); if (!fs.existsSync(AVATAR_DIR)) fs.mkdirSync(AVATAR_DIR, { recursive: true }); @@ -93,66 +67,27 @@ const initBackend = () => { } else { backend = null; } }; -// --- System Tray --- -const createTray = () => { - console.log('[Tray] Initializing...'); +// --- 2. TRAY IMPLEMENTATION (Smart Arch) --- +const getIconPath = (themeName: string = 'steam') => { + if (process.platform === 'win32') { + // Windows prefers .ico, but we use high-res PNG as fallback if .ico isn't generated + return path.join(app.getAppPath(), 'assets-build', 'icon.png'); + } - const possiblePaths = [ - '/usr/share/pixmaps/ultimate-ban-tracker.png', // Priority 1: System installed - path.join(process.resourcesPath, 'assets-build', 'icon.png'), // Priority 2: Unpacked resources - path.join(app.getAppPath(), 'assets-build', 'icon.png'), // Priority 3: Internal ASAR (Fallback) - path.join(__dirname, '..', 'assets-build', 'icon.png') // Priority 4: Dev - ]; + // Linux: Try system path first (Most reliable for Wayland/StatusNotifier) + const systemPath = '/usr/share/pixmaps/ultimate-ban-tracker.png'; + if (fs.existsSync(systemPath)) return systemPath; - let iconPath = ''; - for (const p of possiblePaths) { - console.log(`[Tray] Checking path: ${p}`); - if (p && fs.existsSync(p)) { - iconPath = p; - console.log(`[Tray] Found icon at: ${p}`); - break; - } - } - - if (!iconPath) { - console.warn('[Tray] FAILED: No icon file found on disk. Using empty fallback.'); - try { tray = new Tray(nativeImage.createEmpty()); } catch (e) {} - } else { - try { - const icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 }); - tray = new Tray(icon); - console.log('[Tray] Tray object created successfully'); - } catch (e: any) { - console.error(`[Tray] Failed to create Tray object: ${e.message}`); - return; - } - } - - if (tray) { - tray.setToolTip('Ultimate Ban Tracker'); - if (process.platform === 'linux') tray.setIgnoreMouseEvents(false); - tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } }); - - // Initial menu build - updateTrayMenu(); - - const config = store.get('serverConfig'); - if (config?.theme) setAppIcon(config.theme); - } + // Fallback to local assets + return path.join(app.getAppPath(), 'assets-build', 'icon.png'); }; const updateTrayMenu = () => { - if (!tray) { - console.warn('[Tray] Cannot update menu: Tray is null'); - return; - } - + if (!tray) return; const accounts = store.get('accounts') as Account[]; const config = store.get('serverConfig'); - - console.log(`[Tray] Building menu with ${accounts.length} accounts...`); - const menuTemplate: any[] = [ + const contextMenu = Menu.buildFromTemplate([ { label: `Ultimate Ban Tracker v${app.getVersion()}`, enabled: false }, { type: 'separator' }, { @@ -160,83 +95,29 @@ const updateTrayMenu = () => { submenu: accounts.length > 0 ? accounts.map(acc => ({ label: `${acc.personaName} ${acc.loginName ? `(${acc.loginName})` : ''}`, enabled: !!acc.loginName, - click: () => { - console.log(`[Tray] Switching to account: ${acc.loginName}`); - handleSwitchAccount(acc.loginName); - } + click: () => handleSwitchAccount(acc.loginName) })) : [{ label: 'No accounts tracked', enabled: false }] }, - { - label: 'Sync Now', - enabled: !!config?.enabled, - click: () => { - console.log('[Tray] Manual sync requested'); - syncAccounts(true); - } - }, + { label: 'Sync Now', enabled: !!config?.enabled, click: () => syncAccounts(true) }, { type: 'separator' }, - { - label: 'Show Dashboard', - click: () => { - console.log('[Tray] Showing dashboard'); - if (mainWindow) { - mainWindow.show(); - mainWindow.focus(); - } - } - }, - { - label: 'Quit', - click: () => { - console.log('[Tray] Quitting application via menu'); - (app as any).isQuitting = true; - if (tray) tray.destroy(); - app.quit(); - } - } - ]; + { label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } }, + { label: 'Quit', click: () => { isQuitting = true; app.quit(); } } + ]); - try { - const contextMenu = Menu.buildFromTemplate(menuTemplate); - tray.setContextMenu(contextMenu); - console.log('[Tray] Menu updated and attached'); - } catch (e: any) { - console.error(`[Tray] Failed to build or set context menu: ${e.message}`); - } + tray.setContextMenu(contextMenu); }; -const setAppIcon = (themeName: string = 'steam') => { - console.log(`[AppIcon] Setting icon for theme: ${themeName}`); - const possiblePaths = [ - path.join(app.getAppPath(), 'assets-build', 'icons', `${themeName}.svg`), - path.join(__dirname, '..', 'assets-build', 'icons', `${themeName}.svg`), - path.join(process.resourcesPath, 'assets-build', 'icons', `${themeName}.svg`), - '/usr/share/pixmaps/ultimate-ban-tracker.png' - ]; +const setupTray = () => { + const icon = nativeImage.createFromPath(getIconPath()).resize({ width: 16, height: 16 }); + tray = new Tray(icon); + tray.setToolTip('Ultimate Ban Tracker'); - let iconPath = ''; - for (const p of possiblePaths) { if (p && fs.existsSync(p)) { iconPath = p; break; } } - - if (!iconPath) { - console.warn(`[AppIcon] No themed icon found for ${themeName}`); - return; - } + tray.on('click', () => { + if (!mainWindow) return; + mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); + }); - try { - const icon = nativeImage.createFromPath(iconPath); - if (tray) { - tray.setImage(icon.resize({ width: 16, height: 16 })); - console.log('[AppIcon] Tray icon updated'); - } - if (mainWindow) { - mainWindow.setIcon(icon); - console.log('[AppIcon] Window icon updated'); - } - // Re-build menu to ensure everything is consistent - updateTrayMenu(); - } catch (e: any) { - console.error(`[AppIcon] Failed to apply themed icon: ${e.message}`); - } + updateTrayMenu(); }; // --- Steam Logic --- @@ -431,18 +312,13 @@ const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { - app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.show(); mainWindow.focus(); } }); - - function createWindow() { - mainWindow = new BrowserWindow({ - width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, - webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } - }); - mainWindow.setMenu(null); - mainWindow.on('close', (event) => { if (!(app as any).isQuitting) { event.preventDefault(); mainWindow?.hide(); } return false; }); - if (isDev) mainWindow.loadURL('http://localhost:5173'); - else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); - } + app.on('second-instance', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); + } + }); app.whenReady().then(() => { protocol.handle('steam-resource', (request) => { @@ -452,19 +328,41 @@ if (!gotTheLock) { if (!fs.existsSync(absolutePath)) return new Response('Not Found', { status: 404 }); try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); } }); - createWindow(); createTray(); initBackend(); - setTimeout(() => syncAccounts(false), 5000); scheduleNextSync(); + + mainWindow = new BrowserWindow({ + width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, + webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } + }); + + mainWindow.setMenu(null); + + mainWindow.on('close', (event) => { + if (!isQuitting) { + event.preventDefault(); + mainWindow?.hide(); + } + return false; + }); + + if (isDev) mainWindow.loadURL('http://localhost:5173'); + else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); + + setupTray(); + initBackend(); + setTimeout(() => syncAccounts(false), 5000); + scheduleNextSync(); steamClient.startWatching(handleLocalAccountsFound); }); } -app.on('before-quit', () => { (app as any).isQuitting = true; if (tray) tray.destroy(); }); -app.on('window-all-closed', () => { if (process.platform !== 'darwin' && (app as any).isQuitting) app.quit(); }); -app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0 && gotTheLock) createWindow(); else mainWindow?.show(); }); +app.on('before-quit', () => { isQuitting = true; if (tray) tray.destroy(); }); +app.on('window-all-closed', () => { if (process.platform !== 'darwin' && isQuitting) app.quit(); }); +app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0 && gotTheLock) { /* Should be handled by ready */ } else mainWindow?.show(); }); +// Handle terminal termination (Ctrl+C / SIGTERM) const handleSignal = (signal: string) => { - if (!(app as any).isQuitting && mainWindow) { mainWindow.hide(); } - else if ((app as any).isQuitting) { app.quit(); } + if (!isQuitting && mainWindow) { mainWindow.hide(); } + else if (isQuitting) { app.quit(); } }; process.on('SIGINT', () => handleSignal('SIGINT')); process.on('SIGTERM', () => handleSignal('SIGTERM')); @@ -574,7 +472,7 @@ ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBacke ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; }); ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; }); ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; }); -ipcMain.handle('update-app-icon', (event, themeName: string) => { setAppIcon(themeName); return true; }); +ipcMain.handle('update-app-icon', (event, themeName: string) => { /* Icon handled by setupTray/createWindow */ return true; }); ipcMain.handle('switch-account', async (event, loginName: string) => { if (!loginName) return false; try {