Stripchat VR Filter

Filters VR models on Stripchat

// ==UserScript==
// @name         Stripchat VR Filter
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Filters VR models on Stripchat
// @author       Sergi0
// @match        https://*.stripchat.com/*
// @grant        GM_addStyle
// @icon         https://stripchat.com/favicon.ico
// @license      MIT
// @run-at       document-idle
// @homepageURL  https://greasyfork.org/es/scripts/538673-stripchat-vr-filter
// @supportURL   https://greasyfork.org/es/scripts/538673-stripchat-vr-filter/feedback

// ==/UserScript==

(function() {
    'use strict';

    let vrFilterEnabled = false;
    let observer = null;
    let currentPageUrl = window.location.href;

    // Function to apply the VR filter
    function applyVrFilter() {
        const models = document.querySelectorAll('.model-list-item');
        models.forEach(model => {
            const vrBadge = model.querySelector('span[class*="ModelListItemBadge__vr"]');
            const hasVrBadge = !!vrBadge;

            if (vrFilterEnabled) {
                if (hasVrBadge) {
                    // Show VR models
                    model.style.setProperty('display', 'block', 'important');
                } else {
                    // Hide non-VR models
                    model.style.setProperty('display', 'none', 'important');
                }
            } else {
                // If filter is disabled, show all models
                model.style.setProperty('display', 'block', 'important'); // Ensure all models are visible
            }
        });
    }

    // Function to create and add the toggle to the DOM
    function addVrToggleButton() {
        const existingToggle = document.getElementById('vrFilterToggleContainer');
        if (existingToggle) {
            existingToggle.remove();
        }

        const toggleContainer = document.createElement('div');
        toggleContainer.id = 'vrFilterToggleContainer';
        toggleContainer.style.position = 'fixed';
        toggleContainer.style.bottom = '10px';
        toggleContainer.style.right = '10px';
        toggleContainer.style.zIndex = '9999';
        toggleContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        toggleContainer.style.color = 'white';
        toggleContainer.style.padding = '10px 15px';
        toggleContainer.style.borderRadius = '5px';
        toggleContainer.style.fontFamily = 'Arial, sans-serif';
        toggleContainer.style.display = 'flex';
        toggleContainer.style.alignItems = 'center';
        toggleContainer.style.gap = '10px';
        toggleContainer.style.cursor = 'pointer';

        const label = document.createElement('span');
        label.textContent = 'Show VR Models Only';
        label.style.marginRight = '10px';
        label.style.whiteSpace = 'nowrap';

        const slider = document.createElement('label');
        slider.className = 'switch';

        const input = document.createElement('input');
        input.type = 'checkbox';
        input.id = 'vrFilterCheckbox';
        input.checked = vrFilterEnabled;

        input.addEventListener('change', (event) => {
            vrFilterEnabled = event.target.checked;
            applyVrFilter();
            localStorage.setItem('vrFilterEnabled', vrFilterEnabled);
        });

        const spanSlider = document.createElement('span');
        spanSlider.className = 'slider round';

        slider.appendChild(input);
        slider.appendChild(spanSlider);

        toggleContainer.appendChild(label);
        toggleContainer.appendChild(slider);

        document.body.appendChild(toggleContainer);
    }

    // Add styles for the toggle
    GM_addStyle(`
        .switch {
            position: relative;
            display: inline-block;
            width: 40px;
            height: 24px;
        }

        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            -webkit-transition: .4s;
            transition: .4s;
        }

        .slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            -webkit-transition: .4s;
            transition: .4s;
        }

        input:checked + .slider {
            background-color: #2196F3;
        }

        input:focus + .slider {
            box-shadow: 0 0 1px #2196F3;
        }

        input:checked + .slider:before {
            -webkit-transform: translateX(16px);
            -ms-transform: translateX(16px);
            transform: translateX(16px);
        }

        /* Rounded sliders */
        .slider.round {
            border-radius: 24px;
        }

        .slider.round:before {
            border-radius: 50%;
        }
    `);

    // Mutation Observer to detect DOM changes and URL changes
    function startObserver() {
        if (observer) {
            observer.disconnect();
        }

        const targetNode = document.querySelector('.js-list-content') || document.body;

        observer = new MutationObserver(function(mutations) {
            let modelsChangeDetected = false;
            let urlChanged = (window.location.href !== currentPageUrl);

            // Check for added nodes that are model-list-item or contain them
            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    if (mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                // Check if the added node itself is a model-list-item or contains one
                                if (node.classList.contains('model-list-item') || node.querySelector('.model-list-item')) {
                                    modelsChangeDetected = true;
                                    break;
                                }
                            }
                        }
                    }
                    // Also check if existing model-list-items had their 'style' or 'class' attributes changed.
                    // This is less common for "showing/hiding" issues, but good for robustness.
                } else if (mutation.type === 'attributes' && mutation.target.classList.contains('model-list-item')) {
                    modelsChangeDetected = true;
                }

                if (modelsChangeDetected) break;
            }

            // If URL changed, re-initialize everything (likely a new page load)
            if (urlChanged) {
                currentPageUrl = window.location.href;
                console.log('URL changed, re-initializing script.'); // Debug
                initializeScript();
            } else if (modelsChangeDetected) {
                // If models were added/changed (e.g., infinite scroll or dynamic update), apply filter
                console.log('Models changed, applying filter.'); // Debug
                setTimeout(applyVrFilter, 100); // Increased delay slightly
            }
        });

        // Observe changes on the targetNode: child additions/removals and attribute changes
        // Use subtree: true for deep observation
        observer.observe(targetNode, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] });
    }

    // Initialize the script
    function initializeScript() {
        const savedState = localStorage.getItem('vrFilterEnabled');
        if (savedState !== null) {
            vrFilterEnabled = savedState === 'true';
        } else {
            vrFilterEnabled = false; // Filter is OFF by default
        }

        addVrToggleButton();
        // Apply filter after a slightly longer delay on initial load/re-init
        setTimeout(applyVrFilter, 200); // Increased initial delay
        startObserver();
    }

    // Execute initialization when the DOM is fully loaded, with an initial delay
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(initializeScript, 200));
    } else {
        setTimeout(initializeScript, 200);
    }

    // Listen for SPA navigation events
    window.addEventListener('popstate', () => {
        console.log('Popstate event detected, re-initializing script.'); // Debug
        setTimeout(initializeScript, 200);
    });
    window.addEventListener('hashchange', () => {
        console.log('Hashchange event detected, re-initializing script.'); // Debug
        setTimeout(initializeScript, 200);
    });

    // Also consider using a custom event listener if Stripchat dispatches one
    // when content is fully loaded after an SPA navigation. This is site-specific.
    // For now, the combination of URL changes and MutationObserver should cover most cases.

})();