Coolakov Fixes GFD2

Enhances the Coolakov most_promoted tool with custom layout, font settings, special buttons, link controls, and regex highlighting.

// ==UserScript==
// @name         Coolakov Fixes GFD2
// @namespace    coolakov
// @version      1.3.2
// @description  Enhances the Coolakov most_promoted tool with custom layout, font settings, special buttons, link controls, and regex highlighting.
// @author       GreatFireDragon
// @match        https://coolakov.ru/tools/most_promoted/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=coolakov.ru
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      MIT
// @run-at       document-end
// ==/UserScript==

const $ = window.jQuery;
const regexAmount = 5; // Amount of Regexes
const types = ["1","2","3","4","5"]; // Updated to 5 categories
const emojis = ["🌑", "🌒", "🌓", "🌔", "🌕"]; // Array of 5 different emojis

// Font styles
const fontStyles = {
	Arial: "Arial, sans-serif",
	"Courier New": "Courier New, monospace",
	Cursive: "cursive",
	Georgia: "Georgia, serif",
	"Garamond Premier Pro": "Garamond Premier Pro, serif",
	"Lucida Bright": "Lucida Bright, sans-serif",
	"Lucida Console": "Lucida Console, monospace",
	"Lucida Grande": "Lucida Grande, sans-serif",
	"Lucida Sans": "Lucida Sans, sans-serif",
	"Lucida Sans Typewriter": "Lucida Sans Typewriter, sans-serif",
	"Lucida Sans Unicode": "Lucida Sans Unicode, sans-serif",
	Monospace: "monospace",
	Serif: "serif",
	"Times New Roman": "Times New Roman, serif",
	Courier: "Courier, monospace",
	"Fira Code": "'Fira Code', monospace"
};

// Data migration (optional)
const migrateData = () => {
	const mappings = {
		"GFD_goodLinks": "GFD_1Links",
		"GFD_neutralLinks": "GFD_2Links",
		"GFD_badLinks": "GFD_3Links"
	};
	Object.keys(mappings).forEach(oldKey => {
		if (localStorage.getItem(oldKey)) {
			localStorage.setItem(mappings[oldKey], localStorage.getItem(oldKey));
			localStorage.removeItem(oldKey);
		}
	});
};
migrateData();

// Load and save settings
const loadSettings = () => {
	const settings = JSON.parse(localStorage.getItem("GFD_settings")) || {};
	const { fontStyle = "" } = settings;
	$("#GFD_fontStyle").val(fontStyle);
	$("body").css("font-family", fontStyle);
};
const saveSettings = () => {
	localStorage.setItem("GFD_settings", JSON.stringify({ fontStyle: $("#GFD_fontStyle").val() }));
};

// Navbar font style control
$("#navbar-header").append(
	$("<select>", {
		id: "GFD_fontStyle",
		title: "Стиль шрифта",
		change: e => { $("body").css("font-family", e.target.value); saveSettings(); }
	}).append(
		Object.entries(fontStyles).map(([key, value]) => $("<option>", { value, text: key }))
	).val(JSON.parse(localStorage.getItem("GFD_settings") || '{}').fontStyle || "serif")
);
loadSettings();

// Remove specific span
$("#myform > div:nth-child(5) > label > span").remove();

// Special Buttons
let clickCounter = parseInt(localStorage.getItem('buttonClickCounter')) || 0;
const updateCounter = () => {
	localStorage.setItem('buttonClickCounter', ++clickCounter);
	console.log(`Количество нажатий: ${clickCounter}`);
};

