Sleazy Fork is available in English.

Kemono Fixer

Allow you to blacklist creators on Kemono, and more.

Verze ze dne 05. 10. 2025. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

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

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

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

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

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name        Kemono Fixer
// @namespace   DKKKNND
// @license     WTFPL
// @match       https://kemono.cr/*
// @match       https://coomer.st/*
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @version     1.0
// @author      Kaban
// @description Allow you to blacklist creators on Kemono, and more.
// ==/UserScript==
(function() {
"use strict";
// ==<Options>==
const OPTIONS = JSON.parse(GM_getValue("OPTIONS", `{
  "adapt_post_card_height_to_thumbnail": false,
  "sort_posts_by_publish_date": true,
  "hide_posts_with_no_thumbnail": true,
  "hide_posts_from_blacklisted_creators": true,
  "hide_posts_shown_on_previous_pages": true
}`));
let BLACKLIST_MODE = false;
let BLACKLIST_RAW = JSON.parse(GM_getValue("BLACKLIST", `{}`));
let BLACKLIST = Object.keys(BLACKLIST_RAW);
// ==</Options>==

// ==<Menu>==
updateMenu("Adaptive Post Card Height",
  "adapt_post_card_height_to_thumbnail", true);
updateMenu("Sort Posts by Publish Date",
  "sort_posts_by_publish_date", true);
updateMenu("Hide Posts with No Thumbnail",
  "hide_posts_with_no_thumbnail", false);
updateMenu("Hide Posts from Blacklisted Creators",
  "hide_posts_from_blacklisted_creators", false);
updateMenu("Hide Posts shown on Previous Pages",
  "hide_posts_shown_on_previous_pages", false);
GM_registerMenuCommand(
  "---------------------------------------", null, { autoClose: false });
GM_registerMenuCommand(
  "▶ Enter Blacklist Mode", blacklistMode, { id: "blacklistMode" });
GM_registerMenuCommand(
  "⬆ Export Blacklist to Clipboard", exportBlacklist);
GM_registerMenuCommand(
  "⬇ Import Blacklist from Clipboard", importBlacklist);

function updateMenu(menuName, optionName, reloadPage = false) {
  GM_registerMenuCommand(
    `${OPTIONS[optionName] ? "☑" : "☐"} ${menuName}`, () => {
      updateOption(optionName, menuName, reloadPage)
    }, {
      id: optionName,
      title: `${menuName}${reloadPage ? " (Reloads current page)" : ""}`
    }
  );
}

function updateOption(optionName, menuName, reloadPage) {
  OPTIONS[optionName] = !OPTIONS[optionName];
  updateMenu(menuName, optionName, reloadPage);
  GM_setValue("OPTIONS", JSON.stringify(OPTIONS));
  if (reloadPage) {
    window.location.reload();
  } else {
    updateStyles();
  }
}

function updateStyles() {
  if(OPTIONS.hide_posts_with_no_thumbnail) {
    addStyle(CSS.hideNoThumbnail, "hide_no_thumbnail");
  } else {
    removeStyle("hide_no_thumbnail");
  }
  if(OPTIONS.hide_posts_from_blacklisted_creators) {
    addStyle(CSS.hideBlacklisted, "hide_blacklisted");
  } else {
    removeStyle("hide_blacklisted");
  }
  if(OPTIONS.hide_posts_shown_on_previous_pages) {
    addStyle(CSS.hideAlreadyShown, "hide_already_shown");
  } else {
    removeStyle("hide_already_shown");
  }
  if (BLACKLIST_MODE) {
    addStyle(CSS.blacklistMode, "blacklist_mode");
    removeStyle("hide_blacklisted");
  } else {
    removeStyle("blacklist_mode");
    if(OPTIONS.hide_posts_from_blacklisted_creators)
      addStyle(CSS.hideBlacklisted, "hide_blacklisted");
  }
}

function blacklistMode() {
  BLACKLIST_MODE = !BLACKLIST_MODE;
  updateStyles();
  if (BLACKLIST_MODE) {
    GM_registerMenuCommand(
      "◀ Exit Blacklist Mode", blacklistMode, { id: "blacklistMode" });

  } else {
    GM_registerMenuCommand(
      "▶ Enter Blacklist Mode", blacklistMode, { id: "blacklistMode" });

  }
}

function exportBlacklist() {
  let length = BLACKLIST.length;
  if (length === 0) {
    alert("Blacklist is empty, nothing to export.")
  } else {
    let message = `Do you want to export blacklist (${length} ${length > 1 ? "entries" : "entry"}) to clipboard?`;
    if (confirm(message)) {
      setTimeout(exportToClipboard, 500);
    }
  }
}

async function exportToClipboard() {
  try {
    await navigator.clipboard.writeText(JSON.stringify(BLACKLIST_RAW, null, 2));
    alert(`Exported blacklist (${BLACKLIST.length} entries) to clipboard as JSON.`);
  } catch (error) {
    alert(`Export to clipboard failed:\n${error.message}`);
  }
}

function importBlacklist() {
  let message = "Do you want to import blacklist from clipboard?";
  if (confirm(message)) {
    let length = BLACKLIST.length;
    if (length > 0) {
      message = `Current blacklist (${length} ${length > 1 ? "entries" : "entry"}) will be overwritten!`;
      if (!confirm(message)) return;
    }
    setTimeout(importFromClipboard, 500);
  }
}

async function importFromClipboard() {
  try {
    let rawClipboard = await navigator.clipboard.readText();
    if (!rawClipboard) {
      alert("Clipboard read empty.\n(Permission denied?)");
      return;
    }
    let json;
    if (rawClipboard.startsWith('{')) {
      try {
        json = JSON.parse(rawClipboard);
      } catch (error) {
        alert(`Parse JSON failed:\n${error.message}`)
      }
    } else {
      // backward compatibility with data from old version script
      let lines = rawClipboard.split('\n');
      let regex = /"([^"]+)"[,\s]*\/\/\s*(.*?)\s*$/;
      json = {};
      for (let i = 0; i < lines.length; i++) {
        let match = lines[i].match(regex);
        if (match) {
          json[match[1]] = match[2];
        }
      }
    }
    let length = Object.keys(json).length;
    if (length === 0) {
      let message = "Found no valid entry from the clipboard.\nEnter \"clear\" to clear the blacklist.";
      if (prompt(message, "")?.toLowerCase() === "clear") {
        BLACKLIST_RAW = {};
        BLACKLIST = [];
        GM_setValue("BLACKLIST", "{}");
        alert("Blacklist cleared.");
      } else {
        alert("Import aborted.");
      }
    } else {
      BLACKLIST_RAW = json;
      BLACKLIST = Object.keys(BLACKLIST_RAW);
      GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST_RAW));
      alert(`Imported ${length} blacklist ${length > 1 ? "entries" : "entry"} from clipboard.`)
    }
  } catch (error) {
    alert(`Import from clipboard failed:\n${error.message}`);
  }
}
// ==</Menu>==

