;
saveBinds: () => void;
isWindows: boolean;
}
function buildGeneralSection(
body: HTMLElement, gameConf: any, uiConfRaw: any, perfConf: any, bag: SettingsBag,
): void {
const perfDefaults = { fpsUnlocked: true };
const perf = { ...perfDefaults, ...perfConf };
body.appendChild(createToggleRow({
label: 'Unlimited FPS',
desc: 'Uncap the frame rate (requires restart)',
checked: perf.fpsUnlocked, restart: true,
onChange: (v) => { perf.fpsUnlocked = v; ipcRenderer.invoke('set-config', 'performance', perf); },
}));
const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window' };
const game = { ...gameDefaults, ...gameConf };
body.appendChild(createSelectRow({
label: 'Social/Hub Tab Behaviour',
desc: 'How social, market, and editor pages open when clicked',
options: [{ value: 'New Window', label: 'Tabs (Separate Window)' }, { value: 'Same Window', label: 'Tabs (Overlay Game)' }],
value: game.socialTabBehaviour, instant: true,
onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); },
}));
const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false };
const ui = { ...uiDefaults, ...uiConfRaw };
function saveUI(): void {
ipcRenderer.invoke('set-config', 'ui', ui);
}
body.appendChild(createToggleRow({
label: 'Show Exit Button',
desc: 'Show the exit button in the game sidebar',
checked: ui.showExitButton, instant: true,
onChange: (v) => {
ui.showExitButton = v; saveUI();
const btn = document.getElementById('clientExit');
if (btn) btn.style.display = v ? 'flex' : 'none';
},
}));
body.appendChild(createToggleRow({
label: 'Block Death Screen Animation',
desc: 'Disable the slide-in animation on the death screen',
checked: ui.deathscreenAnimation, instant: true,
onChange: (v) => { ui.deathscreenAnimation = v; saveUI(); setDeathAnimBlock(v); },
}));
body.appendChild(createToggleRow({
label: 'Hide Menu Popups',
desc: 'Hide promotional notifications, offers, and streams on the main menu',
checked: ui.hideMenuPopups, instant: true,
onChange: (v) => {
ui.hideMenuPopups = v; saveUI();
if (v) startHidePopups(); else stopHidePopups();
},
}));
body.appendChild(createToggleRow({
label: 'Join as Spectator',
desc: 'Automatically enable spectate mode when joining a game',
checked: game.joinAsSpectator, instant: true,
onChange: (v) => { game.joinAsSpectator = v; ipcRenderer.invoke('set-config', 'game', game); },
}));
body.appendChild(createToggleRow({
label: 'Menu Timer',
desc: 'Show the game/spectate timer on the menu screen',
checked: ui.menuTimer ?? true, instant: true,
onChange: (v) => { ui.menuTimer = v; saveUI(); setMenuTimer(v); },
}));
body.appendChild(createToggleRow({
label: 'Double Ping Display',
desc: 'Show the real ping value (Krunker displays half the actual latency)',
checked: ui.doublePing ?? true, refreshOnly: true,
onChange: (v) => { ui.doublePing = v; saveUI(); },
}));
body.appendChild(createToggleRow({
label: 'Show Ping in Player List',
desc: 'Replace the ping icon with numeric millisecond values in the player list',
checked: game.showPing ?? true, refreshOnly: true,
onChange: (v) => { game.showPing = v; ipcRenderer.invoke('set-config', 'game', game); },
}));
if (bag.isWindows) {
body.appendChild(createToggleRow({
label: 'Raw Input',
desc: 'Bypass OS mouse acceleration for direct 1:1 sensor input (Windows only)',
checked: game.rawInput ?? true, refreshOnly: true,
onChange: (v) => { game.rawInput = v; ipcRenderer.invoke('set-config', 'game', game); },
}));
}
body.appendChild(createToggleRow({
label: 'Hardpoint Enemy Counter',
desc: 'Show enemy capture points in Hardpoint mode',
checked: game.hpEnemyCounter ?? true, refreshOnly: true,
onChange: (v) => {
game.hpEnemyCounter = v; ipcRenderer.invoke('set-config', 'game', game);
if (v) initHPCounter(); else destroyHPCounter();
},
}));
body.appendChild(createToggleRow({
label: 'Show Changelog',
desc: 'Show release notes popup when the client updates',
checked: ui.showChangelog ?? true, instant: true,
onChange: (v) => { ui.showChangelog = v; saveUI(); },
}));
if (ui.deathscreenAnimation) setDeathAnimBlock(true);
if (ui.menuTimer ?? true) setMenuTimer(true);
if (ui.hideMenuPopups) startHidePopups();
body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
bag.binds.fullscreenToggle = b;
bag.saveBinds();
}, undefined, true));
}
function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any): void {
const swapEnabled = swapperConf ? swapperConf.enabled : true;
const ui = { cssTheme: 'disabled', loadingTheme: 'disabled', backgroundUrl: '', ...uiConfRaw };
function saveUI(): void {
ipcRenderer.invoke('set-config', 'ui', ui);
}
body.appendChild(createToggleRow({
label: 'Resource Swapper',
desc: 'Replace game textures, sounds, and models with local files',
checked: swapEnabled,
restart: true,
onChange: (v) => {
ipcRenderer.invoke('get-config', 'swapper').then((conf: any) => {
ipcRenderer.invoke('set-config', 'swapper', { enabled: v, path: conf ? conf.path : '' });
});
},
}));
const folderRow = document.createElement('div');
folderRow.className = 'setting settName safety-0 has-button';
folderRow.innerHTML =
'Swapper Folder' +
'Place replacement assets here (textures/, sound/, models/)
';
const swapFolderBtn = document.createElement('div');
swapFolderBtn.className = 'settingsBtn';
swapFolderBtn.title = 'Open Folder';
swapFolderBtn.innerHTML = 'folder Swapper';
swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
folderRow.appendChild(swapFolderBtn);
body.appendChild(folderRow);
// ── CSS Theme selector (populated from swap/themes/) ──
const themeRow = document.createElement('div');
themeRow.className = 'setting settName safety-0 sel has-button';
themeRow.innerHTML =
'CSS Theme' +
'Load a custom CSS theme from swap/themes/
';
const themeSelect = document.createElement('select');
themeSelect.className = 's-update inputGrey2';
themeSelect.innerHTML = '';
themeRow.appendChild(themeSelect);
const themeFolderBtn = document.createElement('div');
themeFolderBtn.className = 'settingsBtn';
themeFolderBtn.title = 'Open Themes Folder';
themeFolderBtn.innerHTML = 'folder';
themeFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-themes-folder'));
themeRow.appendChild(themeFolderBtn);
body.appendChild(themeRow);
ipcRenderer.invoke('list-themes').then((themes: Array<{ id: string; label: string }>) => {
themeSelect.innerHTML = '';
for (const t of themes) {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.label;
if (t.id === ui.cssTheme) opt.selected = true;
themeSelect.appendChild(opt);
}
});
themeSelect.addEventListener('change', () => {
ui.cssTheme = themeSelect.value;
saveUI();
onSettingChanged('refresh');
});
// ── Loading Screen Background ──
const bgRow = document.createElement('div');
bgRow.className = 'setting settName safety-0 sel has-button';
bgRow.innerHTML =
'Loading Background' +
'Custom background image for the loading screen (swap/backgrounds/)
';
const bgSelect = document.createElement('select');
bgSelect.className = 's-update inputGrey2';
bgSelect.innerHTML = '';
bgRow.appendChild(bgSelect);
const bgFolderBtn = document.createElement('div');
bgFolderBtn.className = 'settingsBtn';
bgFolderBtn.title = 'Open Backgrounds Folder';
bgFolderBtn.innerHTML = 'folder';
bgFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-backgrounds-folder'));
bgRow.appendChild(bgFolderBtn);
body.appendChild(bgRow);
ipcRenderer.invoke('list-loading-themes').then((themes: Array<{ id: string; label: string }>) => {
bgSelect.innerHTML = '';
for (const t of themes) {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.label;
if (t.id === ui.loadingTheme) opt.selected = true;
bgSelect.appendChild(opt);
}
});
bgSelect.addEventListener('change', () => {
ui.loadingTheme = bgSelect.value;
saveUI();
onSettingChanged('refresh');
});
}
function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true, autoJoin: false };
function saveMM(): void {
ipcRenderer.invoke('set-config', 'matchmaker', mm);
}
body.appendChild(createToggleRow({
label: 'Custom Matchmaker',
desc: 'Use the matchmaker hotkey to find a game matching your criteria',
checked: mm.enabled, instant: true,
onChange: (v) => { mm.enabled = v; saveMM(); },
}));
body.appendChild(createToggleRow({
label: 'Open Server Browser on Cancel',
desc: 'Opens the server browser when no game is found and you cancel',
checked: mm.openServerBrowser, instant: true,
onChange: (v) => { mm.openServerBrowser = v; saveMM(); },
}));
body.appendChild(createToggleRow({
label: 'Auto-Join',
desc: 'Automatically join the best match without showing the popup',
checked: mm.autoJoin ?? false, instant: true,
onChange: (v) => { mm.autoJoin = v; saveMM(); },
}));
body.appendChild(createKeybindRow('Matchmaker Hotkey', 'Key to trigger the custom matchmaker', bag.binds.matchmaker, (b) => {
bag.binds.matchmaker = b;
bag.saveBinds();
}, undefined, true));
body.appendChild(createKeybindRow('Matchmaker Accept', 'Key to accept a found game', bag.binds.matchmakerAccept, (b) => {
bag.binds.matchmakerAccept = b;
bag.saveBinds();
}, undefined, true));
body.appendChild(createKeybindRow('Matchmaker Cancel', 'Key to dismiss the matchmaker popup', bag.binds.matchmakerCancel, (b) => {
bag.binds.matchmakerCancel = b;
bag.saveBinds();
}, undefined, true));
body.appendChild(createNumberRow({
label: 'Min Players', desc: 'Minimum player count in lobby (0-7)',
min: 0, max: 7, value: mm.minPlayers, instant: true,
onChange: (v) => { mm.minPlayers = v; saveMM(); },
}));
body.appendChild(createNumberRow({
label: 'Max Players', desc: 'Maximum player count in lobby (0-7)',
min: 0, max: 7, value: mm.maxPlayers, instant: true,
onChange: (v) => { mm.maxPlayers = v; saveMM(); },
}));
body.appendChild(createNumberRow({
label: 'Min Remaining Time', desc: 'Minimum seconds remaining in match (0-480)',
min: 0, max: 480, value: mm.minRemainingTime, instant: true,
onChange: (v) => { mm.minRemainingTime = v; saveMM(); },
}));
body.appendChild(createCheckboxGrid({
header: 'Regions (none selected = all)',
items: MATCHMAKER_REGIONS.map(r => ({ value: r, label: MATCHMAKER_REGION_NAMES[r] || r })),
selected: mm.regions,
onChange: () => saveMM(),
}));
body.appendChild(createCheckboxGrid({
header: 'Gamemodes (none selected = all)',
items: MATCHMAKER_GAMEMODE_FILTER.map(gm => ({ value: gm, label: gm })),
selected: mm.gamemodes,
onChange: () => saveMM(),
}));
if (!mm.maps) mm.maps = [];
body.appendChild(createCheckboxGrid({
header: 'Maps (none selected = all)',
items: MATCHMAKER_MAP_FILTER.map(m => ({ value: m, label: MATCHMAKER_MAP_NAMES[m] || m })),
selected: mm.maps,
onChange: () => saveMM(),
}));
}
function buildDiscordSection(body: HTMLElement, discordConf: any): void {
const discord = { enabled: false, ...discordConf };
body.appendChild(createToggleRow({
label: 'Discord Rich Presence',
desc: 'Show game activity in your Discord profile',
checked: discord.enabled,
restart: true,
onChange: (v) => {
discord.enabled = v;
ipcRenderer.invoke('set-config', 'discord', discord);
},
}));
}
// ── Alt Manager helpers ──
function switchToAccount(account: { username: string; password: string }): void {
const w = window as any;
if (typeof w.loginOrRegister !== 'function') return;
function doLogin(): void {
w.loginOrRegister();
queueMicrotask(() => {
const toggleBtn = document.querySelector('.auth-toggle-btn') as HTMLElement;
if (toggleBtn && toggleBtn.textContent?.includes('username')) toggleBtn.click();
queueMicrotask(() => {
const nameInput = document.querySelector('#accName') as HTMLInputElement;
const passInput = document.querySelector('#accPass') as HTMLInputElement;
if (!nameInput || !passInput) return;
nameInput.value = account.username;
passInput.value = account.password;
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
passInput.dispatchEvent(new Event('input', { bubbles: true }));
const submitBtn = document.querySelector('.io-button') as HTMLElement;
if (submitBtn) submitBtn.click();
});
});
}
if (typeof w.logoutAcc === 'function') {
w.logoutAcc();
setTimeout(doLogin, 500);
} else {
doLogin();
}
}
function buildAccountsSection(body: HTMLElement, accountsArr: any[]): void {
const accounts: any[] = accountsArr || [];
const addBtn = document.createElement('div');
addBtn.className = 'setting settName safety-0 has-button';
addBtn.innerHTML =
'Add Account' +
'' +
'Save a Krunker account for quick switching
';
body.appendChild(addBtn);
const form = document.createElement('div');
form.className = 'kpc-acc-form';
form.style.display = 'none';
form.innerHTML =
'' +
'' +
'' +
'' +
'' +
'' +
'
';
body.appendChild(form);
const labelIn = form.querySelector('.kpc-acc-label') as HTMLInputElement;
const userIn = form.querySelector('.kpc-acc-user') as HTMLInputElement;
const passIn = form.querySelector('.kpc-acc-pass') as HTMLInputElement;
// Stop Krunker's global keydown handler from eating keystrokes in our inputs
form.querySelectorAll('input').forEach(input => {
input.addEventListener('keydown', (e) => e.stopPropagation());
});
addBtn.querySelector('button')!.addEventListener('click', () => {
form.style.display = form.style.display === 'none' ? '' : 'none';
});
form.querySelector('.kpc-acc-cancel')!.addEventListener('click', () => {
form.style.display = 'none';
});
const listEl = document.createElement('div');
body.appendChild(listEl);
function renderList(): void {
listEl.innerHTML = '';
if (accounts.length === 0) {
listEl.innerHTML = 'No saved accounts
';
return;
}
accounts.forEach((acc, i) => {
const row = document.createElement('div');
row.className = 'kpc-acc-item';
row.innerHTML =
'' +
'' + escapeHtml(acc.label) + '' +
'
' +
'' +
'' +
'' +
'
';
row.querySelector('.kpc-acc-switch')!.addEventListener('click', () => {
ipcRenderer.invoke('alt-get-credentials', i).then((creds: { username: string; password: string } | null) => {
if (creds) switchToAccount(creds);
});
});
row.querySelector('.kpc-acc-delete')!.addEventListener('click', () => {
ipcRenderer.invoke('alt-remove', i).then(() => {
accounts.splice(i, 1);
renderList();
});
});
listEl.appendChild(row);
});
}
renderList();
form.querySelector('.kpc-acc-save')!.addEventListener('click', () => {
const label = labelIn.value.trim();
const user = userIn.value.trim();
const pass = passIn.value;
if (!label || !user || !pass) return;
const newAcc = { label, username: user, password: pass };
ipcRenderer.invoke('alt-save', newAcc).then(() => {
accounts.push({ label });
labelIn.value = '';
userIn.value = '';
passIn.value = '';
form.style.display = 'none';
renderList();
});
});
}
function buildChatSection(body: HTMLElement, gameConf: any, translatorConf: any): void {
const game = { betterChat: true, chatHistorySize: 200, ...gameConf };
function saveGame(): void {
ipcRenderer.invoke('set-config', 'game', game);
}
body.appendChild(createToggleRow({
label: 'Better Chat',
desc: 'Merge team and all-chat with colored [T]/[M] prefixes',
checked: game.betterChat, instant: true,
onChange: (v) => { game.betterChat = v; saveGame(); setBetterChat(v); },
}));
body.appendChild(createNumberRow({
label: 'Chat History Size', desc: 'Maximum chat messages to keep (0 to disable history preservation)',
min: 0, max: 1000, value: game.chatHistorySize, instant: true,
onChange: (v) => { game.chatHistorySize = v; saveGame(); setChatHistorySize(v); },
}));
// Translator settings inline
const tl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf };
function saveTL(): void {
ipcRenderer.invoke('set-config', 'translator', tl);
}
body.appendChild(createToggleRow({
label: 'Chat Translator',
desc: 'Automatically translate non-English chat messages',
checked: tl.enabled, instant: true,
onChange: (v) => {
tl.enabled = v;
saveTL();
updateTranslatorConfig({ enabled: v });
},
}));
body.appendChild(createSelectRow({
label: 'Target Language',
desc: 'Language to translate messages into', instant: true,
options: [
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
{ value: 'de', label: 'German' },
{ value: 'pt', label: 'Portuguese' },
{ value: 'ru', label: 'Russian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'zh', label: 'Chinese' },
{ value: 'ar', label: 'Arabic' },
{ value: 'hi', label: 'Hindi' },
{ value: 'tr', label: 'Turkish' },
{ value: 'pl', label: 'Polish' },
{ value: 'it', label: 'Italian' },
{ value: 'nl', label: 'Dutch' },
],
value: tl.targetLanguage,
onChange: (v) => {
tl.targetLanguage = v;
saveTL();
updateTranslatorConfig({ targetLanguage: v });
},
}));
body.appendChild(createToggleRow({
label: 'Show Language Tag',
desc: 'Show detected language code before translations (e.g. [FR])',
checked: tl.showLanguageTag, instant: true,
onChange: (v) => {
tl.showLanguageTag = v;
saveTL();
updateTranslatorConfig({ showLanguageTag: v });
},
}));
}
function buildAdvancedSection(
body: HTMLElement, advConf: any, perfConf: any, isWindows: boolean,
): void {
const advDefaults = {
removeUselessFeatures: true,
gpuRasterizing: false,
helpfulFlags: true,
increaseLimits: false,
lowLatency: false,
experimentalFlags: false,
angleBackend: 'default',
verboseLogging: false,
};
const adv = { ...advDefaults, ...advConf };
const perf = { cpuThrottleGame: 1, cpuThrottleMenu: 1.5, processPriority: 'Normal', ...perfConf };
function savePerf(): void {
ipcRenderer.invoke('set-config', 'performance', perf);
}
function saveAdv(): void {
ipcRenderer.invoke('set-config', 'advanced', adv);
}
const angleOptions: Array<{ value: string; label: string }> = isWindows
? [
{ value: 'default', label: 'Default (D3D11)' },
{ value: 'gl', label: 'OpenGL' },
{ value: 'd3d9', label: 'Direct3D 9' },
{ value: 'd3d11', label: 'Direct3D 11' },
{ value: 'd3d11on12', label: 'D3D11on12' },
{ value: 'vulkan', label: 'Vulkan' },
]
: [
{ value: 'default', label: 'Default' },
{ value: 'gl', label: 'OpenGL' },
{ value: 'vulkan', label: 'Vulkan' },
];
body.appendChild(createSelectRow({
label: 'ANGLE Backend',
desc: 'Graphics API used for WebGL rendering',
options: angleOptions,
value: adv.angleBackend, restart: true,
onChange: (v) => { adv.angleBackend = v; saveAdv(); },
}));
const advToggles: Array<{ key: string; label: string; desc: string; safety: number }> = [
{ key: 'removeUselessFeatures', label: 'Remove Useless Features', desc: 'Disables crash reporting, metrics, print preview, and other unused Chromium features', safety: 1 },
{ key: 'gpuRasterizing', label: 'GPU Rasterization', desc: 'Force GPU rasterization and out-of-process rasterization', safety: 2 },
{ key: 'helpfulFlags', label: 'Useful Flags', desc: 'Enables WebGL, JS harmony, V8 features, background throttle prevention, and autoplay bypass', safety: 3 },
{ key: 'increaseLimits', label: 'Increase Limits', desc: 'Raises renderer process, WebGL context, and WebRTC CPU limits; ignores GPU blocklist', safety: 4 },
{ key: 'lowLatency', label: 'Low Latency Flags', desc: 'Enables high-resolution timer, QUIC protocol, and high-performance GPU', safety: 4 },
{ key: 'experimentalFlags', label: 'Experimental Flags', desc: 'Enables accelerated video decode, native GPU memory buffers, high DPI support, and disables pings/proxy', safety: 4 },
];
for (const t of advToggles) {
body.appendChild(createToggleRow({
label: t.label, desc: t.desc,
checked: !!adv[t.key], restart: true,
safety: t.safety,
onChange: (v) => { adv[t.key] = v; saveAdv(); },
}));
}
body.appendChild(createNumberRow({
label: 'CPU Throttle (Game)', desc: 'CPU throttle rate during gameplay (1 = no throttle, 3 = heavy throttle)',
min: 1, max: 3, value: perf.cpuThrottleGame, instant: true, safety: 2,
onChange: (v) => { perf.cpuThrottleGame = v; savePerf(); },
}));
body.appendChild(createNumberRow({
label: 'CPU Throttle (Menu)', desc: 'CPU throttle rate on menu screens (1 = no throttle, 3 = heavy throttle)',
min: 1, max: 3, value: perf.cpuThrottleMenu, instant: true, safety: 1,
onChange: (v) => { perf.cpuThrottleMenu = v; savePerf(); },
}));
if (isWindows) {
body.appendChild(createSelectRow({
label: 'Process Priority',
desc: 'OS-level process priority for the client (Windows only)',
options: [
{ value: 'Normal', label: 'Normal' },
{ value: 'Above Normal', label: 'Above Normal' },
{ value: 'High', label: 'High' },
{ value: 'Below Normal', label: 'Below Normal' },
{ value: 'Low', label: 'Low' },
],
value: perf.processPriority, restart: true, safety: 2,
onChange: (v) => { perf.processPriority = v; savePerf(); },
}));
}
body.appendChild(createToggleRow({
label: 'Verbose Logging',
desc: 'Forward all preload console output to the Electron log file',
checked: adv.verboseLogging, instant: true,
onChange: (v) => {
adv.verboseLogging = v; saveAdv();
_verboseLogging = v;
},
}));
}
// ── Search filter + "no settings" cleanup ──
function applySearchFilter(container: HTMLElement, holder: HTMLElement, searchQuery: string): void {
const query = searchQuery.toLowerCase();
const sections = Array.from(container.children).filter(el => el.querySelector('.setHed'));
sections.forEach(sectionEl => {
const sectionTitle = sectionEl.querySelector('.setHed')?.textContent?.toLowerCase() || '';
const body = sectionEl.querySelector('.setBodH');
if (!body) { (sectionEl as HTMLElement).style.display = 'none'; return; }
if (sectionTitle.includes(query)) {
body.classList.remove('setting-category-collapsed');
return;
}
let visibleCount = 0;
Array.from(body.children).forEach(child => {
const el = child as HTMLElement;
const text = el.textContent?.toLowerCase() || '';
if (text.includes(query)) {
el.style.display = '';
visibleCount++;
} else {
el.style.display = 'none';
}
});
if (visibleCount === 0) {
(sectionEl as HTMLElement).style.display = 'none';
} else {
body.classList.remove('setting-category-collapsed');
}
});
const hasVisible = sections.find(el => (el as HTMLElement).style.display !== 'none');
if (hasVisible) {
Array.from(holder.children).forEach(child => {
if ((child as HTMLElement).textContent?.toLowerCase().includes('no settings')) {
(child as HTMLElement).remove();
}
});
}
}
function renderSettings(searchQuery?: string): void {
const holder = document.getElementById('settHolder');
if (!holder) return;
refreshLevel = RefreshLevel.none;
if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; }
if (searchQuery) {
const existing = holder.querySelector('.kpc-settings');
if (existing) existing.remove();
} else {
while (holder.firstChild) holder.removeChild(holder.firstChild);
}
const container = document.createElement('div');
container.className = 'kpc-settings';
// ── Action button grid ──
const actionGrid = document.createElement('div');
actionGrid.className = 'kpc-action-grid';
const actionButtons: Array<{ label: string; color: string; full?: boolean; action: () => void }> = [
{ label: 'Open Resource Swapper', color: 'kpc-ab-pink', action: () => ipcRenderer.invoke('open-swap-folder') },
{ label: 'Reset Resource Swapper', color: 'kpc-ab-pink', action: () => {
if (confirm('Reset resource swapper? This will delete all files in the swapper folder.')) {
ipcRenderer.invoke('reset-swapper');
}
}},
{ label: 'Open Electron Logs', color: 'kpc-ab-red', action: () => ipcRenderer.invoke('open-electron-log') },
{ label: 'Restart Client', color: 'kpc-ab-orange', full: true, action: () => ipcRenderer.invoke('restart-client') },
{ label: 'Reset Options', color: 'kpc-ab-red', action: () => {
if (confirm('Reset all settings to defaults? The client will restart.')) {
ipcRenderer.invoke('reset-options');
}
}},
{ label: 'Delete All Data', color: 'kpc-ab-red', action: () => {
if (confirm('Delete all data (config, logs)? Scripts are preserved. The client will restart.')) {
ipcRenderer.invoke('delete-all-data');
}
}},
];
for (const ab of actionButtons) {
const btn = document.createElement('button');
btn.className = 'kpc-action-btn ' + ab.color + (ab.full ? ' full' : '');
btn.textContent = ab.label;
btn.addEventListener('click', ab.action);
actionGrid.appendChild(btn);
}
container.appendChild(actionGrid);
// ── Create section shells ──
const genSec = createSection('General');
container.appendChild(genSec.section);
const swapSec = createSection('Swapper');
container.appendChild(swapSec.section);
const mmSec = createSection('Matchmaker');
container.appendChild(mmSec.section);
const chatSec = createSection('Chat');
container.appendChild(chatSec.section);
const discordSec = createSection('Discord');
container.appendChild(discordSec.section);
const accSec = createSection('Accounts', true);
container.appendChild(accSec.section);
const advSec = createSection('Advanced');
container.appendChild(advSec.section);
const usSec = createSection('Userscripts');
container.appendChild(usSec.section);
// Load all configs in a single IPC call + platform info
Promise.all([
ipcRenderer.invoke('get-all-config', ['swapper', 'matchmaker', 'keybinds', 'advanced', 'game', 'ui', 'discord', 'translator', 'accounts', 'performance']),
ipcRenderer.invoke('get-platform'),
]).then(([allConf, platformInfo]: [any, any]) => {
const swapperConf = allConf.swapper;
const mmConf = allConf.matchmaker;
const keybindsConf = allConf.keybinds;
const advConf = allConf.advanced;
const gameConf = allConf.game;
const uiConfRaw = allConf.ui;
const discordConf = allConf.discord;
const translatorConf = allConf.translator;
const defaultBinds = {
matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false },
matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
};
const binds = { ...defaultBinds, ...keybindsConf };
const isWindows = platformInfo && platformInfo.isWindows;
const bag: SettingsBag = {
binds,
saveBinds: () => ipcRenderer.invoke('set-config', 'keybinds', binds),
isWindows,
};
// Populate each section
buildGeneralSection(genSec.body, gameConf, uiConfRaw, allConf.performance, bag);
buildSwapperSection(swapSec.body, swapperConf, uiConfRaw);
buildMatchmakerSection(mmSec.body, mmConf, bag);
buildChatSection(chatSec.body, gameConf, translatorConf);
buildDiscordSection(discordSec.body, discordConf);
buildAccountsSection(accSec.body, allConf.accounts);
buildAdvancedSection(advSec.body, advConf, allConf.performance, isWindows);
renderUserscriptsSection(usSec.body);
if (searchQuery) applySearchFilter(container, holder, searchQuery);
holder.appendChild(container);
}).catch((err: any) => {
console.error('[KCC] Settings render error:', err);
});
}
// ── Userscripts settings section ──
function renderUserscriptsSection(body: HTMLElement): void {
ipcRenderer.invoke('get-config', 'userscripts').then((usConf: any) => {
const us = usConf || { enabled: true, path: '' };
body.appendChild(createToggleRow({
label: 'Userscripts',
desc: 'Load custom scripts from the scripts folder',
checked: us.enabled, restart: true,
onChange: (v) => { us.enabled = v; ipcRenderer.invoke('set-config', 'userscripts', us); },
}));
const usFolderRow = document.createElement('div');
usFolderRow.className = 'setting settName safety-0 has-button';
usFolderRow.innerHTML =
'Scripts Folder' +
'Place .js userscript files here
';
const usFolderBtn = document.createElement('div');
usFolderBtn.className = 'settingsBtn';
usFolderBtn.title = 'Open Folder';
usFolderBtn.innerHTML = 'folder Scripts';
usFolderBtn.addEventListener('click', () => ipcRenderer.invoke('userscripts-open-folder'));
usFolderRow.appendChild(usFolderBtn);
body.appendChild(usFolderRow);
const scriptInstances = getInstances();
if (scriptInstances.length === 0) {
const emptyRow = document.createElement('div');
emptyRow.className = 'setting settName safety-0';
emptyRow.innerHTML =
'No userscripts found. Place .js files in the scripts folder and reload.
';
body.appendChild(emptyRow);
return;
}
for (const inst of scriptInstances) {
const scriptRow = document.createElement('div');
scriptRow.className = 'setting settName safety-0 bool';
const displayName = escapeHtml(inst.meta.name || inst.filename);
const metaParts: string[] = [];
if (inst.meta.author) metaParts.push('by ' + escapeHtml(inst.meta.author));
if (inst.meta.version) metaParts.push('v' + escapeHtml(inst.meta.version));
const metaLine = metaParts.length > 0 ? '' + metaParts.join(' · ') + '' : '';
const descText = escapeHtml(inst.meta.desc || '');
scriptRow.innerHTML =
'' + displayName + '' +
'' +
'' + descText + (metaLine ? '
' + metaLine : '') + '
';
body.appendChild(scriptRow);
const cb = scriptRow.querySelector('input[type="checkbox"]') as HTMLInputElement;
const settingsContainer = document.createElement('div');
settingsContainer.className = 'kpc-us-settings';
body.appendChild(settingsContainer);
if (inst.enabled && inst.settings) {
renderScriptSettings(inst, settingsContainer);
}
cb.addEventListener('change', () => {
const { needsReload } = setScriptEnabled(inst.filename, cb.checked, _console);
settingsContainer.innerHTML = '';
if (cb.checked && inst.settings) {
renderScriptSettings(inst, settingsContainer);
}
if (needsReload) {
onSettingChanged('refresh');
}
});
}
});
}
function renderScriptSettings(inst: UserscriptInstance, container: HTMLElement): void {
if (!inst.settings) return;
for (const [, setting] of Object.entries(inst.settings)) {
const typeClass = setting.type === 'bool' ? 'bool' : setting.type === 'sel' ? 'sel' : setting.type === 'num' ? 'num' : setting.type === 'keybind' ? 'keybind' : '';
const row = document.createElement('div');
row.className = 'setting settName safety-0' + (typeClass ? ' ' + typeClass : '');
row.innerHTML =
'' + escapeHtml(setting.title) + '' +
(setting.desc ? '' + escapeHtml(setting.desc) + '
' : '');
switch (setting.type) {
case 'bool': {
const label = document.createElement('label');
label.className = 'switch';
label.innerHTML =
'' +
'';
row.appendChild(label);
const input = label.querySelector('input') as HTMLInputElement;
input.addEventListener('change', () => {
setting.value = input.checked;
if (typeof setting.changed === 'function') setting.changed(setting.value);
saveScriptSetting(inst);
});
break;
}
case 'num': {
const input = document.createElement('input');
input.type = 'number';
input.className = 'rb-input s-update sliderVal';
input.value = String(setting.value);
if (setting.min !== undefined) input.min = String(setting.min);
if (setting.max !== undefined) input.max = String(setting.max);
if (setting.step !== undefined) input.step = String(setting.step);
row.appendChild(input);
input.addEventListener('change', () => {
setting.value = parseFloat(input.value) || 0;
if (typeof setting.changed === 'function') setting.changed(setting.value);
saveScriptSetting(inst);
});
break;
}
case 'sel': {
const select = document.createElement('select');
select.className = 's-update inputGrey2';
if (setting.opts) {
for (const opt of setting.opts) {
const option = document.createElement('option');
option.value = String(opt);
option.textContent = String(opt);
if (String(opt) === String(setting.value)) option.selected = true;
select.appendChild(option);
}
}
row.appendChild(select);
select.addEventListener('change', () => {
setting.value = select.value;
if (typeof setting.changed === 'function') setting.changed(setting.value);
saveScriptSetting(inst);
});
break;
}
case 'color': {
const input = document.createElement('input');
input.type = 'color';
input.className = 'kpc-color-input';
input.value = String(setting.value) || '#ffffff';
row.appendChild(input);
input.addEventListener('input', () => {
setting.value = input.value;
if (typeof setting.changed === 'function') setting.changed(setting.value);
saveScriptSetting(inst);
});
break;
}
case 'keybind': {
const bind = setting.value as Keybind;
const keyEl = document.createElement('span');
keyEl.className = 'keyIcon kpc-keyIcon';
keyEl.textContent = keybindDisplayString(bind);
keyEl.addEventListener('click', () => {
openKeybindDialog(setting.title).then((newBind) => {
setting.value = newBind;
keyEl.textContent = keybindDisplayString(newBind);
if (typeof setting.changed === 'function') setting.changed(setting.value);
saveScriptSetting(inst);
});
});
row.appendChild(keyEl);
break;
}
}
container.appendChild(row);
}
}
function saveScriptSetting(inst: UserscriptInstance): void {
if (!inst.settings) return;
const prefs: Record = {};
for (const [k, s] of Object.entries(inst.settings)) {
prefs[k] = s.value;
}
ipcRenderer.invoke('userscripts-save-prefs', inst.filename, prefs);
}
// ── Hide menu popups (polling-based, safe per MutationObserver constraint) ──
let _hidePopupsInterval: ReturnType | null = null;
const HIDE_POPUPS_CSS =
'#leftTabsHolder > .youNewDiv:not(#battlepassAd), .webpush-container, ' +
'#homeStoreAd, #streamContainerNew, #bundlePop, #genericPop.claimPop, ' +
'#newsHolder, #streamContainer { display: none !important; }';
const HIDE_POPUPS_ELS = ['homeStoreAd', 'streamContainerNew'];
function startHidePopups(): void {
if (_hidePopupsInterval) return;
if (!document.getElementById('kpc-hideMenuPopups')) {
const style = document.createElement('style');
style.id = 'kpc-hideMenuPopups';
style.textContent = HIDE_POPUPS_CSS;
document.head.appendChild(style);
}
const w = window as any;
_hidePopupsInterval = setInterval(() => {
for (const id of HIDE_POPUPS_ELS) {
const el = document.getElementById(id);
if (el && el.style.display !== 'none') el.style.display = 'none';
}
const bundlePop = document.getElementById('bundlePop');
if (bundlePop && bundlePop.children.length > 0 && bundlePop.style.display !== 'none') {
if (typeof w.clearPops === 'function') w.clearPops();
}
const genericPop = document.getElementById('genericPop');
if (genericPop && genericPop.classList.contains('claimPop') && genericPop.style.display !== 'none') {
if (typeof w.clearPops === 'function') w.clearPops();
}
}, 1000);
}
function stopHidePopups(): void {
if (_hidePopupsInterval) { clearInterval(_hidePopupsInterval); _hidePopupsInterval = null; }
const style = document.getElementById('kpc-hideMenuPopups');
if (style) style.remove();
for (const id of HIDE_POPUPS_ELS) {
const el = document.getElementById(id);
if (el) el.style.display = '';
}
}
// ── Matchmaker IPC listener ──
ipcRenderer.on('matchmaker-find', (_e, mmConfig: MatchmakerConfig) => {
fetchGame(mmConfig, _console).catch((err) => _console.error('[KCC] Matchmaker error:', err));
});
// ── Wait for main process to signal page load, then poll for settings window ──
ipcRenderer.on('main_did-finish-load', () => {
_console.log('[KCC] did-finish-load received, waiting to hook settings...');
const isGamePage = window.location.pathname === '/' || window.location.pathname === '';
// ── Batch all config reads into a single IPC call ──
(window as any).closeClient = () => window.close();
Promise.all([
ipcRenderer.invoke('get-all-config', ['ui', 'userscripts', 'game', 'translator', 'keybinds', 'discord', 'advanced', 'performance']),
ipcRenderer.invoke('get-platform'),
ipcRenderer.invoke('get-version'),
]).then(([allConf, _platformInfo, currentVersion]: [any, any, string]) => {
const uiConf = allConf.ui;
const usConf = allConf.userscripts;
const gameConf = allConf.game;
const translatorConf = allConf.translator;
const discordConf = allConf.discord;
const advConf = allConf.advanced;
// ── Verbose logging toggle ──
_verboseLogging = advConf?.verboseLogging ?? false;
// ── Exit button + UI toggles ──
const showExit = uiConf ? (uiConf.showExitButton !== false) : true;
const showExitBtn = () => {
const btn = document.getElementById('clientExit');
if (btn) {
btn.style.display = showExit ? 'flex' : 'none';
return true;
}
return false;
};
if (!showExitBtn()) {
let exitAttempts = 0;
const exitPoll = setInterval(() => {
if (showExitBtn() || ++exitAttempts > 30) clearInterval(exitPoll);
}, 500);
}
if (uiConf?.deathscreenAnimation) setDeathAnimBlock(true);
if (uiConf?.hideMenuPopups) startHidePopups();
if (uiConf?.menuTimer ?? true) setMenuTimer(true);
// ── Double ping display ──
if (isGamePage && (uiConf?.doublePing ?? true)) {
initDoublePing();
}
// ── Show ping in player list ──
if (isGamePage && (gameConf?.showPing ?? true)) {
initShowPing();
}
// ── Raw input (Windows only — unadjustedMovement) ──
if (isGamePage && process.platform === 'win32' && (gameConf?.rawInput ?? true)) {
const origLock = HTMLCanvasElement.prototype.requestPointerLock;
HTMLCanvasElement.prototype.requestPointerLock = function (opts?: any) {
const promise = origLock.call(this, { ...opts, unadjustedMovement: true }) as any;
if (promise && typeof promise.catch === 'function') {
return promise.catch(() => origLock.call(this, opts));
}
return promise;
};
}
// ── Better chat + Chat history ──
if (isGamePage) {
initChat({
betterChat: gameConf?.betterChat ?? true,
chatHistorySize: gameConf?.chatHistorySize ?? 200,
}, _console);
}
// ── Competitive features ──
if (isGamePage && (gameConf?.hpEnemyCounter ?? true)) {
initHPCounter();
}
if (isGamePage) {
initRankProgress();
}
// ── CPU throttle state notifications ──
if (isGamePage) {
let inGame = false;
setInterval(() => {
const uiBase = document.getElementById('uiBase');
const nowInGame = !!uiBase && uiBase.className !== 'onMenu' && uiBase.className !== '';
if (nowInGame !== inGame) {
inGame = nowInGame;
ipcRenderer.send('throttle-state', inGame ? 'game' : 'menu');
}
}, 2000);
}
// ── Changelog popup ──
if (isGamePage && (uiConf?.showChangelog ?? true)) {
checkChangelog(currentVersion, uiConf?.lastSeenVersion || '');
}
// ── Battle Pass Claim All (game page only) ──
// Poll for .bpBotH element — injects button when BP window is visible
if (isGamePage) {
const getClaimable = () => Array.from(document.querySelectorAll('.bpClaimB')).filter(
(el: any) => el.offsetParent !== null && el.textContent?.trim() === 'Claim'
);
setInterval(() => {
const bar = document.querySelector('.bpBotH') as HTMLElement | null;
if (!bar || bar.offsetParent === null) return;
const existing = document.getElementById('claimAllBtn');
if (existing) {
// Update state on re-check (rewards may have become claimable)
const claimable = getClaimable();
if (claimable.length > 0) {
existing.textContent = 'Claim All';
existing.classList.remove('disabled');
} else {
existing.textContent = 'Nothing to Claim';
existing.classList.add('disabled');
}
return;
}
const claimable = getClaimable();
const btn = document.createElement('div');
btn.className = 'bpBtn skip';
btn.id = 'claimAllBtn';
btn.style.cssText = 'margin-left: 8px; cursor: pointer; background: #4CAF50;';
if (claimable.length > 0) {
btn.textContent = 'Claim All';
} else {
btn.textContent = 'Nothing to Claim';
btn.classList.add('disabled');
}
btn.addEventListener('click', async () => {
if (btn.classList.contains('disabled')) return;
(window as any).playSelect?.(0.1);
const items = getClaimable();
if (items.length === 0) return;
btn.textContent = 'Claiming...';
btn.classList.add('disabled');
for (const item of items) {
(item as HTMLElement).click();
await new Promise(r => setTimeout(r, 200));
}
const remaining = getClaimable();
btn.textContent = remaining.length > 0 ? 'Claim All' : 'Nothing to Claim';
btn.classList.toggle('disabled', remaining.length === 0);
});
bar.appendChild(btn);
}, 500);
}
// ── Initialize userscripts ──
const usEnabled = usConf ? usConf.enabled : true;
if (usEnabled) {
initUserscripts(_console).catch(err => _console.error('[KCC] Userscript init error:', err));
}
// ── Join as Spectator — auto-enable spectate on regular game join ──
if (isGamePage && gameConf?.joinAsSpectator) {
let attempts = 0;
const poll = setInterval(() => {
if (++attempts > 300) { clearInterval(poll); return; }
const uiBase = document.getElementById('uiBase');
if (!uiBase || uiBase.className === '') return;
if (uiBase.className === 'onMenu') {
const specBtn = document.querySelector('#spectButton input') as HTMLInputElement;
if (specBtn && !specBtn.checked) {
(window as any).setSpect(1);
}
clearInterval(poll);
} else {
clearInterval(poll);
}
}, 100);
}
// ── Initialize chat translator (game page only) ──
if (isGamePage) {
const mergedTl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf };
initTranslator(_console, mergedTl);
}
// ── Discord Rich Presence game state polling ──
if (isGamePage && discordConf?.enabled) {
let lastDetails = '';
let lastState = '';
let gameStartTimestamp = Math.floor(Date.now() / 1000);
function pollDiscordState(): void {
let details: string;
let state = '';
let startTimestamp: number | undefined = undefined;
const w = window as any;
const spectating = w.spectating;
let gameActivity: any = null;
if (typeof w.getGameActivity === 'function') {
try { gameActivity = w.getGameActivity(); } catch { /* game API unavailable */ }
}
if (spectating) {
details = 'Spectating';
if (gameActivity?.map) {
state = gameActivity.map;
}
} else {
const uiBase = document.getElementById('uiBase');
if (uiBase && uiBase.className === 'onMenu') {
details = 'In Menus';
} else {
if (gameActivity?.mode && gameActivity?.map) {
details = gameActivity.mode + ' on ' + gameActivity.map;
} else {
const mapInfo = document.getElementById('mapInfo');
details = mapInfo?.textContent || 'Playing Krunker';
}
if (gameActivity?.class?.name) {
state = gameActivity.class.name;
} else {
const classElem = document.getElementById('menuClassName');
if (classElem?.textContent) state = classElem.textContent;
}
startTimestamp = gameStartTimestamp;
}
}
if (details !== lastDetails || state !== lastState) {
if (startTimestamp && lastDetails !== details) {
gameStartTimestamp = Math.floor(Date.now() / 1000);
startTimestamp = gameStartTimestamp;
}
lastDetails = details;
lastState = state;
ipcRenderer.send('discord-update', {
details,
state: state || undefined,
startTimestamp,
largeImageKey: 'krunker',
largeImageText: 'Krunker Civilian Client',
});
}
}
pollDiscordState();
setInterval(pollDiscordState, 5000);
document.addEventListener('pointerlockchange', pollDiscordState);
}
// ── In-game Accounts quick-switch button ──
if (isGamePage) {
ipcRenderer.invoke('alt-list').then(() => {
const altBtn = document.createElement('div');
altBtn.id = 'kpcAltBtn';
altBtn.className = 'menuItem';
altBtn.setAttribute('onmouseenter', 'playTick()');
altBtn.innerHTML =
'people' +
'';
function showAltManager(): void {
const windowHolder = document.getElementById('windowHolder') as HTMLElement;
const menuWindow = document.getElementById('menuWindow') as HTMLElement;
const windowHeader = document.getElementById('windowHeader') as HTMLElement;
if (!windowHolder || !menuWindow || !windowHeader) return;
if (windowHolder.style.display !== 'none' && windowHeader.innerText === 'Alt Manager') {
windowHolder.style.display = 'none';
return;
}
windowHolder.className = 'popupWin';
windowHolder.style.display = 'block';
menuWindow.classList.value = 'dark';
menuWindow.style.cssText = 'width:800px;max-height:calc(100% - 330px);overflow-y:auto;top:50%;transform:translate(-50%,-50%);';
windowHeader.innerText = 'Alt Manager';
function renderAccountList(): void {
ipcRenderer.invoke('alt-list').then((accs: any[]) => {
let html =
'Alt Manager
' +
'
' +
'Add Account
' +
'';
if (!accs || accs.length === 0) {
html += '
No saved accounts
';
} else {
accs.forEach((acc, i) => {
html +=
'
' +
'
' + escapeHtml(acc.label) + '' +
'
' +
'login' +
'
' +
'
' +
'
' +
'delete' +
'
' +
'
';
});
}
html += '
';
menuWindow.innerHTML = html;
const addBtn = document.getElementById('kpcAltAddBtn');
if (addBtn) addBtn.addEventListener('click', showAddForm);
menuWindow.querySelectorAll('.kpc-alt-login').forEach((el) => {
el.addEventListener('click', () => {
const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10);
if (accs[idx]) {
windowHolder.style.display = 'none';
ipcRenderer.invoke('alt-get-credentials', idx).then((creds: { username: string; password: string } | null) => {
if (creds) switchToAccount(creds);
});
}
});
});
menuWindow.querySelectorAll('.kpc-alt-del').forEach((el) => {
el.addEventListener('click', () => {
const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10);
if (confirm('Delete account "' + (accs[idx]?.label || '') + '"?')) {
ipcRenderer.invoke('alt-remove', idx).then(() => renderAccountList());
}
});
});
});
}
function showAddForm(): void {
menuWindow.innerHTML =
'' +
'
Add Account
' +
'
' +
'
' +
'
' +
'
' +
'
Add Account
' +
'
Back
' +
'
' +
'
';
// Stop Krunker's global keydown handler from eating keystrokes in our inputs
menuWindow.querySelectorAll('input.accountInput').forEach((input) => {
input.addEventListener('keydown', (e) => e.stopPropagation());
});
document.getElementById('kpcAltBackBtn')!.addEventListener('click', renderAccountList);
document.getElementById('kpcAltSaveBtn')!.addEventListener('click', () => {
const label = (document.getElementById('kpcAltLabel') as HTMLInputElement).value.trim();
const user = (document.getElementById('kpcAltUser') as HTMLInputElement).value.trim();
const pass = (document.getElementById('kpcAltPass') as HTMLInputElement).value;
if (!label || !user || !pass) return;
ipcRenderer.invoke('alt-save', {
label,
username: user,
password: pass,
}).then(() => renderAccountList());
});
}
renderAccountList();
}
altBtn.addEventListener('click', (e) => {
e.stopPropagation();
(window as any).playSelect?.();
showAltManager();
});
function injectAltBtn(): boolean {
if (document.getElementById('kpcAltBtn')) return true;
const menuContainer = document.getElementById('menuItemContainer');
if (!menuContainer) return false;
const exitBtn = document.getElementById('clientExit');
if (exitBtn) {
menuContainer.insertBefore(altBtn, exitBtn);
} else {
menuContainer.appendChild(altBtn);
}
return true;
}
if (!injectAltBtn()) {
let attempts = 0;
const poll = setInterval(() => {
if (injectAltBtn() || ++attempts > 60) clearInterval(poll);
}, 500);
}
});
}
}).catch(() => {});
const pollInterval = setInterval(() => {
const w = window as any;
if (
Object.hasOwn(w, 'showWindow')
&& typeof w.showWindow === 'function'
&& Object.hasOwn(w, 'windows')
&& Array.isArray(w.windows)
&& w.windows.length >= 0
&& typeof w.windows[0] !== 'undefined'
&& typeof w.windows[0].changeTab === 'function'
) {
clearInterval(pollInterval);
_console.log('[KCC] Settings window found, hooking...');
hookSettings();
}
}, 500);
});
// ── Lightweight tab page init (skips game-only features) ──
ipcRenderer.on('main_did-finish-load-tab', () => {
_console.log('[KCC] Tab page loaded');
(window as any).closeClient = () => window.close();
});