Bumble Filter (Interactive Settings with Radio Buttons & Number Filter)

Highlights profiles on Bumble based on various criteria (Nice, Neutral, No-Go) with an interactive settings panel, radio button selection, and specific handling for height.

// ==UserScript==
// @name         Bumble Filter (Interactive Settings with Radio Buttons & Number Filter)
// @namespace    http://tampermonkey.net/
// @version      0.9.1
// @description  Highlights profiles on Bumble based on various criteria (Nice, Neutral, No-Go) with an interactive settings panel, radio button selection, and specific handling for height.
// @author       Your Name
// @match        *://*.bumble.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- GLOBAL VARIABLES AND DEFAULT CONFIGURATION ---
    const SETTINGS_KEY = 'bumbleFilterSettings_v0_9_1'; // Updated version key

    const HIGHLIGHT_COLOR_NICE = 'lightgreen';
    const HIGHLIGHT_COLOR_INDIFFERENT = '#f0f0f0'; // Light gray for "neutral"
    const HIGHLIGHT_COLOR_NO_GO = '#ffcccb';

    const defaultSettingsTemplate = {
        education: { imageSubstring: 'education', desired: [], noGo: [], label: "Education", allKnownValues: [], type: "radio" },
        familyplans: { imageSubstring: 'familyplans', desired: [], noGo: [], label: "Family Plans", allKnownValues: [], type: "radio" },
        height: {
            imageSubstring: 'height',
            desiredMinCm: null,
            desiredMaxCm: null,
            noGoSpecificCm: [],
            label: "Height",
            allKnownValues: [],
            type: "numberRange"
        },
        exercise: { imageSubstring: 'exercise', desired: [], noGo: [], label: "Exercise", allKnownValues: [], type: "radio" },
        drinking: { imageSubstring: 'drinking', desired: [], noGo: [], label: "Drinking Habits", allKnownValues: [], type: "radio" },
        smoking: { imageSubstring: 'smoking', desired: [], noGo: [], label: "Smoking Habits", allKnownValues: [], type: "radio" },
        cannabis: { imageSubstring: 'cannabis', desired: [], noGo: [], label: "Cannabis Use", allKnownValues: [], type: "radio" },
        religion: { imageSubstring: 'religion', desired: [], noGo: [], label: "Religion", allKnownValues: [], type: "radio" },
        intentions: { imageSubstring: 'intentions', desired: [], noGo: [], label: "Intentions", allKnownValues: [], type: "radio" },
        politics: { imageSubstring: 'politics', desired: [], noGo: [], label: "Politics", allKnownValues: [], type: "radio" },
        starsign: { imageSubstring: 'starsign', desired: [], noGo: [], label: "Star Sign", allKnownValues: [], type: "radio" }
    };

    let currentSettings = {};
    const profileCardSelectors = ['.encounters-story'];

    function parseCmValue(cmString) {
        if (typeof cmString !== 'string') return null;
        const match = cmString.match(/(\d+)/);
        return match ? parseInt(match[1], 10) : null;
    }

    function loadSettings() {
        console.log("[Bumble Filter] Loading settings...");
        const storedSettings = GM_getValue(SETTINGS_KEY);
        currentSettings = JSON.parse(JSON.stringify(defaultSettingsTemplate));

        if (storedSettings) {
            console.log("[Bumble Filter] Found stored settings:", JSON.parse(JSON.stringify(storedSettings)));
            for (const key in currentSettings) {
                if (storedSettings[key]) {
                    const categoryTemplate = defaultSettingsTemplate[key];
                    const storedCategory = storedSettings[key];

                    if (categoryTemplate.type === "numberRange") {
                        currentSettings[key].desiredMinCm = storedCategory.desiredMinCm !== undefined ? (parseInt(String(storedCategory.desiredMinCm), 10) || null) : categoryTemplate.desiredMinCm;
                        currentSettings[key].desiredMaxCm = storedCategory.desiredMaxCm !== undefined ? (parseInt(String(storedCategory.desiredMaxCm), 10) || null) : categoryTemplate.desiredMaxCm;
                        currentSettings[key].noGoSpecificCm = Array.isArray(storedCategory.noGoSpecificCm) ? storedCategory.noGoSpecificCm.map(s => parseInt(String(s), 10)).filter(n => !isNaN(n)) : categoryTemplate.noGoSpecificCm;
                    } else {
                        currentSettings[key].desired = Array.isArray(storedCategory.desired) ? storedCategory.desired.map(s => String(s).toLowerCase().trim()).filter(s => s) : categoryTemplate.desired;
                        currentSettings[key].noGo = Array.isArray(storedCategory.noGo) ? storedCategory.noGo.map(s => String(s).toLowerCase().trim()).filter(s => s) : categoryTemplate.noGo;
                    }
                    currentSettings[key].allKnownValues = Array.isArray(storedCategory.allKnownValues) ? storedCategory.allKnownValues.map(s => String(s).toLowerCase().trim()).filter(s => s) : [];
                }
            }
        }
        for (const key in currentSettings) {
            const category = currentSettings[key];
            if (category.type === "radio") {
                const knownValuesSet = new Set(category.allKnownValues);
                category.desired.forEach(val => knownValuesSet.add(String(val).toLowerCase().trim()));
                category.noGo.forEach(val => knownValuesSet.add(String(val).toLowerCase().trim()));
                category.allKnownValues = Array.from(knownValuesSet).sort();
            }
        }
        console.log("[Bumble Filter] Settings loaded and merged:", JSON.parse(JSON.stringify(currentSettings)));
    }

    function saveSettings() {
        console.log("[Bumble Filter] Attempting to save settings. Current settings object before reading DOM:", JSON.parse(JSON.stringify(currentSettings)));
        for (const categoryKey in currentSettings) {
            const category = currentSettings[categoryKey];
            const panelContent = document.getElementById('bf-panel-content');
            if (!panelContent) {
                console.error("[Bumble Filter] Panel content not found for saving.");
                continue;
            }

            console.log(`[Bumble Filter] Saving category: ${categoryKey}`);

            if (category.type === "numberRange") {
                const minInput = panelContent.querySelector(`#bf-desiredMinCm-${categoryKey}`);
                const maxInput = panelContent.querySelector(`#bf-desiredMaxCm-${categoryKey}`);
                const noGoInput = panelContent.querySelector(`#bf-noGoSpecificCm-${categoryKey}`);

                category.desiredMinCm = minInput && minInput.value.trim() !== "" ? parseInt(minInput.value, 10) : null;
                category.desiredMaxCm = maxInput && maxInput.value.trim() !== "" ? parseInt(maxInput.value, 10) : null;
                category.noGoSpecificCm = noGoInput ? noGoInput.value.split('\n').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)) : [];
            } else {
                const newDesired = [];
                const newNoGo = [];

                category.allKnownValues.forEach((value, index) => {
                    const stringValue = String(value);
                    const radioGroupName = `bf-state-${categoryKey}-${index}`;
                    const checkedRadio = panelContent.querySelector(`input[name="${radioGroupName}"]:checked`);

                    if (checkedRadio) {
                        if (checkedRadio.value === 'nice') {
                            newDesired.push(stringValue);
                        } else if (checkedRadio.value === 'nogo') {
                            newNoGo.push(stringValue);
                        }
                    }
                });
                category.desired = newDesired;
                category.noGo = newNoGo;
                console.log(`[Bumble Filter] Category ${categoryKey} after DOM read - Desired:`, [...category.desired], "NoGo:", [...category.noGo]);
            }
        }
        GM_setValue(SETTINGS_KEY, currentSettings);
        console.log("[Bumble Filter] Settings saved to GM_setValue:", JSON.parse(JSON.stringify(currentSettings)));
        document.querySelectorAll('[data-custom-filter-processed-v9-1]').forEach(card => {
            card.removeAttribute('data-custom-filter-processed-v9-1');
        });
        processVisibleProfileCards();
    }

    function resetSettings() {
        console.log("[Bumble Filter] Resetting settings to default.");
        if (confirm("Do you really want to reset all filter settings to their default values?")) {
            GM_setValue(SETTINGS_KEY, undefined);
            loadSettings();
            const panel = document.getElementById('bumble-filter-settings-panel');
            if (panel && panel.style.display === 'block') {
                 buildSettingsPanelContent(panel.querySelector('#bf-panel-content'));
            }
            document.querySelectorAll('[data-custom-filter-processed-v9-1]').forEach(card => {
                card.removeAttribute('data-custom-filter-processed-v9-1');
            });
            processVisibleProfileCards();
            alert("Settings have been reset.");
        }
    }

    function buildSettingsPanelContent(panelContentDiv) {
        if (!panelContentDiv) {
            console.error("[Bumble Filter] buildSettingsPanelContent: panelContentDiv is null");
            return;
        }
        for (const key in currentSettings) {
            const category = currentSettings[key];
            if (category.type === "radio") {
                const knownValuesSet = new Set(category.allKnownValues);
                currentSettings[key].desired.forEach(val => knownValuesSet.add(String(val).toLowerCase().trim()));
                currentSettings[key].noGo.forEach(val => knownValuesSet.add(String(val).toLowerCase().trim()));
                category.allKnownValues = Array.from(knownValuesSet).sort();
            }
        }
        console.log("[Bumble Filter] Building panel content with current settings for UI:", JSON.parse(JSON.stringify(currentSettings)));

        let contentHtml = '';
        for (const categoryKey in currentSettings) {
            const category = currentSettings[categoryKey];
            contentHtml += `<div class="bf-category-section"><h3>${category.label} (ID: ${category.imageSubstring})</h3>`;

            if (category.type === "numberRange") {
                contentHtml += `
                    <div class="bf-input-group">
                        <label for="bf-desiredMinCm-${categoryKey}">Min. desired height (cm):</label>
                        <input type="number" id="bf-desiredMinCm-${categoryKey}" value="${category.desiredMinCm === null ? '' : category.desiredMinCm}" placeholder="e.g. 170">
                    </div>
                    <div class="bf-input-group">
                        <label for="bf-desiredMaxCm-${categoryKey}">Max. desired height (cm):</label>
                        <input type="number" id="bf-desiredMaxCm-${categoryKey}" value="${category.desiredMaxCm === null ? '' : category.desiredMaxCm}" placeholder="e.g. 185">
                    </div>
                    <div class="bf-input-group">
                        <label for="bf-noGoSpecificCm-${categoryKey}">No-Go heights (cm, per line):</label>
                        <textarea id="bf-noGoSpecificCm-${categoryKey}" placeholder="e.g. 160\n195">${category.noGoSpecificCm.join('\n')}</textarea>
                    </div>`;
                if (category.allKnownValues.length > 0) {
                     contentHtml += `<p class="bf-info-text">Discovered heights (examples): ${category.allKnownValues.map(v => parseCmValue(v) + 'cm').filter(v => v !== 'nullcm').slice(0,10).join(', ')}${category.allKnownValues.length > 10 ? '...' : ''}</p>`;
                }
            } else {
                if (category.allKnownValues.length === 0) {
                    contentHtml += `<p class="bf-no-values">No values discovered for this category yet. They will be collected as you swipe.</p>`;
                }
                const sortedKnownValues = [...category.allKnownValues].sort();

                sortedKnownValues.forEach((value, index) => {
                    const stringValue = String(value);
                    const isDesired = category.desired.includes(stringValue);
                    const isNoGo = category.noGo.includes(stringValue);
                    const isNeutral = !isDesired && !isNoGo;
                    const radioGroupName = `bf-state-${categoryKey}-${index}`;

                    contentHtml += `
                        <div class="bf-value-row">
                            <span class="bf-value-label" title="${stringValue}">${stringValue.length > 25 ? stringValue.substring(0, 22) + '...' : stringValue}</span>
                            <div class="bf-radio-group">
                                <label class="bf-radio-label"><input type="radio" name="${radioGroupName}" value="nice" ${isDesired ? 'checked' : ''}> Nice</label>
                                <label class="bf-radio-label"><input type="radio" name="${radioGroupName}" value="neutral" ${isNeutral ? 'checked' : ''}> Neutral</label>
                                <label class="bf-radio-label"><input type="radio" name="${radioGroupName}" value="nogo" ${isNoGo ? 'checked' : ''}> No-Go</label>
                            </div>
                        </div>`;
                });
            }
            contentHtml += `</div>`;
        }
        panelContentDiv.innerHTML = contentHtml;
    }

    function createSettingsPanelShell() {
        let panel = document.getElementById('bumble-filter-settings-panel');
        if (panel) return panel;

        console.log("[Bumble Filter] Creating settings panel shell for the first time.");
        let panelHtml = `
            <div id="bf-panel-header">
                <h2>Bumble Filter Settings</h2>
                <button id="bf-close-panel" title="Close">X</button>
            </div>
            <div id="bf-panel-content"></div>
            <div id="bf-panel-footer">
                <button id="bf-reset-settings" title="Reset all settings to default">Reset</button>
                <button id="bf-save-settings">Save & Apply</button>
            </div>`;
        panel = document.createElement('div');
        panel.id = 'bumble-filter-settings-panel';
        panel.innerHTML = panelHtml;
        document.body.appendChild(panel);

        document.getElementById('bf-save-settings').addEventListener('click', () => saveSettings());
        document.getElementById('bf-close-panel').addEventListener('click', () => {
            const panelToClose = document.getElementById('bumble-filter-settings-panel');
            if(panelToClose) panelToClose.style.display = 'none';
        });
        document.getElementById('bf-reset-settings').addEventListener('click', () => resetSettings());
        return panel;
    }

    function addStyles() {
        GM_addStyle(`
            #bumble-filter-settings-panel {
                position: fixed; top: 50px; right: 20px; width: 480px; max-height: 85vh;
                background-color: white; border: 1px solid #ccc; box-shadow: 0 0 15px rgba(0,0,0,0.2);
                z-index: 100000 !important; display: none; font-family: Arial, sans-serif; font-size: 14px; border-radius: 8px;
                pointer-events: auto !important;
            }
            #bf-panel-header {
                background-color: #f0f0f0; padding: 10px 15px; border-bottom: 1px solid #ccc;
                display: flex; justify-content: space-between; align-items: center;
                border-top-left-radius: 8px; border-top-right-radius: 8px;
            }
            #bf-panel-header h2 { margin: 0; font-size: 16px; }
            #bf-close-panel, #bf-save-settings, #bf-open-settings-button, #bf-reset-settings {
                pointer-events: auto !important; cursor: pointer;
            }
            #bf-close-panel { background: none; border: none; font-size: 20px; padding: 0 5px;}
            #bf-panel-content { padding: 15px; overflow-y: auto; max-height: calc(85vh - 100px); }
            .bf-category-section { margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
            .bf-category-section:last-child { border-bottom: none; margin-bottom: 0; }
            .bf-category-section h3 { font-size: 14px; margin-top: 0; margin-bottom: 10px; color: #333; }
            .bf-no-values, .bf-info-text { font-style: italic; color: #777; margin-left: 10px; font-size: 12px; }
            .bf-value-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; padding: 4px; border-radius: 3px; }
            .bf-value-row:hover { background-color: #f9f9f9; }
            .bf-value-label { flex-basis: 35%; font-size: 13px; margin-right: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
            .bf-radio-group { display: flex; gap: 8px; pointer-events: auto !important; flex-basis: 65%; justify-content: flex-end; }
            .bf-radio-label { font-size: 12px; cursor: pointer; display:flex; align-items:center; pointer-events: auto !important; padding: 2px 5px; border-radius:3px; }
            .bf-radio-label:hover { background-color: #e9e9e9; }
            .bf-radio-label input[type="radio"] {
                margin-right: 3px; cursor: pointer; pointer-events: auto !important;
                z-index: 100001 !important; position: relative; appearance: auto !important;
                -webkit-appearance: auto !important; -moz-appearance: auto !important;
                opacity: 1 !important; visibility: visible !important;
                width: auto !important; height: auto !important;
            }
            .bf-input-group { margin-bottom: 10px; }
            .bf-input-group label { display: block; margin-bottom: 4px; font-weight: bold; font-size: 13px; }
            .bf-input-group input[type="number"], .bf-input-group textarea {
                width: 95%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;
            }
            .bf-input-group textarea { min-height: 50px; resize: vertical; }
            #bf-panel-footer {
                padding: 10px 15px; display: flex; justify-content: space-between; align-items: center;
                border-top: 1px solid #ccc; background-color: #f9f9f9;
                border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;
            }
            #bf-save-settings, #bf-reset-settings {
                padding: 8px 15px; color: white; border: none;
                border-radius: 4px; font-size: 14px;
            }
            #bf-save-settings { background-color: #5cb85c; }
            #bf-save-settings:hover { background-color: #4cae4c; }
            #bf-reset-settings { background-color: #d9534f; }
            #bf-reset-settings:hover { background-color: #c9302c; }
            #bf-open-settings-button {
                position: fixed; bottom: 20px; right: 20px; width: 40px; height: 40px;
                background-color: #7B1FA2; color: white; border: none; border-radius: 50%;
                font-size: 20px; font-weight: bold; text-align: center;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2); z-index: 99999 !important;
            }
            #bf-open-settings-button:hover { background-color: #6A1B9A; }
        `);
    }

    function logPills(profileCard) {
        const allTextPillsOnCard = profileCard.querySelectorAll('.pill__title > div.p-3.text-ellipsis.font-weight-medium, .pill__title > div.text-ellipsis.font-weight-medium.p-3');
        let cardMatchesAnyNiceCriteria = false;
        let cardHasAnyNoGo = false;
        let settingsModifiedByNewValues = false;

        allTextPillsOnCard.forEach(pillTextElement => {
            const pillTextOriginal = pillTextElement.textContent.trim();
            const pillTextLower = pillTextOriginal.toLowerCase().replace(/\u00a0/g, " ").trim();

            if (!pillTextLower) return;

            let pillTypeKey = null;
            let imageSrcFound = null;

            const pillTitleDiv = pillTextElement.parentElement;
            if (pillTitleDiv && pillTitleDiv.classList.contains('pill__title')) {
                const commonPillWrapper = pillTextElement.closest('li, .pill, .profile-badge-container, .user-interest, div.p-2, div.profile-v2-interest__badge') || pillTitleDiv.parentElement;
                if (commonPillWrapper) {
                    const imageElement = commonPillWrapper.querySelector('img.pill__image');
                    if (imageElement && imageElement.src) {
                        imageSrcFound = imageElement.src.toLowerCase();
                        for (const key in currentSettings) {
                            if (currentSettings[key] && currentSettings[key].imageSubstring && imageSrcFound.includes(currentSettings[key].imageSubstring.toLowerCase())) {
                                pillTypeKey = key;
                                break;
                            }
                        }
                    }
                }
            }

            if (pillTypeKey) {
                const category = currentSettings[pillTypeKey];
                const stringPillTextLower = String(pillTextLower);

                if (!category.allKnownValues.map(String).includes(stringPillTextLower)) {
                    category.allKnownValues.push(stringPillTextLower);
                    category.allKnownValues.sort();
                    settingsModifiedByNewValues = true;
                    console.log(`[Bumble Filter] New value discovered for '${category.label}': "${pillTextOriginal}"`);
                }

                let currentPillHighlightColor = HIGHLIGHT_COLOR_INDIFFERENT;
                let isNice = false;
                let isNoGo = false;

                if (category.type === "numberRange") {
                    const heightValue = parseCmValue(stringPillTextLower);
                    if (heightValue !== null) {
                        if (category.noGoSpecificCm && category.noGoSpecificCm.includes(heightValue)) {
                            isNoGo = true;
                        } else {
                            const minOk = category.desiredMinCm === null || heightValue >= category.desiredMinCm;
                            const maxOk = category.desiredMaxCm === null || heightValue <= category.desiredMaxCm;
                            if (minOk && maxOk && (category.desiredMinCm !== null || category.desiredMaxCm !== null)) {
                                isNice = true;
                            }
                        }
                    }
                } else {
                    isNice = category.desired.map(String).includes(stringPillTextLower);
                    isNoGo = category.noGo.map(String).includes(stringPillTextLower);
                }

                if (isNice) {
                    currentPillHighlightColor = HIGHLIGHT_COLOR_NICE;
                    cardMatchesAnyNiceCriteria = true;
                } else if (isNoGo) {
                    currentPillHighlightColor = HIGHLIGHT_COLOR_NO_GO;
                    cardHasAnyNoGo = true;
                }

                pillTextElement.style.backgroundColor = currentPillHighlightColor;
                pillTextElement.style.borderRadius = '5px';
                pillTextElement.style.padding = '2px 4px';
                pillTextElement.style.display = 'inline-block';
                pillTextElement.style.margin = '1px';
            }
        });

        if (settingsModifiedByNewValues) {
            const panel = document.getElementById('bumble-filter-settings-panel');
            if (panel && panel.style.display === 'block') {
                console.log("[Bumble Filter] New values discovered, refreshing panel content.");
                buildSettingsPanelContent(panel.querySelector('#bf-panel-content'));
            }
        }

        if (cardHasAnyNoGo) {
            // profileCard.style.border = `3px solid ${HIGHLIGHT_COLOR_NO_GO}`;
        } else if (cardMatchesAnyNiceCriteria) {
            // profileCard.style.border = `3px solid ${HIGHLIGHT_COLOR_NICE}`;
        }
    }

    function processVisibleProfileCards() {
        let processedSomethingOnThisRun = false;
        for (const selector of profileCardSelectors) {
            const cards = document.querySelectorAll(selector);
            if (cards.length > 0) {
                cards.forEach(card => {
                    if (!card.dataset.customFilterProcessedV9_0) { // Updated dataset version
                        logPills(card);
                        card.dataset.customFilterProcessedV9_0 = 'true';
                        processedSomethingOnThisRun = true;
                    }
                });
                if (processedSomethingOnThisRun) break;
            }
        }
    }

    const observer = new MutationObserver((mutationsList) => {
        let newNodesAddedInThisBatch = false;
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                newNodesAddedInThisBatch = true;
                break;
            }
        }
        if (newNodesAddedInThisBatch) {
            setTimeout(processVisibleProfileCards, 150);
        }
    });

    function init() {
        console.log("[Bumble Filter] Script started (v0.9.0 - Radio Buttons & Number Filter).");
        loadSettings();
        addStyles();

        if (!document.getElementById('bf-open-settings-button')) {
            const openButton = document.createElement('button');
            openButton.id = 'bf-open-settings-button';
            openButton.textContent = 'B';
            openButton.title = 'Bumble Filter Settings';
            document.body.appendChild(openButton);

            openButton.addEventListener('click', () => {
                let panel = document.getElementById('bumble-filter-settings-panel');
                if (!panel) {
                    panel = createSettingsPanelShell();
                }

                if (panel.style.display === 'block') {
                    panel.style.display = 'none';
                } else {
                    buildSettingsPanelContent(panel.querySelector('#bf-panel-content'));
                    panel.style.display = 'block';
                }
            });
        }

        let targetNode = document.querySelector('main.page__content') || document.querySelector('#main main') || document.body;
        if (targetNode) {
            processVisibleProfileCards();
            observer.observe(targetNode, { childList: true, subtree: true });
        } else {
            console.warn("[Bumble Filter] Main target node for observer not found.");
            const lateObserver = new MutationObserver(() => {
                targetNode = document.querySelector('main.page__content') || document.querySelector('#main main') || document.body;
                if (targetNode) {
                    lateObserver.disconnect();
                    processVisibleProfileCards();
                    observer.observe(targetNode, { childList: true, subtree: true });
                }
            });
            lateObserver.observe(document.body, { childList: true, subtree: true });
        }
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        init();
    } else {
        window.addEventListener('DOMContentLoaded', init);
    }

})();