Allow you to blacklist creators on Kemono, and more.
// ==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>==
})();