Universal Content Filter

Universal content filter for all websites

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name     		Universal Content Filter
// @description Universal content filter for all websites
// @version  		1.0.0
// @author      petracoding
// @namespace   petracoding
// @match       *://*/*
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.registerMenuCommand
// @grant       GM.listValues
// @grant       GM.deleteValue
// @license     MIT
// ==/UserScript==

const prefix = "pcucf";
const groupCount = 10;

const domainHint =
  "Only works for full websites (e.g. example.com), not specific pages (e.g. example.com/page). Do not include http://www or slashes. One domain per line. (Current domain: <b>" +
  window.location.hostname +
  "</b>)";

// -------------------------------------------------------------------------
//
//    D E F A U L T S :
//
// -------------------------------------------------------------------------

const DEFAULT_VALUES = addGroups(
  {
    active: true,
    enabledSites: "",
    disabledSites: "",
    blurStrength: 5,
    translucentOpacity: 0.1,
    highlightColor: "yellow",
    replaceStr: "#####",
    showOnHover: true,
    searchAltTexts: true,
    disableClicking: true,
    slowerSearch: false,
    findWrapper: true,
    wrapperSelectors: [
      // General:
      ".post",
      ".post_inner",
      // Youtube:
      "ytd-rich-item-renderer",
      "yt-lockup-view-model",
      "ytd-comment-view-model",
      // Neocities:
      ".news-item",
      // Status.cafe:
      ".status",
      // Listography:
      ".listbox-content",
      // Last.fm:
      ".grid-items-item.js-focus-controls-container",
      ".chartlist-row",
      // Goodreads:
      ".update",
      ".ReviewCard",
      // Letterboxd:
      ".film-reviews .listitem",
      ".diary-entry-row",
      // Vinted:
      ".feed-grid__item",
      // Amazon:
      ".puis-card-container.s-card-container",
    ].join("\n"),
  },
  groupCount,
);

function addGroups(obj, n) {
  let i = 1;
  while (i <= n) {
    obj["active" + i] = i == 1;
    obj["action" + i] = "blur";
    obj["fullWord" + i] = true;
    obj["list" + i] = getGroupBlacklistExampleContent(i);
    obj["enabledSites" + i] = "";
    obj["disabledSites" + i] = "";
    i++;
  }
  return obj;
}

function getGroupBlacklistExampleContent(n) {
  if (n == 1) return "death*\ndrug use\nstranger things spoiler*";
  if (n == 2)
    return `abuse*
abusive
assault*
blood*
bullied
bullying
death*
discrimination
domestic violence
drug use
drugs
gore
harassment
kidnapping
racism
scar*
self harm*
self-harm*
sexual violence
suici*
suicide
violence`;
  if (n == 3)
    return `ai
chatgpt
artificial intelligence
openai
machine learning
deep learning
neural network
large language model
llm
gpt
deepmind
dall-e
dalle
ai slop`;

  if (n == 4)
    return `9/11
abortion rights
abraham accords
acab
affirmative action
all cops are bastards
alt left
alt right
alt-left
alt-right
anarchism
anarcho-capitalism
anarchy
anti-zionism
antifa
apartheid
arab league
attack helicopter
authoritarianism
bathroom bills
biden
black lives matter
blue lives matter
boris johnson
bourgeoisie
boycott*
build the wall
cancel culture
ceasefire
centrist
christian nationalism
civil rights
class struggle
class warfare
climate justice
clinton
colonialism
communism
conservatism
conservative
conversion therapy
david cameron
defund the police
democratic socialism
dictator
dictatorship
don't say gay
eat the rich
eco-socialism
far left
far right
far-left
far-right
fascism
feminism
feminist
free speech
from the river to the sea
gaza
gender critical
globalism
gun control
gun rights
hamas
harris
hate speech
hezbollah
hitler
human rights
identity politics
ideologies
ideology
imperialism
intersectionality
intifada
iran
iron dome
islamism
isolationism
israel
keir starmer
left-leaning
left-wing
leninism
lgbtq rights
liberal
liberalism
libertarianism
liz truss
make america great again
maoism
margaret thatcher
marriage equality
marxism
marxist
monarchism
nakba
nationalism
nazi*
nazism
neoliberalism
obama
open borders
oslo accords
palestine
palestinian
patriarchy
patriotism
peace process
pence
political correctness
politically correct
populism
pro choice
pro life
pro-choice
pro-life
progressivism
proletariat
puritan
puritanism
radical left
reactionary
reagan
right of return
right-leaning
right-wing
rishi sunak
school shooting
second amendment
secularism
sexism
sexist
sharia law
social democracy
socialism
socialist
stalinism
sunak
tax the rich
terf
theocracy
theresa may
totalitarianism
traditionalism
trickle-down economics
trump
unionization
universal healthcare
vance
war crime
welfare state
west bank
wokeness
world war
ww1
ww2
wwi
wwii
zionism`;
  if (n == 5)
    return `anti-gay*
anti-genderqueer*
anti-intersex*
anti-lgbt*
anti-lgbtq*
anti-lgbtqia*
anti-non-binary*
anti-nonbinary*
anti-queer*
anti-trans*
biphob*
cis-supremac*
deadnam*
faggo*
gay-bash*
heterosupremac*
homophob*
jkr
misgender*
queerphob*
rowling
terf
tranny*
trans-bash*
transmed
transphob*
two genders`;
  return "";
}

