nascent / PSPrices Subscriptions Filter - Toggle Visibility of Unique, Rare and Common Deals

// ==UserScript==
// @name         PSPrices Subscriptions Filter - Toggle Visibility of Unique, Rare and Common Deals
// @namespace    https://psprices.com
// @version      0.74
// @description  Toggle showing games during their Lowest or New Lowest deals, and filter Mass-release publisher games
// @author       nascent
// @match        https://psprices.com/my/subscriptions/*
// @match        https://psprices.com/region-*/collection/*
// @match        https://psprices.com/region-*/genre/*
// @match        https://psprices.com/region-*/index
// @match        https://psprices.com/my/wishlist/*
// @exclude      *q=*
// @updateURL    https://openuserjs.org/meta/nascent/PSPrices_Subscriptions_Filter_-_Toggle_Visibility_of_Unique,_Rare_and_Common_Deals.meta.js
// @downloadURL  https://openuserjs.org/install/nascent/PSPrices_Subscriptions_Filter_-_Toggle_Visibility_of_Unique,_Rare_and_Common_Deals.user.js
// @license      GPL-3.0-or-later
// @icon         https://www.google.com/s2/favicons?domain=psprices.com
// @grant        GM.setValue
// @grant        GM.getValue
// ==/UserScript==

(function () {
    'use strict';

    let hideLowest = false;
    let hideOtherDeals = false;
    let hideMassRelease = false;

    const MASS_RELEASE_SELECTOR = 'button[title="Mass-release publisher"]';
    const LOWEST_BADGE_SELECTOR = '[data-test-id="lowest-flag"]';
    const NEW_LOWEST_BADGE_SELECTOR = '[data-test-id="lowest-ever-flag"]';

    const HIDE_LOWEST_CLASS = 'psprices-hidden-lowest';
    const HIDE_MODERATE_CLASS = 'psprices-hidden-moderate';
    const HIDE_MASS_RELEASE_CLASS = 'psprices-hidden-mass-release';

    const style = document.createElement('style');
    style.innerHTML = `
        .${HIDE_LOWEST_CLASS},
        .${HIDE_MODERATE_CLASS},
        .${HIDE_MASS_RELEASE_CLASS} {
            display: none !important;
        }
        .psprices-filter-btn {
            transition: all 0.2s ease;
            white-space: nowrap;
        }
    `;
    document.head.appendChild(style);

    function hasLowestBadge(div) {
        return div.querySelector(LOWEST_BADGE_SELECTOR) !== null;
    }

    function hasNewLowestBadge(div) {
        return div.querySelector(NEW_LOWEST_BADGE_SELECTOR) !== null;
    }

    function isSpecialDeal(div) {
        return hasLowestBadge(div) || hasNewLowestBadge(div);
    }

    // Find the game card container
    function getGameCard(element) {
        let card = element.closest('div.game-fragment');
        if (!card) {
            card = element.closest('div[class*="col-span-"]');
        }
        return card;
    }

    // Climb up from the game-fragment to the outermost element that
    // exclusively wraps this card (e.g. the <div class=""> grid cell on
    // the index page). Hiding that element removes the blank gap.
    function getHideTarget(card) {
        let target = card;
        while (
            target.parentElement &&
            target.parentElement !== document.body &&
            target.parentElement.tagName === 'DIV' &&
            target.parentElement.childElementCount === 1
        ) {
            target = target.parentElement;
        }
        return target;
    }

    function setHidden(card, className, shouldHide) {
        const target = getHideTarget(card);
        target.classList.toggle(className, shouldHide);
    }

    function updateLowestVisibility() {
        document.querySelectorAll('div.game-fragment').forEach(div => {
            setHidden(div, HIDE_LOWEST_CLASS, hideLowest && hasLowestBadge(div));
        });
    }

    function updateOtherDealsVisibility() {
        document.querySelectorAll('div.game-fragment').forEach(div => {
            setHidden(div, HIDE_MODERATE_CLASS, hideOtherDeals && !isSpecialDeal(div));
        });
    }

    function updateMassReleaseVisibility() {
        document.querySelectorAll('div.game-fragment').forEach(div => {
            const isMassRelease = div.querySelector(MASS_RELEASE_SELECTOR) !== null;
            setHidden(div, HIDE_MASS_RELEASE_CLASS, hideMassRelease && isMassRelease);
        });
    }

    function updateButtonState(button, isActive) {
        button.style.opacity = isActive ? '0.7' : '1';
        button.style.filter = isActive ? 'grayscale(50%)' : 'none';
    }

    function createExpandButton(shortText, fullText, bgColor, top) {
        const button = document.createElement('button');
        button.innerHTML = shortText;
        button.className = 'psprices-filter-btn';
        button.style.position = 'fixed';
        button.style.top = top;
        button.style.right = '10px';
        button.style.zIndex = '10000';
        button.style.padding = '10px 15px';
        button.style.backgroundColor = bgColor;
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';

        button.addEventListener('mouseenter', () => {
            button.innerHTML = fullText;
        });
        button.addEventListener('mouseleave', () => {
            button.innerHTML = shortText;
        });

        return button;
    }

    const lowestButton = createExpandButton('Lowest', 'Toggle Best Price Deals', '#4CAF50', '120px');
    const moderateButton = createExpandButton('Moderate', 'Toggle Moderate Deals', '#f44336', '170px');
    const massReleaseButton = createExpandButton('Mass Release', 'Toggle Mass-release Publisher', '#FF9800', '220px');

    document.body.appendChild(lowestButton);
    document.body.appendChild(moderateButton);
    document.body.appendChild(massReleaseButton);

    lowestButton.addEventListener('click', async () => {
        hideLowest = !hideLowest;
        await GM.setValue('hideLowest', hideLowest);
        updateButtonState(lowestButton, hideLowest);
        updateLowestVisibility();
    });

    moderateButton.addEventListener('click', async () => {
        hideOtherDeals = !hideOtherDeals;
        await GM.setValue('hideOtherDeals', hideOtherDeals);
        updateButtonState(moderateButton, hideOtherDeals);
        updateOtherDealsVisibility();
    });

    massReleaseButton.addEventListener('click', async () => {
        hideMassRelease = !hideMassRelease;
        await GM.setValue('hideMassRelease', hideMassRelease);
        updateButtonState(massReleaseButton, hideMassRelease);
        updateMassReleaseVisibility();
    });

    async function initialize() {
        hideLowest = await GM.getValue('hideLowest', false);
        hideOtherDeals = await GM.getValue('hideOtherDeals', false);
        hideMassRelease = await GM.getValue('hideMassRelease', false);

        updateButtonState(lowestButton, hideLowest);
        updateButtonState(moderateButton, hideOtherDeals);
        updateButtonState(massReleaseButton, hideMassRelease);

        updateLowestVisibility();
        updateOtherDealsVisibility();
        updateMassReleaseVisibility();
    }

    initialize();

    let timeout;
    const observer = new MutationObserver((mutations) => {
        const hasAddedNodes = mutations.some(m => m.addedNodes.length > 0);
        if (hasAddedNodes) {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                updateLowestVisibility();
                updateOtherDealsVisibility();
                updateMassReleaseVisibility();
            }, 100);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();