// ==<Helper Functions>==
function gOne(id) {
  return document.getElementById(id);
}
function qOne(selector) {
  return document.querySelector(selector);
}
Element.prototype.qOne = function(selector) {
  return this.querySelector(selector);
}
function qAll(selector) {
  let results = document.querySelectorAll(selector);
  return results.length === 0 ? null : results;
}
Element.prototype.qAll = function(selector) {
  let results = this.querySelectorAll(selector);
  return results.length === 0 ? null : results;
}
function createKfElement(type, id = null, content = null) {
  let element = document.createElement(type);
  if (id) {
    element.id = `kf-${id}`;
    let oldElement = gOne(element.id);
    if (oldElement) oldElement.remove();
  }
  if (content) element.textContent = content;
  return element;
}
HTMLElement.prototype.addKfClass = function(className) {
  return this.classList.add(`kf-${className}`);
}
HTMLElement.prototype.setKfAttr = function(attributeName, value) {
  return this.setAttribute(`data-kf-${attributeName}`, value);
}
HTMLElement.prototype.getKfAttr = function(attributeName) {
  return this.getAttribute(`data-kf-${attributeName}`);
}
HTMLElement.prototype.getDataAttr = function(attributeName) {
  return this.getAttribute(`data-${attributeName}`);
}
// ==</Helper Functions>==

