Kemono QoL Improvements

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

Versão de: 24/09/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name        Kemono QoL Improvements
// @namespace   Kemono_QoL_Improvements
// @license     WTFPL
// @match       https://kemono.cr/*
// @match       https://coomer.st/*
// @grant       none
// @version     3.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 option_hide_posts_with_no_preview = true;

const hide_group = [
  "fanbox-000000000",  // [UserName] I recommend you to add comments like this,
  "patreon-000000000", // [UserName] so you don't forget why you added them to the blacklist.
];

const highlight_group_1 = [ // Highlight posts from these users (red background)
  "fanbox-000000000",  // [UserName] I use this to mark creators who have their contents
  "patreon-000000000", // [UserName] stored on external services like MEGA / Google Drive.
];

const highlight_group_2 = [ // Highlight posts from these users (green background)
  "fanbox-000000000",  // [UserName] You can leave this list empty (highlight_group_2 = [];)
  "patreon-000000000", // [UserName] if you don't need this feature.
];

const custom_header = new Map([ // Display a custom line below the post title
  ["fanbox-000000000",  "Custom Name"], // You can use this to display the user's changed name,
  ["patreon-000000000", "Custom Tags"], // or anything you want like tags. Leave empty to disable.
]);

/* Here's a helper bookmarklet that converts your favorite users into the format of the "custom_header" above:

javascript:(function()%7Blet%20output%3D%5B%5D%3BJSON.parse(document.getElementsByTagName('textarea')%5B0%5D.textContent).artists.forEach(artist%3D%3E%7Blet%20key%3D%60%24%7Bartist.service%7D-%24%7Bartist.id%7D%60%3Blet%20line%3D%60%5B%22%24%7Bkey.trim()%7D%22%2C%60%3Boutput.push(%60%20%20%24%7Bline.padEnd(21%2C%20'%20')%7D%20%22%24%7Bartist.name.trim()%7D%22%5D%2C%60)%7D)%3Bnavigator.clipboard.writeText(output.join('%5Cn'))%7D)()%3B

1. Save the entire line as a bookmark and put it on the bookmark bar
2. Go to Export Favorites page (https://kemono.cr/account/favorites/export)
3. Click on "Export Favorites", wait for it to finish (you'll see a wall of text in a textbox)
4. Click on the bookmarklet to run the script, the output will be copied to the clipboard
*/

// ==<User Settings>==

// ==<CSS>==
// Custom classes (__usr_*) are added to the <article> elements with functions below,
// so we won't have long and complicate css selectors as user lists above grows.

const css_posts_page = `img.post-card__image { object-fit: contain; }
article.post-card { height: calc(var(--card-size) / 2 * 3); }
article.__usr_blacklisted { opacity: 0.5; }
article.__usr_no_previews { height: fit-content; }
article.__usr_highlight_group_1 header, article.__usr_highlight_group_1 footer { background: rgb(153 0 0 / 50%) !important; }
article.__usr_highlight_group_2 header, article.__usr_highlight_group_2 footer { background: rgb(24 153 0 / 50%) !important; }
article header > div { text-align: right; }`;

const css_posts_page_blacklisted = `article.__usr_blacklisted { display: none; }`;
const css_posts_page_no_previews = `article.__usr_no_previews { display: none; }`;

// Don't hide or highlight posts on user page
const css_user_page = `img.post-card__image { object-fit: contain; }
article.post-card { height: calc(var(--card-size) / 2 * 3); }`;

// Allow images to tile horizontally on single post page
const css_post_page = `div.post__files { flex-flow: wrap; }`;

// ==</CSS>==

