Steam Store Redirector

Redirects removed games from the Steam store to SteamCommunity or SteamDB.

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

// ==UserScript==
// @name Steam Store Redirector
// @namespace https://rafaelgssa.gitlab.io/monkey-scripts
// @version 4.1.1
// @author rafaelgssa
// @description Redirects removed games from the Steam store to SteamCommunity or SteamDB.
// @match *://*/*
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require https://greasyfork.org/scripts/405802-monkey-dom/code/Monkey%20DOM.js?version=819513
// @require https://greasyfork.org/scripts/405831-monkey-storage/code/Monkey%20Storage.js?version=819508
// @require https://greasyfork.org/scripts/405813-monkey-utils/code/Monkey%20Utils.js?version=819175
// @require https://greasyfork.org/scripts/405840-monkey-wizard/code/Monkey%20Wizard.js?version=819509
// @run-at document-start
// @grant GM.info
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM_info
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @noframes
// ==/UserScript==

/* global MonkeyDom, MonkeyStorage, MonkeyWizard */

/**
 * @typedef {DestinationMap[keyof DestinationMap]} Destination
 * @typedef {Object} DestinationMap
 * @property {'0'} STEAM_COMMUNITY
 * @property {'1'} STEAM_DB
 * @typedef {'app' | 'sub'} SteamType
 */

(async () => {
	'use strict';

	const scriptId = 'ssr';
	const scriptName = GM.info.script.name;

	/** @type {DestinationMap} */
	const DESTINATIONS = {
		STEAM_COMMUNITY: '0',
		STEAM_DB: '1',
	};

	/** @type {WizardSchema[]} */
	const schemas = [
		{
			type: 'multi',
			id: 'destination',
			message: 'Where do you want to be redirected to?',
			defaultValue: DESTINATIONS.STEAM_COMMUNITY,
			choices: [
				{
					id: '0',
					template: '"%" for SteamCommunity',
					value: DESTINATIONS.STEAM_COMMUNITY,
				},
				{
					id: '1',
					template: '"%" for SteamDB',
					value: DESTINATIONS.STEAM_DB,
				},
			],
		},
	];

	/** @type {MutationObserver} */
	let observer;

	/**
	 * Loads the script.
	 * @returns {Promise<void>}
	 */
	const load = async () => {
		const matches = window.location.href.match(
			/^https:\/\/store\.steampowered\.com\/#(app|sub)_(\d+)/
		);
		if (matches) {
			const [, type, id] = matches;
			await _redirectGame(/** @type {SteamType} */ (type), id);
		} else {
			_removePageUrlFragment();
			_checkPageLoaded();
		}
	};

	/**
	 * Redirects a game to the appropriate page.
	 * @param {SteamType} type The Steam type of the game.
	 * @param {string} id The Steam ID of the game.
	 * @returns {Promise<void>}
	 */
	const _redirectGame = async (type, id) => {
		const destination = /** @type {Destination} */ (await MonkeyStorage.getSetting('destination'));
		const url = _getDestinationUrl(destination);
		window.location.href = `${url}/${type}/${id}`;
	};

	/**
	 * Returns the URL for a destination.
	 * @param {Destination} destination The destination.
	 * @returns {string} The URL for the destination.
	 */
	const _getDestinationUrl = (destination) => {
		/** @type {{ [K in Destination]: string }} */
		const urls = {
			[DESTINATIONS.STEAM_COMMUNITY]: 'https://steamcommunity.com',
			[DESTINATIONS.STEAM_DB]: 'https://steamdb.info',
		};
		return urls[destination];
	};

	/**
	 * Removes the fragment from the page URL.
	 */
	const _removePageUrlFragment = () => {
		if (
			window.location.hostname === 'store.steampowered.com' &&
			(window.location.hash.includes('#app_') || window.location.hash.includes('#sub_'))
		) {
			window.history.replaceState(
				'',
				document.title,
				`${window.location.origin}${window.location.pathname}${window.location.search}`
			);
		}
	};

	/**
	 * Checks if the page is fully loaded.
	 */
	const _checkPageLoaded = () => {
		document.removeEventListener('pjax:end', _checkPageLoaded);
		document.removeEventListener('turbolinks:load', _checkPageLoaded);
		if (document.readyState === 'loading') {
			document.addEventListener('DOMContentLoaded', _onPageLoad);
		} else {
			_onPageLoad();
		}
	};

	/**
	 * Triggered when the page is fully loaded.
	 */
	const _onPageLoad = () => {
		document.removeEventListener('DOMContentLoaded', _onPageLoad);
		_addUrlFragments(document.body);
		if (observer) {
			observer.disconnect();
		}
		observer = MonkeyDom.observeNode(
			document.body,
			null,
			/** @type {NodeCallback} */ (_addUrlFragments)
		);
		document.addEventListener('pjax:end', _checkPageLoaded);
		document.addEventListener('turbolinks:load', _checkPageLoaded);
	};

	/**
	 * Adds the URL fragments to links in a context.
	 * @param {Element} context The context where to add the fragments.
	 */
	const _addUrlFragments = (context) => {
		if (!(context instanceof Element)) {
			return;
		}
		let wasAdded = false;
		const selectors = [
			'[href*="store.steampowered.com/app/"]',
			'[href*="store.steampowered.com/sub/"]',
		].join(', ');
		if (context.matches(selectors)) {
			wasAdded = _addUrlFragment(/** @type {HTMLAnchorElement} */ (context));
		} else {
			/** @type {NodeListOf<HTMLAnchorElement>} */
			const elements = context.querySelectorAll(selectors);
			wasAdded = Array.from(elements).filter(_addUrlFragment).length > 0;
		}
		if (wasAdded && context === document.body) {
			// Keep adding until there are no more links without the fragments.
			window.setTimeout(_addUrlFragments, 1000, context);
		}
	};

	/**
	 * Adds the URL fragment to a link, if not already exists.
	 * @param {HTMLAnchorElement} link The link where to add the fragment.
	 * @returns {boolean} Whether the fragment was added or not.
	 */
	const _addUrlFragment = (link) => {
		const url = link.getAttribute('href');
		let fragment = link.dataset[scriptId];
		if (!url || (fragment && url.includes(fragment))) {
			return false;
		}
		const matches = url.match(/(app|sub)\/(\d+)/);
		if (!matches) {
			return false;
		}
		const [, type, id] = matches;
		fragment = `#${type}_${id}`;
		link.href = `${url.replace(/#.*/, '')}${fragment}`;
		link.dataset[scriptId] = fragment;
		return true;
	};

	try {
		await MonkeyStorage.init(scriptId, {
			settings: {
				destination: DESTINATIONS.STEAM_COMMUNITY,
			},
		});
		await MonkeyWizard.init(scriptId, scriptName, schemas);
		await load();
	} catch (err) {
		console.log(`Failed to load ${scriptName}: `, err);
	}
})();