// -------------------------------------------------------------------------
//
//    I N I T :
//
// -------------------------------------------------------------------------

let settings = {};
let timer;
let initialElements = [];
let useLongerDebounceTime = false;
let isExportingOrImporting = false;

(async () => {
  // await deleteAllValuesFromGM(); // for testing
  await createSettingsPanel();
  GM.registerMenuCommand("Universal Content Filter - Settings", async () => {
    document.querySelector("." + prefix).classList.remove(prefix + "--hidden");
    document.querySelector("." + prefix).removeAttribute("aria-hidden");
    openTab(1);
  });
  if (settings.active && activeOnThisSite()) init();
})();

function init() {
  addScriptSpecificCSS();
  searchForElements();

  const observer = new MutationObserver(() => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(
      () => {
        searchForElements();
      },
      useLongerDebounceTime ? (settings.slowerSearch ? 2000 : 1000) : settings.slowerSearch ? 500 : 250,
    );
  });
  observer.observe(document.body, { childList: true, subtree: true });
}

function searchForElements() {
  const textElements = [...document.body.querySelectorAll("*:not(script):not(style):not(." + prefix + "-highlight):not(input):not(textarea)")];
  const altElements = settings.searchAltTexts ? [...document.body.querySelectorAll("[alt]")] : [];
  let elements = textElements.concat(altElements);
  if (elements.length > 5000) useLongerDebounceTime = true;
  let count = 0;

  // Don't go through all elements every time the DOM changes:
  if (!initialElements.length) {
    initialElements = elements;
  } else {
    elements = elements.filter((el) => !initialElements.includes(el));
    initialElements.concat(elements);
  }

  elements.forEach((el) => {
    if (el.innerText && isDeepElement(el)) {
      const text = el.getAttribute("alt")
        ? el
            .getAttribute("alt")
            .toLowerCase()
            .replace(/[^a-zA-Z ]/g, "")
        : el.innerText.toLowerCase();

      let i = 1;
      while (i <= groupCount) {
        if (settings["action" + i] && settings["active" + i] == true && activeOnThisSite(i)) {
          textareaToArray(settings["list" + i]).forEach((word) => {
            const preparedWord = escapeRegExp(word).replaceAll("*", "[a-z]*").replaceAll("\\", "");
            const flag = settings["fullWord" + i] ? "\\b" : "";
            const regex = flag + preparedWord + flag;
            if (new RegExp(regex, "i").test(text)) {
              count++;
              onElementFound(el, word, settings["action" + i], i, regex);
            }
          });
        }
        i++;
      }
    }
  });

  logToConsole(count);
}

