Greasy Fork

Greasy Fork is available in English.

Steam/GOG Games Links to Free Download Site

Simply adds a pirate link to all games on the GOG store

当前为 2024-09-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Steam/GOG Games Links to Free Download Site
// @namespace   Kozinc
// @version     0.4.7
// @license      MIT
// @description  Simply adds a pirate link to all games on the GOG store
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @match        https://www.gog.com/game/*
// @match        https://www.gog.com/en/game/*
// @match        https://store.steampowered.com/app/*
// @grant		     GM_registerMenuCommand
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM.getValue
// @grant              GM.setValue
// @grant		     GM_deleteValue
// @grant       GM_xmlhttpRequest
// @run-at      document-load
// ==/UserScript==

/*
* usergui.js -- https://github.com/AugmentedWeb/UserGui/raw/Release-1.0/usergui.js
* v1.0.0
* https://github.com/AugmentedWeb/UserGui
* Apache 2.0 licensed
*/

class UserGui {
	constructor() {
		const grantArr = GM_info?.script?.grant;

		if(typeof grantArr == "object") {
			if(!grantArr.includes("GM_xmlhttpRequest")) {
				prompt(`${this.#projectName} needs GM_xmlhttpRequest!\n\nPlease add this to your userscript's header...`, "// @grant       GM_xmlhttpRequest");
			}

			if(!grantArr.includes("GM_getValue")) {
				prompt(`${this.#projectName} needs GM_getValue!\n\nPlease add this to your userscript's header...`, "// @grant       GM_getValue");
			}

			if(!grantArr.includes("GM_setValue")) {
				prompt(`${this.#projectName} needs GM_setValue!\n\nPlease add this to your userscript's header...`, "// @grant       GM_setValue");
			}
		}
	}

	#projectName = "UserGui";
	window = undefined;
	document = undefined;
	iFrame = undefined;
	settings = {
		"window" : {
			"title" : "No title set",
			"name" : "userscript-gui",
			"external" : false,
			"centered" : false,
			"size" : {
				"width" : 300,
				"height" : 500,
				"dynamicSize" : true
			}
		},
		"gui" : {
			"centeredItems" : false,
			"internal" : {
				"darkCloseButton" : false,
				"style" : `
					body {
						background-color: #ffffff;
						overflow: hidden;
						width: 100% !important;
					}

					form {
						padding: 10px;
					}

					#gui {
						height: fit-content;
					}

					.rendered-form {
						padding: 10px;
					}

					#header {
						padding: 10px;
						cursor: move;
						z-index: 10;
						background-color: #2196F3;
						color: #fff;
						height: fit-content;
					}

					.header-item-container {
						display: flex;
						justify-content: space-between;
						align-items: center;
					}

					.left-title {
						font-size: 14px;
						font-weight: bold;
						padding: 0;
						margin: 0;
					}

					#button-close-gui {
						vertical-align: middle;
					}

					div .form-group {
						margin-bottom: 15px;
					}

					#resizer {
						width: 10px;
						height: 10px;
						cursor: se-resize;
						position: absolute;
						bottom: 0;
						right: 0;
					}

					.formbuilder-button {
					    width: fit-content;
					}
				`
			},
			"external" : {
				"popup" : true,
				"style" : `
					.rendered-form {
						padding: 10px;
					}
					div .form-group {
						margin-bottom: 15px;
					}
				`
			}
		},
		"messages" : {
			"blockedPopups" : () => alert(`The GUI (graphical user interface) failed to open!\n\nPossible reason: The popups are blocked.\n\nPlease allow popups for this site. (${window.location.hostname})`)
		}
	};

	// This error page will be shown if the user has not added any pages
	#errorPage = (title, code) => `
		<style>
			.error-page {
				width: 100%;
				height: fit-content;
				background-color: black;
				display: flex;
				justify-content: center;
				align-items: center;
				text-align: center;
				padding: 25px
			}
			.error-page-text {
				font-family: monospace;
				font-size: x-large;
				color: white;
			}
			.error-page-tag {
				margin-top: 20px;
				font-size: 10px;
				color: #4a4a4a;
				font-style: italic;
				margin-bottom: 0px;
			}
		</style>
		<div class="error-page">
			<div>
				<p class="error-page-text">${title}</p>
				<code>${code}</code>
				<p class="error-page-tag">${this.#projectName} error message</p>
			</div>
		</div>`;

	// The user can add multiple pages to their GUI. The pages are stored in this array.
	#guiPages = [
		{
			"name" : "default_no_content_set",
			"content" : this.#errorPage("Content missing", "Gui.setContent(html, tabName);")
		}
	];

	// The userscript manager's xmlHttpRequest is used to bypass CORS limitations (To load Bootstrap)
	async #bypassCors(externalFile) {
		const res = await new Promise(resolve => {
			GM_xmlhttpRequest({
			method: "GET",
			url: externalFile,
			onload: resolve
			});
		});

		return res.responseText;
	}

	// Returns one tab (as HTML) for the navigation tabs
	#createNavigationTab(page) {
		const name = page.name;

		if(name == undefined) {
			console.error(`[${this.#projectName}] Gui.addPage(html, name) <- name missing!`);
			return undefined;
		} else {
			const modifiedName = name.toLowerCase().replaceAll(' ', '').replace(/[^a-zA-Z0-9]/g, '') + Math.floor(Math.random() * 1000000000);

			const content = page.content;
			const indexOnArray = this.#guiPages.map(x => x.name).indexOf(name);
			const firstItem = indexOnArray == 0 ? true : false;

			return {
				"listItem" : `
					<li class="nav-item" role="presentation">
						<button class="nav-link ${firstItem ? 'active' : ''}" id="${modifiedName}-tab" data-bs-toggle="tab" data-bs-target="#${modifiedName}" type="button" role="tab" aria-controls="${modifiedName}" aria-selected="${firstItem}">${name}</button>
					</li>
				`,
				"panelItem" : `
					<div class="tab-pane ${firstItem ? 'active' : ''}" id="${modifiedName}" role="tabpanel" aria-labelledby="${modifiedName}-tab">${content}</div>
				`
			};
		}
	}

	// Make tabs function without bootstrap.js (CSP might block bootstrap and make the GUI nonfunctional)
	#initializeTabs() {
		const handleTabClick = e => {
			const target = e.target;
			const contentID = target.getAttribute("data-bs-target");

			target.classList.add("active");
			this.document.querySelector(contentID).classList.add("active");

			[...this.document.querySelectorAll(".nav-link")].forEach(tab => {
				if(tab != target) {
					const contentID = tab.getAttribute("data-bs-target");

					tab.classList.remove("active");
					this.document.querySelector(contentID).classList.remove("active");
				}
			});
		}

		[...this.document.querySelectorAll(".nav-link")].forEach(tab => {
			tab.addEventListener("click", handleTabClick);
		});
	}

