// ==UserScript==
// @name 2huai preview
// @namespace http://tampermonkey.net/
// @version 0.0.1
// @description Load catboxed images
// @author Anonymous
// @match https://boards.4chan.org/jp/thread/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=4chan.org
// @grant none
// @run-at document-start
// ==/UserScript==
const blurNSFW = true;
const consoleLogging = true;
const log = (...args) => consoleLogging && console.log(...args);
const logError = (...args) => consoleLogging && console.error(...args);
const NSFW_KEYWORDS = ["pussy", "nsfw", "nude", "naked", "nipple", "breasts"];
const imageContainsKeywords = async (url, keywords) => {
try {
const response = await fetch(url);
if (!response.ok) {
return false;
}
const buffer = await response.arrayBuffer();
const dataStr = new TextDecoder("utf-8").decode(buffer);
return keywords.some((v) => dataStr.includes(v));
} catch (err) {
console.error("Error fetching or processing image:", err);
return false;
}
};
const preparePost = (link, i) => {
const img = document.createElement("img");
img.src = link;
const imgContainer = document.createElement("div");
imgContainer.appendChild(img);
imgContainer.classList.add("imgContainer");
imgContainer.classList.add(`i${i}`);
imgContainer.tabIndex = "-1";
return {imgContainer};
};
const prepareLinkElement = (linkText, i) => {
const s = document.createElement("span");
s.innerText = linkText;
s.classList.add("fancyLink");
s.classList.add(`i${i}`);
return s;
};
const prepareContainer = () => {
const container = document.createElement("div");
container.classList.add("aiPreviewContainer");
return container;
};
const processLink = (link, postElement, _linkElement, previewContainer, i) => {
if (!link) {
logError("link null", postElement);
return;
}
const {imgContainer} = preparePost(link, i);
previewContainer.appendChild(imgContainer);
};
const processPosts = (postv) => {
log("Processing", postv.length, "posts:", postv);
for (const post of postv) {
if (!post.innerText.includes(".catbox.moe")) {
continue;
}
const blockquote = post.querySelector(".postMessage");
const matches = Array.from(
blockquote.innerText.matchAll(/(?:files|litter)\.catbox\.moe\/.+?\.(?:jpg|jpeg|png|webp|avif)/g)
).flat();
if (!matches.length) {
continue;
}
const container = prepareContainer();
let i = 0;
for (let link of matches) {
if (!link.startsWith("http")) {
link = "https://" + link;
}
const linkElement = prepareLinkElement(link, i);
blockquote.innerHTML = blockquote.innerHTML.replace(link, linkElement.outerHTML);
processLink(link, post, linkElement, container, i);
i++;
}
blockquote.appendChild(container);
if (blurNSFW) {
const spans = Array.from(blockquote.querySelectorAll(".fancyLink"));
for (const span of spans) {
imageContainsKeywords(span.innerText, NSFW_KEYWORDS).then((isNSFW) => {
if (isNSFW) {
const idx = spans.indexOf(span);
const imgContainer = container.querySelector(`.imgContainer.i${idx}`);
if (imgContainer) {
imgContainer.classList.add("nsfw");
}
}
});
}
}
}
};
{
const style = document.createElement("style");
style.innerHTML = `
${Array.from({length: 20})
.map(
(_, i) => `
.postMessage:has(.fancyLink.i${i}:hover) .imgContainer.i${i}::before {
opacity: 0.3;
}
.postMessage:has(.imgContainer.i${i}:hover) .fancyLink.i${i} {
background-color: hsl(${(i * 137.5) % 360}, 80%, 70%, 0.5);
}
.fancyLink.i${i} { text-decoration-color: hsl(${(i * 137.5) % 360}, 80%, 70%); }
`
)
.join("\n")}
.fancyLink {
text-decoration: underline;
}
.aiPreviewContainer {
display: flex;
margin-top: 0.5rem;
gap: 1rem;
.imgContainer {
height: 100px;
position: relative;
}
.imgContainer img {
width: auto;
height: 100%;
object-fit: contain;
border-bottom: 2px solid var(--ai-highlight-color, transparent);
}
.imgContainer:focus {
height: 400px;
}
.imgContainer::before {
content: "";
position: absolute;
inset: 0;
background-color: var(--ai-highlight-color, transparent);
z-index: 1;
pointer-events: none;
opacity: 0;
}
${Array.from({length: 20})
.map(
(_, i) => `
.imgContainer.i${i} { --ai-highlight-color: hsl(${(i * 137.5) % 360}, 80%, 70%); }
.postMessage:has(.fancyLink.i${i}:hover) .imgContainer.i${i}::before {
opacity: 0.3;
}
`
)
.join("\n")}
.imgContainer.nsfw {
overflow: hidden;
}
.imgContainer.nsfw:not(:hover) img {
filter: blur(8px);
}
.imgContainer.nsfw:hover img {
filter: none;
}
}
`;
const run = () => {
"use strict";
globalThis.__2huai_loaded = true;
const thread = document.querySelector(".thread");
processPosts([...document.querySelectorAll(".postContainer")]);
const observer = new MutationObserver((mutations) => {
const newPosts = mutations
.map((v) => [...v.addedNodes].filter((v) => v.classList?.contains("postContainer")))
.flat();
if (newPosts.length) {
processPosts(newPosts);
}
});
observer.observe(thread, {childList: true, subtree: true});
};
const init = () => {
document.head.appendChild(style);
run();
};
// this waits for either DOMContentLoaded or document.readyState === "complete" and runs the init function.
Promise.race([
new Promise((resolve, _) =>
document.addEventListener("DOMContentLoaded", () => resolve("event"), {once: true})
),
new Promise((resolve, _) => {
const h = setInterval(() => {
if (document.readyState === "complete") {
clearInterval(h);
resolve("poll");
}
}, 200);
}),
]).then((reason) =>
globalThis.__2huai_loaded
? log("already init image preview")
: (init(), log("init image preview because of", reason))
);
}