// ==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.31
// @author Kaban
// @description Allow you to blacklist creators on Kemono, and more.
// ==/UserScript==
(function() {
"use strict";
// ==<TO-DO>==
// 1. local favorates (highlight_group)
// 2. collect user name and display below header, allow adding custom tags
// ==</TO-DO>==
// ==<Options>==
const 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,
"auto_next_page_if_none_shown": false,
"remove_video_section_from_post_body": false,
"remove_duplicate_files_from_post": true,
};
readOptions(OPTIONS, "OPTIONS");
const ADVANCED_OPTIONS = {
"auto_next_page_delay": 5000,
"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 = JSON.parse(GM_getValue("BLACKLIST", `{}`));
const RENAME_CREATOR = JSON.parse(GM_getValue("RENAME_CREATOR", `{}`));
function readOptions(defaults, storedKey) {
const 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) {
const results = document.querySelectorAll(selector);
return results.length === 0 ? null : results;
}
Element.prototype.qAll = function(selector) {
const results = this.querySelectorAll(selector);
return results.length === 0 ? null : results;
};
function createKfElement(type, id = null, content = null) {
const element = document.createElement(type);
if (id) {
element.id = `kf-${id}`;
const oldElement = gOne(element.id);
if (oldElement) oldElement.remove();
}
if (content) element.textContent = content;
return element;
}
function kfExist(id) {
const 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) {
const 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) {
const 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>==
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 });
}
const observer = new MutationObserver(onMutation);
observer.observe(document.body, { childList: true, subtree: true });
// ==</Mutation Observer>==
// ==<Shared Info>==
let pageInfo = {};
function updatePageInfo() {
if (pageInfo.href === window.location.href) return;
pageInfo = {};
const pathname = window.location.pathname;
if (pathname === "/posts") {
pageInfo.pageType = "Posts";
} else if (pathname === "/posts/popular") {
pageInfo.pageType = "Popular Posts";
} else {
const segments = pathname.split('/').filter(segment => segment);
if (segments[1] === "user") {
pageInfo.creatorKey = `${segments[0]}-${segments[2]}`;
if (segments.length === 3) {
// {service}/user/{user_id}
pageInfo.pageType = "Creator Posts";
} else if (segments[3] === "post") {
if (segments.length === 5 || segments.length === 7 && segments[5] === "revision" ) {
// {service}/user/{user_id}/post/{post_id}
// {service}/user/{user_id}/post/{post_id}/revision/{revision_id}
pageInfo.pageType = "Post Details";
}
}
} else if (segments.length === 4 && segments[0] === "discord") {
// discord/server/{server_id}/{room_id}
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: center;
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.pagination-button-current:hover {
background: var(--anchor-internal-color2-primary) !important;
color:var(--anchor-internal-color1-secondary) !important;
}
menu > a:hover {
background: transparent !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_body {
border-left: solid hsl(0,0%,50%) 0.125em;
border-right: solid hsl(0,0%,50%) 0.125em;
padding: 0.5em;
container-type: inline-size;
container-name: post_files_parent;
}
#kf-post_attachments {
list-style: none;
padding: .5em;
margin: 0;
}
#kf-post_attachments li {
padding: .25em 0;
}
#kf-post_files {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 5px;
justify-content: center;
}
@container post_files_parent (min-width: 725px) {
#kf-post_files {
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
}
}
#kf-post_files div {
background-image: repeating-conic-gradient(var(--color1-secondary) 0% 25%, transparent 0% 50%);
background-size: 20px 20px;
}
#kf-post_files a {
display: block;
width: 100%;
height: 100%;
padding: 0;
border: none;
background: unset;
}
#kf-post_files img {
display: block;
width: 100%;
height: 100%;
max-height: 800px;
object-fit: contain;
opacity: 0;
transition: opacity 0.5s ease-out, filter 0.5s ease-out;
}
.kf-wide_thumb {
grid-column: span 2;
}
#kf-post_files .kf-embed_view_container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.kf-embed_view {
background: var(--color1-secondary);
padding: .5em;
}
`;
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;
const style = createKfElement("style", id, css);
style.setKfAttr("href", window.location.href);
document.head.append(style);
}
function removeStyle(id = "style") {
const style = gOne(`kf-${id}`);
if (style) style.remove();
}
// ==</Style>==
// ==<Script Menu>==
function updateScriptMenu() {
const scriptMenuFlag = createKfElement("div", "script_menu_flag");
switch (pageInfo.pageType) {
case "Posts":
case "Popular 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 "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 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;
const advancedOptionsPage = createKfElement("div", "advanced_options");
advancedOptionsPage.innerHTML = `<style>#kf-advanced_options {
z-index: 99;
position: fixed;
width: 100%;
height: 100%;
overflow-y: auto;
overscroll-behavior-y: contain;
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>`;
const 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 => {
const 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 => {
const 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() {
if (!confirm("Do you want to export Blacklist to clipboard?")) return;
setTimeout(exportToClipboard, 500); // wait for confirm() to close and return focus
}
async function exportToClipboard() {
const blacklistLength = Object.keys(BLACKLIST).length;
if (blacklistLength === 0) {
alert("Blacklist is empty, nothing to export.");
return;
}
try {
const blacklistJSON = JSON.stringify(BLACKLIST, null, 2);
await navigator.clipboard.writeText(blacklistJSON);
if (blacklistLength === 1) {
alert("Exported 1 Blacklist entry to clipboard as JSON.");
} else {
alert(`Exported ${blacklistLength} Blacklist 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?";
const blacklistLength = Object.keys(BLACKLIST).length;
if (blacklistLength === 1) {
message += "\n1 existing entry will be overwritten!";
} else if (blacklistLength > 1) {
message += `\n${blacklistLength} existing entries will be overwritten!`;
}
if (!confirm(message)) return;
setTimeout(importFromClipboard, 500); // wait for confirm() to close and return focus
}
async function importFromClipboard() {
try {
const rawClipboard = await navigator.clipboard.readText();
if (!rawClipboard) {
alert("Clipboard read empty.\n(Permission denied?)");
return;
}
let jsonObject;
if (rawClipboard.startsWith('{')) {
jsonObject = JSON.parse(rawClipboard);
} else {
// backward compatibility with data from old version script
const lines = rawClipboard.split('\n');
const regex = /"([^"]+)"[,\s]*\/\/\s*(.*?)\s*$/;
jsonObject = {};
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(regex);
if (match) jsonObject[match[1]] = match[2];
}
}
const length = Object.keys(jsonObject).length;
if (length === 0) {
const message = "Found no valid entry in clipboard.\nEnter \"clear\" to clear the blacklist.";
if (prompt(message, "")?.toLowerCase() === "clear") {
BLACKLIST = {};
GM_setValue("BLACKLIST", "{}");
alert("Blacklist cleared.");
} else {
alert("Import aborted.");
}
return;
}
BLACKLIST = jsonObject;
GM_setValue("BLACKLIST", JSON.stringify(jsonObject));
if (length === 1) {
alert("Imported 1 Blacklist entry from clipboard.");
} else {
alert(`Imported ${length} Blacklist entries 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;
const srcPostCardContainer = qOne(".card-list__items");
if (!srcPostCardContainer) return;
const srcPostCards = srcPostCardContainer.qAll(".post-card");
if (!srcPostCards) return;
// reset all page buttons active state
const currentPageButton = qOne(".pagination-button-current");
if (currentPageButton) currentPageButton.focus();
const newPostCardContainer = createKfElement("div", "post_cards_container");
const newPostCards = [];
for(const card of srcPostCards) {
const newCard = card.cloneKfNode();
if (OPTIONS.sort_posts_by_publish_date) {
const 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"));
}
const hiddenPosts = {
noThumbnail: 0,
creatorBlacklisted: 0,
blacklistedCreators: new Set(),
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) {
const newGridItem = createKfElement("div");
const thumbnail = card.qOne(".post-card__image");
if (!thumbnail) {
hiddenPosts.noThumbnail++;
newGridItem.addKfClass("no_thumbnail");
} else {
thumbnail.addEventListener("load", fixThumbnailAspectRatio);
}
const blacklistControl = createKfElement("div", "blacklist_control")
blacklistControl.addEventListener("click", preventDefault);
const blacklistButton = document.createElement("input");
blacklistButton.type = "button";
blacklistControl.appendChild(blacklistButton);
card.qOne("a").prepend(blacklistControl);
const creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
blacklistButton.setKfAttr("creator-key", creatorKey);
if (BLACKLIST.hasOwnProperty(creatorKey)) {
hiddenPosts.creatorBlacklisted++;
newGridItem.addKfClass("blacklisted");
hiddenPosts.blacklistedCreators.add(creatorKey);
blacklistButton.value = "Remove from Blacklist";
blacklistButton.addEventListener("click", unblacklistCreator);
} else {
blacklistButton.value = "Add to Blacklist";
blacklistButton.addEventListener("click", blacklistCreator);
}
const 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
const srcMessage = qOne("#paginator-top small");
if (srcMessage) {
const messageContainer = createKfElement("div", "post_message_container");
srcMessage.after(messageContainer);
if (hiddenPosts.noThumbnail > 0) {
let message;
if (hiddenPosts.noThumbnail === 1) {
message = "Hid 1 post with no thumbnail.";
} else {
message = `Hid ${hiddenPosts.noThumbnail} posts with no thumbnail.`;
}
const 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 1 post from a blacklisted creator.";
} else {
message = `Hid ${hiddenPosts.creatorBlacklisted} posts from `;
if (hiddenPosts.blacklistedCreators.size === 1) {
message += "a blacklisted creator.";
} else {
message += `${hiddenPosts.blacklistedCreators.size} blacklisted creators.`;
}
}
const 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 1 post already shown on previous pages.";
} else {
message = `Hid ${hiddenPosts.alreadyShown} posts already shown on previous pages.`;
}
const messageAlreadyShown = createKfElement("small", "already_shown_msg", message);
messageAlreadyShown.addEventListener("click", showPostsFromPreviousPages);
messageContainer.appendChild(messageAlreadyShown);
}
// auto click on next page if all posts are hidden
if (OPTIONS.auto_next_page_if_none_shown) {
// find next page button first
let nextPageButton = qOne("menu");
if (nextPageButton) {
nextPageButton = nextPageButton.lastChild;
if (nextPageButton.textContent === ">>") {
nextPageButton = nextPageButton.previousSibling;
}
if (nextPageButton.textContent !== ">" ||
nextPageButton.hasClass("pagination-button-disabled")) {
nextPageButton = null;
}
}
if (nextPageButton) {
let hidCount = 0;
const cards = newPostCardContainer.children;
for (const card of cards) {
if (OPTIONS.hide_posts_with_no_thumbnail && card.hasClass("kf-no_thumbnail")) {
hidCount++;
continue;
}
if (OPTIONS.hide_posts_from_blacklisted_creators && card.hasClass("kf-blacklisted")) {
hidCount++;
continue;
}
if (OPTIONS.hide_posts_shown_on_previous_pages && card.hasClass("kf-already_shown")) {
hidCount++;
}
}
if (hidCount === cards.length) {
let message = "Auto loading next page...";
const messageAlreadyShown = createKfElement("small", null, message);
messageContainer.appendChild(messageAlreadyShown);
setTimeout(() => {
nextPageButton.click();
}, ADVANCED_OPTIONS.auto_next_page_delay);
}
}
}
}
newPostCardContainer.setKfAttr("href", window.location.href);
srcPostCardContainer.after(newPostCardContainer);
srcPostCardContainer.style.display = "none";
}
function fixThumbnailAspectRatio(event) {
const img = event.target;
const aspectRatio = img.naturalWidth / img.naturalHeight;
const 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();
const creatorKey = event.target.getKfAttr("creator-key");
let message = `Do you want to blacklist ${creatorKey}?`;
const title = event.target.closest("article").qOne("header").childNodes[0];
if (title) message += `\n(Creator of post ${title.textContent})`;
const comment = "(you can add a comment here)";
const userInput = prompt(message, comment);
if (userInput === null) return;
addToBlacklist(creatorKey, userInput === comment ? "" : userInput.trim());
}
function addToBlacklist(creatorKey, comment) {
BLACKLIST[creatorKey] = comment;
GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST));
alert(`${creatorKey} added to blacklist, reload to take effect.`);
}
function unblacklistCreator(event) {
event.preventDefault();
const creatorKey = event.target.getKfAttr("creator-key");
let message = `Do you want to unblacklist ${creatorKey}?`;
const 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[creatorKey];
GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST));
alert(`${creatorKey} removed from Blacklist, reload to take effect.`);
}
function fixPostDetails() {
updatePostInfo();
fixPostBody();
}
let postInfo = {};
function updatePostInfo() {
if (postInfo.path === window.location.pathname) return;
const postTitle = qAll(".post__title span");
if (!postTitle) return;
postInfo = {};
postInfo.title = postTitle[0].textContent;
postInfo.service = postTitle[1].textContent.replace(/^\(|\)$/g, '');
postInfo.creator = qOne(".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) {
const 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) {
const 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;
const srcPostBody = qOne(".post__body");
if (!srcPostBody) return;
const 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("post__content")) {
/* WIP */
newNode = srcNode.cloneKfNode();
/* WIP */
} else if (srcNode.hasClass("post__files")) {
const postFiles = srcNode.children;
if (postFiles.length === 0) {
const h2Files = newPostBody.qOne("#kf-files");
if (h2Files) h2Files.remove();
continue;
}
newNode = createKfElement("div", "post_files");
let fileLinks = [];
let fileNames = [];
let thumbLinks = [];
const thumbnails = srcNode.qAll(".fileThumb");
for (const thumbnail of thumbnails) {
const fileLink = thumbnail.href;
const fileName = thumbnail.download;
const thumbLink = thumbnail.qOne("img").src;
const 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 = [];
const padding = fileLinks.length > 99 ? 3 : 2;
for (let i = 0; i < fileLinks.length; i++) {
const append = ADVANCED_OPTIONS.copy_post_file_links_template
.replace("{index}", `${(i + 1).toString().padStart(padding, '0')}`)
.replace("{filename}", getLegalWindowsURIComponent(fileNames[i], true))
.replace("{title}", getLegalWindowsURIComponent(postInfo.title))
.replace("{service}", postInfo.service)
.replace("{creator}", getLegalWindowsURIComponent(postInfo.creator))
.replace("{published}", postInfo.published);
postInfo.fileLinks[i] = fileLinks[i] + append;
}
const embedViews = srcNode.qAll(".embed-view");
if (embedViews) {
for (const embedView of embedViews) {
const newGridItem = createKfElement("div");
const newThumbnail = createKfElement("a");
newThumbnail.href = embedView.closest("a").href;
newThumbnail.target = "_blank";
newThumbnail.addKfClass("embed_view_container");
const newEmbedView = embedView.cloneKfNode();
newEmbedView.addKfClass("embed_view");
newThumbnail.appendChild(newEmbedView);
newGridItem.appendChild(newThumbnail);
newNode.appendChild(newGridItem);
}
}
}
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");
const attachments = srcNode.qAll(".post__attachment");
const attachmentLinks = [];
const fileNames = [];
const browseLinks = [];
for (const attachment of attachments) {
const a = attachment.qAll("a");
const attachmentLink = a[0].href;
const 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)
}
}
const 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++) {
const append = ADVANCED_OPTIONS.copy_post_attachment_links_template
.replace("{filename}", getLegalWindowsURIComponent(fileNames[i], true))
.replace("{title}", getLegalWindowsURIComponent(postInfo.title))
.replace("{service}", postInfo.service)
.replace("{creator}", getLegalWindowsURIComponent(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) {
const img = event.target;
const aspectRatio = img.naturalWidth / img.naturalHeight;
const 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);
}
function getLegalWindowsURIComponent(input, check = false) {
let decodedInput = input;
if (check) {
try {
const temp = decodeURIComponent(input);
if (encodeURIComponent(temp) === input) decodedInput = temp;
} catch (error) {
// decoding failed, decodedInput still holds input
}
}
return encodeURIComponent(getLegalWindowsFileName(decodedInput));
}
// ==</Page Fixing Functions>==
})();