function onElementFound(el, word, categoryAction, groupNumber, regex) {
  const hint = `⚑⚑⚑ FOUND "${word}" FROM BLACKLIST GROUP ${groupNumber} ⚑⚑⚑`;
  const classToAdd = prefix + "-" + categoryAction.toLowerCase();

  // Find wrapper by priority, only search for wrapper div if no other wrapper is found.
  let wrapperEl = el.closest(textareaToArray(settings.wrapperSelectors).join(", "));
  if (!wrapperEl) wrapperEl = el.closest("article, details, blockquote, table, ul, ol");
  if (!wrapperEl) wrapperEl = el.closest("div");

  if (settings.findWrapper && wrapperEl && categoryAction != "highlight" && categoryAction != "replace") {
    wrapperEl.classList.add(classToAdd);
    if (settings.showOnHover) wrapperEl.classList.add(prefix + "-show-on-hover");
    if (hint) wrapperEl.setAttribute("title", hint);
  } else {
    if (categoryAction != "highlight") el.classList.add(classToAdd);
    if (settings.showOnHover) el.classList.add(prefix + "-show-on-hover");
    if (hint) el.setAttribute("title", hint);
    const linkEl = el.closest("a, button");
    if (linkEl) linkEl.classList.add(classToAdd);

    if (categoryAction == "replace") {
      const oldInnerHtml = el.innerHTML;
      el.innerHTML = oldInnerHtml.replaceAll(new RegExp(regex, "ig"), settings.replaceStr);
    } else if (categoryAction == "highlight") {
      const oldInnerHtml = el.innerHTML;
      el.innerHTML = oldInnerHtml.replaceAll(new RegExp(regex, "ig"), "<span class='" + prefix + "-highlight'>$&</span>");
    }
  }
}

// -------------------------------------------------------------------------
//
//    S E T T I N G S :
//
// -------------------------------------------------------------------------

async function createSettingsPanel() {
  const settingsPanel = document.createElement("div");
  settingsPanel.setAttribute("class", prefix + " " + prefix + "--hidden");
  settingsPanel.setAttribute("aria-hidden", "true");

  settingsPanel.innerHTML = getSettingsPanelHTML();
  document.body.appendChild(settingsPanel);

  const saveBtn = document.querySelector("." + prefix + "__save-btn");
  if (saveBtn)
    saveBtn.addEventListener("click", async () => {
      saveBtn.classList.add(prefix + "__btn--loading");
      await saveValuesToGM();
      saveBtn.classList.remove(prefix + "__btn--loading");
      if (!isExportingOrImporting) {
        if (confirm("Settings have been saved. Refresh page to see changes?")) window.location.reload();
      }
    });

  const closeBtn = document.querySelector("." + prefix + "__close-btn");
  if (closeBtn)
    closeBtn.addEventListener("click", () => {
      if (confirm("Close without saving?")) {
        document.querySelector("." + prefix).setAttribute("aria-hidden", "true");
        document.querySelector("." + prefix).classList.add(prefix + "--hidden");
      }
    });

  const importBtn = document.querySelector("." + prefix + "__import-btn");
  if (importBtn)
    importBtn.addEventListener("click", () => {
      importSettings();
    });

  const exportBtn = document.querySelector("." + prefix + "__export-btn");
  if (exportBtn)
    exportBtn.addEventListener("click", () => {
      exportSettings();
    });

  const resetBtn = document.querySelector("." + prefix + "__reset-btn");
  if (resetBtn)
    resetBtn.addEventListener("click", () => {
      if (confirm("Are you sure you want to reset the userscript settings to their defaults? This will also delete your lists of words!")) {
        resetSettings();
      }
    });

  const tabsBtns = document.querySelectorAll("." + prefix + "__tab-btn");
  [...tabsBtns].forEach((tabBtn) => {
    tabBtn.addEventListener("click", () => {
      const n = tabBtn.getAttribute("data-" + prefix + "-open-tab");
      openTab(n);
    });
  });

  const styleSheet = document.createElement("style");
  styleSheet.setAttribute("type", "text/css");
  document.head.appendChild(styleSheet);
  styleSheet.innerText = getSettingsPanelCSS();

  await loadValuesFromGM();
}

function openTab(n) {
  const tabsBtns = document.querySelectorAll("." + prefix + "__tab-btn");
  const tabs = document.querySelectorAll("." + prefix + "__groups" + " ." + prefix + "__group");

  [...tabsBtns].forEach((el) => {
    el.classList.remove(prefix + "__tab-btn--active");
  });
  [...tabs].forEach((el) => {
    el.style.display = "none";
  });

  const tabBtn = document.querySelector("." + prefix + "__tab-btn[data-" + prefix + "-open-tab='" + n + "'");
  tabBtn.classList.add(prefix + "__tab-btn--active");

  const tabToOpen = document.querySelector("." + prefix + "__group[data-" + prefix + "-tab='" + n + "'");
  tabToOpen.style.display = "block";
}

/* 
${getCheckboxHTML("test1", "Name", "Info", "")}
${getTextInputHTML("test2", "Name", "Info", "")}
${getNumberInputHTML("test3", "Name", "Info", "", 0, 10)}
${getTextareaHTML("test4", "Name", "Info", "")}
${getRadioButtonsHTML("test5", "Name", "Info", "", ["a", "b", "c"])}
*/

