Itch.io Web Integration

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

目前為 2020-06-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.7
// @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 CACHE_VERSION_KEY = "CacheVersion";
    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 versionCacheInvalidator() {
        const sVersion = v => {
            if (!v || !v.match(/\d+\.\d+/)) return 0;
            return parseFloat(v.match(/\d+\.\d+/)[0]);
        }
        const prev = sVersion(GM_getValue(CACHE_VERSION_KEY, '0.0'));
        if (prev < 0.1) {
            console.log(`${GM_info.script.version} > ${prev}`);
            console.log(`New minor version of ${GM_info.script.name} detected. Invalidating cache.`)
            _clearItchCache();
        }
        GM_setValue(CACHE_VERSION_KEY, GM_info.script.version);
    }
    
    function _clearItchCache() {
        ItchGameCache = {};
        _saveItchCache();
    }
    
    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;
    }
    
    // Parses a DOM into a game object
    function parsePage(url, dom) {
        // 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;
        
        game.isOwned = dom.querySelector(".purchase_banner_inner .key_row .ownership_reason") !== 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.hasFreeDownload = [...dom.querySelectorAll("a.download_btn,a.buy_btn")].filter(e => e.innerText == "Download" || e.innerText == "Download Now").length > 0;
        game.original_price = txt("span.original_price");
        game.price = txt("span[itemprop=price]");
        game.saleRate = txt(".sale_rate");
        return game;
    }
    
    // Sends an XHR request and parses the results into a game object
    async function fetchItchGame(url) {
        const response = await Request({method: "GET",
                                 url: url});
        if (response.status != 200) {
            console.log(`Error ${response.status} fetching page ${url}`);
            return null;
        }
        const parser = new DOMParser();
        const dom = parser.parseFromString(response.responseText, 'text/html');
        return parsePage(url, dom);
    }
    
    // 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;
            // Expiration checking currently disabled
            /*if (isExpired) {
                game = null;
            }*/
        }
        if (game === null) {
            game = await fetchItchGame(url);
            if (game !== null)
                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 === null) {
            ownMark = `<span title="Status unknown. Try refreshing.">❓</span>`;
        } else if (game.isOwned) {
            ownMark = `<span title="Game is already claimed on itch.io">✔️</span>`;
        } else {
            if (!game.isClaimable) {
                if (game.hasFreeDownload) {
                    ownMark = `<span title="Game is a free download but not claimable">⛔</span>`;
                } else if (game.price) {
                    ownMark = `<span title="🛒 Game costs ${game.price}">🛒</span>`;
                } else {
                    ownMark = `<span title="Status unknown">👽</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);
    }
    
    function isGameUrl(url) {
        return /^https:\/\/[^.]+\.itch\.io\/[^/]+$/.test(url);
    }
    
    // Finds all the itch.io links on the current page
    function getItchLinks() {
        let links = [...document.querySelectorAll("a[href*='itch.io/']")];
        links = links.filter(a => isGameUrl(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() {
        if (isGameUrl(window.location.href)) {
            const game = parsePage(window.location.href, document);
            setItchGameCache(window.location.href, game);
        }
        const as = getItchLinks();
        as.forEach(handleLink);
    }
    
    versionCacheInvalidator();
    handlePage();
})();