Kemono QoL Improvements

Allow you to hide or highlight posts by user id on Posts / Popular Posts page.

As of 2025-09-23. See the latest version.

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.

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.

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

// ==UserScript==
// @name        Kemono QoL Improvements
// @namespace   Kemono_QoL_Improvements
// @license     WTFPL
// @match       https://kemono.cr/*
// @match       https://coomer.st/*
// @grant       none
// @version     2.2
// @author      Kaban
// @description Allow you to hide or highlight posts by user id on Posts / Popular Posts page.
// @description Allow you to hide posts with no preview images on Posts / Popular Posts page.
// @description Auto-sorts posts by Post time on Posts / Popular Posts page (within current page).
// @description Makes post thumbnails taller, allow in-post images to be tiled horizontally.
// ==/UserScript==
(() => {
// ==<User Settings>==
 
const hide_group = [
  ["fanbox",   "00000000"], // [UserName] I recommend you to take note here as comments, so that
  ["patreon", "000000000"]  // [UserName] you don't forget why you added them to the blacklist.
];
 
const highlight_group_1 = [ // Red Background
  ["patreon",  "00000000"], // [UserName] If you clear this list: const highlight_group_1 = [];
  ["patreon", "000000000"]  // [UserName] Then this group will be skipped.
]
 
const highlight_group_2 = [ // Green Background
  ["patreon",  "00000000"], // [UserName] You may also add more groups as you like, just modify the code under
  ["patreon", "000000000"]  // [UserName] section <CSS Generation> as well, I tried to make it easy to read.
];
 
const option_hide_posts_with_no_preview_images = true;
 
// ==</User Settings>==
 
// ==<CSS Generation>==
let styles = [];
let selectors = [];
 
hide_group.forEach(user => {
  selectors.push(`article[data-service="${user[0]}"][data-user="${user[1]}"]`);
});
const selector_blacklisted_users = selectors.join(",\n"); // used later for counting how many posts hidden
const css_posts_page_blacklisted_users = `${selector_blacklisted_users} {
  display: none;
}`;
styles.push(`${selector_blacklisted_users} {
  opacity: 0.5;
}`); // make the hidden posts remain semi-transparent when click on the message to show them
 
selectors = [];
highlight_group_1.forEach(user => {
  selectors.push(`article[data-service="${user[0]}"][data-user="${user[1]}"] header.post-card__header`);
  selectors.push(`article[data-service="${user[0]}"][data-user="${user[1]}"] footer.post-card__footer`);
});
styles.push(`${selectors.join(",\n")} {
  background: rgb(153 0 0 / 50%);
}`);

selectors = [];
highlight_group_2.forEach(user => {
  selectors.push(`article[data-service="${user[0]}"][data-user="${user[1]}"] header.post-card__header`);
  selectors.push(`article[data-service="${user[0]}"][data-user="${user[1]}"] footer.post-card__footer`);
});
styles.push(`${selectors.join(",\n")} {
  background: rgb(24 153 0 / 50%);
}`);
 
styles.push(`article.post-card {
  height: calc(var(--card-size) / 2 * 3);
}
img.post-card__image {
  object-fit: contain;
}`); // make posts taller on Posts / Popular Posts page
 
const selector_no_preview_images = "article.post-card--preview.post-card:not(:has(div.post-card__image-container))"; // used later for counting how many posts hidden
const css_posts_page_hide_no_preview_images = `${selector_no_preview_images} {
  display: none;
}`;

styles.push(`${selector_no_preview_images} {
  height: fit-content;
}`); // make posts with no preview images shorter, so the take less space when shown

const css_posts_page = styles.join("\n");
 
const css_user_page = `article.post-card {
  height: calc(var(--card-size) / 2 * 3);
}
img.post-card__image {
  object-fit: contain;
}`; // just make the posts taller, don't hide any posts on User page
 
const css_user_page_in_post = `div.post__files {
  flex-flow: wrap;
}`; // allow images to tile horizontally on single Post page
// ==</CSS Generation>==
 
let old_url = "";
function addStyles() {
  if (window.location.href === old_url) return; // run only once per url change
  old_url = window.location.href;
 
  let styleElement = document.getElementById("__usr__page_style");
  if (styleElement) styleElement.remove();
  styleElement = document.createElement("style");
  styleElement.id = "__usr__page_style";
 
  if (window.location.pathname === "/posts" || window.location.pathname === "/posts/popular") {
    styleElement.textContent = css_posts_page;
    document.head.append(styleElement);
 
    styleElement = document.getElementById("__usr__page_style_hide_blacklisted_users");
    if (styleElement) styleElement.remove();
    styleElement = document.createElement("style");
    styleElement.id = "__usr__page_style_hide_blacklisted_users";
    styleElement.textContent = css_posts_page_blacklisted_users;
    document.head.append(styleElement);
 
    if (!option_hide_posts_with_no_preview_images) return;
    styleElement = document.getElementById("__usr__page_style_hide_no_preview_images");
    if (styleElement) styleElement.remove();
    styleElement = document.createElement("style");
    styleElement.id = "__usr__page_style_hide_no_preview_images";
    styleElement.textContent = css_posts_page_hide_no_preview_images;
    document.head.append(styleElement);
 
  } else if (window.location.pathname.includes("/user/")) {
    if (window.location.pathname.includes("/post/")) {
      styleElement.textContent = css_user_page_in_post;
    } else {
      styleElement.textContent = css_user_page;
    }
    document.head.append(styleElement);
  }
}
 
function sortPosts() {
  let sortMessageElement = document.getElementById("__usr__sort_message");
  if (sortMessageElement) sortMessageElement.remove();
  let hidPostsMessageElement = document.getElementById("__usr__hid_posts_message");
  if (hidPostsMessageElement) hidPostsMessageElement.remove();
  sortMessageElement = document.createElement("small");
  sortMessageElement.id = "__usr__sort_message";
  hidPostsMessageElement = document.createElement("div");
  hidPostsMessageElement.id = "__usr__hid_posts_message";
 
  let postsHolder = document.getElementsByClassName("card-list__items")[0];
  let posts = postsHolder.getElementsByTagName("article");
  if (posts.length === 0) return;
  if (posts.length > 1) {
    // ==<Sort Posts>==
    let sortedPosts = Array.from(posts).sort( (a, b) => new Date(b.getElementsByTagName("time")[0].dateTime) - new Date(a.getElementsByTagName("time")[0].dateTime) );
    postsHolder.replaceChildren();
    sortedPosts.forEach(post => {
      postsHolder.appendChild(post);
    });
    // ==</Sort Posts>==
  }
  let insertPosition = document.getElementById("paginator-top").getElementsByTagName("small")[0];
  if (insertPosition) {
    sortMessageElement.textContent = " posts (sorted by Post time).";
    insertPosition.after(sortMessageElement);
  } else {
    sortMessageElement.textContent = `Found ${posts.length} ${posts.length > 1 ? "posts (sorted by Post time)." : "post."}`;
    document.getElementById("paginator-top").appendChild(sortMessageElement);
  }
  sortMessageElement.after(hidPostsMessageElement);
 
  // ==<Count Hidden Posts>==
  let hidPostsMessages = [];
 
  let hid_posts = document.querySelectorAll(selector_blacklisted_users);
  if (hid_posts.length > 0) {
    let blacklistedUsersMessageElement = document.createElement("small");
    blacklistedUsersMessageElement.id = "__usr__hid_posts_message_blacklisted_users";
    blacklistedUsersMessageElement.addEventListener('click', function() { showPostsFromBlacklistedUsers(); });
    blacklistedUsersMessageElement.textContent = `${hid_posts.length} ${hid_posts.length > 1 ? "posts from blacklisted users" : "post from a blacklisted user"}`;
    hidPostsMessages.push(blacklistedUsersMessageElement);
  }
  if (option_hide_posts_with_no_preview_images) {
    hid_posts = document.querySelectorAll(selector_no_preview_images);
    if (hid_posts.length > 0) {
      let noPreviewImagesMessageElement = document.createElement("small");
      noPreviewImagesMessageElement.id = "__usr__hid_posts_message_no_preview_images";
      noPreviewImagesMessageElement.addEventListener('click', function() { showPostsWithNoPreviewImages(); });
      noPreviewImagesMessageElement.textContent = `${hid_posts.length} ${hid_posts.length > 1 ? "posts with no preview images" : "post with no preview image"}`;
      hidPostsMessages.push(noPreviewImagesMessageElement);
    }
  }
 
  if (hidPostsMessages.length > 0) {
    let prefix = document.createElement("small");
    prefix.textContent = "Hid ";
    hidPostsMessageElement.appendChild(prefix)
    hidPostsMessageElement.appendChild(hidPostsMessages[0]);
    if (hidPostsMessages.length > 1) {
      let separator = document.createElement("small");
      separator.id = "__usr__hid_posts_message_separator";
      separator.textContent = ", ";
      hidPostsMessageElement.appendChild(separator);
      hidPostsMessageElement.appendChild(hidPostsMessages[1]);
    }
    let suffix = document.createElement("small");
    suffix.textContent = ".";
    hidPostsMessageElement.appendChild(suffix)
  }
  // ==</Count Hidden Posts>==
 
}
 
// Kemono is a SPA (Single Page Application)
// It is necessary to use MutationObserver in order to run scripts when switching between pages
let observer = new MutationObserver(mutations => {
  addStyles(); // this will fire multiple times as the page loads, but I want the styles to be added as early as possible so keep it here
  if (window.location.pathname === "/posts" || window.location.pathname === "/posts/popular") {
    let pageLoaded = false;
    mutations.forEach(mutation => {
      if (mutation.target.classList.contains("ad-container")) pageLoaded = true; // div.ad-container is the last mutations observerd on page update
    });
    if (pageLoaded) {
      observer.disconnect();
      sortPosts();
      observer.observe(document.body, { childList: true, subtree: true });
    }
  }
});
observer.observe(document.body, { childList: true, subtree: true });
 
function showPostsFromBlacklistedUsers() {
  document.getElementById("__usr__page_style_hide_blacklisted_users").remove();
  if (document.getElementById("__usr__hid_posts_message_separator")) {
    document.getElementById("__usr__hid_posts_message_separator").remove();
    document.getElementById("__usr__hid_posts_message_blacklisted_users").remove()
  } else {
    document.getElementById("__usr__hid_posts_message").remove();
  }
}
 
function showPostsWithNoPreviewImages() {
  document.getElementById("__usr__page_style_hide_no_preview_images").remove();
  if (document.getElementById("__usr__hid_posts_message_separator")) {
    document.getElementById("__usr__hid_posts_message_separator").remove();
    document.getElementById("__usr__hid_posts_message_no_preview_images").remove()
  } else {
    document.getElementById("__usr__hid_posts_message").remove();
  }
}
 
})();