您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Seamlessly adds a stylish background download button to osu! beatmap pages - grab those beautiful covers with one click!
// ==UserScript== // @name osu! BackgroundGrabber // @namespace http://tampermonkey.net/ // @version 1.4 // @description Seamlessly adds a stylish background download button to osu! beatmap pages - grab those beautiful covers with one click! // @author Noxie // @match https://osu.ppy.sh/* // @icon https://raw.githubusercontent.com/Noxie0/osu-background-grabber/refs/heads/main/icon.png // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; // Settings management const SETTINGS_KEY = 'osu_backgroundgrabber_settings'; const DEFAULT_COLOR = '#3986ac'; const defaultSettings = { buttonEnabled: true, textEnabled: true, iconEnabled: true, accentColor: '#ff6bb3', useCustomColor: true }; function getSettings() { try { const saved = localStorage.getItem(SETTINGS_KEY); return saved ? { ...defaultSettings, ...JSON.parse(saved) } : defaultSettings; } catch (e) { console.warn('[BackgroundGrabber] Failed to load settings, using defaults:', e); return defaultSettings; } } function saveSettings(settings) { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) { console.warn('[BackgroundGrabber] Failed to save settings:', e); } } let currentSettings = getSettings(); function hexToRgba(hex, alpha = 1) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function getEffectiveColor() { return currentSettings.useCustomColor ? currentSettings.accentColor : DEFAULT_COLOR; } function updateColorVariables() { const root = document.documentElement; const effectiveColor = getEffectiveColor(); root.style.setProperty('--bg-grabber-accent', effectiveColor); root.style.setProperty('--bg-grabber-accent-90', hexToRgba(effectiveColor, 0.9)); root.style.setProperty('--bg-grabber-accent-100', hexToRgba(effectiveColor, 1)); const colorPreview = document.getElementById('bg-color-preview'); if (colorPreview) { colorPreview.style.background = effectiveColor; } } const style = document.createElement('style'); style.textContent = ` :root { --bg-grabber-accent: ${getEffectiveColor()}; --bg-grabber-accent-90: ${hexToRgba(getEffectiveColor(), 0.9)}; --bg-grabber-accent-100: ${hexToRgba(getEffectiveColor(), 1)}; } .background-btn { min-width: 120px !important; white-space: nowrap !important; padding: 0 20px !important; height: auto !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s ease !important; background-color: var(--bg-grabber-accent) !important; border-color: var(--bg-grabber-accent) !important; } .background-btn:hover { background-color: var(--bg-grabber-accent-100) !important; border-color: var(--bg-grabber-accent-100) !important; } .background-btn .fa-image { font-size: 16px !important; margin-right: 8px !important; line-height: 1 !important; } .background-btn.icon-only .fa-image { font-size: 20px !important; margin-right: 0 !important; } .background-btn span { font-size: 14px !important; font-weight: 600 !important; line-height: 1 !important; } .bg-grabber-settings { position: fixed; top: 70px; right: 20px; background: #2a2a2a; color: white; padding: 20px; border-radius: 10px; border: 2px solid var(--bg-grabber-accent); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); z-index: 10000; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; min-width: 300px; opacity: 0; visibility: hidden; transform: translateY(10px); transition: opacity 0.3s ease-out, visibility 0.3s ease-out, transform 0.3s ease-out; pointer-events: none; } .bg-grabber-settings.show { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; } .bg-grabber-settings h3 { margin: 0 0 20px 0; font-size: 18px; color: var(--bg-grabber-accent); border-bottom: 2px solid var(--bg-grabber-accent); padding-bottom: 8px; text-align: center; } .bg-grabber-setting-item { display: flex; align-items: center; margin-bottom: 15px; padding: 8px 0; border-bottom: 1px solid #444; } .bg-grabber-setting-item:last-of-type { border-bottom: none; margin-bottom: 0; } .bg-grabber-setting-item label { flex: 1; cursor: pointer; font-weight: 500; } .bg-grabber-setting-item input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; margin-right: 12px; accent-color: var(--bg-grabber-accent); } .bg-grabber-setting-item input[type="checkbox"]:disabled { opacity: 0.5; cursor: not-allowed; } .bg-grabber-setting-item label.disabled { opacity: 0.5; cursor: not-allowed; } .bg-grabber-color-section { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; } .bg-grabber-color-controls { display: flex; align-items: center; gap: 10px; } .bg-grabber-color-picker { width: 40px; height: 30px; border: 2px solid var(--bg-grabber-accent); border-radius: 6px; cursor: pointer; background: var(--bg-grabber-accent); transition: all 0.2s ease; } .bg-grabber-color-picker:hover { transform: scale(1.05); } .bg-grabber-color-picker:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .bg-grabber-hex-input { flex: 1; background: #1a1a1a; border: 1px solid #555; border-radius: 4px; padding: 6px 10px; color: white; font-family: 'Courier New', monospace; font-size: 13px; transition: border-color 0.2s ease; } .bg-grabber-hex-input:focus { outline: none; border-color: var(--bg-grabber-accent); } .bg-grabber-hex-input:disabled { opacity: 0.5; cursor: not-allowed; } .bg-grabber-hex-input.invalid { border-color: #ff4444; } .bg-grabber-color-preview { width: 20px; height: 20px; border-radius: 50%; border: 2px solid #555; background: var(--bg-grabber-accent); transition: all 0.2s ease; } .bg-grabber-reset-btn { background: #666; color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.2s ease; white-space: nowrap; } .bg-grabber-reset-btn:hover { background: #777; } .bg-grabber-reset-btn:disabled { opacity: 0.5; cursor: not-allowed; background: #666; } .bg-grabber-settings-footer { margin-top: 20px; padding-top: 15px; border-top: 1px solid #444; display: flex; justify-content: space-around; gap: 10px; } .bg-grabber-settings-footer button, .bg-grabber-settings-footer a { flex: 1; padding: 8px 12px; border: none; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: 600; text-align: center; text-decoration: none; transition: background-color 0.2s ease, transform 0.1s ease; color: white; } .bg-grabber-settings-footer .github-btn { background-color: #333; } .bg-grabber-settings-footer .github-btn:hover { background-color: #555; transform: translateY(-1px); } .bg-grabber-settings-footer .bug-report-btn { background-color: #d9534f; } .bg-grabber-settings-footer .bug-report-btn:hover { background-color: #c9302c; transform: translateY(-1px); } .bg-grabber-settings-icon { position: fixed; top: 20px; right: 20px; background: var(--bg-grabber-accent-90); color: white; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; z-index: 9999; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); transition: all 0.2s ease; opacity: 0; visibility: hidden; pointer-events: none; } .bg-grabber-settings-icon.visible { opacity: 1; visibility: visible; pointer-events: auto; } .bg-grabber-settings-icon:hover { background: var(--bg-grabber-accent-100); transform: scale(1.1); } `; document.head.appendChild(style); updateColorVariables(); function isValidHex(hex) { return /^#[0-9A-F]{6}$/i.test(hex); } function createSettingsPanel() { const panel = document.createElement('div'); panel.className = 'bg-grabber-settings'; panel.innerHTML = ` <h3>Background Grabber Settings</h3> <div class="bg-grabber-setting-item"> <input type="checkbox" id="bg-button-toggle" ${currentSettings.buttonEnabled ? 'checked' : ''}> <label for="bg-button-toggle">Enable Button</label> </div> <div class="bg-grabber-setting-item"> <input type="checkbox" id="bg-text-toggle" ${currentSettings.textEnabled ? 'checked' : ''}> <label for="bg-text-toggle">Show Text</label> </div> <div class="bg-grabber-setting-item"> <input type="checkbox" id="bg-icon-toggle" ${currentSettings.iconEnabled ? 'checked' : ''}> <label for="bg-icon-toggle">Show Icon</label> </div> <div class="bg-grabber-setting-item"> <input type="checkbox" id="bg-custom-color-toggle" ${currentSettings.useCustomColor ? 'checked' : ''}> <label for="bg-custom-color-toggle">Use Custom Color</label> </div> <div class="bg-grabber-setting-item"> <label>Accent Color</label> </div> <div class="bg-grabber-color-section"> <div class="bg-grabber-color-controls"> <input type="color" class="bg-grabber-color-picker" value="${currentSettings.accentColor}" id="bg-color-picker" ${!currentSettings.useCustomColor ? 'disabled' : ''}> <input type="text" class="bg-grabber-hex-input" value="${currentSettings.accentColor}" id="bg-hex-input" placeholder="#ff6bb3" ${!currentSettings.useCustomColor ? 'disabled' : ''}> <button class="bg-grabber-reset-btn" id="bg-reset-btn" ${!currentSettings.useCustomColor ? 'disabled' : ''}>Reset</button> <div class="bg-grabber-color-preview" id="bg-color-preview" style="background: ${getEffectiveColor()};"></div> </div> </div> <div class="bg-grabber-settings-footer"> <a href="https://github.com/Noxie0/osu-BackgroundGrabber" target="_blank" rel="noopener noreferrer" class="github-btn"> GitHub </a> <a href="https://github.com/Noxie0/osu-BackgroundGrabber/issues" target="_blank" rel="noopener noreferrer" class="bug-report-btn"> Report a Bug </a> </div> `; const buttonToggle = panel.querySelector('#bg-button-toggle'); const textToggle = panel.querySelector('#bg-text-toggle'); const iconToggle = panel.querySelector('#bg-icon-toggle'); const customColorToggle = panel.querySelector('#bg-custom-color-toggle'); const colorPicker = panel.querySelector('#bg-color-picker'); const hexInput = panel.querySelector('#bg-hex-input'); const colorPreview = panel.querySelector('#bg-color-preview'); const resetBtn = panel.querySelector('#bg-reset-btn'); resetBtn.addEventListener('click', () => { if (!currentSettings.useCustomColor) return; currentSettings.accentColor = DEFAULT_COLOR; colorPicker.value = DEFAULT_COLOR; hexInput.value = DEFAULT_COLOR; hexInput.classList.remove('invalid'); updateColorVariables(); saveSettings(currentSettings); updateButtonContent(); }); customColorToggle.addEventListener('change', (e) => { currentSettings.useCustomColor = e.target.checked; colorPicker.disabled = !currentSettings.useCustomColor; hexInput.disabled = !currentSettings.useCustomColor; resetBtn.disabled = !currentSettings.useCustomColor; updateColorVariables(); saveSettings(currentSettings); updateButtonContent(); }); colorPicker.addEventListener('input', (e) => { if (!currentSettings.useCustomColor) return; const newColor = e.target.value; currentSettings.accentColor = newColor; hexInput.value = newColor; hexInput.classList.remove('invalid'); updateColorVariables(); saveSettings(currentSettings); updateButtonContent(); }); hexInput.addEventListener('input', (e) => { if (!currentSettings.useCustomColor) return; const newColor = e.target.value; if (isValidHex(newColor)) { currentSettings.accentColor = newColor; colorPicker.value = newColor; hexInput.classList.remove('invalid'); updateColorVariables(); saveSettings(currentSettings); updateButtonContent(); } else { hexInput.classList.add('invalid'); } }); buttonToggle.addEventListener('change', (e) => { currentSettings.buttonEnabled = e.target.checked; if (currentSettings.buttonEnabled && !currentSettings.textEnabled && !currentSettings.iconEnabled) { currentSettings.textEnabled = true; textToggle.checked = true; } saveSettings(currentSettings); updateButtonVisibility(); updateSettingsState(); tryInjectButton(); // Always try to re-inject/update button }); textToggle.addEventListener('change', (e) => { currentSettings.textEnabled = e.target.checked; if (!currentSettings.textEnabled && !currentSettings.iconEnabled) { currentSettings.buttonEnabled = false; buttonToggle.checked = false; updateSettingsState(); } saveSettings(currentSettings); updateButtonContent(); updateButtonVisibility(); tryInjectButton(); // Always try to re-inject/update button }); iconToggle.addEventListener('change', (e) => { currentSettings.iconEnabled = e.target.checked; if (!currentSettings.iconEnabled && !currentSettings.textEnabled) { currentSettings.buttonEnabled = false; buttonToggle.checked = false; updateSettingsState(); } saveSettings(currentSettings); updateButtonContent(); updateButtonVisibility(); tryInjectButton(); // Always try to re-inject/update button }); updateSettingsState(); return panel; } function createSettingsIcon() { const icon = document.createElement('button'); icon.className = 'bg-grabber-settings-icon'; icon.innerHTML = '⚙️'; icon.title = 'Background Grabber Settings'; icon.addEventListener('click', (event) => { event.stopPropagation(); const panel = document.querySelector('.bg-grabber-settings'); if (panel) { panel.classList.toggle('show'); } }); return icon; } function updateButtonVisibility() { const button = document.querySelector('.background-btn'); if (button) { if (currentSettings.buttonEnabled && (currentSettings.textEnabled || currentSettings.iconEnabled)) { button.style.display = 'inline-flex'; button.style.visibility = 'visible'; } else { button.style.display = 'none'; button.style.visibility = 'hidden'; } } } function updateSettingsIconVisibility() { const icon = document.querySelector('.bg-grabber-settings-icon'); if (icon) { const isOnBeatmapPage = window.location.pathname.includes('/beatmapsets/'); if (isOnBeatmapPage) { icon.classList.add('visible'); ensureSettingsPanel(); } else { icon.classList.remove('visible'); const panel = document.querySelector('.bg-grabber-settings'); if (panel) { panel.classList.remove('show'); } } } } function updateSettingsState() { const textToggle = document.querySelector('#bg-text-toggle'); const iconToggle = document.querySelector('#bg-icon-toggle'); const textLabel = document.querySelector('label[for="bg-text-toggle"]'); const iconLabel = document.querySelector('label[for="bg-icon-toggle"]'); const buttonToggle = document.querySelector('#bg-button-toggle'); if (textToggle && iconToggle && textLabel && iconLabel && buttonToggle) { const isDisabled = !currentSettings.buttonEnabled; textToggle.disabled = isDisabled; iconToggle.disabled = isDisabled; if (isDisabled) { textLabel.classList.add('disabled'); iconLabel.classList.add('disabled'); } else { textLabel.classList.remove('disabled'); iconLabel.classList.remove('disabled'); if (!textToggle.checked && !iconToggle.checked) { currentSettings.textEnabled = true; textToggle.checked = true; saveSettings(currentSettings); updateButtonContent(); } } } const customColorToggle = document.querySelector('#bg-custom-color-toggle'); const colorPicker = document.querySelector('#bg-color-picker'); const hexInput = document.querySelector('#bg-hex-input'); const resetBtn = document.querySelector('#bg-reset-btn'); if (customColorToggle && colorPicker && hexInput && resetBtn) { const colorControlsDisabled = !currentSettings.useCustomColor; colorPicker.disabled = colorControlsDisabled; hexInput.disabled = colorControlsDisabled; resetBtn.disabled = colorControlsDisabled; } } function updateButtonContent() { const button = document.querySelector('.background-btn'); if (!button) return; const iconHtml = currentSettings.iconEnabled ? '<i class="fas fa-image"></i>' : ''; const textHtml = currentSettings.textEnabled ? '<span>Background</span>' : ''; button.innerHTML = iconHtml + textHtml; if (!currentSettings.iconEnabled && !currentSettings.textEnabled) { button.style.display = 'none'; button.style.visibility = 'hidden'; button.classList.remove('icon-only'); } else if (currentSettings.iconEnabled && !currentSettings.textEnabled) { button.style.setProperty('padding', '0', 'important'); button.style.setProperty('min-width', '45px', 'important'); button.style.setProperty('width', '45px', 'important'); button.style.setProperty('height', '45px', 'important'); button.classList.add('icon-only'); } else if (!currentSettings.iconEnabled && currentSettings.textEnabled) { button.style.setProperty('padding', '0 16px', 'important'); button.style.setProperty('min-width', '80px', 'important'); button.style.setProperty('width', 'auto', 'important'); button.style.setProperty('height', 'auto', 'important'); button.classList.remove('icon-only'); } else { button.style.setProperty('padding', '0 20px', 'important'); button.style.setProperty('min-width', '120px', 'important'); button.style.setProperty('width', 'auto', 'important'); button.style.setProperty('height', 'auto', 'important'); button.classList.remove('icon-only'); } updateButtonVisibility(); } function createButton(beatmapSetId, container) { const rawUrl = `https://assets.ppy.sh/beatmaps/${beatmapSetId}/covers/raw.jpg`; const fallbackUrl = `https://assets.ppy.sh/beatmaps/${beatmapSetId}/covers/cover.jpg`; const existingButtonReference = container.querySelector('a[class*="btn"], button[class*="btn"]'); const bgBtn = document.createElement('a'); bgBtn.className = existingButtonReference ? existingButtonReference.className : 'btn-osu-big btn-osu-big--beatmapset-header'; bgBtn.classList.add('background-btn'); bgBtn.href = '#'; bgBtn.target = '_blank'; bgBtn.rel = 'noopener noreferrer'; bgBtn.addEventListener('click', (e) => { e.preventDefault(); const testImg = new Image(); testImg.onload = () => window.open(rawUrl, '_blank'); testImg.onerror = () => window.open(fallbackUrl, '_blank'); testImg.src = rawUrl; }); container.appendChild(bgBtn); updateButtonContent(); updateButtonVisibility(); } // --- NEW: MutationObserver for the button container --- let buttonContainerObserver = null; let lastBeatmapSetId = null; // To track if we are on the same beatmapset page function tryInjectButton() { const match = window.location.pathname.match(/\/beatmapsets\/(\d+)/); const container = document.querySelector('.beatmapset-header__buttons'); const existingButton = document.querySelector('.background-btn'); if (!match) { // Not on a beatmap page if (existingButton) existingButton.remove(); if (buttonContainerObserver) { buttonContainerObserver.disconnect(); buttonContainerObserver = null; } lastBeatmapSetId = null; return; } const currentBeatmapSetId = match[1]; // If we are on a new beatmapset page, or button is missing, re-create if (currentBeatmapSetId !== lastBeatmapSetId || !existingButton || !container || !container.contains(existingButton)) { if (existingButton) existingButton.remove(); // Clean up old button if (container) { createButton(currentBeatmapSetId, container); lastBeatmapSetId = currentBeatmapSetId; // Update last known ID // Set up observer if not already active for this container if (!buttonContainerObserver) { buttonContainerObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.removedNodes.length > 0) { // Check if our button was removed const ourButtonRemoved = Array.from(mutation.removedNodes).some(node => node.classList && node.classList.contains('background-btn')); if (ourButtonRemoved) { // Button was removed, re-inject immediately // Use a small timeout to allow React to finish its immediate DOM operations setTimeout(() => tryInjectButton(), 50); break; // Only need to react once } } } }); buttonContainerObserver.observe(container, { childList: true }); } } } else if (existingButton) { // Button exists and is in place, just update its content/visibility updateButtonContent(); updateButtonVisibility(); } } function ensureSettingsPanel() { let existingPanel = document.querySelector('.bg-grabber-settings'); if (!existingPanel) { const settingsPanel = createSettingsPanel(); document.body.appendChild(settingsPanel); updateSettingsState(); } else { updateSettingsState(); } } // --- NEW: MutationObserver for the settings icon --- let settingsIconObserver = null; function tryInjectSettingsIcon() { currentSettings = getSettings(); updateColorVariables(); let existingIcon = document.querySelector('.bg-grabber-settings-icon'); if (!window.location.pathname.includes('/beatmapsets/')) { // Not on beatmap page if (existingIcon) existingIcon.remove(); if (settingsIconObserver) { settingsIconObserver.disconnect(); settingsIconObserver = null; } return; } if (!existingIcon) { const settingsIcon = createSettingsIcon(); document.body.appendChild(settingsIcon); existingIcon = settingsIcon; // Set up observer for the icon if not already active if (!settingsIconObserver) { settingsIconObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.removedNodes.length > 0) { const ourIconRemoved = Array.from(mutation.removedNodes).some(node => node.classList && node.classList.contains('bg-grabber-settings-icon')); if (ourIconRemoved) { setTimeout(() => tryInjectSettingsIcon(), 50); break; } } } }); // Observe the body, as the icon is directly appended to it settingsIconObserver.observe(document.body, { childList: true }); } } updateSettingsIconVisibility(); } function waitForContainer(callback, attempts = 0) { if (attempts >= 50) return; const container = document.querySelector('.beatmapset-header__buttons'); if (container) { callback(); } else { setTimeout(() => waitForContainer(callback, attempts + 1), 100); // Shorter interval } } function waitForBody(callback, attempts = 0) { if (attempts >= 50) return; if (document.body) { callback(); } else { setTimeout(() => waitForBody(callback, attempts + 1), 100); // Shorter interval } } function setupObservers() { let lastPath = location.pathname; const reactRouteObserver = new MutationObserver(() => { if (location.pathname !== lastPath) { lastPath = location.pathname; // Still use a generous delay for full route changes setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); }, 600); // Slightly reduced from 800ms } }); waitForBody(() => { reactRouteObserver.observe(document.body, { childList: true, subtree: true }); }); // General DOM content observer - now less critical due to specific observers const domContentObserver = new MutationObserver((mutations) => { let shouldCheck = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { if (mutation.target.matches('.beatmapset-header__buttons') || mutation.target.closest('.beatmapset-header__buttons') || mutation.target.id === 'app' || mutation.target.tagName === 'MAIN' || mutation.target.matches('body')) { shouldCheck = true; break; } } } if (shouldCheck) { setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); }, 100); // Shorter delay } }); waitForBody(() => { const reactAppRoot = document.getElementById('app') || document.getElementById('root') || document.body; domContentObserver.observe(reactAppRoot, { childList: true, subtree: true }); }); window.addEventListener('popstate', () => { setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); }, 600); }); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(history, args); setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); }, 600); }; history.replaceState = function(...args) { originalReplaceState.apply(history, args); setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); }, 600); }; } // Initial setup when the script loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setupObservers(); setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); ensureSettingsPanel(); }, 50); // Very short initial delay }); } else { setupObservers(); setTimeout(() => { tryInjectButton(); tryInjectSettingsIcon(); ensureSettingsPanel(); }, 50); // Very short initial delay } waitForBody(ensureSettingsPanel); })();