Kemono QoL Improvements

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

24.09.2025 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name        Kemono QoL Improvements
// @namespace   Kemono_QoL_Improvements
// @license     WTFPL
// @match       https://kemono.cr/*
// @match       https://coomer.st/*
// @grant       none
// @version     3.4
// @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 getThumbSize() {
  let defaultSize = 180;
  let thumbSize = 180;
  // ==<Copied Code from card_list.tsx>==
  let viewportWidth = window.innerWidth;
  let offset = 24;
  let viewportWidthExcludingMargin = viewportWidth - offset;
  let howManyFit = viewportWidthExcludingMargin / thumbSize;

  if (howManyFit < 2.0 && 1.5 < howManyFit) {
    thumbSize = viewportWidthExcludingMargin / 2;
  } else if (howManyFit > 12) {
    thumbSize = defaultSize * 1.5;
  }
  // ==</Copied Code from card_list.tsx>==
  return thumbSize;
}

function sortPosts() {
  let sortedPostsHolder = document.getElementById("__usr__sorted_posts_holder");
  if (sortedPostsHolder) sortedPostsHolder.remove();
  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];
  if (!postsHolder) return;
  postsHolder.style.display = "";
  let posts = postsHolder.getElementsByTagName("article");
  if (posts.length === 0) return;

  let count_posts_blacklisted = 0;
  let count_posts_no_previews = 0;
  let blacklistedUsers = [];

  let sortedPosts = [];
  Array.from(posts).forEach(originalPost => {
    let post = originalPost.cloneNode(true);
    let key = `${post.getAttribute("data-service")}-${post.getAttribute("data-user")}`;
    if (hide_group.includes(key)) {
      post.classList.add("__usr_blacklisted");
      count_posts_blacklisted++;
      if (!blacklistedUsers.includes(key)) blacklistedUsers.push(key);
    }
    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);
    }
    sortedPosts.push(post);
  });

  sortedPosts = sortedPosts.sort( (a, b) => new Date(b.getElementsByTagName("time")[0].dateTime) - new Date(a.getElementsByTagName("time")[0].dateTime) );
  sortedPostsHolder = document.createElement("div");
  sortedPostsHolder.id = "__usr__sorted_posts_holder";
  sortedPostsHolder.classList.add("card-list__items");
  sortedPostsHolder.setAttribute("style", `--card-size:${getThumbSize()}px;`);
  sortedPosts.forEach(post => {
    sortedPostsHolder.appendChild(post);
  });
  postsHolder.style.display = "none";
  postsHolder.after(sortedPostsHolder);

  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 " : "post from "}`;
    if (blacklistedUsers.length > 1) {
      blacklistedUsersMessageElement.textContent += `${blacklistedUsers.length} blacklisted users`;
    } else {
      blacklistedUsersMessageElement.textContent += "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.

const debug_mode = true;
let mutationTimeout;
let observer = new MutationObserver(mutations => {
  if (debug_mode) console.log(`[${performance.now()}] == mutations begin: ${window.location.href} ==`);
  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") {
    clearTimeout(mutationTimeout);
    mutations.forEach(mutation => {
      if (debug_mode) console.log(`[${performance.now()}] mutation target: ${mutation.target} classList ${mutation.target.classList}`);
    });
    mutationTimeout = setTimeout(() => {
      observer.disconnect();
      if (debug_mode) console.log(`[${performance.now()}] == sort begin: ${window.location.href} ==`);
      sortPosts();
      if (debug_mode) console.log(`[${performance.now()}] == sort end: ${window.location.href} ==`);
      observer.observe(document.body, { childList: true, subtree: true });
    }, 100);
  }
  if (debug_mode) console.log(`[${performance.now()}] == mutations end: ${window.location.href} ==`);
});
observer.observe(document.body, { childList: true, subtree: true });

// ==<Adjust Thumbnail Size on Window Resize>==
window.addEventListener("resize", () => {
  let sortedPostsHolder = document.getElementById("__usr__sorted_posts_holder");
  if (!sortedPostsHolder) return;
  sortedPostsHolder.setAttribute("style", `--card-size:${getThumbSize()}px;`);
});
// ==</Adjust Thumbnail Size on Window Resize>==

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