// ==UserScript==
// @name Rule34 Favorites Search Gallery
// @namespace bruh3396
// @version 1.0
// @description Search, View, and Play Favorites All on One Page
// @author bruh3396
// @match https://rule34.xxx/index.php?page=favorites&s=view&id=*
// @match https://rule34.xxx/index.php?page=post&s=list*
// ==/UserScript==
// utilities.js
const IDS_TO_REMOVE_ON_RELOAD_KEY = "recentlyRemovedIds";
const TAG_BLACKLIST = getTagBlacklist();
const CURSOR_POSITION = {
X: 0,
Y: 0
};
const PREFERENCES = "preferences";
let onPostPageFlag;
let usingFirefoxFlag;
/**
* @param {String} key
* @param {any} value
*/
function setCookie(key, value) {
let cookieString = `${key}=${value || ""}`;
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
cookieString += `; expires=${expirationDate.toUTCString()}`;
cookieString += "; path=/";
document.cookie = cookieString;
}
/**
* @param {String} key
* @param {any} defaultValue
* @returns {String | null}
*/
function getCookie(key, defaultValue) {
const nameEquation = `${key}=`;
const cookies = document.cookie.split(";").map(cookie => cookie.trimStart());
for (const cookie of cookies) {
if (cookie.startsWith(nameEquation)) {
return cookie.substring(nameEquation.length, cookie.length);
}
}
return defaultValue === undefined ? null : defaultValue;
}
/**
* @param {String} key
* @param {any} value
*/
function setPreference(key, value) {
const preferences = JSON.parse(localStorage.getItem(PREFERENCES) || "{}");
preferences[key] = value;
localStorage.setItem(PREFERENCES, JSON.stringify(preferences));
}
/**
* @param {String} key
* @param {any} defaultValue
* @returns {String | null}
*/
function getPreference(key, defaultValue) {
const preferences = JSON.parse(localStorage.getItem(PREFERENCES) || "{}");
const preference = preferences[key];
if (preference === undefined) {
return defaultValue === undefined ? null : defaultValue;
}
return preference;
}
/**
* @returns {String | null}
*/
function getUserId() {
return getCookie("user_id");
}
/**
* @returns {String | null}
*/
function getFavoritesPageId() {
const match = (/(?:&|\?)id=(\d+)/).exec(window.location.href);
return match ? match[1] : null;
}
/**
* @returns {Boolean}
*/
function userIsOnTheirOwnFavoritesPage() {
return getUserId() === getFavoritesPageId();
}
/**
* @param {String} url
* @param {Function} callback
* @param {Number} delayIncrement
* @param {Number} delay
*/
function requestPageInformation(url, callback, delay = 0) {
const httpRequest = new XMLHttpRequest();
const delayIncrement = 500;
httpRequest.open("GET", url, true);
httpRequest.onreadystatechange = () => {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 503) {
requestPageInformation(url, callback, delay + delayIncrement);
}
return callback(httpRequest.responseText);
}
return null;
};
setTimeout(() => {
httpRequest.send();
}, delay);
}
/**
* @param {Number} value
* @param {Number} min
* @param {Number} max
* @returns {Number}
*/
function clamp(value, min, max) {
if (value <= min) {
return min;
} else if (value >= max) {
return max;
}
return value;
}
/**
* @param {Number} milliseconds
* @returns
*/
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
/**
* @param {Boolean} removeButtonsAreVisible
*/
function hideCaptionsWhenRemoveButtonsAreVisible(removeButtonsAreVisible) {
for (const caption of document.getElementsByClassName("caption")) {
if (removeButtonsAreVisible) {
caption.classList.add("remove");
} else {
caption.classList.remove("remove");
}
}
}
function updateVisibilityOfAllRemoveButtons() {
const removeButtonCheckbox = document.getElementById("show-remove-buttons");
if (removeButtonCheckbox === null) {
return;
}
const removeButtonsAreVisible = removeButtonCheckbox.checked;
const visibility = removeButtonsAreVisible ? "visible" : "hidden";
injectStyleHTML(`
.remove-button {
visibility: ${visibility} !important;
}
`, "remove-button-visibility");
hideCaptionsWhenRemoveButtonsAreVisible(removeButtonsAreVisible);
}
/**
* @param {HTMLElement} thumb
* @returns {String | null}
*/
function getRemoveLinkFromThumb(thumb) {
return thumb.querySelector(".remove-button");
}
/**
* @param {HTMLImageElement} image
*/
function removeTitleFromImage(image) {
if (image.hasAttribute("title")) {
image.setAttribute("tags", image.title);
image.removeAttribute("title");
}
}
/**
* @param {HTMLImageElement} image
* @returns {HTMLElement}
*/
function getThumbFromImage(image) {
return image.parentNode.parentNode;
}
/**
* @param {HTMLElement} thumb
* @returns {HTMLImageElement}
*/
function getImageFromThumb(thumb) {
return thumb.children[0].children[0];
}
/**
* @returns {HTMLCollectionOf.<Element>}
*/
function getAllThumbs() {
const className = onPostPage() ? "thumb" : "thumb-node";
return document.getElementsByClassName(className);
}
/**
* @returns {HTMLElement[]}
*/
function getAllVisibleThumbs() {
return Array.from(getAllThumbs())
.filter(thumbNodeElement => thumbNodeElement.style.display !== "none");
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
function getOriginalImageURLFromThumb(thumb) {
return getOriginalImageURL(getImageFromThumb(thumb).src);
}
/**
* @param {String} thumbURL
* @returns {String}
*/
function getOriginalImageURL(thumbURL) {
return thumbURL
.replace("thumbnails", "/images")
.replace("thumbnail_", "")
.replace("us.rule34", "rule34");
}
/**
* @param {String} originalImageURL
* @returns {String}
*/
function getThumbURL(originalImageURL) {
return originalImageURL
.replace(/\/images\/\/(\d+)\//, "thumbnails/$1/thumbnail_")
.replace(/(?:gif|jpeg|png)/, "jpg")
.replace("us.rule34", "rule34");
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
function getTagsFromThumb(thumb) {
const image = getImageFromThumb(thumb);
return image.hasAttribute("tags") ? image.getAttribute("tags") : image.title;
}
/**
* @param {String} tag
* @param {String} tags
* @returns
*/
function includesTag(tag, tags) {
return tags.includes(` ${tag} `) || tags.endsWith(` ${tag}`) || tags.startsWith(`${tag} `);
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
function isVideo(thumb) {
if (thumb.classList.contains("video")) {
return true;
}
const tags = getTagsFromThumb(thumb);
return includesTag("video", tags) || includesTag("mp4", tags);
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
function isGif(thumb) {
if (isVideo(thumb)) {
return false;
}
const tags = getTagsFromThumb(thumb);
return includesTag("gif", tags) || includesTag("animated", tags) || includesTag("animated_png", tags) || getImageFromThumb(thumb).hasAttribute("gif");
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
function isImage(thumb) {
return !isVideo(thumb) && !isGif(thumb);
}
/**
* @param {String} svgContent
* @param {String} id
* @param {Number} newWidth
* @param {Number} newHeight
* @param {String} position
* @param {Number} duration
*/
function showOverlayingIcon(svgContent, id, newWidth, newHeight, position = "center", duration = 500) {
const skip = true;
if (skip) {
return;
}
const svgDocument = new DOMParser().parseFromString(svgContent, "image/svg+xml");
const svgElement = svgDocument.documentElement;
const zoomLevel = getZoomLevel();
svgElement.setAttribute("width", Math.round(newWidth / zoomLevel));
svgElement.setAttribute("height", Math.round(newHeight / zoomLevel));
if (document.getElementById(id) !== null) {
return;
}
const svgOverlay = document.createElement("div");
svgOverlay.id = id;
switch (position) {
case "center":
svgOverlay.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999;";
break;
case "bottom-left":
svgOverlay.style.cssText = "position: fixed; bottom: 0; left: 0; z-index: 9999;";
break;
case "bottom-right":
svgOverlay.style.cssText = "position: fixed; bottom: 0; right: 0; z-index: 9999;";
break;
case "top-left":
svgOverlay.style.cssText = "position: fixed; top: 0; left: 0; z-index: 9999;";
break;
case "top-right":
svgOverlay.style.cssText = "position: fixed; top: 0; right: 0; z-index: 9999;";
break;
default:
svgOverlay.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999;";
}
svgOverlay.style.cssText += " pointer-events:none;";
svgOverlay.innerHTML = new XMLSerializer().serializeToString(svgElement);
// document.body.appendChild(svgOverlay);
setTimeout(() => {
svgOverlay.remove();
}, duration);
}
/**
* @param {any[]} array
*/
function shuffleArray(array) {
let maxIndex = array.length;
let randomIndex;
while (maxIndex > 0) {
randomIndex = Math.floor(Math.random() * maxIndex);
maxIndex -= 1;
[
array[maxIndex],
array[randomIndex]
] = [
array[randomIndex],
array[maxIndex]
];
}
}
/**
* @returns {Number}
*/
function getZoomLevel() {
const zoomLevel = window.outerWidth / window.innerWidth;
return zoomLevel;
}
/**
* @param {String} tags
* @returns {String}
*/
function negateTags(tags) {
return tags.replace(/(\S+)/g, "-$1");
}
/**
* @param {HTMLInputElement} input
* @returns {Boolean}
*/
function awesompleteIsHidden(input) {
if (input.parentElement.className === "awesomplete") {
return input.parentElement.children[1].hasAttribute("hidden");
}
return false;
}
function awesompleteIsUnselected(input) {
const awesomplete = input.parentElement;
if (awesomplete === null) {
return true;
}
const searchSuggestions = Array.from(awesomplete.querySelectorAll("li"));
if (searchSuggestions.length === 0) {
return true;
}
const somethingIsSelected = searchSuggestions.map(li => li.getAttribute("aria-selected"))
.some(element => element === true || element === "true");
return !somethingIsSelected;
}
/**
* @param {HTMLInputElement} input
* @returns
*/
function clearAwesompleteSelection(input) {
const awesomplete = input.parentElement;
if (awesomplete === null) {
return;
}
const searchSuggestions = Array.from(awesomplete.querySelectorAll("li"));
if (searchSuggestions.length === 0) {
return;
}
for (const li of searchSuggestions) {
li.setAttribute("aria-selected", false);
}
}
function trackCursorPosition() {
document.addEventListener("mousemove", (event) => {
CURSOR_POSITION.X = event.clientX;
CURSOR_POSITION.Y = event.clientY;
});
}
/**
* @param {String} optionId
* @param {String} optionText
* @param {String} optionTitle
* @param {Boolean} optionIsChecked
* @param {Function} onOptionChanged
* @param {Boolean} optionIsVisible
* @returns {HTMLElement | null}
*/
function addOptionToFavoritesPage(optionId, optionText, optionTitle, optionIsChecked, onOptionChanged, optionIsVisible) {
const favoritesPageOptions = document.getElementById("favorite-options");
const checkboxId = `${optionId}Checkbox`;
if (favoritesPageOptions === null) {
return null;
}
if (optionIsVisible === undefined || optionIsVisible) {
optionIsVisible = "block";
} else {
optionIsVisible = "none";
}
favoritesPageOptions.insertAdjacentHTML("beforeend", `
<div id="${optionId}" style="display: ${optionIsVisible}">
<label class="checkbox" title="${optionTitle}">
<input id="${checkboxId}" type="checkbox" > ${optionText}</label>
</div>
`);
const newOptionsCheckbox = document.getElementById(checkboxId);
newOptionsCheckbox.checked = optionIsChecked;
newOptionsCheckbox.onchange = onOptionChanged;
return document.getElementById(optionId);
}
/**
* @returns {Boolean}
*/
function onPostPage() {
if (onPostPageFlag === undefined) {
onPostPageFlag = location.href.includes("page=post");
}
return onPostPageFlag;
}
/**
* @returns {String[]}
*/
function getIdsToRemoveOnReload() {
return JSON.parse(localStorage.getItem(IDS_TO_REMOVE_ON_RELOAD_KEY)) || [];
}
/**
* @param {String} postId
*/
function setIdToBeRemovedOnReload(postId) {
const idsToRemoveOnReload = getIdsToRemoveOnReload();
idsToRemoveOnReload.push(postId);
localStorage.setItem(IDS_TO_REMOVE_ON_RELOAD_KEY, JSON.stringify(idsToRemoveOnReload));
}
function clearRecentlyRemovedIds() {
localStorage.removeItem(IDS_TO_REMOVE_ON_RELOAD_KEY);
}
/**
* @param {String} html
* @param {String} id
*/
function injectStyleHTML(html, id) {
const style = document.createElement("style");
style.textContent = html.replace("<style>", "").replace("</style>", "");
if (id !== undefined) {
const oldStyle = document.getElementById(id);
if (oldStyle !== null) {
oldStyle.remove();
}
style.id = id;
}
document.head.appendChild(style);
}
/**
* @param {HTMLElement} content
*/
function populateMetadata(content) {
const scripts = Array.from(content.getElementsByTagName("script"));
scripts.shift();
scripts.forEach((script) => {
// eval(script.innerHTML);
});
}
/**
* @param {HTMLElement} image
*/
function addMetaDataToThumb(image) {
const thumb = getThumbFromImage(image);
const metadata = posts[thumb.id];
thumb.setAttribute("rating", metadata.rating);
thumb.setAttribute("score", metadata.score);
}
/**
* @param {HTMLElement} thumb
* @param {String} appropriateRating
* @returns {Boolean}
*/
function hasAppropriateRating(thumb, appropriateRating) {
const ratings = {
"Safe": 0,
"Questionable": 1,
"Explicit": 2
};
return ratings[thumb.getAttribute("rating")] <= ratings[appropriateRating];
}
/**
* @param {String} appropriateRating
*/
function removeInappropriatelyRatedContent(appropriateRating) {
Array.from(getAllThumbs()).forEach((thumb) => {
if (!hasAppropriateRating(thumb, appropriateRating)) {
// setThumbDisplay(thumb, false);
}
});
}
function getTagDistribution() {
const images = Array.from(getAllThumbs()).map(thumb => getImageFromThumb(thumb));
const tagOccurrences = {};
images.forEach((image) => {
const tags = image.getAttribute("tags").replace(/ \d+$/, "").split(" ");
tags.forEach((tag) => {
const occurrences = tagOccurrences[tag];
tagOccurrences[tag] = occurrences === undefined ? 1 : occurrences + 1;
});
});
const sortedTagOccurrences = sortObjectByValues(tagOccurrences);
let result = "";
let i = 0;
const max = 50;
sortedTagOccurrences.forEach(item => {
if (i < max) {
result += `${item.key}: ${item.value}\n`;
}
i += 1;
});
}
/**
* @param {{key: any, value: any}} obj
* @returns {{key: any, value: any}}
*/
function sortObjectByValues(obj) {
const sortable = Object.entries(obj);
sortable.sort((a, b) => b[1] - a[1]);
return sortable.map(item => ({
key: item[0],
value: item[1]
}));
}
function injectCommonStyles() {
injectStyleHTML(`
.light-green-gradient {
background: linear-gradient(to bottom, #aae5a4, #89e180);
color: black;
}
.dark-green-gradient {
background: linear-gradient(to bottom, #5e715e, #293129);
color: white;
}
img {
border: none !important;
}
`, "lightDarkTheme");
setTimeout(() => {
if (onPostPage()) {
removeInlineImgStyles();
}
configureVideoOutlines();
}, 100);
}
function configureVideoOutlines() {
injectStyleHTML("img.video {outline: 4px solid blue;}", "video-border");
}
function removeInlineImgStyles() {
for (const image of document.getElementsByTagName("img")) {
image.removeAttribute("style");
}
}
function setTheme() {
setTimeout(() => {
if (usingDarkTheme()) {
for (const element of document.querySelectorAll(".light-green-gradient")) {
element.classList.remove("light-green-gradient");
element.classList.add("dark-green-gradient");
}
}
}, 10);
}
/**
* @param {String} eventName
* @param {Number} delay
* @returns
*/
function dispatchEventWithDelay(eventName, delay) {
if (delay === undefined) {
dispatchEvent(new Event(eventName));
return;
}
setTimeout(() => {
dispatchEvent(new Event(eventName));
}, delay);
}
/**
* @param {String} postId
* @returns
*/
function getThumbByPostId(postId) {
return document.getElementById(postId);
}
/**
* @param {String} content
* @returns {Blob | MediaSource}
*/
function getWorkerURL(content) {
return URL.createObjectURL(new Blob([content], {
type: "text/javascript"
}));
}
function initializeUtilities() {
injectCommonStyles();
trackCursorPosition();
setTheme();
}
/**
* @returns {String}
*/
function getTagBlacklist() {
let tags = getCookie("tag_blacklist", "");
for (let i = 0; i < 3; i += 1) {
tags = decodeURIComponent(tags).replace(/(?:^| )-/, "");
}
return tags;
}
/**
* @returns {HTMLElement | null}
*/
function getThumbUnderCursor() {
const elementUnderCursor = document.elementFromPoint(CURSOR_POSITION.X, CURSOR_POSITION.Y);
if (elementUnderCursor !== undefined && elementUnderCursor !== null && elementUnderCursor.nodeName.toLowerCase() === "img") {
return getThumbFromImage(elementUnderCursor);
}
return null;
}
/**
* @returns {Boolean}
*/
function hoveringOverThumb() {
return getThumbUnderCursor() !== null;
}
/**
* @returns {Boolean}
*/
function usingCaptions() {
const result = document.getElementById("captionList") !== null;
return result;
}
/**
* @returns {Boolean}
*/
function usingRenderer() {
return document.getElementById("original-content-container") !== null;
}
function getThumbUnderCursorOnLoad() {
const thumbNodeElement = getThumbUnderCursor();
dispatchEvent(new CustomEvent("thumbUnderCursorOnLoad", {
detail: thumbNodeElement
}));
}
/**
* @param {String} word
* @returns {String}
*/
function capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
/**
* @param {Number} number
* @returns {Number}
*/
function roundToTwoDecimalPlaces(number) {
return Math.round((number + Number.EPSILON) * 100) / 100;
}
/**
* @returns {Boolean}
*/
function usingDarkTheme() {
return getCookie("theme", "") === "dark";
}
/**
* @param {Event} event
* @returns {Boolean}
*/
function enteredOverCaptionTag(event) {
return event.relatedTarget !== null && event.relatedTarget.classList.contains("caption-tag");
}
/**
* @param {String[]} postId
* @param {Boolean} doAnimation
*/
function scrollToThumb(postId, doAnimation = true) {
const element = document.getElementById(postId);
const elementIsNotAThumb = element === null || (!element.classList.contains("thumb") && !element.classList.contains("thumb-node"));
if (elementIsNotAThumb) {
if (postId === "") {
alert("Please enter a post ID");
} else {
alert(`Favorite with post ID ${postId} not found`);
}
return;
}
const rect = element.getBoundingClientRect();
const favoritesHeader = document.getElementById("favorites-top-bar");
const favoritesSearchHeight = favoritesHeader === null ? 0 : favoritesHeader.getBoundingClientRect().height;
window.scroll({
top: rect.top + window.scrollY + (rect.height / 2) - (window.innerHeight / 2) - (favoritesSearchHeight / 2),
behavior: "smooth"
});
if (!doAnimation) {
return;
}
const image = getImageFromThumb(element);
image.classList.add("found");
setTimeout(() => {
image.classList.remove("found");
}, 2000);
}
/**
* @param {HTMLElement} thumb
*/
function assignContentType(thumb) {
const image = getImageFromThumb(thumb);
const tagAttribute = image.hasAttribute("tags") ? "tags" : "title";
const tags = image.getAttribute(tagAttribute);
image.classList.add(getContentType(tags));
}
/**
* @param {String} tags
* @returns {String}
*/
function getContentType(tags) {
tags += " ";
const isAnimated = tags.includes("animated ") || tags.includes("video ");
const isAGif = isAnimated && !tags.includes("video ");
return isAGif ? "gif" : isAnimated ? "video" : "image";
}
function correctMisspelledTags(tags) {
if ((/vide(?:\s|$)/).test(tags)) {
tags += " video";
}
return tags;
}
/**
* @param {String} searchQuery
* @returns {{orGroups: String[][], remainingSearchTags: String[]}}
*/
function extractTagGroups(searchQuery) {
searchQuery = searchQuery.toLowerCase();
const orRegex = /\( (.*?) \)/g;
const orGroups = Array.from(removeExtraWhiteSpace(searchQuery)
.matchAll(orRegex))
.map((orGroup) => orGroup[1].split(" ~ "));
const remainingSearchTags = removeExtraWhiteSpace(searchQuery
.replace(orRegex, ""))
.split(" ")
.filter((searchTag) => searchTag !== "");
return {
orGroups,
remainingSearchTags
};
}
/**
* @param {String} string
* @returns {String}
*/
function removeExtraWhiteSpace(string) {
return string.trim().replace(/\s+/g, " ");
}
/**
*
* @param {HTMLImageElement} image
* @returns {Boolean}
*/
function imageIsLoaded(image) {
return image.complete || image.naturalWidth !== 0;
}
/**
* @returns {Boolean}
*/
function usingFirefox() {
if (usingFirefoxFlag === undefined) {
usingFirefoxFlag = navigator.userAgent.toLowerCase().includes("firefox");
}
return usingFirefoxFlag;
}
initializeUtilities();
// match.js
class PostTags {
/**
* @type {Set.<String>}
*/
set;
/**
* @type {String[]}
*/
array;
/**
* @param {String} tags
*/
constructor(tags) {
this.create(tags);
}
/**
* @param {String} tags
*/
create(tags) {
this.array = removeExtraWhiteSpace(tags)
.split(" ")
.sort();
this.set = new Set(this.array);
}
}
/**
* @param {String} searchQuery
* @returns {{orGroups: String[][], remainingSearchTags: String[], isEmpty: Boolean}}
*/
function getSearchCommand(searchQuery) {
const {orGroups, remainingSearchTags} = extractTagGroups(searchQuery);
return {
orGroups,
remainingSearchTags,
isEmpty: searchQuery.trim() === ""
};
}
/**
* @param {{orGroups: String[][], remainingSearchTags: String[], isEmpty: Boolean}} searchCommand
* @param {PostTags} postTags
* @returns {Boolean}
*/
function postTagsMatchSearch(searchCommand, postTags) {
if (searchCommand.isEmpty) {
return true;
}
if (!postTagsMatchAllRemainingSearchTags(searchCommand.remainingSearchTags, postTags)) {
return false;
}
return postTagsMatchAllOrGroups(searchCommand.orGroups, postTags);
}
/**
* @param {String[]} remainingSearchTags
* @param {PostTags} postTags
* @returns {Boolean}
*/
function postTagsMatchAllRemainingSearchTags(remainingSearchTags, postTags) {
for (const searchTag of remainingSearchTags) {
if (!postTagsMatchSearchTag(searchTag, postTags, false)) {
return false;
}
}
return true;
}
/**
* @param {String[][]} orGroups
* @param {PostTags} postTags
* @returns {Boolean}
*/
function postTagsMatchAllOrGroups(orGroups, postTags) {
for (const orGroup of orGroups) {
if (!atLeastOnePostTagIsInOrGroup(orGroup, postTags)) {
return false;
}
}
return true;
}
/**
* @param {String[]} orGroup
* @param {PostTags} postTags
* @returns {Boolean}
*/
function atLeastOnePostTagIsInOrGroup(orGroup, postTags) {
for (const orTag of orGroup) {
if (postTagsMatchSearchTag(orTag, postTags, true)) {
return true;
}
}
return false;
}
/**
* @param {String} searchTag
* @param {PostTags} postTags
* @param {Boolean} inOrGroup
* @returns {Boolean}
*/
function postTagsMatchSearchTag(searchTag, postTags, inOrGroup) {
const isNegated = inOrGroup ? false : searchTag.startsWith("-");
const isWildcard = searchTag.endsWith("*");
searchTag = isWildcard ? searchTag.slice(0, -1) : searchTag;
searchTag = isNegated ? searchTag.substring(1) : searchTag;
const postTagsContainSearchTag = postTags.set.has(searchTag);
if (postTagsContainSearchTag) {
return !isNegated;
}
if (isWildcard && binarySearchStartsWith(searchTag, postTags.array)) {
return !isNegated;
}
return isNegated;
}
/**
* @param {String} target
* @param {String[]} array
* @returns {Boolean}
*/
function binarySearchStartsWith(target, array) {
let left = 0;
let right = array.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (array[mid].startsWith(target)) {
return true;
} else if (array[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
// thumb_node.js
const THUMB_NODE_TEMPLATE = new DOMParser().parseFromString("<div></div>", "text/html").createElement("div");
THUMB_NODE_TEMPLATE.className = "thumb-node";
THUMB_NODE_TEMPLATE.innerHTML = `
<div>
<img loading="lazy">
<button class="remove-button light-green-gradient" style="visibility: hidden;">Remove</button>
<canvas></canvas>
</div>
`;
class ThumbNode {
static baseURLs = {
post: "https://rule34.xxx/index.php?page=post&s=view&id=",
remove: "https://rule34.xxx/index.php?page=favorites&s=delete&id="
};
static thumbSourceExtractionRegex = /thumbnails\/\/([0-9]+)\/thumbnail_([0-9a-f]+)/;
/**
* @param {String} compressedSource
* @param {String} id
* @returns {String}
*/
static decompressThumbSource(compressedSource, id) {
compressedSource = compressedSource.split("_");
return `https://us.rule34.xxx/thumbnails//${compressedSource[0]}/thumbnail_${compressedSource[1]}.jpg?${id}`;
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
static getIdFromThumb(thumb) {
const elementWithId = onPostPage() ? thumb : thumb.children[0];
return elementWithId.id.substring(1);
}
/**
* @type {HTMLDivElement}
*/
root;
/**
* @type {String}
*/
id;
/**
* @type {HTMLElement}
*/
container;
/**
* @type {HTMLImageElement}
*/
image;
/**
* @type {String}
*/
tags;
/**
* @type {PostTags}
*/
postTags;
/**
* @type {HTMLButtonElement}
*/
removeButton;
/**
* @type {String}
*/
get removeURL() {
return ThumbNode.baseURLs.remove + this.id;
}
/**
* @type {String}
*/
get href() {
return ThumbNode.baseURLs.post + this.id;
}
/**
* @type {String[]}
*/
get tagList() {
return this.tags.split(" ");
}
/**
* @type {{id: String, tags: String, src: String}}
*/
get databaseRecord() {
return {
id: this.id,
tags: this.tags,
src: this.compressedThumbSource
};
}
/**
* @type {String}
*/
get compressedThumbSource() {
return this.image.src.match(ThumbNode.thumbSourceExtractionRegex).splice(1).join("_");
}
/**
* @type {Boolean}
*/
get isVisible() {
return this.root.style.display !== "none";
}
/**
* @param {HTMLElement | {id: String, tags: String, src: String, type: String}} thumb
* @param {Boolean} fromRecord
*/
constructor(thumb, fromRecord) {
this.instantiateTemplate();
this.populateAttributes(thumb, fromRecord);
this.setupRemoveButton();
this.setupOnClickLink();
}
instantiateTemplate() {
this.root = THUMB_NODE_TEMPLATE.cloneNode(true);
this.container = this.root.children[0];
this.image = this.root.children[0].children[0];
this.removeButton = this.root.children[0].children[1];
}
setupRemoveButton() {
if (userIsOnTheirOwnFavoritesPage()) {
this.removeButton.onclick = (event) => {
event.stopPropagation();
setIdToBeRemovedOnReload(this.id);
fetch(this.removeURL);
this.removeButton.remove();
};
}
}
/**
* @param {HTMLElement | {id: String, tags: String, src: String, type: String}} thumb
* @param {Boolean} fromDatabaseRecord
*/
populateAttributes(thumb, fromDatabaseRecord) {
if (fromDatabaseRecord) {
this.createFromDatabaseRecord(thumb);
} else {
this.createFromHTMLElement(thumb);
}
this.root.id = this.id;
this.image.setAttribute("tags", this.tags);
this.postTags = new PostTags(this.tags);
}
/**
* @param {{id: String, tags: String, src: String, type: String}} record
*/
createFromDatabaseRecord(record) {
this.image.src = ThumbNode.decompressThumbSource(record.src, record.id);
this.id = record.id;
this.tags = record.tags;
this.image.classList.add(record.type);
}
/**
* @param {HTMLElement} thumb
*/
createFromHTMLElement(thumb) {
const imageElement = thumb.children[0].children[0];
this.image.src = imageElement.src;
this.id = ThumbNode.getIdFromThumb(thumb);
this.tags = `${correctMisspelledTags(imageElement.title)} ${this.id}`;
this.image.classList.add(getContentType(this.tags));
}
setupOnClickLink() {
if (usingRenderer()) {
this.container.setAttribute("href", this.href);
} else {
this.container.onclick = () => {
window.open(this.href, "_blank");
};
this.container.addEventListener("mousedown", (event) => {
const middleClick = 1;
if (event.button === middleClick) {
event.preventDefault();
window.open(this.href, "_blank");
}
});
}
}
/**
* @param {HTMLElement} element
* @param {String} position
*/
insertInDocument(element, position) {
element.insertAdjacentElement(position, this.root);
}
/**
* @param {Boolean} value
*/
toggleVisibility(value) {
this.root.style.display = value ? "" : "none";
}
}
// load.js
class FavoritesLoader {
static loadState = {
notStarted: 0,
started: 1,
finished: 2,
indexedDB: 3
};
static objectStoreName = `user${getFavoritesPageId()}`;
static databaseName = "Favorites";
static webWorkers = {
database:
`
/* eslint-disable prefer-template */
/**
* @param {Number} milliseconds
* @returns {Promise}
*/
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
class FavoritesDatabase {
/**
* @type {String}
*/
name = "Favorites";
/**
* @type {String}
*/
objectStoreName;
/**
* @type {Number}
*/
version;
/**
* @param {String} objectStoreName
* @param {Number | String} version
*/
constructor(objectStoreName, version) {
this.objectStoreName = objectStoreName;
this.version = version;
this.createObjectStore();
}
createObjectStore() {
this.openConnection((event) => {
/**
* @type {IDBDatabase}
*/
const database = event.target.result;
const objectStore = database
.createObjectStore(this.objectStoreName, {
autoIncrement: true
});
objectStore.createIndex("id", "id", {
unique: true
});
}).then((event) => {
event.target.result.close();
});
}
/**
* @param {Function} onUpgradeNeeded
* @returns {Promise}
*/
openConnection(onUpgradeNeeded) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.name, this.version);
request.onsuccess = resolve;
request.onerror = reject;
request.onupgradeneeded = onUpgradeNeeded;
});
}
/**
* @param {[{id: String, tags: String, src: String}]} favorites
*/
storeFavorites(favorites) {
this.openConnection()
.then((event) => {
const database = event.target.result;
const favoritesObjectStore = database
.transaction(this.objectStoreName, "readwrite")
.objectStore(this.objectStoreName);
favorites.forEach(favorite => {
this.addContentTypeToFavorite(favorite);
favoritesObjectStore.add(favorite);
database.close();
});
postMessage({
response: "finishedStoring"
});
})
.catch((event) => {
const error = event.target.error;
const errorType = error.name;
if (errorType === "VersionError") {
this.version += 1;
this.storeFavorites(favorites);
} else {
console.error(error);
}
});
}
/**
* @param {String[]} idsToDelete
*/
async loadFavorites(idsToDelete) {
await this.openConnection()
.then(async(event) => {
const database = event.target.result;
const objectStore = database
.transaction(this.objectStoreName, "readwrite")
.objectStore(this.objectStoreName);
const index = objectStore.index("id");
for (const id of idsToDelete) {
const deleteRequest = index.getKey(id);
await new Promise((resolve, reject) => {
deleteRequest.onsuccess = resolve;
deleteRequest.onerror = reject;
}).then((event1) => {
const primaryKey = event1.target.result;
if (primaryKey !== undefined) {
objectStore.delete(primaryKey);
}
});
}
objectStore.getAll().onsuccess = (successEvent) => {
const results = successEvent.target.result.reverse();
postMessage({
response: "finishedLoading",
favorites: results
});
};
database.close();
});
}
/**
* @param {{id: String, tags: String, src: String}} favorite
*/
addContentTypeToFavorite(favorite) {
const tags = favorite.tags + " ";
const isAnimated = tags.includes("animated ") || tags.includes("video ");
const isGif = isAnimated && !tags.includes("video ");
favorite.type = isGif ? "gif" : isAnimated ? "video" : "image";
}
}
/**
* @type {FavoritesDatabase}
*/
let favoritesDatabase;
onmessage = (message) => {
const request = message.data;
switch (request.command) {
case "create":
favoritesDatabase = new FavoritesDatabase(request.objectStoreName, request.version);
break;
case "store":
favoritesDatabase.storeFavorites(request.favorites);
break;
case "load":
favoritesDatabase.loadFavorites(request.deletedIds);
break;
default:
break;
}
};
`
};
static tagNegation = {
useTagBlacklist: true,
negatedTagBlacklist: negateTags(TAG_BLACKLIST)
};
/**
* @type {ThumbNode[]}
*/
allThumbNodes;
/**
* @type {Number}
*/
finalPageNumber;
/**
* @type {HTMLLabelElement}
*/
matchCountLabel;
/**
* @type {Number}
*/
matchingFavoritesCount;
/**
* @type {Number}
*/
maxNumberOfFavoritesToDisplay;
/**
* @type {[{url: String, indexToInsert: Number}]}
*/
failedFetchRequests;
/**
* @type {LoadState}
*/
currentLoadState;
/**
* @type {Number}
*/
expectedFavoritesCount;
/**
* @type {Boolean}
*/
expectedFavoritesCountFound;
/**
* @type {String}
*/
searchQuery;
/**
* @type {Worker}
*/
databaseWorker;
/**
* @type {Boolean}
*/
searchResultsAreShuffled;
/**
* @type {HTMLTextAreaElement}
*/
favoritesSearchInput;
/**
* @type {Number}
*/
currentFavoritesPageNumber;
/**
* @type {Boolean}
*/
get databaseAccessIsAllowed() {
// return userIsOnTheirOwnFavoritesPage();
return true;
}
/**
* @type {String}
*/
get finalSearchQuery() {
if (FavoritesLoader.tagNegation.useTagBlacklist) {
return `${this.searchQuery} ${FavoritesLoader.tagNegation.negatedTagBlacklist}`;
}
return this.searchQuery;
}
/**
* @type {Boolean}
*/
get matchCountLabelExists() {
if (this.matchCountLabel === null) {
this.matchCountLabel = document.getElementById("match-count-label");
if (this.matchCountLabel === null) {
return false;
}
}
return true;
}
constructor() {
if (onPostPage()) {
return;
}
this.allThumbNodes = [];
this.finalPageNumber = this.getFinalFavoritesPageNumber();
this.matchCountLabel = document.getElementById("match-count-label");
this.maxNumberOfFavoritesToDisplay = 100000;
this.failedFetchRequests = [];
this.currentLoadState = FavoritesLoader.loadState.notStarted;
this.expectedFavoritesCount = 53;
this.expectedFavoritesCountFound = false;
this.matchingFavoritesCount = 0;
this.searchQuery = "";
this.databaseWorker = new Worker(getWorkerURL(FavoritesLoader.webWorkers.database));
this.favoritesSearchInput = document.getElementById("favorites-search-box");
this.currentFavoritesPageNumber = 0;
this.createDatabaseMessageHandler();
this.loadFavoritesPage();
}
createDatabaseMessageHandler() {
this.databaseWorker.onmessage = (message) => {
message = message.data;
switch (message.response) {
case "finishedLoading":
this.currentLoadState = FavoritesLoader.loadState.indexedDB;
this.attachSavedFavoritesToDocument(message.favorites);
this.updateSavedFavorites();
break;
case "finishedStoring":
setTimeout(() => {
this.databaseWorker.terminate();
}, 5000);
default:
break;
}
};
}
loadFavoritesPage() {
this.clearContent();
this.setFavoritesCount();
this.searchFavorites();
}
setFavoritesCount() {
const profilePage = `https://rule34.xxx/index.php?page=account&s=profile&id=${getFavoritesPageId()}`;
fetch(profilePage)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status);
})
.then((html) => {
const table = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html").querySelector("table");
if (table === null) {
return;
}
for (const row of table.querySelectorAll("tr")) {
const cells = row.querySelectorAll("td");
if (cells.length >= 2 && cells[0].textContent.trim() === "Favorites") {
this.expectedFavoritesCountFound = true;
this.expectedFavoritesCount = parseInt(cells[1].textContent.trim());
return;
}
}
})
.catch((error) => {
console.error(error);
});
}
clearContent() {
const thumbs = Array.from(document.getElementsByClassName("thumb"));
setTimeout(() => {
dispatchEvent(new CustomEvent("originalContentCleared", {
detail: thumbs
}));
}, 1000);
document.getElementById("content").innerHTML = "";
}
/**
* @param {String} searchQuery
*/
searchFavorites(searchQuery) {
this.searchQuery = searchQuery === undefined ? this.searchQuery : searchQuery;
this.hideAwesomplete();
this.resetMatchCount();
dispatchEvent(new Event("searchStarted"));
switch (this.currentLoadState) {
case FavoritesLoader.loadState.started:
this.showSearchResultsAfterStartedLoading();
break;
case FavoritesLoader.loadState.finished:
this.showSearchResultsAfterFinishedLoading();
break;
case FavoritesLoader.loadState.indexedDB:
break;
default:
this.showSearchResultsBeforeStartedLoading();
}
}
showSearchResultsAfterStartedLoading() {
const resultIds = this.getSearchResultIds(this.allThumbNodes, this.allThumbNodes.length - 1);
for (const thumbNode of this.allThumbNodes) {
if (resultIds[thumbNode.id] === undefined) {
thumbNode.toggleVisibility(false);
} else {
thumbNode.toggleVisibility(true);
this.incrementMatchCount();
}
}
}
showSearchResultsAfterFinishedLoading() {
const resultIds = this.getSearchResultIds(this.allThumbNodes);
let addedFavoritesCount = 0;
this.unShuffleSearchResults();
for (const thumbNode of this.allThumbNodes) {
if (resultIds[thumbNode.id] === undefined || addedFavoritesCount >= this.maxNumberOfFavoritesToDisplay) {
thumbNode.toggleVisibility(false);
} else {
thumbNode.toggleVisibility(true);
this.incrementMatchCount();
addedFavoritesCount += 1;
}
}
dispatchEventWithDelay("finishedSearching");
}
async showSearchResultsBeforeStartedLoading() {
if (!this.databaseAccessIsAllowed) {
this.fetchFavorites();
return;
}
const databaseStatus = await this.getDatabaseStatus();
this.databaseWorker.postMessage({
command: "create",
objectStoreName: FavoritesLoader.objectStoreName,
version: databaseStatus.version
});
const eventThatFavoritesBecomeVisibleAt = databaseStatus.objectStoreExists ? "favoritesLoaded" : "load";
this.broadcastThumbUnderCursorOnLoadWhenAvailable(eventThatFavoritesBecomeVisibleAt);
if (databaseStatus.objectStoreIsNotEmpty) {
this.loadFavorites();
} else {
this.fetchFavorites();
}
}
/**
* @returns {{version: Number, objectStoreIsNotEmpty: Boolean}}
*/
getDatabaseStatus() {
return window.indexedDB.databases()
.then((rule34Databases) => {
const favoritesDatabases = rule34Databases.filter(database => database.name === FavoritesLoader.databaseName);
if (favoritesDatabases.length !== 1) {
return {
version: 1,
objectStoreIsNotEmpty: false
};
}
const foundDatabase = favoritesDatabases[0];
return new Promise((resolve, reject) => {
const databaseRequest = indexedDB.open(FavoritesLoader.databaseName, foundDatabase.version);
databaseRequest.onsuccess = resolve;
databaseRequest.onerror = reject;
}).then((event) => {
const database = event.target.result;
const objectStoreExists = database.objectStoreNames.contains(FavoritesLoader.objectStoreName);
const version = database.version;
if (!objectStoreExists) {
database.close();
return {
version: database.version + 1,
objectStoreIsNotEmpty: false
};
}
const countRequest = database
.transaction(FavoritesLoader.objectStoreName, "readonly")
.objectStore(FavoritesLoader.objectStoreName).count();
return new Promise((resolve, reject) => {
countRequest.onsuccess = resolve;
countRequest.onerror = reject;
}).then((countEvent) => {
database.close();
return {
version,
objectStoreIsNotEmpty: countEvent.target.result > 0
};
});
});
});
}
/**
* @param {ThumbNode[]} thumbNodes
* @param {Number} stopIndex
* @returns {ThumbNode[]}
*/
getSearchResults(thumbNodes, stopIndex) {
const searchCommand = getSearchCommand(this.finalSearchQuery);
const results = [];
stopIndex = stopIndex === undefined ? thumbNodes.length : stopIndex;
stopIndex = Math.min(stopIndex, thumbNodes.length);
for (let i = 0; i < stopIndex; i += 1) {
if (postTagsMatchSearch(searchCommand, thumbNodes[i].postTags)) {
results.push(thumbNodes[i]);
}
}
return results;
}
/**
* @param {ThumbNode[]} thumbNodes
* @param {Number} stopIndex
* @returns {Object.<String, Number>}
*/
getSearchResultIds(thumbNodes, stopIndex) {
const results = {};
for (const thumbNode of this.getSearchResults(thumbNodes, stopIndex)) {
results[thumbNode.id] = 0;
}
return results;
}
fetchNewFavoritesWithoutReloadingPage() {
const previousFavoriteCount = this.expectedFavoritesCount;
let currentFavoritesCount = 0;
this.setFavoritesCount();
setTimeout(() => {
currentFavoritesCount = getIdsToRemoveOnReload().length + this.expectedFavoritesCount;
const newFavoritesCount = currentFavoritesCount - previousFavoriteCount;
if (newFavoritesCount > 0) {
this.updateSavedFavorites();
this.setProgressText(`Fetching ${newFavoritesCount} new favorite${newFavoritesCount > 1 ? "s" : ""}`);
this.showProgressText(true);
}
}, 800);
}
updateSavedFavorites() {
setTimeout(() => {
this.addNewFavoritesToSavedFavorites(this.getAllFavoriteIds(), 0, []);
}, 100);
}
/**
* @param {Object.<string, ThumbNode>} allFavoriteIds
* @param {Number} currentPageNumber
* @param {ThumbNode[]} newFavoritesToAdd
*/
addNewFavoritesToSavedFavorites(allFavoriteIds, currentPageNumber, newFavoritesToAdd) {
const favoritesPageURL = `${document.location.href}&pid=${currentPageNumber}`;
let allNewFavoritesFound = false;
requestPageInformation(favoritesPageURL, (response) => {
const thumbNodes = this.extractThumbNodesFromFavoritesPage(response);
for (const thumbNode of thumbNodes) {
const favoriteIsNotNew = allFavoriteIds[thumbNode.id] !== undefined;
if (favoriteIsNotNew) {
allNewFavoritesFound = true;
break;
}
newFavoritesToAdd.push(thumbNode);
}
if (!allNewFavoritesFound && currentPageNumber < this.finalPageNumber) {
this.addNewFavoritesToSavedFavorites(allFavoriteIds, currentPageNumber + 50, newFavoritesToAdd);
} else {
this.allThumbNodes = newFavoritesToAdd.concat(this.allThumbNodes);
this.finishUpdatingSavedFavorites(newFavoritesToAdd);
}
});
}
/**
* @param {ThumbNode[]} newThumbNodes
*/
finishUpdatingSavedFavorites(newThumbNodes) {
if (newThumbNodes.length > 0) {
this.insertNewFavoritesAfterReloadingPage(newThumbNodes);
this.storeFavorites(newThumbNodes);
this.toggleLoadingUI(false);
} else {
this.databaseWorker.terminate();
}
this.updateMatchCount(getAllVisibleThumbs().length);
}
async fetchFavorites() {
let currentPageNumber = 0;
this.currentLoadState = FavoritesLoader.loadState.started;
this.toggleContentVisibility(true);
setTimeout(() => {
dispatchEvent(new Event("startedFetchingFavorites"));
}, 50);
while (this.currentLoadState === FavoritesLoader.loadState.started) {
await this.fetchFavoritesStep(currentPageNumber * 50);
let progressText = `Saving Favorites ${this.allThumbNodes.length}`;
if (this.expectedFavoritesCountFound) {
progressText = `${progressText} / ${this.expectedFavoritesCount}`;
}
this.setProgressText(progressText);
currentPageNumber += 1;
}
}
/**
* @param {Number} currentPageNumber
*/
async fetchFavoritesStep(currentPageNumber) {
let finishedLoading = this.allThumbNodes.length >= this.expectedFavoritesCount - 2;
finishedLoading = finishedLoading || currentPageNumber >= (this.finalPageNumber * 2) + 1;
finishedLoading = finishedLoading && this.failedFetchRequests.length === 0;
if (currentPageNumber <= this.finalPageNumber) {
await this.fetchFavoritesFromSinglePage(currentPageNumber);
} else if (this.failedFetchRequests.length > 0) {
const failedRequest = this.failedFetchRequests.shift();
await this.fetchFavoritesFromSinglePage(currentPageNumber, failedRequest);
} else if (finishedLoading) {
this.onAllFavoritesLoaded();
this.storeFavorites();
}
}
/**
* @param {String} html
* @returns {{thumbNodes: ThumbNode[], searchResults: ThumbNode[]}}
*/
extractFavoritesPage(html) {
const thumbNodes = this.extractThumbNodesFromFavoritesPage(html);
const searchResults = this.getSearchResults(thumbNodes);
return {
thumbNodes,
searchResults
};
}
/**
* @param {Number} pageNumber
* @param {{url: String, indexToInsert: Number}} failedRequest
*/
fetchFavoritesFromSinglePage(pageNumber, failedRequest) {
const refetching = failedRequest !== undefined;
const favoritesPageURL = refetching ? failedRequest.url : `${document.location.href}&pid=${pageNumber}`;
return fetch(favoritesPageURL)
.then((response) => {
if (response.ok) {
return response.text();
}
failedRequest = refetching ? failedRequest : this.getFailedFetchRequest(response, pageNumber);
this.failedFetchRequests.push(failedRequest);
throw new Error(response.status);
})
.then((html) => {
const {thumbNodes, searchResults} = this.extractFavoritesPage(html);
setTimeout(() => {
dispatchEvent(new CustomEvent("favoritesFetched", {
detail: thumbNodes.map(thumbNode => thumbNode.root)
}));
}, 250);
if (refetching) {
this.allThumbNodes.splice(failedRequest.indexToInsert, 0, ...thumbNodes);
} else {
this.allThumbNodes = this.allThumbNodes.concat(thumbNodes);
}
if (this.allThumbNodes.length < this.maxNumberOfFavoritesToDisplay) {
this.incrementMatchCount(searchResults.length);
this.addFavoritesToContent(searchResults, failedRequest);
}
})
.catch((error) => {
console.error(error);
});
}
/**
* @param {String} response
* @param {Number} pageNumber
* @returns {{url: String, indexToInsert: Number}}
*/
getFailedFetchRequest(response, pageNumber) {
return {
url: response.url,
indexToInsert: pageNumber - 1
};
}
/**
* @param {String} response
* @returns {ThumbNode[]}
*/
extractThumbNodesFromFavoritesPage(response) {
const dom = new DOMParser().parseFromString(`<div>${response}</div>`, "text/html");
return Array.from(dom.getElementsByClassName("thumb")).map(thumb => new ThumbNode(thumb, false));
}
invertSearchResults() {
this.resetMatchCount();
let addedFavoritesCount = 0;
for (const thumbNode of this.allThumbNodes) {
if (addedFavoritesCount < this.maxNumberOfFavoritesToDisplay && !thumbNode.isVisible) {
thumbNode.toggleVisibility(true);
this.incrementMatchCount();
addedFavoritesCount += 1;
} else {
thumbNode.toggleVisibility(false);
}
}
window.scrollTo(0, 0);
dispatchEventWithDelay("finishedSearching");
}
shuffleSearchResults() {
const thumbs = Array.from(getAllVisibleThumbs());
const content = document.getElementById("content");
shuffleArray(thumbs);
for (const thumb of thumbs) {
content.insertBefore(thumb, content.firstChild);
}
this.searchResultsAreShuffled = true;
dispatchEventWithDelay("shuffle");
}
unShuffleSearchResults() {
if (!this.searchResultsAreShuffled) {
return;
}
const content = document.getElementById("content");
for (const thumbNode of this.allThumbNodes) {
content.appendChild(thumbNode.root);
}
this.searchResultsAreShuffled = false;
}
onAllFavoritesLoaded() {
this.currentLoadState = FavoritesLoader.loadState.finished;
this.toggleLoadingUI(false);
dispatchEventWithDelay("favoritesLoaded");
}
/**
* @param {Boolean} value
*/
toggleLoadingUI(value) {
this.showLoadingWheel(value);
this.toggleContentVisibility(!value);
this.setProgressText(value ? "Loading Favorites" : "All Favorites Loaded");
if (!value) {
setTimeout(() => {
this.showProgressText(false);
}, 500);
}
}
/**
* @param {[{id: String, tags: String, src: String}]} databaseRecords
* @returns {{ content: HTMLElement | null, searchResults: ThumbNode[]}}
*/
reconstructContent(databaseRecords) {
if (databaseRecords === null) {
return null;
}
const dom = new DOMParser().parseFromString("<div id=\"content\"></div>", "text/html");
const content = dom.getElementById("content");
const searchCommand = getSearchCommand(this.finalSearchQuery);
const searchResults = [];
let addedFavoritesCount = 0;
for (const record of databaseRecords) {
const thumbNode = new ThumbNode(record, true);
const underMaximumFavorites = addedFavoritesCount < this.maxNumberOfFavoritesToDisplay;
const isBlacklisted = !postTagsMatchSearch(searchCommand, thumbNode.postTags);
if (isBlacklisted) {
if (userIsOnTheirOwnFavoritesPage()) {
thumbNode.toggleVisibility(false);
} else {
continue;
}
} else {
searchResults.push(thumbNode);
}
this.allThumbNodes.push(thumbNode);
if (underMaximumFavorites) {
addedFavoritesCount += 1;
content.appendChild(thumbNode.root);
}
}
return {
content,
searchResults
};
}
loadFavorites() {
this.toggleLoadingUI(true);
let recentlyRemovedFavoriteIds = [];
if (this.databaseAccessIsAllowed && userIsOnTheirOwnFavoritesPage()) {
recentlyRemovedFavoriteIds = getIdsToRemoveOnReload();
clearRecentlyRemovedIds();
}
this.databaseWorker.postMessage({
command: "load",
deletedIds: recentlyRemovedFavoriteIds
});
}
/**
* @param {ThumbNode[]} thumbNodes
*/
storeFavorites(thumbNodes) {
if (!this.databaseAccessIsAllowed) {
return;
}
const storeAll = thumbNodes === undefined;
thumbNodes = storeAll ? this.allThumbNodes : thumbNodes;
const records = thumbNodes.map(thumbNode => thumbNode.databaseRecord);
if (storeAll) {
records.reverse();
}
this.databaseWorker.postMessage({
command: "store",
favorites: records
});
}
hideAwesomplete() {
if (this.favoritesSearchInput === null) {
this.favoritesSearchInput = document.getElementById("favorites-search-box");
}
if (this.favoritesSearchInput === null) {
return;
}
this.favoritesSearchInput.blur();
setTimeout(() => {
this.favoritesSearchInput.focus();
}, 100);
}
/**
* @returns {Number}
*/
getFinalFavoritesPageNumber() {
const lastPage = document.getElementsByName("lastpage")[0];
if (lastPage === undefined) {
return 0;
}
return parseInt(lastPage.getAttribute("onclick").match(/pid=([0-9]*)/)[1]);
}
deletePersistentData() {
localStorage.clear();
indexedDB.deleteDatabase("Favorites");
}
/**
* @param {Boolean} value
*/
showLoadingWheel(value) {
document.getElementById("loading-wheel").style.display = value ? "flex" : "none";
}
/**
* @param {Boolean} value
*/
showProgressText(value) {
document.getElementById("favorites-fetch-progress-label").style.display = value ? "inline-block" : "none";
}
/**
* @param {String} text
*/
setProgressText(text) {
document.getElementById("favorites-fetch-progress-label").textContent = text;
}
resetMatchCount() {
this.updateMatchCount(0);
}
/**
* @param {Boolean} value
*/
updateMatchCount(value) {
if (!this.matchCountLabelExists) {
return;
}
this.matchingFavoritesCount = value === undefined ? this.getSearchResults(this.allThumbNodes).length : value;
this.matchCountLabel.textContent = `${this.matchingFavoritesCount} Matches`;
}
/**
* @param {Boolean} value
*/
incrementMatchCount(value) {
if (!this.matchCountLabelExists) {
return;
}
this.matchingFavoritesCount += value === undefined ? 1 : value;
this.matchCountLabel.textContent = `${this.matchingFavoritesCount} Matches`;
}
/**
* @param {Boolean} value
*/
toggleContentVisibility(value) {
document.getElementById("content").style.display = value ? "" : "none";
}
/**
* @param {[{id: String, tags: String, src: String}]} databaseRecords
*/
attachSavedFavoritesToDocument(databaseRecords) {
const {content, searchResults} = this.reconstructContent(databaseRecords);
document.getElementById("content").remove();
document.body.appendChild(content);
this.paginateSearchResults(searchResults, false);
this.updateMatchCount(getAllVisibleThumbs().length);
this.onAllFavoritesLoaded();
}
/**
* @param {ThumbNode[]} newThumbNodes
*/
insertNewFavoritesAfterReloadingPage(newThumbNodes) {
const content = document.getElementById("content");
const searchCommand = getSearchCommand(this.searchQuery);
newThumbNodes.reverse();
for (const thumbNode of newThumbNodes) {
thumbNode.insertInDocument(content, "afterbegin");
if (!postTagsMatchSearch(searchCommand, thumbNode.postTags)) {
thumbNode.toggleVisibility(false);
}
}
setTimeout(() => {
dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
detail: newThumbNodes.map(thumbNode => thumbNode.root)
}));
}, 250);
}
/**
* @param {ThumbNode[]} thumbNodes
* @param {{url: String, indexToInsert: Number}} failedRequest
*/
addFavoritesToContent(thumbNodes, failedRequest) {
const content = document.getElementById("content");
let elementToInsertAround = content;
let placeToInsert = "beforeend";
if (failedRequest !== undefined) {
elementToInsertAround = getAllThumbs()[failedRequest.indexToInsert];
placeToInsert = "afterend";
thumbNodes = Array.from(thumbNodes).reverse();
}
for (const thumbNode of thumbNodes) {
thumbNode.insertInDocument(elementToInsertAround, placeToInsert);
}
}
/**
* @returns {Object.<String, ThumbNode>}
*/
getVisibleFavoriteIds() {
const ids = {};
for (const thumbNode of this.allThumbNodes) {
if (thumbNode.isVisible) {
ids[thumbNode.id] = thumbNode;
}
}
return ids;
}
/**
* @returns {Object.<String, ThumbNode>}
*/
getAllFavoriteIds() {
const favoriteIds = {};
for (const thumbNode of this.allThumbNodes) {
favoriteIds[thumbNode.id] = thumbNode;
}
return favoriteIds;
}
/**
* @param {String} eventName
*/
broadcastThumbUnderCursorOnLoadWhenAvailable(eventName) {
window.addEventListener(eventName, () => {
setTimeout(() => {
getThumbUnderCursorOnLoad();
}, 500);
}, {
once: true
});
}
/**
* @param {Boolean} value
*/
toggleTagBlacklistExclusion(value) {
FavoritesLoader.tagNegation.useTagBlacklist = value;
}
/**
* @param {ThumbNode[]} searchResults
*/
paginateSearchResults(searchResults, clearAllContent = true) {
const favoritesPagination = document.getElementById("favorites-pagination-container");
const content = document.getElementById("content");
if (favoritesPagination !== null) {
favoritesPagination.remove();
}
if (searchResults.length < this.maxNumberOfFavoritesToDisplay) {
return;
}
const pageCount = Math.floor(searchResults.length / this.maxNumberOfFavoritesToDisplay) + 1;
const favoritesPerPage = this.maxNumberOfFavoritesToDisplay;
const placeToInsertPagination = document.getElementById("left-favorites-panel-top-row");
const container = document.createElement("span");
if (clearAllContent) {
content.innerHTML = "";
}
container.id = "favorites-pagination-container";
placeToInsertPagination.appendChild(container);
this.currentFavoritesPageNumber = 1;
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {
const isMiddlePage = pageNumber > 2 && pageNumber < pageCount;
if (isMiddlePage) {
continue;
}
const pageNavigationButton = document.createElement("button");
pageNavigationButton.id = `favorites-page-${pageNumber}`;
pageNavigationButton.onclick = () => {
this.changeResultsPage(pageNumber, favoritesPerPage, searchResults, container);
};
container.appendChild(pageNavigationButton);
pageNavigationButton.textContent = pageNumber;
}
this.createPageTraversalButtons(favoritesPerPage, searchResults, pageCount, container);
}
/**
* @param {Number} favoritesPerPage
* @param {ThumbNode[]} searchResults
* @param {Number} pageCount
* @param {HTMLElement} container
*/
createPageTraversalButtons(favoritesPerPage, searchResults, pageCount, container) {
const previousPage = document.createElement("button");
const firstPage = document.createElement("button");
const nextPage = document.createElement("button");
const finalPage = document.createElement("button");
previousPage.style.display = "none";
firstPage.style.display = "none";
previousPage.textContent = "<";
firstPage.textContent = "<<";
nextPage.textContent = ">";
finalPage.textContent = ">>";
previousPage.onclick = () => {
this.changeResultsPage(this.currentFavoritesPageNumber - 1, favoritesPerPage, searchResults, container);
};
firstPage.onclick = () => {
this.changeResultsPage(1, favoritesPerPage, searchResults, container);
};
nextPage.onclick = () => {
this.changeResultsPage(this.currentFavoritesPageNumber + 1, favoritesPerPage, searchResults, container);
};
finalPage.onclick = () => {
this.changeResultsPage(pageCount, favoritesPerPage, searchResults, container);
};
container.insertAdjacentElement("afterbegin", previousPage);
container.insertAdjacentElement("afterbegin", firstPage);
container.appendChild(nextPage);
container.appendChild(finalPage);
}
/**
* @param {Number} pageNumber
* @param {Number} favoritesPerPage
* @param {ThumbNode[]} searchResults
* @param {HTMLElement} container
*/
changeResultsPage(pageNumber, favoritesPerPage, searchResults, container) {
const start = favoritesPerPage * (pageNumber - 1);
const end = favoritesPerPage * pageNumber;
const newThumbNodes = searchResults.slice(start, end);
this.updateVisibilityOfPageTraversalButtons(end, searchResults, pageNumber, container);
this.currentFavoritesPageNumber = pageNumber;
content.innerHTML = "";
for (const thumbNode of newThumbNodes) {
content.appendChild(thumbNode.root);
}
window.scrollTo(0, 0);
dispatchEventWithDelay("favoritesLoaded");
}
/**
* @param {Number} end
* @param {ThumbNode[]} searchResults
* @param {Number} pageNumber
* @param {HTMLElement} container
*/
updateVisibilityOfPageTraversalButtons(end, searchResults, pageNumber, container) {
const onFinalPage = end >= searchResults.length;
const onFirstPage = pageNumber === 1;
const pageButtons = Array.from(container.children);
for (const element of pageButtons.slice(0, 2)) {
element.style.display = onFirstPage ? "none" : "";
}
for (const element of pageButtons.slice(-2)) {
element.style.display = onFinalPage ? "none" : "";
}
}
}
const favoritesLoader = new FavoritesLoader();
// ui.js
const uiHTML = `<div id="favorites-top-bar" class="light-green-gradient">
<style>
#favorites-top-bar {
position: sticky;
top: 0;
padding: 10px;
z-index: 30;
margin-bottom: 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#favorites-top-bar-panels {
>div {
flex: 1;
textarea {
max-width: 100%;
height: 50px;
width: 95%;
padding: 10px;
border-radius: 6px;
resize: vertical;
}
}
}
#left-favorites-panel {
>div:first-of-type {
margin-bottom: 5px;
min-width: 560px;
>label {
align-content: center;
margin-right: 5px;
margin-top: 4px;
}
button {
height: 35px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
filter: brightness(140%);
}
}
}
}
#right-favorites-panel {
margin-left: 10px;
}
.checkbox {
display: block;
cursor: pointer;
padding: 2px 6px 2px 0px;
border-radius: 4px;
margin-left: -3px;
height: 27px;
&:hover {
color: #000;
background: #93b393;
text-shadow: none;
cursor: pointer;
}
input {
vertical-align: -5px;
cursor: pointer;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
}
.loading-wheel {
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 1s ease-in-out infinite;
pointer-events: none;
z-index: 9998;
position: fixed;
max-height: 100vh;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.remove-button {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 100%;
cursor: pointer;
color: #0075FF;
font-weight: bold;
font-size: 20px;
height: 40px;
background: white;
border: none;
z-index: 2;
}
.thumb-node {
position: relative;
>a,
>div {
position: relative;
&:has(.remove-button:hover) {
outline: 3px solid red;
>.remove-button {
box-shadow: 0px 3px 0px 0px red;
color: red;
}
}
}
img, canvas {
width: 100%;
z-index: 1;
}
&.hidden {
display: none;
}
}
.found {
opacity: 1;
animation: wiggle 2s;
}
@keyframes wiggle {
10%,
90% {
transform: translate3d(-2px, 0, 0);
}
20%,
80% {
transform: translate3d(4px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-8px, 0, 0);
}
40%,
60% {
transform: translate3d(8px, 0, 0);
}
}
#favorite-options-container {
display: flex;
flex-flow: row wrap;
width: 60%;
>div {
flex: 1;
padding-right: 6px;
flex-basis: 45%;
}
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
appearance: none;
-moz-appearance: textfield;
width: 15px;
}
#column-resize-container {
margin-top: 10px;
>span {
>button {
width: 20px;
text-align: center;
align-items: center;
}
}
}
#find-favorite {
margin-top: 7px;
>input {
border-radius: 6px;
height: 35px;
width: 75px;
border: 1px solid;
}
}
#favorites-pagination-container {
>button {
background: transparent;
margin: 0px 2px;
padding: 2px 6px;
border: 1px solid white;
cursor: pointer;
font-size: 14px;
color: white;
font-weight: normal;
&:hover {
background-color: #93b393;
}
&.selected {
border: none;
font-weight: bold;
}
}
}
#content {
display: grid !important;
grid-template-columns: repeat(10, 1fr);
grid-gap: 1em;
}
#help-links-container {
margin-top: 17px;
}
#left-favorites-panel-bottom-row {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
}
</style>
<div id="favorites-top-bar-panels" style="display: flex;">
<div id="left-favorites-panel">
<h2 style="display: inline;">Search Favorites</h2>
<span style="margin-left: 10px;">
<label id="match-count-label"></label>
<label id="favorites-fetch-progress-label" style="color: #3498db;"></label>
</span>
<div id="left-favorites-panel-top-row">
<button title="Search favorites\nctrl+click: Search all posts" id="search-button">Search</button>
<button title="Show results not matched by search" id="invert-button">Invert</button>
<button title="Shuffle order of search results" id="shuffle-button">Shuffle</button>
<button title="Clear the search box" id="clear-button">Clear</button>
<button title="Reset saved favorites" id="reset-button">Reset</button>
<span id="find-favorite" class="light-green-gradient" style="display: none;">
<button title="Scroll to favorite using its ID" id="find-favorite-button"
style="white-space: nowrap; ">Find</button>
<input type="number" id="find-favorite-input" type="text" placeholder="ID">
</span>
<span id="help-links-container">
<a href="https://github.com/bruh3396/favorites-search-gallery#controls" target="_blank">Help</a>
</span>
</div>
<div>
<textarea name="tags" id="favorites-search-box" placeholder="Search by Tags or IDs"
spellcheck="false"></textarea>
</div>
<div id="left-favorites-panel-bottom-row" style="display: flex; flex-flow: row-wrap;">
<div id="favorite-options-container">
<div id="show-options"><label class="checkbox" title="Toggle options"><input type="checkbox"
id="options-checkbox"> Options</label></div>
<div id="favorite-options">
<div><label class="checkbox" title="Toggle remove buttons"><input type="checkbox" id="show-remove-buttons">
Remove Buttons</label></div>
<div><label class="checkbox" title="Exclude blacklisted tags from search"><input type="checkbox"
id="filter-blacklist-checkbox"> Exclude Blacklist</label></div>
</div>
<div id="additional-favorite-options">
<div id="column-resize-container">
<span>
<label>Columns</label>
<button id="column-resize-minus">-</button>
<input type="number" id="column-resize-input" min="2" max="20">
<button id="column-resize-plus">+</button>
</span>
</div>
</div>
</div>
<div id="show-ui-container" style="width: 35%;">
<div id="show-ui-div" style="max-width: 400px;"><label class="checkbox" title="Toggle UI"><input type="checkbox" id="show-ui">UI</label></div>
</div>
</div>
</div>
<div id="right-favorites-panel" style="flex: 2;"></div>
</div>
<div class="loading-wheel" id="loading-wheel" style="display: none;"></div>
</div>
`;
if (!onPostPage()) {
document.getElementById("content").insertAdjacentHTML("beforebegin", uiHTML);
}
const FAVORITE_OPTIONS = [document.getElementById("favorite-options"), document.getElementById("additional-favorite-options")];
const MAX_SEARCH_HISTORY_LENGTH = 100;
const FAVORITE_SEARCH_PREFERENCES = {
textareaWidth: "searchTextareaWidth",
textareaHeight: "searchTextareaHeight",
showRemoveButtons: "showRemoveButtons",
showOptions: "showOptions",
filterBlacklist: "filterBlacklistCheckbox",
searchHistory: "favoritesSearchHistory",
findFavorite: "findFavorite",
thumbSize: "thumbSize",
columnCount: "columnCount",
showUI: "showUI"
};
const FAVORITE_SEARCH_LOCAL_STORAGE = {
searchHistory: "favoritesSearchHistory"
};
const FAVORITE_SEARCH_BUTTONS = {
search: document.getElementById("search-button"),
shuffle: document.getElementById("shuffle-button"),
clear: document.getElementById("clear-button"),
invert: document.getElementById("invert-button"),
reset: document.getElementById("reset-button"),
findFavorite: document.getElementById("find-favorite-button"),
columnPlus: document.getElementById("column-resize-plus"),
columnMinus: document.getElementById("column-resize-minus")
};
const FAVORITE_SEARCH_CHECKBOXES = {
showOptions: document.getElementById("options-checkbox"),
showRemoveButtons: document.getElementById("show-remove-buttons"),
filterBlacklist: document.getElementById("filter-blacklist-checkbox"),
showUI: document.getElementById("show-ui")
};
const FAVORITE_SEARCH_INPUTS = {
searchBox: document.getElementById("favorites-search-box"),
findFavorite: document.getElementById("find-favorite-input"),
columnCount: document.getElementById("column-resize-input")
};
const FAVORITE_SEARCH_LABELS = {
findFavorite: document.getElementById("find-favorite-label")
};
let searchHistory = [];
let searchHistoryIndex = 0;
let lastSearchQuery = "";
function initializeFavoritesPage() {
addEventListenersToFavoritesPage();
loadFavoritesPagePreferences();
removePaginatorFromFavoritesPage();
hideRemoveLinksWhenNotOnOwnFavoritesPage();
}
function loadFavoritesPagePreferences() {
const height = getPreference(FAVORITE_SEARCH_PREFERENCES.textareaHeight);
const width = getPreference(FAVORITE_SEARCH_PREFERENCES.textareaWidth);
if (height !== null && width !== null) {
/*
* FAVORITE_SEARCH_INPUTS.searchBox.style.width = width + "px"
* FAVORITE_SEARCH_INPUTS.searchBox.style.height = height + "px"
*/
}
const removeButtonsAreVisible = getPreference(FAVORITE_SEARCH_PREFERENCES.showRemoveButtons, false) && userIsOnTheirOwnFavoritesPage();
FAVORITE_SEARCH_CHECKBOXES.showRemoveButtons.checked = removeButtonsAreVisible;
setTimeout(() => {
updateVisibilityOfAllRemoveButtons();
}, 100);
const showOptions = getPreference(FAVORITE_SEARCH_PREFERENCES.showOptions, false);
FAVORITE_SEARCH_CHECKBOXES.showOptions.checked = showOptions;
toggleFavoritesOptions(showOptions);
if (userIsOnTheirOwnFavoritesPage()) {
FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.checked = getPreference(FAVORITE_SEARCH_PREFERENCES.filterBlacklist, false);
favoritesLoader.toggleTagBlacklistExclusion(FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.checked);
} else {
FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.checked = true;
FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.parentElement.style.display = "none";
}
searchHistory = JSON.parse(localStorage.getItem(FAVORITE_SEARCH_LOCAL_STORAGE.searchHistory)) || [];
if (searchHistory.length > 0) {
FAVORITE_SEARCH_INPUTS.searchBox.value = searchHistory[0];
}
FAVORITE_SEARCH_INPUTS.findFavorite.value = getPreference(FAVORITE_SEARCH_PREFERENCES.findFavorite, "");
FAVORITE_SEARCH_INPUTS.columnCount.value = getPreference(FAVORITE_SEARCH_PREFERENCES.columnCount, 6);
changeColumnCount(FAVORITE_SEARCH_INPUTS.columnCount.value);
const showUI = getPreference(FAVORITE_SEARCH_PREFERENCES.showUI, true);
FAVORITE_SEARCH_CHECKBOXES.showUI.checked = showUI;
toggleUI(showUI);
}
function removePaginatorFromFavoritesPage() {
const paginator = document.getElementById("paginator");
const pi = document.getElementById("pi");
if (paginator !== null) {
paginator.style.display = "none";
}
if (pi !== null) {
pi.remove();
}
}
function addEventListenersToFavoritesPage() {
FAVORITE_SEARCH_BUTTONS.search.onclick = (event) => {
const query = FAVORITE_SEARCH_INPUTS.searchBox.value;
if (event.ctrlKey) {
const queryWithFormattedIds = query.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
const postPageURL = `https://rule34.xxx/index.php?page=post&s=list&tags=${encodeURIComponent(queryWithFormattedIds)}`;
window.open(postPageURL);
} else {
favoritesLoader.searchFavorites(query);
addToFavoritesSearchHistory(query);
}
};
FAVORITE_SEARCH_INPUTS.searchBox.addEventListener("keydown", (event) => {
switch (event.key) {
case "Enter":
if (awesompleteIsUnselected(FAVORITE_SEARCH_INPUTS.searchBox)) {
event.preventDefault();
FAVORITE_SEARCH_BUTTONS.search.click();
} else {
clearAwesompleteSelection(FAVORITE_SEARCH_INPUTS.searchBox);
}
break;
case "ArrowUp":
case "ArrowDown":
if (awesompleteIsHidden(FAVORITE_SEARCH_INPUTS.searchBox)) {
event.preventDefault();
traverseFavoritesSearchHistory(event.key);
} else {
updateLastSearchQuery();
}
break;
case "Tab":
{
event.preventDefault();
const awesomplete = FAVORITE_SEARCH_INPUTS.searchBox.parentElement;
const searchSuggestions = Array.from(awesomplete.querySelectorAll("li")) || [];
if (!awesompleteIsUnselected(FAVORITE_SEARCH_INPUTS.searchBox)) {
const selectedSearchSuggestion = searchSuggestions.find(suggestion => suggestion.getAttribute("aria-selected") === "true");
completeSearchSuggestion(selectedSearchSuggestion);
} else if (!awesompleteIsHidden(FAVORITE_SEARCH_INPUTS.searchBox)) {
completeSearchSuggestion(searchSuggestions[0]);
}
break;
}
case "Escape":
if (!awesompleteIsHidden(FAVORITE_SEARCH_INPUTS.searchBox)) {
favoritesLoader.hideAwesomplete();
}
break;
default:
updateLastSearchQuery();
break;
}
});
FAVORITE_SEARCH_INPUTS.searchBox.addEventListener("wheel", (event) => {
const direction = event.deltaY > 0 ? "ArrowDown" : "ArrowUp";
traverseFavoritesSearchHistory(direction);
event.preventDefault();
});
FAVORITE_SEARCH_CHECKBOXES.showOptions.onchange = () => {
toggleFavoritesOptions(FAVORITE_SEARCH_CHECKBOXES.showOptions.checked);
setPreference(FAVORITE_SEARCH_PREFERENCES.showOptions, FAVORITE_SEARCH_CHECKBOXES.showOptions.checked);
};
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
setPreference(FAVORITE_SEARCH_PREFERENCES.textareaWidth, entry.contentRect.width);
setPreference(FAVORITE_SEARCH_PREFERENCES.textareaHeight, entry.contentRect.height);
}
});
resizeObserver.observe(FAVORITE_SEARCH_INPUTS.searchBox);
FAVORITE_SEARCH_CHECKBOXES.showRemoveButtons.onchange = () => {
updateVisibilityOfAllRemoveButtons();
setPreference(FAVORITE_SEARCH_PREFERENCES.showRemoveButtons, FAVORITE_SEARCH_CHECKBOXES.showRemoveButtons.checked);
};
FAVORITE_SEARCH_BUTTONS.shuffle.onclick = () => {
favoritesLoader.shuffleSearchResults();
};
FAVORITE_SEARCH_BUTTONS.clear.onclick = () => {
FAVORITE_SEARCH_INPUTS.searchBox.value = "";
};
FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.onchange = () => {
setPreference(FAVORITE_SEARCH_PREFERENCES.filterBlacklist, FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.checked);
favoritesLoader.toggleTagBlacklistExclusion(FAVORITE_SEARCH_CHECKBOXES.filterBlacklist.checked);
favoritesLoader.searchFavorites();
};
FAVORITE_SEARCH_BUTTONS.invert.onclick = () => {
favoritesLoader.invertSearchResults();
};
FAVORITE_SEARCH_BUTTONS.reset.onclick = () => {
favoritesLoader.deletePersistentData();
};
FAVORITE_SEARCH_INPUTS.findFavorite.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
scrollToThumb(FAVORITE_SEARCH_INPUTS.findFavorite.value);
setPreference(FAVORITE_SEARCH_PREFERENCES.findFavorite, postId);
}
});
FAVORITE_SEARCH_BUTTONS.findFavorite.onclick = () => {
scrollToThumb(FAVORITE_SEARCH_INPUTS.findFavorite.value);
setPreference(FAVORITE_SEARCH_PREFERENCES.findFavorite, postId);
};
FAVORITE_SEARCH_BUTTONS.columnPlus.onclick = () => {
changeColumnCount(parseInt(FAVORITE_SEARCH_INPUTS.columnCount.value) + 1);
};
FAVORITE_SEARCH_BUTTONS.columnMinus.onclick = () => {
changeColumnCount(parseInt(FAVORITE_SEARCH_INPUTS.columnCount.value) - 1);
};
FAVORITE_SEARCH_INPUTS.columnCount.onchange = () => {
changeColumnCount(parseInt(FAVORITE_SEARCH_INPUTS.columnCount.value));
};
FAVORITE_SEARCH_CHECKBOXES.showUI.onchange = () => {
toggleUI(FAVORITE_SEARCH_CHECKBOXES.showUI.checked);
};
}
function completeSearchSuggestion(suggestion) {
suggestion = suggestion.innerText.replace(/ \([0-9]+\)$/, "");
favoritesLoader.hideAwesomplete();
FAVORITE_SEARCH_INPUTS.searchBox.value = FAVORITE_SEARCH_INPUTS.searchBox.value.replace(/\S+$/, `${suggestion} `);
}
function hideRemoveLinksWhenNotOnOwnFavoritesPage() {
if (!userIsOnTheirOwnFavoritesPage()) {
FAVORITE_SEARCH_CHECKBOXES.showRemoveButtons.parentElement.style.display = "none";
}
}
function updateLastSearchQuery() {
if (FAVORITE_SEARCH_INPUTS.searchBox.value !== lastSearchQuery) {
lastSearchQuery = FAVORITE_SEARCH_INPUTS.searchBox.value;
}
searchHistoryIndex = -1;
}
/**
* @param {String} newSearch
*/
function addToFavoritesSearchHistory(newSearch) {
newSearch = newSearch.trim();
searchHistory = searchHistory.filter(search => search !== newSearch);
searchHistory.unshift(newSearch);
searchHistory.length = Math.min(searchHistory.length, MAX_SEARCH_HISTORY_LENGTH);
localStorage.setItem(FAVORITE_SEARCH_LOCAL_STORAGE.searchHistory, JSON.stringify(searchHistory));
}
/**
* @param {String} direction
*/
function traverseFavoritesSearchHistory(direction) {
if (searchHistory.length > 0) {
if (direction === "ArrowUp") {
searchHistoryIndex = Math.min(searchHistoryIndex + 1, searchHistory.length - 1);
} else {
searchHistoryIndex = Math.max(searchHistoryIndex - 1, -1);
}
if (searchHistoryIndex === -1) {
FAVORITE_SEARCH_INPUTS.searchBox.value = lastSearchQuery;
} else {
FAVORITE_SEARCH_INPUTS.searchBox.value = searchHistory[searchHistoryIndex];
}
}
}
function toggleFavoritesOptions(value) {
for (const option of FAVORITE_OPTIONS) {
option.style.display = value ? "block" : "none";
}
}
function changeColumnCount(count) {
count = clamp(parseInt(count), 4, 20);
injectStyleHTML(`
#content {
grid-template-columns: repeat(${count}, 1fr) !important;
}
`, "columnCount");
FAVORITE_SEARCH_INPUTS.columnCount.value = count;
setPreference(FAVORITE_SEARCH_PREFERENCES.columnCount, count);
}
/**
* @param {Boolean} value
*/
function toggleUI(value) {
const favoritesTopBar = document.getElementById("favorites-top-bar");
const favoritesTopBarPanels = document.getElementById("favorites-top-bar-panels");
const header = document.getElementById("header");
const showUIContainer = document.getElementById("show-ui-container");
const showUIDiv = document.getElementById("show-ui-div");
if (value) {
header.style.display = "";
showUIContainer.appendChild(showUIDiv);
favoritesTopBarPanels.style.display = "flex";
} else {
favoritesTopBar.appendChild(showUIDiv);
header.style.display = "none";
favoritesTopBarPanels.style.display = "none";
}
setPreference(FAVORITE_SEARCH_PREFERENCES.showUI, value);
}
async function findSomeoneWithMoreThanXFavorites(X) {
const alreadyCheckedUserIds = {
"2": null
};
const commentsAPIURL = "https://api.rule34.xxx/index.php?page=dapi&s=comment&q=index&post_id=";
for (const thumb of getAllThumbs()) {
const user = await fetch(commentsAPIURL + thumb.id)
.then((response) => {
return response.text();
})
.then(async(html) => {
let userWithMostFavorites = 2;
let mostFavoritesSeen = -1;
const dom1 = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
const userIds = Array.from(dom1.getElementsByTagName("comment")).map(comment => comment.getAttribute("creator_id"));
for (const userId of userIds) {
if (alreadyCheckedUserIds[userId] !== undefined) {
break;
}
alreadyCheckedUserIds[userId] = null;
const favoritesCount = await fetch(`https://rule34.xxx/index.php?page=account&s=profile&id=${userId}`)
.then((response) => {
return response.text();
})
.then((responseHTML) => {
const dom2 = new DOMParser().parseFromString(`<div>${responseHTML}</div>`, "text/html");
const tableElement = dom2.querySelector("table");
if (tableElement) {
const rows = tableElement.querySelectorAll("tr");
const targetItem = "Favorites";
for (const row of rows) {
const cells = row.querySelectorAll("td");
if (cells.length >= 2 && cells[0].textContent.trim() === targetItem) {
return parseInt(cells[1].textContent.trim());
}
}
}
return 0;
});
if (favoritesCount > mostFavoritesSeen) {
mostFavoritesSeen = favoritesCount;
userWithMostFavorites = userId;
}
}
return {
id: userWithMostFavorites,
count: mostFavoritesSeen
};
});
if (user.count > X) {
alert(`https://rule34.xxx/index.php?page=account&s=profile&id=${user.id}`);
return;
}
}
alert(`Could not find user with more than ${X} favorites`);
}
if (!onPostPage()) {
initializeFavoritesPage();
}
// render.js
const renderHTML = `<style>
body {
width: 99.5vw;
overflow-x: hidden;
}
/* .thumb,
.thumb-node {
&.loaded {
.image {
outline: 2px solid transparent;
animation: outlineGlow 1s forwards;
opacity: 1;
}
}
.image {
opacity: 0.4;
transition: transform 0.1s ease-in-out, opacity 0.5s ease;
}
}
.image.loaded {
animation: outlineGlow 1s forwards;
opacity: 1;
}
@keyframes outlineGlow {
0% {
outline-color: transparent;
}
100% {
outline-color: turquoise;
}
} */
/* .image {
outline: 2px solid slategrey;
} */
.gif {
outline: 2px solid hotpink;
}
.focused {
transition: none;
float: left;
overflow: hidden;
z-index: 9997;
pointer-events: none;
position: fixed;
height: 100vh;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#fullscreen-canvas {
float: left;
overflow: hidden;
z-index: 9998;
pointer-events: none;
position: fixed;
height: 100vh;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.thumb-node.selected,
.thumb.selected {
>a>img,
>span>img,
>div>img {
outline: 3px solid yellow !important;
}
}
a.hide {
cursor: default;
}
option {
font-size: 15px;
}
#resolution-dropdown {
text-align: center;
width: 160px;
height: 25px;
cursor: pointer;
}
.thumb-node,
.thumb {
>div,
>a {
>canvas {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 1;
}
}
}
</style>`;/* eslint-disable no-useless-escape */
class Renderer {
static clickCodes = {
leftClick: 0,
middleClick: 1
};
static galleryDirections = {
d: "d",
a: "a",
right: "ArrowRight",
left: "ArrowLeft"
};
static galleryTraversalCooldown = {
timeout: null,
waitTime: 200,
get ready() {
if (this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
}, this.waitTime);
return true;
}
return false;
}
};
static icons = {
openEye: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"white\" class=\"w-6 h-6\"> <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z\" /><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" /></svg>",
closedEye: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"white\" class=\"w-6 h-6\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88\" /></svg>",
openLock: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"white\" class=\"w-6 h-6\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z\" /></svg>",
closedLock: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"white\" class=\"w-6 h-6\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z\" /></svg>"
};
static preferences = {
showOnHover: "showImagesWhenHovering",
backgroundOpacity: "galleryBackgroundOpacity",
resolution: "galleryResolution"
};
static localStorageKeys = {
imageExtensions: "imageExtensions"
};
static webWorkers = {
imageFetcher:
`
/* eslint-disable prefer-template */
const RETRY_DELAY_INCREMENT = 100;
let retryDelay = 0;
/**
* @param {String} imageURL
* @param {String} extension
* @param {String} postId
* @param {Number} thumbIndex
*/
async function getImageBitmap(imageURL, extension, postId, thumbIndex) {
const extensionAlreadyFound = extension !== null && extension !== undefined;
let newExtension = extension;
if (extensionAlreadyFound) {
imageURL = imageURL.replace("jpg", extension);
} else {
imageURL = await getOriginalImageURL(postId);
newExtension = getExtensionFromImageURL(imageURL);
}
const result = await fetchImage(imageURL);
if (result) {
const imageBitmap = await createImageBitmap(result.blob);
setTimeout(() => {
postMessage({
newExtension,
postId,
thumbIndex,
extensionAlreadyFound,
imageBitmap
});
}, 50);
}
}
/**
* @param {Number} milliseconds
* @returns {Promise}
*/
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
/**
* @param {String} postId
* @returns {String}
*/
function getOriginalImageURLFromPostPage(postId) {
const postPageURL = "https://rule34.xxx/index.php?page=post&s=view&id=" + postId;
return fetch(postPageURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status + ": " + postPageURL);
})
.then((html) => {
return (/itemprop="image" content="(.*)"/g).exec(html)[1].replace("us.rule34", "rule34");
}).catch(async(error) => {
if (!error.message.includes("503")) {
console.error(error);
return "https://rule34.xxx/images/r34chibi.png";
}
await sleep(retryDelay);
retryDelay += RETRY_DELAY_INCREMENT;
if (retryDelay > RETRY_DELAY_INCREMENT * 5) {
retryDelay = RETRY_DELAY_INCREMENT;
}
return getOriginalImageURLFromPostPage(postPageURL);
});
}
/**
* @param {String} postId
* @returns {String}
*/
function getOriginalImageURL(postId) {
const apiURL = "https://api.rule34.xxx//index.php?page=dapi&s=post&q=index&id=" + postId;
return fetch(apiURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status + ": " + postId);
})
.then((html) => {
return (/ file_url="(.*?)"/).exec(html)[1].replace("api-cdn.", "");
}).catch(() => {
return getOriginalImageURLFromPostPage(postId);
});
}
/**
*
* @param {String} imageURL
* @returns {{url: String, blob: Blob} | {url: String, error: String}}
*/
async function fetchImage(imageURL) {
const response = await fetch(imageURL);
if (response.ok) {
const blob = await response.blob();
return {
url: imageURL,
blob
};
}
return {
url: imageURL,
error: response.statusText
};
}
/**
* @param {String} imageURL
* @returns {String}
*/
function getExtensionFromImageURL(imageURL) {
return (/\.(png|jpg|jpeg|gif)/g).exec(imageURL)[1];
}
/**
* @param {String} postId
* @returns {String}
*/
async function getImageExtensionFromPostId(postId) {
const imageURL = await getOriginalImageURL(postId);
return getExtensionFromImageURL(imageURL);
}
onmessage = async(message) => {
const request = message.data;
if (request.findExtension) {
const extension = await getImageExtensionFromPostId(request.postId);
postMessage({
foundExtension: extension,
postId: request.postId
});
} else {
await getImageBitmap(request.imageURL, request.extension, request.postId, request.thumbIndex);
}
};
`,
thumbnailRenderer:
`
/**
* @type {Map.<String, OffscreenCanvas>}
*/
const OFFSCREEN_CANVASES = new Map();
let screenWidth = 1080;
/**
* @param {OffscreenCanvas} offscreenCanvas
* @param {ImageBitmap} imageBitmap
* @param {String} id
* @param {Number} maxResolutionFraction
*/
function draw(offscreenCanvas, imageBitmap, id, maxResolutionFraction) {
OFFSCREEN_CANVASES.set(id, offscreenCanvas);
setOffscreenCanvasDimensions(offscreenCanvas, imageBitmap, maxResolutionFraction);
drawOffscreenCanvas(offscreenCanvas, imageBitmap);
}
/**
* @param {OffscreenCanvas} offscreenCanvas
* @param {ImageBitmap} imageBitmap
* @param {Number} maxResolutionFraction
*/
function setOffscreenCanvasDimensions(offscreenCanvas, imageBitmap, maxResolutionFraction) {
const newWidth = screenWidth / maxResolutionFraction;
const ratio = newWidth / imageBitmap.width;
const newHeight = ratio * imageBitmap.height;
offscreenCanvas.width = newWidth;
offscreenCanvas.height = newHeight;
}
/**
* @param {OffscreenCanvas} offscreenCanvas
* @param {ImageBitmap} imageBitmap
*/
function drawOffscreenCanvas(offscreenCanvas, imageBitmap) {
const context = offscreenCanvas.getContext("2d");
const ratio = Math.min(offscreenCanvas.width / imageBitmap.width, offscreenCanvas.height / imageBitmap.height);
const centerShiftX = (offscreenCanvas.width - (imageBitmap.width * ratio)) / 2;
const centerShiftY = (offscreenCanvas.height - (imageBitmap.height * ratio)) / 2;
context.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
context.drawImage(
imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height,
centerShiftX, centerShiftY, imageBitmap.width * ratio, imageBitmap.height * ratio
);
imageBitmap.close();
}
onmessage = (message) => {
message = message.data;
switch (message.action) {
case "draw":
draw(message.offscreenCanvas, message.imageBitmap, message.id, message.maxResolutionFraction);
break;
case "setScreenWidth":
screenWidth = message.screenWidth;
break;
case "delete":
break;
default:
break;
}
};
`
};
static defaultResolutions = {
postPage: "7680x4320",
favoritesPage: "7680x4320"
};
static attributes = {
thumbIndex: "index"
};
static extensionDecodings = {
0: "jpg",
1: "png",
2: "jpeg",
3: "gif"
};
static extensionEncodings = {
"jpg": 0,
"png": 1,
"jpeg": 2,
"gif": 3
};
/**
* @type {HTMLCanvasElement}
*/
fullscreenCanvas;
/**
* @type {CanvasRenderingContext2D}
*/
fullscreenContext;
/**
* @type {Map.<String, ImageBitmap>}
*/
imageBitmaps;
/**
* @type {HTMLImageElement}
*/
fullscreenCanvasPlaceholder;
/**
* @type {Worker[]}
*/
imageBitmapFetchers;
/**
* @type {Worker}
*/
thumbUpscaler;
/**
* @type {Set}
*/
upscaledThumbs;
/**
* @type {Object[]}
*/
upscaleRequests;
/**
* @type {Boolean}
*/
currentlyUpscaling;
/**
* @type {{minIndex: Number, maxIndex: Number}}
*/
renderedThumbRange;
/**
* @type {HTMLElement[]}
*/
visibleThumbs;
/**
* @type {Object.<Number, String>}
*/
imageExtensions;
/**
* @type {Number}
*/
imageFetchDelay;
/**
* @type {Number}
*/
extensionAlreadyKnownFetchSpeed;
/**
* @type {Number}
*/
recentlyDiscoveredImageExtensionCount;
/**
* @type {Number}
*/
currentlySelectedThumbIndex;
/**
* @type {Number}
*/
imageBitmapFetcherIndex;
/**
* @type {Number}
*/
lastSelectedThumbIndexBeforeEnteringGalleryMode;
/**
* @type {Boolean}
*/
inGallery;
/**
* @type {Boolean}
*/
recentlyExitedGalleryMode;
/**
* @type {Boolean}
*/
stopRendering;
/**
* @type {Boolean}
*/
currentlyRendering;
/**
* @type {Boolean}
*/
finishedLoading;
/**
* @type {Number}
*/
maxNumberOfImagesToRender;
/**
* @type {Boolean}
*/
showOriginalContentOnHover;
/**
* @type {HTMLVideoElement}
*/
videoContainer;
/**
* @type {HTMLImageElement}
*/
gifContainer;
/**
* @type {HTMLDivElement}
*/
background;
constructor() {
this.initializeFields();
this.createWebWorkers();
this.createFullscreenCanvasImagePlaceholder();
this.createVideoBackground();
this.setFullscreenCanvasResolution();
this.addEventListeners();
this.loadDiscoveredImageExtensions();
this.preparePostPage();
this.injectHTML();
this.updateBackgroundOpacity(getPreference(Renderer.preferences.backgroundOpacity, 1));
}
initializeFields() {
this.fullscreenCanvas = document.createElement("canvas");
this.fullscreenContext = this.fullscreenCanvas.getContext("2d");
this.imageBitmaps = new Map();
this.renderedThumbRange = {
minIndex: 0,
maxIndex: 0
};
this.visibleThumbs = [];
this.imageExtensions = {};
this.upscaledThumbs = new Set();
this.upscaleRequests = [];
this.currentlyUpscaling = false;
this.imageFetchDelay = 200;
this.extensionAlreadyKnownFetchSpeed = 8;
this.recentlyDiscoveredImageExtensionCount = 0;
this.currentlySelectedThumbIndex = 0;
this.imageBitmapFetcherIndex = 0;
this.lastSelectedThumbIndexBeforeEnteringGalleryMode = 0;
this.inGallery = false;
this.recentlyExitedGalleryMode = false;
this.stopRendering = false;
this.currentlyRendering = false;
this.finishedLoading = onPostPage();
this.showOriginalContentOnHover = window.location.href.includes("favorites") ? getPreference(Renderer.preferences.showOnHover, true) : false;
}
createWebWorkers() {
this.imageBitmapFetchers = [];
this.thumbUpscaler = new Worker(getWorkerURL(Renderer.webWorkers.thumbnailRenderer));
this.thumbUpscaler.postMessage({
action: "setScreenWidth",
screenWidth: window.screen.width
});
for (let i = 0; i < 1; i += 1) {
this.imageBitmapFetchers.push(new Worker(getWorkerURL(Renderer.webWorkers.imageFetcher)));
}
}
injectHTML() {
this.injectStyleHTML();
this.injectOptionsHTML();
this.injectOriginalContentContainerHTML();
}
injectStyleHTML() {
injectStyleHTML(renderHTML);
}
injectOptionsHTML() {
addOptionToFavoritesPage(
Renderer.preferences.showOnHover,
"Enlarge On Hover",
"View full resolution images/play videos when hovering over any thumbnail (Middle mouse click)",
this.showOriginalContentOnHover, (element) => {
setPreference(Renderer.preferences.showOnHover, element.target.checked);
this.toggleAllVisibility();
},
true
);
this.injectImageResolutionOptionsHTML();
}
injectOriginalContentContainerHTML() {
const originalContentContainerHTML = `
<div id="original-content-container">
<video id="original-video-container" width="90%" height="90%" autoplay muted loop style="display: none; top:5%; left:5%; position:fixed; z-index:9998;pointer-events:none;">
</video>
<img id="original-gif-container" class="focused"></img>
<div id="original-content-background" style="position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background: black; z-index: 999; display: none; pointer-events: none;"></div>
</div>
`;
document.body.insertAdjacentHTML("afterbegin", originalContentContainerHTML);
const originalContentContainer = document.getElementById("original-content-container");
originalContentContainer.insertBefore(this.fullscreenCanvas, originalContentContainer.firstChild);
this.background = document.getElementById("original-content-background");
this.videoContainer = document.getElementById("original-video-container");
this.gifContainer = document.getElementById("original-gif-container");
this.fullscreenCanvas.id = "fullscreen-canvas";
this.toggleOriginalContentVisibility(this.showOriginalContentOnHover);
}
addEventListeners() {
document.addEventListener("mousedown", (event) => {
let thumb;
switch (event.button) {
case Renderer.clickCodes.leftClick:
if (this.inGallery) {
if (isVideo(this.getSelectedThumb())) {
return;
}
this.exitGallery();
this.toggleAllVisibility(false);
return;
}
thumb = getThumbUnderCursor();
if (thumb === null) {
return;
}
this.toggleAllVisibility(true);
this.showOriginalContent(thumb);
this.enterGallery();
break;
case Renderer.clickCodes.middleClick:
event.preventDefault();
if (hoveringOverThumb() || this.inGallery) {
this.openPostInNewPage();
} else if (!this.inGallery) {
this.toggleAllVisibility();
}
break;
default:
break;
}
});
window.addEventListener("auxclick", (event) => {
if (event.button === Renderer.clickCodes.middleClick) {
event.preventDefault();
}
});
document.addEventListener("wheel", (event) => {
if (this.inGallery) {
const delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
const direction = delta > 0 ? Renderer.galleryDirections.left : Renderer.galleryDirections.right;
this.traverseGallery.bind(this)(direction, false);
} else if (hoveringOverThumb() && this.showOriginalContentOnHover) {
let opacity = parseFloat(getPreference(Renderer.preferences.backgroundOpacity, 1));
opacity -= event.deltaY * 0.0005;
opacity = clamp(opacity, "0", "1");
this.updateBackgroundOpacity(opacity);
}
}, {
passive: true
});
document.addEventListener("contextmenu", (event) => {
if (this.inGallery) {
event.preventDefault();
this.exitGallery();
}
});
document.addEventListener("keydown", (event) => {
if (this.inGallery) {
switch (event.key) {
case Renderer.galleryDirections.a:
case Renderer.galleryDirections.d:
case Renderer.galleryDirections.left:
case Renderer.galleryDirections.right:
event.preventDefault();
this.traverseGallery(event.key, event.repeat);
break;
case "X":
case "x":
this.unFavoriteSelectedContent();
break;
case "M":
case "m":
if (isVideo(this.getSelectedThumb())) {
this.videoContainer.muted = !this.videoContainer.muted;
}
break;
case "Escape":
this.exitGallery();
this.toggleAllVisibility(false);
break;
default:
break;
}
}
});
window.addEventListener("load", () => {
if (onPostPage()) {
this.initializeThumbsForHovering.bind(this)();
this.enumerateVisibleThumbs();
}
this.hideCaptionsWhenShowingOriginalContent();
}, {
once: true,
passive: true
});
window.addEventListener("favoritesFetched", (event) => {
this.initializeThumbsForHovering.bind(this)(event.detail);
this.enumerateVisibleThumbs();
});
window.addEventListener("newFavoritesFetchedOnReload", (event) => {
this.initializeThumbsForHovering.bind(this)(event.detail);
this.enumerateVisibleThumbs();
if (event.detail.length > 0) {
const thumb = event.detail[0];
this.upscaleAnimatedVisibleThumbsAround(thumb);
this.renderImagesAround(thumb);
}
});
window.addEventListener("startedFetchingFavorites", () => {
setTimeout(() => {
const thumb = document.querySelector(".thumb-node");
if (thumb !== null && !this.finishedLoading) {
this.renderImagesAround(thumb, 10);
this.upscaleAnimatedVisibleThumbsAround(thumb);
}
}, 650);
}, {
once: true
});
window.addEventListener("favoritesLoaded", () => {
this.finishedLoading = true;
this.initializeThumbsForHovering.bind(this)();
this.enumerateVisibleThumbs();
this.renderImagesInTheBackground();
this.assignImageExtensionsInTheBackground();
});
window.addEventListener("finishedSearching", () => {
this.enumerateVisibleThumbs();
this.onFavoritesSearch();
});
window.addEventListener("shuffle", async() => {
this.enumerateVisibleThumbs();
await this.deleteAllRenders();
this.renderImagesInTheBackground();
});
this.imageBitmapFetchers.forEach((renderer) => {
renderer.onmessage = (message) => {
this.handleRendererMessage(message.data);
};
});
if (this.thumbUpscaler !== undefined) {
this.thumbUpscaler.onmessage = (message) => {
message = message.data;
switch (message.action) {
default:
break;
}
};
}
}
/**
* @param {HTMLElement[]} thumbs
*/
initializeThumbsForHovering(thumbs) {
const thumbElements = thumbs === undefined ? getAllThumbs() : thumbs;
for (const thumbElement of thumbElements) {
this.addEventListenersToThumb(thumbElement);
}
}
/**
* @param {Object} message
*/
handleRendererMessage(message) {
if (message.foundExtension) {
this.assignExtension(message.postId, message.foundExtension);
return;
}
this.onRenderFinished(message);
}
/**
* @param {Object} message
*/
onRenderFinished(message) {
this.deleteOldestRender();
const thumb = document.getElementById(message.postId);
this.imageBitmaps.set(message.postId, message.imageBitmap);
if (thumb === null) {
return;
}
thumb.classList.add("loaded");
this.upscaleThumbResolution(thumb, message.imageBitmap, 4);
if (message.extension === "gif") {
getImageFromThumb(thumb).setAttribute("gif", true);
return;
}
if (!message.extensionAlreadyFound) {
this.assignExtension(message.postId, message.newExtension);
}
if (this.inGallery) {
if (this.getSelectedThumb().id === message.postId) {
this.showOriginalContent(thumb);
}
} else if (this.showOriginalContentOnHover) {
const thumbUnderCursor = getThumbUnderCursor();
const hoveringOverSameThumb = (thumbUnderCursor !== null) && thumbUnderCursor.id === message.postId;
if (hoveringOverSameThumb) {
this.showOriginalContent(thumb);
}
}
}
/**
* @param {HTMLElement} thumb
* @param {ImageBitmap} imageBitmap
* @param {Number} maxResolutionFraction
*/
upscaleThumbResolution(thumb, imageBitmap, maxResolutionFraction) {
if (onPostPage() || this.upscaledThumbs.has(thumb.id) || this.thumbUpscaler === undefined) {
return;
}
this.upscaledThumbs.add(thumb.id);
const message = {
action: "draw",
id: thumb.id,
offscreenCanvas: thumb.querySelector("canvas").transferControlToOffscreen(),
imageBitmap,
maxResolutionFraction
};
// this.upscaleRequests.push(message);
// this.dispatchThumbResolutionUpscaleRequests();
this.thumbUpscaler.postMessage(message, [message.offscreenCanvas]);
}
async dispatchThumbResolutionUpscaleRequests() {
if (this.currentlyUpscaling) {
return;
}
this.currentlyUpscaling = true;
while (this.upscaleRequests.length > 0) {
await sleep(25);
const message = this.upscaleRequests.shift();
this.thumbUpscaler.postMessage(message, [message.offscreenCanvas]);
}
this.currentlyUpscaling = false;
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
isUpscaled(thumb) {
return this.upscaledThumbs.has(thumb.id);
}
/**
* @param {String} postId
* @param {String} extension
*/
assignExtension(postId, extension) {
this.setImageExtension(postId, extension);
this.recentlyDiscoveredImageExtensionCount += 1;
if (this.recentlyDiscoveredImageExtensionCount >= 3) {
this.recentlyDiscoveredImageExtensionCount = 0;
if (!onPostPage()) {
localStorage.setItem(Renderer.localStorageKeys.imageExtensions, JSON.stringify(this.imageExtensions));
}
}
}
hideCaptionsWhenShowingOriginalContent() {
for (const caption of document.getElementsByClassName("caption")) {
if (this.showOriginalContentOnHover) {
caption.classList.add("hide");
} else {
caption.classList.remove("hide");
const thumb = getThumbUnderCursor();
if (thumb !== null) {
dispatchEvent(new CustomEvent("showCaption", {
detail: thumb
}));
}
}
}
}
async preparePostPage() {
if (!onPostPage()) {
return;
}
const imageList = document.getElementsByClassName("image-list")[0];
const thumbs = Array.from(imageList.querySelectorAll(".thumb"));
for (const thumb of thumbs) {
removeTitleFromImage(getImageFromThumb(thumb));
assignContentType(thumb);
thumb.id = thumb.id.substring(1);
}
window.addEventListener("unload", () => {
this.deleteAllRenders();
});
window.onblur = () => {
this.deleteAllRenders();
};
await this.findImageExtensionsOnPostPage();
this.renderImagesInTheBackground();
}
async deleteAllRenders() {
await this.pauseRendering(10);
for (const id of this.imageBitmaps.keys()) {
this.deleteRender(id);
}
this.imageBitmaps.clear();
}
deleteRender(id) {
const thumb = document.getElementById(id);
if (thumb !== null) {
thumb.classList.remove("loaded");
}
this.imageBitmaps.get(id).close();
this.imageBitmaps.delete(id);
}
findImageExtensionsOnPostPage() {
const postPageAPIURL = this.getPostPageAPIURL();
return fetch(postPageAPIURL)
.then((response) => {
if (response.ok) {
return response.text();
}
return null;
}).then((html) => {
if (html === null) {
console.error(`Failed to fetch: ${postPageAPIURL}`);
}
const dom = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
const posts = Array.from(dom.getElementsByTagName("post"));
for (const post of posts) {
const originalImageURL = post.getAttribute("file_url");
const isAnImage = getContentType(post.getAttribute("tags")) === "image";
const isBlacklisted = originalImageURL === "https://api-cdn.rule34.xxx/images//";
if (!isAnImage || isBlacklisted) {
continue;
}
const postId = post.getAttribute("id");
const extension = (/\.(png|jpg|jpeg|gif)/g).exec(originalImageURL)[1];
this.assignExtension(postId, extension);
}
});
}
/**
* @returns {String}
*/
getPostPageAPIURL() {
const postsPerPage = 42;
const apiURL = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&limit=${postsPerPage}`;
let blacklistedTags = ` ${negateTags(TAG_BLACKLIST)}`.replace(/\s-/g, "+-");
let pageNumber = (/&pid=(\d+)/).exec(location.href);
let tags = (/&tags=([^&]*)/).exec(location.href);
pageNumber = pageNumber === null ? 0 : Math.floor(parseInt(pageNumber[1]) / postsPerPage);
tags = tags === null ? "" : tags[1];
if (tags === "all") {
tags = "";
blacklistedTags = "";
}
return `${apiURL}&tags=${tags}${blacklistedTags}&pid=${pageNumber}`;
}
enumerateVisibleThumbs() {
this.visibleThumbs = Array.from(getAllVisibleThumbs());
for (let i = 0; i < this.visibleThumbs.length; i += 1) {
this.enumerateThumb(this.visibleThumbs[i], i);
}
this.indexRenderRange();
}
/**
* @param {HTMLElement} thumb
* @param {Number} index
*/
enumerateThumb(thumb, index) {
thumb.setAttribute(Renderer.attributes.thumbIndex, index);
}
/**
* @param {HTMLElement} thumb
*/
addEventListenersToThumb(thumb) {
const image = getImageFromThumb(thumb);
image.onmouseover = () => {
if (this.inGallery || this.recentlyExitedGalleryMode) {
return;
}
this.showOriginalContent(thumb);
};
image.onmouseout = (event) => {
if (this.inGallery || enteredOverCaptionTag(event)) {
return;
}
this.hideOriginalContent(thumb);
};
}
loadDiscoveredImageExtensions() {
this.imageExtensions = JSON.parse(localStorage.getItem(Renderer.localStorageKeys.imageExtensions)) || {};
}
openPostInNewPage() {
const firstChild = this.getSelectedThumb().children[0];
if (firstChild.hasAttribute("href")) {
window.open(firstChild.getAttribute("href"), "_blank");
} else {
firstChild.click();
}
}
unFavoriteSelectedContent() {
const removeLink = getRemoveLinkFromThumb(this.getSelectedThumb());
if (removeLink === null || removeLink.style.visibility === "hidden") {
return;
}
removeLink.click();
}
enterGallery() {
const selectedThumb = this.getSelectedThumb();
this.lastSelectedThumbIndexBeforeEnteringGalleryMode = this.currentlySelectedThumbIndex;
this.background.style.pointerEvents = "auto";
this.highlightThumb(selectedThumb, true);
if (isVideo(selectedThumb)) {
this.toggleCursorVisibility(true);
this.toggleVideoControls(true);
}
this.inGallery = true;
this.showLockIcon();
}
exitGallery() {
this.toggleVideoControls(false);
this.background.style.pointerEvents = "none";
const thumbIndex = this.getIndexOfThumbUnderCursor();
if (thumbIndex !== this.lastSelectedThumbIndexBeforeEnteringGalleryMode) {
this.hideOriginalContent(this.getSelectedThumb());
if (thumbIndex !== null && this.showOriginalContentOnHover) {
this.showOriginalContent(this.visibleThumbs[thumbIndex]);
}
}
this.recentlyExitedGalleryMode = true;
setTimeout(() => {
this.recentlyExitedGalleryMode = false;
}, 300);
this.inGallery = false;
this.showLockIcon();
}
/**
* @param {String} direction
* @param {Boolean} keyIsHeldDown
*/
traverseGallery(direction, keyIsHeldDown) {
if (keyIsHeldDown && !Renderer.galleryTraversalCooldown.ready) {
return;
}
let selectedThumb = this.getSelectedThumb();
this.clearOriginalContentSources();
this.highlightThumb(selectedThumb, false);
this.setNextSelectedThumbIndex(direction);
selectedThumb = this.getSelectedThumb();
this.highlightThumb(selectedThumb, true);
this.renderInAdvanceWhileTraversingInGalleryMode(selectedThumb, direction);
if (!usingFirefox()) {
scrollToThumb(selectedThumb.id, false);
}
if (isVideo(selectedThumb)) {
this.toggleCursorVisibility(true);
this.toggleVideoControls(true);
this.showOriginalVideo(selectedThumb);
} else if (isGif(selectedThumb)) {
this.toggleCursorVisibility(false);
this.toggleVideoControls(false);
this.toggleOriginalVideo(false);
this.showOriginalGIF(selectedThumb);
} else {
this.toggleCursorVisibility(false);
this.toggleVideoControls(false);
this.toggleOriginalVideo(false);
this.showOriginalImage(selectedThumb);
}
}
/**
* @param {String} direction
*/
setNextSelectedThumbIndex(direction) {
if (direction === Renderer.galleryDirections.left || direction === Renderer.galleryDirections.a) {
this.currentlySelectedThumbIndex -= 1;
this.currentlySelectedThumbIndex = this.currentlySelectedThumbIndex < 0 ? this.visibleThumbs.length - 1 : this.currentlySelectedThumbIndex;
} else {
this.currentlySelectedThumbIndex += 1;
this.currentlySelectedThumbIndex = this.currentlySelectedThumbIndex >= this.visibleThumbs.length ? 0 : this.currentlySelectedThumbIndex;
}
}
/**
* @param {Boolean} value
*/
toggleAllVisibility(value) {
this.showOriginalContentOnHover = value === undefined ? !this.showOriginalContentOnHover : value;
this.toggleOriginalContentVisibility();
this.showEyeIcon();
if (hoveringOverThumb()) {
this.toggleBackgroundVisibility();
this.toggleScrollbarVisibility();
}
this.hideCaptionsWhenShowingOriginalContent();
const showOnHoverCheckbox = document.getElementById("showImagesWhenHoveringCheckbox");
if (showOnHoverCheckbox !== null) {
setPreference(Renderer.preferences.showOnHover, this.showOriginalContentOnHover);
showOnHoverCheckbox.checked = this.showOriginalContentOnHover;
}
}
/**
* @param {HTMLElement} thumb
*/
hideOriginalContent(thumb) {
this.highlightThumb(thumb, false);
this.toggleBackgroundVisibility(false);
this.toggleScrollbarVisibility(true);
this.toggleCursorVisibility(true);
this.clearOriginalContentSources();
this.toggleOriginalVideo(false);
this.toggleOriginalGIF(false);
}
clearOriginalContentSources() {
this.clearFullscreenCanvas();
this.videoContainer.src = "";
this.gifContainer.src = "";
}
/**
* @returns {Boolean}
*/
currentlyHoveringOverVideoThumb() {
const thumb = getThumbUnderCursor();
if (thumb === null) {
return false;
}
return isVideo(thumb);
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} value
*/
highlightThumb(thumb, value) {
thumb.classList.toggle("selected", value);
}
/**
* @param {HTMLElement} thumb
*/
showOriginalContent(thumb) {
this.highlightThumb(thumb, true);
this.currentlySelectedThumbIndex = parseInt(thumb.getAttribute(Renderer.attributes.thumbIndex));
const animatedThumbsToUpscale = this.getAdjacentVisibleThumbs(thumb, 20, (_) => {
return true;
}).filter(t => !isImage(t) && !this.isUpscaled(t));
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
if (isVideo(thumb)) {
this.showOriginalVideo(thumb);
} else if (isGif(thumb)) {
this.showOriginalGIF(thumb);
} else {
this.showOriginalImage(thumb);
}
if (this.showOriginalContentOnHover) {
this.toggleCursorVisibility(false);
this.toggleBackgroundVisibility(true);
this.toggleScrollbarVisibility(false);
}
}
/**
* @param {HTMLElement} thumb
*/
showOriginalVideo(thumb) {
if (!this.showOriginalContentOnHover) {
return;
}
this.toggleFullscreenCanvas(false);
this.videoContainer.style.display = "block";
this.playOriginalVideo(thumb);
if (!this.inGallery) {
this.toggleVideoControls(false);
}
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
getVideoSource(thumb) {
return getOriginalImageURLFromThumb(thumb).replace("jpg", "mp4");
}
/**
* @param {HTMLElement} thumb
*/
playOriginalVideo(thumb) {
this.videoContainer.src = this.getVideoSource(thumb);
this.videoContainer.play().catch(() => { });
}
/**
* @param {HTMLElement} thumb
*/
showOriginalGIF(thumb) {
const extension = includesTag("animated_png", getTagsFromThumb(thumb)) ? "png" : "gif";
const originalSource = getOriginalImageURLFromThumb(thumb).replace("jpg", extension);
this.gifContainer.src = originalSource;
if (this.showOriginalContentOnHover) {
this.gifContainer.style.visibility = "visible";
}
}
/**
* @param {HTMLElement} thumb
*/
showOriginalImage(thumb) {
if (this.isNotRendered(thumb)) {
this.renderOriginalImage(thumb);
this.renderImagesAround(thumb);
} else {
this.drawFullscreenCanvas(this.imageBitmaps.get(thumb.id));
}
this.toggleOriginalContentVisibility(this.showOriginalContentOnHover);
}
deleteOldestRender() {
if (this.imageBitmaps.size > this.maxNumberOfImagesToRender) {
const iterator = this.imageBitmaps.keys().next();
if (!iterator.done) {
this.deleteRender(iterator.value);
}
}
}
/**
* @param {HTMLElement} initialThumb
*/
async renderImagesAround(initialThumb) {
if (onPostPage()) {
return;
}
if (this.currentlyRendering) {
if (this.thumbInRenderRange(initialThumb)) {
return;
}
await this.pauseRendering(this.imageFetchDelay);
}
this.currentlyRendering = true;
const amountToRender = Math.ceil(this.maxNumberOfImagesToRender / 6);
const imageThumbsToRender = this.getAdjacentVisibleThumbs(initialThumb, amountToRender, (thumb) => {
return isImage(thumb) && this.isNotRendered(thumb);
});
const indicesOfImageThumbsToRender = imageThumbsToRender.map(imageThumb => parseInt(imageThumb.getAttribute(Renderer.attributes.thumbIndex)));
this.setRenderRange(indicesOfImageThumbsToRender);
await this.renderImages(imageThumbsToRender);
this.currentlyRendering = false;
}
/**
* @param {HTMLElement} initialThumb
* @param {Number} limit
* @param {Function} additionalQualifier
* @returns {HTMLElement[]}
*/
getAdjacentVisibleThumbs(initialThumb, limit, additionalQualifier) {
const adjacentVisibleThumbs = [];
let currentThumb = initialThumb;
let previousThumb = initialThumb;
let nextThumb = initialThumb;
let traverseForward = true;
while (currentThumb !== null && adjacentVisibleThumbs.length < limit) {
if (traverseForward) {
nextThumb = this.getAdjacentVisibleThumb(nextThumb, true);
} else {
previousThumb = this.getAdjacentVisibleThumb(previousThumb, false);
}
if (previousThumb === null) {
traverseForward = true;
} else if (nextThumb === null) {
traverseForward = false;
} else {
traverseForward = !traverseForward;
}
currentThumb = traverseForward ? nextThumb : previousThumb;
if (currentThumb !== null) {
if (this.isVisible(currentThumb) && additionalQualifier(currentThumb)) {
adjacentVisibleThumbs.push(currentThumb);
}
}
}
return adjacentVisibleThumbs;
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} traverseForward
* @returns {HTMLElement}
*/
getAdjacentVisibleThumb(thumb, traverseForward) {
let adjacentThumb = this.getAdjacentThumb(thumb, traverseForward);
while (adjacentThumb !== null && !this.isVisible(adjacentThumb)) {
adjacentThumb = this.getAdjacentThumb(adjacentThumb, traverseForward);
}
return adjacentThumb;
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} traverseForward
* @returns {HTMLElement}
*/
getAdjacentThumb(thumb, traverseForward) {
return traverseForward ? thumb.nextElementSibling : thumb.previousElementSibling;
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
isVisible(thumb) {
return thumb.style.display !== "none";
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
isNotRendered(thumb) {
return this.imageBitmaps.get(thumb.id) === undefined;
}
/**
* @param {HTMLElement} thumb
*/
renderOriginalImage(thumb) {
const renderMessage = {
imageURL: getOriginalImageURLFromThumb(thumb),
postId: thumb.id,
thumbIndex: thumb.getAttribute(Renderer.attributes.thumbIndex),
extension: this.getImageExtension(thumb.id)
};
this.imageBitmapFetchers[this.imageBitmapFetcherIndex].postMessage(renderMessage);
this.imageBitmapFetcherIndex += 1;
this.imageBitmapFetcherIndex = this.imageBitmapFetcherIndex < this.imageBitmapFetchers.length ? this.imageBitmapFetcherIndex : 0;
const image = getImageFromThumb(thumb);
if (!imageIsLoaded(image)) {
return;
}
createImageBitmap(image)
.then((imageBitmap) => {
if (this.imageBitmaps.get(thumb.id) === undefined) {
this.imageBitmaps.set(thumb.id, imageBitmap);
}
});
}
/**
* @param {Boolean} value
*/
toggleOriginalContentVisibility(value) {
this.toggleFullscreenCanvas(value);
this.toggleOriginalGIF(value);
if (!value) {
this.toggleOriginalVideo(false);
}
}
/**
* @param {Boolean} value
*/
toggleBackgroundVisibility(value) {
if (value === undefined) {
this.background.style.display = this.background.style.display === "block" ? "none" : "block";
return;
}
this.background.style.display = value ? "block" : "none";
}
/**
* @param {Boolean} value
*/
toggleScrollbarVisibility(value) {
if (value === undefined) {
document.body.style.overflowY = document.body.style.overflowY === "auto" ? "hidden" : "auto";
return;
}
document.body.style.overflowY = value ? "auto" : "hidden";
}
/**
* @param {Boolean} value
*/
toggleCursorVisibility(value) {
// const image = getImageFromThumb(this.getSelectedThumb());
// if (value === undefined) {
// image.style.cursor = image.style.cursor === "pointer" ? "none" : "pointer";
// return;
// }
// if (value) {
// image.style.cursor = "pointer";
// document.body.style.cursor = "pointer";
// } else {
// image.style.cursor = "none";
// document.body.style.cursor = "none";
// }
}
/**
* @param {Boolean} value
*/
toggleVideoControls(value) {
if (value === undefined) {
this.videoContainer.style.pointerEvents = this.videoContainer.style.pointerEvents === "auto" ? "none" : "auto";
this.videoContainer.style.controls = this.videoContainer.style.controls === "controls" ? false : "controls";
} else {
this.videoContainer.style.pointerEvents = value ? "auto" : "none";
this.videoContainer.controls = value ? "controls" : false;
}
}
/**
* @param {Boolean} value
*/
toggleFullscreenCanvas(value) {
if (value === undefined) {
this.fullscreenCanvas.style.visibility = this.fullscreenCanvas.style.visibility === "visible" ? "hidden" : "visible";
} else {
this.fullscreenCanvas.style.visibility = value ? "visible" : "hidden";
}
}
/**
* @param {Boolean} value
*/
toggleOriginalVideo(value) {
if (value !== undefined && this.videoContainer.src !== "") {
this.videoContainer.style.display = value ? "block" : "none";
return;
}
if (!this.currentlyHoveringOverVideoThumb() || this.videoContainer.style.display === "block") {
this.videoContainer.style.display = "none";
} else {
this.videoContainer.style.display = "block";
}
}
/**
* @param {Boolean} value
*/
toggleOriginalGIF(value) {
if (value === undefined) {
this.gifContainer.style.visibility = this.gifContainer.style.visibility === "visible" ? "hidden" : "visible";
} else {
this.gifContainer.style.visibility = value ? "visible" : "hidden";
}
}
/**
* @param {Number} opacity
*/
updateBackgroundOpacity(opacity) {
this.background.style.opacity = opacity;
setPreference(Renderer.preferences.backgroundOpacity, opacity);
}
/**
* @returns {Number}
*/
getIndexOfThumbUnderCursor() {
const thumb = getThumbUnderCursor();
return thumb === null ? null : parseInt(thumb.getAttribute(Renderer.attributes.thumbIndex));
}
/**
* @returns {HTMLElement}
*/
getSelectedThumb() {
return this.visibleThumbs[this.currentlySelectedThumbIndex];
}
/**
* @param {ImageBitmap} imageBitmap
*/
drawFullscreenCanvas(imageBitmap) {
const ratio = Math.min(this.fullscreenCanvas.width / imageBitmap.width, this.fullscreenCanvas.height / imageBitmap.height);
const centerShiftX = (this.fullscreenCanvas.width - (imageBitmap.width * ratio)) / 2;
const centerShiftY = (this.fullscreenCanvas.height - (imageBitmap.height * ratio)) / 2;
this.fullscreenContext.clearRect(0, 0, this.fullscreenCanvas.width, this.fullscreenCanvas.height);
this.fullscreenContext.drawImage(
imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height,
centerShiftX, centerShiftY, imageBitmap.width * ratio, imageBitmap.height * ratio
);
}
clearFullscreenCanvas() {
this.fullscreenContext.clearRect(0, 0, this.fullscreenCanvas.width, this.fullscreenCanvas.height);
}
showEyeIcon() {
const eyeIcon = document.getElementById("svg-eye");
const svg = this.showOriginalContentOnHover ? Renderer.icons.openEye : Renderer.icons.closedEye;
if (eyeIcon) {
eyeIcon.remove();
}
showOverlayingIcon(svg, "svg-eye", 100, 100, "bottom-right");
}
showLockIcon() {
const lockIcon = document.getElementById("svg-lock");
const svg = this.inGallery ? Renderer.icons.closedLock : Renderer.icons.openLock;
if (lockIcon) {
lockIcon.remove();
}
showOverlayingIcon(svg, "svg-lock", 100, 100, "bottom-left");
}
/**
* @returns {HTMLElement[]}
*/
getVisibleUnrenderedImageThumbs() {
let thumbs = Array.from(getAllVisibleThumbs()).filter((thumb) => {
return isImage(thumb) && this.isNotRendered(thumb);
});
if (onPostPage()) {
thumbs = thumbs.filter(thumb => !thumb.classList.contains("blacklisted-image"));
}
return thumbs;
}
async onFavoritesSearch() {
this.deleteRendersNotIncludedInNewSearch();
await this.pauseRendering(50);
this.renderImagesInTheBackground();
}
deleteRendersNotIncludedInNewSearch() {
for (const id of this.imageBitmaps.keys()) {
const thumb = document.getElementById(id);
if (thumb !== null && !this.isVisible(thumb)) {
this.deleteRender(thumb.id);
}
}
}
async renderImagesInTheBackground() {
if (this.currentlyRendering) {
return;
}
this.currentlyRendering = true;
const unrenderedImageThumbs = this.getVisibleUnrenderedImageThumbs();
const imageThumbsToRender = [];
const imagesAlreadyRenderedCount = this.imageBitmaps.size;
const animatedThumbsToUpscale = Array.from(getAllVisibleThumbs())
.slice(0, this.maxNumberOfImagesToRender / 2)
.filter(thumb => !isImage(thumb) && !this.isUpscaled(thumb));
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
for (let i = 0; i < unrenderedImageThumbs.length && i + imagesAlreadyRenderedCount < this.maxNumberOfImagesToRender; i += 1) {
imageThumbsToRender.push(unrenderedImageThumbs[i]);
}
if (imageThumbsToRender.length > 0) {
this.renderedThumbRange.minIndex = imageThumbsToRender[0].getAttribute(Renderer.attributes.thumbIndex);
this.renderedThumbRange.maxIndex = imageThumbsToRender[imageThumbsToRender.length - 1].getAttribute(Renderer.attributes.thumbIndex);
}
await this.renderImages(imageThumbsToRender);
this.currentlyRendering = false;
}
/**
* @param {HTMLElement[]} imagesToRender
*/
async renderImages(imagesToRender) {
for (const thumb of imagesToRender) {
if (this.stopRendering) {
break;
}
this.renderOriginalImage(thumb);
await sleep(this.getImageFetchDelay(thumb.id));
}
}
/**
* @param {HTMLElement} animatedThumbs
*/
async upscaleAnimatedThumbs(animatedThumbs) {
for (const thumb of animatedThumbs) {
if (this.isUpscaled(thumb)) {
continue;
}
const image = getImageFromThumb(thumb);
let newImage = new Image();
let source = getOriginalImageURL(image.src);
if (isGif(thumb)) {
source = source.replace("jpg", "gif");
}
newImage.src = source;
newImage.onload = () => {
createImageBitmap(newImage)
.then((imageBitmap) => {
this.upscaleThumbResolution(thumb, imageBitmap, 5.5);
newImage = null;
});
};
await sleep(this.imageFetchDelay);
}
}
/**
* @param {String} postId
* @returns {Number}
*/
getImageFetchDelay(postId) {
return this.extensionIsKnown(postId) ? this.imageFetchDelay / this.extensionAlreadyKnownFetchSpeed : this.imageFetchDelay;
}
/**
*
* @param {String} postId
* @returns {Boolean}
*/
extensionIsKnown(postId) {
return this.getImageExtension(postId) !== undefined;
}
/**
* @returns {Number}
*/
getMaxNumberOfImagesToRender() {
const availableMemory = 1200;
const averageImageSize = 20;
const maxImagesToRender = Math.floor(availableMemory / averageImageSize);
return maxImagesToRender;
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
thumbInRenderRange(thumb) {
const index = parseInt(thumb.getAttribute(Renderer.attributes.thumbIndex));
return index >= this.renderedThumbRange.minIndex && index <= this.renderedThumbRange.maxIndex;
}
injectImageResolutionOptionsHTML() {
const additionalFavoriteOptions = document.getElementById("additional-favorite-options");
if (additionalFavoriteOptions === null) {
return;
}
const scale = 40;
const width = 16 * scale;
const height = 9 * scale;
const defaultResolution = getPreference(Renderer.preferences.resolution, Renderer.defaultResolutions.favoritesPage);
const container = document.createElement("div");
container.style.paddingTop = "8px";
const resolutionLabel = document.createElement("label");
const resolutionDropdown = document.createElement("select");
resolutionLabel.textContent = "Image Resolution";
resolutionDropdown.id = "resolution-dropdown";
for (let i = 1; i <= 7680 / width; i += 1) {
const resolution = `${i * width}x${i * height}`;
const resolutionOption = document.createElement("option");
if (resolution === defaultResolution) {
resolutionOption.selected = "selected";
}
resolutionOption.textContent = resolution;
resolutionDropdown.appendChild(resolutionOption);
}
resolutionDropdown.onchange = () => {
setPreference(Renderer.preferences.resolution, resolutionDropdown.value);
this.setFullscreenCanvasResolution();
};
container.appendChild(resolutionLabel);
container.appendChild(document.createElement("br"));
container.appendChild(resolutionDropdown);
additionalFavoriteOptions.insertAdjacentElement("afterbegin", container);
container.style.display = "none";
}
setFullscreenCanvasResolution() {
const resolution = onPostPage() ? Renderer.defaultResolutions.postPage : getPreference(Renderer.preferences.resolution, Renderer.defaultResolutions.favoritesPage);
const dimensions = resolution.split("x").map(dimension => parseFloat(dimension));
this.fullscreenCanvas.width = dimensions[0];
this.fullscreenCanvas.height = dimensions[1];
this.maxNumberOfImagesToRender = this.getMaxNumberOfImagesToRender();
}
/**
* @param {HTMLElement} thumb
* @param {String} direction
* @returns
*/
renderInAdvanceWhileTraversingInGalleryMode(thumb, direction) {
const currentThumbIndex = parseInt(thumb.getAttribute(Renderer.attributes.thumbIndex));
const lookahead = Math.min(12, Math.round(this.maxNumberOfImagesToRender / 2) - 2);
let possiblyUnrenderedThumbIndex;
if (direction === Renderer.galleryDirections.left || direction === Renderer.galleryDirections.a) {
possiblyUnrenderedThumbIndex = currentThumbIndex - lookahead;
} else {
possiblyUnrenderedThumbIndex = currentThumbIndex + lookahead;
}
if (possiblyUnrenderedThumbIndex < 0 || possiblyUnrenderedThumbIndex >= this.visibleThumbs.length) {
return;
}
const possiblyUnrenderedThumb = this.visibleThumbs[possiblyUnrenderedThumbIndex];
if (this.isNotRendered(possiblyUnrenderedThumb)) {
this.upscaleAnimatedVisibleThumbsAround(possiblyUnrenderedThumb);
this.renderImagesAround(possiblyUnrenderedThumb);
}
}
/**
* @param {HTMLElement} thumb
*/
upscaleAnimatedVisibleThumbsAround(thumb) {
const animatedThumbsToUpscale = this.getAdjacentVisibleThumbs(thumb, 10, (t) => {
return !isImage(t) && !this.isUpscaled(t);
});
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
}
/**
* @param {Number[]} indices
*/
setRenderRange(indices) {
indices.sort((a, b) => {
return a - b;
});
this.renderedThumbRange.minIndex = indices[0];
this.renderedThumbRange.maxIndex = indices[indices.length - 1];
}
indexRenderRange() {
if (this.imageBitmaps.size === 0) {
return;
}
const indices = [];
for (const postId of this.imageBitmaps.keys()) {
const thumb = getThumbByPostId(postId);
if (thumb === null) {
break;
}
indices.push(parseInt(thumb.getAttribute(Renderer.attributes.thumbIndex)));
}
this.setRenderRange(indices);
}
/**
* @param {String[]} ids
*/
async assignImageExtensionsInTheBackground(ids) {
const postIdsWithUnknownExtensions = ids === undefined ? this.getPostIdsWithUnknownExtensions() : ids;
while (postIdsWithUnknownExtensions.length > 0) {
await sleep(4000);
while (postIdsWithUnknownExtensions.length > 0 && this.finishedLoading && !this.currentlyRendering) {
const postId = postIdsWithUnknownExtensions.pop();
if (postId !== undefined && postId !== null && !this.extensionIsKnown(postId)) {
this.imageBitmapFetchers[0].postMessage({
findExtension: true,
postId
});
await sleep(10);
}
}
}
}
/**
* @returns {String[]}
*/
getPostIdsWithUnknownExtensions() {
return Array.from(getAllThumbs())
.filter(thumb => isImage(thumb))
.filter(thumb => !this.extensionIsKnown(thumb.id))
.map(thumb => thumb.id);
}
createVideoBackground() {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
this.videoContainer.setAttribute("poster", URL.createObjectURL(blob));
});
}
createFullscreenCanvasImagePlaceholder() {
this.fullscreenCanvasPlaceholder = document.createElement("img");
this.fullscreenCanvasPlaceholder.src = "https://rule34.xxx/images/header2.png";
}
/**
* @param {Number} duration
*/
async pauseRendering(duration) {
this.stopRendering = true;
await sleep(duration);
this.stopRendering = false;
}
/**
* @param {String | Number} postId
* @returns {String}
*/
getImageExtension(postId) {
return Renderer.extensionDecodings[this.imageExtensions[parseInt(postId)]];
}
/**
* @param {String | Number} postId
* @param {String} extension
*/
setImageExtension(postId, extension) {
this.imageExtensions[parseInt(postId)] = Renderer.extensionEncodings[extension];
}
}
const renderer = new Renderer();
// tooltip.js
const tooltipHTML = `<div id="tooltip-container">
<style>
#tooltip {
max-width: 750px;
border: 1px solid black;
padding: 0.25em;
position: absolute;
box-sizing: border-box;
z-index: 25;
pointer-events: none;
visibility: hidden;
opacity: 0;
transition: visibility 0s, opacity 0.25s linear;
font-size: 1.3em;
}
#tooltip.visible {
visibility: visible;
opacity: 1;
}
</style>
<span id="tooltip" class="light-green-gradient"></span>
</div>`;
class Tooltip {
/**
* @type {HTMLDivElement}
*/
tooltip;
/**
* @type {String}
*/
defaultTransition;
/**
* @type {Boolean}
*/
enabled;
/**
* @type {Object.<String,String>}
*/
tagColorCodes;
/**
* @type {HTMLTextAreaElement}
*/
searchBox;
/**
* @type {String}
*/
previousSearch;
constructor() {
this.enabled = getPreference("showTooltip", true);
document.body.insertAdjacentHTML("afterbegin", tooltipHTML);
this.tooltip = document.getElementById("tooltip");
this.defaultTransition = this.tooltip.style.transition;
this.tagColorCodes = {};
this.setTheme();
this.addEventListeners();
this.assignColorsToMatchedTags();
}
addEventListeners() {
if (onPostPage()) {
window.addEventListener("load", () => {
this.addEventListenersToThumbs.bind(this)();
});
} else {
this.addFavoritesOptions();
window.addEventListener("favoritesFetched", (event) => {
this.addEventListenersToThumbs.bind(this)(event.detail);
});
window.addEventListener("favoritesLoaded", () => {
this.addEventListenersToThumbs.bind(this)();
});
window.addEventListener("thumbUnderCursorOnLoad", (event) => {
this.showOnLoadIfHoveringOverThumb(event.detail);
}, {
once: true
});
}
}
setTheme() {
if (usingDarkTheme()) {
this.tooltip.classList.remove("light-green-gradient");
this.tooltip.classList.add("dark-green-gradient");
}
}
assignColorsToMatchedTags() {
if (onPostPage()) {
this.assignColorsToMatchedTagsOnPostPage();
} else {
this.searchBox = document.getElementById("favorites-search-box");
this.assignColorsToMatchedTagsOnFavoritesPage();
this.searchBox.addEventListener("input", () => {
this.assignColorsToMatchedTagsOnFavoritesPage();
});
window.addEventListener("searchStarted", () => {
this.assignColorsToMatchedTagsOnFavoritesPage();
});
}
}
/**
* @param {HTMLCollectionOf.<Element>} thumbs
*/
addEventListenersToThumbs(thumbs) {
thumbs = thumbs === undefined ? getAllThumbs() : thumbs;
for (const thumb of thumbs) {
const image = getImageFromThumb(thumb);
if (image.hasAttribute("hasTooltipListener")) {
return;
}
image.onmouseenter = () => {
if (this.enabled) {
this.show(image);
}
};
image.onmouseleave = (event) => {
if (!enteredOverCaptionTag(event)) {
this.hide();
}
};
image.setAttribute("hasTooltipListener", true);
}
}
/**
* @param {HTMLImageElement} image
*/
setPosition(image) {
const imageRect = image.getBoundingClientRect();
let tooltipRect;
const offset = 7;
this.tooltip.style.top = `${imageRect.bottom + offset + window.scrollY}px`;
this.tooltip.style.left = `${imageRect.x - 3}px`;
this.tooltip.classList.toggle("visible", true);
tooltipRect = this.tooltip.getBoundingClientRect();
const toolTipIsClippedAtBottom = tooltipRect.bottom > window.innerHeight;
if (!toolTipIsClippedAtBottom) {
return;
}
this.tooltip.style.top = `${imageRect.top - tooltipRect.height + window.scrollY - offset}px`;
tooltipRect = this.tooltip.getBoundingClientRect();
const favoritesTopBar = document.getElementById("favorites-top-bar");
const elementAboveTooltip = favoritesTopBar === null ? document.getElementById("header") : favoritesTopBar;
const elementAboveTooltipRect = elementAboveTooltip.getBoundingClientRect();
const toolTipIsClippedAtTop = tooltipRect.top < elementAboveTooltipRect.bottom;
if (!toolTipIsClippedAtTop) {
return;
}
const tooltipIsLeftOfCenter = tooltipRect.left < (window.innerWidth / 2);
this.tooltip.style.top = `${imageRect.top + window.scrollY + (imageRect.height / 2) - offset}px`;
if (tooltipIsLeftOfCenter) {
this.tooltip.style.left = `${imageRect.right + offset}px`;
} else {
this.tooltip.style.left = `${imageRect.left - 750 - offset}px`;
}
}
/**
* @param {String} tags
*/
setText(tags) {
this.tooltip.innerHTML = this.formatHTML(tags);
}
/**
* @param {HTMLImageElement} image
*/
show(image) {
let tags = image.hasAttribute("tags") ? image.getAttribute("tags") : image.getAttribute("title");
tags = this.removeIdFromTags(image, tags);
this.setText(tags);
this.setPosition(image);
}
hide() {
this.tooltip.style.transition = "none";
this.tooltip.classList.toggle("visible", false);
setTimeout(() => {
this.tooltip.style.transition = this.defaultTransition;
}, 5);
}
/**
* @param {HTMLImageElement} image
* @param {String} tags
* @returns
*/
removeIdFromTags(image, tags) {
const id = getThumbFromImage(image).id;
if (this.tagColorCodes[id] === undefined) {
tags = tags.replace(` ${id}`, "");
}
return tags;
}
/**
* @returns {String}
*/
getRandomColor() {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i += 1) {
if (i === 2 || i === 3) {
color += "0";
} else {
color += letters[Math.floor(Math.random() * letters.length)];
}
}
return color;
}
/**
* @param {String[]} allTags
*/
formatHTML(allTags) {
let html = "";
const tags = allTags.split(" ");
for (let i = tags.length - 1; i >= 0; i -= 1) {
const tag = tags[i];
const tagColor = this.getColorCode(tag);
const tagWithSpace = `${tag} `;
if (tagColor !== undefined) {
html = `<span style="color:${tagColor}"><b>${tagWithSpace}</b></span>${html}`;
} else if (includesTag(tag, TAG_BLACKLIST)) {
html += `<span style="color:red"><s><b>${tagWithSpace}</b></s></span>`;
} else {
html += tagWithSpace;
}
}
return html === "" ? allTags : html;
}
/**
* @param {String} searchQuery
*/
assignTagColors(searchQuery) {
searchQuery = this.removeNotTags(searchQuery);
const {orGroups, remainingSearchTags} = extractTagGroups(searchQuery);
this.tagColorCodes = {};
this.assignColorsToOrGroupTags(orGroups);
this.assignColorsToRemainingTags(remainingSearchTags);
}
/**
* @param {String[][]} orGroups
*/
assignColorsToOrGroupTags(orGroups) {
for (const orGroup of orGroups) {
const color = this.getRandomColor();
for (const tag of orGroup) {
this.addColorCodedTag(tag, color);
}
}
}
/**
* @param {String[]} remainingTags
*/
assignColorsToRemainingTags(remainingTags) {
for (const tag of remainingTags) {
this.addColorCodedTag(tag, this.getRandomColor());
}
}
/**
* @param {String} tags
* @returns {String}
*/
removeNotTags(tags) {
return tags.replace(/(?:^| )-\S+/gm, "");
}
sanitizeTags(tags) {
return tags.toLowerCase().trim();
}
addColorCodedTag(tag, color) {
tag = this.sanitizeTags(tag);
if (this.tagColorCodes[tag] === undefined) {
this.tagColorCodes[tag] = color;
}
}
/**
* @param {String} tag
* @returns {String | null}
*/
getColorCode(tag) {
if (this.tagColorCodes[tag] !== undefined) {
return this.tagColorCodes[tag];
}
for (const [tagPrefix, _] of Object.entries(this.tagColorCodes)) {
if (tagPrefix.endsWith("*")) {
if (tag.startsWith(tagPrefix.replace(/\*$/, ""))) {
return this.tagColorCodes[tagPrefix];
}
}
}
return undefined;
}
addFavoritesOptions() {
addOptionToFavoritesPage(
"show-tooltip",
" Tooltips",
"Show related tags when hovering over a thumbnail",
this.enabled, (event) => {
setPreference("showTooltip", event.target.checked);
this.setVisible(event.target.checked);
},
true
);
}
/**
* @param {HTMLElement | null} thumb
*/
showOnLoadIfHoveringOverThumb(thumb) {
if (thumb !== null) {
this.show(getImageFromThumb(thumb));
}
}
/**
* @param {Boolean} value
*/
setVisible(value) {
this.enabled = value;
}
assignColorsToMatchedTagsOnPostPage() {
const searchQuery = document.getElementsByName("tags")[0].getAttribute("value");
this.assignTagColors(searchQuery);
}
assignColorsToMatchedTagsOnFavoritesPage() {
if (this.searchBox.value === this.previousSearch) {
return;
}
this.previousSearch = this.searchBox.value;
this.assignTagColors(this.searchBox.value);
}
}
const tooltip = new Tooltip();
// saved_searches.js
const savedSearchesHTML = `<div id="saved-searches">
<style>
#saved-searches-container {
margin: 0;
display: flex;
flex-direction: column;
padding: 0;
}
#saved-searches-input-container {
margin-bottom: 10px;
}
#saved-searches-input {
flex: 15 1 auto;
margin-right: 10px;
}
#savedSearches {
max-width: 100%;
button {
flex: 1 1 auto;
cursor: pointer;
}
}
#saved-searches-buttons button {
margin-right: 1px;
margin-bottom: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
height: 35px;
&:hover {
filter: brightness(140%);
}
}
#saved-search-list-container {
direction: rtl;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
margin: 0;
padding: 0;
}
#saved-search-list {
direction: ltr;
>li {
display: flex;
flex-direction: row;
cursor: pointer;
background: rgba(0, 0, 0, .1);
&:nth-child(odd) {
background: rgba(0, 0, 0, 0.2);
}
>div {
padding: 4px;
align-content: center;
svg {
height: 20px;
width: 20px;
}
}
}
}
.save-search-label {
flex: 1000 30px;
text-align: left;
&:hover {
color: white;
background: #0075FF;
}
}
.edit-saved-search-button {
text-align: center;
flex: 1 20px;
&:hover {
color: white;
background: slategray;
}
}
.remove-saved-search-button {
text-align: center;
flex: 1 20px;
&:hover {
color: white;
background: #f44336;
}
}
.move-saved-search-to-top-button {
text-align: center;
&:hover {
color: white;
background: steelblue;
}
}
</style>
<h2>Saved Searches</h2>
<div id="saved-searches-buttons">
<button title="Save custom search" id="save-custom-search-button">Save</button>
<button title="Save results as search" id="save-results-button">Save Results</button>
<button id="stop-editing-saved-search-button" style="display: none;">Cancel</button>
<span>
<button id="export-saved-search-button">Export</button>
<button id="import-saved-search-button">Import</button>
</span>
</div>
<div id="saved-searches-container">
<div id="saved-searches-input-container">
<textarea id="saved-searches-input" spellcheck="false" style="width: 97%;"
placeholder="Save Custom Search"></textarea>
</div>
<div id="saved-search-list-container">
<div id="saved-search-list"></div>
</div>
</div>
</div>
`;
class SavedSearches {
static preferences = {
textareaWidth: "savedSearchesTextAreaWidth",
textareaHeight: "savedSearchesTextAreaHeight",
savedSearches: "savedSearches",
visibility: "savedSearchVisibility",
tutorial: "savedSearchesTutorial"
};
static localStorageKeys = {
savedSearches: "savedSearches"
};
static icons = {
delete: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-trash\"><polyline points=\"3 6 5 6 21 6\"></polyline><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path></svg>",
edit: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-edit\"><path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"></path><path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"></path></svg>",
copy: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-copy\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>",
upArrow: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-up\"><line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\"></line><polyline points=\"5 12 12 5 19 12\"></polyline></svg>"
};
/**
* @type {HTMLTextAreaElement}
*/
textarea;
/**
* @type {HTMLElement}
*/
savedSearchesList;
/**
* @type {HTMLButtonElement}
*/
stopEditingButton;
/**
* @type {HTMLButtonElement}
*/
saveButton;
/**
* @type {HTMLButtonElement}
*/
importButton;
/**
* @type {HTMLButtonElement}
*/
exportButton;
/**
* @type {HTMLButtonElement}
*/
saveSearchResultsButton;
constructor() {
if (onPostPage()) {
return;
}
this.initialize();
}
initialize() {
this.insertHTMLIntoDocument();
this.saveButton = document.getElementById("save-custom-search-button");
this.textarea = document.getElementById("saved-searches-input");
this.savedSearchesList = document.getElementById("saved-search-list");
this.stopEditingButton = document.getElementById("stop-editing-saved-search-button");
this.importButton = document.getElementById("import-saved-search-button");
this.exportButton = document.getElementById("export-saved-search-button");
this.saveSearchResultsButton = document.getElementById("save-results-button");
this.addEventListeners();
this.loadSavedSearches();
}
insertHTMLIntoDocument() {
const showSavedSearches = getPreference(SavedSearches.preferences.visibility, true);
let placeToInsertSavedSearches = document.getElementById("right-favorites-panel");
if (placeToInsertSavedSearches === null) {
placeToInsertSavedSearches = document.getElementById("favorites-top-bar");
}
placeToInsertSavedSearches.insertAdjacentHTML("beforeend", savedSearchesHTML);
document.getElementById("saved-searches").style.display = showSavedSearches ? "block" : "none";
const options = addOptionToFavoritesPage(
"savedSearchesCheckbox",
"Saved Searches",
"Toggle saved searches",
showSavedSearches,
(e) => {
document.getElementById("saved-searches").style.display = e.target.checked ? "block" : "none";
setPreference(SavedSearches.preferences.visibility, e.target.checked);
},
true
);
document.getElementById("show-options").insertAdjacentElement("afterend", options);
}
addEventListeners() {
this.saveButton.onclick = () => {
this.saveSearch(this.textarea.value.trim());
};
this.textarea.addEventListener("keydown", (event) => {
switch (event.key) {
case "Enter":
if (awesompleteIsUnselected(this.textarea)) {
event.preventDefault();
this.saveButton.click();
this.textarea.blur();
setTimeout(() => {
this.textarea.focus();
}, 100);
}
break;
case "Escape":
if (awesompleteIsUnselected(this.textarea) && this.stopEditingButton.style.display === "block") {
this.stopEditingButton.click();
}
break;
default:
break;
}
});
this.exportButton.onclick = () => {
this.exportSavedSearches();
};
this.importButton.onclick = () => {
this.importSavedSearches();
};
this.saveSearchResultsButton.onclick = () => {
this.copySearchResultIdsToClipboard();
};
}
/**
* @param {String} newSavedSearch
*/
saveSearch(newSavedSearch) {
if (newSavedSearch === "" || newSavedSearch === undefined) {
return;
}
const newListItem = document.createElement("li");
const savedSearchLabel = document.createElement("div");
const editButton = document.createElement("div");
const removeButton = document.createElement("div");
const moveToTopButton = document.createElement("div");
savedSearchLabel.innerText = newSavedSearch;
editButton.innerHTML = SavedSearches.icons.edit;
removeButton.innerHTML = SavedSearches.icons.delete;
moveToTopButton.innerHTML = SavedSearches.icons.upArrow;
savedSearchLabel.className = "save-search-label";
editButton.className = "edit-saved-search-button";
removeButton.className = "remove-saved-search-button";
moveToTopButton.className = "move-saved-search-to-top-button";
newListItem.appendChild(removeButton);
newListItem.appendChild(editButton);
newListItem.appendChild(moveToTopButton);
newListItem.appendChild(savedSearchLabel);
this.savedSearchesList.insertBefore(newListItem, this.savedSearchesList.firstChild);
savedSearchLabel.onclick = () => {
const searchBox = document.getElementById("favorites-search-box");
navigator.clipboard.writeText(savedSearchLabel.innerText);
if (searchBox === null) {
return;
}
if (searchBox.value !== "") {
searchBox.value += " ";
}
searchBox.value += savedSearchLabel.innerText;
};
removeButton.onclick = () => {
if (this.inEditMode()) {
alert("Cancel current edit before removing another search");
return;
}
if (confirm(`Remove Saved Search: ${savedSearchLabel.innerText} ?`)) {
this.savedSearchesList.removeChild(newListItem);
this.storeSavedSearches();
}
};
editButton.onclick = () => {
if (this.inEditMode()) {
alert("Cancel current edit before editing another search");
} else {
this.editSavedSearches(savedSearchLabel, newListItem);
}
};
moveToTopButton.onclick = () => {
if (this.inEditMode()) {
alert("Cancel current edit before moving this search to the top");
return;
}
newListItem.parentElement.insertAdjacentElement("afterbegin", newListItem);
};
this.stopEditingButton.onclick = () => {
this.stopEditingSavedSearches(newListItem);
};
this.textarea.value = "";
this.storeSavedSearches();
}
/**
* @param {HTMLLabelElement} savedSearchLabel
*/
editSavedSearches(savedSearchLabel) {
this.textarea.value = savedSearchLabel.innerText;
this.saveButton.textContent = "Save Changes";
this.textarea.focus();
this.exportButton.style.display = "none";
this.importButton.style.display = "none";
this.stopEditingButton.style.display = "";
this.saveButton.onclick = () => {
savedSearchLabel.innerText = this.textarea.value.trim();
this.storeSavedSearches();
this.stopEditingButton.click();
};
}
/**
* @param {HTMLElement} newListItem
*/
stopEditingSavedSearches(newListItem) {
this.saveButton.textContent = "Save";
this.saveButton.onclick = () => {
this.saveSearch(this.textarea.value.trim());
};
this.textarea.value = "";
this.exportButton.style.display = "";
this.importButton.style.display = "";
this.stopEditingButton.style.display = "none";
newListItem.style.border = "";
}
storeSavedSearches() {
const savedSearches = JSON.stringify(Array.from(document.getElementsByClassName("save-search-label"))
.map(element => element.innerText));
localStorage.setItem(SavedSearches.localStorageKeys.savedSearches, savedSearches);
}
loadSavedSearches() {
const savedSearches = JSON.parse(localStorage.getItem(SavedSearches.localStorageKeys.savedSearches)) || [];
const firstUse = getPreference(SavedSearches.preferences.tutorial, true);
setPreference(SavedSearches.preferences.tutorial, false);
if (firstUse && savedSearches.length === 0) {
this.createTutorialSearches();
return;
}
for (let i = savedSearches.length - 1; i >= 0; i -= 1) {
this.saveSearch(savedSearches[i]);
}
}
createTutorialSearches() {
const searches = [];
window.addEventListener("startedFetchingFavorites", async() => {
await sleep(1000);
const postIds = getAllVisibleThumbs().map(thumb => thumb.id);
shuffleArray(postIds);
const exampleSearch = `( EXAMPLE: ~ ${postIds.slice(0, 9).join(" ~ ")} ) ( male* ~ female* ~ 1boy ~ 1girls )`;
searches.push(exampleSearch);
for (let i = searches.length - 1; i >= 0; i -= 1) {
this.saveSearch(searches[i]);
}
});
}
/**
* @returns {Boolean}
*/
inEditMode() {
return this.stopEditingButton.style.display !== "none";
}
exportSavedSearches() {
const savedSearchString = Array.from(document.getElementsByClassName("save-search-label")).map(search => search.innerText).join("\n");
navigator.clipboard.writeText(savedSearchString);
alert("Copied saved searches to clipboard");
}
importSavedSearches() {
const doesNotHaveSavedSearches = this.savedSearchesList.querySelectorAll("li").length === 0;
if (doesNotHaveSavedSearches || confirm("are you sure you want to import saved searches? ( this will overwrite current saved searches )")) {
const savedSearches = this.textarea.value.split("\n");
this.savedSearchesList.innerHTML = "";
for (let i = savedSearches.length - 1; i >= 0; i -= 1) {
this.saveSearch(savedSearches[i]);
}
this.storeSavedSearches();
}
}
copySearchResultIdsToClipboard() {
const resultIds = [];
for (const thumb of getAllVisibleThumbs()) {
resultIds.push(thumb.id);
}
if (resultIds.length === 0) {
return;
}
if (resultIds.length > 300) {
if (!confirm(`Are you sure you want to save ${resultIds.length} ids as one search?`));
}
const customSearch = `( ${resultIds.join(" ~ ")} )`;
this.saveSearch(customSearch);
}
}
const savedSearches = new SavedSearches();
// caption.js
const captionHTML = `<style>
.caption {
overflow: hidden;
pointer-events: none;
background: rgba(0, 0, 0, .75);
z-index: 15;
position: absolute;
width: 100%;
height: 100%;
top: -100%;
left: 0px;
top: 0px;
text-align: left;
transform: translateX(-100%);
/* transition: transform .3s cubic-bezier(.26,.28,.2,.82); */
transition: transform .4s ease;
padding-left: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
h6 {
display: block;
color: white;
padding-top: 0px;
}
li {
width: fit-content;
list-style-type: none;
display: inline-block;
}
&.active {
transform: translateX(0%);
}
&.transition-completed {
.caption-tag {
pointer-events: all;
}
}
}
.caption.hide {
display: none;
}
.caption.inactive {
display: none;
}
.caption-tag {
pointer-events: none;
color: #6cb0ff;
word-wrap: break-word;
&:hover {
text-decoration-line: underline;
cursor: pointer;
}
}
.artist-tag {
color: #f0a0a0;
}
.character-tag {
color: #f0f0a0;
}
.copyright-tag {
color: #e73ee7;
}
.metadata-tag {
color: #FF8800;
}
.caption-wrapper {
pointer-events: none;
position: absolute !important;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block !important;
}
</style>`;
class Caption {
static preferences = {
visibility: "showCaptions"
};
static localStorageKeys = {
tagCategories: "tagCategories"
};
static importantTagCategories = new Set([
"copyright",
"character",
"artist"
// "metadata"
]);
static tagCategoryEncodings = {
0: "general",
1: "artist",
2: "metadata",
3: "copyright",
4: "character"
};
static template = `
<ul id="caption-list">
<li id="caption-id" style="display: block;"><h6>ID</h6></li>
${Caption.getCategoryHeaderHTML()}
</ul>
`;
/**
* @returns {String}
*/
static getCategoryHeaderHTML() {
let html = "";
for (const category of Caption.importantTagCategories) {
const capitalizedCategory = capitalize(category);
const header = capitalizedCategory === "Metadata" ? "Meta" : capitalizedCategory;
html += `<li id="caption${capitalizedCategory}" style="display: none;"><h6>${header}</h6></li>`;
}
return html;
}
/**
* @param {String} tagCategory
* @returns {Number}
*/
static getTagCategoryEncoding(tagCategory) {
for (const [encoding, category] of Object.entries(Caption.tagCategoryEncodings)) {
if (category === tagCategory) {
return encoding;
}
}
return 0;
}
/**
* @type {Boolean}
*/
get disabled() {
return this.caption.classList.contains("hide") || this.caption.classList.contains("disabled") || this.caption.classList.contains("remove");
}
/**
* @type {HTMLDivElement}
*/
captionWrapper;
/**
* @type {HTMLDivElement}
*/
caption;
/**
* @type {Object.<String, Number>}
*/
tagCategoryAssociations;
/**
* @type {String[]}
*/
problematicTags;
/**
* @type {Boolean}
*/
currentlyCorrectingProblematicTags;
/**
* @type {String}
*/
currentThumbId;
constructor() {
this.tagCategoryAssociations = this.loadSavedTags();
this.problematicTags = [];
this.currentlyCorrectingProblematicTags = false;
this.previousThumb = null;
this.currentThumbId = null;
this.findCategoriesOfAllTags();
this.create();
this.injectHTML();
this.setVisibility(this.getVisibilityPreference());
this.addEventListeners();
}
create() {
this.captionWrapper = document.createElement("div");
this.captionWrapper.className = "caption-wrapper";
this.caption = document.createElement("div");
this.caption.className = "caption";
this.captionWrapper.appendChild(this.caption);
document.head.appendChild(this.captionWrapper);
this.caption.innerHTML = Caption.template;
}
injectHTML() {
injectStyleHTML(captionHTML);
addOptionToFavoritesPage(
"show-captions",
"Details",
"Show details when hovering over thumbnail",
this.getVisibilityPreference(),
(event) => {
this.setVisibility(event.target.checked);
},
true
);
}
async addEventListenersToThumbs() {
await sleep(500);
const thumbs = getAllThumbs();
for (const thumb of thumbs) {
const imageContainer = getImageFromThumb(thumb).parentElement;
if (imageContainer.hasAttribute("has-caption-listener")) {
continue;
}
imageContainer.setAttribute("has-caption-listener", true);
imageContainer.addEventListener("mouseenter", () => {
this.show(thumb);
});
imageContainer.addEventListener("mouseleave", () => {
this.hide(thumb);
});
}
}
/**
* @param {HTMLElement} thumb
*/
show(thumb) {
if (this.disabled || thumb === null) {
return;
}
thumb.querySelectorAll(".caption-wrapper-clone").forEach(element => element.remove());
this.caption.classList.remove("inactive");
this.caption.innerHTML = Caption.template;
this.captionWrapper.removeAttribute("style");
const captionIdHeader = document.getElementById("caption-id");
const captionIdTag = document.createElement("li");
captionIdTag.className = "caption-tag";
captionIdTag.textContent = thumb.id;
captionIdTag.onclick = () => {
this.tagOnClick(thumb.id);
};
captionIdTag.addEventListener("contextmenu", (event) => {
event.preventDefault();
this.tagOnClick(`-${thumb.id}`);
});
captionIdHeader.insertAdjacentElement("afterend", captionIdTag);
thumb.children[0].appendChild(this.captionWrapper);
this.populateTags(thumb);
}
/**
* @param {HTMLElement} thumb
*/
hide(thumb) {
if (this.disabled) {
return;
}
this.animateExit(thumb);
this.animate(false);
this.caption.classList.add("inactive");
this.caption.classList.remove("transition-completed");
}
/**
* @param {HTMLElement} thumb
*/
animateExit(thumb) {
const captionWrapperClone = this.captionWrapper.cloneNode(true);
const captionClone = captionWrapperClone.children[0];
thumb.querySelectorAll(".caption-wrapper-clone").forEach(element => element.remove());
captionWrapperClone.classList.add("caption-wrapper-clone");
captionWrapperClone.querySelectorAll("*").forEach(element => element.removeAttribute("id"));
captionClone.ontransitionend = () => {
captionWrapperClone.remove();
};
thumb.children[0].appendChild(captionWrapperClone);
setTimeout(() => {
captionClone.classList.remove("active");
}, 4);
}
/**
* @param {HTMLElement} thumb
*/
resizeFont(thumb) {
const imageRect = getImageFromThumb(thumb).getBoundingClientRect();
const captionListRect = this.caption.children[0].getBoundingClientRect();
const ratio = imageRect.height / captionListRect.height;
const scale = ratio > 1 ? Math.sqrt(ratio) : ratio * 0.85;
this.caption.parentElement.style.fontSize = `${roundToTwoDecimalPlaces(scale)}em`;
}
/**
* @param {String} tagCategory
* @param {String} tagName
*/
addTag(tagCategory, tagName) {
if (!Caption.importantTagCategories.has(tagCategory)) {
return;
}
const header = document.getElementById(this.getCategoryHeaderId(tagCategory));
const tag = document.createElement("li");
tag.className = `${tagCategory}-tag caption-tag`;
tag.textContent = this.replaceUnderscoresWithSpaces(tagName);
header.insertAdjacentElement("afterend", tag);
header.style.display = "block";
tag.onmouseover = (event) => {
event.stopPropagation();
};
tag.onclick = () => {
this.tagOnClick(tagName);
};
tag.addEventListener("contextmenu", (event) => {
event.preventDefault();
this.tagOnClick(`-${this.replaceSpacesWithUnderscores(tag.textContent)}`);
});
}
addEventListeners() {
this.caption.addEventListener("transitionend", () => {
if (this.caption.classList.contains("active")) {
this.caption.classList.add("transition-completed");
}
this.caption.classList.remove("transitioning");
});
this.caption.addEventListener("transitionstart", () => {
this.caption.classList.add("transitioning");
});
if (onPostPage()) {
window.addEventListener("load", () => {
this.addEventListenersToThumbs.bind(this)();
}, {
once: true,
passive: true
});
} else {
window.addEventListener("favoritesLoaded", this.addEventListenersToThumbs.bind(this)(), {
once: true
});
window.addEventListener("favoritesFetched", () => {
this.addEventListenersToThumbs.bind(this)();
});
window.addEventListener("thumbUnderCursorOnLoad", (event) => {
const showOnHoverCheckbox = document.getElementById("showOnHover");
if (showOnHoverCheckbox !== null && showOnHoverCheckbox.checked) {
this.show(event.detail);
}
});
window.addEventListener("showCaption", (event) => {
this.show(event.detail);
});
}
}
/**
* @returns {Object.<String, Number>}
*/
loadSavedTags() {
return JSON.parse(localStorage.getItem(Caption.localStorageKeys.tagCategories) || "{}");
}
saveTags() {
localStorage.setItem(Caption.localStorageKeys.tagCategories, JSON.stringify(this.tagCategoryAssociations));
}
/**
* @param {String} value
*/
tagOnClick(value) {
const searchBox = onPostPage() ? document.getElementsByName("tags")[0] : document.getElementById("favorites-search-box");
const searchBoxDoesNotIncludeTag = true;
// const searchBoxDoesNotIncludeTag = searchBox !== null && !searchBox.value.includes(` ${value}`);
navigator.clipboard.writeText(value);
if (searchBoxDoesNotIncludeTag) {
searchBox.value += ` ${value}`;
searchBox.focus();
value = searchBox.value;
searchBox.value = "";
searchBox.value = value;
}
}
/**
* @param {String} tagName
* @returns {String}
*/
replaceUnderscoresWithSpaces(tagName) {
return tagName.replace(/_/gm, " ");
}
/**
* @param {String} tagName
* @returns {String}
*/
replaceSpacesWithUnderscores(tagName) {
return tagName.replace(/\s/gm, "_");
}
/**
* @param {Boolean} value
*/
setVisibility(value) {
if (value) {
this.caption.classList.remove("disabled");
} else if (!this.caption.classList.contains("disabled")) {
this.caption.classList.add("disabled");
}
setPreference(Caption.preferences.visibility, value);
}
/**
* @returns {Boolean}
*/
getVisibilityPreference() {
return getPreference(Caption.preferences.visibility, true);
}
/**
* @param {Boolean} value
*/
animate(value) {
this.caption.classList.toggle("active", value);
}
/**
* @param {String} tagCategory
* @returns {String}
*/
getCategoryHeaderId(tagCategory) {
return `caption${capitalize(tagCategory)}`;
}
/**
* @param {HTMLElement} thumb
*/
populateTags(thumb) {
const tagNames = getTagsFromThumb(thumb).replace(/\s\d+$/, "")
.split(" ");
const unknownThumbTags = tagNames
.filter(tag => this.tagCategoryAssociations[tag] === undefined);
this.currentThumbId = thumb.id;
if (unknownThumbTags.length > 0) {
this.findTagCategories(unknownThumbTags, 1, () => {
this.addTags(tagNames, thumb);
});
return;
}
this.addTags(tagNames, thumb);
}
/**
* @param {String[]} tagNames
* @param {HTMLElement} thumb
*/
addTags(tagNames, thumb) {
this.saveTags();
if (this.currentThumbId !== thumb.id) {
return;
}
if (thumb.getElementsByClassName("caption-tag").length > 1) {
return;
}
for (const tagName of tagNames) {
const category = this.getTagCategory(tagName);
this.addTag(category, tagName);
}
this.resizeFont(thumb);
this.animate(true);
}
/**
* @param {String} tagName
* @returns {String}
*/
getTagCategory(tagName) {
const encoding = this.tagCategoryAssociations[tagName];
if (encoding === undefined) {
return "general";
}
return Caption.tagCategoryEncodings[encoding];
}
/**
* @param {String} problematicTag
*/
async correctProblematicTag(problematicTag) {
this.problematicTags.push(problematicTag);
if (this.currentlyCorrectingProblematicTags) {
return;
}
this.currentlyCorrectingProblematicTags = true;
while (this.problematicTags.length > 0) {
const tagName = this.problematicTags.pop();
const tagPageURL = `https://rule34.xxx/index.php?page=tags&s=list&tags=${tagName}`;
await fetch(tagPageURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.statusText);
})
.then((html) => {
const dom = new DOMParser().parseFromString(html, "text/html");
const columnOfFirstRow = dom.getElementsByClassName("highlightable")[0].getElementsByTagName("td");
if (columnOfFirstRow.length !== 3) {
this.tagCategoryAssociations[tagName] = 0;
this.saveTags();
return;
}
const category = columnOfFirstRow[2].textContent.split(",")[0].split(" ")[0];
this.tagCategoryAssociations[tagName] = Caption.getTagCategoryEncoding(category);
this.saveTags();
});
}
this.currentlyCorrectingProblematicTags = false;
}
/**
* @param {String[]} tagNames
* @param {Number} fetchDelay
* @param {Function} onAllCategoriesFound
*/
async findTagCategories(tagNames, fetchDelay, onAllCategoriesFound) {
const parser = new DOMParser();
const lastTagName = tagNames[tagNames.length - 1];
const uniqueTagNames = new Set(tagNames);
for (const tagName of uniqueTagNames) {
const apiURL = `https://api.rule34.xxx//index.php?page=dapi&s=tag&q=index&name=${encodeURIComponent(tagName)}`;
fetch(apiURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.statusText);
})
.then((html) => {
const dom = parser.parseFromString(html, "text/html");
const encoding = dom.getElementsByTagName("tag")[0].getAttribute("type");
if (encoding === "array") {
this.correctProblematicTag(tagName);
return;
}
this.tagCategoryAssociations[tagName] = encoding;
if (tagName === lastTagName && onAllCategoriesFound !== undefined) {
onAllCategoriesFound();
}
});
await sleep(fetchDelay);
}
}
/**
* @param {HTMLElement[]} thumbs
* @returns {String[]}
*/
getTagNamesWithUnknownCategories(thumbs) {
return Array.from(thumbs)
.map(thumb => getTagsFromThumb(thumb).replace(/ \d+$/, ""))
.join(" ")
.split(" ")
.filter(tagName => this.tagCategoryAssociations[tagName] === undefined);
}
findCategoriesOfAllTags() {
window.addEventListener("originalContentCleared", (event) => {
const thumbs = event.detail;
const tagNames = this.getTagNamesWithUnknownCategories(thumbs);
this.findTagCategories(tagNames, 3, () => {
this.saveTags();
});
});
window.addEventListener("favoritesLoaded", () => {
const allTagNames = this.getTagNamesWithUnknownCategories(getAllThumbs);
if (allTagNames.length === 0) {
return;
}
this.findTagCategories(allTagNames, 2, () => {
this.saveTags();
});
});
}
}
if (!onPostPage()) {
const caption = new Caption();
}
// awesomplete.min.js
// Awesomplete - Lea Verou - MIT license
!(function () {
function t(t) {
const e = Array.isArray(t) ? {
label: t[0],
value: t[1]
} : typeof t === "object" && t != null && "label" in t && "value" in t ? t : {
label: t,
value: t
};
this.label = e.label || e.value, this.value = e.value, this.type = e.type;
}
function e(t, e, i) {
for (const n in e) {
const s = e[n],
r = t.input.getAttribute(`data-${n.toLowerCase()}`);
typeof s === "number" ? t[n] = parseInt(r) : !1 === s ? t[n] = r !== null : s instanceof Function ? t[n] = null : t[n] = r, t[n] || t[n] === 0 || (t[n] = n in i ? i[n] : s);
}
}
function i(t, e) {
return typeof t === "string" ? (e || document).querySelector(t) : t || null;
}
function n(t, e) {
return o.call((e || document).querySelectorAll(t));
}
function s() {
n("input.awesomplete").forEach((t) => {
new r(t);
});
}
var r = function (t, n) {
const s = this;
this.isOpened = !1, this.input = i(t), this.input.setAttribute("autocomplete", "off"), this.input.setAttribute("aria-autocomplete", "list"), n = n || {}, e(this, {
minChars: 2,
maxItems: 10,
autoFirst: !1,
data: r.DATA,
filter: r.FILTER_CONTAINS,
sort: !1 !== n.sort && r.SORT_BYLENGTH,
item: r.ITEM,
replace: r.REPLACE
}, n), this.index = -1, this.container = i.create("div", {
className: "awesomplete",
around: t
}), this.ul = i.create("ul", {
hidden: "hidden",
inside: this.container
}), this.status = i.create("span", {
className: "visually-hidden",
role: "status",
"aria-live": "assertive",
"aria-relevant": "additions",
inside: this.container
}), this._events = {
input: {
input: this.evaluate.bind(this),
blur: this.close.bind(this, {
reason: "blur"
}),
keypress(t) {
const e = t.keyCode;
if (s.opened) {
switch (e) {
case 13: // RETURN
if (s.selected == true) {
t.preventDefault();
s.select();
break;
}
case 66:
break;
case 27: // ESC
s.close({
reason: "esc"
});
break;
}
}
},
keydown(t) {
const e = t.keyCode;
if (s.opened) {
switch (e) {
case 9: // TAB
if (s.selected == true) {
t.preventDefault();
s.select();
break;
}
case 38: // up arrow
t.preventDefault();
s.previous();
break;
case 40:
t.preventDefault();
s.next();
break;
}
}
}
},
form: {
submit: this.close.bind(this, {
reason: "submit"
})
},
ul: {
mousedown(t) {
let e = t.target;
if (e !== this) {
for (; e && !(/li/i).test(e.nodeName);) e = e.parentNode;
e && t.button === 0 && (t.preventDefault(), s.select(e, t.target));
}
}
}
}, i.bind(this.input, this._events.input), i.bind(this.input.form, this._events.form), i.bind(this.ul, this._events.ul), this.input.hasAttribute("list") ? (this.list = `#${this.input.getAttribute("list")}`, this.input.removeAttribute("list")) : this.list = this.input.getAttribute("data-list") || n.list || [], r.all.push(this);
};
r.prototype = {
set list(t) {
if (Array.isArray(t)) this._list = t;
else if (typeof t === "string" && t.indexOf(",") > -1) this._list = t.split(/\s*,\s*/);
else if ((t = i(t)) && t.children) {
const e = [];
o.apply(t.children).forEach((t) => {
if (!t.disabled) {
const i = t.textContent.trim(),
n = t.value || i,
s = t.label || i;
n !== "" && e.push({
label: s,
value: n
});
}
}), this._list = e;
}
document.activeElement === this.input && this.evaluate();
},
get selected() {
return this.index > -1;
},
get opened() {
return this.isOpened;
},
close(t) {
this.opened && (this.ul.setAttribute("hidden", ""), this.isOpened = !1, this.index = -1, i.fire(this.input, "awesomplete-close", t || {}));
},
open() {
this.ul.removeAttribute("hidden"), this.isOpened = !0, this.autoFirst && this.index === -1 && this.goto(0), i.fire(this.input, "awesomplete-open");
},
destroy() {
i.unbind(this.input, this._events.input), i.unbind(this.input.form, this._events.form);
const t = this.container.parentNode;
t.insertBefore(this.input, this.container), t.removeChild(this.container), this.input.removeAttribute("autocomplete"), this.input.removeAttribute("aria-autocomplete");
const e = r.all.indexOf(this);
e !== -1 && r.all.splice(e, 1);
},
next() {
const t = this.ul.children.length;
this.goto(this.index < t - 1 ? this.index + 1 : t ? 0 : -1);
},
previous() {
const t = this.ul.children.length,
e = this.index - 1;
this.goto(this.selected && e !== -1 ? e : t - 1);
},
goto(t) {
const e = this.ul.children;
this.selected && e[this.index].setAttribute("aria-selected", "false"), this.index = t, t > -1 && e.length > 0 && (e[t].setAttribute("aria-selected", "true"), this.status.textContent = e[t].textContent, this.ul.scrollTop = e[t].offsetTop - this.ul.clientHeight + e[t].clientHeight, i.fire(this.input, "awesomplete-highlight", {
text: this.suggestions[this.index]
}));
},
select(t, e) {
if (t ? this.index = i.siblingIndex(t) : t = this.ul.children[this.index], t) {
const n = this.suggestions[this.index];
i.fire(this.input, "awesomplete-select", {
text: n,
origin: e || t
}) && (this.replace(n), this.close({
reason: "select"
}), i.fire(this.input, "awesomplete-selectcomplete", {
text: n
}));
}
},
evaluate() {
const e = this,
i = this.input.value;
i.length >= this.minChars && this._list.length > 0 ? (this.index = -1, this.ul.innerHTML = "", this.suggestions = this._list.map((n) => {
return new t(e.data(n, i));
}).filter((t) => {
return e.filter(t, i);
}), !1 !== this.sort && (this.suggestions = this.suggestions.sort(this.sort)), this.suggestions = this.suggestions.slice(0, this.maxItems), this.suggestions.forEach((t) => {
e.ul.appendChild(e.item(t, i));
}), this.ul.children.length === 0 ? this.close({
reason: "nomatches"
}) : this.open()) : this.close({
reason: "nomatches"
});
}
}, r.all = [], r.FILTER_CONTAINS = function (t, e) {
return RegExp(i.regExpEscape(e.trim()), "i").test(t);
}, r.FILTER_STARTSWITH = function (t, e) {
return RegExp(`^${i.regExpEscape(e.trim())}`, "i").test(t);
}, r.SORT_BYLENGTH = function (t, e) {
return t.length !== e.length ? t.length - e.length : t < e ? -1 : 1;
}, r.ITEM = function (t, e) {
return i.create("li", {
innerHTML: e.trim() === "" ? t : t.replace(RegExp(i.regExpEscape(e.trim()), "gi"), "<mark>$&</mark>"),
"aria-selected": "false"
});
}, r.REPLACE = function (t) {
this.input.value = t.value;
}, r.DATA = function (t) {
return t;
}, Object.defineProperty(t.prototype = Object.create(String.prototype), "length", {
get() {
return this.label.length;
}
}), t.prototype.toString = t.prototype.valueOf = function () {
return `${this.label}`;
};
var o = Array.prototype.slice;
i.create = function (t, e) {
const n = document.createElement(t);
for (const s in e) {
const r = e[s];
if (s === "inside") i(r).appendChild(n);
else if (s === "around") {
const o = i(r);
o.parentNode.insertBefore(n, o), n.appendChild(o);
} else s in n ? n[s] = r : n.setAttribute(s, r);
}
return n;
}, i.bind = function (t, e) {
if (t) for (const i in e) {
var n = e[i];
i.split(/\s+/).forEach((e) => {
t.addEventListener(e, n);
});
}
}, i.unbind = function (t, e) {
if (t) for (const i in e) {
var n = e[i];
i.split(/\s+/).forEach((e) => {
t.removeEventListener(e, n);
});
}
}, i.fire = function (t, e, i) {
const n = document.createEvent("HTMLEvents");
n.initEvent(e, !0, !0);
for (const s in i) n[s] = i[s];
return t.dispatchEvent(n);
}, i.regExpEscape = function (t) {
return t.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
}, i.siblingIndex = function (t) {
for (var e = 0; t = t.previousElementSibling; e++);
return e;
}, typeof Document !== "undefined" && (document.readyState !== "loading" ? s() : document.addEventListener("DOMContentLoaded", s)), r.$ = i, r.$$ = n, typeof self !== "undefined" && (self.Awesomplete_ = r), typeof module === "object" && module.exports && (module.exports = r);
}());
var decodeEntities = (function () {
// this prevents any overhead from creating the object each time
const element = document.createElement("div");
function decodeHTMLEntities(str) {
if (str && typeof str === "string") {
// strip script/html tags
str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gmi, "");
str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, "");
element.innerHTML = str;
str = element.textContent;
element.textContent = "";
}
return str;
}
return decodeHTMLEntities;
}());
// awesomplete.js
/* eslint-disable new-cap */
class AwesompleteWrapper {
constructor() {
document.querySelectorAll("textarea").forEach((textarea) => {
this.addAwesompleteToInput(textarea);
});
}
/**
* @param {HTMLTextAreaElement} input
*/
addAwesompleteToInput(input) {
const awesomplete = new Awesomplete_(input, {
minChars: 1,
list: [],
filter: (suggestion, _) => {
return Awesomplete_.FILTER_STARTSWITH(suggestion.value, this.getCurrentTag(awesomplete.input));
},
sort: false,
item: (suggestion, tags) => {
const html = tags.trim() === "" ? suggestion.label : suggestion.label.replace(RegExp(Awesomplete_.$.regExpEscape(tags.trim()), "gi"), "<mark>$&</mark>");
return Awesomplete_.$.create("li", {
innerHTML: html,
"aria-selected": "false",
className: `tag-type-${suggestion.type}`
});
},
replace: (suggestion) => {
this.insertSuggestion(awesomplete.input, decodeEntities(suggestion.value));
}
});
input.oninput = () => {
this.populateAwesompleteList(this.getCurrentTag(input), awesomplete);
};
}
/**
* @param {String} prefix
* @param {Awesomplete_} awesomplete
*/
populateAwesompleteList(prefix, awesomplete) {
fetch(`https://rule34.xxx/autocomplete.php?q=${prefix}`)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status);
})
.then((suggestions) => {
awesomplete.list = JSON.parse(suggestions);
}).catch(() => {
});
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @param {String} suggestion
*/
insertSuggestion(input, suggestion) {
const firstHalf = input.value.slice(0, input.selectionStart);
const secondHalf = input.value.slice(input.selectionStart);
const firstHalfWithPrefixRemoved = firstHalf.replace(/(?:^|\s)(-?)\S+$/, " $1");
const result = removeExtraWhiteSpace(`${firstHalfWithPrefixRemoved}${suggestion} ${secondHalf} `);
const newSelectionStart = firstHalfWithPrefixRemoved.length + suggestion.length + 1;
input.value = `${result} `.replace(/^\s+/, "");
input.selectionStart = newSelectionStart;
input.selectionEnd = newSelectionStart;
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns {String}
*/
getCurrentTag(input) {
return this.getLastTag(input.value.slice(0, input.selectionStart));
}
/**
* @param {String} searchQuery
* @returns {String}
*/
getLastTag(searchQuery) {
const lastTag = searchQuery.match(/[^ -][^ ]*$/);
return lastTag === null ? "" : lastTag[0];
}
}
const awesomplete = new AwesompleteWrapper();