Bundle Helper

Add tools for many bundle sites. Modified from https://greatest.deepsurf.us/scripts/16105-bundle-helper/

Version vom 12.06.2020. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name            Bundle Helper
// @version         2.0.3
// @author          Dillon Regimbal
// @namespace       https://dillonr.com
// @description     Add tools for many bundle sites. Modified from https://greatest.deepsurf.us/scripts/16105-bundle-helper/
// @match           *://cubicbundle.com/*
// @match           *://dailyindiegame.com/*
// @match           *://forums.steampowered.com/forums/showthread.php?*
// @match           *://www.gogobundle.com/latest/bundles/*
// @match           *://otakumaker.com/*
// @match           *://www.otakumaker.com/*
// @match           *://otakubundle.com/latest/bundles/*
// @match           *://steamcommunity.com/*/home*
// @match           *://steamcommunity.com/groups/*/announcements*
// @match           *://steamcompanion.com/gifts/*
// @match           *://steamground.com/*
// @match           *://store.steampowered.com/
// @match           *://store.steampowered.com/account/notinterested/*
// @match           *://store.steampowered.com/app/*
// @match           *://store.steampowered.com/widget/*
// @match           *://store.steampowered.com/search/*
// @match           *://whosgamingnow.net/*
// @match           *://www.bunchkeys.com/*
// @match           *://www.bundlekings.com/*
// @match           *://www.fanatical.com/*
// @match           *://www.dailyindiegame.com/*
// @match           *://www.gamebundle.com/*
// @match           *://www.hrkgame.com/*
// @match           *://www.humblebundle.com/*
// @match           *://www.indiegala.com/*
// @match           *://www.orlygift.com/*
// @match           *://www.reddit.com/r/*/comments/*
// @match           *://www.superduperbundle.com/*
// @match           *://www.sgtools.info/*
// @match           *://steamkeys.ovh/*
// @match           *://steamdb.info/*
// @match           *://itch.io/*
// @match           *://*.itch.io/*
// @run-at          document-start
// @grant           GM_addStyle
// @grant           GM_xmlhttpRequest
// @grant           GM_getValue
// @grant           GM_setValue
// @connect         store.steampowered.com
// @connect         www.hrkgame.com
// @connect         www.fanatical.com
// @connect         www.steamgifts.com
// @icon            https://store.steampowered.com/favicon.ico
// @license         GPL-3.0-only
// @noframes
// ==/UserScript==

// Connect to store.steampowered.com to get owner info
// Connect to www.hrkgame.com and www.fanatical.com to get Steam ID of each products
// Connect to www.steamgifts.com to get bundle threads

// License: GPL-3.0-only - https://spdx.org/licenses/GPL-3.0-only.html

// Since 2016-01-10
// https://greatest.deepsurf.us/scripts/16105-bundle-helper/

// Since 2020-05-21
// https://greatest.deepsurf.us/scripts/403878-bundle-helper
// https://github.com/dregimbal/UserScripts/blob/master/BundleHelper.user.js