function getSettingsPanelHTML() {
  let tabsHTML = "";
  let groupsHTML = "";
  let i = 1;
  while (i <= groupCount) {
    tabsHTML += `<button type="button" class="${prefix}__tab-btn" data-${prefix}-open-tab="${i}">Group ${i}</button>`;
    groupsHTML += getGroupHTML(i);
    i++;
  }

  return `<div class="${prefix}__inner">
 <div class="${prefix}__heading">Universal Content Filter</div>

 <div class="${prefix}__credit">
    <div>
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
    </div>
    <div>
      <p>Script by petracoding: <a href="https://greasyfork.org/en/users/354138-petracoding" target="_blank">https://greasyfork.org/en/users/354138-petracoding</a>. <br />Don't forget to save your changes! (Button in the top right corner.)</p>
    </div>
 </div>
   
<div class="${prefix}__group">

    <div class="${prefix}__group-heading">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
      <span>General Settings</span>
    </div>

    <div class="${prefix}__active">
      ${getCheckboxHTML("active", "Script active", "", "")}
    </div>

    <div class="${prefix}__two-columns">
      ${getTextareaHTML("enabledSites", "Enabled Sites", domainHint + " Leave this empty to run script on all sites by default!", "")}
      ${getTextareaHTML("disabledSites", "Disabled Sites", domainHint, "")}
    </div>

    <div class="${prefix}__two-columns">
      ${getNumberInputHTML("blurStrength", "Blur Strength", "Strength of blur for action 'blur'. Default: 5.", "", 0, 10)}
      ${getNumberInputHTML("translucentOpacity", "Translucent opacity", "Opacity for action 'transcluent'. Default: 0.1", "", 0, 1)}
    </div>

    <div class="${prefix}__two-columns">
      ${getTextInputHTML("highlightColor", "Highlight Color", "Background color for action 'highlight'. Default: yellow<br>Valid CSS colors only! Get color values <a href='https://www.w3schools.com/cssref/css_colors.php' target='_blank'>here</a> or <a href='https://htmlcolorcodes.com/' target='_blank'>here</a>.", "")}
      ${getTextInputHTML("replaceStr", "Replacement text", "Replacement text for action 'replace'. Default: #####", "")}
    </div>

    <div class="${prefix}__two-columns">
      ${getCheckboxHTML("showOnHover", "Show blurred/transcluent on hover", "", "")}
      ${getCheckboxHTML("disableClicking", "Disable clicking on blocked content", "", "")}
    </div>

    <div class="${prefix}__two-columns">
      ${getCheckboxHTML("searchAltTexts", "Search alternative texts of images as well", "This may result in images being filtered, but is not a guarantee, and will also lead to false positives.", "")}
      ${getCheckboxHTML("slowerSearch", "Improve site performance", "Only activate this if you experience performance issues, especially on sites with lots of content. This will slow down the filter script.", "")}
    </div>
    
    ${getCheckboxHTML("findWrapper", "Find wrapper", "Whether or not wrappers (e.g. whole post) of blacklisted text should be completely blurred/hidden instead of just a paragraph. Only works on certain websites, see 'Wrapper CSS Selectors' below.", "")}
    ${getTextareaHTML("wrapperSelectors", "Wrapper CSS Selectors", "Only expand this list if you know CSS! One selector per line.<br/>By default the selectors are for: YouTube, Neocities, Status.cafe, Listography, Last.fm, Goodreads, Letterboxd, Vinted, Amazon. Please note that the following websites use scrambled CSS classes, so it is not possible to use selectors for them: Twitter/X, Instagram, Tumblr, Outlook, Facebook, Pinterest.", "")}

</div>

<div class="${prefix}__tabs">${tabsHTML}</div>
<div class="${prefix}__groups">${groupsHTML}</div>

<div class="${prefix}__buttons">
  <div>
    ${getButtonHTML("save-btn", "Save")}
    ${getButtonHTML("close-btn", "Close")}
    ${getButtonHTML("reset-btn", "Reset settings")}
  </div>
  <div>
    ${getButtonHTML("export-btn", "Export settings")}
    ${getButtonHTML("import-btn", "Import settings")}
  </div>
</div>

</div>
`;
}

