Itchio Show Categories

Displays tag categories of games on itch.io

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Itchio Show Categories
// @version         0.0.3
// @author          Dillon Regimbal
// @namespace       https://dillonr.com
// @description     Displays tag categories of games on itch.io
// @match           *://itch.io/*
// @match           *://*.itch.io/*
// @run-at          document-idle
// @grant           GM_addStyle
// @grant           GM_xmlhttpRequest
// @grant           GM_getValue
// @grant           GM_setValue
// @icon            https://itch.io/favicon.ico
// @noframes
// ==/UserScript==

// Since 2020-06-12
// https://greasyfork.org/en/users/420789-dillon-regimbal
// https://greasyfork.org/en/scripts/405228-itchio-show-categories
// https://github.com/dregimbal/UserScripts/blob/master/ItchioShowCategories.user.js

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

    // #region Config and Variables

    let btn_category_class = 'dr_category_button'
    let btn_category_id = 'dr_markCategory'
    let btn_category_text = 'Checked 0/0'
    let game_store_link_selector = '.game_cell_data a.game_link, .bundle_game_grid_widget .game_cell a.title'
    let category_text_class = 'dr_category_text'
    let meta_tag_class = 'meta_tag'
    let game_cell_class = 'game_cell'
    let game_cell_data_class = 'game_cell_data'

    let categoryMap = [
        {
            container: '#wrapper',
            categories: [
                {
                    title: 'Co-op',
                    searchStrings: ['co-op', ' coop ']
                }
            ]
        },
        {
            container: '.game_info_panel_widget',
            categories: [
                {
                    title: 'Local Multiplayer',
                    searchStrings: ['Local Multiplayer']

                },
                {
                    title: 'Networked',
                    searchStrings: ['Networked Multiplayer']

                },
                {
                    title: 'Controller',
                    searchStrings: ['Gamepad', 'Xbox Controller', 'Joystick', 'Playstation Controller', 'Joy-Con', 'Wiimote']

                },
                {
                    title: 'Phone Control',
                    searchStrings: ['Smartphone']

                },
                {
                    title: 'Physical',
                    searchStrings: ['Physical Game', 'Tabletop']

                }
            ]
        }
    ]

    let gamesToCheck = new Map()

    // #endregion

    // #region Create button and styles

    let divButton = document.createElement('div')
    divButton.classList.add(btn_category_class)
    divButton.id = btn_category_id

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

    divButton.appendChild(eleA)

    GM_addStyle(`
    .${btn_category_class} {
        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;
    }

    .${btn_category_class} a {
        text-decoration: none !important;
        color: #FFF !important;
        padding: 0px 2px;
    }

    .${btn_category_class}:hover a {
        color: #0079BF !important;
    }

    .${btn_category_class}, .${btn_category_class} a {
        font-family: Verdana;
        font-size: 12px;
        line-height: 16px;
    }

    .${game_cell_class} .${game_cell_data_class} a.${category_text_class}.${meta_tag_class}, .${game_cell_class} a.${category_text_class}.${meta_tag_class}  {
        padding: 3px;
        margin: 2px;
        font-size: 14px;
        color: #ffffff;
        background-color: #17199d;
    }

    #${btn_category_id} {
        position: fixed;
        right: 20px;
        bottom: 65px;
        z-index: 33;
    }
    .scrolling_outer {
        height: auto !important;
    }
    `)

    // #endregion

    function queueCheckingGames() {
        let storePageLinkElements = document.querySelectorAll(game_store_link_selector)
        for (let storelink of storePageLinkElements) {
            // Don't search bundle pages for game details
            if (!storelink.href.includes('/b/')) {
                if (!gamesToCheck.has(storelink.href)) {
                    // New link
                    gamesToCheck.set(storelink.href,
                        {
                            link: storelink.href,
                            elements: new Set([storelink.parentElement]),
                            checked: false,
                            categories: new Set()
                        })
                } else {
                    // Existing link
                    gamesToCheck.get(storelink.href).elements.add(storelink.parentElement)
                }
                // Update the count on the button
                eleA.innerText = getNumberOfCheckedGames()
            }
        }
        return
    }

    async function checkGameLinks() {
        for (let game of gamesToCheck.values()) {
            await fetchGameCategories(game.link)
                .then(() => {
                    for (let element of game.elements) {
                        let nextSibling = element.nextElementSibling
                        if (nextSibling !== null && nextSibling.classList.contains(category_text_class)) {
                            // console.log('Categories already added')
                        } else {
                            for (let category of game.categories) {
                                addCategoryText(element, category)
                            }
                        }
                    }
                })
            eleA.innerText = getNumberOfCheckedGames()
        }
        return
    }

    function getNumberOfCheckedGames() {
        return `Checked ${Array.from(gamesToCheck.values()).reduce((acc, game) => {
            if (game.checked) {
                // eslint-disable-next-line no-param-reassign
                acc++
            }
            return acc
        }, 0)}/${gamesToCheck.size}`
    }

    /**
     * Checks a page for game categories
     * @param {string} storePageUrl The store page that contains the categories
     * @returns {Promise} The categories of the game
     */
    function fetchGameCategories(storePageUrl) {
        return new Promise((resolve, reject) => {
            let game = gamesToCheck.get(storePageUrl)
            if (game.checked) {
                resolve(game.categories)
            } else {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: game.link,
                    onload: function (response) {
                        console.assert(response.status === 200, [
                            response.status,
                            response.statusText,
                            response.readyState,
                            response.responseHeaders,
                            response.responseText,
                            response.finalUrl
                        ].join(' - '))

                        let parser = new DOMParser()
                        let storePage = parser.parseFromString(response.responseText, 'text/html')
                        let validCategories = new Set()
                        for (let scope of categoryMap) {
                            let scopedElements = storePage.querySelectorAll(scope.container)
                            console.assert(scopedElements.length > 0, `No elements matching "${scope.container}" found on ${game.link}`)
                            for (let scopedElement of scopedElements) {
                                for (let category of scope.categories) {
                                    for (let searchString of category.searchStrings) {
                                        if (scopedElement.textContent.toLowerCase().includes(searchString.toLowerCase())) {
                                            validCategories.add(category.title)
                                        }
                                    }
                                }
                            }
                        }
                        game.checked = true
                        game.categories = validCategories
                        resolve(validCategories)
                    }
                })
            }
        })
    }

    /**
     * @description Add text after an element
     * @param {HTMLElement} element the element to add the text to
     * @param {string} text the contents of the text
     * @returns {undefined}
     */
    function addCategoryText(element, text) {
        if (typeof element !== 'undefined' && element !== null) {
            let categoryText = document.createElement('a')
            categoryText.classList.add(meta_tag_class)
            categoryText.classList.add(category_text_class)
            categoryText.innerText = text
            element.parentNode.insertBefore(categoryText, element.nextSibling)
            // console.log(`Adding ${text}`)
        } else {
            console.log(`Element null, cannot add: ${text}`)
        }
        return
    }

    let url = document.documentURI
    let checking = false

    if (url.includes('/my-collections') || url.includes('/my-purchases') || url.includes('/games') || url.includes('/s/') || url.includes('/c/') || url.includes('/b/')) {
        divButton.addEventListener('click', async () => {
            if (!checking) {
                checking = true
                queueCheckingGames()
                await checkGameLinks()
                checking = false
            } else {
                console.log('Wait a second, eh')
            }
        })
        document.body.appendChild(divButton)
        queueCheckingGames()
    }
}())