feat/makepkg + project structure #12

Open
nvrl wants to merge 15 commits from feat/makepkg into main
2 changed files with 77 additions and 179 deletions
Showing only changes of commit 06317cff2c - Show all commits

View File

@@ -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')

View File

@@ -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 {