// ==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()
}
}())