Greasy Fork is available in English.

Wish.com - Price filter & more

Filtering by min/max price, allow hidding nsfw products, see reviews

// ==UserScript==
// @name         Wish.com - Price filter & more
// @namespace    http://tampermonkey.net/
// @version      5.4
// @description  Filtering by min/max price, allow hidding nsfw products, see reviews
// @author       Shuunen
// @match        https://*.wish.com/*
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    console.log('wish price filter : init');

    var $ = jQuery.noConflict(true);
    var minPrice = 0;
    var maxPrice = 1000;
    var minStars = parseInt(localStorage.abwMinStars) || 1;
    var hideNsfw = localStorage.abwHideNsfw !== 'false';
    var itemsPerBatch = 10
    var itemSelector = '[class^="FeedItemV2__Wrapper"]';
    var itemImageSelector = '[class^="FeedItemV2__Image"]';
    var itemPriceSelector = '[class^="FeedItemV2__ActualPrice"]';
    var itemDetailsSelector = '[class^="FeedItemV2__Row2"]';

    // Returns a function, that, as long as it continues to be invoked, will not
    // be triggered. The function will be called after it stops being called for
    // N milliseconds. If `immediate` is passed, trigger the function on the
    // leading edge, instead of the trailing.
    function debounce(func, wait, immediate) {
        var timeout;
        return function() {
            var context = this;
            var args = arguments;
            var later = function() {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    }

    function fetchData(id, productEl, originalPicture) {
        // console.log('will get data for', id);
        const url = 'https://www.wish.com/c/' + id;
        // console.log('getting data for', id);
        fetch(url)
            .then((r) => r.text())
            .then((html) => {
            let dataMatches = html.match(/"aggregateRating" : ([\w\W\n]+"\n}),/gi);
            let ratings = 0
            let count = 0
            if(dataMatches) {
                const dataStr = dataMatches[0];
                const data = JSON.parse('{' + dataStr.replace('},', '}').replace(/\n/g, '') + '}');
                ratings = Math.round(data.aggregateRating.ratingValue * 100) / 100;
                count = Math.round(data.aggregateRating.ratingCount);
            } else {
                dataMatches = html.match(/"product_rating": {"rating": (\d\.?\d+), "rating_count": (\d\.?\d+)/i);
                if(dataMatches.length === 3){
                    ratings = dataMatches[1]
                    count = dataMatches[2]
                } else {
                    dataMatches = null
                }
            }
            if(dataMatches) {
                ratings = Math.round(ratings * 100) / 100;
                count = Math.round(count);
            } else {
                debugger; //TODO: remove this
                throw "did not found ratings & count in wish page :" + url
            }
            // console.log(id, ': found a rating of', ratings, 'over', count, 'reviews :)');
            const nbStars = Math.round(ratings);
            let roundedRatings = Math.round(ratings);
            let ratingsStr = '';
            while (roundedRatings--) {
                ratingsStr += '<img class="abw-star" src="https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678064-star-20.png" />';
            }
            if (count > 0) {
                ratingsStr += '<hr> over ' + count + ' reviews';
            } else {
                ratingsStr = 'no reviews !';
            }
            productEl.find(itemDetailsSelector).css('display', 'flex').css('align-items', 'center').html(ratingsStr);
            const shippingMatches = html.match(/"localized_shipping":\s{"localized_value":\s(\d)/i);
            const shippingFees = parseInt(shippingMatches[1]);
            // console.log(id, ': shipping fees', shippingFees);
            const priceMatches = productEl.find(itemPriceSelector).text().match(/\d+/);
            const price = parseInt(priceMatches && priceMatches.length ? priceMatches[0] : 0);
            // console.log(id, ': base price', price);
            const totalPrice = shippingFees + price;
            const priceEl = productEl.find(itemPriceSelector);
            priceEl.html(totalPrice + ' ' + getCurrency());
            const nsfwMatches = html.match(/sex|lingerie|crotch|masturbat|vibrator|bdsm|bondage|nipple/gi);
            if (nsfwMatches && nsfwMatches.length) {
                // console.log(id, ': is NSFW');
                productEl.addClass('abw-nsfw');
            }
            showHideProduct(productEl, originalPicture, totalPrice, nbStars);
        })
            .catch((error) => {
            console.error('did not managed to found ratings for product "', id, '"', error);
        });
    }

    var currency = null;
    function getCurrency(){
        if(currency){
            return currency;
        }
        $(itemPriceSelector).each(function(index, el){
            if(currency) {
                return;
            }
            var arr = el.textContent.replace(/\d+/, '_').split('_');
            if(arr.length === 2){
                console.log('currency found at iteration', index);
                currency = arr[1];
            }
        });
        if(currency){
            console.log('detected currency :', currency);
        } else {
            console.warn('failed at detect currency, try to update "itemPriceSelector"');
        }
        return currency;
    }

    var loadedUrl = '//main.cdn.wish.com/fd9acde14ab5/img/ajax_loader_16.gif?v=13';

    function getData(element) {
        const productEl = $(element.tagName ? element : element.currentTarget);
        if (productEl.hasClass('abw-with-data')) {
            // console.log('product has already data');
            return;
        }
        productEl.addClass('abw-with-data');
        const image = productEl.find(itemImageSelector);
        if (!image || !image[0]) {
            console.error('did not found image on product', productEl);
            console.error('try to update "itemImageSelector"');
            return;
        }
        const href = productEl.attr('href');
        if (!href) {
            console.error('did not found href on product', productEl);
            return;
        }
        const id = href.split('/').reverse()[0];
        const originalPicture = image[0].style.backgroundImage;
        image[0].style.backgroundImage = 'url(' + loadedUrl + ')';
        image[0].style.backgroundSize = '10%';
        fetchData(id, productEl, originalPicture);
    }

    function getNextData() {
        let items = $(itemSelector + ':visible:not(.abw-with-data):lt(' + itemsPerBatch + ')');
        if(items.length){
            console.log('getting next', items.length, 'items data');
            items.each((index, element) => {
                setTimeout(() => getData(element), index * 300);
            });
        } else {
            console.log('found no items to parse, please review const "itemSelector"');
        }
    }

    function showHideProduct(element, originalPicture, totalPrice, nbStars) {
        var productEl = $(element);
        if(!totalPrice){
            totalPrice = productEl.find(itemPriceSelector).text().replace(/\D/g, '');
        }
        if(!nbStars){
            nbStars = productEl.find('img.abw-star').size();
        }
        var priceOk = totalPrice <= maxPrice;
        if (minPrice && minPrice > 0) {
            priceOk = priceOk && totalPrice >= minPrice;
            // console.log('min price',priceOk? '': 'NOT', 'passed');
        }
        if (priceOk && minStars && minStars > 0 && productEl.hasClass('abw-with-data')) {
            priceOk = nbStars >= minStars;
            // console.log('min stars',priceOk? '': 'NOT', 'passed');
            // console[(priceOk ? 'log':'error')](nbStars, '>=',minStars);
        }
        if (priceOk && hideNsfw && productEl.hasClass('abw-nsfw')) {
            priceOk = false;
            // console.log('nsfw',priceOk? '': 'NOT', 'passed');
        }
        if (originalPicture) {
            const image = productEl.find(itemImageSelector);
            if (!image || !image[0]) {
                console.error('did not found image on product', productEl);
                console.error('try to update "itemImageSelector"');
                return;
            }
            image[0].style.backgroundImage = originalPicture;
            image[0].style.backgroundSize = '100%';
        }
        if (priceOk) {
            productEl.show('fast');
            if (!productEl.hasClass('abw-on-hover')) {
                productEl.addClass('abw-on-hover');
                productEl.hover(getData);
            }
        } else {
            productEl.hide('fast');
        }
    }

    function showHideProducts(event) {
        console.log('wish price filter : showHideProducts');
        setTimeout(hideUseless, 100);
        setTimeout(getNextData, 100);
        $(itemSelector).each((index, element) => {
            showHideProduct(element);
        });
    }

    // prepare a debounced function
    var showHideProductsDebounced = debounce(showHideProducts, 1000);

    // activate when window is scrolled
    $('.feed-grid-scroll-container, .search-grid-scroll-container').scroll(showHideProductsDebounced);

    function hideUseless() {
        // hide products that can't be rated in order hsitory
        $('.transaction-expanded-row-item .rate-button').parents('.transaction-expanded-row-item').addClass('abw-has-rate');
        $('.transaction-expanded-row-item:not(.abw-has-rate)').remove();
        // delete useless marketing stuff
        $('[class^="FeedItemV2__UrgencyInventory"], [class^="FeedItemV2__AuthorizedBrand"], [class^="FeedItemV2__ProductBoost"], [class^="FeedItemV2__CrossedPrice"]').remove();
        // delete fake discount
        $('[class^="FeedItemV2__DiscountBanner"]').remove();
        // delete wish express
        $('[class^="SearchPage__WishExpressRowContainer"]').remove();
        // delete sms reminders
        $('.transaction-opt-in-banner, .sms-div, .sms-notification-request').remove()
        // delete tab bar, footer
        $('[class^="TabBarV2__Wrapper"], [class^="FooterV2__Wrapper"]').remove();
    }

    setTimeout(hideUseless, 100);

    var html = '<div id="wish_tweaks_config" style="float:left; white-space: nowrap; margin-right:10px;display:flex;justify-content:space-between;align-items:center; font-size: 14px; margin-left: 15px;">';
    html += 'Min / Max Price : <input id="wtc_min_price" type="text" style="width: 30px; text-align: center; margin-left: 5px;">&nbsp;/<input id="wtc_max_price" type="text" style="width: 30px; text-align: center; margin-left: 5px; margin-right: 10px;">';
    html += 'Min stars : <input id="wtc_min_stars" type="number" min="0" max="5" style="width: 40px; text-align: center; margin: 0 5px;">&nbsp;';
    html += 'Hide nsfw : <input id="wtc_hide_nsfw" type="checkbox" checked style="margin: 0; height: 16px; width: 16px; margin: 0 5px;">';
    html += '</div>';

    if ($('#header-left').length) {
        // insert controllers in v1 header
        $('#mobile-app-buttons').remove();
        $('#nav-search-input-wrapper').width(320);
        $('#header-left').after(html);
    } else if ($('.left.feed-v2').length) {
        // insert controllers in v2 header
        $('.left.feed-v2').before(html);
    } else if ($('[class^="Navbar__Left"]').length) {
        // insert controllers in v3 header
        $('[class^="Navbar__Left"]').html(html);
        $('[class^="NavbarCartAndModalPages__Wrapper"]').css('paddingBottom',0)
    } else if ($('[class^="NavbarV2__Left"]').length) {
        // insert controllers in v4 header
        $('[class^="NavbarV2__Left"]').after(html);
    } else {
        console.error('failed at inserting controllers')
    }

    // get elements
    var hideNsfwCheckbox = $('#wtc_hide_nsfw');
    var minStarsInput = $('#wtc_min_stars');
    var minPriceInput = $('#wtc_min_price');
    var maxPriceInput = $('#wtc_max_price');

    // restore previous choices
    hideNsfwCheckbox.attr('checked', hideNsfw);
    minStarsInput.val(minStars);

    // start filtering by default
    setTimeout(() => {
        showHideProductsDebounced();
        getNextData();
    }, 1000);

    // when input value change
    hideNsfwCheckbox.change((event) => {
        hideNsfw = event.currentTarget.checked;
        localStorage.abwHideNsfw = hideNsfw;
        // console.log('hideNsfw is now', hideNsfw);
        showHideProductsDebounced();
    });
    minPriceInput.change((event) => {
        minPrice = parseInt(event.currentTarget.value) || 0;
        // console.log('minPrice is now', minPrice);
        showHideProductsDebounced();
    });
    maxPriceInput.change((event) => {
        maxPrice = parseInt(event.currentTarget.value) || 1000;
        // console.log('maxPrice is now', maxPrice);
        showHideProductsDebounced();
    });
    minStarsInput.change((event) => {
        minStars = parseInt(event.currentTarget.value);
        localStorage.abwMinStars = minStars;
        // console.log('minStars is now', minStars);
        showHideProductsDebounced();
    });

})();