diff --git a/src/main/client-ui.ts b/src/main/client-ui.ts index e1fd679..eb87f4b 100644 --- a/src/main/client-ui.ts +++ b/src/main/client-ui.ts @@ -679,5 +679,10 @@ export const HP_COUNTER_CSS = ` } `; +// ── Battle Pass Claim All CSS ── +export const BP_CLAIM_ALL_CSS = ` +#claimAllBtn.disabled { opacity: 0.4; pointer-events: none; } +`; + /** Pre-concatenated CSS for single-call injection (excludes HIDE_ADS_CSS which is separate) */ -export const ALL_CLIENT_CSS = `${CLIENT_SETTINGS_CSS}\n${MATCHMAKER_SETTINGS_CSS}\n${TRANSLATOR_CSS}\n${ALT_MANAGER_CSS}\n${HP_COUNTER_CSS}`; +export const ALL_CLIENT_CSS = `${CLIENT_SETTINGS_CSS}\n${MATCHMAKER_SETTINGS_CSS}\n${TRANSLATOR_CSS}\n${ALT_MANAGER_CSS}\n${HP_COUNTER_CSS}\n${BP_CLAIM_ALL_CSS}`; diff --git a/src/main/config.ts b/src/main/config.ts index 769ece6..8e5f30f 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -77,6 +77,7 @@ export interface AppConfig { deathscreenAnimation: boolean; hideMenuPopups: boolean; cleanerMenu: boolean; + menuTimer: boolean; doublePing: boolean; cssTheme: string; loadingTheme: string; @@ -186,6 +187,7 @@ export const config = new Store({ deathscreenAnimation: true, hideMenuPopups: false, cleanerMenu: false, + menuTimer: true, doublePing: true, cssTheme: 'disabled', loadingTheme: 'disabled', diff --git a/src/preload/index.ts b/src/preload/index.ts index 24579cc..427f277 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -4,7 +4,7 @@ import type { MatchmakerConfig } from './matchmaker'; import { initUserscripts, getInstances, setScriptEnabled } from './userscripts'; import type { UserscriptInstance } from './userscripts'; import { initTranslator, updateTranslatorConfig } from './translator'; -import { setDeathAnimBlock, setCleanerMenu, escapeHtml } from './utils'; +import { setDeathAnimBlock, setCleanerMenu, setMenuTimer, escapeHtml } from './utils'; import { initChat, setBetterChat, setChatHistorySize } from './chat'; import { initHPCounter, destroyHPCounter } from './competitive'; import { checkChangelog } from './changelog'; @@ -615,6 +615,13 @@ function buildGeneralSection( onChange: (v) => { ui.cleanerMenu = v; saveUI(); setCleanerMenu(v); }, })); + 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)', @@ -656,6 +663,7 @@ function buildGeneralSection( })); 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) => { @@ -1635,6 +1643,7 @@ ipcRenderer.on('main_did-finish-load', () => { if (uiConf?.deathscreenAnimation) setDeathAnimBlock(true); if (uiConf?.hideMenuPopups) startHidePopups(); if (uiConf?.cleanerMenu) setCleanerMenu(true); + if (uiConf?.menuTimer ?? true) setMenuTimer(true); // ── Double ping display ── if (isGamePage && (uiConf?.doublePing ?? true)) { @@ -1689,6 +1698,58 @@ ipcRenderer.on('main_did-finish-load', () => { 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) { diff --git a/src/preload/utils.ts b/src/preload/utils.ts index 6d98d88..55bce0b 100644 --- a/src/preload/utils.ts +++ b/src/preload/utils.ts @@ -101,6 +101,57 @@ const CLEANER_MENU_CSS = ` .headerBarR { right: -23px !important; } `; +// ── Menu Timer ── +// Shows the native spectate/game timer prominently on the menu screen. +// CSS approach from crankshaft/glorp. + +const MENU_TIMER_ID = 'kpc-menuTimer'; +const MENU_TIMER_CSS = ` +#uiBase.onMenu #spectateUI { display: block !important; } +#uiBase.onCompMenu.onMenu #specTimer, +#uiBase.onMenu #specGMessage, +#uiBase.onMenu #spec1, +#uiBase.onMenu #specGameInfo, +#uiBase.onMenu #spec0, +#uiBase.onMenu #specControlHolder, +#uiBase.onMenu #specNames { display: none !important; } +#uiBase.onMenu #spectateHUD { + box-sizing: border-box; display: flex !important; justify-content: center; + height: 0.5rem; white-space: nowrap; width: max-content; + position: fixed; top: calc(50% + 140px); +} +#uiBase.onMenu #spectateHUD #specGMessage { top: 0; } +#uiBase.onMenu #spectateUI > #spectateHUD { z-index: 1; transform: unset; } +#uiBase.onMenu .spectateInfo { + position: fixed; top: calc(50% + 80px); left: 50%; transform: translate(-50%, -50%); +} +#uiBase.onMenu #spectateUI div .spectateInfo #specTimer { + background-color: transparent; padding: 25px; font-size: 42px; border-radius: 0.5em; +} +#uiBase.onMenu #specKPDContr { display: none; } +#uiBase.onMenu #spectateUI div#specStats { + position: absolute; top: calc(50% + 13em); left: 50%; transform: translateX(-50%); z-index: 1; +} +#uiBase.onMenu #spectateUI div#specStats:before { + content: "Spectating"; position: absolute; bottom: 100%; left: 50%; + transform: translateX(-50%); font-size: 1.2em; padding-bottom: 0.5em; +} +`; + +export function setMenuTimer(enabled: boolean): void { + let el = document.getElementById(MENU_TIMER_ID); + if (enabled) { + if (!el) { + el = document.createElement('style'); + el.id = MENU_TIMER_ID; + el.textContent = MENU_TIMER_CSS; + document.head.appendChild(el); + } + } else if (el) { + el.remove(); + } +} + export function setCleanerMenu(enabled: boolean): void { let el = document.getElementById(CLEANER_MENU_ID); if (enabled) {