function getGroupHTML(n) {
  return `
    <div class="${prefix}__group" data-${prefix}-tab="${n}">
    <b class="${prefix}__group-heading">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
    <span>Group ${n}</span>
    </b>
    <div class="${prefix}__group-content">

      <div class="${prefix}__two-columns">
        ${getCheckboxHTML("active" + n, "Group active", "uncheck to disable the category for now", "")}
        ${getCheckboxHTML("fullWord" + n, "Full word search only", "Recommended: on. If turned off 'war' will match 'wars', 'aware' etc. too!", "")}
      </div>
      
      ${getRadioButtonsHTML("action" + n, "Action", "determines how words in this category are treated", "", ["blur", "hide", "replace", "translucent", "highlight"])}
      ${getTextareaHTML("list" + n, "List of words", "One word/phrase per line, case insensitive. You can use * as a wildcard, e.g. war* matches 'war' and 'wars' and 'warthog' etc.", "")}

      <div class="${prefix}__two-columns">
        ${getTextareaHTML("enabledSites" + n, "Only enable this group on these websites", domainHint + " Leave empty to enable this group for all sites by default!", "")}
        ${getTextareaHTML("disabledSites" + n, "Disable this group on these websites", domainHint, "")}
      </div>
    </div>
  </div>`;
}

// -------------------------------------------------------------------------
//
//    I N P U T S :
//
// -------------------------------------------------------------------------

function getButtonHTML(id, label) {
  let icon = "";
  switch (id) {
    case "save-btn":
      icon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>`;
      break;
    case "close-btn":
      icon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
      break;
    case "export-btn":
      icon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.2 15c.7-1.2 1-2.5.7-3.9-.6-2-2.4-3.5-4.4-3.5h-1.2c-.7-3-3.2-5.2-6.2-5.6-3-.3-5.9 1.3-7.3 4-1.2 2.5-1 6.5.5 8.8m8.7-1.6V21"/><path d="M16 16l-4-4-4 4"/></svg>`;
      break;
    case "import-btn":
      icon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.2 15c.7-1.2 1-2.5.7-3.9-.6-2-2.4-3.5-4.4-3.5h-1.2c-.7-3-3.2-5.2-6.2-5.6-3-.3-5.9 1.3-7.3 4-1.2 2.5-1 6.5.5 8.8M12 19.8V12M16 17l-4 4-4-4"/></svg>`;
      break;
    case "reset-btn":
      icon = `<svg fill="#000000" viewBox="0 0 1920 1920" xmlns="http://www.w3.org/2000/svg" style="width:16px"><g  stroke-width="0"></g><g " stroke-linecap="round" stroke-linejoin="round"></g><g> <path d="M960 0v112.941c467.125 0 847.059 379.934 847.059 847.059 0 467.125-379.934 847.059-847.059 847.059-467.125 0-847.059-379.934-847.059-847.059 0-267.106 126.607-515.915 338.824-675.727v393.374h112.94V112.941H0v112.941h342.89C127.058 407.38 0 674.711 0 960c0 529.355 430.645 960 960 960s960-430.645 960-960S1489.355 0 960 0" fill-rule="evenodd"></path> </g></svg>`;
      break;
  }

  return `
  <button type="button" class="${prefix}__${id}">${icon}<span>${label}</span></button>`;
}

function getCheckboxHTML(id, label, info, bigInfo) {
  const checkedAtt = DEFAULT_VALUES[id] ? 'checked="checked"' : "";
  return `
  <div class="${prefix}__setting ${prefix}__setting--checkbox">
    <div class="input-group">
      <input type="checkbox" id="${id}" name="${id}" ${checkedAtt} />
      <label for="${id}">${label}${bigInfo ? " " + bigInfo : ""}
      </label>
    </div>
    <div class="${prefix}__info">${info}</div>
  </div>`;
}

function getTextInputHTML(id, label, info, bigInfo) {
  return `
  <div class="${prefix}__setting ${prefix}__setting--text-input">
    <div class="input-group">
      <label class="label">${label}${bigInfo ? " " + bigInfo : ""}</label>
      <input autocomplete="off" name="${id}" class="input" type="text" value="${DEFAULT_VALUES[id] ? DEFAULT_VALUES[id] : ``}">
    </div>
    <div class="${prefix}__info">${info}</div>
  </div>`;
}