	// Will determine if a navbar is needed, returns either a regular GUI, or a GUI with a navbar
	#getContent() {
		// Only one page has been set, no navigation tabs will be created
		if(this.#guiPages.length == 1) {
			return this.#guiPages[0].content;
		}
		// Multiple pages has been set, dynamically creating the navigation tabs
		else if(this.#guiPages.length > 1) {
			const tabs = (list, panels) => `
				<ul class="nav nav-tabs" id="userscript-tab" role="tablist">
					${list}
				</ul>
				<div class="tab-content">
					${panels}
				</div>
			`;

			let list = ``;
			let panels = ``;

			this.#guiPages.forEach(page => {
				const data = this.#createNavigationTab(page);

				if(data != undefined) {
					list += data.listItem + '\n';
					panels += data.panelItem + '\n';
				}
			});

			return tabs(list, panels);
		}
	}

	// Returns the GUI's whole document as string
	async #createDocument() {
		const bootstrapStyling = await this.#bypassCors("https://raw.githubusercontent.com/AugmentedWeb/UserGui/Release-1.0/resources/bootstrap.css");

		const externalDocument = `
		<!DOCTYPE html>
		<html>
		<head>
			<title>${this.settings.window.title}</title>
			<style>
			${bootstrapStyling}
			${this.settings.gui.external.style}
			${
			this.settings.gui.centeredItems
				? `.form-group {
						display: flex;
						justify-content: center;
					}`
				: ""
			}
			</style>
		</head>
		<body>
		${this.#getContent()}
		</body>
		</html>
		`;

		const internalDocument = `
		<!doctype html>
		<html lang="en">
		<head>
			<style>
			${bootstrapStyling}
			${this.settings.gui.internal.style}
			${
			this.settings.gui.centeredItems
				? `.form-group {
						display: flex;
						justify-content: center;
					}`
				: ""
			}
			</style>
		</head>
		<body>
			<div id="gui">
				<div id="header">
					<div class="header-item-container">
						<h1 class="left-title">${this.settings.window.title}</h1>
						<div class="right-buttons">
							<button type="button" class="${this.settings.gui.internal.darkCloseButton ? "btn-close" : "btn-close btn-close-white"}" aria-label="Close" id="button-close-gui"></button>
						</div>
					</div>
				</div>
				<div id="content">
				${this.#getContent()}
				</div>
				<div id="resizer"></div>
			</div>
		</body>
		</html>
		`;

		if(this.settings.window.external) {
			return externalDocument;
		} else {
			return internalDocument;
		}
	}

	// The user will use this function to add a page to their GUI, with their own HTML (Bootstrap 5)
	addPage(tabName, htmlString) {
		if(this.#guiPages[0].name == "default_no_content_set") {
			this.#guiPages = [];
		}

		this.#guiPages.push({
			"name" : tabName,
			"content" : htmlString
		});
	}

	#getCenterScreenPosition() {
		const guiWidth = this.settings.window.size.width;
		const guiHeight = this.settings.window.size.height;

		const x = (screen.width - guiWidth) / 2;
		const y = (screen.height - guiHeight) / 2;

		return { "x" : x, "y": y };
	}

	#getCenterWindowPosition() {
		const guiWidth = this.settings.window.size.width;
		const guiHeight = this.settings.window.size.height;

		const x = (window.innerWidth - guiWidth) / 2;
		const y = (window.innerHeight - guiHeight) / 2;

		return { "x" : x, "y": y };
	}

	#initializeInternalGuiEvents(iFrame) {
		// - The code below will consist mostly of drag and resize implementations
		// - iFrame window <-> Main window interaction requires these to be done
		// - Basically, iFrame document's event listeners make the whole iFrame move on the main window

		// Sets the iFrame's size
		function setFrameSize(x, y) {
			iFrame.style.width = `${x}px`;
			iFrame.style.height = `${y}px`;
		}

		// Gets the iFrame's size
		function getFrameSize() {
			const frameBounds = iFrame.getBoundingClientRect();

			return { "width" : frameBounds.width, "height" : frameBounds.height };
		}

		// Sets the iFrame's position relative to the main window's document
		function setFramePos(x, y) {
			iFrame.style.left = `${x}px`;
			iFrame.style.top = `${y}px`;
		}

		// Gets the iFrame's position relative to the main document
		function getFramePos() {
			const frameBounds = iFrame.getBoundingClientRect();

			return { "x": frameBounds.x, "y" : frameBounds.y };
		}

		// Gets the frame body's offsetHeight
		function getInnerFrameSize() {
			const innerFrameElem = iFrame.contentDocument.querySelector("#gui");

			return { "x": innerFrameElem.offsetWidth, "y" : innerFrameElem.offsetHeight };
		}

		// Sets the frame's size to the innerframe's size
		const adjustFrameSize = () => {
			const innerFrameSize = getInnerFrameSize();

			setFrameSize(innerFrameSize.x, innerFrameSize.y);
		}

		// Variables for draggable header
		let dragging = false,
			dragStartPos = { "x" : 0, "y" : 0 };

		// Variables for resizer
		let resizing = false,
			mousePos = { "x" : undefined, "y" : undefined },
			lastFrame;

		function handleResize(isInsideFrame, e) {
			if(mousePos.x == undefined && mousePos.y == undefined) {
				mousePos.x = e.clientX;
				mousePos.y = e.clientY;

				lastFrame = isInsideFrame;
			}

			const deltaX = mousePos.x - e.clientX,
				  deltaY = mousePos.y - e.clientY;

			const frameSize = getFrameSize();
			const allowedSize = frameSize.width - deltaX > 160 && frameSize.height - deltaY > 90;

			if(isInsideFrame == lastFrame && allowedSize) {
				setFrameSize(frameSize.width - deltaX, frameSize.height - deltaY);
			}

			mousePos.x = e.clientX;
			mousePos.y = e.clientY;

			lastFrame = isInsideFrame;
		}

		function handleDrag(isInsideFrame, e) {
			const bR = iFrame.getBoundingClientRect();

			const windowWidth = window.innerWidth,
				windowHeight = window.innerHeight;

			let x, y;

			if(isInsideFrame) {
				x = getFramePos().x += e.clientX - dragStartPos.x;
				y = getFramePos().y += e.clientY - dragStartPos.y;
			} else {
				x = e.clientX - dragStartPos.x;
				y = e.clientY - dragStartPos.y;
			}

			// Check out of bounds: left
			if(x <= 0) {
				x = 0
			}

			// Check out of bounds: right
			if(x + bR.width >= windowWidth) {
				x = windowWidth - bR.width;
			}

			// Check out of bounds: top
			if(y <= 0) {
				y = 0;
			}

			// Check out of bounds: bottom
			if(y + bR.height >= windowHeight) {
				y = windowHeight - bR.height;
			}

			setFramePos(x, y);
		}

		// Dragging start (iFrame)
		this.document.querySelector("#header").addEventListener('mousedown', e => {
			e.preventDefault();

			dragging = true;

			dragStartPos.x = e.clientX;
			dragStartPos.y = e.clientY;
		});

		// Resizing start
		this.document.querySelector("#resizer").addEventListener('mousedown', e => {
			e.preventDefault();

			resizing = true;
		});

		// While dragging or resizing (iFrame)
		this.document.addEventListener('mousemove', e => {
			if(dragging)
				handleDrag(true, e);

			if(resizing)
				handleResize(true, e);
		});

		// While dragging or resizing (Main window)
		document.addEventListener('mousemove', e => {
			if(dragging)
				handleDrag(false, e);

			if(resizing)
				handleResize(false, e);
		});

		// Stop dragging and resizing (iFrame)
		this.document.addEventListener('mouseup', e => {
			e.preventDefault();

			dragging = false;
			resizing = false;
		});

		// Stop dragging and resizing (Main window)
		document.addEventListener('mouseup', e => {
			dragging = false;
			resizing = false;
		});

		// Listener for the close button, closes the internal GUI
		this.document.querySelector("#button-close-gui").addEventListener('click', e => {
			e.preventDefault();

			this.close();
		});

		const guiObserver = new MutationObserver(adjustFrameSize);
		const guiElement = this.document.querySelector("#gui");

		guiObserver.observe(guiElement, {
			childList: true,
			subtree: true,
			attributes: true
		});

		adjustFrameSize();
	}

	async #openExternalGui(readyFunction) {
		const noWindow = this.window?.closed;

		if(noWindow || this.window == undefined) {
			let pos = "";
			let windowSettings = "";

			if(this.settings.window.centered && this.settings.gui.external.popup) {
				const centerPos = this.#getCenterScreenPosition();
				pos = `left=${centerPos.x}, top=${centerPos.y}`;
			}

			if(this.settings.gui.external.popup) {
				windowSettings = `width=${this.settings.window.size.width}, height=${this.settings.window.size.height}, ${pos}`;
			}

			// Create a new window for the GUI
			this.window = window.open("", this.settings.windowName, windowSettings);

			if(!this.window) {
				this.settings.messages.blockedPopups();
				return;
			}

			// Write the document to the new window
			this.window.document.open();
			this.window.document.write(await this.#createDocument());
			this.window.document.close();

			if(!this.settings.gui.external.popup) {
				this.window.document.body.style.width = `${this.settings.window.size.width}px`;

				if(this.settings.window.centered) {
					const centerPos = this.#getCenterScreenPosition();

					this.window.document.body.style.position = "absolute";
					this.window.document.body.style.left = `${centerPos.x}px`;
					this.window.document.body.style.top = `${centerPos.y}px`;
				}
			}

			// Dynamic sizing (only height & window.outerHeight no longer works on some browsers...)
			this.window.resizeTo(
				this.settings.window.size.width,
				this.settings.window.size.dynamicSize
					? this.window.document.body.offsetHeight + (this.window.outerHeight - this.window.innerHeight)
					: this.settings.window.size.height
			);

			this.document = this.window.document;

			this.#initializeTabs();

			// Call user's function
			if(typeof readyFunction == "function") {
				readyFunction();
			}

			window.onbeforeunload = () => {
				// Close the GUI if parent window closes
				this.close();
			}
		}

		else {
			// Window was already opened, bring the window back to focus
			this.window.focus();
		}
	}

	async #openInternalGui(readyFunction) {
		if(this.iFrame) {
			return;
		}

		const fadeInSpeedMs = 250;

		let left = 0, top = 0;

		if(this.settings.window.centered) {
			const centerPos = this.#getCenterWindowPosition();

			left = centerPos.x;
			top = centerPos.y;
		}

		const iframe = document.createElement("iframe");
		iframe.srcdoc = await this.#createDocument();
		iframe.style = `
			position: fixed;
			top: ${top}px;
			left: ${left}px;
			width: ${this.settings.window.size.width};
			height: ${this.settings.window.size.height};
			border: 0;
			opacity: 0;
			transition: all ${fadeInSpeedMs/1000}s;
			border-radius: 5px;
			box-shadow: rgb(0 0 0 / 6%) 10px 10px 10px;
			z-index: 2147483647;
		`;

		const waitForBody = setInterval(() => {
			if(document?.body) {
				clearInterval(waitForBody);

				// Prepend the GUI to the document's body
				document.body.prepend(iframe);

				iframe.contentWindow.onload = () => {
					// Fade-in implementation
					setTimeout(() => iframe.style["opacity"] = "1", fadeInSpeedMs/2);
					setTimeout(() => iframe.style["transition"] = "none", fadeInSpeedMs + 500);

					this.window = iframe.contentWindow;
					this.document = iframe.contentDocument;
					this.iFrame = iframe;

					this.#initializeInternalGuiEvents(iframe);
					this.#initializeTabs();

					readyFunction();
				}
			}
		}, 100);
	}

	// Determines if the window is to be opened externally or internally
	open(readyFunction) {
		if(this.settings.window.external) {
			this.#openExternalGui(readyFunction);
		} else {
			this.#openInternalGui(readyFunction);
		}
	}

	// Closes the GUI if it exists
	close() {
		if(this.settings.window.external) {
			if(this.window) {
				this.window.close();
			}
		} else {
			if(this.iFrame) {
				this.iFrame.remove();
				this.iFrame = undefined;
			}
		}
	}

	saveConfig() {
		let config = [];

		if(this.document) {
			[...this.document.querySelectorAll(".form-group")].forEach(elem => {
				const inputElem = elem.querySelector("[name]");

				const name = inputElem.getAttribute("name"),
					  data = this.getData(name);

				if(data) {
					config.push({ "name" : name, "value" : data });
				}
			});
		}

		GM_setValue("config", config);
	}

	loadConfig() {
		const config = this.getConfig();

		if(this.document && config) {
			config.forEach(elemConfig => {
				this.setData(elemConfig.name, elemConfig.value);
			})
		}
	}

	getConfig() {
		return GM_getValue("config");
	}

	resetConfig() {
		const config = this.getConfig();

		if(config) {
			GM_setValue("config", []);
		}
	}

	dispatchFormEvent(name) {
		const type = name.split("-")[0].toLowerCase();
		const properties = this.#typeProperties.find(x => type == x.type);
		const event = new Event(properties.event);

		const field = this.document.querySelector(`.field-${name}`);
		field.dispatchEvent(event);
	}

	setPrimaryColor(hex) {
		const styles = `
		#header {
			background-color: ${hex} !important;
		}
		.nav-link {
			color: ${hex} !important;
		}
		.text-primary {
			color: ${hex} !important;
		}
		`;

		const styleSheet = document.createElement("style")
		styleSheet.innerText = styles;
		this.document.head.appendChild(styleSheet);
	}

	// Creates an event listener a GUI element
	event(name, event, eventFunction) {
		this.document.querySelector(`.field-${name}`).addEventListener(event, eventFunction);
	}

	// Disables a GUI element
	disable(name) {
		[...this.document.querySelector(`.field-${name}`).children].forEach(childElem => {
			childElem.setAttribute("disabled", "true");
		});
	}

	// Enables a GUI element
	enable(name) {
		[...this.document.querySelector(`.field-${name}`).children].forEach(childElem => {
			if(childElem.getAttribute("disabled")) {
				childElem.removeAttribute("disabled");
			}
		});
	}

	// Gets data from types: TEXT FIELD, TEXTAREA, DATE FIELD & NUMBER
	getValue(name) {
		return this.document.querySelector(`.field-${name}`).querySelector(`[id=${name}]`).value;
	}

	// Sets data to types: TEXT FIELD, TEXT AREA, DATE FIELD & NUMBER
	setValue(name, newValue) {
		this.document.querySelector(`.field-${name}`).querySelector(`[id=${name}]`).value = newValue;

		this.dispatchFormEvent(name);
	}

	// Gets data from types: RADIO GROUP
	getSelection(name) {
		return this.document.querySelector(`.field-${name}`).querySelector(`input[name=${name}]:checked`).value;
	}

	// Sets data to types: RADIO GROUP
	setSelection(name, newOptionsValue) {
		this.document.querySelector(`.field-${name}`).querySelector(`input[value=${newOptionsValue}]`).checked = true;

		this.dispatchFormEvent(name);
	}

	// Gets data from types: CHECKBOX GROUP
	getChecked(name) {
		return [...this.document.querySelector(`.field-${name}`).querySelectorAll(`input[name*=${name}]:checked`)]
			.map(checkbox => checkbox.value);
	}

	// Sets data to types: CHECKBOX GROUP
	setChecked(name, checkedArr) {
		const checkboxes = [...this.document.querySelector(`.field-${name}`).querySelectorAll(`input[name*=${name}]`)]

		checkboxes.forEach(checkbox => {
			if(checkedArr.includes(checkbox.value)) {
				checkbox.checked = true;
			}
		});

		this.dispatchFormEvent(name);
	}

	// Gets data from types: FILE UPLOAD
	getFiles(name) {
		return this.document.querySelector(`.field-${name}`).querySelector(`input[id=${name}]`).files;
	}

	// Gets data from types: SELECT
	getOption(name) {
		const selectedArr = [...this.document.querySelector(`.field-${name} #${name}`).selectedOptions].map(({value}) => value);

		return selectedArr.length == 1 ? selectedArr[0] : selectedArr;
	}

	// Sets data to types: SELECT
	setOption(name, newOptionsValue) {
		if(typeof newOptionsValue == 'object') {
		    newOptionsValue.forEach(optionVal => {
			this.document.querySelector(`.field-${name}`).querySelector(`option[value=${optionVal}]`).selected = true;
		    });
		} else {
		    this.document.querySelector(`.field-${name}`).querySelector(`option[value=${newOptionsValue}]`).selected = true;
		}

		this.dispatchFormEvent(name);
	}

	#typeProperties = [
		{
			"type": "button",
			"event": "click",
			"function": {
				"get" : null,
				"set" : null
			}
		},
		{
			"type": "radio",
			"event": "change",
			"function": {
				"get" : n => this.getSelection(n),
				"set" : (n, nV) => this.setSelection(n, nV)
			}
		},
		{
			"type": "checkbox",
			"event": "change",
			"function": {
				"get" : n => this.getChecked(n),
				"set" : (n, nV) => this.setChecked(n, nV)
			}
		},
		{
			"type": "date",
			"event": "change",
			"function": {
				"get" : n => this.getValue(n),
				"set" : (n, nV) => this.setValue(n, nV)
			}
		},
		{
			"type": "file",
			"event": "change",
			"function": {
				"get" : n => this.getFiles(n),
				"set" : null
			}
		},
		{
			"type": "number",
			"event": "input",
			"function": {
				"get" : n => this.getValue(n),
				"set" : (n, nV) => this.setValue(n, nV)
			}
		},
		{
			"type": "select",
			"event": "change",
			"function": {
				"get" : n => this.getOption(n),
				"set" : (n, nV) => this.setOption(n, nV)
			}
		},
		{
			"type": "text",
			"event": "input",
			"function": {
				"get" : n => this.getValue(n),
				"set" : (n, nV) => this.setValue(n, nV)
			}
		},
		{
			"type": "textarea",
			"event": "input",
			"function": {
				"get" : n => this.getValue(n),
				"set" : (n, nV) => this.setValue(n, nV)
			}
		},
	];

	// The same as the event() function, but automatically determines the best listener type for the element
	// (e.g. button -> listen for "click", textarea -> listen for "input")
	smartEvent(name, eventFunction) {
		if(name.includes("-")) {
			const type = name.split("-")[0].toLowerCase();
			const properties = this.#typeProperties.find(x => type == x.type);

			if(typeof properties == "object") {
				this.event(name, properties.event, eventFunction);

			} else {
				console.warn(`${this.#projectName}'s smartEvent function did not find any matches for the type "${type}". The event could not be made.`);
			}

		} else {
			console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s smartEvent. The event could not be made.`);
		}
	}

	// Will automatically determine the suitable function for data retrivial
	// (e.g. file select -> use getFiles() function)
	getData(name) {
		if(name.includes("-")) {
			const type = name.split("-")[0].toLowerCase();
			const properties = this.#typeProperties.find(x => type == x.type);

			if(typeof properties == "object") {
				const getFunction = properties.function.get;

				if(typeof getFunction == "function") {
					return getFunction(name);

				} else {
					console.error(`${this.#projectName}'s getData function can't be used for the type "${type}". The data can't be taken.`);
				}

			} else {
				console.warn(`${this.#projectName}'s getData function did not find any matches for the type "${type}". The event could not be made.`);
			}

		} else {
			console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s getData function. The event could not be made.`);
		}
	}

	// Will automatically determine the suitable function for data retrivial (e.g. checkbox -> use setChecked() function)
	setData(name, newData) {
		if(name.includes("-")) {
			const type = name.split("-")[0].toLowerCase();
			const properties = this.#typeProperties.find(x => type == x.type);

			if(typeof properties == "object") {
				const setFunction = properties.function.set;

				if(typeof setFunction == "function") {
					return setFunction(name, newData);

				} else {
					console.error(`${this.#projectName}'s setData function can't be used for the type "${type}". The data can't be taken.`);
				}

			} else {
				console.warn(`${this.#projectName}'s setData function did not find any matches for the type "${type}". The event could not be made.`);
			}

		} else {
			console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s setData function. The event could not be made.`);
		}
	}
};

const Gui = new UserGui;
Gui.settings.window.title = "Pirate Games Links Settings";
Gui.settings.window.centered = true;

// Default buttonSet
var buttonSet = [
	  { url: "https://steamrip.com/?s=",           title: "SteamRIP",         urlSpecial: "" },
    { url: "https://www.ovagames.com/?s=",       title: "OVA Games",        urlSpecial: "" },
	  { url: "https://fitgirl-repacks.site/?s=",   title: "FitGirl",          urlSpecial: "" },
	  { url: "https://dodi-repacks.site/?s=",      title: "DODI",             urlSpecial: "" },
	  { url: "https://gload.to/?s=",               title: "Gload",            urlSpecial: "" },
    { url: "https://search.rlsbb.ru/?s=",        title: "Release BB",       urlSpecial: "" },
    { url: "https://scnlog.me/?s=",              title: "SCNLOG",           urlSpecial: "" },
    { url: "https://cpgrepacks.site/?s=",        title: "CPG Repacks",      urlSpecial: "" },
    { url: "https://www.tiny-repacks.win/?s=",   title: "Tiny Repacks",     urlSpecial: "" },
    { url: "https://g4u.to/en/search/?str=",     title: "g4u",              urlSpecial: "" },
    { url: "https://gog-games.to/search/",       title: "GOG-Games.to",     urlSpecial: "" },
];
var unsafeButtonSet = [
    { url: "https://gogunlocked.com/?s=",        title: "GOG Unlocked",     urlSpecial: "" },
    { url: "https://igg-games.com/?s=",          title: "IGG",              urlSpecial: "" },
    { url: "https://pcgamestorrents.com/?s=",    title: "PC games Torrent", urlSpecial: "" },
];

var siteSet = [
    { url: "https://www.gog.com/game/*",           title: "GOG",            urlSpecial: "" },
    { url: "https://www.gog.com/en/game/*",        title: "GOG",            urlSpecial: "" },
    { url: "https://store.steampowered.com/app/*", title: "Steam",          urlSpecial: "" },
//    { url: /https:\/\/igg-games.com\/.*.html/,     title: "IGG" },
];


var p = GM_getValue("enableUnsafeButtonSet", null);
if(p === "true") {
  // unsafeButtonSet
  buttonSet = [...buttonSet, ...unsafeButtonSet];
}

var steamDisplaySidebar = GM_getValue("steamDisplaySidebar", true);
var steamDisplayCart    = GM_getValue("steamDisplayCart",    false);


var gogDisplaySidebar = GM_getValue("gogDisplaySidebar", true);
var gogDisplayCart    = GM_getValue("gogDisplayCart",    false);

var siteSetResult = "";

siteSet.forEach((el) => {
    if(!!document.URL.match(el.url)) siteSetResult = el.title;
})

// Load saved buttonSet preference
let savedButtonSet = GM_getValue("enabledButtonSet", []);
if(savedButtonSet.length === 0) {
  savedButtonSet = buttonSet;
}

Gui.addPage("Settings", `
<div class="rendered-form">
    <div class="">
        <h2 access="false" class="text-primary" id="control-274549">Button Settings</h2>
    </div>
    <div class="checkbox-group formbuilder-checkbox-group form-group field-checkbox-group-steamDisplay">
        <div class="formbuilder-checkbox-group form-group field-checkbox-group-steamDisplay">
            <label for="checkbox-group-steamDisplay" class="formbuilder-checkbox-group-label">Steam display:</label>
            <div class="checkbox-group-steamDisplay">
                <div class="formbuilder-checkbox-inline">
                    <label for="checkbox-group-steamDisplay-0" class="kc-toggle">
                        <input name="checkbox-group-steamDisplay[]" access="false" id="checkbox-group-steamDisplay-0" value="steamDisplaySidebar" ${steamDisplaySidebar ? 'checked' : ''} type="checkbox"><span></span>Sidebar</label>
                </div>
                <div class="formbuilder-checkbox-inline">
                    <label for="checkbox-group-steamDisplayCart-1" class="kc-toggle">
                        <input name="checkbox-group-steamDisplayCart[]" access="false" id="checkbox-group-steamDisplayCart-1" value="steamDisplayCart" ${steamDisplayCart ? 'checked' : ''} type="checkbox"><span></span>Cart</label>
                </div>
            </div>
        </div>
    </div>
    <div class="checkbox-group formbuilder-checkbox-group form-group field-checkbox-group-gogDisplay">
        <div class="formbuilder-checkbox-group form-group field-checkbox-group-gogDisplay">
            <label for="checkbox-group-gogDisplay" class="formbuilder-checkbox-group-label">GOG display:</label>
            <div class="checkbox-group-gogDisplay">
                <div class="formbuilder-checkbox-inline">
                    <label for="checkbox-group-gogDisplay-0" class="kc-toggle">
                        <input name="checkbox-group-gogDisplay[]" access="false" id="checkbox-group-gogDisplay-0" value="gogDisplaySidebar" ${gogDisplaySidebar ? 'checked' : ''} type="checkbox"><span></span>Sidebar</label>
                </div>
                <div class="formbuilder-checkbox-inline">
                    <label for="checkbox-group-gogDisplayCart-1" class="kc-toggle">
                        <input name="checkbox-group-gogDisplayCart[]" access="false" id="checkbox-group-gogDisplayCart-1" value="gogDisplayCart" ${gogDisplayCart ? 'checked' : ''} type="checkbox"><span></span>Cart</label>
                </div>
            </div>
        </div>
    </div>
    <div class="checkbox-group formbuilder-checkbox-group form-group field-checkbox-group-saved">
        <h3>Toggle Buttons:</h3>
        <div class="checkbox-group-saved">
            ${buttonSet.map((button, index) => `
                <div class="formbuilder-checkbox">
                    <input name="checkbox-group-saved[]" id="checkbox-group-saved-${index}" type="checkbox" value="${button.title}"  ${savedButtonSet.some(item => item.title.includes(button.title)) ? 'checked' : ''} ${savedButtonSet.some(item => item.title.includes(button.title)) ? 'checked="checked"' : ''}>
                    <label for="checkbox-group-saved-${index}">${button.title}</label>
                </div>
            `).join('')}
        </div>
    </div>
    <div class="formbuilder-button form-group field-button-save-config">
        <button type="button" class="btn-success btn" name="button-save-config" access="false" style="success" id="button-save-config">Save</button>
    </div>
</div>
`);

function applyButtonSettings() {
    const enabledButtonSet = [];
    [...document.querySelectorAll('[id^="button-toggle-"]')].forEach((checkbox, index) => {
        if (checkbox.checked) {
            enabledButtonSet.push(index);
        }
    });

}

function openSettingsGui() {
    Gui.open(() => {
        Gui.smartEvent("button-save-config", (data) => {
            const buttons = Gui.getData("checkbox-group-saved");
            const steamDisplay = Gui.getData("checkbox-group-steamDisplay");
            const gogDisplay = Gui.getData("checkbox-group-gogDisplay");
            GM_setValue("enabledButtonSet", buttonSet.filter(item => buttons.includes(item.title)));
            GM_setValue("steamDisplaySidebar", steamDisplay.includes("steamDisplaySidebar"));
            GM_setValue("steamDisplayCart",    steamDisplay.includes("steamDisplayCart"));
            GM_setValue("gogDisplaySidebar",   gogDisplay.includes("gogDisplaySidebar"));
            GM_setValue("gogDisplayCart",      gogDisplay.includes("gogDisplayCart"));
            // Gui.saveConfig();
            location.reload(); // Reload the page to reflect changes
        });
        Gui.loadConfig();
    });
}




var appName = "";
switch(siteSetResult) {
    case "GOG":
        appName = document.getElementsByClassName("productcard-basics__title")[0].textContent;
        appName = appName.trim().replace(/[^a-zA-Z0-9' ]/g, '');
        if (gogDisplayCart) {
            savedButtonSet.forEach((el) => {
                $("button.cart-button")[0].parentElement.parentElement.append(furnishGOG(el.url+appName, el.title))
            })
        }
        if (gogDisplaySidebar) {
            /*
            <div class="table__row details__row">
                <div class="details__category table__row-label">Genre:</div>
                <div class="details__content table__row-content">
                    <a href="" class="details__link ng-scope">Role-playing</a>
                </div>
            </div>
            */
            const tableRow = document.createElement('div');
            tableRow.classList.add('table__row', 'details__row');

            // Create the category div
            const categoryDiv = document.createElement('div');
            categoryDiv.classList.add('details__category', 'table__row-label');
            categoryDiv.textContent = 'Search for ' + appName + ':';

            // Create the content div
            const contentDiv = document.createElement('div');
            contentDiv.classList.add('details__content', 'table__row-content');

            savedButtonSet.forEach((el, index) => {
                const anchor = document.createElement('a');
                anchor.href = el.url+appName; // You can set the href attribute value as needed
                anchor.target = '_blank';
                anchor.classList.add('details__link', 'ng-scope');
                anchor.textContent = el.title;
                contentDiv.appendChild(anchor);

                if (index < savedButtonSet.length - 1) {
                    const lineBreak = document.createElement('br');
                    contentDiv.appendChild(lineBreak);
                    // const comma = document.createTextNode(', ');
                    // contentDiv.appendChild(comma);
                }
            })
            tableRow.appendChild(categoryDiv);
            tableRow.appendChild(contentDiv);

            // Finally, append the entire structure to the desired parent element in the DOM
            document.querySelector("div.details.table.table--without-border.ng-scope").prepend(tableRow); // Or append to a specific element
        }
        break;
    case "Steam":
        appName = document.getElementsByClassName("apphub_AppName")[0].textContent;
        appName = appName.trim().replace(/[^a-zA-Z0-9' ]/g, '');
        // $(".game_purchase_action_bg:first").css({"height": "32px"}); remove

        if (steamDisplayCart) {
            $(".game_purchase_action_bg:first").css({
                "height": "50px",
                "max-width": "500px",
                "text-wrap": "wrap"
            });
        }

        //////////
        if (steamDisplaySidebar) {
            // Sidebar for Steam
            // $(".glance_ctn_responsive_left:first").append(' <div class="dev_row"><div class="subtitle column"><br></div></div><hr><br>');
            $(".block.responsive_apppage_details_left:first").parent().prepend(' <div class="block responsive_apppage_details_left" ><div><div style="color: #8f98a0;margin-bottom: 6px;">Search for ' + appName +': </div></div> ');


            // Create and insert the style element for custom CSS rules
            var style = document.createElement('style');
            style.innerHTML = `
                .pirate_row {
                    display: flex;
                }
                .pirate_row, .pirate_row .column {
                    white-space: normal !important;
                }
                .pirate_row .column {
                    color: #556772;
                }
                .pirate_row .subtitle {
                    text-transform: uppercase;
                    font-size: 10px;
                    padding-right: 10px;
                    min-width: 120px;
                }
                .pirate_row .summary {
                    overflow: hidden;
                    text-overflow: ellipsis;
                    color: #556772;
                }
                .pirate_row:hover {
                    background-color: #333; /* Dark grey background on hover */
                }
            `;
            document.head.appendChild(style);
        }
        ////////////

        if (steamDisplaySidebar) {
            savedButtonSet.forEach((el) => {
                $(".block.responsive_apppage_details_left:first").append(furnishSteamSidebar(el.url+appName + el.urlSpecial, el.title, appName))
                // $(".glance_ctn_responsive_left:first").append(furnishSteamSidebar(el.url+appName + el.urlSpecial, el.title, appName))
            })
        }
        if (steamDisplayCart) {
            savedButtonSet.forEach((el) => {
                $(".game_purchase_action_bg:first").append(furnishSteam(el.url+appName + el.urlSpecial, el.title))
            })
        }

        break;
    case "IGG":
        appName = $(".uk-article-title")[0].innerHTML.replace(" Free Download","");
        appName = appName.trim().replace(/[^a-zA-Z0-9 ]/g, '');
        savedButtonSet.forEach((el) => {
            $(".uk-article-meta")[0].append("  --  ")
            $(".uk-article-meta")[0].append(furnishIGG(el.url+appName, el.title))
        })
        break;
}

function furnishGOG(href, innerHTML) {
    let element = document.createElement("a");
    element.target= "_blank";
    element.style = "margin: 5px 0 5px 0 !important; padding: 5px 10px 5px 10px;";
    element.classList.add("button");
    //element.classList.add("button--small");
    element.classList.add("button--big");
    element.classList.add("cart-button");
    element.classList.add("ng-scope");
    element.href = href;
    element.innerHTML= innerHTML;
    return element;
}
function furnishSteam(href, innerHTML) {
    let element = document.createElement("a");
    element.target= "_blank";
    element.style = "margin-left: 10px; padding-right: 10px;";
    element.href = href;
    element.innerHTML= innerHTML;
    return element;
}
function furnishSteamSidebar(searchUrl, appName, gameName) {
    // Create the main container div
    var devRowDiv = document.createElement('div');
    devRowDiv.className = 'dev_row pirate_row';

    // Create the subtitle div
    var subtitleDiv = document.createElement('div');
    subtitleDiv.className = 'subtitle column';
    subtitleDiv.innerHTML = appName + ':';

    // Create the summary div
    var summaryDiv = document.createElement('div');
    summaryDiv.className = 'summary column';

    // Create the anchor element
    var anchor = document.createElement('a');
    anchor.href = searchUrl;
    anchor.target = '_blank';
    // anchor.innerHTML = 'Search ' + appName + ' for ' + gameName;
    anchor.innerHTML = appName;

    // Append the anchor to the summary div
    summaryDiv.appendChild(anchor);

    // Append the subtitle and summary divs to the main container div
    devRowDiv.appendChild(subtitleDiv);
    devRowDiv.appendChild(summaryDiv);

    // Return the created element
    return devRowDiv;
}

function furnishIGG(href, innerHTML) {
    let element = document.createElement("a");
    element.target= "_blank";
    element.href = href;
    element.innerHTML= innerHTML;
    return element;
}



try{ GM_registerMenuCommand = GM_registerMenuCommand || this.GM_registerMenuCommand; }catch(e){ GM_registerMenuCommand = false; }

if(p !== "true"){
  if(GM_registerMenuCommand){
    GM_registerMenuCommand('Show unsafe websites', function(){
      if(confirm('Are you sure you want to show possibly unsafe websites?\n'+
        '(It can be hidden later with this menu)')){
        GM_setValue("enableUnsafeButtonSet", "true");
        GM_deleteValue("enabledButtonSet");
        location.reload();
      }
    });
  }
} else if (GM_registerMenuCommand) {
  GM_registerMenuCommand('Hide unsafe websites', function(){
    if(confirm('Are you sure you want to hide possibly unsafe websites?\n'+
        '(It can be shown later with this menu)')){
      GM_deleteValue("enableUnsafeButtonSet");
      GM_deleteValue("enabledButtonSet");
      location.reload();
    }
  });
}

if (GM_registerMenuCommand) {
  GM_registerMenuCommand('Open Settings GUI', function(){
    openSettingsGui();
  });
}
if (GM_registerMenuCommand) {
  GM_registerMenuCommand('Reset settings', function(){
      GM_deleteValue("enableUnsafeButtonSet");
      GM_deleteValue("enabledButtonSet");
      GM_deleteValue("steamDisplaySidebar");
      GM_deleteValue("steamDisplayCart");
      GM_deleteValue("gogDisplaySidebar");
      GM_deleteValue("gogDisplayCart");
      location.reload();
  });
}