87ddf1499d
Cross-platform Krunker.io game client forked from Krunker Police Client with all KPD/moderator features stripped: no KPD auth, OBS recording, evidence uploads, yt-dlp, bytenode, or code obfuscation. Retained: unlimited FPS (custom Electron 42), ad blocking, resource swapper, matchmaker, userscripts, chat translator, Discord RPC, alt account manager, configurable keybinds, and advanced Chromium flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1653 lines
65 KiB
TypeScript
1653 lines
65 KiB
TypeScript
import { ipcRenderer } from 'electron';
|
|
import { fetchGame, MATCHMAKER_GAMEMODES, MATCHMAKER_REGIONS, MATCHMAKER_REGION_NAMES } from './matchmaker';
|
|
import type { MatchmakerConfig } from './matchmaker';
|
|
import { initUserscripts, getInstances, setScriptEnabled } from './userscripts';
|
|
import type { UserscriptInstance, UserscriptSetting } from './userscripts';
|
|
import { initTranslator, updateTranslatorConfig } from './translator';
|
|
import { setDeathAnimBlock, escapeHtml } from './utils';
|
|
|
|
|
|
// ── Save console methods before Krunker overwrites them ──
|
|
// Wrapped to forward errors/warnings always, and logs when verbose is enabled
|
|
let _verboseLogging = false;
|
|
|
|
const _console = {
|
|
log: (...args: unknown[]) => {
|
|
console.log(...args);
|
|
if (_verboseLogging) ipcRenderer.send('verbose-log', 'log', ...args);
|
|
},
|
|
warn: (...args: unknown[]) => {
|
|
console.warn(...args);
|
|
ipcRenderer.send('verbose-log', 'warn', ...args);
|
|
},
|
|
error: (...args: unknown[]) => {
|
|
console.error(...args);
|
|
ipcRenderer.send('verbose-log', 'error', ...args);
|
|
},
|
|
};
|
|
|
|
_console.log('[KCC] Preload script loaded');
|
|
|
|
// ── Krunker-native settings styling constants (from Crankshaft) ──
|
|
const SAFETY_SVG = '<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M12 12.5ZM3.425 20.5Q2.9 20.5 2.65 20.05Q2.4 19.6 2.65 19.15L11.2 4.35Q11.475 3.9 12 3.9Q12.525 3.9 12.8 4.35L21.35 19.15Q21.6 19.6 21.35 20.05Q21.1 20.5 20.575 20.5ZM12 10.2Q11.675 10.2 11.463 10.412Q11.25 10.625 11.25 10.95V14.45Q11.25 14.75 11.463 14.975Q11.675 15.2 12 15.2Q12.325 15.2 12.538 14.975Q12.75 14.75 12.75 14.45V10.95Q12.75 10.625 12.538 10.412Q12.325 10.2 12 10.2ZM12 17.8Q12.35 17.8 12.575 17.575Q12.8 17.35 12.8 17Q12.8 16.65 12.575 16.425Q12.35 16.2 12 16.2Q11.65 16.2 11.425 16.425Q11.2 16.65 11.2 17Q11.2 17.35 11.425 17.575Q11.65 17.8 12 17.8ZM4.45 19H19.55L12 6Z"/></svg>';
|
|
const REFRESH_SVG = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M12 6v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V4c-4.42 0-8 3.58-8 8 0 1.04.2 2.04.57 2.95.27.67 1.13.85 1.64.34.27-.27.38-.68.23-1.04C6.15 13.56 6 12.79 6 12c0-3.31 2.69-6 6-6zm5.79 2.71c-.27.27-.38.69-.23 1.04.28.7.44 1.46.44 2.25 0 3.31-2.69 6-6 6v-1.79c0-.45-.54-.67-.85-.35l-2.79 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.31.31.85.09.85-.35V20c4.42 0 8-3.58 8-8 0-1.04-.2-2.04-.57-2.95-.27-.67-1.13-.85-1.64-.34z"/></svg>';
|
|
const SAFETY_DESCS = [
|
|
'This setting is safe/standard',
|
|
'Proceed with caution',
|
|
'This setting is not recommended',
|
|
'This setting is experimental',
|
|
'This setting is experimental and unstable. Use at your own risk.',
|
|
];
|
|
|
|
const enum RefreshLevel { none, refresh, restart }
|
|
let refreshLevel: number = RefreshLevel.none;
|
|
let refreshPopupEl: HTMLElement | null = null;
|
|
|
|
function safetyIcon(safety: string): string {
|
|
return '<span class="desc-icon" title="' + safety + '">' + SAFETY_SVG + '</span>';
|
|
}
|
|
|
|
function refreshIcon(mode: 'instant' | 'refresh-icon'): string {
|
|
return '<span class="desc-icon ' + mode + '" title="' + (mode === 'instant' ? 'Applies instantly! (No refresh of page required)' : 'Refresh page to see changes') + '">' + REFRESH_SVG + '</span>';
|
|
}
|
|
|
|
function restartIcon(): string {
|
|
return '<span class="desc-icon restart-icon" title="Requires client restart">' + SAFETY_SVG + '</span>';
|
|
}
|
|
|
|
function settingIcon(safety: number, instant?: boolean, refreshOnly?: boolean, restart?: boolean): string {
|
|
if (safety > 0) return safetyIcon(SAFETY_DESCS[safety]);
|
|
if (instant) return refreshIcon('instant');
|
|
if (refreshOnly) return refreshIcon('refresh-icon');
|
|
if (restart) return restartIcon();
|
|
return '';
|
|
}
|
|
|
|
function onSettingChanged(level: 'refresh' | 'restart'): void {
|
|
const newLevel = level === 'restart' ? RefreshLevel.restart : RefreshLevel.refresh;
|
|
if (newLevel > refreshLevel) refreshLevel = newLevel;
|
|
updateRefreshNotification();
|
|
}
|
|
|
|
function updateRefreshNotification(): void {
|
|
if (refreshLevel === RefreshLevel.none) {
|
|
if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; }
|
|
return;
|
|
}
|
|
if (refreshPopupEl) { try { refreshPopupEl.remove(); } catch (_e) { /* noop */ } }
|
|
refreshPopupEl = document.createElement('div');
|
|
refreshPopupEl.className = 'kpc-holder-update refresh-popup';
|
|
if (refreshLevel === RefreshLevel.restart) {
|
|
refreshPopupEl.innerHTML = '<span class="restart-msg">Restart client fully to see changes</span>';
|
|
} else {
|
|
refreshPopupEl.innerHTML = '<span class="reload-msg">' + refreshIcon('refresh-icon') + 'Reload page with <code>F5</code> or <code>CTRL + R</code> to see changes</span>';
|
|
}
|
|
document.body.appendChild(refreshPopupEl);
|
|
}
|
|
|
|
// ── Tell Krunker this is a client (enables "Client" settings tab) ──
|
|
(window as any).OffCliV = true;
|
|
|
|
// ── IPC bridge exposed as window.kpc ──
|
|
(window as any).kpc = {
|
|
platform: {
|
|
getInfo: () => ipcRenderer.invoke('get-platform'),
|
|
},
|
|
config: {
|
|
get: (key: string) => ipcRenderer.invoke('get-config', key),
|
|
getAll: (keys: string[]) => ipcRenderer.invoke('get-all-config', keys),
|
|
set: (key: string, value: unknown) => ipcRenderer.invoke('set-config', key, value),
|
|
},
|
|
window: {
|
|
minimize: () => ipcRenderer.invoke('window-minimize'),
|
|
maximize: () => ipcRenderer.invoke('window-maximize'),
|
|
close: () => ipcRenderer.invoke('window-close'),
|
|
isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
|
},
|
|
dev: {
|
|
toggleDevTools: () => ipcRenderer.invoke('toggle-devtools'),
|
|
},
|
|
swapper: {
|
|
openFolder: () => ipcRenderer.invoke('open-swap-folder'),
|
|
getPath: () => ipcRenderer.invoke('get-swap-dir'),
|
|
},
|
|
userscripts: {
|
|
openFolder: () => ipcRenderer.invoke('userscripts-open-folder'),
|
|
getPath: () => ipcRenderer.invoke('userscripts-get-dir'),
|
|
},
|
|
};
|
|
|
|
// ── Client settings tab in Krunker's settings ──
|
|
|
|
function hasOwn(obj: any, key: string): boolean {
|
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
}
|
|
|
|
// ── Keybind types + helpers ──
|
|
interface Keybind { key: string; ctrl: boolean; shift: boolean; alt: boolean; }
|
|
|
|
function keybindDisplayString(bind: Keybind): string {
|
|
return (bind.shift ? 'Shift+' : '') + (bind.ctrl ? 'Ctrl+' : '') + (bind.alt ? 'Alt+' : '') + bind.key.toUpperCase();
|
|
}
|
|
|
|
// ── Keybind capture dialog (Crankshaft-style) ──
|
|
let capturingKeybind: { resolve: (bind: Keybind) => void } | null = null;
|
|
|
|
const kbOverlay = document.createElement('div');
|
|
kbOverlay.className = 'kpc-keybind-overlay';
|
|
const kbDialog = document.createElement('div');
|
|
kbDialog.className = 'kpc-keybind-dialog';
|
|
const kbTitle = document.createElement('div');
|
|
kbTitle.className = 'kpc-keybind-dialog-title';
|
|
const kbSub = document.createElement('div');
|
|
kbSub.className = 'kpc-keybind-dialog-sub';
|
|
kbSub.innerHTML = 'Press any key. Press <code>Shift+Escape</code> to cancel.';
|
|
const kbModifiers = document.createElement('div');
|
|
kbModifiers.className = 'kpc-keybind-dialog-modifiers';
|
|
const kbShift = document.createElement('div');
|
|
kbShift.className = 'kpc-keybind-modifier';
|
|
kbShift.textContent = 'Shift';
|
|
const kbCtrl = document.createElement('div');
|
|
kbCtrl.className = 'kpc-keybind-modifier';
|
|
kbCtrl.textContent = 'Control';
|
|
const kbAlt = document.createElement('div');
|
|
kbAlt.className = 'kpc-keybind-modifier';
|
|
kbAlt.textContent = 'Alt';
|
|
const kbCancel = document.createElement('div');
|
|
kbCancel.className = 'kpc-keybind-dialog-cancel';
|
|
kbCancel.textContent = 'Cancel';
|
|
kbCancel.addEventListener('click', dismissKeybindDialog);
|
|
|
|
kbModifiers.appendChild(kbShift);
|
|
kbModifiers.appendChild(kbCtrl);
|
|
kbModifiers.appendChild(kbAlt);
|
|
kbDialog.appendChild(kbCancel);
|
|
kbDialog.appendChild(kbTitle);
|
|
kbDialog.appendChild(kbSub);
|
|
kbDialog.appendChild(kbModifiers);
|
|
kbOverlay.appendChild(kbDialog);
|
|
|
|
function dismissKeybindDialog(): void {
|
|
kbShift.classList.remove('active');
|
|
kbCtrl.classList.remove('active');
|
|
kbAlt.classList.remove('active');
|
|
document.removeEventListener('keydown', kbKeydownHandler, true);
|
|
document.removeEventListener('keyup', kbKeyupHandler, true);
|
|
if (kbOverlay.parentNode) kbOverlay.remove();
|
|
capturingKeybind = null;
|
|
}
|
|
|
|
function kbKeydownHandler(event: KeyboardEvent): void {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
if (event.key === 'Control') kbCtrl.classList.add('active');
|
|
else if (event.key === 'Shift') kbShift.classList.add('active');
|
|
else if (event.key === 'Alt') kbAlt.classList.add('active');
|
|
}
|
|
|
|
function kbKeyupHandler(event: KeyboardEvent): void {
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
if (!capturingKeybind) return;
|
|
|
|
if (event.key === 'Escape' && event.shiftKey) {
|
|
dismissKeybindDialog();
|
|
return;
|
|
}
|
|
|
|
// Modifier-only releases just clear indicators
|
|
if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt') {
|
|
const bind: Keybind = { key: event.key, ctrl: false, shift: false, alt: false };
|
|
capturingKeybind.resolve(bind);
|
|
dismissKeybindDialog();
|
|
return;
|
|
}
|
|
|
|
const bind: Keybind = {
|
|
key: event.key,
|
|
ctrl: event.ctrlKey,
|
|
shift: event.shiftKey,
|
|
alt: event.altKey,
|
|
};
|
|
capturingKeybind.resolve(bind);
|
|
dismissKeybindDialog();
|
|
}
|
|
|
|
function openKeybindDialog(title: string): Promise<Keybind> {
|
|
return new Promise((resolve) => {
|
|
capturingKeybind = { resolve };
|
|
kbTitle.textContent = 'Edit Keybind: ' + title;
|
|
kbShift.classList.remove('active');
|
|
kbCtrl.classList.remove('active');
|
|
kbAlt.classList.remove('active');
|
|
document.addEventListener('keydown', kbKeydownHandler, true);
|
|
document.addEventListener('keyup', kbKeyupHandler, true);
|
|
const uiBase = document.getElementById('uiBase');
|
|
if (uiBase) uiBase.appendChild(kbOverlay);
|
|
});
|
|
}
|
|
|
|
function createKeybindRow(label: string, desc: string, currentBind: Keybind, onBind: (bind: Keybind) => void, safety?: number, instant?: boolean): HTMLElement {
|
|
const s = safety || 0;
|
|
const row = document.createElement('div');
|
|
row.className = 'setting settName safety-' + s + ' keybind';
|
|
row.innerHTML =
|
|
settingIcon(s, instant) +
|
|
'<span class="setting-title">' + label + '</span>' +
|
|
'<span class="keyIcon kpc-keyIcon">' + keybindDisplayString(currentBind) + '</span>' +
|
|
'<div class="setting-desc-new">' + desc + '</div>';
|
|
const keyEl = row.querySelector('.kpc-keyIcon') as HTMLElement;
|
|
keyEl.addEventListener('click', () => {
|
|
openKeybindDialog(label).then((newBind) => {
|
|
keyEl.textContent = keybindDisplayString(newBind);
|
|
onBind(newBind);
|
|
});
|
|
});
|
|
return row;
|
|
}
|
|
|
|
function createToggleRow(opts: {
|
|
label: string;
|
|
desc: string;
|
|
checked: boolean;
|
|
onChange: (checked: boolean) => void;
|
|
restart?: boolean;
|
|
disabled?: boolean;
|
|
safety?: number;
|
|
instant?: boolean;
|
|
refreshOnly?: boolean;
|
|
}): HTMLElement {
|
|
const s = opts.safety || 0;
|
|
const row = document.createElement('div');
|
|
row.className = 'setting settName safety-' + s + ' bool';
|
|
row.innerHTML =
|
|
settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
|
|
'<span class="setting-title">' + opts.label + '</span>' +
|
|
'<label class="switch">' +
|
|
'<input type="checkbox" class="s-update"' + (opts.checked ? ' checked' : '') + (opts.disabled ? ' disabled' : '') + '>' +
|
|
'<div class="slider round"></div>' +
|
|
'</label>' +
|
|
'<div class="setting-desc-new">' + opts.desc + '</div>';
|
|
if (!opts.disabled) {
|
|
const cb = row.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
cb.addEventListener('change', () => {
|
|
opts.onChange(cb.checked);
|
|
if (opts.restart) onSettingChanged('restart');
|
|
else if (opts.refreshOnly) onSettingChanged('refresh');
|
|
});
|
|
}
|
|
return row;
|
|
}
|
|
|
|
function createSelectRow(opts: {
|
|
label: string;
|
|
desc: string;
|
|
options: Array<{ value: string; label: string }>;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
restart?: boolean;
|
|
safety?: number;
|
|
instant?: boolean;
|
|
refreshOnly?: boolean;
|
|
}): HTMLElement {
|
|
const s = opts.safety || 0;
|
|
const row = document.createElement('div');
|
|
row.className = 'setting settName safety-' + s + ' sel';
|
|
row.innerHTML =
|
|
settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
|
|
'<span class="setting-title">' + opts.label + '</span>' +
|
|
'<div class="setting-desc-new">' + opts.desc + '</div>';
|
|
const select = document.createElement('select');
|
|
select.className = 's-update inputGrey2';
|
|
for (const o of opts.options) {
|
|
const option = document.createElement('option');
|
|
option.value = o.value;
|
|
option.textContent = o.label;
|
|
if (o.value === opts.value) option.selected = true;
|
|
select.appendChild(option);
|
|
}
|
|
select.addEventListener('change', () => {
|
|
opts.onChange(select.value);
|
|
if (opts.restart) onSettingChanged('restart');
|
|
else if (opts.refreshOnly) onSettingChanged('refresh');
|
|
});
|
|
row.appendChild(select);
|
|
return row;
|
|
}
|
|
|
|
function createNumberRow(opts: {
|
|
label: string;
|
|
desc: string;
|
|
min: number;
|
|
max: number;
|
|
value: number;
|
|
onChange: (value: number) => void;
|
|
safety?: number;
|
|
restart?: boolean;
|
|
instant?: boolean;
|
|
refreshOnly?: boolean;
|
|
}): HTMLElement {
|
|
const s = opts.safety || 0;
|
|
const row = document.createElement('div');
|
|
row.className = 'setting settName safety-' + s + ' num';
|
|
row.innerHTML =
|
|
settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
|
|
'<span class="setting-title">' + opts.label + '</span>' +
|
|
'<span class="setting-input-wrapper">' +
|
|
'<div class="slidecontainer"><input type="range" class="sliderM s-update-secondary" min="' + opts.min + '" max="' + opts.max + '" value="' + opts.value + '"></div>' +
|
|
'<input type="number" class="rb-input s-update sliderVal" min="' + opts.min + '" max="' + opts.max + '" value="' + opts.value + '">' +
|
|
'</span>' +
|
|
'<div class="setting-desc-new">' + opts.desc + '</div>';
|
|
const rangeInput = row.querySelector('input[type="range"]') as HTMLInputElement;
|
|
const numInput = row.querySelector('input[type="number"]') as HTMLInputElement;
|
|
rangeInput.addEventListener('input', () => {
|
|
numInput.value = rangeInput.value;
|
|
});
|
|
rangeInput.addEventListener('change', () => {
|
|
const v = Math.max(opts.min, Math.min(opts.max, parseInt(rangeInput.value) || 0));
|
|
rangeInput.value = String(v);
|
|
numInput.value = String(v);
|
|
opts.onChange(v);
|
|
if (opts.restart) onSettingChanged('restart');
|
|
else if (opts.refreshOnly) onSettingChanged('refresh');
|
|
});
|
|
numInput.addEventListener('change', () => {
|
|
const v = Math.max(opts.min, Math.min(opts.max, parseInt(numInput.value) || 0));
|
|
numInput.value = String(v);
|
|
rangeInput.value = String(v);
|
|
opts.onChange(v);
|
|
if (opts.restart) onSettingChanged('restart');
|
|
else if (opts.refreshOnly) onSettingChanged('refresh');
|
|
});
|
|
return row;
|
|
}
|
|
|
|
function createCheckboxGrid(opts: {
|
|
header: string;
|
|
items: Array<{ value: string; label: string }>;
|
|
selected: string[];
|
|
onChange: (selected: string[]) => void;
|
|
}): HTMLElement {
|
|
const row = document.createElement('div');
|
|
row.className = 'setting settName safety-0 multisel';
|
|
row.innerHTML = '<span class="setting-title">' + opts.header + '</span>';
|
|
const grid = document.createElement('div');
|
|
grid.className = 'kpc-multisel-parent';
|
|
for (const item of opts.items) {
|
|
const label = document.createElement('label');
|
|
label.className = 'hostOpt';
|
|
label.innerHTML =
|
|
'<span class="optName">' + item.label + '</span>' +
|
|
'<input type="checkbox"' + (opts.selected.includes(item.value) ? ' checked' : '') + '>' +
|
|
'<div class="optCheck"></div>';
|
|
const cb = label.querySelector('input') as HTMLInputElement;
|
|
cb.addEventListener('change', () => {
|
|
if (cb.checked) {
|
|
if (!opts.selected.includes(item.value)) opts.selected.push(item.value);
|
|
} else {
|
|
const idx = opts.selected.indexOf(item.value);
|
|
if (idx >= 0) opts.selected.splice(idx, 1);
|
|
}
|
|
opts.onChange(opts.selected);
|
|
});
|
|
grid.appendChild(label);
|
|
}
|
|
row.appendChild(grid);
|
|
return row;
|
|
}
|
|
|
|
function hookSettings(): void {
|
|
const w = window as any;
|
|
const settingsWindow = w.windows[0];
|
|
let selectedTab: number = settingsWindow.tabIndex;
|
|
|
|
function isClientTab(): boolean {
|
|
const tabs = settingsWindow.tabs[settingsWindow.settingType];
|
|
return tabs && selectedTab === tabs.length - 1;
|
|
}
|
|
|
|
function safeRender(): void {
|
|
if (isClientTab()) renderSettings();
|
|
}
|
|
|
|
const origShowWindow = w.showWindow.bind(w);
|
|
const origChangeTab = settingsWindow.changeTab.bind(settingsWindow);
|
|
const origSearchList = settingsWindow.searchList.bind(settingsWindow);
|
|
|
|
w.showWindow = (...args: unknown[]) => {
|
|
const result = origShowWindow(...args);
|
|
if (args[0] === 1) {
|
|
if (settingsWindow.settingType === 'basic') {
|
|
settingsWindow.toggleType({ checked: true });
|
|
}
|
|
const advSlider = document.querySelector('.advancedSwitch input#typeBtn') as HTMLInputElement | null;
|
|
if (advSlider) {
|
|
advSlider.disabled = true;
|
|
if (advSlider.nextElementSibling) {
|
|
advSlider.nextElementSibling.setAttribute('title', 'Client auto-enables advanced settings mode');
|
|
}
|
|
}
|
|
|
|
const searchInput = document.getElementById('settSearch') as HTMLInputElement | null;
|
|
const searchQuery = searchInput?.value?.trim() ?? '';
|
|
if (searchQuery.length > 0) renderSettings(searchQuery);
|
|
else if (isClientTab()) renderSettings();
|
|
}
|
|
return result;
|
|
};
|
|
|
|
settingsWindow.changeTab = (...args: unknown[]) => {
|
|
const result = origChangeTab(...args);
|
|
selectedTab = settingsWindow.tabIndex;
|
|
safeRender();
|
|
return result;
|
|
};
|
|
|
|
settingsWindow.searchList = (...args: unknown[]) => {
|
|
const result = origSearchList(...args);
|
|
const searchInput = document.getElementById('settSearch') as HTMLInputElement | null;
|
|
const query = searchInput?.value?.trim() ?? '';
|
|
if (query.length > 0) {
|
|
renderSettings(query);
|
|
} else {
|
|
const existing = document.querySelector('#settHolder .kpc-settings');
|
|
if (existing && !isClientTab()) existing.remove();
|
|
else if (isClientTab()) renderSettings();
|
|
}
|
|
return result;
|
|
};
|
|
|
|
safeRender();
|
|
}
|
|
|
|
function createSection(title: string, collapsed?: boolean): { section: HTMLElement; body: HTMLElement } {
|
|
const section = document.createElement('div');
|
|
const header = document.createElement('div');
|
|
header.className = 'setHed';
|
|
header.innerHTML = '<span class="material-icons plusOrMinus">' + (collapsed ? 'keyboard_arrow_right' : 'keyboard_arrow_down') + '</span>' + title;
|
|
const body = document.createElement('div');
|
|
body.className = 'setBodH' + (collapsed ? ' setting-category-collapsed' : '');
|
|
header.addEventListener('click', () => {
|
|
const isCollapsed = body.classList.toggle('setting-category-collapsed');
|
|
const arrow = header.querySelector('.plusOrMinus');
|
|
if (arrow) arrow.textContent = isCollapsed ? 'keyboard_arrow_right' : 'keyboard_arrow_down';
|
|
});
|
|
section.appendChild(header);
|
|
section.appendChild(body);
|
|
return { section, body };
|
|
}
|
|
|
|
// ── Settings section builders ──
|
|
|
|
interface SettingsBag {
|
|
binds: Record<string, Keybind>;
|
|
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: 'New Window' }, { value: 'Same Window', label: 'Same Window' }],
|
|
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); },
|
|
}));
|
|
|
|
if (ui.deathscreenAnimation) setDeathAnimBlock(true);
|
|
if (ui.hideMenuPopups) startHidePopups();
|
|
|
|
body.appendChild(createKeybindRow('Pause Chat', 'Freeze chat auto-scroll to read history (default F10)', bag.binds.pauseChat, (b) => {
|
|
bag.binds.pauseChat = b;
|
|
bag.saveBinds();
|
|
}, undefined, true));
|
|
|
|
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): void {
|
|
const swapEnabled = swapperConf ? swapperConf.enabled : true;
|
|
|
|
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 =
|
|
'<span class="setting-title">Swapper Folder</span>' +
|
|
'<div class="setting-desc-new">Place replacement assets here (textures/, sound/, models/)</div>';
|
|
const swapFolderBtn = document.createElement('div');
|
|
swapFolderBtn.className = 'settingsBtn';
|
|
swapFolderBtn.title = 'Open Folder';
|
|
swapFolderBtn.innerHTML = '<span class="material-icons">folder</span> Swapper';
|
|
swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
|
|
folderRow.appendChild(swapFolderBtn);
|
|
body.appendChild(folderRow);
|
|
}
|
|
|
|
function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
|
|
const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true };
|
|
|
|
function saveMM(): void {
|
|
ipcRenderer.invoke('set-config', 'matchmaker', mm);
|
|
}
|
|
|
|
body.appendChild(createToggleRow({
|
|
label: 'Custom Matchmaker',
|
|
desc: 'Press F6 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(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_GAMEMODES.map(gm => ({ value: gm, label: gm })),
|
|
selected: mm.gamemodes,
|
|
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 encodeCredential(decoded: string): string {
|
|
const key = decoded.length;
|
|
return encodeURIComponent(
|
|
decoded.split('').map(c => String.fromCharCode(c.charCodeAt(0) + key)).join('')
|
|
);
|
|
}
|
|
|
|
function decodeCredential(encoded: string): string {
|
|
const str = decodeURIComponent(encoded);
|
|
const key = str.length;
|
|
return str.split('').map(c => String.fromCharCode(c.charCodeAt(0) - key)).join('');
|
|
}
|
|
|
|
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 = decodeCredential(account.username);
|
|
passInput.value = decodeCredential(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 =
|
|
'<span class="setting-title">Add Account</span>' +
|
|
'<button class="kpc-acc-save" style="margin-left:auto;padding:4px 14px;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit;background:var(--kpc-accent);color:#fff;">+ Add</button>' +
|
|
'<div class="setting-desc-new">Save a Krunker account for quick switching</div>';
|
|
body.appendChild(addBtn);
|
|
|
|
const form = document.createElement('div');
|
|
form.className = 'kpc-acc-form';
|
|
form.style.display = 'none';
|
|
form.innerHTML =
|
|
'<input type="text" placeholder="Label (e.g. Main, Alt1)" class="kpc-acc-label">' +
|
|
'<input type="text" placeholder="Krunker Username" class="kpc-acc-user">' +
|
|
'<input type="password" placeholder="Krunker Password" class="kpc-acc-pass">' +
|
|
'<div class="kpc-acc-form-buttons">' +
|
|
'<button class="kpc-acc-save">Save</button>' +
|
|
'<button class="kpc-acc-cancel">Cancel</button>' +
|
|
'</div>';
|
|
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;
|
|
|
|
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 = '<div class="kpc-acc-empty">No saved accounts</div>';
|
|
return;
|
|
}
|
|
accounts.forEach((acc, i) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'kpc-acc-item';
|
|
row.innerHTML =
|
|
'<div class="kpc-acc-item-info">' +
|
|
'<span class="kpc-acc-item-label">' + escapeHtml(acc.label) + '</span>' +
|
|
'</div>' +
|
|
'<div class="kpc-acc-item-actions">' +
|
|
'<button class="kpc-acc-switch">Switch</button>' +
|
|
'<button class="kpc-acc-delete">Delete</button>' +
|
|
'</div>';
|
|
row.querySelector('.kpc-acc-switch')!.addEventListener('click', () => switchToAccount(acc));
|
|
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: encodeCredential(user),
|
|
password: encodeCredential(pass),
|
|
};
|
|
ipcRenderer.invoke('alt-save', newAcc).then(() => {
|
|
accounts.push(newAcc);
|
|
labelIn.value = '';
|
|
userIn.value = '';
|
|
passIn.value = '';
|
|
form.style.display = 'none';
|
|
renderList();
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildTranslatorSection(body: HTMLElement, translatorConf: any): void {
|
|
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, isWindows: boolean,
|
|
): void {
|
|
const advDefaults = {
|
|
removeUselessFeatures: true,
|
|
gpuRasterizing: false,
|
|
helpfulFlags: true,
|
|
disableAccelerated2D: false,
|
|
increaseLimits: false,
|
|
lowLatency: false,
|
|
experimentalFlags: false,
|
|
angleBackend: 'default',
|
|
};
|
|
const adv = { ...advDefaults, ...advConf };
|
|
|
|
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: 'disableAccelerated2D', label: 'Disable Accelerated 2D Canvas', desc: 'Disables hardware-accelerated 2D canvas rendering', 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 accelerated 2D canvas', 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(); },
|
|
}));
|
|
}
|
|
}
|
|
|
|
// ── 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 tlSec = createSection('Translator');
|
|
container.appendChild(tlSec.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 },
|
|
pauseChat: { key: 'F10', 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);
|
|
buildMatchmakerSection(mmSec.body, mmConf, bag);
|
|
buildTranslatorSection(tlSec.body, translatorConf);
|
|
buildDiscordSection(discordSec.body, discordConf);
|
|
buildAccountsSection(accSec.body, allConf.accounts);
|
|
buildAdvancedSection(advSec.body, advConf, 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 =
|
|
'<span class="setting-title">Scripts Folder</span>' +
|
|
'<div class="setting-desc-new">Place .js userscript files here</div>';
|
|
const usFolderBtn = document.createElement('div');
|
|
usFolderBtn.className = 'settingsBtn';
|
|
usFolderBtn.title = 'Open Folder';
|
|
usFolderBtn.innerHTML = '<span class="material-icons">folder</span> 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 =
|
|
'<div class="setting-desc-new">No userscripts found. Place .js files in the scripts folder and reload.</div>';
|
|
body.appendChild(emptyRow);
|
|
return;
|
|
}
|
|
|
|
for (const inst of scriptInstances) {
|
|
const scriptRow = document.createElement('div');
|
|
scriptRow.className = 'setting settName safety-0 bool';
|
|
|
|
const displayName = inst.meta.name || inst.filename;
|
|
let metaParts: string[] = [];
|
|
if (inst.meta.author) metaParts.push('by ' + inst.meta.author);
|
|
if (inst.meta.version) metaParts.push('v' + inst.meta.version);
|
|
const metaLine = metaParts.length > 0 ? '<span class="kpc-us-meta">' + metaParts.join(' · ') + '</span>' : '';
|
|
const descText = inst.meta.desc || '';
|
|
|
|
scriptRow.innerHTML =
|
|
'<span class="setting-title">' + displayName + '</span>' +
|
|
'<label class="switch">' +
|
|
'<input type="checkbox" class="s-update"' + (inst.enabled ? ' checked' : '') + '>' +
|
|
'<div class="slider round"></div>' +
|
|
'</label>' +
|
|
'<div class="setting-desc-new">' + descText + (metaLine ? '<br>' + metaLine : '') + '</div>';
|
|
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 [key, 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 =
|
|
'<span class="setting-title">' + setting.title + '</span>' +
|
|
(setting.desc ? '<div class="setting-desc-new">' + setting.desc + '</div>' : '');
|
|
|
|
switch (setting.type) {
|
|
case 'bool': {
|
|
const label = document.createElement('label');
|
|
label.className = 'switch';
|
|
label.innerHTML =
|
|
'<input type="checkbox" class="s-update"' + (setting.value ? ' checked' : '') + '>' +
|
|
'<div class="slider round"></div>';
|
|
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, key);
|
|
});
|
|
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, key);
|
|
});
|
|
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, key);
|
|
});
|
|
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, key);
|
|
});
|
|
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, key);
|
|
});
|
|
});
|
|
row.appendChild(keyEl);
|
|
break;
|
|
}
|
|
}
|
|
|
|
container.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function saveScriptSetting(inst: UserscriptInstance, _key: string): void {
|
|
if (!inst.settings) return;
|
|
const prefs: Record<string, unknown> = {};
|
|
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<typeof setInterval> | 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));
|
|
});
|
|
|
|
// ── Chat pause ──
|
|
let chatPaused = false;
|
|
let chatSavedScrollTop = 0;
|
|
|
|
function onChatWheel(e: WheelEvent): void {
|
|
const chatList = document.getElementById('chatList');
|
|
if (!chatList) return;
|
|
chatSavedScrollTop = Math.max(0, Math.min(
|
|
chatSavedScrollTop + e.deltaY,
|
|
chatList.scrollHeight - chatList.clientHeight,
|
|
));
|
|
chatList.scrollTop = chatSavedScrollTop;
|
|
}
|
|
|
|
ipcRenderer.on('toggle-chat-pause', () => {
|
|
const chatList = document.getElementById('chatList');
|
|
if (!chatList) return;
|
|
|
|
chatPaused = !chatPaused;
|
|
|
|
if (chatPaused) {
|
|
chatSavedScrollTop = chatList.scrollTop;
|
|
chatList.classList.add('kpc-chat-paused');
|
|
chatList.style.overflow = 'hidden';
|
|
chatList.addEventListener('wheel', onChatWheel, { passive: true });
|
|
} else {
|
|
chatList.classList.remove('kpc-chat-paused');
|
|
chatList.style.overflow = '';
|
|
chatList.removeEventListener('wheel', onChatWheel);
|
|
chatList.scrollTop = chatList.scrollHeight;
|
|
}
|
|
});
|
|
|
|
// ── 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']),
|
|
ipcRenderer.invoke('get-platform'),
|
|
]).then(([allConf, platformInfo]: [any, any]) => {
|
|
const uiConf = allConf.ui;
|
|
const usConf = allConf.userscripts;
|
|
const gameConf = allConf.game;
|
|
const translatorConf = allConf.translator;
|
|
const discordConf = allConf.discord;
|
|
|
|
// ── 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();
|
|
|
|
// ── 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 = '';
|
|
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 {}
|
|
}
|
|
|
|
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((accounts: any[]) => {
|
|
if (!accounts || accounts.length === 0) return;
|
|
|
|
const altBtn = document.createElement('div');
|
|
altBtn.id = 'kpcAltBtn';
|
|
altBtn.className = 'menuItem';
|
|
altBtn.setAttribute('onmouseenter', 'playTick()');
|
|
altBtn.innerHTML =
|
|
'<span class="material-icons-outlined menBtnIcn" style="color:#4fc3f7">people</span>' +
|
|
'<div class="menuItemTitle" style="font-size:13px">Accounts</div>';
|
|
|
|
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 =
|
|
'<div style="font-size:30px;text-align:center;margin:3px;font-weight:700;color:#fff;">Alt Manager</div>' +
|
|
'<hr style="color:rgba(28,28,28,.5);">' +
|
|
'<div class="button buttonPI lgn" id="kpcAltAddBtn" style="text-align:center;width:98%;margin:3px;padding-top:5px;padding-bottom:13px;">Add Account</div>' +
|
|
'<div class="amHolder" style="display:flex;flex-direction:column;justify-content:center;">';
|
|
|
|
if (!accs || accs.length === 0) {
|
|
html += '<div style="color:rgba(255,255,255,0.4);text-align:center;padding:20px 0;font-size:18px;">No saved accounts</div>';
|
|
} else {
|
|
accs.forEach((acc, i) => {
|
|
html +=
|
|
'<div class="amAccName" style="display:flex;justify-content:flex-end;align-items:center;padding:4px 0;">' +
|
|
'<span style="margin-right:auto;color:#fff;font-size:18px;">' + escapeHtml(acc.label) + '</span>' +
|
|
'<div class="button buttonG lgn kpc-alt-login" data-idx="' + i + '" style="width:70px;margin-right:0;padding-top:3px;padding-bottom:15px;transform:scale(0.75);">' +
|
|
'<span class="material-icons" style="vertical-align:bottom;color:#fff;font-size:30px;margin-bottom:-1px;">login</span>' +
|
|
'</div>' +
|
|
'<div class="verticalSeparator" style="height:35px;background:rgba(28,28,28,.3);"></div>' +
|
|
'<div class="button buttonR lgn kpc-alt-del" data-idx="' + i + '" style="width:70px;margin-right:0;padding-top:3px;padding-bottom:15px;transform:scale(0.75);">' +
|
|
'<span class="material-icons" style="vertical-align:bottom;color:#fff;font-size:30px;margin-bottom:-1px;">delete</span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
}
|
|
html += '</div>';
|
|
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';
|
|
switchToAccount(accs[idx]);
|
|
}
|
|
});
|
|
});
|
|
|
|
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 =
|
|
'<div class="setBodH" style="padding:20px;">' +
|
|
'<div style="font-size:25px;text-align:center;margin-bottom:15px;color:#fff;">Add Account</div>' +
|
|
'<input class="accountInput" id="kpcAltLabel" type="text" placeholder="Label (e.g. Main, Alt1)" style="width:100%;margin-bottom:8px;">' +
|
|
'<input class="accountInput" id="kpcAltUser" type="text" placeholder="Krunker Username" style="width:100%;margin-bottom:8px;">' +
|
|
'<input class="accountInput" id="kpcAltPass" type="password" placeholder="Krunker Password" style="width:100%;margin-bottom:15px;">' +
|
|
'<div style="display:flex;gap:8px;">' +
|
|
'<div class="button buttonG lgn" id="kpcAltSaveBtn" style="flex:1;text-align:center;padding-top:5px;padding-bottom:13px;">Add Account</div>' +
|
|
'<div class="button buttonR lgn" id="kpcAltBackBtn" style="width:120px;text-align:center;padding-top:5px;padding-bottom:13px;">Back</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
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: encodeCredential(user),
|
|
password: encodeCredential(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 (
|
|
hasOwn(w, 'showWindow')
|
|
&& typeof w.showWindow === 'function'
|
|
&& 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);
|
|
});
|