// ==<Mutation Observer>==
const observer = new MutationObserver(onMutation);
observer.observe(document.body, { childList: true, subtree: true });

function onMutation(mutations, observer) {
  // stop observer before DOM manipulation
  observer.disconnect();
  // pathname-based dispatch
  let path = window.location.pathname;
  if (path === "/posts" || path === "/posts/popular") {
    addStyle(CSS.posts);
    fixPosts();
  } else {
    let segments = path.split('/').filter(segment => segment);
    if (segments[1] === "user") {
      // {service}/user/{user_id}
      if (segments.length === 3) {
        //fixCreatorPosts();
      } else if (segments[3] === "post") {
        if (
          // {service}/user/{user_id}/post/{post_id}
          segments.length === 5 ||
          // {service}/user/{user_id}/post/{post_id}/revision/{revision_id}
          segments.length === 7 && segments[5] === "revision"
        ) {
          //fixCurrentPost();
        }
      // discord/server/{server_id}/{room_id}
      } else if (segments.length === 4 && segments[0] === "discord") {
        //fixDiscord();
      }
    }
  }
  // restart observer
  observer.observe(document.body, { childList: true, subtree: true });
}
// ==</Mutation Observer>==

// ==<Page Fixing Functions>==
function kfExist(id) {
  let element = gOne(`kf-${id}`);
  if (!element) return false;
  if (element.getKfAttr("href") === window.location.href) return true;
  element.remove();
  return false;
}

function addStyle(css, id = "style") {
  if (kfExist(id)) return;
  let style = createKfElement("style", id, css);
  style.setKfAttr("href", window.location.href);
  document.head.append(style);
}

function removeStyle(id = "style") {
  let style = gOne(`kf-${id}`);
  if(style) style.remove();
}

