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_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @version 1.1
// @author Kaban
// @description Allow you to blacklist creators on Kemono, and more.
// ==/UserScript==
(function() {
"use strict";
// ==<Options>==
let OPTIONS = {
"allow_wide_post_cards": true,
"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,
"remove_video_section_from_post_body": false,
"remove_duplicate_files_from_post": true,
}
readOptions(OPTIONS, "OPTIONS");
let ADVANCED_OPTIONS = {
"dont_allow_grid_auto_flow": false,
"post_duplicate_files_keep_first": false,
"copy_post_folder_name_template": "[{published}] {title}",
"copy_creator_folder_name_template": "[{creator}] {service}",
"copy_post_file_links_template": "#{index}_{filename}&t={title}&s={service}&c={creator}&p={published}",
"copy_post_attachment_links_template": "#{filename}&t={title}&s={service}&c={creator}&p={published}",
}
readOptions(ADVANCED_OPTIONS, "ADVANCED_OPTIONS");
let BLACKLIST_MODE = false;
let BLACKLIST_RAW = JSON.parse(GM_getValue("BLACKLIST", `{}`));
let BLACKLIST = Object.keys(BLACKLIST_RAW);
function readOptions(defaults, storedKey) {
let read = JSON.parse(GM_getValue(storedKey, `{}`));
Object.keys(read).forEach(key => {
if (defaults.hasOwnProperty(key)) defaults[key] = read[key];
});
}
// ==</Options>==
// ==<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;
}
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 kfExistByPath(id, segmentId = 0) {
let element = gOne(`kf-${id}`);
if (!element) return false;
if (segmentId > 0) {
if (element.getKfAttr("segment") === window.location.pathname.split('/')[segmentId]) return true;
} else {
if (element.getKfAttr("pathname") === window.location.pathname) return true;
}
element.remove();
return false;
}
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}`);
}
HTMLElement.prototype.hasClass = function(className) {
return this.classList.contains(className);
}
HTMLElement.prototype.cloneKfNode = function(id = null) {
let clonedNode = this.cloneNode(true);
if (id) clonedNode.id = `kf-${id}`;
clonedNode.querySelectorAll("[id]").forEach(node => {
node.id = `kf-${node.id}`;
});
return clonedNode;
}
// ==</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();
// dispatch
updatePageInfo();
updateStyle();
updateScriptMenu();
fixPage();
// restart observer
observer.observe(document.body, { childList: true, subtree: true });
}
// ==</Mutation Observer>==
// ==<Shared Info>==
let pageInfo = {};
function updatePageInfo() {
if (pageInfo.href === window.location.href) return;
pageInfo = {};
let pathname = window.location.pathname;
if (pathname === "/posts") {
pageInfo.pageType = "Posts";
} else if (pathname === "/posts/popular") {
pageInfo.pageType = "Popular Posts";
} else {
let segments = pathname.split('/').filter(segment => segment);
if (segments[1] === "user") {
// {service}/user/{user_id}
if (segments.length === 3) {
pageInfo.pageType = "Creator Posts";
} 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"
) {
pageInfo.pageType = "Post Details";
}
// discord/server/{server_id}/{room_id}
} else if (segments.length === 4 && segments[0] === "discord") {
//fixDiscord();
pageInfo.pageType = "Discord";
}
}
}
pageInfo.href = window.location.href;
}
// ==</Shared Info>==
// ==<Style>==
const CSS = {};
CSS.posts = `#kf-post_cards_container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));`;
if (!ADVANCED_OPTIONS.dont_allow_grid_auto_flow) CSS.posts += `
grid-auto-flow: row dense;`;
CSS.posts +=`
grid-auto-rows: 1fr;
gap: 5px;
padding: 0 5px;
align-items: end;
justify-content: center;
text-align: left; /* fix for popular posts page */
}
.card-list {
container-type: inline-size;
container-name: post_card_container_parent;
}
@container post_card_container_parent (min-width: 1845px) {
#kf-post_cards_container {
grid-template-columns: repeat(10, 1fr);
}
}
#kf-post_cards_container article {
aspect-ratio: 2/3;
width: 100%;
height: 100%;
filter: drop-shadow(0 2px 2px #000);
}
.kf-wide_card {
grid-column: span 2;
height: 100%;
}
#kf-post_cards_container .kf-wide_card article {
aspect-ratio: 365/270; /* only used when there's not a single narrow card */
}
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-already_shown:hover {
opacity: 1;
}
#kf-no_thumbnail_msg,
#kf-blacklisted_msg,
#kf-already_shown_msg {
display: none;
}
#kf-blacklist_control {
z-index: 1;
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;
}
/* disable hover effect on mobile */
@media (hover: none) and (pointer: coarse) {
menu > a:hover {
background: unset !important;
color:var(--color0-secondary) !important;
}
}`;
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.postDetails = `#kf-post_user {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#kf-post_user a {
border: none;
background-color: unset;
margin-top: .5em;
}
#kf-post_body {
border-left: solid hsl(0,0%,50%) 0.125em;
border-right: solid hsl(0,0%,50%) 0.125em;
padding: 0.5em;
}
#kf-post_attachments {
list-style: none;
padding: .5 em;
margin: 0;
}
#kf-post_attachments li {
padding: .25em 0;
}
#kf-post_files {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr) minmax(360px, 1fr));
gap: 5px;
align-items: end;
justify-content: center;
}
#kf-post_files a {
display: block;
padding: 0;
border: none;
background-color: unset;
}
#kf-post_files img {
display: block;
width: 100%;
height: 100%;
}
.kf-wide_thumb {
grid-column: span 2;
}
#kf-post_files img {
opacity: 0;
transition: opacity 0.5s ease-out, filter 0.5s ease-out;
}`;
function updateStyle() {
switch (pageInfo.pageType) {
case "Posts":
case "Popular Posts":
case "Creator Posts":
addStyle(CSS.posts);
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");
}
break;
case "Post Details":
removeStyle("hide_no_thumbnail");
removeStyle("hide_blacklisted");
removeStyle("hide_already_shown");
addStyle(CSS.postDetails);
break;
case "Discord":
default:
removeStyle("hide_no_thumbnail");
removeStyle("hide_blacklisted");
removeStyle("hide_already_shown");
removeStyle();
break;
}
}
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();
}
// ==</Style>==
// ==<Script Menu>==
function updateScriptMenu() {
let scriptMenuFlag = createKfElement("div", "script_menu_flag");
switch (pageInfo.pageType) {
case "Posts":
case "Popular Posts":
case "Creator Posts":
if (kfExistByPath("script_menu_flag")) return;
removeAllMenuCommand();
updateMenuCommand("Allow Wide Post Cards",
"allow_wide_post_cards", true);
updateMenuCommand("Sort Posts by Publish Date",
"sort_posts_by_publish_date", true);
updateMenuCommand("Hide Posts with No Thumbnail",
"hide_posts_with_no_thumbnail", false);
updateMenuCommand("Hide Posts from Blacklisted Creators",
"hide_posts_from_blacklisted_creators", false);
updateMenuCommand("Hide Posts Shown on Previous Pages",
"hide_posts_shown_on_previous_pages", false);
addMenuSeparator();
GM_registerMenuCommand(
"▶ Enter Blacklist Mode", blacklistMode, { id: "blacklistMode" });
GM_registerMenuCommand(
"⚙ Advanced Options", advancedOptions, { id: "advancedOptions" });
scriptMenuFlag.setKfAttr("pathname", window.location.pathname);
break;
case "Post Details":
if (kfExistByPath("script_menu_flag", 3)) return;
removeAllMenuCommand();
updateMenuCommand("Allow Wide File Thumbnails",
"allow_wide_post_cards", true);
updateMenuCommand("Remove Duplicate Post Files",
"remove_duplicate_files_from_post", true);
updateMenuCommand("Remove Videos Section",
"remove_video_section_from_post_body", true);
addMenuSeparator();
GM_registerMenuCommand(
"⚙ Advanced Options", advancedOptions, { id: "advancedOptions" });
scriptMenuFlag.setKfAttr("segment", window.location.pathname.split('/')[3]);
break;
case "Discord":
default:
if (kfExist("script_menu_flag")) return;
removeAllMenuCommand();
GM_registerMenuCommand(
"⚙ Advanced Options", advancedOptions, { id: "advancedOptions" });
scriptMenuFlag.setKfAttr("href", window.location.href);
}
document.body.appendChild(scriptMenuFlag);
}
function removeAllMenuCommand() {
GM_unregisterMenuCommand("allow_wide_post_cards");
GM_unregisterMenuCommand("sort_posts_by_publish_date");
GM_unregisterMenuCommand("hide_posts_with_no_thumbnail");
GM_unregisterMenuCommand("hide_posts_from_blacklisted_creators");
GM_unregisterMenuCommand("hide_posts_shown_on_previous_pages");
GM_unregisterMenuCommand("remove_duplicate_files_from_post");
GM_unregisterMenuCommand("remove_video_section_from_post_body");
GM_unregisterMenuCommand("separator");
GM_unregisterMenuCommand("blacklistMode");
GM_unregisterMenuCommand("advancedOptions");
}
function addMenuSeparator() {
GM_registerMenuCommand(
"---------------------------------------",
null,
{ id: "separator", autoClose: false }
);
}
function updateMenuCommand(menuName, optionName, reloadPage = false) {
GM_registerMenuCommand(
`${OPTIONS[optionName] ? "☑" : "☐"} ${menuName}`,
() => {
toggleOption(optionName, menuName, reloadPage)
},
{
id: optionName,
title: `${menuName}${reloadPage ? " (Reloads current page)" : ""}`
}
);
}
function toggleOption(optionName, menuName, reloadPage) {
OPTIONS[optionName] = !OPTIONS[optionName];
updateMenuCommand(menuName, optionName, reloadPage);
GM_setValue("OPTIONS", JSON.stringify(OPTIONS));
if (reloadPage) {
window.location.reload();
} else {
updateStyle();
}
}
function blacklistMode() {
BLACKLIST_MODE = !BLACKLIST_MODE;
updateStyle();
if (BLACKLIST_MODE) {
GM_registerMenuCommand(
"◀ Exit Blacklist Mode", blacklistMode, { id: "blacklistMode" });
} else {
GM_registerMenuCommand(
"▶ Enter Blacklist Mode", blacklistMode, { id: "blacklistMode" });
}
}
function advancedOptions() {
if (kfExist("advanced_options")) return;
let advancedOptionsPage = createKfElement("div", "advanced_options");
advancedOptionsPage.innerHTML = `<style>#kf-advanced_options {
z-index: 99;
position: fixed;
width: 100%;
height: 100%;
padding: 10px;
background: rgba(0, 0, 0, .8);
backdrop-filter: blur(10px);
line-height: 1.5em;
}
input[type="text"] {
width: 100%;
}</style>
<h2>Kemono Fixer - Advanced Options</h2>
<form>
<fieldset>
<legend>Blacklist</legend>
<input type="button" value="Export to Clipboard">
<input type="button" value="Import from Clipboard">
</fieldset>
<fieldset>
<legend>Posts Page</legend>
<input type="checkbox" id="dont_allow_grid_auto_flow">
<label for="dont_allow_grid_auto_flow">Don't Allow grid-auto-flow<br>When "Allow Wide Post Cards" is enabled, latter post cards may move forward to fill in the gaps. Enable this option to leave the gaps unfilled if you wish the post cards to be strictly ordered.</label>
</fieldset>
<fieldset>
<legend>Post Details Page</legend>
<input type="checkbox" id="post_duplicate_files_keep_first">
<label for="post_duplicate_files_keep_first"><b>Keep First Duplicate File</b><br>When "Remove Duplicate Post Files" is enabled, by default the last one of the duplicates is kept. Enable this option if you wish to keep the first file and ignore latter duplicates. The first file is usually used as post thumbnail, and could be a copy of a latter one in sequence, hense the default to keep last one.</label><br><br>
<label for="copy_post_folder_name_template"><b>Post Folder Name Template:</b></label><br>
<input type="text" id="copy_post_folder_name_template"><br>
<label for="copy_post_folder_name_template"><b>Creator Folder Name Template:</b></label><br>
<input type="text" id="copy_creator_folder_name_template"><br>
<label>When you click on the title or the service name after it, a string ready to be used as Windows folder name is copied to the clipboard.<br>Variables: {title} {service} {creator} {published}</label><br><br>
<label for="copy_post_attachment_links_template"><b>Post Attachment Links Template:</b></label><br>
<input type="text" id="copy_post_attachment_links_template"><br>
<label for="copy_post_file_links_template"><b>Post File Links Template:</b></label><br>
<input type="text" id="copy_post_file_links_template"><br>
<label>When you click on "Downloads" or Files", all download links are copied to the clipboard, with the above template appended to the end.<br>Variables: {index} (file links only) | {filename} {title} {service} {creator} {published}</label><br>
</fieldset>
<input type="button" value="Save">
<input type="button" value="Discard">
</form>`;
let buttons = advancedOptionsPage.qAll(`input[type="button"]`);
buttons[0].addEventListener("click", exportBlacklist);
buttons[1].addEventListener("click", importBlacklist);
buttons[2].addEventListener("click", saveAdvancedOptions);
buttons[3].addEventListener("click", discardAdvancedOptions);
advancedOptionsPage.setKfAttr("href", window.location.href);
document.body.prepend(advancedOptionsPage);
readAdvancedOptions();
}
function readAdvancedOptions() {
Object.keys(ADVANCED_OPTIONS).forEach(key => {
let value = ADVANCED_OPTIONS[key];
switch (typeof value) {
case "boolean":
if (value) gOne(key).checked = true;
break;
case "string":
gOne(key).value = value;
break;
}
});
}
function saveAdvancedOptions(event) {
Object.keys(ADVANCED_OPTIONS).forEach(key => {
let value = ADVANCED_OPTIONS[key];
switch (typeof value) {
case "boolean":
ADVANCED_OPTIONS[key] = gOne(key).checked;
break;
case "string":
ADVANCED_OPTIONS[key] = gOne(key).value;
break;
}
});
GM_setValue("ADVANCED_OPTIONS", JSON.stringify(ADVANCED_OPTIONS));
gOne("kf-advanced_options").remove();
alert("Advanced Options saved. Reload page to take effect.")
}
function discardAdvancedOptions(event) {
gOne("kf-advanced_options").remove();
}
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); // wait for confirm() to close and return focus to document
}
}
}
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); // wait for confirm() to close and return focus to document
}
}
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}`);
}
}
// ==</Script Menu>==
// ==<Page Fixing Functions>==
function fixPage() {
switch (pageInfo.pageType) {
case "Posts":
case "Popular Posts":
case "Creator Posts":
fixPosts();
break;
case "Post Details":
fixPostDetails();
break;
case "Discord":
// fixDiscord();
break;
}
}
function fixPosts() {
if (kfExist("post_cards_container")) return;
let srcPostCardContainer = qOne(".card-list__items");
if (!srcPostCardContainer) return;
let srcPostCards = srcPostCardContainer.qAll(".post-card");
if (!srcPostCards) return;
// reset all page buttons active state
let currentPageButton = qOne(".pagination-button-disabled");
if (currentPageButton) currentPageButton.focus();
let newPostCardContainer = createKfElement("div", "post_cards_container");
let newPostCards = [];
for(const card of srcPostCards) {
let newCard = card.cloneKfNode();
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.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 newGridItem = createKfElement("div");
let thumbnail = card.qOne(".post-card__image");
if (!thumbnail) {
hiddenPosts.noThumbnail++;
newGridItem.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")}`;
blacklistButton.setKfAttr("creator-key", creatorKey);
if (BLACKLIST.includes(creatorKey)) {
hiddenPosts.creatorBlacklisted++;
newGridItem.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++;
newGridItem.addKfClass("already_shown");
} else {
shownPosts.push(postKey);
}
newGridItem.appendChild(card)
newPostCardContainer.appendChild(newGridItem);
}
sessionStorage.setItem("lastOffset", currentOffset);
sessionStorage.setItem("shownPosts", JSON.stringify(shownPosts));
// add messages about hidden posts
let srcMessage = qOne("#paginator-top small");
if (srcMessage) {
let messageContainer = createKfElement("div", "post_message_container");
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 creators.`;
} 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);
}
}
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;
let gridItem = img.closest("article").closest("div");
if (OPTIONS.allow_wide_post_cards && aspectRatio > 1.25) {
gridItem.addKfClass("wide_card");
if (aspectRatio > 1.5) {
img.style.objectFit = "contain";
}
} else {
if (aspectRatio > 0.8 || aspectRatio < 0.5625) {
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 creatorKey = event.target.getKfAttr("creator-key");
let message = `Do you want to blacklist ${creatorKey}?`;
let title = event.target.closest("article").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 creatorKey = event.target.getKfAttr("creator-key");
let message = `Do you want to unblacklist ${creatorKey}?`;
let title = event.target.closest("article").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.`);
}
function fixPostDetails() {
fixPostUser();
updatePostInfo();
fixPostBody();
}
function fixPostUser() {
if (gOne("kf-post_user")) return; // no need to check for href as it doesn't change
let srcPostUser = qOne(".post__user");
if (!srcPostUser) return;
let newPostUser = srcPostUser.cloneKfNode("post_user");
/* WIP */
srcPostUser.after(newPostUser);
srcPostUser.style.display = "none";
}
let postInfo = {};
function updatePostInfo() {
if (postInfo.path === window.location.pathname) return;
let postTitle = qAll(".post__title span");
if (!postTitle) return;
postInfo = {};
postInfo.title = postTitle[0].textContent;
postInfo.service = postTitle[1].textContent.replace(/^\(|\)$/g, '');
postInfo.creator = qOne("#kf-post_user .post__user-name").textContent;
postInfo.published = qOne(".post__published time").textContent;
postTitle[0].addEventListener("click", copyPostFolderName);
postTitle[0].style.cursor = "pointer";
postTitle[1].addEventListener("click", copyCreatorFolderName);
postTitle[1].style.cursor = "pointer";
postInfo.path = window.location.pathname;
}
function glow(target) {
target.style.transition = "filter 0.5s ease-out";
target.style.filter = "drop-shadow(0 0 5px #fff)";
setTimeout(() => {
target.style.filter = "";
}, 500);
}
function copyPostFolderName(event) {
let name = ADVANCED_OPTIONS.copy_post_folder_name_template
.replace("{title}", postInfo.title)
.replace("{service}", postInfo.service)
.replace("{creator}", postInfo.creator)
.replace("{published}", postInfo.published);
navigator.clipboard.writeText(getLegalWindowsFileName(name));
glow(event.target);
}
function copyCreatorFolderName(event) {
let name = ADVANCED_OPTIONS.copy_creator_folder_name_template
.replace("{title}", postInfo.title)
.replace("{service}", postInfo.service)
.replace("{creator}", postInfo.creator)
.replace("{published}", postInfo.published);
navigator.clipboard.writeText(getLegalWindowsFileName(name));
glow(event.target);
}
function getLegalWindowsFileName(input) {
const reservedHalfToFull = {
'<': '<', // U+FF1C
'>': '>', // U+FF1E
':': ':', // U+FF1A
'"': '"', // U+FF02
'/': '/', // U+FF0F
'\\': '\', // U+FF3C
'|': '|', // U+FF5C
'?': '?', // U+FF1F
'*': '*' // U+FF0A
};
// return input.replace(/[\x00-\x1F]/g, '') // already sanitized by browser
return input.replace(/[<>:"\/\\|?*]/g, (match) => reservedHalfToFull[match])
.replace(/\s(?=[<>:"/\|?*])|(?<=[<>:"/\|?*])\s/g, '')
.replace(/[.\s]+$/, '');
}
function fixPostBody() {
if (kfExist("post_body")) return;
let srcPostBody = qOne(".post__body");
if (!srcPostBody) return;
let newPostBody = createKfElement("div", "post_body");
for (const srcNode of srcPostBody.childNodes) {
let newNode;
switch (srcNode.tagName) {
case "H2":
switch (srcNode.textContent) {
case "Downloads":
newNode = srcNode.cloneKfNode();
newNode.style.cursor = "pointer";
newNode.addEventListener("click", copyAllDownloadLinks);
break;
case "Files":
newNode = srcNode.cloneKfNode("files");
newNode.style.cursor = "pointer";
newNode.addEventListener("click", copyAllFileLinks);
break;
case "Videos":
if (OPTIONS.remove_video_section_from_post_body) continue;
case "Content":
default:
newNode = srcNode.cloneKfNode();
}
break;
case "DIV":
if (srcNode.hasClass("ad-container")) continue;
if (srcNode.hasClass("post__content")) {
newNode = srcNode.cloneKfNode();
/* WIP */
} else if (srcNode.hasClass("post__files")) {
let thumbnails = srcNode.qAll(".fileThumb");
if (!thumbnails) {
let h2Files = newPostBody.qOne("#kf-files");
if(h2Files) h2Files.remove();
continue;
}
newNode = createKfElement("div", "post_files");
let fileLinks = [];
let fileNames = [];
let thumbLinks = [];
for (const thumbnail of thumbnails) {
let fileLink = thumbnail.href;
let fileName = thumbnail.download;
let thumbLink = thumbnail.qOne("img").src;
let index = fileLinks.indexOf(fileLink);
if (index === -1) {
fileLinks.push(fileLink);
fileNames.push(fileName);
thumbLinks.push(thumbLink);
} else if (!ADVANCED_OPTIONS.post_duplicate_files_keep_first) {
fileLinks[index] = null;
fileNames[index] = null;
thumbLinks[index] = null;
fileLinks.push(fileLink);
fileNames.push(fileName);
thumbLinks.push(thumbLink);
}
}
if (!ADVANCED_OPTIONS.post_duplicate_files_keep_first) {
fileLinks = fileLinks.filter(link => link);
fileNames = fileNames.filter(link => link);
thumbLinks = thumbLinks.filter(link => link);
}
if (OPTIONS.remove_duplicate_files_from_post) {
for (let i = 0; i < fileLinks.length; i++) {
const newGridItem = createKfElement("div");
const newThumbnail = createKfElement("a");
newThumbnail.href = fileLinks[i];
newThumbnail.download = fileNames[i];
newThumbnail.target = "_blank";
const newImg = createKfElement("img");
newImg.src = thumbLinks[i];
newImg.loading = "lazy";
newImg.addEventListener("load", postDetailsWideThumbnail);
newThumbnail.appendChild(newImg);
newGridItem.appendChild(newThumbnail);
newNode.appendChild(newGridItem);
}
} else {
for (const thumbnail of thumbnails) {
const newGridItem = createKfElement("div");
const newThumbnail = thumbnail.cloneKfNode();
newThumbnail.target = "_blank";
newThumbnail.qOne("img").addEventListener("load", postDetailsWideThumbnail);
newGridItem.appendChild(newThumbnail);
newNode.appendChild(newGridItem);
}
}
postInfo.fileLinks = [];
let padding = fileLinks.length > 99 ? 3 : 2;
for (let i = 0; i < fileLinks.length; i++) {
let append = ADVANCED_OPTIONS.copy_post_file_links_template
.replace("{index}", `${(i + 1).toString().padStart(padding, '0')}`)
.replace("{filename}", getLegalWindowsFileName(fileNames[i]))
.replace("{title}", encodeURIComponent(postInfo.title))
.replace("{service}", postInfo.service)
.replace("{creator}", encodeURIComponent(postInfo.creator))
.replace("{published}", postInfo.published);
postInfo.fileLinks[i] = fileLinks[i] + append;
}
}
break;
case "UL":
if (OPTIONS.remove_video_section_from_post_body && srcNode.qOne("video")) continue;
if (srcNode.hasClass("post__attachments")) {
newNode = createKfElement("ul", "post_attachments");
let attachments = srcNode.qAll(".post__attachment");
let attachmentLinks = [];
let fileNames = [];
let browseLinks = [];
for (const attachment of attachments) {
let a = attachment.qAll("a");
let attachmentLink = a[0].href;
let index = attachmentLinks.indexOf(attachmentLink);
if (index === -1) {
attachmentLinks.push(attachmentLink);
fileNames.push(a[0].download);
if (a[1]) browseLinks.push(a[1].getAttribute("href")); // .getAttribute for raw value (relative path)
}
}
let attachmentNodes = [];
for (let i = 0; i < attachmentLinks.length; i++) {
const newAttachment = createKfElement("li");
const aAttachment = createKfElement("a");
aAttachment.href = attachmentLinks[i];
aAttachment.download = fileNames[i];
aAttachment.textContent = `Download ${fileNames[i]}`;
newAttachment.appendChild(aAttachment);
if (browseLinks[i]) {
const aBrowse = createKfElement("a");
aBrowse.href = browseLinks[i];
aBrowse.target = "_blank";
aBrowse.textContent = "(Browse >>)";
newAttachment.appendChild(aBrowse);
}
newAttachment.setKfAttr("filename", fileNames[i]);
attachmentNodes.push(newAttachment);
}
attachmentNodes.sort((a, b) => a.getKfAttr("filename").localeCompare(b.getKfAttr("filename")));
postInfo.attachmentLinks = [];
for (let i = 0; i < attachmentNodes.length; i++) {
let append = ADVANCED_OPTIONS.copy_post_attachment_links_template
.replace("{filename}", getLegalWindowsFileName(fileNames[i]))
.replace("{title}", encodeURIComponent(postInfo.title))
.replace("{service}", postInfo.service)
.replace("{creator}", encodeURIComponent(postInfo.creator))
.replace("{published}", postInfo.published);
postInfo.attachmentLinks[i] = attachmentLinks[i] + append;
newNode.appendChild(attachmentNodes[i]);
}
} else {
newNode = srcNode.cloneKfNode();
}
break;
case "SCRIPT":
continue;
}
if (newNode) newPostBody.appendChild(newNode);
}
newPostBody.setKfAttr("href", window.location.href);
srcPostBody.after(newPostBody);
srcPostBody.style.display = "none";
}
function postDetailsWideThumbnail(event) {
let img = event.target;
let aspectRatio = img.naturalWidth / img.naturalHeight;
let gridItem = img.closest("div");
if (OPTIONS.allow_wide_post_cards && aspectRatio > 1.25) {
gridItem.addKfClass("wide_thumb");
}
img.style.opacity = 1;
}
function copyAllDownloadLinks(event) {
navigator.clipboard.writeText(postInfo.attachmentLinks.join("\n"));
glow(event.target);
}
function copyAllFileLinks(event) {
navigator.clipboard.writeText(postInfo.fileLinks.join("\n"));
glow(event.target);
}
// ==</Page Fixing Functions>==
})();