(function () {
    'use strict'

    let write_console_messages = true

    let name_profile_json = 'bh_profile_json'
    let name_profile_time = 'bh_profile_time'
    let owned_item_class = 'bh_owned'
    let steam_profile = getSteamProfile()

    let default_steam_url_selector = 'a[href*=\'store.steampowered.com/\']'

    let divButton = document.createElement('div')

    function attachOnLoad(callback) {
        window.addEventListener('load', function (e) {
            callback()
        })
    }

    function attachOnReady(callback) {
        document.addEventListener('DOMContentLoaded', function (e) {
            callback()
        })
    }

    function writeConsoleMessage(message) {
        if (write_console_messages) {
            console.log(message)
        }
    }

    let timeoutList = []
    let intervalList = []

    function setTimeoutCustom(func, tm, params) {
        let id = setTimeout(func, tm, params)
        timeoutList.push(id)
        return id
    }

    function clearTimeoutAll() {
        for (let i = 0; i < timeoutList.length; i++) {
            clearTimeout(timeoutList[i])
        }
    }

    function clearIntervalAll() {
        for (let i = 0; i < intervalList.length; i++) {
            clearInterval(intervalList[i])
        }
    }

    function getUnixTimestamp() {
        return parseInt(Date.now() / 1000)
    }

    function isProfileCacheExpired() {
        let isExpired = false
        let timestampExpired = 15 * 60

        let profileTimestamp = GM_getValue(name_profile_time, 0)

        let profileTimestampDiff = getUnixTimestamp() - profileTimestamp
        if (profileTimestampDiff > timestampExpired) {
            isExpired = true
        }

        if (!isExpired) {
            writeConsoleMessage('Profile Cache Updated ' + profileTimestampDiff + 's ago')
        } else {
            writeConsoleMessage('Profile Cache Expired: ' + profileTimestampDiff)
        }

        return isExpired
    }

    function setProfileCache(json) {
        GM_setValue(name_profile_json, json)
        GM_setValue(name_profile_time, getUnixTimestamp())
    }

    function getSteamProfile() {
        if (isProfileCacheExpired()) {
            updateSteamProfileCache()
        }
        return GM_getValue(name_profile_json, 0)
    }

    function markOwned(query, getElementCallback, getProductIdCallback
        , classOwned, classNotInterested, classWished, getCountCallback) {
        if (!document.querySelector(query)) {
            // writeConsoleMessage("markOwned: empty");
            return
        }

        if (!getElementCallback) {
            getElementCallback = function (ele, type) {
                // type -> 1: Owned, 2: Ignored, 3: Wishlist
                return ele
            }
        }

        if (!getProductIdCallback) {
            getProductIdCallback = function (ele) {
                return ele.getAttribute('href')
            }
        }

        if (!getCountCallback) {
            getCountCallback = function (appCount, subCount, appOwned, subOwned) {
            }
        }

        if (!classOwned) {
            classOwned = ''
        }
        if (!classNotInterested) {
            classNotInterested = ''
        }
        if (!classWished) {
            classWished = ''
        }

        let rgxId = /[0-9]{3,}/g
        let rgxApp = /((:\/\/(store\.steampowered\.com|steamcommunity\.com|steamdb\.info)(\/agecheck)?\/app|\/steam\/apps)\/[0-9]+|^[0-9]{3,}$)/i
        let rgxSub = /(:\/\/(store\.steampowered\.com|steamdb\.info)\/sub|\/steam\/subs)\/[0-9]+/i

        let markFromJson = function (dataRes) {
            if (!dataRes) {
                writeConsoleMessage('markFromJson: empty')
                return
            }

            let countOwned = [0, 0]
            let countAll = [0, 0]

            let eleApps = document.querySelectorAll(query)
            writeConsoleMessage(eleApps)
            for (let i = 0; i < eleApps.length; i++) {
                let attrHref = getProductIdCallback(eleApps[i])
                let ids = attrHref.match(rgxId)
                if (ids) {
                    // writeConsoleMessage('Matched ID "' + ids[0] + '" from url: ' + attrHref)
                    let valId = parseInt(ids[0])
                    if (rgxApp.test(attrHref)) {
                        if (isAppOwned(valId)) {
                            let ele = getElementCallback(eleApps[i], 1)
                            if (ele && classOwned !== '') {
                                ele.classList.add(classOwned)
                            }
                            countOwned[0]++
                        } else if (isAppWishlisted(valId)) {
                            let ele = getElementCallback(eleApps[i], 3)
                            if (ele && classWished !== '') {
                                ele.classList.add(classWished)
                            }
                        } else if (isAppIgnored(valId)) {
                            let ele = getElementCallback(eleApps[i], 2)
                            if (ele && classNotInterested !== '') {
                                ele.classList.add(classNotInterested)
                            }
                        } else {
                            // writeConsoleMessage('App: Unowned - https://store.steampowered.com/app/' + valId + '/')
                        }

                        countAll[0]++
                    } else if (rgxSub.test(attrHref)) {
                        if (steam_profile.rgOwnedPackages.indexOf(valId) > -1) {
                            writeConsoleMessage('Sub: owned - https://store.steampowered.com/sub/' + valId + '/')
                            let ele = getElementCallback(eleApps[i], 1)
                            if (ele && classOwned !== '') {
                                ele.classList.add(classOwned)
                            }
                            countOwned[1]++
                        } else {
                            // writeConsoleMessage('Sub: not owned - https://store.steampowered.com/sub/' + valId + '/')
                        }
                        countAll[1]++
                    } else {
                        writeConsoleMessage('Cannot determine url type: ' + attrHref)
                    }
                } else {
                    writeConsoleMessage('Cannot match ID from url: ' + attrHref)
                }
            }

            writeConsoleMessage('App: Owned ' + countOwned[0] + '/' + countAll[0])
            writeConsoleMessage('Sub: Owned ' + countOwned[1] + '/' + countAll[1])

            getCountCallback(countAll[0], countAll[1], countOwned[0], countOwned[1])
        }

        markFromJson(steam_profile)
    }

    function updateSteamProfileCache() {
        GM_xmlhttpRequest(
            {
                method: 'GET',
                url: 'https://store.steampowered.com/dynamicstore/userdata/?t=' + getUnixTimestamp(),
                onload: function (response) {
                    writeConsoleMessage('Steam User Data: ' + response.responseText.length + ' bytes')

                    let dataRes = JSON.parse(response.responseText)

                    setProfileCache(dataRes)
                    steam_profile = dataRes
                }
            })
    }

    // eslint-disable-next-line no-unused-vars
    function createCacheResetButton() {
        let divCacheResetButton = document.createElement('div')
        divCacheResetButton.classList.add('bh_button')
        divCacheResetButton.id = 'bh_cacheReset'

        let cacheResetA = document.createElement('a')
        cacheResetA.setAttribute('onclick', 'return false;')
        cacheResetA.textContent = 'Reset Bundle Helper Cache'

        divCacheResetButton.appendChild(cacheResetA)
        document.body.appendChild(divCacheResetButton)

        divCacheResetButton.addEventListener('click',
            function () {
                updateSteamProfileCache()
            })
    }

    function addMarkBtnHandler(onClickFunction, argsArray) {
        if (!document.body.contains(divButton)) {
            document.body.appendChild(divButton)
        }

        divButton.addEventListener('click', () => {
            onClickFunction.apply(null, argsArray)
        })
    }

    function setElementOwned(element) {
        if (typeof element !== 'undefined' && element !== null) {
            element.classList.add(owned_item_class)
        }
    }

    /**
     * @description Checks the Steam game's ID against the owned apps
     * @param {number} steamID The ID to check
     * @returns {boolean} True when the game is owned
     */
    function isAppOwned(steamID) {
        if (steam_profile.rgOwnedApps.includes(parseInt(steamID))) {
            writeConsoleMessage('App: Owned - https://store.steampowered.com/app/' + steamID + '/')
            return true
        }
        // writeConsoleMessage('App: Unowned - https://store.steampowered.com/app/' + steamID + '/')
        return false
    }

    /**
     * @description Checks the Steam game's ID against the wishlisted apps
     * @param {number} steamID The ID to check
     * @returns {boolean} True when the game is wishlisted
     */
    function isAppWishlisted(steamID) {
        if (steam_profile.rgWishlist.includes(parseInt(steamID))) {
            writeConsoleMessage('App: Wishlisted - https://store.steampowered.com/app/' + steamID + '/')
            return true
        }
        return false
    }

    /**
     * @description Checks the Steam game's ID against the ignored apps
     * @param {number} steamID The ID to check
     * @returns {boolean} True when the game is ignored
     */
    function isAppIgnored(steamID) {
        if (typeof steam_profile.rgIgnoredApps[steamID] !== 'undefined') {
            writeConsoleMessage('App: Ignored - https://store.steampowered.com/app/' + steamID + '/')
            return true
        }
        return false
    }

    /**
     * @description Parses a string for a Steam game ID
     * @param {string} str The string/URL that contains the Steam game ID
     * @returns {number} Steam game ID
     */
    function getSteamIDFromString(str) {
        let rgxId = /[0-9]{3,}/g
        let matches = str.match(rgxId)
        if (matches) {
            return parseInt(matches[0])
        }
        return null
    }

    /**
     * Searches the document for Steam game ownership
     * @param {string|null} steamLinkSelector The CSS selector to match Steam links
     * @param {HTMLElement} elementToMark The element to mark as owned
     * @returns {undefined}
     */
    function markBySteamLinkSelector(steamLinkSelector, elementToMark) {
        let selectorQuery
        if (typeof steamLinkSelector === 'undefined' || steamLinkSelector === null) {
            selectorQuery = default_steam_url_selector
        } else {
            selectorQuery = steamLinkSelector
        }

        document.querySelectorAll(selectorQuery).forEach(steamStoreLink => {
            let steamID = getSteamIDFromString(steamStoreLink.href)
            if (steamID !== null) {
                if (isAppOwned(steamID)) {
                    if (typeof elementToMark === 'undefined' || elementToMark === null) {
                        // No element passed, mark the link element itself
                        setElementOwned(steamStoreLink)
                    } else if (typeof elementToMark === 'function') {
                        // Function passed, call the function passing in the link element
                        let element = elementToMark(steamStoreLink)
                        setElementOwned(element)
                    } else {
                        // Element passed, attempt to mark
                        setElementOwned(elementToMark)
                    }
                }
            }
        })
    }

    /**
     * Checks a page for Steam game ownership
     * @param {string} storePageUrl The store page that contains the Steam link
     * @param {string|null} steamLinkSelector The CSS selector to match Steam links
     * @param {HTMLElement} elementToMark The element to mark as owned
     * @returns {undefined}
     */
    function markByStorePageUrl(storePageUrl, steamLinkSelector, elementToMark) {
        let selector
        if (typeof steamLinkSelector === 'undefined' || steamLinkSelector === null) {
            selector = default_steam_url_selector
        } else {
            selector = steamLinkSelector
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: storePageUrl,
            onload: function (response) {
                let parser = new DOMParser()
                let storePage = parser.parseFromString(response.responseText, 'text/html')

                let steamLink = storePage.querySelector(selector)
                if (steamLink !== null) {
                    let steamID = getSteamIDFromString(steamLink.href)
                    if (steamID !== null) {
                        if (isAppOwned(steamID)) {
                            setElementOwned(elementToMark)
                        }
                    }
                } else {
                    writeConsoleMessage(`No steam links found on page "${storePageUrl}" with selector "${selector}"`)
                }
            }
        })
        return
    }

    /**
     * Checks all matching links for Steam game ownership
     * @param {string} storePageSelector The CSS selector to match store links
     * @param {string} steamLinkSelector The CSS selector to match Steam links
     * @param {HTMLElement} elementToMark The element to mark as owned
     * @returns {undefined}
     */
    function markByStorePageSelector(storePageSelector, steamLinkSelector, elementToMark) {
        let storePageLinkElements = document.querySelectorAll(storePageSelector)

        storePageLinkElements.forEach(storePageLinkElement => {
            if (typeof elementToMark === 'undefined' || elementToMark === null) {
                markByStorePageUrl(storePageLinkElement.href, steamLinkSelector, storePageLinkElement)
            } else if (typeof elementToMark === 'function') {
                // Function passed, call the function passing in the link element
                let element = elementToMark(storePageLinkElement)
                markByStorePageUrl(storePageLinkElement.href, steamLinkSelector, element)
            } else {
                writeConsoleMessage(storePageLinkElement)
                markByStorePageUrl(storePageLinkElement, steamLinkSelector, elementToMark)
            }
        })
        return
    }

    function main() {
        if (window !== window.parent) {
            // https://developer.mozilla.org/en-US/docs/Web/API/Window/parent
            // Don't run inside of a frame
            return
        }

        if (!divButton) {
            divButton = document.createElement('div')
        }
        divButton.classList.add('bh_button')
        divButton.id = 'bh_markOwned'

        let eleA = document.createElement('a')
        eleA.setAttribute('onclick', 'return false;')
        eleA.textContent = 'Mark Owned'

        divButton.appendChild(eleA)

        // Create button to refresh profile details
        // createCacheResetButton()

        GM_addStyle(
            '   .bh_button { '
            + '	  border-radius: 2px; border: medium none; padding: 10px; display: inline-block; '
            + '   cursor: pointer; background: #67C1F5 none repeat scroll 0% 0%; '
            + '   width: 120px; text-align: center; } '
            + ' .bh_button a { '
            + '   text-decoration: none !important; color: #FFF !important; '
            + '   padding: 0px 2px; } '
            + ' .bh_button:hover a { '
            + '   color: #0079BF !important; } '
            + ' .bh_button, .bh_button a { '
            + '   font-family: Verdana; font-size: 12px; '
            + '   line-height: 16px; } '
            + ' .bh_owned { background-color: #7CA156 !important; '
            + '   transition: background 500ms ease 0s; } '
            + ' #bh_markOwned { '
            + '   position: fixed; right: 20px; bottom: 20px; z-index: 33; } '
            + ' #bh_cacheReset { '
            + '   position: fixed; right: 20px; bottom: 60px; z-index: 33; } '
            + ' #bh_OpenLib { '
            + '   position: fixed; right: 20px; bottom: 65px; z-index: 33; } '
        )

        let url = document.documentURI

        if (url.includes('hrkgame.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #2B823A !important;/* background-color: #97BA22 !important;*/ } '
                + ' #bh_markOwned { bottom: 40px !important; } '
                + ' #bh_cacheReset { bottom: 80px !important; } '
                + '.catalog.ui.items .item.bh_owned .content a, .catalog.ui.items .item.bh_owned .content .description {color: #333 !important;}'
                + '.hrktable_content a.browse_catalogues_items div.label.bh_owned { background-color: #2B823A!important }'
            )

            if (url.includes('/randomkeyshop/make-bundle')) {
                let onClickFunction = function () {
                    document.querySelectorAll('#result div.header[data-href*=\'/games/product/\']').forEach(link => {
                        markByStorePageUrl(link.getAttribute('data-href'), 'a.item[href*=\'store.steampowered.com/\']', element => element.parentElement.parentElement)
                    })
                }
                addMarkBtnHandler(onClickFunction)
            } else if (url.includes('/games/products/?search')) {
                addMarkBtnHandler(markByStorePageSelector, ['.item a.header[href*=\'/games/product/\']', 'a.item[href*=\'store.steampowered.com/\']', element => element.parentElement.parentElement])
            } else if (url.includes('/games/product/')) {
                addMarkBtnHandler(markBySteamLinkSelector, ['a.item[href*=\'store.steampowered.com/\']', document.querySelector('.ui.maincontainer')])
            } else {
                let onClickFunction = function () {
                    markByStorePageSelector('.offer_column a[href*=\'/games/product/\']', 'a.item[href*=\'store.steampowered.com/\']', element => element.parentElement)

                    document.querySelectorAll('a.browse_catalogues_items[href*=\'/games/product/\']').forEach(link => {
                        if (link.textContent.includes('Steam')) {
                            let elementToMark = link.querySelector('div.label')
                            markByStorePageUrl(link.href, 'a.item[href*=\'store.steampowered.com/\']', elementToMark)
                        }
                    })
                }
                addMarkBtnHandler(onClickFunction)
            }
        } else if (url.includes('itch.io')) {
            if (url.includes('/my-collections') || url.includes('/my-purchases') || url.includes('/games') || url.includes('/s/') || url.includes('/c/') || url.includes('/b/')) {
                GM_addStyle(
                    ' .grid_outer .game_grid_widget .game_cell.bh_owned span, .grid_outer .game_grid_widget .game_cell.bh_owned div, .grid_outer .game_grid_widget .game_cell.bh_owned a { color: #ffffff !important; } '
                )

                addMarkBtnHandler(() => {
                    let storePageLinkElements = document.querySelectorAll('.game_cell_data a.game_link')

                    storePageLinkElements.forEach(storePageLinkElement => {
                        if (!storePageLinkElement.href.includes('/b/')) {
                            // Don't search bundle pages for Steam links
                            markByStorePageUrl(storePageLinkElement.href, default_steam_url_selector, storePageLinkElement.parentElement.parentElement.parentElement)
                        }
                    })
                })
            } else if (url.includes('/recommendations')) {
                GM_addStyle(
                    ' .index_game_cell_widget.game_cell.bh_owned span, .index_game_cell_widget.game_cell.bh_owned div, .index_game_cell_widget.game_cell.bh_owned a.user_link { color: #ffffff; } '
                )
                addMarkBtnHandler(markByStorePageSelector, ['a.title', default_steam_url_selector, element => element.parentElement.parentElement])
            }
        } else if (url.includes('fanatical.com')) {
            GM_addStyle(
                ' .bh_owned { background-color: #0c6c22 !important; } '
                + ' .bh_owned .card-body div.card-body { background-color: #0c6c22 !important; } '
                + ' .bh_owned .card-body { background-color: #0c6c22; } '
            )

            if (url.includes('/game/')) {
                addMarkBtnHandler(markBySteamLinkSelector, [default_steam_url_selector, () => document.querySelector('.details-content-container')])
            } else if (url.includes('/bundle/')) {
                let obTarget_root = document.querySelector('#root')
                if (obTarget_root) {
                    let tmOb_root = -1
                    let obMu_root = new MutationObserver(function (mutations) {
                        mutations.forEach(function (mutation) {
                            if (mutation.type !== 'attributes'
                                || mutation.target.tagName === 'TR') {
                                clearTimeout(tmOb_root)
                                tmOb_root = setTimeoutCustom(function () {
                                    markBySteamLinkSelector(default_steam_url_selector, element => element.parentElement
                                        .parentElement.parentElement.parentElement)
                                }, 200)
                            }
                        })
                    })

                    let obConfig_root = { childList: true, subtree: true }
                    obMu_root.observe(obTarget_root, obConfig_root)
                }
            } else if (url.includes('/pick-and-mix/')) {
                let onClickFunction = function () {
                    let hook = __REACT_DEVTOOLS_GLOBAL_HOOK__
                    let rootFragmentFiber = Array.from(hook.getFiberRoots(1))[0].current
                    let rootCompFiber = rootFragmentFiber.child
                    let rootComp = rootCompFiber.stateNode
                    let state = rootComp.props.store.getState()
                    let productsArr = state.pickAndMix.one.products
                    let ownedGames = []
                    productsArr.forEach(product => {
                        if (isAppOwned(product.steam.id)) {
                            writeConsoleMessage('You own game ID: ' + product.steam.id + ' - "' + product.name + '"')
                            ownedGames.push(product.name)
                        }
                    })
                    if (ownedGames.length > 0) {
                        document.querySelectorAll('.card-overlay p').forEach(p => {
                            if (ownedGames.includes(p.textContent)) {
                                setElementOwned(p.parentElement.parentElement.parentElement.parentElement)
                            }
                        })
                    }
                }
                addMarkBtnHandler(onClickFunction)
            }

            let onClickFunction = function () {
                let timeouts = []
                let gameUrls = []
                document.querySelectorAll('a[href*=\'/game/\']').forEach(game => {
                    if (gameUrls.includes(game.href)) {
                        return
                    }
                    gameUrls.push(game.href)
                    timeouts.push(function () {
                        let gamePage = game.href.replace('/en', '').replace('/game/', '/api/products/') + '/en'
                        GM_xmlhttpRequest({
                            method: 'GET',
                            headers: {
                                'Host': 'www.fanatical.com',
                                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
                                'Accept': 'application/json',
                                'Content-Type': 'application/x-www-form-urlencoded',
                                'Referer': game.href
                            },
                            url: gamePage,
                            onload: function (response) {
                                // writeConsoleMessage('status ' + response.status + ' ' + gamePage)
                                if (response.status === 200) {
                                    let apiResponse = JSON.parse(response.responseText)
                                    if (typeof apiResponse.steam.id !== 'undefined') {
                                        if (isAppOwned(apiResponse.steam.id)) {
                                            setElementOwned(game.parentElement.parentElement.parentElement.parentElement)
                                        }
                                    }
                                    if (timeouts.length > 0) {
                                        setTimeout(timeouts.pop(), 50)
                                    }
                                } else if (timeouts.length > 0) {
                                    setTimeout(timeouts.pop(), 400)
                                }
                            }
                        })
                    })
                })
                setTimeout(timeouts.pop(), 100)
            }
            addMarkBtnHandler(onClickFunction)
        } else if (url.includes('reddit.com')) {
            GM_addStyle(
                '   .bh_owned , .md .bh_owned code { background-color: #DFF0D8 !important; } '
                + ' li > .bh_owned, div > p > .bh_owned { padding: 0px 2px 0px 2px; } '
            )

            addMarkBtnHandler(markOwned, ['td > a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement
            }, null, 'bh_owned'])


            setTimeout(function () {
                markOwned('td > a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement.parentElement
                }, null, 'bh_owned')

                markOwned('li > a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement
                }, null, 'bh_owned')

                markOwned('li > p > a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement.parentElement
                }, null, 'bh_owned')

                markOwned('div > p > a[href*=\'store.steampowered.com/\']'
                    , null, null, 'bh_owned')
            }, 1000)
        } else if (url.includes('indiegala.com')) {
            GM_addStyle(
                '  #bh_markOwned {bottom: 70px !important;}'
                + ' .bh_owned, .bh_owned .bundle-item-trading { background-color: rgba(125, 174, 45, 0.9) !important; } '
                + ' .ig-bundle { padding-left: 3px; padding-right: 3px; margin-bottom: 3px; } '
                + ' .bh_owned.ig-bundle { background-color: rgba(125, 174, 45) !important; } '
                + ' .bh_owned.ig-bundle .bundle-item-trading { background-color: rgba(125, 174, 45, 0) !important; } '
                + ' .bh_owned .add-info-button-cont .left, .bh_owned .add-info-button-cont .palette-background-2 { '
                + '   background-color: #7DAE2D !important; } '
                + ' .bh_owned .add-info-button-cont .right .inner-info, .bh_owned .add-info-button-cont .right .palette-border-2 { '
                + '   border-color: #7DAE2D !important; } '
                + ' .bh_owned.medium-game .game-cover-medium { border: 3px solid #7DAE2D; background-color: rgba(125, 174, 45, 0.4); } '
                + ' .bh_owned.game-data-cont { background-color: #76AD1C !important; } '
                + ' .bundle-item-trading-cards-cont span { opacity: 0.7; } '
                + ' .span-title .title_game, .span-title .title_drm, .span-title .title_music { '
                + '   line-height: 43px !important; margin: 10px 0px 10px 15px !important; '
                + '   padding-left: 10px !important; border-radius: 3px !important; } '
                + ' .medium-game { min-height: 146px; } '
            )

            // Insert email to bundle section
            let countRetryEmail = 10
            let tmRetryEmail = setInterval(function () {
                let eleEmail = document.querySelector('.account-email')
                let eleInput = document.querySelector('.email-input')
                if (eleEmail && eleInput) {
                    let email = eleEmail.textContent.trim()
                    if (email !== '') {
                        eleInput.value = email
                        clearInterval(tmRetryEmail)
                    }
                }

                if (countRetryEmail < 0) {
                    clearInterval(tmRetryEmail)
                }
                countRetryEmail--
            }, 3000)

            // Change title
            let countRetryTitle = 10
            let tmRetryTitle = setInterval(function () {
                let elesPrice = document.querySelectorAll('.bundle-claim-phrase')
                for (let i = elesPrice.length - 1; i > -1; i--) {
                    let elePrice = elesPrice[i].querySelector('span')
                    if (elePrice) {
                        let price = elePrice.textContent.trim()
                        if (price.indexOf('$') === 0) {
                            document.title = price + ' ' + document.title
                            clearInterval(tmRetryTitle)
                            break
                        }
                    }
                }

                if (countRetryTitle < 0) {
                    clearInterval(tmRetryTitle)
                }
                countRetryTitle--
            }, 3000)

            if (url.includes('indiegala.com/store/') || url.includes('indiegala.com/games') || url === 'https://www.indiegala.com/') {
                let onClickFunction = function () {
                    let gameBrowserLinks = document.querySelectorAll('a.main-list-item-clicker')
                    for (let i = 0; i < gameBrowserLinks.length; i++) {
                        let steamID = getSteamIDFromString(gameBrowserLinks[i].href)
                        if (steamID !== null) {
                            if (isAppOwned(steamID)) {
                                setElementOwned(gameBrowserLinks[i].parentElement)
                            }
                        }
                    }

                    let smallListLinks = document.querySelectorAll('a.fit-click')
                    for (let i = 0; i < smallListLinks.length; i++) {
                        let steamID = getSteamIDFromString(smallListLinks[i].href)
                        if (steamID !== null) {
                            if (isAppOwned(steamID)) {
                                setElementOwned(smallListLinks[i].parentElement.querySelector('.item-inner'))
                            }
                        }
                    }
                }
                addMarkBtnHandler(onClickFunction)
            }
        } else if (url.includes('orlygift.com')) {
            addMarkBtnHandler(markByStorePageSelector, ['a[href*=\'/games/\']', default_steam_url_selector, element => element.parentElement])
        } else if (url.includes('cubicbundle.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #91BA07 !important; } '
            )
            addMarkBtnHandler(markOwned, ['.price a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement.parentElement.parentElement.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('dailyindiegame.com')) {
            GM_addStyle(
                '   .bh_owned, .bh_owned a, .bh_owned a:not(:visited) .DIG2content { color: #202020 !important; } '
            )

            let onClickFunction = function () {
                let markMap = [{
                    selector: '.DIG-content a[href*=\'store.steampowered.com/\']',
                    callback: function (ele) {
                        return ele.parentElement
                            .parentElement.parentElement
                            .parentElement.parentElement
                    }
                },
                {
                    selector: '.DIG2content a[href*=\'store.steampowered.com/\']',
                    callback: function (ele) {
                        return ele.parentElement.parentElement
                    }
                },
                {
                    selector: '.DIG3_14_Gray a[href*=\'store.steampowered.com/\']',
                    callback: function (ele) {
                        return ele.parentElement.parentElement.parentElement
                    }
                }]
                for (let i = 0; i < markMap.length; i++) {
                    if (document.querySelectorAll(markMap[i].selector).length > 0) {
                        markOwned(markMap[i].selector, markMap[i].callback, null, 'bh_owned')
                    }
                }
            }
            addMarkBtnHandler(onClickFunction)
        } else if (url.includes('bundlekings.com')) {
            addMarkBtnHandler(markOwned, ['.content-wrap a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('otakumaker.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #91BA07 !important; } '
            )
            addMarkBtnHandler(markOwned, ['.gantry-width-spacer a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('otakubundle.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #91BA07 !important; } '
            )
            addMarkBtnHandler(markOwned, ['#hikashop_product_left_part > .g-grid > .g-block > .g-block > a[href*=\'store.steampowered.com/\']',
                function (ele) {
                    return ele.parentElement.parentElement
                },
                null,
                'bh_owned'])
        } else if (url.includes('gogobundle.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #91BA07 !important; border: 1px solid white; } '
            )

            addMarkBtnHandler(markOwned, ['.g-block > .g-block > a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('superduperbundle.com')) {
            addMarkBtnHandler(markOwned, ['#gameslist a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('gamebundle.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #A0CC41 !important; border-bottom: 45px solid rgba(233, 233, 233, 0.5); } '
                + ' .bh_owned .activebundle_game_bundle_debut_title { background-color: #A0CC41 !important; } '
            )
            addMarkBtnHandler(markOwned, ['.activebundle_game_section_full a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('humblebundle.com')) {
            GM_addStyle(
                '   .game-box img { max-height: 180px !important; max-width: 130px !important; } '
                + ' .image-grid { animation: none !important; } '
            )
            if (url.includes('/games/')) {
                let onClickFunction = function () {
                    let gameBrowserLinks = document.querySelectorAll(default_steam_url_selector)
                    for (let i = 0; i < gameBrowserLinks.length; i++) {
                        let steamID = getSteamIDFromString(gameBrowserLinks[i].href)
                        if (steamID !== null) {
                            if (isAppOwned(steamID)) {
                                setElementOwned(gameBrowserLinks[i].parentElement.parentElement.parentElement.parentElement)
                            }
                        }
                    }
                }
                addMarkBtnHandler(onClickFunction)
            } else if (url.includes('/store')) {
                let onClickFunction = function () {
                    let gameBrowserLinks = document.querySelectorAll(default_steam_url_selector)
                    for (let i = 0; i < gameBrowserLinks.length; i++) {
                        let steamID = getSteamIDFromString(gameBrowserLinks[i].href)
                        if (steamID !== null) {
                            if (isAppOwned(steamID)) {
                                setElementOwned(gameBrowserLinks[i].parentElement.parentElement)
                            }
                        }
                    }
                }
                addMarkBtnHandler(onClickFunction)
            }
        } else if (url.includes('steamcompanion.com')) {
            GM_addStyle(
                '   .bh_owned.banner { margin-bottom: 5px !important; margin-top: 35px !important; '
                + '   padding-bottom: 15px !important; padding-top: 15px !important; } '
                + ' .bh_owned.giveaway-links { opacity: 0.75; } '
            )

            markOwned('#hero a[href*=\'store.steampowered.com/\']'
                , null, null, 'bh_owned')

            // Mark
            {
                let query = '.giveaway-links img[src^=\'https://steamcdn-a.akamaihd.net/steam/apps/\']'
                let getLabelCallback = function (ele) {
                    return ele.parentElement.parentElement.parentElement
                }

                let apps = []

                let eleApps = document.querySelectorAll(query)

                for (let i = 0; i < eleApps.length; i++) {
                    let app = /[0-9]+/.exec(eleApps[i].getAttribute('src'))
                    if (app !== null) {
                        apps.push(app[0])
                    }
                }

                apps = apps.filter(function (elem, index, self) {
                    return index === self.indexOf(elem)
                })

                writeConsoleMessage('Apps: ' + apps.length)
                let appAll = apps.join(',')

                GM_xmlhttpRequest(
                    {
                        method: 'GET',
                        headers:
                        {
                            'Cache-Control': 'max-age=0'
                        },
                        url: 'https://store.steampowered.com/api/appuserdetails/?appids=' + appAll,
                        onload: function (response) {
                            let dataRes = JSON.parse(response.responseText)

                            let countOwned = 0

                            let elementApps = document.querySelectorAll(query)
                            for (let i = 0; i < elementApps.length; i++) {
                                let appUrl = elementApps[i].getAttribute('src')
                                if (appurl.includes('https://steamcdn-a.akamaihd.net/steam/apps/')) {
                                    let app = /[0-9]+/.exec(appUrl)
                                    if (app !== null) {
                                        if (typeof dataRes[app] !== 'undefined') {
                                            if (dataRes[app].success) {
                                                if (dataRes[app].data.is_owned) {
                                                    let eleLabel = getLabelCallback(elementApps[i])
                                                    eleLabel.classList.add('bh_owned')
                                                    countOwned++
                                                } else {
                                                    // writeConsoleMessage("App: not owned - http://store.steampowered.com/app/" + app + "/");
                                                }
                                            } else {
                                                // writeConsoleMessage("App: not success - https://steamdb.info/app/" + app + "/");
                                            }
                                        }
                                    }
                                }
                            }

                            writeConsoleMessage('Apps: owned - ' + countOwned)
                        }
                        // End onload
                    })
            }
        } else if (url.includes('store.steampowered.com')) {
            if (url.includes('/widget/')) {
                GM_addStyle(
                    '   .bh_owned { background-color: transparent !important; } '
                    + ' .bh_owned a { color: #71A034 !important; }'
                )

                markOwned('.main_text a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement
                }, null, 'bh_owned')
            } else if (url.includes('/app/')) {
                GM_addStyle(
                    '   .bh_owned { '
                    + '   background-color: #6D8C1A !important; '
                    + '   padding: 0px 2px 0px 2px; '
                    + ' } '
                )

                markOwned(
                    '.glance_details p > a[href*=\'store.steampowered.com/\']'
                    + ', .game_area_dlc_bubble  a[href*=\'store.steampowered.com/\']'
                    ,
                    null,
                    null,
                    'bh_owned')
            } else if (url.includes('/notinterested/')) {
                GM_addStyle(
                    '   .bh_owned { '
                    + '   background-color: #6D8C1A !important; '
                    + '   padding: 5px 100px 5px 5px !important; '
                    + '   margin-left: -5px; margin-right: 50px; '
                    + ' } '
                )

                markOwned('.ignoredapps > a[href*=\'store.steampowered.com/\']'
                    , null, null, 'bh_owned')
            } else if (url.includes('/search/')) {
                GM_addStyle(
                    '   .bh_owned { '
                    + '   background-color: #6D8C1A66 !important; '
                    + ' } '
                )

                markOwned('.search_result_row[href*=\'store.steampowered.com/\']'
                    , null, null, 'bh_owned')
            }
        } else if (url.includes('steamcommunity.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #71A034 !important; '
                + '   padding: 0px 2px 0px 2px; } '
                + ' .bh_owned.blotter_userstatus_game { padding: 0px; border-color: #71A034; } '
            )

            if (url.includes('/home')) {
                let querySteamHome = '.blotter_gamepurchase_details a[href*=\'store.steampowered.com/\']:not(.bh_owned) '
                    + ', .blotter_author_block a[href*=\'store.steampowered.com/\']:not(.bh_owned) '
                    + ', .blotter_author_block a[href*=\'steamcommunity.com/app/\']:not(.bh_owned) '
                    + ', .blotter_daily_rollup_line a[href*=\'steamcommunity.com/app/\']:not(.bh_owned) '
                markOwned(querySteamHome, function (ele, type) {
                    if (type === 1) {
                        if (ele.classList.contains('blotter_userstats_game')) {
                            ele.parentElement.classList.add('bh_owned')
                        } else {
                            ele.classList.add('bh_owned')
                        }
                    }
                })

                let targetObMark = document.getElementById('blotter_content')
                if (targetObMark) {
                    let tmObMark = -1
                    let obMark = new MutationObserver(function (mutations) {
                        mutations.forEach(function () {
                            clearTimeout(tmObMark)
                            tmObMark = setTimeout(function (querySteamH) {
                                markOwned(querySteamH, function (ele, type) {
                                    if (type === 1 && !ele.classList.contains('blotter_userstats_game')) {
                                        ele.classList.add('bh_owned')
                                    }
                                })
                            }, 100, querySteamHome)
                        })
                    })

                    let configObMark = { childList: true }
                    obMark.observe(targetObMark, configObMark)
                }
            } else if (url.includes('/announcements')) {
                markOwned('.announcement_body a[href*=\'store.steampowered.com/\']'
                    , null, null, 'bh_owned')
            }
        } else if (url.includes('forums.steampowered.com')) {
            GM_addStyle(
                '   .bh_owned { background-color: #71A034 !important; '
                + '   padding: 0px 2px 0px 2px;'
                + ' } '
            )

            markOwned('div[id^=\'post_message\'] a[href*=\'store.steampowered.com/\']'
                , null, null, 'bh_owned')
        } else if (url.includes('whosgamingnow.net')) {
            if (url.includes('/discussion')) {
                GM_addStyle(
                    '   .bh_owned { '
                    + '   padding: 0px 2px 0px 2px;'
                    + ' } '
                )

                markOwned('.MessageList a[href*=\'store.steampowered.com/\']'
                    , null, null, 'bh_owned')
            } else if (url.includes('/redeem')) {
                GM_addStyle(
                    '   .bh_owned { '
                    + '   border: 1px solid #FFF;'
                    + ' } '
                    + ' .bh_owned .BoxArt { '
                    + '   border: 0px !important;'
                    + ' } '
                )

                markOwned('.GameInfo a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement.parentElement.parentElement
                })
            } else if (url.includes('/giveaway')) {
                GM_addStyle(
                    '   .bh_owned { '
                    + '   border: 5px solid #7CA156;'
                    + ' } '
                )

                markOwned('img[src*=\'://cdn.akamai.steamstatic.com/steam/\']'
                    , null, null, 'bh_owned')
            }
        } else if (url.includes('steamground.com') && url.includes('/wholesale')) {
            GM_addStyle(
                '   .bh_owned { background-color: #48B24B !important; } '
                + ' .bh_owned .wholesale-card_title { color: #373d41 !important; } '
                + ' .bh_steam { display: none; } '
            )

            let elesTitle = document.querySelectorAll('.wholesale-card_title')
            if (elesTitle.length > 0) {
                GM_xmlhttpRequest(
                    {
                        method: 'GET',
                        url: 'https://www.steamgifts.com/discussion/iy081/steamground-wholesale-build-a-bundle',
                        onload: function (response) {
                            let data = response.responseText
                            let eleContainer = document.createElement('div')
                            eleContainer.innerHTML = data

                            let eleComment = eleContainer.querySelector('.comment__description')
                            if (eleComment) {
                                let elesGame = eleComment.querySelectorAll('table td:nth-child(1) a[href*=\'store.steampowered.com/\']')
                                if (elesGame.length > 0) {
                                    let arrTitle = []
                                    for (let i = 0; i < elesTitle.length; i++) {
                                        arrTitle.push(elesTitle[i].textContent.trim())
                                    }

                                    for (let i = 0; i < elesGame.length; i++) {
                                        let isMatch = false
                                        let game = elesGame[i].textContent.trim().toLowerCase()
                                        for (let j = 0; j < elesTitle.length; j++) {
                                            let title = elesTitle[j].textContent.trim().toLowerCase()
                                            if (game === title
                                                || (title.indexOf('|') > -1 && game === title.replace('|', ':'))
                                                || (game === 'ball of light' && title === 'ball of light (journey)')
                                                || (game === 'its your last chance in new school' && title === 'it is yоur last chance in new schооl')
                                                || (game === 'shake your money simulator 2016' && title === 'shake your money simulator')
                                                || (game === 'spakoyno: back to the ussr 2.0' && title === 'spakoyno back to the ussr 2.0')
                                                || (game === 'or' && title === 'or!')) {
                                                isMatch = true

                                                arrTitle = arrTitle.filter(function (value) {
                                                    return value !== elesTitle[j].textContent.trim()
                                                })
                                            }

                                            if (isMatch) {
                                                let elemA = document.createElement('a')
                                                elemA.classList.add('bh_steam')
                                                elemA.href = elesGame[i].href
                                                elesTitle[j].parentElement.parentElement.appendChild(elemA)

                                                break
                                            }
                                        }
                                        if (!isMatch) {
                                            writeConsoleMessage('Not match: ' + elesGame[i].href + ' ' + elesGame[i].textContent)
                                        }
                                    }

                                    if (arrTitle.length > 0) {
                                        writeConsoleMessage('Not match: ' + arrTitle.length)
                                        for (let i = 0; i < arrTitle.length; i++) {
                                            writeConsoleMessage('Not match: ' + arrTitle[i])
                                        }
                                    }

                                    markOwned('.wholesale-card > a[href*=\'store.steampowered.com/\']', function (ele) {
                                        return ele.parentElement
                                    }, null, 'bh_owned')
                                }
                            }
                        }
                        // End onload
                    })
            }
        } else if (url.includes('bunchkeys.com')) {
            GM_addStyle(
                '   .bh_owned { border: #B5D12E 3px solid !important; '
                + '   margin-left: -3px; margin-top: -3px; } '
            )

            addMarkBtnHandler(markOwned, [default_steam_url_selector, function (ele) {
                return ele.parentElement
            }, null, 'bh_owned'])
        } else if (url.includes('sgtools.info')) {
            GM_addStyle(
                '   .bh_owned { background-color: #71A034 !important; } '
            )
            if (url.includes('/lastbundled')) {
                markOwned('#content > div > table > tbody > tr > td > a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement.parentElement
                }, null, 'bh_owned')
            } else if (url.includes('/deals')) {
                markOwned('.deal_game_image > img[src*=\'cdn.akamai.steamstatic.com/steam/\']', function (ele) {
                    return ele.parentElement
                }, null, 'bh_owned')
            } else if (url.includes('/whitelisted')) {
                markOwned('.cmGame > a[href*=\'store.steampowered.com/\']', function (ele) {
                    return ele.parentElement
                }, null, 'bh_owned')
            }
        } else if (url.includes('steamkeys.ovh')) {
            markOwned('td > a[href*=\'store.steampowered.com/\']', function (ele) {
                return ele.parentElement.parentElement
            }, null, 'bh_owned')
        } else if (url.includes('steamdb.info')) {
            if (window !== window.parent) {
                return
            }

            GM_addStyle(
                '   .bh_owned, tr.bh_owned td { background-color: #DDF7D3 !important; } '
                + ' .bh_owned_transparent { background-color: #bcf0a880 !important; } '
            )

            markOwned(' \
            #apps .app \
            , #dlc .app \
            , .container > .table .app \
            , .sales-section .app \
            , .page-search .app \
            ', null, function (ele) {
                return ele.getAttribute('data-appid')
            }, 'bh_owned')

            markOwned(' \
            #subs .package \
            , .sales-section .package \
            , .page-search .package \
            ', null, function (ele) {
                return '/steam/subs/' + ele.getAttribute('data-subid')
            }, 'bh_owned')

            markOwned('.table-products .app'
                , null, function (ele) {
                    return ele.getAttribute('data-appid')
                }, 'bh_owned_transparent')

            markOwned('.app-history .appid'
                , function (ele) {
                    return ele.parentElement
                }, function (ele) {
                    return ele.textContent.trim()
                }, 'bh_owned')
        }

        window.addEventListener('beforeunload', function () {
            clearTimeoutAll()
            clearIntervalAll()
        })
    }

    attachOnReady(main)
}())