function fixPosts() {
  if (kfExist("postCardContainer")) return;

  let srcPostCardContainer = qOne(".card-list__items");
  if (!srcPostCardContainer) return;
  let srcPostCards = srcPostCardContainer.qAll(".post-card");
  if (!srcPostCards) return;

  let newPostCardContainer = createKfElement("div", "postCardContainer");
  let newPostCards = [];
  for(const card of srcPostCards) {
    let newCard = card.cloneNode(true);
    if (OPTIONS.sort_posts_by_publish_date) {
      let timeStamp = new Date(newCard.qOne("time").dateTime).getTime();
      newCard.setKfAttr("published", timeStamp);
    }
    newPostCards.push(newCard);
  }
  if (OPTIONS.sort_posts_by_publish_date) {
    newPostCards = newPostCards.sort((a, b) =>
      b.getKfAttr("published") - a.getKfAttr("published")
    );
  }

  let hiddenPosts = {
    noThumbnail: 0,
    creatorBlacklisted: 0,
    blacklistedCreators: [],
    alreadyShown: 0
  };
  let shownPosts = [];
  let currentOffset = parseInt(new URLSearchParams(window.location.search).get("o")) || 0;
  if (currentOffset > parseInt(sessionStorage.getItem("lastOffset")))
    shownPosts = JSON.parse(sessionStorage.getItem("shownPosts")) || [];
  for(const card of newPostCards) {
    let thumbnail = card.qOne(".post-card__image");
    if (!thumbnail) {
      hiddenPosts.noThumbnail++;
      card.addKfClass("no_thumbnail");
    } else {
      thumbnail.addEventListener("load", fixThumbnailAspectRatio);
    }

    let blacklistControl = createKfElement("div", "blacklist_control")
    blacklistControl.addEventListener("click", preventDefault);
    let blacklistButton = document.createElement("input");
    blacklistButton.type = "button";
    blacklistControl.appendChild(blacklistButton);
    card.qOne("a").prepend(blacklistControl);
    let creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
    if (BLACKLIST.includes(creatorKey)) {
      hiddenPosts.creatorBlacklisted++;
      card.addKfClass("blacklisted");
      if (!hiddenPosts.blacklistedCreators.includes(creatorKey))
        hiddenPosts.blacklistedCreators.push(creatorKey);
      blacklistButton.value = "Remove from Blacklist";
      blacklistButton.addEventListener("click", unblacklistCreator);

    } else {
      blacklistButton.value = "Add to Blacklist";
      blacklistButton.addEventListener("click", blacklistCreator);
    }

    let postKey = `${creatorKey}-${card.getDataAttr("id")}`;
    if (shownPosts.includes(postKey)) {
      hiddenPosts.alreadyShown++;
      card.addKfClass("already_shown");
    } else {
      shownPosts.push(postKey);
    }

    newPostCardContainer.appendChild(card);
  }
  sessionStorage.setItem("lastOffset", currentOffset);
  sessionStorage.setItem("shownPosts", JSON.stringify(shownPosts));

  // reset all page buttons active state
  let pageButtons = qAll("menu > a");
  if (pageButtons) for (const button of pageButtons) {
    button.blur();
  }

  // add messages about hidden posts
  let srcMessage = qOne("#paginator-top small");
  if (srcMessage) {
    let messageContainer = createKfElement("div", "postMessageContainer");
    srcMessage.after(messageContainer);

    if (hiddenPosts.noThumbnail > 0) {
      let message;
      if (hiddenPosts.noThumbnail > 1) {
        message = `Hid ${hiddenPosts.noThumbnail} posts with no thumbnail.`;
      } else {
        message = "Hid 1 post with no thumbnail.";
      }
      let messageNoThumbnail = createKfElement("small", "no_thumbnail_msg", message);
      messageNoThumbnail.addEventListener("click", showPostsWithNoThumbnail);
      messageContainer.appendChild(messageNoThumbnail);
    }

    if (hiddenPosts.creatorBlacklisted > 0) {
      let message;
      if (hiddenPosts.creatorBlacklisted > 1) {
        message = `Hid ${hiddenPosts.creatorBlacklisted} posts from `;
        if (hiddenPosts.blacklistedCreators.length > 1) {
          message += `${hiddenPosts.blacklistedCreators.length} blacklisted creator.`;
        } else {
          message += "a blacklisted creator."
        }
      } else {
        message = "hid 1 post from a blacklisted creator."
      }
      let messageBlacklisted = createKfElement("small", "blacklisted_msg", message);
      messageBlacklisted.addEventListener("click", showPostsFromBlacklistedCreators);
      messageContainer.appendChild(messageBlacklisted);
    }

    if (hiddenPosts.alreadyShown > 0) {
      let message;
      if (hiddenPosts.alreadyShown > 1) {
        message = `Hid ${hiddenPosts.alreadyShown} posts already shown on previous pages.`;
      } else {
        message = "Hid 1 post already shown on previous pages.";
      }
      let messageAlreadyShown = createKfElement("small", "already_shown_msg", message);
      messageAlreadyShown.addEventListener("click", showPostsFromPreviousPages);
      messageContainer.appendChild(messageAlreadyShown);
    }

    updateStyles();
  }
  newPostCardContainer.setKfAttr("href", window.location.href);
  srcPostCardContainer.after(newPostCardContainer);
  srcPostCardContainer.style.display = "none";
}

function fixThumbnailAspectRatio(event) {
  let img = event.target;
  let aspectRatio = img.naturalWidth / img.naturalHeight;
  if (OPTIONS.adapt_post_card_height_to_thumbnail) {
    img.closest("article").style.aspectRatio = Math.min(aspectRatio, 1);
    if (aspectRatio - 1 > 0.167) img.style.objectFit = "contain";
  } else {
    if (aspectRatio - 2/3 > 0.167) img.style.objectFit = "contain";
  }
  img.style.opacity = 1;
}

function showPostsWithNoThumbnail(event) {
  removeStyle("hide_no_thumbnail");
}

function showPostsFromBlacklistedCreators(event) {
  removeStyle("hide_blacklisted");
}

function showPostsFromPreviousPages(event) {
  removeStyle("hide_already_shown");
}

function preventDefault(event) {
  event.preventDefault();
}