function getNumberInputHTML(id, label, info, bigInfo, min, max) {
  return `
  <div class="${prefix}__setting ${prefix}__setting--number-input">
    <div class="input-group">
      <label class="label">${label}${bigInfo ? " " + bigInfo : ""}</label>
      <input autocomplete="off" name="${id}" class="input" type="number" min="${min}" max="${max}" value="${DEFAULT_VALUES[id] ? DEFAULT_VALUES[id] : ``}">
    </div>
    <div class="${prefix}__info">${info}</div>
    <div class="${prefix}__hint">This field only accepts numbers (${min} to ${max}).</div>
  </div>`;
}

function getTextareaHTML(id, label, info, bigInfo) {
  return `
  <div class="${prefix}__setting ${prefix}__setting--textarea">
    <div class="input-group">
      <label class="label">${label + ":"}${bigInfo ? " " + bigInfo : ""}</label>
      <textarea autocomplete="off" name="${id}" class="input">${DEFAULT_VALUES[id] ? DEFAULT_VALUES[id] : ""}</textarea>
    </div>
    <div class="${prefix}__info">${info}</div>
  </div>`;
}

function getRadioButtonsHTML(id, label, info, bigInfo, optionsArray) {
  let optionsHTML = "";
  optionsArray.forEach((o) => {
    const checkedAtt = DEFAULT_VALUES[id] == o ? 'checked="checked"' : "";
    optionsHTML += `
      <label class="radio">
        <input type="radio" name="${id}" ${checkedAtt} value="${o}">
        <span class="name">${o}</span>
      </label>
    `;
  });
  return `
  <div class="${prefix}__setting ${prefix}__setting--radios">
    <div class="input-group">
      <label class="label">${label}${bigInfo ? " " + bigInfo : ""}</label>
      <div class="radio-inputs">${optionsHTML}</div>
    </div>
    <div class="${prefix}__info">${info}</div>
  </div>`;
}

// -------------------------------------------------------------------------
//
//    L O A D / S A V E :
//
// -------------------------------------------------------------------------

function getSettingSelector(key) {
  return "." + prefix + "__setting [name='" + key + "']";
}

function getInputElement(key, what) {
  const radioBtns = document.querySelectorAll(getSettingSelector(key) + "[type='radio']");
  if (radioBtns.length) {
    if (what == "load") {
      const valueToCheck = settings[key];
      return document.querySelector(getSettingSelector(key) + "[value='" + valueToCheck + "']");
    } else if (what == "save") {
      let checkedRadioBtn;
      radioBtns.forEach((radioBtn) => {
        if (radioBtn.checked) checkedRadioBtn = radioBtn;
      });
      return checkedRadioBtn;
    }
  } else {
    return document.querySelector(getSettingSelector(key));
  }
}

async function loadValuesFromGM() {
  for (const [key, defaultValue] of Object.entries(DEFAULT_VALUES)) {
    settings[key] = await GM.getValue(key, defaultValue);
    const el = getInputElement(key, "load");
    loadSettingIntoInputElement(el, key);
  }
  return settings;
}

function loadSettingIntoInputElement(el, key) {
  if (!el) return;
  if (el.getAttribute("type") == "radio") {
    el.checked = true;
  } else if (el.getAttribute("type") == "checkbox") {
    el.checked = settings[key];
  } else {
    el.value = settings[key];
  }
}

async function saveValuesToGM() {
  for (const [key, defaultValue] of Object.entries(DEFAULT_VALUES)) {
    const el = getInputElement(key, "save");
    if (el) {
      if (el.getAttribute("type") == "checkbox") {
        settings[key] = el.checked;
      } else {
        settings[key] = el.value;
      }
    }
    if (settings[key] === false) {
      await GM.setValue(key, false);
    } else {
      await GM.setValue(key, settings[key] || defaultValue);
    }
  }
}

async function resetSettings() {
  for (const [key, defaultValue] of Object.entries(DEFAULT_VALUES)) {
    settings[key] = defaultValue;
    const el = document.querySelector(getSettingSelector(key));
    loadSettingIntoInputElement(el, key);
  }
  await saveValuesToGM();
  if (confirm("Settings have been reset. Refresh page to see changes?")) window.location.reload();
}

async function exportSettings() {
  const exportBtn = document.querySelector("." + prefix + "__export-btn");
  exportBtn.classList.add(prefix + "__btn--loading");
  isExportingOrImporting = true;
  await saveValuesToGM();
  copyToClipboard(JSON.stringify(await loadValuesFromGM()));
  exportBtn.classList.remove(prefix + "__btn--loading");
  isExportingOrImporting = false;
  alert("Script Settings have been copied to your clipboard.");
}

