Compare commits
16 Commits
release/v1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4021e3cc42 | |||
| 1d4fb03104 | |||
| d6d87107f5 | |||
| 2ef8dd06e7 | |||
| 559c7bfdef | |||
| 2124845848 | |||
| 4ad4e1c9de | |||
| 3f7c325604 | |||
| 776e05fb52 | |||
| fc19f66ace | |||
| eca3a728fc | |||
| 60b3dd1ca1 | |||
| 589acdebcb | |||
| 4037d7bce3 | |||
| 5d611fd8be | |||
| 88d2a2133c |
@@ -1,4 +1,33 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="64" fill="#171A21"/>
|
||||
<path d="M256 64C150.13 64 64 150.13 64 256C64 361.87 150.13 448 256 448C361.87 448 448 361.87 448 256C448 150.13 375.73 64 256 64ZM256 405.33C173.6 405.33 106.67 338.4 106.67 256C106.67 221.33 118.4 189.33 138.13 164.27L347.73 373.87C322.67 393.6 290.67 405.33 256 405.33ZM373.87 347.73L164.27 138.13C189.33 118.4 221.33 106.67 256 106.67C338.4 106.67 405.33 173.6 405.33 256C405.33 290.67 393.6 322.67 373.87 347.73Z" fill="#66C0F4"/>
|
||||
<defs>
|
||||
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#1B2838"/>
|
||||
<stop offset="100%" stop-color="#101419"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#66C0F4"/>
|
||||
<stop offset="100%" stop-color="#1A9FFF"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Outer Rounded Container -->
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||
<rect x="2" y="2" width="508" height="508" rx="98" stroke="white" stroke-opacity="0.05" stroke-width="4"/>
|
||||
|
||||
<!-- Tracking Ring (Detailed) -->
|
||||
<circle cx="256" cy="256" r="180" stroke="#66C0F4" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||
|
||||
<!-- Central Shield Symbol -->
|
||||
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||
|
||||
<!-- "Ban" Intersect (Stylized Cross) -->
|
||||
<path d="M210 220L302 312M302 220L210 312" stroke="#1B2838" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||
|
||||
<!-- Glass Highlight -->
|
||||
<path d="M100 100C150 60 362 60 412 100" stroke="white" stroke-opacity="0.1" stroke-width="20" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 1.7 KiB |
21
frontend/assets-build/icons/latte.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#EFF1F5"/>
|
||||
<stop offset="100%" stop-color="#DCE0E8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#1E66F5"/>
|
||||
<stop offset="100%" stop-color="#179299"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||
<circle cx="256" cy="256" r="180" stroke="#1E66F5" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||
<path d="M210 220L302 312M302 220L210 312" stroke="#EFF1F5" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/mocha.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#1E1E2E"/>
|
||||
<stop offset="100%" stop-color="#11111B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#B4BEFE"/>
|
||||
<stop offset="100%" stop-color="#89B4FA"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||
<circle cx="256" cy="256" r="180" stroke="#B4BEFE" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||
<path d="M210 220L302 312M302 220L210 312" stroke="#1E1E2E" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/nord.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#3B4252"/>
|
||||
<stop offset="100%" stop-color="#2E3440"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#88C0D0"/>
|
||||
<stop offset="100%" stop-color="#81A1C1"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||
<circle cx="256" cy="256" r="180" stroke="#88C0D0" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||
<path d="M210 220L302 312M302 220L210 312" stroke="#3B4252" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/steam.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#1B2838"/>
|
||||
<stop offset="100%" stop-color="#101419"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#66C0F4"/>
|
||||
<stop offset="100%" stop-color="#1A9FFF"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||
<circle cx="256" cy="256" r="180" stroke="#66C0F4" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||
<path d="M210 220L302 312M302 220L210 312" stroke="#1B2838" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
21
frontend/assets-build/icons/tokyo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg_grad" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#1A1B26"/>
|
||||
<stop offset="100%" stop-color="#10101A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="symbol_grad" x1="256" y1="100" x2="256" y2="412" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#7AA2F7"/>
|
||||
<stop offset="100%" stop-color="#3D59A1"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="15" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg_grad)"/>
|
||||
<circle cx="256" cy="256" r="180" stroke="#7AA2F7" stroke-width="8" stroke-dasharray="20 10" opacity="0.2"/>
|
||||
<circle cx="256" cy="256" r="160" stroke="url(#symbol_grad)" stroke-width="20" stroke-linecap="round" stroke-dasharray="350 500" filter="url(#glow)"/>
|
||||
<path d="M256 120L360 160V260C360 330 310 380 256 400C202 380 152 330 152 260V160L256 120Z" fill="url(#symbol_grad)" filter="url(#glow)"/>
|
||||
<path d="M210 220L302 312M302 220L210 312" stroke="#1A1B26" stroke-width="24" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -99,7 +99,14 @@ const createTray = () => {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
} });
|
||||
updateTrayMenu();
|
||||
// Load initial themed icon
|
||||
const config = store.get('serverConfig');
|
||||
if (config?.theme) {
|
||||
setAppIcon(config.theme);
|
||||
}
|
||||
else {
|
||||
updateTrayMenu(); // Fallback to refresh menu
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
};
|
||||
@@ -127,6 +134,21 @@ const updateTrayMenu = () => {
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
};
|
||||
const setAppIcon = (themeName = 'steam') => {
|
||||
const assetsDir = path_1.default.join(__dirname, '..', 'assets-build', 'icons');
|
||||
const iconPath = path_1.default.join(assetsDir, `${themeName}.svg`);
|
||||
if (!fs_1.default.existsSync(iconPath))
|
||||
return;
|
||||
const icon = electron_1.nativeImage.createFromPath(iconPath);
|
||||
// Update Tray
|
||||
if (tray) {
|
||||
tray.setImage(icon.resize({ width: 16, height: 16 }));
|
||||
}
|
||||
// Update Main Window
|
||||
if (mainWindow) {
|
||||
mainWindow.setIcon(icon);
|
||||
}
|
||||
};
|
||||
// --- Steam Logic ---
|
||||
const killSteam = async () => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -197,9 +219,13 @@ const scrapeAccountData = async (account) => {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (e.message.includes('cookie') || e.message.includes('Sign In'))
|
||||
if (e instanceof scraper_1.SteamAuthError) {
|
||||
account.authError = true;
|
||||
}
|
||||
else {
|
||||
console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (backend && !account._id.startsWith('shared_')) {
|
||||
await backend.shareAccount(account);
|
||||
@@ -237,7 +263,16 @@ const syncAccounts = async (isManual = false) => {
|
||||
else {
|
||||
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||
if (sDate > lDate) {
|
||||
// 1. SENSITIVE DATA SYNC (Credentials)
|
||||
const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||
const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||
const isLocalAccount = !exists._id.startsWith('shared_');
|
||||
const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError;
|
||||
// SMART OVERWRITE LOGIC:
|
||||
// - If it's a remote shared account: Newest wins.
|
||||
// - If it's a LOCAL account: Only overwrite if our local session is broken/missing.
|
||||
const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate);
|
||||
if (shouldOverwriteCredentials) {
|
||||
if (s.loginName)
|
||||
exists.loginName = s.loginName;
|
||||
if (s.loginConfig)
|
||||
@@ -250,7 +285,7 @@ const syncAccounts = async (isManual = false) => {
|
||||
exists.sessionUpdatedAt = s.sessionUpdatedAt;
|
||||
hasChanges = true;
|
||||
}
|
||||
// Metadata Sync (Pull)
|
||||
// 2. Metadata Sync (Pull) - Always "Newest Wins"
|
||||
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
|
||||
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
|
||||
if (sMetaDate > lMetaDate) {
|
||||
@@ -588,6 +623,11 @@ electron_1.ipcMain.handle('admin-delete-user', async (event, userId) => { initBa
|
||||
electron_1.ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; });
|
||||
electron_1.ipcMain.handle('admin-remove-account', async (event, steamId) => { initBackend(); if (backend)
|
||||
await backend.forceRemoveAccount(steamId); return true; });
|
||||
electron_1.ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; });
|
||||
electron_1.ipcMain.handle('update-app-icon', (event, themeName) => {
|
||||
setAppIcon(themeName);
|
||||
return true;
|
||||
});
|
||||
electron_1.ipcMain.handle('switch-account', async (event, loginName) => {
|
||||
if (!loginName)
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,7 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', {
|
||||
revokeAccountAccess: (steamId, targetSteamId) => electron_1.ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
|
||||
revokeAllAccountAccess: (steamId) => electron_1.ipcRenderer.invoke('revoke-all-account-access', steamId),
|
||||
openExternal: (url) => electron_1.ipcRenderer.invoke('open-external', url),
|
||||
updateAppIcon: (theme) => electron_1.ipcRenderer.invoke('update-app-icon', theme),
|
||||
openSteamAppLogin: () => electron_1.ipcRenderer.invoke('open-steam-app-login'),
|
||||
openSteamLogin: (steamId) => electron_1.ipcRenderer.invoke('open-steam-login', steamId),
|
||||
// Server Config & Auth
|
||||
|
||||
@@ -36,9 +36,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.scrapeCooldown = void 0;
|
||||
exports.scrapeCooldown = exports.SteamAuthError = void 0;
|
||||
const axios_1 = __importDefault(require("axios"));
|
||||
const cheerio = __importStar(require("cheerio"));
|
||||
// Custom error to identify session death
|
||||
class SteamAuthError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "SteamAuthError";
|
||||
}
|
||||
}
|
||||
exports.SteamAuthError = SteamAuthError;
|
||||
const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
||||
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
|
||||
try {
|
||||
@@ -47,13 +55,17 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
||||
'Cookie': steamLoginSecure,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
},
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
validateStatus: (status) => status < 500 // Allow redirects to handle them manually
|
||||
});
|
||||
const $ = cheerio.load(response.data);
|
||||
if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) {
|
||||
throw new Error('Invalid or expired steamLoginSecure cookie');
|
||||
// If Steam redirects us to the login page, the cookie is dead
|
||||
if (response.data.includes('Sign In') || response.request.path.includes('/login')) {
|
||||
throw new SteamAuthError('Invalid or expired steamLoginSecure cookie');
|
||||
}
|
||||
const $ = cheerio.load(response.data);
|
||||
if (!response.data.includes('Personal Game Data')) {
|
||||
throw new SteamAuthError('Session invalid: Personal Game Data not accessible');
|
||||
}
|
||||
// 1. Locate the specific table containing cooldown info
|
||||
let expirationDate = undefined;
|
||||
$('table').each((_, table) => {
|
||||
const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get();
|
||||
@@ -63,25 +75,18 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
||||
rows.each((_, row) => {
|
||||
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
|
||||
if (dateText && dateText !== '') {
|
||||
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
|
||||
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
||||
const parsed = new Date(cleanDateText);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
// We want the newest expiration date found
|
||||
if (!expirationDate || parsed > expirationDate) {
|
||||
if (!expirationDate || parsed > expirationDate)
|
||||
expirationDate = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (expirationDate && expirationDate.getTime() > Date.now()) {
|
||||
console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`);
|
||||
return {
|
||||
isActive: true,
|
||||
expiresAt: expirationDate
|
||||
};
|
||||
return { isActive: true, expiresAt: expirationDate };
|
||||
}
|
||||
const content = $('#personal_game_data_content').text();
|
||||
if (content.includes('Competitive Cooldown') || content.includes('Your account is currently')) {
|
||||
@@ -90,8 +95,10 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => {
|
||||
return { isActive: false };
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`[Scraper] Error for ${steamId}:`, error.message);
|
||||
if (error instanceof SteamAuthError)
|
||||
throw error;
|
||||
console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message);
|
||||
throw error; // Generic errors don't trigger re-auth
|
||||
}
|
||||
};
|
||||
exports.scrapeCooldown = scrapeCooldown;
|
||||
|
||||
@@ -49,10 +49,6 @@ class SteamClientService {
|
||||
return null;
|
||||
return path_1.default.join(this.steamPath, 'config', 'config.vdf');
|
||||
}
|
||||
/**
|
||||
* Safe Atomic Write: Writes to a temp file and renames it.
|
||||
* This prevents file corruption if the app crashes during write.
|
||||
*/
|
||||
safeWriteVdf(filePath, data) {
|
||||
const tempPath = `${filePath}.tmp_${Date.now()}`;
|
||||
const dir = path_1.default.dirname(filePath);
|
||||
@@ -61,7 +57,6 @@ class SteamClientService {
|
||||
fs_1.default.mkdirSync(dir, { recursive: true });
|
||||
const vdfContent = (0, simple_vdf_1.stringify)(data);
|
||||
fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8');
|
||||
// Atomic rename
|
||||
fs_1.default.renameSync(tempPath, filePath);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -77,7 +72,6 @@ class SteamClientService {
|
||||
if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) {
|
||||
this.readLocalAccounts();
|
||||
chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
|
||||
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
|
||||
this.readLocalAccounts();
|
||||
});
|
||||
}
|
||||
@@ -89,7 +83,7 @@ class SteamClientService {
|
||||
try {
|
||||
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
||||
if (!content.trim())
|
||||
return; // Empty file
|
||||
return;
|
||||
const data = (0, simple_vdf_1.parse)(content);
|
||||
if (!data || !data.users)
|
||||
return;
|
||||
@@ -99,8 +93,7 @@ class SteamClientService {
|
||||
if (!user || !user.AccountName)
|
||||
continue;
|
||||
accounts.push({
|
||||
steamId: steamId64,
|
||||
accountName: user.AccountName,
|
||||
steamId: steamId64, accountName: user.AccountName,
|
||||
personaName: user.PersonaName || user.AccountName,
|
||||
timestamp: parseInt(user.Timestamp) || 0
|
||||
});
|
||||
@@ -108,9 +101,7 @@ class SteamClientService {
|
||||
if (this.onAccountsChanged)
|
||||
this.onAccountsChanged(accounts);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
|
||||
}
|
||||
catch (error) { }
|
||||
}
|
||||
extractAccountConfig(accountName) {
|
||||
const configPath = this.getConfigVdfPath();
|
||||
@@ -123,7 +114,6 @@ class SteamClientService {
|
||||
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[SteamClient] Failed to extract config.vdf data');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -131,17 +121,7 @@ class SteamClientService {
|
||||
const configPath = this.getConfigVdfPath();
|
||||
if (!configPath)
|
||||
return;
|
||||
let data = {
|
||||
InstallConfigStore: {
|
||||
Software: {
|
||||
Valve: {
|
||||
Steam: {
|
||||
Accounts: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
||||
if (fs_1.default.existsSync(configPath)) {
|
||||
try {
|
||||
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
||||
@@ -151,18 +131,23 @@ class SteamClientService {
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
// Ensure safe nesting
|
||||
if (!data.InstallConfigStore)
|
||||
data.InstallConfigStore = {};
|
||||
if (!data.InstallConfigStore.Software)
|
||||
data.InstallConfigStore.Software = {};
|
||||
if (!data.InstallConfigStore.Software.Valve)
|
||||
data.InstallConfigStore.Software.Valve = {};
|
||||
if (!data.InstallConfigStore.Software.Valve.Steam)
|
||||
data.InstallConfigStore.Software.Valve.Steam = {};
|
||||
if (!data.InstallConfigStore.Software.Valve.Steam.Accounts)
|
||||
data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
|
||||
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
|
||||
const ensurePath = (obj, keys) => {
|
||||
let curr = obj;
|
||||
for (const key of keys) {
|
||||
if (!curr[key] || typeof curr[key] !== 'object')
|
||||
curr[key] = {};
|
||||
curr = curr[key];
|
||||
}
|
||||
return curr;
|
||||
};
|
||||
const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']);
|
||||
// FAILPROOF: Force crucial flags that Steam uses to decide session validity
|
||||
steamAccounts[accountName] = {
|
||||
...accountData,
|
||||
RememberPassword: "1",
|
||||
AllowAutoLogin: "1",
|
||||
Timestamp: Math.floor(Date.now() / 1000).toString()
|
||||
};
|
||||
try {
|
||||
this.safeWriteVdf(configPath, data);
|
||||
console.log(`[SteamClient] Safely injected session for ${accountName}`);
|
||||
@@ -220,6 +205,7 @@ class SteamClientService {
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
// Injection of the actual authentication blob
|
||||
if (accountConfig && accountName) {
|
||||
this.injectAccountConfig(accountName, accountConfig);
|
||||
}
|
||||
@@ -232,11 +218,7 @@ class SteamClientService {
|
||||
for (const regPath of regLocations) {
|
||||
if (!fs_1.default.existsSync(path_1.default.dirname(regPath)))
|
||||
continue;
|
||||
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: {
|
||||
AutoLoginUser: "",
|
||||
RememberPassword: "1",
|
||||
AlreadyLoggedIn: "1"
|
||||
} } } } } };
|
||||
let regData = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } };
|
||||
if (fs_1.default.existsSync(regPath)) {
|
||||
try {
|
||||
const content = fs_1.default.readFileSync(regPath, 'utf-8');
|
||||
@@ -246,7 +228,6 @@ class SteamClientService {
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
// Deep merge helper
|
||||
const ensurePath = (obj, keys) => {
|
||||
let curr = obj;
|
||||
for (const key of keys) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { fetchProfileData, scrapeBanStatus } from './services/steam-web';
|
||||
import { scrapeCooldown } from './services/scraper';
|
||||
import { scrapeCooldown, SteamAuthError } from './services/scraper';
|
||||
import { steamClient, LocalSteamAccount } from './services/steam-client';
|
||||
import { BackendService } from './services/backend';
|
||||
|
||||
@@ -135,7 +135,14 @@ const createTray = () => {
|
||||
tray = new Tray(icon);
|
||||
tray.setToolTip('Ultimate Ban Tracker');
|
||||
tray.on('click', () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } });
|
||||
updateTrayMenu();
|
||||
|
||||
// Load initial themed icon
|
||||
const config = store.get('serverConfig');
|
||||
if (config?.theme) {
|
||||
setAppIcon(config.theme);
|
||||
} else {
|
||||
updateTrayMenu(); // Fallback to refresh menu
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
@@ -162,6 +169,25 @@ const updateTrayMenu = () => {
|
||||
tray.setContextMenu(contextMenu);
|
||||
};
|
||||
|
||||
const setAppIcon = (themeName: string = 'steam') => {
|
||||
const assetsDir = path.join(__dirname, '..', 'assets-build', 'icons');
|
||||
const iconPath = path.join(assetsDir, `${themeName}.svg`);
|
||||
|
||||
if (!fs.existsSync(iconPath)) return;
|
||||
|
||||
const icon = nativeImage.createFromPath(iconPath);
|
||||
|
||||
// Update Tray
|
||||
if (tray) {
|
||||
tray.setImage(icon.resize({ width: 16, height: 16 }));
|
||||
}
|
||||
|
||||
// Update Main Window
|
||||
if (mainWindow) {
|
||||
mainWindow.setIcon(icon);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Steam Logic ---
|
||||
const killSteam = async () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
@@ -221,7 +247,11 @@ const scrapeAccountData = async (account: Account) => {
|
||||
if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString());
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true;
|
||||
if (e instanceof SteamAuthError) {
|
||||
account.authError = true;
|
||||
} else {
|
||||
console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (backend && !account._id.startsWith('shared_')) {
|
||||
@@ -260,15 +290,31 @@ const syncAccounts = async (isManual = false) => {
|
||||
} else {
|
||||
const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||
const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||
if (sDate > lDate) {
|
||||
// 1. SENSITIVE DATA SYNC (Credentials)
|
||||
const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0);
|
||||
const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0);
|
||||
|
||||
const isLocalAccount = !exists._id.startsWith('shared_');
|
||||
const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError;
|
||||
|
||||
// SMART OVERWRITE LOGIC:
|
||||
// - If it's a remote shared account: Newest wins.
|
||||
// - If it's a LOCAL account: Only overwrite if our local session is broken/missing.
|
||||
const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate);
|
||||
|
||||
if (shouldOverwriteCredentials) {
|
||||
if (s.loginName) exists.loginName = s.loginName;
|
||||
if (s.loginConfig) exists.loginConfig = s.loginConfig;
|
||||
if (s.steamLoginSecure) { exists.steamLoginSecure = s.steamLoginSecure; exists.autoCheckCooldown = true; exists.authError = false; }
|
||||
if (s.steamLoginSecure) {
|
||||
exists.steamLoginSecure = s.steamLoginSecure;
|
||||
exists.autoCheckCooldown = true;
|
||||
exists.authError = false;
|
||||
}
|
||||
exists.sessionUpdatedAt = s.sessionUpdatedAt;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// Metadata Sync (Pull)
|
||||
// 2. Metadata Sync (Pull) - Always "Newest Wins"
|
||||
const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0);
|
||||
const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0);
|
||||
if (sMetaDate > lMetaDate) {
|
||||
@@ -571,6 +617,13 @@ 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('switch-account', async (event, loginName: string) => {
|
||||
if (!loginName) return false;
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
revokeAccountAccess: (steamId: string, targetSteamId: string) => ipcRenderer.invoke('revoke-account-access', steamId, targetSteamId),
|
||||
revokeAllAccountAccess: (steamId: string) => ipcRenderer.invoke('revoke-all-account-access', steamId),
|
||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
||||
updateAppIcon: (theme: string) => ipcRenderer.invoke('update-app-icon', theme),
|
||||
openSteamAppLogin: () => ipcRenderer.invoke('open-steam-app-login'),
|
||||
openSteamLogin: (steamId: string) => ipcRenderer.invoke('open-steam-login', steamId),
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@ export interface CooldownData {
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
// Custom error to identify session death
|
||||
export class SteamAuthError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "SteamAuthError";
|
||||
}
|
||||
}
|
||||
|
||||
export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): Promise<CooldownData> => {
|
||||
const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`;
|
||||
|
||||
@@ -15,16 +23,21 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
||||
'Cookie': steamLoginSecure,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
},
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
validateStatus: (status) => status < 500 // Allow redirects to handle them manually
|
||||
});
|
||||
|
||||
// If Steam redirects us to the login page, the cookie is dead
|
||||
if (response.data.includes('Sign In') || response.request.path.includes('/login')) {
|
||||
throw new SteamAuthError('Invalid or expired steamLoginSecure cookie');
|
||||
}
|
||||
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) {
|
||||
throw new Error('Invalid or expired steamLoginSecure cookie');
|
||||
if (!response.data.includes('Personal Game Data')) {
|
||||
throw new SteamAuthError('Session invalid: Personal Game Data not accessible');
|
||||
}
|
||||
|
||||
// 1. Locate the specific table containing cooldown info
|
||||
let expirationDate: Date | undefined = undefined;
|
||||
|
||||
$('table').each((_, table) => {
|
||||
@@ -36,15 +49,10 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
||||
rows.each((_, row) => {
|
||||
const dateText = $(row).find('td').eq(expirationIndex).text().trim();
|
||||
if (dateText && dateText !== '') {
|
||||
// Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC'
|
||||
const cleanDateText = dateText.replace(' GMT', ' UTC');
|
||||
const parsed = new Date(cleanDateText);
|
||||
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
// We want the newest expiration date found
|
||||
if (!expirationDate || parsed > (expirationDate as Date)) {
|
||||
expirationDate = parsed;
|
||||
}
|
||||
if (!expirationDate || parsed > (expirationDate as Date)) expirationDate = parsed;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -52,11 +60,7 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
||||
});
|
||||
|
||||
if (expirationDate && (expirationDate as Date).getTime() > Date.now()) {
|
||||
console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`);
|
||||
return {
|
||||
isActive: true,
|
||||
expiresAt: expirationDate
|
||||
};
|
||||
return { isActive: true, expiresAt: expirationDate };
|
||||
}
|
||||
|
||||
const content = $('#personal_game_data_content').text();
|
||||
@@ -66,7 +70,8 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string):
|
||||
|
||||
return { isActive: false };
|
||||
} catch (error: any) {
|
||||
console.error(`[Scraper] Error for ${steamId}:`, error.message);
|
||||
throw error;
|
||||
if (error instanceof SteamAuthError) throw error;
|
||||
console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message);
|
||||
throw error; // Generic errors don't trigger re-auth
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,20 +55,13 @@ class SteamClientService {
|
||||
return path.join(this.steamPath, 'config', 'config.vdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe Atomic Write: Writes to a temp file and renames it.
|
||||
* This prevents file corruption if the app crashes during write.
|
||||
*/
|
||||
private safeWriteVdf(filePath: string, data: any) {
|
||||
const tempPath = `${filePath}.tmp_${Date.now()}`;
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
const vdfContent = stringify(data);
|
||||
fs.writeFileSync(tempPath, vdfContent, 'utf-8');
|
||||
|
||||
// Atomic rename
|
||||
fs.renameSync(tempPath, filePath);
|
||||
} catch (e: any) {
|
||||
console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`);
|
||||
@@ -80,11 +73,9 @@ class SteamClientService {
|
||||
public startWatching(callback: (accounts: LocalSteamAccount[]) => void) {
|
||||
this.onAccountsChanged = callback;
|
||||
const loginUsersPath = this.getLoginUsersPath();
|
||||
|
||||
if (loginUsersPath && fs.existsSync(loginUsersPath)) {
|
||||
this.readLocalAccounts();
|
||||
chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => {
|
||||
console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`);
|
||||
this.readLocalAccounts();
|
||||
});
|
||||
}
|
||||
@@ -93,64 +84,41 @@ class SteamClientService {
|
||||
private readLocalAccounts() {
|
||||
const filePath = this.getLoginUsersPath();
|
||||
if (!filePath || !fs.existsSync(filePath)) return;
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
if (!content.trim()) return; // Empty file
|
||||
|
||||
if (!content.trim()) return;
|
||||
const data = parse(content) as any;
|
||||
if (!data || !data.users) return;
|
||||
|
||||
const accounts: LocalSteamAccount[] = [];
|
||||
for (const [steamId64, userData] of Object.entries(data.users)) {
|
||||
const user = userData as any;
|
||||
if (!user || !user.AccountName) continue;
|
||||
|
||||
accounts.push({
|
||||
steamId: steamId64,
|
||||
accountName: user.AccountName,
|
||||
steamId: steamId64, accountName: user.AccountName,
|
||||
personaName: user.PersonaName || user.AccountName,
|
||||
timestamp: parseInt(user.Timestamp) || 0
|
||||
});
|
||||
}
|
||||
|
||||
if (this.onAccountsChanged) this.onAccountsChanged(accounts);
|
||||
} catch (error) {
|
||||
console.error('[SteamClient] Error parsing loginusers.vdf:', error);
|
||||
}
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
public extractAccountConfig(accountName: string): any | null {
|
||||
const configPath = this.getConfigVdfPath();
|
||||
if (!configPath || !fs.existsSync(configPath)) return null;
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const data = parse(content) as any;
|
||||
const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts;
|
||||
return (accounts && accounts[accountName]) ? accounts[accountName] : null;
|
||||
} catch (e) {
|
||||
console.error('[SteamClient] Failed to extract config.vdf data');
|
||||
return null;
|
||||
}
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
public injectAccountConfig(accountName: string, accountData: any) {
|
||||
const configPath = this.getConfigVdfPath();
|
||||
if (!configPath) return;
|
||||
|
||||
let data: any = {
|
||||
InstallConfigStore: {
|
||||
Software: {
|
||||
Valve: {
|
||||
Steam: {
|
||||
Accounts: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } };
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
@@ -159,14 +127,24 @@ class SteamClientService {
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
// Ensure safe nesting
|
||||
if (!data.InstallConfigStore) data.InstallConfigStore = {};
|
||||
if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {};
|
||||
if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {};
|
||||
if (!data.InstallConfigStore.Software.Valve.Steam) data.InstallConfigStore.Software.Valve.Steam = {};
|
||||
if (!data.InstallConfigStore.Software.Valve.Steam.Accounts) data.InstallConfigStore.Software.Valve.Steam.Accounts = {};
|
||||
const ensurePath = (obj: any, keys: string[]) => {
|
||||
let curr = obj;
|
||||
for (const key of keys) {
|
||||
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
|
||||
curr = curr[key];
|
||||
}
|
||||
return curr;
|
||||
};
|
||||
|
||||
data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData;
|
||||
const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']);
|
||||
|
||||
// FAILPROOF: Force crucial flags that Steam uses to decide session validity
|
||||
steamAccounts[accountName] = {
|
||||
...accountData,
|
||||
RememberPassword: "1",
|
||||
AllowAutoLogin: "1",
|
||||
Timestamp: Math.floor(Date.now() / 1000).toString()
|
||||
};
|
||||
|
||||
try {
|
||||
this.safeWriteVdf(configPath, data);
|
||||
@@ -226,6 +204,7 @@ class SteamClientService {
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
// Injection of the actual authentication blob
|
||||
if (accountConfig && accountName) {
|
||||
this.injectAccountConfig(accountName, accountConfig);
|
||||
}
|
||||
@@ -239,13 +218,7 @@ class SteamClientService {
|
||||
|
||||
for (const regPath of regLocations) {
|
||||
if (!fs.existsSync(path.dirname(regPath))) continue;
|
||||
|
||||
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {
|
||||
AutoLoginUser: "",
|
||||
RememberPassword: "1",
|
||||
AlreadyLoggedIn: "1"
|
||||
} } } } } };
|
||||
|
||||
let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } };
|
||||
if (fs.existsSync(regPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(regPath, 'utf-8');
|
||||
@@ -254,13 +227,9 @@ class SteamClientService {
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
// Deep merge helper
|
||||
const ensurePath = (obj: any, keys: string[]) => {
|
||||
let curr = obj;
|
||||
for (const key of keys) {
|
||||
if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {};
|
||||
curr = curr[key];
|
||||
}
|
||||
for (const key of keys) { if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; curr = curr[key]; }
|
||||
return curr;
|
||||
};
|
||||
|
||||
@@ -269,10 +238,7 @@ class SteamClientService {
|
||||
steamKey.RememberPassword = "1";
|
||||
steamKey.AlreadyLoggedIn = "1";
|
||||
steamKey.WantsOfflineMode = "0";
|
||||
|
||||
try {
|
||||
this.safeWriteVdf(regPath, regData);
|
||||
} catch (e) { }
|
||||
try { this.safeWriteVdf(regPath, regData); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ultimate-ban-tracker-desktop",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ultimate-ban-tracker-desktop",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ultimate-ban-tracker-desktop",
|
||||
"description": "Professional Steam Account Manager & Ban Tracker",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"author": "Nils Pukropp <nils@narl.io>",
|
||||
"homepage": "https://narl.io",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
@@ -31,8 +31,27 @@ export const AppThemeProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
||||
if (api?.updateServerConfig) {
|
||||
await api.updateServerConfig({ theme });
|
||||
}
|
||||
if (api?.updateAppIcon) {
|
||||
try {
|
||||
await api.updateAppIcon(theme);
|
||||
} catch (e) { }
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateIcon = async () => {
|
||||
const api = (window as any).electronAPI;
|
||||
if (api?.updateAppIcon && currentTheme) {
|
||||
try {
|
||||
await api.updateAppIcon(currentTheme);
|
||||
} catch (e) {
|
||||
console.warn("[ThemeContext] updateAppIcon failed (likely not registered yet)");
|
||||
}
|
||||
}
|
||||
};
|
||||
updateIcon();
|
||||
}, [currentTheme]);
|
||||
|
||||
const theme = useMemo(() => getTheme(currentTheme), [currentTheme]);
|
||||
|
||||
return (
|
||||
|
||||