// ==<Script Functions>==

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");
    if (styleElement) styleElement.remove();
    styleElement = document.createElement("style");
    styleElement.id = "__usr__page_style_hide_blacklisted";
    styleElement.textContent = css_posts_page_blacklisted;
    document.head.append(styleElement);

    styleElement = document.getElementById("__usr__page_style_hide_no_previews");
    if (styleElement) styleElement.remove();
    styleElement = document.createElement("style");
    styleElement.id = "__usr__page_style_hide_no_previews";
    styleElement.textContent = css_posts_page_no_previews;
    document.head.append(styleElement);

  } else if (window.location.pathname.includes("/user/")) {
    if (window.location.pathname.includes("/post/")) {
      styleElement.textContent = css_post_page;
    } 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();

  let postsHolder = document.getElementsByClassName("card-list__items")[0];
  let posts = postsHolder.getElementsByTagName("article");
  if (posts.length === 0) return;

  let count_posts_blacklisted = 0;
  let count_posts_no_previews = 0;
  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 => {
    let key = `${post.getAttribute("data-service")}-${post.getAttribute("data-user")}`;
    if (hide_group.includes(key)) {
      post.classList.add("__usr_blacklisted");
      count_posts_blacklisted++;
    }
    if (option_hide_posts_with_no_preview && post.getElementsByClassName("post-card__image-container").length === 0) {
      post.classList.add("__usr_no_previews");
      count_posts_no_previews++;
    }
    if (highlight_group_1.includes(key)) {
      post.classList.add("__usr_highlight_group_1");
    } else if (highlight_group_2.includes(key)) {
      post.classList.add("__usr_highlight_group_2");
    }
    if (custom_header.get(key)) {
      let creatorElement = document.createElement("div");
      creatorElement.textContent = custom_header.get(key);
      post.getElementsByTagName("header")[0].appendChild(creatorElement);
    }
    postsHolder.appendChild(post);
  });

  sortMessageElement = document.createElement("small");
  sortMessageElement.id = "__usr__sort_message";
  hidPostsMessageElement = document.createElement("div");
  hidPostsMessageElement.id = "__usr__hid_posts_message";
  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);

  let hidPostsMessages = [];
  if (count_posts_blacklisted > 0) {
    let blacklistedUsersMessageElement = document.createElement("small");
    blacklistedUsersMessageElement.id = "__usr__hid_posts_message_blacklisted";
    blacklistedUsersMessageElement.addEventListener('click', function() { showPostsFromBlacklistedUsers(); });
    blacklistedUsersMessageElement.textContent = `${count_posts_blacklisted} ${count_posts_blacklisted > 1 ? "posts from blacklisted users" : "post from a blacklisted user"}`;
    hidPostsMessages.push(blacklistedUsersMessageElement);
  }
  if (count_posts_no_previews > 0) {
    let noPreviewImagesMessageElement = document.createElement("small");
    noPreviewImagesMessageElement.id = "__usr__hid_posts_message_no_previews";
    noPreviewImagesMessageElement.addEventListener('click', function() { showPostsWithNoPreviewImages(); });
    noPreviewImagesMessageElement.textContent = `${count_posts_no_previews} ${count_posts_no_previews > 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)
  }
}

function showPostsFromBlacklistedUsers() {
  document.getElementById("__usr__page_style_hide_blacklisted").remove();
  observer.disconnect();
  if (document.getElementById("__usr__hid_posts_message_separator")) {
    document.getElementById("__usr__hid_posts_message_separator").remove();
    document.getElementById("__usr__hid_posts_message_blacklisted").remove()
  } else {
    document.getElementById("__usr__hid_posts_message").remove();
  }
  observer.observe(document.body, { childList: true, subtree: true });
}

function showPostsWithNoPreviewImages() {
  document.getElementById("__usr__page_style_hide_no_previews").remove();
  observer.disconnect();
  if (document.getElementById("__usr__hid_posts_message_separator")) {
    document.getElementById("__usr__hid_posts_message_separator").remove();
    document.getElementById("__usr__hid_posts_message_no_previews").remove()
  } else {
    document.getElementById("__usr__hid_posts_message").remove();
  }
  observer.observe(document.body, { childList: true, subtree: true });
}

// ==</Script Functions>==

// ==<Main>==
// Kemono is a SPA (Single Page Application).
// We use MutationObserver to (re)run scripts when page content changes.

let mutationTimeout;
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
  clearTimeout(mutationTimeout);
  if (window.location.pathname === "/posts" || window.location.pathname === "/posts/popular") {
    mutationTimeout = setTimeout(() => {
      observer.disconnect();
      sortPosts();
      observer.observe(document.body, { childList: true, subtree: true });
    }, 100);
  }
});
observer.observe(document.body, { childList: true, subtree: true });

// ==</Main>==
})();