function processTextarea(transformFn) {
	const textarea = $("#myform > div:nth-child(5) > textarea");
	const lines = textarea.val()
	.split('\n')
	.map(line => line.replace(/[+:.\-\?!#_]/g, ' ').replace(/\(\d+\)/g, ''))
	.filter(line => line.trim() !== '')
	.map(transformFn)
	.map(line => line.replace(/\s\s+/g, ' ').trim())
	.join('\n')
	textarea.val(lines);
	updateCounter();
}

// FORM ACTIONS
// Create the default "Собрать выдачу" button.
$("<button>", {
    id: "GFD_trimSpecialChars", tabindex: 9,
    text: "Собрать выдачу", class: "GFD_specialButton"
})
.on("click", () => {
    processTextarea(line => line);
})
.appendTo("#myform > div:nth-child(6)");

// Create an input for comma-separated phrases and append it to the navbar.
const $input = $("<input>", {
    id: "phrase-input",
    placeholder: "Введите фразы, разделённые запятой..."
});
$("#navbar-header").append($input);

// Load saved phrases from localStorage (if any) and set them in the input.
const savedPhrases = localStorage.getItem("phrases");
if (savedPhrases) {
    $input.val(savedPhrases);
}

// Create (or select) a container for dynamic buttons.
// Using a separate container ensures the default button is not overwritten.
let $dynamicContainer = $("#dynamic-buttons-container");
if (!$dynamicContainer.length) {
    $dynamicContainer = $("<div>", { id: "dynamic-buttons-container" });
    $("#myform > div:nth-child(6)").append($dynamicContainer);
}

// Utility function to escape regex special characters in a phrase.
function escapeRegExp(string) {return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');}

// Create buttons: '-' removes the phrase, '+' appends it if absent.
function createButtons() {
  $dynamicContainer.empty();
  const phrases = $input.val().split(",").map(s => s.trim()).filter(Boolean);
  phrases.forEach(phrase => {
    const $minus = $("<button>", {
      text: `- '${phrase}'`, tabindex: 9, class: "GFD_specialButton"
    }).on("click", () =>
      processTextarea(line => line.replace(new RegExp(escapeRegExp(phrase), 'g'), ''))
    );

    const $plus = $("<button>", {
      text: `+ '${phrase}'`, tabindex: 9, class: "GFD_specialButton"
    }).on("click", () =>
      processTextarea(line => new RegExp(escapeRegExp(phrase)).test(line) ? line : `${line} ${phrase}` )
    );

    $dynamicContainer.append($minus, $plus);
  });
}

// Update buttons (and save to localStorage) after the user stops typing.
let debounceTimer;
$input.on("input", function() {
    clearTimeout(debounceTimer);
    // Save the current phrases into localStorage.
    localStorage.setItem("phrases", $input.val());
    debounceTimer = setTimeout(createButtons, 500);
});

// Also update buttons on blur (when the input loses focus).
$input.on("blur", function() {
    localStorage.setItem("phrases", $input.val());
    createButtons();
});

// Create initial buttons from the loaded phrases.
createButtons();

// Link Controls
const linkDiv = $("<div>", { class: "GFD_linksControl" });
const createTextarea = key => $("<textarea>").val(decodeURI(localStorage.getItem(key) ?? ""));

// Create 5 link textareas and clear buttons using a loop
const linkControls = types.map(t => createTextarea(`GFD_${t}Links`));
linkControls.forEach((ta, i) => {
	linkDiv.append(
		ta,
		$("<button>", { text: "🧹 Clear " + types[i] }).on("click", () => clearLinks(types[i]))
	);
});
$("main.main div.container").eq(2).append(linkDiv);

// Remove first header container
$("main.main div.container").eq(0).remove();

let intervalId;
const observer = new MutationObserver(() => {
    const table = $("#myTable");
    if (table.length) {
        parseTable(table);
        $(".header").eq(3).text("#");
        parseAndHighlightRegexp();
        // Clear the previous interval if it exists
        if (intervalId) { clearInterval(intervalId); }
        intervalId = setInterval(parseAndHighlightRegexp, 100);
        updateCounters();
    }
});
observer.observe($("#result")[0], { childList: true });


// Create 5 RegExp highlight textareas
for (let i = 0; i < regexAmount; i++) {
	const storageKey = `GFD_highlightRegexp${i + 1}`;
	const storedValue = localStorage.getItem(storageKey) || "";
	$("<textarea>", {
		id: `highlightRegExpTextarea${i + 1}`,
		placeholder: `RegExp highlight ${i + 1}`
	}).val(storedValue).on("input", e => {
		localStorage.setItem(storageKey, e.target.value);
		parseAndHighlightRegexp();
		updateCounters(); // Update counters when regex textareas change
	}).appendTo(linkDiv);
}

// Function to generate regex lists
const getRegexLists = () => Array.from({ length: regexAmount }, (_, i) =>
	(localStorage.getItem(`GFD_highlightRegexp${i + 1}`) || "")
	.split("\n").map(r => r.trim())
	.filter(r => r.length >= 2)
	.map(r => { try { return new RegExp(r, 'i') } catch { return null } })
	.filter(Boolean)
);

// Function to parse and highlight using regex
function parseAndHighlightRegexp() {
	const regexLists = getRegexLists(); // Generate regex lists

	// Remove existing highlight classes
	$("tbody tr").removeClass(
		Array.from({ length: regexAmount }, (_, i) => `GFD_highlight${i + 1}`).join(" ")
	);

	// Highlight matching rows
	$("tbody tr").each(function () {
		const $row = $(this);

		$row.find("td, a").each(function () {
			const cellText = $(this).text(); // Get the text content of the <td>

			regexLists.forEach((regexList, i) => {
				if (regexList.some(regexp => regexp.test(cellText))) {
					$row.addClass(`GFD_highlight${i + 1}`);
				}
			});
		});
	});
};

// Function to calculate and update counters
function updateCounters() {
	const regexLists = getRegexLists(); // Generate regex lists
	const combinedCounters = Array(regexAmount).fill(0); // Single counter array for active + regex

	// Count active and regex links
	$(".ellipsis").each(function () {
		const $row = $(this);

		// Check for active buttons in the row
		let hasActive = false;
		types.forEach((type, index) => {
			if ($row.find(`.GFD_${type}Active`).length) {
				combinedCounters[index]++;
				hasActive = true;
			}
		});

		// If no active button, process regex highlights
		if (!hasActive) {
			$row.find("a").each(function () {
				const linkText = $(this).text();
				regexLists.forEach((regexList, i) => {
					if (regexList.some(regexp => regexp.test(linkText))) {
						combinedCounters[i]++;
					}
				});
			});
		}
	});

	// Update or create the counter container
	const $counterContainer = $('#result div:first').find('#highlightCounters').length
	? $('#result div:first').find('#highlightCounters').empty()
	: $('<div>', { id: 'highlightCounters' }).appendTo($('#result div:first'));

	// Add combined counters
	combinedCounters.forEach(count => $counterContainer.append(`<p>${count}</p>`));
};


// Parse table
function parseTable(table) {
	$("tbody tr", table).each(function() {
		const linkCell = $(this).find("td:nth-child(2) a");
		const trimmedHref = linkCell.attr("href");

		let linkText;
		try {
			linkText = decodeURIComponent(trimmedHref.replace(/^https?:\/\//i, "").replace(/^www\./, ""));
		} catch (e) {
			linkText = trimmedHref.replace(/^https?:\/\//i, "").replace(/^www\./, "");
		}

		const linkParts = linkText.split("/").filter(Boolean);
		linkCell.empty().append(
			linkParts.map((part, index) =>
				$("<span>", { class: index === 0 ? "GFD_domain" : index === 1 ? "GFD_category" : "", text: part })
				.append(index < linkParts.length - 1 ? "/" : "")
			)
		);

		// Add buttons for 1 to 5
		let activeButton = null;
		types.forEach((type, index) => {
			const links = localStorage.getItem(`GFD_${type}Links`)
			? decodeURI(localStorage.getItem(`GFD_${type}Links`)).split("\n") : [];
			const isActive = links.includes(trimmedHref);
			const button = $("<button>", {
				text: isActive ? "❌" : emojis[index], // Assign emoji based on type
				class: isActive ? `GFD_${type}Active` : ""
			}).on("click", () => handleButtonClick(button, type, trimmedHref));
			linkCell.parent().append(button);
			if (isActive) activeButton = button;
		});
		if (activeButton) linkCell.parent().find("button").not(activeButton).prop("disabled", true);

		// Favicon (click does nothing now)
		const domain = trimmedHref.split("/")[2].replace("www.", "");
		$("<img>", {
			src: `https://www.google.com/s2/favicons?sz=128&domain=${trimmedHref}`,
			"data-domain": domain,
			title: domain,
		}).appendTo($(this).find("td:first"));
	});
};

// Handle button clicks
const handleButtonClick = (button, type, href) => {
	const index = types.indexOf(type); // Find the index of the type
	let links = localStorage.getItem(`GFD_${type}Links`)
	? decodeURI(localStorage.getItem(`GFD_${type}Links`)).split("\n") : [];

	if ($(button).text() === "❌") {
		// If the button is active, deactivate it
		links = links.filter(item => item !== href);
		$(button).text(emojis[index]).removeClass(`GFD_${type}Active`); // Set emoji from the array
		$(button).siblings("button").prop("disabled", false);
	} else {
		// If the button is inactive, activate it
		links.push(href);
		$(button).text("❌").addClass(`GFD_${type}Active`); // Set active state
		$(button).siblings("button").prop("disabled", true);
	}

	localStorage.setItem(`GFD_${type}Links`, encodeURI(links.join("\n")));
	updateTextareas();
	updateCounters();
};

// Update textareas
const updateTextareas = () => {
	linkControls.forEach((ta, i) => ta.val(decodeURI(localStorage.getItem(`GFD_${types[i]}Links`) || "")));
};

// Clear links
const clearLinks = (type) => {
	localStorage.removeItem(`GFD_${type}Links`);
	window.location.reload();
};

// Restore last textarea value
const lastValue = localStorage.getItem("GFD_lastValue");
if (lastValue !== null) {
	$("#myform > div:nth-child(5) > textarea").val(lastValue);
	$("#myform > div:nth-child(6) > input").click();
}
$("#myform > div:nth-child(5) > textarea").on("input", () => {
	localStorage.setItem("GFD_lastValue", $("#myform > div:nth-child(5) > textarea").val());
});

// Initial highlight
parseAndHighlightRegexp();