Itch.io Web Integration

Shows if an Itch.io link has been claimed or not

当前为 2020-06-06 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Itch.io Web Integration
// @namespace   Lex@GreasyFork
// @match       *://*/*
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @version     0.1.3
// @author      Lex
// @description Shows if an Itch.io link has been claimed or not
// @connect     itch.io
// ==/UserScript==

// TODO
// Add ability to claim the game with one click

(function(){
    'use strict';
    
    const INVALIDATION_TIME = 5*60*60*1000; // 5 hour cache time
    const ITCH_GAME_CACHE_KEY = 'ItchGameCache';
    var ItchGameCache;
    
    // Promise wrapper for GM_xmlhttpRequest
    const Request = details => new Promise((resolve, reject) => {
        details.onerror = details.ontimeout = reject;
        details.onload = resolve;
        GM_xmlhttpRequest(details);
    });
    
    function loadItchCache() {
        ItchGameCache = JSON.parse(GM_getValue(ITCH_GAME_CACHE_KEY) || '{}');
    }
    
    function _saveItchCache() {
        if (ItchGameCache === undefined) return;
        GM_setValue(ITCH_GAME_CACHE_KEY, JSON.stringify(ItchGameCache));
    }
    
    function setItchGameCache(key, game) {
        loadItchCache(); // refresh our cache in case another tab has edited it
        ItchGameCache[key] = game;
        _saveItchCache();
    }
    
    function deleteItchGameCache(key) {
        if (key === undefined) return;
        loadItchCache();
        delete ItchGameCache[key];
        _saveItchCache();
    }
    
    function getItchGameCache(link) {
        if (!ItchGameCache) loadItchCache();
        if (Object.prototype.hasOwnProperty.call(ItchGameCache, link)) {
            return ItchGameCache[link];
        }
        return null;
    }
    
    // Sends an XHR request and parses the results into a game object
    async function fetchItchGame(url) {
        const response = await Request({method: "GET",
                                 url: url});
        const parser = new DOMParser();
        const dom = parser.parseFromString(response.responseText, 'text/html');
        // Gets the inner text of an element if it can be found otherwise returns undefined
        const txt = query => { const e = dom.querySelector(query); return e && e.innerText.trim(); };
        
        const game = {};
        
        game.cachetime = (new Date()).getTime();
        game.url = url;
        
        const h2 = dom.querySelector(".purchase_banner_inner .key_row .ownership_reason");
        game.isOwned = h2 !== null;
        
        game.isClaimable = [...dom.querySelectorAll(".buy_btn")].filter(e => e.innerText == "Download or claim").length > 0;
        game.isFree = [...dom.querySelectorAll("span[itemprop=price]")].filter(e => e.innerText == "$0.00 USD").length > 0;
        game.original_price = txt("span.original_price");
        game.price = txt("span[itemprop=price]");
        game.saleRate = txt(".sale_rate");
        
        return game;
    }
    
    // Loads an itch game from cache or fetches the page if needed
    async function getItchGame(url) {
        let game = getItchGameCache(url);
        if (game !== null) {
            const isExpired = (new Date()).getTime() - game.cachetime > INVALIDATION_TIME;
            if (isExpired) {
                game = null;
            }
        }
        if (game === null) {
            game = await fetchItchGame(url);
            setItchGameCache(url, game);
        }
        return game;
    }
    
    // Appends the isOwned tag to an anchor link
    function appendTags(a, game) {
        const div = document.createElement("div");
        a.after(div);
        let ownMark = '';
        if (game.isOwned) {
            ownMark = `<span title="Game is already claimed on itch.io">✔️</span>`;
        } else {
            if (!game.isClaimable) {
                let tooltip = game.price ? `🛒 Game costs ${game.price}` : `Game is not free`;
                ownMark = `<span title="${tooltip}">⛔</span>`;
            } else {
                const origPrice = game.original_price ? ` 🛒 Original price: ${game.original_price} 💸 Current Price: ${game.price}` : '';
                ownMark = `<span title="Game is claimable but you haven't claimed it.${origPrice}">❌</span>`;
            }
        }
        div.outerHTML = `<div style="margin-left: 5px; background:rgb(200,200,200); border-radius: 5px; display: inline-block;">${ownMark}</div>`;
    }
    
    function addClickHandler(a) {
        a.addEventListener('mouseup', event => {
            deleteItchGameCache(event.target.href);
        });
    }

    // Handles an itch.io link on a page
    async function handleLink(a) {
        addClickHandler(a);
        const game = await getItchGame(a.href);
        appendTags(a, game);
    }
    
    // Finds all the itch.io links on the current page
    function getItchLinks() {
        let links = [...document.querySelectorAll("a[href*='itch.io/']")];
        links = links.filter(a => /^https:\/\/[^.]+\.itch\.io\/[^/]+$/.test(a.href));
        links = links.filter(a => !a.classList.contains("return_link"));
        links = links.filter(a => { const t = a.textContent.trim(); return t !== "" && t !== "GIF"; });
        return links;
    }
    
    function handlePage() {
        const as = getItchLinks();
        as.forEach(handleLink);
    }
    
    handlePage();
})();