async function importSettings() {
  const str = prompt("Paste exported script settings here to import them.", "");
  if (!str) return;

  try {
    const newSettings = JSON.parse(str);
    if (newSettings) {
      isExportingOrImporting = true;
      settings = newSettings;
      for (const [key, defaultValue] of Object.entries(DEFAULT_VALUES)) {
        const el = document.querySelector(getSettingSelector(key));
        loadSettingIntoInputElement(el, key);
      }
      const importBtn = document.querySelector("." + prefix + "__import-btn");
      importBtn.classList.add(prefix + "__btn--loading");
      await saveValuesToGM();
      importBtn.classList.remove(prefix + "__btn--loading");
      if (confirm("Imported. Refresh page to see changes?")) window.location.reload();
      isExportingOrImporting = false;
    }
  } catch (e) {
    alert("Invalid settings!");
  }
}

async function deleteAllValuesFromGM() {
  let keys = await GM.listValues();
  for (let key of keys) {
    GM.deleteValue(key);
  }
}

// -------------------------------------------------------------------------
//
//    H E L P E R S :
//
// -------------------------------------------------------------------------

function activeOnThisSite(groupNumber) {
  const groupNumberString = groupNumber ? groupNumber : "";
  const sitesToDisable = getListOfDomains("disabledSites" + groupNumberString);
  if (sitesToDisable.includes(window.location.hostname.replace("www.", ""))) return false;

  const sitesToEnable = getListOfDomains("enabledSites" + groupNumberString);
  if (sitesToEnable.length) return sitesToEnable.includes(window.location.hostname.replace("www.", ""));

  return true;
}

function textareaToArray(str) {
  return str
    .split("\n")
    .filter((s) => s.trim())
    .map((s) => s.trim());
}

function copyToClipboard(text) {
  const input = document.createElement("textarea");
  input.innerHTML = text;
  document.body.appendChild(input);
  input.select();
  const result = document.execCommand("copy");
  document.body.removeChild(input);
  return result;
}

function escapeRegExp(string) {
  return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}

function isDeepElement(el) {
  const DEEP_ELEMENTS = ["SPAN", "A", "LI", "P", "B", "I", "U", "STRONG", "EM", "S", "ABBR", "SUMMARY", "H1", "H2", "H3", "H4", "H5", "H6"];
  if (!el.children.length || DEEP_ELEMENTS.includes(el.tagName)) return true;
  return false;
}

function logToConsole(n) {
  if (n) {
    console.log("Universal Content Filter (by petracoding): Detected " + n + " words successfully. Settings:");
  }
  // console.log(settings);
}

function getListOfDomains(settingsName) {
  return textareaToArray(settings[settingsName])
    .map((s) => s.replace("http://", "").replace("https://", "").replace("www.", "").replace("/", "").trim())
    .filter((v) => v.length);
}

// -------------------------------------------------------------------------
//
//    C S S :
//
// -------------------------------------------------------------------------

function addScriptSpecificCSS() {
  let CSS = `
  .${prefix}-blur { filter: blur(${settings.blurStrength}px) !important; } 
  .${prefix}-hide { display: none !important; }
  .${prefix}-translucent { opacity: ${settings.translucentOpacity}; } 
  .${prefix}-highlight { background: ${settings.highlightColor}; }
  .${prefix}-show-on-hover { transition: 0.3s ease; }
  .${prefix}-show-on-hover:hover { filter: initial !important; opacity: initial !important; }

  .${prefix}__setting textarea[name*="list"] {
    min-height: 200px;
  }
  `;

  if (settings.disableClicking)
    CSS += `
    .${prefix}-blur *,
    .${prefix}-hide *,
    .${prefix}-replace *,
    .${prefix}-translucent * { pointer-events: none !important; }
   `;

  const styleSheet = document.createElement("style");
  styleSheet.setAttribute("type", "text/css");
  document.head.appendChild(styleSheet);
  styleSheet.innerText = CSS;
}

