Greasy Fork

IcePetsPlus

Some minor QOL improvements for icepets

// ==UserScript==
// @name         IcePetsPlus
// @namespace
// @version      1.0
// @description  Some minor QOL improvements for icepets
// @author       Cullen
// @match        https://www.icepets.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=icepets.com
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/428778
// ==/UserScript==
(function() {
    'use strict';

    const globals = {
        sortAsc: false
    }

    // RUN THIS THROUGH THIS SOMETIMES
    // https://beautifier.io/

    GM_addStyle(`
		input:not([type='text']), button {
			padding: 5px 10px;
			margin: 1px;
			cursor: pointer;
		}

		.copyTooltip {
		  position: fixed;
		  z-index: 1000;
		  padding: 5px 10px;
		  border: 1px solid #111;
		  background: #f2f2f5;
		  font-size: 12px;
		  font-family: verdana;
		}

		.tableheader td {
			cursor:pointer;
		}
	`);

    const routes = () => {
        // Globals
        setupToolstips()

        // Routed
        const path = window.location.pathname;
        const search = window.location.search;
        switch (path) {
            case '/halipar-jungle/companion-reserve':
            case '/snowslide-mountains/snow-jar-igloo':
            case '/glacia/page-turners':
            case '/glacia/frozen-collectives-emporium':
            case '/glacia/post-office':
            case '/glacia/plushie-palace':
            case '/glacia/toy-trunk':
            case '/misty-isle/grooming-parlour':
            case '/glacia/glacial-grocer':
            case '/misty-isle/battle-shop':
            case '/snowslide-mountains/sugar-rush':
            case '/snowslide-mountains/affogato':
            case '/misty-isle/golden-touch':
                setupShopPage();
                break;
            case '/halipar-jungle/collectors-quest':
            case '/quests/collect.php':
                setupCollectorPage();
                break;
            case '/usershops.php':
                search == '?stock' ? setupShopStockPage() : null;
                break;
            case '/arcade/gs.php':
                setupSlotsPage();
                break;
            default:
                break;
        }
    }

    // Notes/weirdthings in comments below

    /*
    const apiURL = "https://new.icepets.com/api/v1/search/items?item=&category=Companion&perPage=9999";

    fetch(apiURL)
      .then(response => response.json())
      .then(data => {
    	const filteredObjects = data.data.map(obj => !obj.slug.includes("-") ? obj.name : null).filter(x=>x!=null);
    	console.log(filteredObjects);
      })
      .catch(error => {
    	console.error("Error:", error);
      });

    */

    // https://www.icepets.com/snowslide-mountains/found-snow-jar

    const setupCollectorPage = () => {
        console.log('setupCollectorPage')
        document.querySelectorAll('table table tbody td img').forEach(questItem => {
            const substrStart = questItem.src.indexOf('/items/') + 7;
            const substrEnd = questItem.src.length - 4

            const itemId = questItem.src.substring(substrStart, substrEnd);

            const button = document.createElement('button')
            button.style.transition = "background-color 0.2s ease-out";
            button.style.width = '90px';
            button.style.margin = '1px'
            button.innerHTML = 'Buy';
            button.addEventListener('click', function() {
                button.innerHTML = 'Checking...';
                _buyScrapItem(itemId, button);
            });

            questItem.parentElement.append(button)
        })
    }

    const setupShopPage = () => {
        console.log('setupShopPage')
        const timerCountdown = () => {
            const timer = document.querySelector('b')
            const time = timer.innerText.split(' seconds')[0]
            let timeInSeconds = time.indexOf(':') != -1 ? Number(time.split(':')[0] * 60) + Number(time.split(':')[1]) : time;
            timeInSeconds--;
            const minutes = Math.floor(timeInSeconds / 60);
            const seconds = (timeInSeconds % 60).toLocaleString('en-US', {
                minimumIntegerDigits: 2,
                useGrouping: false
            })
            if (timeInSeconds == -1) return
            timer.innerHTML = minutes == 0 ? `${seconds} seconds` : `${minutes}:${seconds} seconds`
            // Wtf workers make timeouts work even in inactive tabs
            // https://stackoverflow.com/questions/5927284/how-can-i-make-setinterval-also-work-when-a-tab-is-inactive-in-chrome/5927432#12522580
            const blob = new Blob(["setTimeout(function(){postMessage('')}, 1000)"])
            const worker = new Worker(window.URL.createObjectURL(blob))
            worker.onmessage = timerCountdown;
        }

        timerCountdown()
    }

    const setupShopStockPage = () => {
        console.log('setupShopStockPage')
        // Copy "Update Stock" button to the top

        const updateButtonCopy = document.querySelector('.submitbutton').cloneNode(true);
        document.querySelector('.submitbutton').parentElement.prepend(updateButtonCopy)
        updateButtonCopy.after(document.createElement('br'))
        updateButtonCopy.after(document.createElement('br'))

        // Add sorting to the stock table
        // https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript/49041392#49041392
        const headers = document.querySelectorAll('tr.tableheader td');
        headers.forEach(th => th.addEventListener('click', (() => _sortTableByHeader(th, headers))));

        // Sort manually the first time to sort price low to high
        const th = document.querySelectorAll('tr.tableheader td')[2];
        _sortTableByHeader(th, headers);
    }

    const setupSlotsPage = () => {
        const verify = document.querySelector('input[name="gs_verify"]')

        if (verify) verify.focus();
        document.body.addEventListener('keyup', (e) => {
            if (e.key == 'Enter') document.querySelector('input[name="submit"]').click()
        })

    }

    const setupToolstips = () => {
        if (['/news.php', '/newboards/viewposts.php'] // Skip these
            .indexOf(window.location.pathname) != -1) return

        console.log('setupToolstips')
        GM_addStyle(`
			img[src*="images/items"] {
				cursor: pointer;
			}
		`);
        Array.from(document.querySelectorAll(':not(a) img[src*="images/items"]'))
            .filter(item => item.parentElement.tagName != 'A')
            .forEach(shopItem => {
                const itemName = _getItemNameFromNode(shopItem);
                shopItem.addEventListener('click', e => {
                    navigator.clipboard.writeText(itemName);
                    _addTooltip(shopItem, 'Copied item name!', e)
                });
            })
    }

    const _addTooltip = (element, text, e, speed) => {
        // Create a tooltip element.
        var tooltip = document.createElement('div');
        tooltip.textContent = text;
        tooltip.className = 'copyTooltip'
        tooltip.style.top = (e.clientY + 20) + "px";
        tooltip.style.left = (e.clientX + 20) + "px"

        // Add the tooltip to the DOM.
        element.parentNode.prepend(tooltip);
        setTimeout(() => _removeFadeOut(tooltip, speed ?? 500), 1000);
    }

    const _buyScrapItem = (itemId, button, price) => {
        const XHR = new XMLHttpRequest();
        const FD = new FormData();
        const dataFormat = {
            offer_price: price ?? '1618',
            action_buy: 'Buy Another'
        }

        for (const [name, value] of Object.entries(dataFormat)) {
            FD.append(name, value);
        }

        XHR.addEventListener("load", (event) => {
            //console.log("Yeah! Data sent and response loaded.", event);
        });

        XHR.addEventListener("error", (event) => {
            _setButtonSuccess(false, 'Oops! Error', button)
        });

        XHR.onreadystatechange = () => {
            if (XHR.readyState == 4) {
                if (XHR.status == 200) {
                    // The request was successful
                    if (XHR.responseText.indexOf('The item you were purchasing is no longer available')) {
                        _setButtonSuccess(true, 'Bought! :)', button)
                    } else {
                        _setButtonSuccess(false, 'Nope :(', button)
                    }
                } else {
                    _setButtonSuccess(false, 'Oops! Error', button)
                }
                return false
            }
        };

        XHR.open("POST", `https://www.icepets.com/scrapshop/index.php?act=buyitem&item=${itemId}`);

        XHR.send(FD);
    }

    const _checkAllWordsStartWithCapitalLetter = (str) => {
        const words = str?.split(' ');
        if (!words) return false
        for (const word of words) {
            if (word[0]?.toUpperCase() !== word[0]) {
                return false;
            }
        }
        return true;
    }

    const _checkTokenDabuRewards = (shopItem) => {
        if (!shopItem?.parentElement?.childNodes) return undefined
        return Array.from(shopItem?.parentElement?.childNodes)
            .filter(node => node?.tagName == "STRONG" &&
                _checkAllWordsStartWithCapitalLetter(node?.textContent) &&
                node?.children?.length == 0 &&
                node?.textContent?.indexOf(':') == -1
            )[0]?.textContent
    }

    const _checkNovitariaQuest = (shopItem) => {
        if (!shopItem?.parentElement?.childNodes) return undefined
        return Array.from(shopItem?.parentElement?.childNodes)
            .filter(node => node?.wholeText && node?.textContent.length > 2 && node?.textContent?.length < 100)[0]
            .textContent
    }

    const _compareSortingValues = (v1, v2) => {
        const validVersions = v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)
        return validVersions ? v1 - v2 : v1.toString().localeCompare(v2)
    }

    const _getCellValue = (tr, idx) => {
        return tr.children[idx].children[0]?.value || // input fields
            tr.children[idx]?.innerText || // text from first node
            tr.children[idx]?.textContent; // text from all child nodes
    }

    const _getItemNameFromNode = (shopItem) => {
        return shopItem?.parentElement?.querySelector('b')?.innerHTML ?? // NPC Shops
            _checkTokenDabuRewards(shopItem) ?? // Token Dabu reward
            shopItem?.parentElement?.querySelectorAll('strong')?.[2]?.innerHTML ?? // Beauty King
            shopItem?.parentElement?.querySelector('strong')?.innerHTML ?? // Collector Quest, Shop Purchase Confirmation, Cube Grab Main Page, Storage
            (shopItem?.alt.length > 0 ? shopItem?.alt : null) ?? // Solitary Sprite
            _checkNovitariaQuest(shopItem) ?? // Novitaria Quest
            shopItem?.parentElement?.lastChild?.textContent ?? // Scratchcard prizes, Shop Stock
            ''; // Uhhhhh
    }

    const _removeFadeOut = (el, speed) => {
        var seconds = speed / 1000;
        el.style.transition = "opacity " + seconds + "s ease";

        el.style.opacity = 0;
        setTimeout(function() {
            el.parentNode.removeChild(el);
        }, speed);
    }

    const _setButtonSuccess = (success, message, button) => {
        button.innerHTML = message;
        button.style.backgroundColor = success ? '#8fdf96' : '#df8f8f';

        setTimeout(() => {
            button.style.backgroundColor = '#d6e7ff';
            button.innerHTML = 'Buy';
        }, 1250)
    }

    const _sortTableByHeader = (th, headers) => {
        const table = th.closest('table');
        const tableRows = Array.from(th.parentNode.children);
        const thIndex = tableRows.indexOf(th)

        headers.forEach(th2 => {
            th2.innerHTML = th2.innerHTML.replace(' ↓', '').replace(' ↑', '')
        })
        th.innerHTML += globals.sortAsc ? ' ↑' : ' ↓';

        Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
            .sort(_sortTableSortHandler(thIndex))
            .forEach(tr => table.appendChild(tr));
    }

    const _sortTableSortHandler = (idx) => {
        globals.sortAsc = !globals.sortAsc;
		// .sort((a, b) => yourFunctionBlock)
        return (a, b) => {
            return _compareSortingValues(_getCellValue(globals.sortAsc ? a : b, idx), _getCellValue(globals.sortAsc ? b : a, idx));
        }
    }

    routes()
})();