function blacklistCreator(event) {
  event.preventDefault();
  let card = event.target.closest("article");
  let creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
  let message = `Do you want to blacklist ${creatorKey}?`;
  let title = card.qOne("header").childNodes[0];
  if (title) {
    message += `\n(Creator of post ${title.textContent})`;
  }
  if (confirm(message)) {
    addToBlacklist(creatorKey, title ? title.textContent : "");
  }
}

function addToBlacklist(creatorKey, comment) {
  BLACKLIST_RAW[creatorKey] = comment;
  GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST_RAW));
  alert(`${creatorKey} added to blacklist, reload to take effect.`);
}

function unblacklistCreator(event) {
  event.preventDefault();
  let card = event.target.closest("article");
  let creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
  let message = `Do you want to unblacklist ${creatorKey}?`;
  let title = card.qOne("header").childNodes[0];
  if (title) {
    message += `\n(Creator of post ${title.textContent})`;
  }
  if (confirm(message)) {
    removeFromBlacklist(creatorKey);
  }
}

function removeFromBlacklist(creatorKey) {
  delete BLACKLIST_RAW[creatorKey];
  GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST_RAW));
  alert(`${creatorKey} removed from blacklist, reload to take effect.`);
}
// ==</Page Fixing Functions>==

// ==<CSS>==
const CSS = {};
CSS.posts = `#kf-postCardContainer {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 5px;
  padding: 0 5px;
  align-items: end;
  justify-content: center;
}
.card-list {
  container-type: inline-size;
  container-name: postCardContainerParent;
}
@container postCardContainerParent (min-width: 1845px) {
  #kf-postCardContainer {
    grid-template-columns: repeat(10, 1fr);
  }
}
#kf-postCardContainer article {
  width: auto;
  height: auto;
  aspect-ratio: 2/3;
  filter: drop-shadow(0 2px 2px #000);
}
article a {
  border: 0;
}
.post-card__image-container {
  overflow: hidden;
}
.post-card__image {
  opacity: 0;
  transition: opacity 0.5s ease-out, filter 0.5s ease-out;
}
.kf-no_thumbnail footer::before {
  content: "No Thumbnail";
}
.kf-blacklisted .post-card__image {
  filter: blur(10px);
}
.kf-blacklisted .post-card__image:hover {
  filter: blur(0);
}
.kf-already_shown {
  opacity: .5;
}
#kf-no_thumbnail_msg,
#kf-blacklisted_msg,
#kf-already_shown_msg {
  display: none;
}
#kf-blacklist_control {
  z-index: 99;
  position: absolute;
  width: 100%;
  height: 100%;
  display: none;
  justify-content: center;
  align-items: center;
  cursor: default;
}
#kf-blacklist_control input {
  border: none;
  border-radius: 10px;
  padding: 5px;
  font-size: 15px;
  background: #9c2121;
  color: white;
  font-weight: bold;
}
#kf-blacklist_control input:hover {
  background: #d94a4a;
  cursor: pointer;
}
#kf-blacklist_control input[value="Remove from Blacklist"] {
  background: #47d5a6;
}
#kf-blacklist_control input[value="Remove from Blacklist"]:hover {
  background: #9ae8ce;
}

`;
if (OPTIONS.adapt_post_card_height_to_thumbnail) CSS.posts += `
#kf-postCardContainer article {
  aspect-ratio: 1;
  transition: aspect-ratio 0.5s ease-out;
}
#kf-postCardContainer .kf-no_thumbnail {
  aspect-ratio: unset;
  height: fit-content;
}`;
CSS.hideNoThumbnail = `.kf-no_thumbnail { display: none; }
#kf-no_thumbnail_msg { display: block !important; }`;
CSS.hideBlacklisted = `.kf-blacklisted { display: none; }
#kf-blacklisted_msg { display: block !important; }`;
CSS.hideAlreadyShown = `.kf-already_shown { display: none; }
#kf-already_shown_msg { display: block !important; }`;
CSS.blacklistMode = `.kf-blacklisted .post-card__image { filter: blur(0) !important; }
#kf-blacklist_control { display: flex !important; }`;
// ==</CSS>==
})();