Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13219f3a6c | |||
| 0ca6802475 | |||
| caf64447b8 | |||
| 68344c6465 | |||
| 19af04468e | |||
| c96c151851 | |||
| c86263291b | |||
| 50fae3b3df | |||
| 0e75affe0d | |||
| 021acf67a0 |
@@ -76,6 +76,15 @@ jobs:
|
|||||||
echo "=== Build output sizes ==="
|
echo "=== Build output sizes ==="
|
||||||
ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
|
ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
|
run: |
|
||||||
|
git fetch --unshallow 2>/dev/null || true
|
||||||
|
chmod +x scripts/generate-release-notes.sh
|
||||||
|
scripts/generate-release-notes.sh "${{ steps.version-check.outputs.TAG }}" > /tmp/release-notes.md
|
||||||
|
echo "--- Generated release notes ---"
|
||||||
|
cat /tmp/release-notes.md
|
||||||
|
|
||||||
- name: Create GitHub release and upload assets
|
- name: Create GitHub release and upload assets
|
||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
env:
|
env:
|
||||||
@@ -100,5 +109,5 @@ jobs:
|
|||||||
gh release create "${{ steps.version-check.outputs.TAG }}" \
|
gh release create "${{ steps.version-check.outputs.TAG }}" \
|
||||||
--repo "$GITHUB_REPOSITORY" \
|
--repo "$GITHUB_REPOSITORY" \
|
||||||
--title "${{ steps.version-check.outputs.TAG }}" \
|
--title "${{ steps.version-check.outputs.TAG }}" \
|
||||||
--notes "Automated build for ${{ steps.version-check.outputs.TAG }}" \
|
--notes-file /tmp/release-notes.md \
|
||||||
"${ASSETS[@]}"
|
"${ASSETS[@]}"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.6.0",
|
"version": "0.6.3",
|
||||||
"description": "Cross-platform Krunker game client",
|
"description": "Cross-platform Krunker game client",
|
||||||
"main": "dist/main/index.js",
|
"main": "dist/main/index.js",
|
||||||
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Generate Markdown release notes from conventional commits.
|
||||||
|
# Usage: ./scripts/generate-release-notes.sh <tag> [prev-ref]
|
||||||
|
#
|
||||||
|
# Skips version bumps, CI-only changes, and other noise.
|
||||||
|
# If prev-ref is not provided, tries git describe to find previous tag.
|
||||||
|
# If no previous ref is found, includes all commits up to HEAD.
|
||||||
|
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
TAG="${1:?Usage: generate-release-notes.sh <tag> [prev-ref]}"
|
||||||
|
PREV_REF="${2:-}"
|
||||||
|
|
||||||
|
# If no prev-ref provided, try to find one from git tags
|
||||||
|
if [ -z "$PREV_REF" ]; then
|
||||||
|
PREV_REF=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$PREV_REF" ]; then
|
||||||
|
RANGE="${PREV_REF}..HEAD"
|
||||||
|
else
|
||||||
|
RANGE="HEAD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect commits into temp files by category
|
||||||
|
TMPDIR_NOTES=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMPDIR_NOTES"' EXIT
|
||||||
|
|
||||||
|
for prefix in feat fix refactor perf other; do
|
||||||
|
: > "${TMPDIR_NOTES}/${prefix}"
|
||||||
|
done
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
|
||||||
|
# Skip version bump commits (e.g. "v0.6.1", "v0.6.2 — description")
|
||||||
|
[[ "$line" =~ ^v[0-9] ]] && continue
|
||||||
|
|
||||||
|
# Skip chore/docs/test/ci commits — not user-facing
|
||||||
|
[[ "$line" =~ ^(chore|docs|test|ci)(\(.*\))?: ]] && continue
|
||||||
|
|
||||||
|
MATCHED=false
|
||||||
|
for prefix in feat fix refactor perf; do
|
||||||
|
if [[ "$line" =~ ^${prefix}(\(.*\))?:\ (.+)$ ]]; then
|
||||||
|
MSG="${BASH_REMATCH[2]}"
|
||||||
|
echo "- ${MSG}" >> "${TMPDIR_NOTES}/${prefix}"
|
||||||
|
MATCHED=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$MATCHED" = false ]; then
|
||||||
|
echo "- ${line}" >> "${TMPDIR_NOTES}/other"
|
||||||
|
fi
|
||||||
|
done < <(git log --format="%s" "$RANGE" 2>/dev/null)
|
||||||
|
|
||||||
|
# Section display names
|
||||||
|
section_title() {
|
||||||
|
case "$1" in
|
||||||
|
feat) echo "## New" ;;
|
||||||
|
fix) echo "## Fixes" ;;
|
||||||
|
refactor) echo "## Improvements" ;;
|
||||||
|
perf) echo "## Performance" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build output — only user-facing sections
|
||||||
|
HAS_CONTENT=false
|
||||||
|
|
||||||
|
for prefix in feat fix refactor perf; do
|
||||||
|
if [ -s "${TMPDIR_NOTES}/${prefix}" ]; then
|
||||||
|
section_title "$prefix"
|
||||||
|
echo ""
|
||||||
|
cat "${TMPDIR_NOTES}/${prefix}"
|
||||||
|
echo ""
|
||||||
|
HAS_CONTENT=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -s "${TMPDIR_NOTES}/other" ]; then
|
||||||
|
echo "## Other"
|
||||||
|
echo ""
|
||||||
|
cat "${TMPDIR_NOTES}/other"
|
||||||
|
echo ""
|
||||||
|
HAS_CONTENT=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HAS_CONTENT" = false ]; then
|
||||||
|
echo "Bug fixes and improvements."
|
||||||
|
fi
|
||||||
@@ -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) */
|
/** 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}`;
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export interface AppConfig {
|
|||||||
deathscreenAnimation: boolean;
|
deathscreenAnimation: boolean;
|
||||||
hideMenuPopups: boolean;
|
hideMenuPopups: boolean;
|
||||||
cleanerMenu: boolean;
|
cleanerMenu: boolean;
|
||||||
|
menuTimer: boolean;
|
||||||
doublePing: boolean;
|
doublePing: boolean;
|
||||||
cssTheme: string;
|
cssTheme: string;
|
||||||
loadingTheme: string;
|
loadingTheme: string;
|
||||||
@@ -186,6 +187,7 @@ export const config = new Store<AppConfig>({
|
|||||||
deathscreenAnimation: true,
|
deathscreenAnimation: true,
|
||||||
hideMenuPopups: false,
|
hideMenuPopups: false,
|
||||||
cleanerMenu: false,
|
cleanerMenu: false,
|
||||||
|
menuTimer: true,
|
||||||
doublePing: true,
|
doublePing: true,
|
||||||
cssTheme: 'disabled',
|
cssTheme: 'disabled',
|
||||||
loadingTheme: 'disabled',
|
loadingTheme: 'disabled',
|
||||||
|
|||||||
+1
-1
@@ -192,7 +192,7 @@ app.whenReady().then(async () => {
|
|||||||
if (!cancelled && !updateWin.isDestroyed()) {
|
if (!cancelled && !updateWin.isDestroyed()) {
|
||||||
sendProgress(`Downloading update... ${pct}%`, pct);
|
sendProgress(`Downloading update... ${pct}%`, pct);
|
||||||
}
|
}
|
||||||
});
|
}, update.sha256);
|
||||||
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
sendProgress('Installing update...', 100);
|
sendProgress('Installing update...', 100);
|
||||||
|
|||||||
+42
-6
@@ -1,5 +1,6 @@
|
|||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
import { createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
|
import { createReadStream, createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { electronLog } from './logger';
|
import { electronLog } from './logger';
|
||||||
@@ -8,6 +9,7 @@ export interface UpdateInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
sha256: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProgressCallback = (percent: number) => void;
|
export type ProgressCallback = (percent: number) => void;
|
||||||
@@ -15,7 +17,7 @@ export type ProgressCallback = (percent: number) => void;
|
|||||||
const UPDATE_CONFIG = {
|
const UPDATE_CONFIG = {
|
||||||
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
|
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
|
||||||
assetPattern: /Setup\.exe$/i,
|
assetPattern: /Setup\.exe$/i,
|
||||||
allowedHosts: ['github.com', 'api.github.com', 'objects.githubusercontent.com'],
|
allowedHosts: ['github.com', 'githubusercontent.com'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHECK_TIMEOUT_MS = 10000;
|
const CHECK_TIMEOUT_MS = 10000;
|
||||||
@@ -104,7 +106,7 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assets: Array<{ name: string; browser_download_url: string; size: number }> = release.assets || [];
|
const assets: Array<{ name: string; browser_download_url: string; size: number; digest: string }> = release.assets || [];
|
||||||
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
|
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
|
||||||
if (!setupAsset) {
|
if (!setupAsset) {
|
||||||
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
|
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
|
||||||
@@ -119,11 +121,20 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| Download:', setupAsset.browser_download_url, '| Size:', setupAsset.size);
|
// Extract SHA-256 digest from GitHub API (format: "sha256:<hex>")
|
||||||
|
const sha256 = (setupAsset.digest || '').replace(/^sha256:/i, '');
|
||||||
|
if (!sha256) {
|
||||||
|
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| SHA-256:', sha256.substring(0, 16) + '...');
|
||||||
resolve({
|
resolve({
|
||||||
version: remoteVersion,
|
version: remoteVersion,
|
||||||
downloadUrl: setupAsset.browser_download_url,
|
downloadUrl: setupAsset.browser_download_url,
|
||||||
fileSize: setupAsset.size,
|
fileSize: setupAsset.size,
|
||||||
|
sha256,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
electronLog.error('[KCC-Update] Failed to parse release data:', err);
|
electronLog.error('[KCC-Update] Failed to parse release data:', err);
|
||||||
@@ -149,7 +160,22 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback): Promise<void> {
|
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
const stream = createReadStream(filePath);
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk));
|
||||||
|
stream.on('end', () => {
|
||||||
|
const actual = hash.digest('hex');
|
||||||
|
electronLog.log('[KCC-Update] SHA-256 expected:', expectedSha256);
|
||||||
|
electronLog.log('[KCC-Update] SHA-256 actual: ', actual);
|
||||||
|
resolve(actual === expectedSha256);
|
||||||
|
});
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback, expectedSha256?: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tmpPath = destPath + '.tmp';
|
const tmpPath = destPath + '.tmp';
|
||||||
|
|
||||||
@@ -194,8 +220,18 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
|
|||||||
res.pipe(file);
|
res.pipe(file);
|
||||||
|
|
||||||
file.on('finish', () => {
|
file.on('finish', () => {
|
||||||
file.close(() => {
|
file.close(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (expectedSha256) {
|
||||||
|
const valid = await verifyChecksum(tmpPath, expectedSha256);
|
||||||
|
if (!valid) {
|
||||||
|
electronLog.error('[KCC-Update] Checksum mismatch — file may be corrupted or tampered');
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(new Error('SHA-256 checksum mismatch'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
electronLog.log('[KCC-Update] Checksum verified');
|
||||||
|
}
|
||||||
if (existsSync(destPath)) unlinkSync(destPath);
|
if (existsSync(destPath)) unlinkSync(destPath);
|
||||||
renameSync(tmpPath, destPath);
|
renameSync(tmpPath, destPath);
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
+62
-1
@@ -4,7 +4,7 @@ import type { MatchmakerConfig } from './matchmaker';
|
|||||||
import { initUserscripts, getInstances, setScriptEnabled } from './userscripts';
|
import { initUserscripts, getInstances, setScriptEnabled } from './userscripts';
|
||||||
import type { UserscriptInstance } from './userscripts';
|
import type { UserscriptInstance } from './userscripts';
|
||||||
import { initTranslator, updateTranslatorConfig } from './translator';
|
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 { initChat, setBetterChat, setChatHistorySize } from './chat';
|
||||||
import { initHPCounter, destroyHPCounter } from './competitive';
|
import { initHPCounter, destroyHPCounter } from './competitive';
|
||||||
import { checkChangelog } from './changelog';
|
import { checkChangelog } from './changelog';
|
||||||
@@ -615,6 +615,13 @@ function buildGeneralSection(
|
|||||||
onChange: (v) => { ui.cleanerMenu = v; saveUI(); setCleanerMenu(v); },
|
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({
|
body.appendChild(createToggleRow({
|
||||||
label: 'Double Ping Display',
|
label: 'Double Ping Display',
|
||||||
desc: 'Show the real ping value (Krunker displays half the actual latency)',
|
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.deathscreenAnimation) setDeathAnimBlock(true);
|
||||||
|
if (ui.menuTimer ?? true) setMenuTimer(true);
|
||||||
if (ui.hideMenuPopups) startHidePopups();
|
if (ui.hideMenuPopups) startHidePopups();
|
||||||
|
|
||||||
body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
|
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?.deathscreenAnimation) setDeathAnimBlock(true);
|
||||||
if (uiConf?.hideMenuPopups) startHidePopups();
|
if (uiConf?.hideMenuPopups) startHidePopups();
|
||||||
if (uiConf?.cleanerMenu) setCleanerMenu(true);
|
if (uiConf?.cleanerMenu) setCleanerMenu(true);
|
||||||
|
if (uiConf?.menuTimer ?? true) setMenuTimer(true);
|
||||||
|
|
||||||
// ── Double ping display ──
|
// ── Double ping display ──
|
||||||
if (isGamePage && (uiConf?.doublePing ?? true)) {
|
if (isGamePage && (uiConf?.doublePing ?? true)) {
|
||||||
@@ -1689,6 +1698,58 @@ ipcRenderer.on('main_did-finish-load', () => {
|
|||||||
checkChangelog(currentVersion, uiConf?.lastSeenVersion || '');
|
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 ──
|
// ── Initialize userscripts ──
|
||||||
const usEnabled = usConf ? usConf.enabled : true;
|
const usEnabled = usConf ? usConf.enabled : true;
|
||||||
if (usEnabled) {
|
if (usEnabled) {
|
||||||
|
|||||||
@@ -101,6 +101,57 @@ const CLEANER_MENU_CSS = `
|
|||||||
.headerBarR { right: -23px !important; }
|
.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 {
|
export function setCleanerMenu(enabled: boolean): void {
|
||||||
let el = document.getElementById(CLEANER_MENU_ID);
|
let el = document.getElementById(CLEANER_MENU_ID);
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
|||||||
Reference in New Issue
Block a user