Steam/GOG Games Links to Free Download Site

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

// ==UserScript==
// @name        Steam/GOG Games Links to Free Download Site
// @namespace   Kozinc
// @version     0.4.8
// @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==

// 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/?q=",           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" },
];

/*
* 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;

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();
  });
}