osu! BackgroundGrabber

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);

})();