function getSettingsPanelCSS() {
  return `

/* SETTINGS PANEL */

.${prefix}.${prefix}--hidden { display: none!important; }

.${prefix} {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 999999;
  display: flex;
  align-content: center;
  align-items: center;
  justify-content: center;
}

.${prefix}__inner {
  position: relative;
  padding: 40px;
  max-height: 80%;
  overflow-y: scroll;
  width: 60%;
  background: white;
  border: 1px solid #cacaca;
  border-radius: 8px;
  font-size: 13px;
  font-family: Verdana, sans-serif;
  color: black;
  font-style: normal;
  font-weight: normal;
  box-shadow: 0px 0px 10px #0000004a;
}

@media (max-width: 1200px) {
  .${prefix}__inner {
    width: 80%;
    max-height: 85%%;
  }
}

/* BUTTONS */

.${prefix}__buttons {
  position: fixed;
  top: 10px;
  right: 10px;
  display: flex;
  flex-direction: column;
  align-items: end;
  z-index: 1;
}

.${prefix}__buttons > div {
  display: flex;
  flex-direction: column;
  align-items: end;
}

.${prefix}__buttons button {
  margin: 5px;
  display: flex;
  align-items: center;
  cursor: pointer;
  background: white;
  border: none;
  border-radius: 4px;
  box-shadow: 0px 0px 10px gray;
  font-size: 14px;
  letter-spacing: 1px;
  padding: 5px 10px;
  height: 30px;
  transition: 0.3s ease;
}

.${prefix}__buttons button.${prefix}__save-btn {
  background: #6fe371;
}

.${prefix}__buttons button.${prefix}__close-btn {
  background: #ec8787;
}

.${prefix}__buttons button:hover,
.${prefix}__buttons button:active {
  box-shadow: 0px 0px 10px black;
  transform: scale(1.1);
}

.${prefix}__buttons button svg {
  margin-right: 5px;
  width: 20px;
}

.${prefix}__btn--loading::after {
  content: "...";
}

/* HEADING ETC */

.${prefix}__heading {
  font-size: 2em;
  text-align: center;
}

.${prefix}__credit {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
}

.${prefix}__credit svg {
  margin-right: 10px;
}

.${prefix}__active input {
  transform: scale(1.3);
}

.${prefix}__setting input[name*="active"] ~ label {
  font-size: 18px;
  color: red;
}

.${prefix}__setting input[name*="active"]:checked ~ label {
  color: black;
}

.${prefix}__group-heading {
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  background: #e4e4e4;
  padding: 1em;
  border-radius: 17px;
}

.${prefix}__group-heading span {
  font-size: 1.5em;
  margin-left: 10px;
}

.${prefix}__group {
  margin-top: 10px;
}

/* TABS */

.${prefix}__tabs {
  margin-top: 50px;
  margin-bottom: 30px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}

.${prefix}__tab-btn {
  background: none;
  border: 1px solid #afafaf;
  border-radius: 3px;
  padding: 3px 6px;
  cursor: pointer;
  margin-right: 3px;
  font-size: 14px;
}

.${prefix}__tab-btn.${prefix}__tab-btn--active {
  border-bottom-color: white;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* INPUTS */

.${prefix}__setting {
  margin-bottom: 25px;
}

.${prefix}__setting label {
  font-size: 14px;
  margin-right: 1em;
}

.${prefix}__setting .radio-inputs label {
  font-weight: normal;
}

.${prefix}__setting textarea,
.${prefix}__setting input {
  border: 1px solid #b4b4b4;
  border-radius: 5px;
}

.${prefix}__setting textarea {
  display: block;
  width: 100%;
  min-height: 70px;
  padding: 10px;
  font-family: inherit;
  font-size: 13px;
  box-sizing: border-box;
  resize: vertical;
}

.${prefix}__setting--checkbox label,
.${prefix}__setting--radios label {
  cursor: pointer;
}

.${prefix}__setting input[type="text"] ,
.${prefix}__setting input[type="number"] {
  padding: 4px 6px;
  min-width: 50px;
}

.${prefix}__info {
  font-size: 11px;
  opacity: 0.6;
  margin-top: 4px;
  line-height: 1.4;
}

.${prefix}__hint {
  font-size: 11px;
  opacity: 0.6;
  margin-top: 2px;
}

/* TWO COLUMNS */

.${prefix}__two-columns {
  display: flex;
}

.${prefix}__two-columns > *:first-child {
  flex: 0 0 49%;
  margin-right: 1%;
}

.${prefix}__two-columns > *:last-child {
  flex: 0 0 49%;
  margin-left: 1%;
}
  `;
}

// -------------------------------------------------------------------------
// end.