Kemono QoL Improvements

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

Od 19.09.2025.. Pogledajte najnovija verzija.

// ==UserScript==
// @name        Kemono QoL Improvements
// @namespace   Kemono_QoL_Improvements
// @license     WTFPL
// @match       https://kemono.cr/*
// @match       https://coomer.st/*
// @grant       none
// @version     2.0.1
// @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]}"]`);
});
styles.push(`${selectors.join(",\n")} {
  opacity: rgb(153 0 0 / 50%);
}`);
 
selectors = [];
highlight_group_2.forEach(user => {
  selectors.push(`article[data-service="${user[0]}"][data-user="${user[1]}"]`);
});
styles.push(`${selectors.join(",\n")} {
  opacity: rgb(24 153 0 / 50%)
}`);
 
styles.push(`article.post-card {
  height: calc(var(--card-size) / 9 * 16);
}
img.post-card__image {
  object-fit: contain;
}`); // make posts taller on Posts / Popular Posts page
 
const css_posts_page = styles.join("\n");
 
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;
}`;
 
const css_user_page = `article.post-card {
  height: calc(var(--card-size) / 9 * 16);
}
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();
  }
}
 
})();