// ==UserScript==
// @name Rule34 Favorites Search Gallery
// @namespace bruh3396
// @version 1.16.1
// @description Search, View, and Play Rule34 Favorites (Dekstop/Androiod/iOS)
// @author bruh3396
// @compatible Chrome
// @compatible Edge
// @compatible Firefox
// @compatible Safari
// @compatible Opera
// @match https://rule34.xxx/index.php?page=favorites&s=view&id=*
// @match https://rule34.xxx/index.php?page=post&s=list*
// ==/UserScript==
// utilities.js
/* eslint-disable max-classes-per-file */
class Cooldown {
/**
* @type {Number}
*/
timeout;
/**
* @type {Number}
*/
waitTime;
/**
* @type {Boolean}
*/
skipCooldown;
/**
* @type {Boolean}
*/
debounce;
/**
* @type {Boolean}
*/
debouncing;
/**
* @type {Function}
*/
onDebounceEnd;
/**
* @type {Function}
*/
onCooldownEnd;
get ready() {
if (this.skipCooldown) {
return true;
}
if (this.timeout === null) {
this.start();
return true;
}
if (this.debounce) {
this.debouncing = true;
clearTimeout(this.timeout);
this.start();
}
return false;
}
/**
* @param {Number} waitTime
* @param {Boolean} debounce
*/
constructor(waitTime, debounce = false) {
this.timeout = null;
this.waitTime = waitTime;
this.skipCooldown = false;
this.debounce = debounce;
this.debouncing = false;
this.onDebounceEnd = () => { };
this.onCooldownEnd = () => { };
}
start() {
this.timeout = setTimeout(() => {
this.timeout = null;
if (this.debouncing) {
this.onDebounceEnd();
this.debouncing = false;
}
this.onCooldownEnd();
}, this.waitTime);
}
stop() {
if (this.timeout === null) {
return;
}
clearTimeout(this.timeout);
}
restart() {
this.stop();
this.start();
}
}
class MetadataSearchExpression {
/**
* @type {String}
*/
metric;
/**
* @type {String}
*/
operator;
/**
* @type {String | Number}
*/
value;
/**
* @param {String} metric
* @param {String} operator
* @param {String} value
*/
constructor(metric, operator, value) {
this.metric = metric;
this.operator = operator;
this.value = this.setValue(value);
}
/**
* @param {String} value
* @returns {String | Number}
*/
setValue(value) {
if (!isNumber(value)) {
return value;
}
if (this.metric === "id" && this.operator === ":") {
return value;
}
return parseInt(value);
}
}
const IDS_TO_REMOVE_ON_RELOAD_KEY = "recentlyRemovedIds";
const TAG_BLACKLIST = getTagBlacklist();
const PREFERENCES_LOCAL_STORAGE_KEY = "preferences";
const FLAGS = {
set: false,
onSearchPage: {
set: false,
value: undefined
},
onFavoritesPage: {
set: false,
value: undefined
},
onPostPage: {
set: false,
value: undefined
},
usingFirefox: {
set: false,
value: undefined
},
onMobileDevice: {
set: false,
value: undefined
},
userIsOnTheirOwnFavoritesPage: {
set: false,
value: undefined
},
usingRenderer: {
set: false,
value: undefined
}
};
const 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>",
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>",
heartPlus: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"#FF69B4\"><path d=\"M440-501Zm0 381L313-234q-72-65-123.5-116t-85-96q-33.5-45-49-87T40-621q0-94 63-156.5T260-840q52 0 99 22t81 62q34-40 81-62t99-22q81 0 136 45.5T831-680h-85q-18-40-53-60t-73-20q-51 0-88 27.5T463-660h-46q-31-45-70.5-72.5T260-760q-57 0-98.5 39.5T120-621q0 33 14 67t50 78.5q36 44.5 98 104T440-228q26-23 61-53t56-50l9 9 19.5 19.5L605-283l9 9q-22 20-56 49.5T498-172l-58 52Zm280-160v-120H600v-80h120v-120h80v120h120v80H800v120h-80Z\"/></svg>",
heartMinus: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"#FF0000\"><path d=\"M440-501Zm0 381L313-234q-72-65-123.5-116t-85-96q-33.5-45-49-87T40-621q0-94 63-156.5T260-840q52 0 99 22t81 62q34-40 81-62t99-22q84 0 153 59t69 160q0 14-2 29.5t-6 31.5h-85q5-18 8-34t3-30q0-75-50-105.5T620-760q-51 0-88 27.5T463-660h-46q-31-45-70.5-72.5T260-760q-57 0-98.5 39.5T120-621q0 33 14 67t50 78.5q36 44.5 98 104T440-228q26-23 61-53t56-50l9 9 19.5 19.5L605-283l9 9q-22 20-56 49.5T498-172l-58 52Zm160-280v-80h320v80H600Z\"/></svg>",
heartCheck: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"#51b330\"><path d=\"M718-313 604-426l57-56 57 56 141-141 57 56-198 198ZM440-501Zm0 381L313-234q-72-65-123.5-116t-85-96q-33.5-45-49-87T40-621q0-94 63-156.5T260-840q52 0 99 22t81 62q34-40 81-62t99-22q81 0 136 45.5T831-680h-85q-18-40-53-60t-73-20q-51 0-88 27.5T463-660h-46q-31-45-70.5-72.5T260-760q-57 0-98.5 39.5T120-621q0 33 14 67t50 78.5q36 44.5 98 104T440-228q26-23 61-53t56-50l9 9 19.5 19.5L605-283l9 9q-22 20-56 49.5T498-172l-58 52Z\"/></svg>",
error: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"#FF0000\"><path d=\"M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z\"/></svg>",
warning: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"#DAB600\"><path d=\"m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z\"/></svg>",
empty: "<button>123</button>"
};
const DEFAULTS = {
columnCount: 6,
resultsPerPage: 200
};
const ADDED_FAVORITE_STATUS = {
error: 0,
alreadyAdded: 1,
notLoggedIn: 2,
success: 3
};
const STYLES = {
thumbHoverOutline: `
.thumb-node,
.thumb {
>a,
>span,
>div {
&:hover {
outline: 3px solid #0075FF;
}
}
}`,
thumbHoverOutlineDisabled: `
.thumb-node,
.thumb {
>a,
>span,
>div:not(:has(img.video)) {
&:hover {
outline: none;
}
}
}`
};
const TYPEABLE_INPUTS = new Set([
"color",
"email",
"number",
"password",
"search",
"tel",
"text",
"url",
"datetime"
]);
/**
* @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_LOCAL_STORAGE_KEY) || "{}");
preferences[key] = value;
localStorage.setItem(PREFERENCES_LOCAL_STORAGE_KEY, JSON.stringify(preferences));
}
/**
* @param {String} key
* @param {any} defaultValue
* @returns {String | null}
*/
function getPreference(key, defaultValue) {
const preferences = JSON.parse(localStorage.getItem(PREFERENCES_LOCAL_STORAGE_KEY) || "{}");
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() {
if (!FLAGS.userIsOnTheirOwnFavoritesPage.set) {
FLAGS.userIsOnTheirOwnFavoritesPage.value = getUserId() === getFavoritesPageId();
FLAGS.userIsOnTheirOwnFavoritesPage.set = true;
}
return FLAGS.userIsOnTheirOwnFavoritesPage.value;
}
/**
* @param {String} url
* @param {Function} onSuccess
* @param {Number} delayIncrement
* @param {Number} delay
*/
function requestPageInformation(url, onSuccess, delay = 0) {
const delayIncrement = 500;
setTimeout(() => {
fetch((url))
.then((response) => {
if (response.status === 503) {
requestPageInformation(url, onSuccess, delay + delayIncrement);
}
return response.text();
})
.then((html) => {
onSuccess(html);
});
}, 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} value
*/
function forceHideCaptions(value) {
for (const caption of document.getElementsByClassName("caption")) {
if (value) {
caption.classList.add("remove");
caption.classList.add("inactive");
} else {
caption.classList.remove("remove");
}
}
}
/**
* @param {HTMLElement} thumb
* @returns {String | null}
*/
function getRemoveFavoriteButtonFromThumb(thumb) {
return thumb.querySelector(".remove-favorite-button");
}
/**
* @param {HTMLElement} thumb
* @returns {String | null}
*/
function getAddFavoriteButtonFromThumb(thumb) {
return thumb.querySelector(".add-favorite-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.<HTMLElement>}
*/
function getAllThumbs() {
const className = onSearchPage() ? "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} imageURL
* @returns {String}
*/
function getExtensionFromImageURL(imageURL) {
try {
return (/\.(png|jpg|jpeg|gif)/g).exec(imageURL)[1];
} catch (error) {
return "jpg";
}
}
/**
* @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) {
if (onSearchPage()) {
const image = getImageFromThumb(thumb);
return image.hasAttribute("tags") ? image.getAttribute("tags") : image.title;
}
const thumbNode = ThumbNode.allThumbNodes.get(thumb.id);
return thumbNode === undefined ? "" : thumbNode.finalTags;
}
/**
* @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 {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]
];
}
}
/**
* @param {String} tags
* @returns {String}
*/
function negateTags(tags) {
return tags.replace(/(\S+)/g, "-$1");
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns {HTMLDivElement | null}
*/
function getAwesompleteFromInput(input) {
const awesomplete = input.parentElement;
if (awesomplete === null || awesomplete.className !== "awesomplete") {
return null;
}
return awesomplete;
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns {Boolean}
*/
function awesompleteIsVisible(input) {
const awesomplete = getAwesompleteFromInput(input);
if (awesomplete === null) {
return false;
}
const awesompleteSuggestions = awesomplete.querySelector("ul");
return awesompleteSuggestions !== null && !awesompleteSuggestions.hasAttribute("hidden");
}
/**
*
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns
*/
function awesompleteIsUnselected(input) {
const awesomplete = getAwesompleteFromInput(input);
if (awesomplete === null) {
return true;
}
if (!awesompleteIsVisible(input)) {
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 | HTMLTextAreaElement} 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);
}
}
/**
* @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 onSearchPage() {
if (!FLAGS.onSearchPage.set) {
FLAGS.onSearchPage.value = location.href.includes("page=post&s=list");
FLAGS.onSearchPage.set = true;
}
return FLAGS.onSearchPage.value;
}
/**
* @returns {Boolean}
*/
function onFavoritesPage() {
if (!FLAGS.onFavoritesPage.set) {
FLAGS.onFavoritesPage.value = location.href.includes("page=favorites");
FLAGS.onFavoritesPage.set = true;
}
return FLAGS.onFavoritesPage.value;
}
/**
* @returns {Boolean}
*/
function onPostPage() {
if (!FLAGS.onPostPage.set) {
FLAGS.onPostPage.value = location.href.includes("page=post&s=view");
FLAGS.onPostPage.set = true;
}
return FLAGS.onPostPage.value;
}
/**
* @returns {String[]}
*/
function getIdsToDeleteOnReload() {
return JSON.parse(localStorage.getItem(IDS_TO_REMOVE_ON_RELOAD_KEY)) || [];
}
/**
* @param {String} postId
*/
function setIdToBeRemovedOnReload(postId) {
const idsToRemoveOnReload = getIdsToDeleteOnReload();
idsToRemoveOnReload.push(postId);
localStorage.setItem(IDS_TO_REMOVE_ON_RELOAD_KEY, JSON.stringify(idsToRemoveOnReload));
}
function clearIdsToDeleteOnReload() {
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);
}
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;
}
input[type=number] {
border: 1px solid #767676;
border-radius: 2px;
}
.size-calculation-div {
position: absolute !important;
top: 0;
left: 0;
width: 100%;
height: 100%;
visibility: hidden;
transition: none !important;
transform: scale(1.1, 1.1);
}
`, "utilities-common-styles");
injectStyleHTML(STYLES.thumbHoverOutline, "thumb-hover-outlines");
setTimeout(() => {
if (onSearchPage()) {
removeInlineImgStyles();
}
configureVideoOutlines();
}, 100);
}
/**
* @param {Boolean} value
*/
function toggleFancyImageHovering(value) {
if (onMobileDevice() || onSearchPage()) {
value = false;
}
if (!value) {
const style = document.getElementById("fancy-image-hovering");
if (style !== null) {
style.remove();
}
return;
}
injectStyleHTML(`
#content {
padding: 40px 40px 30px !important;
grid-gap: 2.5em !important;
}
.thumb-node,
.thumb {
>a,
>span,
>div {
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
transition: transform 0.2s ease-in-out;
position: relative;
&::after {
content: '';
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
opacity: 0;
top: 0;
left: 0;
border-radius: 5px;
box-shadow: 5px 10px 15px rgba(0,0,0,0.45);
transition: opacity 0.3s ease-in-out;
}
&:hover {
outline: none !important;
transform: scale(1.1, 1.1);
z-index: 10;
img {
outline: none !important;
}
&::after {
opacity: 1;
}
}
}
}
`, "fancy-image-hovering");
}
function configureVideoOutlines() {
const size = onMobileDevice() ? 2 : 3;
injectStyleHTML(`
.thumb-node, .thumb {
>a,
>div {
&:has(img.video) {
outline: ${size}px solid blue;
}
&:has(img.gif) {
outline: 2px solid hotpink;
}
}
}
`, "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");
injectStyleHTML(`
input[type=number] {
background-color: #303030;
color: white;
}
`, "dark-theme-number-input");
injectStyleHTML(`
#favorites-pagination-container {
>button {
border: 1px solid white !important;
color: white !important;
}
}
`, "pagination-style");
}
}
}, 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 getThumbById(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() {
if (onPostPage()) {
return;
}
const enableOnSearchPages = getPreference("enableOnSearchPages", false) && getPerformanceProfile() === 0;
if (!enableOnSearchPages && onSearchPage()) {
throw new Error("Disabled on search pages");
}
injectCommonStyles();
toggleFancyImageHovering(true);
setTheme();
removeBlacklistedThumbs();
prefetchAdjacentSearchPages();
}
function prefetchAdjacentSearchPages() {
if (!onSearchPage()) {
return;
}
const id = "search-page-prefetch";
const alreadyPrefetched = document.getElementById(id) !== null;
if (alreadyPrefetched) {
return;
}
const container = document.createElement("div");
const currentPage = document.getElementById("paginator").children[0].querySelector("b");
for (const sibling of [currentPage.previousElementSibling, currentPage.nextElementSibling]) {
if (sibling !== null && sibling.tagName.toLowerCase() === "a") {
container.appendChild(createPrefetchLink(sibling.href));
}
}
container.id = "search-page-prefetch";
document.head.appendChild(container);
}
/**
* @param {String} url
* @returns {HTMLLinkElement}
*/
function createPrefetchLink(url) {
const link = document.createElement("link");
link.rel = "prefetch";
link.href = url;
return link;
}
function removeBlacklistedThumbs() {
if (!onSearchPage()) {
return;
}
const blacklistedThumbs = Array.from(document.getElementsByClassName("blacklisted-image"));
for (const thumb of blacklistedThumbs) {
thumb.remove();
}
}
/**
* @returns {String}
*/
function getTagBlacklist() {
let tags = getCookie("tag_blacklist", "");
for (let i = 0; i < 3; i += 1) {
tags = decodeURIComponent(tags).replace(/(?:^| )-/, "");
}
return tags;
}
/**
* @returns {Boolean}
*/
function usingCaptions() {
const result = document.getElementById("captionList") !== null;
return result;
}
/**
* @returns {Boolean}
*/
function usingRenderer() {
if (!FLAGS.usingRenderer.set) {
FLAGS.usingRenderer.value = document.getElementById("original-content-container") !== null;
FLAGS.usingRenderer.set = true;
}
return FLAGS.usingRenderer.value;
}
/**
* @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 hasVideoTag = (/(?:^|\s)video(?:$|\s)/).test(tags);
const hasAnimatedTag = (/(?:^|\s)animated(?:$|\s)/).test(tags);
const isAnimated = hasAnimatedTag || hasVideoTag;
const isAGif = hasAnimatedTag && !hasVideoTag;
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\s+/g, " ");
}
/**
* @param {String} string
* @param {String} replacement
* @returns {String}
*/
function replaceLineBreaks(string, replacement = "") {
return string.replace(/(\r\n|\n|\r)/gm, replacement);
}
/**
*
* @param {HTMLImageElement} image
* @returns {Boolean}
*/
function imageIsLoaded(image) {
return image.complete || image.naturalWidth !== 0;
}
/**
* @returns {Boolean}
*/
function usingFirefox() {
if (!FLAGS.usingFirefox.set) {
FLAGS.usingFirefox.value = navigator.userAgent.toLowerCase().includes("firefox");
FLAGS.usingFirefox.set = true;
}
return FLAGS.usingFirefox.value;
}
/**
* @returns {Boolean}
*/
function onMobileDevice() {
if (!FLAGS.onMobileDevice.set) {
FLAGS.onMobileDevice.value = (/iPhone|iPad|iPod|Android/i).test(navigator.userAgent);
FLAGS.onMobileDevice.set = true;
}
return FLAGS.onMobileDevice.value;
}
/**
* @returns {Number}
*/
function getPerformanceProfile() {
return parseInt(getPreference("performanceProfile", 0));
}
/**
* @param {String} tagName
* @returns {Promise.<Boolean>}
*/
function isOfficialTag(tagName) {
const tagPageURL = `https://rule34.xxx/index.php?page=tags&s=list&tags=${tagName}`;
return 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");
return columnOfFirstRow.length === 3;
})
.catch((error) => {
console.error(error);
return false;
});
}
/**
* @param {String} searchQuery
*/
function openSearchPage(searchQuery) {
window.open(`https://rule34.xxx/index.php?page=post&s=list&tags=${encodeURIComponent(searchQuery)}`);
}
/**
* @param {Map} map
* @returns {Object}
*/
function mapToObject(map) {
return Array.from(map).reduce((object, [key, value]) => {
object[key] = value;
return object;
}, {});
}
/**
* @param {Object} object
* @returns {Map}
*/
function objectToMap(object) {
return new Map(Object.entries(object));
}
/**
* @param {String} string
* @returns {Boolean}
*/
function isNumber(string) {
return (/^\d+$/).test(string);
}
/**
* @param {String} id
* @returns {Promise.<Number>}
*/
function addFavorite(id) {
fetch(`https://rule34.xxx/index.php?page=post&s=vote&id=${id}&type=up`);
return fetch(`https://rule34.xxx/public/addfav.php?id=${id}`)
.then((response) => {
return response.text();
})
.then((html) => {
return parseInt(html);
})
.catch(() => {
return ADDED_FAVORITE_STATUS.error;
});
}
/**
* @param {String} id
*/
function removeFavorite(id) {
setIdToBeRemovedOnReload(id);
fetch(`https://rule34.xxx/index.php?page=favorites&s=delete&id=${id}`);
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @param {String} suggestion
*/
function insertSuggestion(input, suggestion) {
const cursorAtEnd = input.selectionStart === input.value.length;
const firstHalf = input.value.slice(0, input.selectionStart);
const secondHalf = input.value.slice(input.selectionStart);
const firstHalfWithPrefixRemoved = firstHalf.replace(/(\s?)(-?)\S+$/, "$1$2");
const combinedHalves = removeExtraWhiteSpace(`${firstHalfWithPrefixRemoved}${suggestion} ${secondHalf}`);
const result = cursorAtEnd ? `${combinedHalves} ` : combinedHalves;
const selectionStart = firstHalfWithPrefixRemoved.length + suggestion.length + 1;
input.value = result;
input.selectionStart = selectionStart;
input.selectionEnd = selectionStart;
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
*/
function hideAwesomplete(input) {
getAwesompleteFromInput(input).querySelector("ul").setAttribute("hidden", "");
}
/**
* @param {String} svg
* @param {Number} duration
*/
function showFullscreenIcon(svg, duration = 500) {
const svgDocument = new DOMParser().parseFromString(svg, "image/svg+xml");
const svgElement = svgDocument.documentElement;
const svgOverlay = document.createElement("div");
svgOverlay.classList.add("fullscreen-icon");
svgOverlay.innerHTML = new XMLSerializer().serializeToString(svgElement);
svgOverlay.width = "90vw";
document.body.appendChild(svgOverlay);
setTimeout(() => {
svgOverlay.remove();
}, duration);
}
/**
* @param {String} svg
* @returns {String}
*/
function createObjectURLFromSvg(svg) {
const blob = new Blob([svg], {
type: "image/svg+xml"
});
return URL.createObjectURL(blob);
}
/**
* @param {HTMLElement} element
* @returns {Boolean}
*/
function isTypeableInput(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === "textarea") {
return true;
}
if (tagName === "input") {
return TYPEABLE_INPUTS.has(element.getAttribute("type"));
}
return false;
}
initializeUtilities();
// metadata.js
class FavoriteMetadata {
/**
* @type {Map.<String, FavoriteMetadata>}
*/
static allMetadata = new Map();
static parser = new DOMParser();
/**
* @type {FavoriteMetadata[]}
*/
static missingMetadataFetchQueue = [];
/**
* @type {FavoriteMetadata[]}
*/
static deletedPostFetchQueue = [];
static currentlyFetchingFromQueue = false;
static allFavoritesLoaded = false;
static fetchDelay = {
normal: 10,
deleted: 300
};
static postStatisticsRegex = /Posted:\s*(\S+\s\S+).*Size:\s*(\d+)x(\d+).*Rating:\s*(\S+).*Score:\s*(\d+)/gm;
/**
* @param {FavoriteMetadata} favoriteMetadata
*/
static async fetchMissingMetadata(favoriteMetadata) {
if (favoriteMetadata !== undefined) {
FavoriteMetadata.missingMetadataFetchQueue.push(favoriteMetadata);
}
if (FavoriteMetadata.currentlyFetchingFromQueue) {
return;
}
FavoriteMetadata.currentlyFetchingFromQueue = true;
while (FavoriteMetadata.missingMetadataFetchQueue.length > 0) {
const metadata = this.missingMetadataFetchQueue.pop();
if (metadata.postIsDeleted) {
metadata.populateMetadataFromPost();
} else {
metadata.populateMetadataFromAPI(true);
}
await sleep(metadata.fetchDelay);
}
FavoriteMetadata.currentlyFetchingFromQueue = false;
}
/**
* @param {String} rating
* @returns {Number}
*/
static encodeRating(rating) {
return {
"Explicit": 4,
"E": 4,
"e": 4,
"Questionable": 2,
"Q": 2,
"q": 2,
"Safe": 1,
"S": 1,
"s": 1
}[rating] || 4;
}
static {
if (!onPostPage()) {
window.addEventListener("favoritesLoaded", () => {
FavoriteMetadata.allFavoritesLoaded = true;
FavoriteMetadata.missingMetadataFetchQueue = FavoriteMetadata.missingMetadataFetchQueue.concat(FavoriteMetadata.deletedPostFetchQueue);
FavoriteMetadata.fetchMissingMetadata();
}, {
once: true
});
}
}
/**
* @type {String}
*/
id;
/**
* @type {Number}
*/
width;
/**
* @type {Number}
*/
height;
/**
* @type {Number}
*/
score;
/**
* @type {Number}
*/
rating;
/**
* @type {Number}
*/
creationTimestamp;
/**
* @type {Number}
*/
lastChangedTimestamp;
/**
* @type {Boolean}
*/
postIsDeleted;
/**
* @returns {String}
*/
get apiURL() {
return `https://api.rule34.xxx//index.php?page=dapi&s=post&q=index&id=${this.id}`;
}
get postURL() {
return `https://rule34.xxx/index.php?page=post&s=view&id=${this.id}`;
}
get fetchDelay() {
return this.postIsDeleted ? FavoriteMetadata.fetchDelay.deleted : FavoriteMetadata.fetchDelay.normal;
}
/**
* @type {String}
*/
get json() {
return JSON.stringify({
width: this.width,
height: this.height,
score: this.score,
rating: this.rating,
create: this.creationTimestamp,
change: this.lastChangedTimestamp,
deleted: this.postIsDeleted
});
}
/**
* @type {Number}
*/
get pixelCount() {
return this.width * this.height;
}
/**
* @param {String} id
* @param {Object.<String, String>} record
*/
constructor(id, record) {
this.id = id;
this.setDefaults();
this.populateMetadata(record);
this.addInstanceToAllMetadata();
}
setDefaults() {
this.width = 0;
this.height = 0;
this.score = 0;
this.creationTimestamp = 0;
this.lastChangedTimestamp = 0;
// this.rating = 4;
this.postIsDeleted = false;
}
/**
* @param {Number} rating
*/
presetRating(rating) {
this.rating = rating;
}
/**
* @param {Object.<String, String>} record
*/
populateMetadata(record) {
if (record === undefined) {
this.populateMetadataFromAPI();
} else if (record === null) {
FavoriteMetadata.fetchMissingMetadata(this, true);
} else {
this.populateMetadataFromRecord(JSON.parse(record));
if (this.isEmpty()) {
FavoriteMetadata.fetchMissingMetadata(this, true);
}
}
}
/**
* @param {Boolean} missingInDatabase
*/
populateMetadataFromAPI(missingInDatabase = false) {
fetch(this.apiURL)
.then((response) => {
return response.text();
})
.then((html) => {
const dom = FavoriteMetadata.parser.parseFromString(html, "text/html");
const metadata = dom.querySelector("post");
if (metadata === null) {
throw new Error(`metadata is null - ${this.apiURL}`, {
cause: "DeletedMetadata"
});
}
this.width = parseInt(metadata.getAttribute("width"));
this.height = parseInt(metadata.getAttribute("height"));
this.score = parseInt(metadata.getAttribute("score"));
this.rating = FavoriteMetadata.encodeRating(metadata.getAttribute("rating"));
this.creationTimestamp = Date.parse(metadata.getAttribute("created_at"));
this.lastChangedTimestamp = parseInt(metadata.getAttribute("change"));
const extension = getExtensionFromImageURL(metadata.getAttribute("file_url"));
if (extension !== "mp4") {
dispatchEvent(new CustomEvent("favoriteMetadataFetched", {
detail: {
id: this.id,
extension
}
}));
}
if (missingInDatabase) {
dispatchEvent(new CustomEvent("missingMetadata", {
detail: this.id
}));
}
})
.catch((error) => {
if (error.cause === "DeletedMetadata") {
this.postIsDeleted = true;
FavoriteMetadata.deletedPostFetchQueue.push(this);
} else if (error.message === "Failed to fetch") {
FavoriteMetadata.missingMetadataFetchQueue.push(this);
} else {
console.error(error);
}
});
}
/**
* @param {Object.<String, String>} record
*/
populateMetadataFromRecord(record) {
this.width = record.width;
this.height = record.height;
this.score = record.score;
this.rating = record.rating;
this.creationTimestamp = record.create;
this.lastChangedTimestamp = record.change;
this.postIsDeleted = record.deleted;
}
populateMetadataFromPost() {
fetch(this.postURL)
.then((response) => {
return response.text();
})
.then((html) => {
const dom = FavoriteMetadata.parser.parseFromString(html, "text/html");
const statistics = dom.getElementById("stats");
if (statistics === null) {
return;
}
const textContent = replaceLineBreaks(statistics.textContent.trim(), " ");
const match = FavoriteMetadata.postStatisticsRegex.exec(textContent);
FavoriteMetadata.postStatisticsRegex.lastIndex = 0;
if (!match) {
return;
}
this.width = parseInt(match[2]);
this.height = parseInt(match[3]);
this.score = parseInt(match[5]);
this.rating = FavoriteMetadata.encodeRating(match[4]);
this.creationTimestamp = Date.parse(match[1]);
this.lastChangedTimestamp = this.creationTimestamp / 1000;
if (FavoriteMetadata.allFavoritesLoaded) {
dispatchEvent(new CustomEvent("missingMetadata", {
detail: this.id
}));
}
});
}
/**
* @returns {Boolean}
*/
isEmpty() {
return this.width === 0 && this.height === 0;
}
/**
* @param {{metric: String, operator: String, value: String, negated: Boolean}[]} filters
* @returns {Boolean}
*/
satisfiesAllFilters(filters) {
for (const expression of filters) {
if (!this.satisfiesExpression(expression)) {
return false;
}
}
return true;
}
/**
* @param {MetadataSearchExpression} expression
* @returns {Boolean}
*/
satisfiesExpression(expression) {
const metricMap = {
"id": this.id,
"width": this.width,
"height": this.height,
"score": this.score
};
const metricValue = metricMap[expression.metric] || 0;
const value = metricMap[expression.value] || expression.value;
return this.evaluateExpression(metricValue, expression.operator, value);
}
/**
* @param {Number} metricValue
* @param {String} operator
* @param {Number} value
* @returns {Boolean}
*/
evaluateExpression(metricValue, operator, value) {
let result = false;
switch (operator) {
case ":":
result = metricValue === value;
break;
case ":<":
result = metricValue < value;
break;
case ":>":
result = metricValue > value;
break;
default:
break;
}
return result;
}
addInstanceToAllMetadata() {
if (!FavoriteMetadata.allMetadata.has(this.id)) {
FavoriteMetadata.allMetadata.set(this.id, this);
}
}
}
// thumb_node.js
class ThumbNode {
/**
* @type {Map.<String, ThumbNode>}
*/
static allThumbNodes = new Map();
/**
* @type {RegExp}
*/
static thumbSourceExtractionRegex = /thumbnails\/\/([0-9]+)\/thumbnail_([0-9a-f]+)/;
/**
* @type {DOMParser}
*/
static parser = new DOMParser();
/**
* @type {HTMLElement}
*/
static template;
/**
* @type {String}
*/
static removeFavoriteButtonHTML;
/**
* @type {String}
*/
static addFavoriteButtonHTML;
static {
if (!onPostPage()) {
this.createTemplates();
this.addEventListeners();
}
}
static createTemplates() {
ThumbNode.template = ThumbNode.parser.parseFromString("<div class=\"thumb-node\"></div>", "text/html").createElement("div");
const canvasHTML = getPerformanceProfile() > 0 ? "" : "<canvas></canvas>";
const heartPlusBlobURL = createObjectURLFromSvg(ICONS.heartPlus);
const heartMinusBlobURL = createObjectURLFromSvg(ICONS.heartMinus);
const heartPlusImageHTML = `<img src=${heartPlusBlobURL}>`;
const heartMinusImageHTML = `<img src=${heartMinusBlobURL}>`;
ThumbNode.removeFavoriteButtonHTML = `<button class="remove-favorite-button auxillary-button">${heartMinusImageHTML}</button>`;
ThumbNode.addFavoriteButtonHTML = `<button class="add-favorite-button auxillary-button">${heartPlusImageHTML}</button>`;
const auxillaryButtonHTML = userIsOnTheirOwnFavoritesPage() ? ThumbNode.removeFavoriteButtonHTML : ThumbNode.addFavoriteButtonHTML;
ThumbNode.template.className = "thumb-node";
ThumbNode.template.innerHTML = `
<div>
<img loading="lazy">
${auxillaryButtonHTML}
${canvasHTML}
</div>
`;
}
static addEventListeners() {
window.addEventListener("favoriteAddedOrDeleted", (event) => {
const id = event.detail;
const thumbNode = this.allThumbNodes.get(id);
if (thumbNode !== undefined) {
thumbNode.swapAuxillaryButton();
}
});
}
/**
* @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 = onSearchPage() ? thumb : thumb.children[0];
return elementWithId.id.substring(1);
}
/**
* @param {HTMLElement} thumb
* @returns {Number}
*/
static extractRatingFromThumb(thumb) {
const rating = (/'rating':'(\S)/).exec(thumb.nextSibling.textContent)[1];
return FavoriteMetadata.encodeRating(rating);
}
/**
* @param {String} id
* @returns {Number}
*/
static getPixelCount(id) {
const thumbNode = ThumbNode.allThumbNodes.get(id);
if (thumbNode === undefined || thumbNode.metadata === undefined) {
return 0;
}
return thumbNode.metadata.pixelCount;
}
/**
* @type {Map.<String, ThumbNode>}
*/
static get thumbNodesMatchedBySearch() {
const thumbNodes = new Map();
for (const [id, thumbNode] of ThumbNode.allThumbNodes.entries()) {
if (thumbNode.matchedByMostRecentSearch) {
thumbNodes.set(id, thumbNode);
}
}
return thumbNodes;
}
/**
* @param {String} id
* @returns {String}
*/
static getExtensionFromThumbNode(id) {
const thumbNode = ThumbNode.allThumbNodes.get(id);
if (thumbNode === undefined) {
return undefined;
}
if (thumbNode.metadata.isEmpty()) {
return undefined;
}
return thumbNode.metadata.extension;
}
/**
* @type {HTMLDivElement}
*/
root;
/**
* @type {String}
*/
id;
/**
* @type {HTMLElement}
*/
container;
/**
* @type {HTMLImageElement}
*/
image;
/**
* @type {HTMLButtonElement}
*/
auxillaryButton;
/**
* @type {String}
*/
additionalTags;
/**
* @type {Set.<String>}
*/
tagSet;
/**
* @type {Boolean}
*/
matchedByMostRecentSearch;
/**
* @type {FavoriteMetadata}
*/
metadata;
/**
* @type {String}
*/
get href() {
return `https://rule34.xxx/index.php?page=post&s=view&id=${this.id}`;
}
/**
* @type {String}
*/
get compressedThumbSource() {
return this.image.src.match(ThumbNode.thumbSourceExtractionRegex).splice(1).join("_");
}
/**
* @type {{id: String, tags: String, src: String, metadata: String}}
*/
get databaseRecord() {
return {
id: this.id,
tags: this.originalTags,
src: this.compressedThumbSource,
metadata: this.metadata.json
};
}
/**
* @type {String}
*/
get originalTags() {
return Array.from(this.tagSet).join(" ");
}
/**
* @type {String}
*/
get finalTags() {
return this.mergeTags(this.originalTags, this.additionalTags);
}
/**
* @param {HTMLElement | {id: String, tags: String, src: String, type: String}} thumb
* @param {Boolean} fromRecord
*/
constructor(thumb, fromRecord) {
this.instantiateTemplate();
this.populateAttributes(thumb, fromRecord);
this.setupAuxillaryButton();
this.setupClickLink();
this.setMatched(true);
this.addInstanceToAllThumbNodes();
}
instantiateTemplate() {
this.root = ThumbNode.template.cloneNode(true);
this.container = this.root.children[0];
this.image = this.root.children[0].children[0];
this.auxillaryButton = this.root.children[0].children[1];
}
setupAuxillaryButton() {
if (userIsOnTheirOwnFavoritesPage()) {
this.auxillaryButton.onclick = this.removeFavoriteButtonOnClick.bind(this);
} else {
this.auxillaryButton.onclick = this.addFavoriteButtonOnClick.bind(this);
}
}
/**
* @param {MouseEvent} event
*/
removeFavoriteButtonOnClick(event) {
event.stopPropagation();
removeFavorite(this.id);
this.swapAuxillaryButton();
}
/**
* @param {MouseEvent} event
*/
addFavoriteButtonOnClick(event) {
event.stopPropagation();
addFavorite(this.id);
this.swapAuxillaryButton();
}
swapAuxillaryButton() {
const isRemoveFavoriteButton = this.auxillaryButton.classList.contains("remove-favorite-button");
if (isRemoveFavoriteButton) {
this.auxillaryButton.outerHTML = ThumbNode.addFavoriteButtonHTML;
this.auxillaryButton = this.root.children[0].children[1];
this.auxillaryButton.onclick = this.addFavoriteButtonOnClick.bind(this);
} else {
this.auxillaryButton.outerHTML = ThumbNode.removeFavoriteButtonHTML;
this.auxillaryButton = this.root.children[0].children[1];
this.auxillaryButton.onclick = this.removeFavoriteButtonOnClick.bind(this);
}
}
/**
* @param {HTMLElement | {id: String, tags: String, src: String, type: String, metadata: String}} thumb
* @param {Boolean} fromDatabaseRecord
*/
populateAttributes(thumb, fromDatabaseRecord) {
if (fromDatabaseRecord) {
this.populateAttributesFromDatabaseRecord(thumb);
} else {
this.populateAttributesFromHTMLElement(thumb);
}
this.root.id = this.id;
this.additionalTags = TagModifier.tagModifications.get(this.id) || "";
this.updateTags();
}
/**
* @param {{id: String, tags: String, src: String, type: String, metadata: String}} record
*/
populateAttributesFromDatabaseRecord(record) {
this.image.src = ThumbNode.decompressThumbSource(record.src, record.id);
this.id = record.id;
this.tagSet = this.createTagSet(record.tags);
this.image.className = record.type;
if (record.metadata === undefined) {
record.metadata = null;
}
this.metadata = new FavoriteMetadata(this.id, record.metadata);
}
/**
* @param {HTMLElement} thumb
*/
populateAttributesFromHTMLElement(thumb) {
if (onMobileDevice()) {
const noScript = thumb.querySelector("noscript");
if (noScript !== null) {
thumb.children[0].insertAdjacentElement("afterbegin", noScript.children[0]);
}
}
const imageElement = thumb.children[0].children[0];
this.image.src = imageElement.src;
this.id = ThumbNode.getIdFromThumb(thumb);
const thumbTags = `${correctMisspelledTags(imageElement.title)} ${this.id}`;
this.tagSet = this.createTagSet(thumbTags);
this.image.classList.add(getContentType(thumbTags));
this.metadata = new FavoriteMetadata(this.id);
this.metadata.presetRating(ThumbNode.extractRatingFromThumb(thumb));
}
setupClickLink() {
if (usingRenderer()) {
this.container.setAttribute("href", this.href);
} else {
this.container.setAttribute("onclick", `window.open("${this.href}")`);
}
}
/**
* @param {HTMLElement} element
* @param {String} position
*/
insertInDocument(element, position) {
element.insertAdjacentElement(position, this.root);
}
addInstanceToAllThumbNodes() {
if (!ThumbNode.allThumbNodes.has(this.id)) {
ThumbNode.allThumbNodes.set(this.id, this);
}
}
toggleMatched() {
this.matchedByMostRecentSearch = !this.matchedByMostRecentSearch;
}
/**
* @param {Boolean} value
*/
setMatched(value) {
this.matchedByMostRecentSearch = value;
}
/**
*
* @param {String} oldTags
* @param {String} newTags
* @returns {String}
*/
mergeTags(oldTags, newTags) {
if (newTags === "") {
return oldTags;
}
oldTags = removeExtraWhiteSpace(oldTags);
newTags = removeExtraWhiteSpace(newTags);
const finalTags = new Set(oldTags.split(" "));
for (const newTag of newTags.split(" ")) {
if (newTag !== "") {
finalTags.add(newTag);
}
}
if (finalTags.size === 0) {
return "";
}
return removeExtraWhiteSpace(Array.from(finalTags.keys()).join(" "));
}
/**
*
* @param {String} tags
* @returns {Set.<String>}
*/
createTagSet(tags) {
return new Set(removeExtraWhiteSpace(tags).split(" ").sort());
}
updateTags() {
if (this.additionalTags !== "") {
this.tagSet = this.createTagSet(this.finalTags);
}
}
/**
* @param {String} newTags
* @returns {String}
*/
addAdditionalTags(newTags) {
this.additionalTags = this.mergeTags(this.additionalTags, newTags);
this.updateTags();
return this.additionalTags;
}
/**
* @param {String} tagsToRemove
* @returns {String}
*/
removeAdditionalTags(tagsToRemove) {
const tagsToRemoveList = tagsToRemove.split(" ");
this.additionalTags = Array.from(this.additionalTags.split(" "))
.filter(tag => !tagsToRemoveList.includes(tag))
.join(" ");
this.updateTags();
return this.additionalTags;
}
resetAdditionalTags() {
if (this.additionalTags === "") {
return;
}
this.additionalTags = "";
this.updateTags();
}
}
// match.js
/* eslint-disable max-classes-per-file */
class SearchTag {
/**
* @type {String}
*/
value;
/**
* @type {Boolean}
*/
negated;
/**
* @type {Number}
*/
get cost() {
return 0;
}
/**
* @type {Number}
*/
get finalCost() {
return this.negated ? this.cost + 1 : this.cost;
}
/**
* @param {String} searchTag
* @param {Boolean} inOrGroup
*/
constructor(searchTag, inOrGroup) {
this.negated = inOrGroup ? false : searchTag.startsWith("-");
this.value = this.negated ? searchTag.substring(1) : searchTag;
}
/**
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
matches(thumbNode) {
if (thumbNode.tagSet.has(this.value)) {
return !this.negated;
}
return this.negated;
}
}
class WildCardSearchTag extends SearchTag {
static unmatchableRegex = /^\b$/;
static startsWithRegex = /^[^*]*\*$/;
/**
* @type {RegExp}
*/
regex;
/**
* @type {Boolean}
*/
equivalentToStartsWith;
/**
* @type {String}
*/
startsWithPrefix;
/**
* @type {Number}
*/
get cost() {
return this.equivalentToStartsWith ? 10 : 20;
}
/**
* @param {String} searchTag
* @param {Boolean} inOrGroup
*/
constructor(searchTag, inOrGroup) {
super(searchTag, inOrGroup);
this.regex = this.createWildcardRegex();
this.equivalentToStartsWith = WildCardSearchTag.startsWithRegex.test(searchTag);
this.startsWithPrefix = this.value.slice(0, -1);
}
/**
* @returns {RegExp}
*/
createWildcardRegex() {
try {
return new RegExp(`^${this.value.replaceAll(/\*/g, ".*")}$`);
} catch {
return WildCardSearchTag.unmatchableRegex;
}
}
/**
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
matches(thumbNode) {
if (this.equivalentToStartsWith) {
return this.matchesPrefix(thumbNode);
}
return this.matchesWildcard(thumbNode);
}
/**
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
matchesPrefix(thumbNode) {
for (const tag of thumbNode.tagSet.values()) {
if (tag.startsWith(this.startsWithPrefix)) {
return !this.negated;
}
if (this.startsWithPrefix < tag) {
break;
}
}
return this.negated;
}
/**
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
matchesWildcard(thumbNode) {
for (const tag of thumbNode.tagSet.values()) {
if (this.regex.test(tag)) {
return !this.negated;
}
}
return this.negated;
}
}
class MetadataSearchTag extends SearchTag {
static regex = /^-?(score|width|height|id)(:[<>]?)(\d+|score|width|height|id)$/;
/**
* @type {MetadataSearchExpression}
*/
expression;
/**
* @type {Number}
*/
get cost() {
return 0;
}
/**
* @param {String} searchTag
* @param {Boolean} inOrGroup
*/
constructor(searchTag, inOrGroup) {
super(searchTag, inOrGroup);
this.expression = this.createExpression(searchTag);
}
/**
* @param {String} searchTag
* @returns {MetadataSearchExpression}
*/
createExpression(searchTag) {
const extractedExpression = MetadataSearchTag.regex.exec(searchTag);
return new MetadataSearchExpression(
extractedExpression[1],
extractedExpression[2],
extractedExpression[3]
);
}
/**
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
matches(thumbNode) {
const metadata = FavoriteMetadata.allMetadata.get(thumbNode.id);
if (metadata === undefined) {
return false;
}
if (metadata.satisfiesExpression(this.expression)) {
return !this.negated;
}
return this.negated;
}
}
/**
* @param {String[]} searchTagValues
* @param {Boolean} isOrGroup
* @returns {SearchTag[]}
*/
function createSearchTagGroup(searchTagValues, isOrGroup) {
const uniqueSearchTagValues = new Set();
const searchTags = [];
for (const searchTagValue of searchTagValues) {
if (uniqueSearchTagValues.has(searchTagValue)) {
continue;
}
uniqueSearchTagValues.add(searchTagValue);
searchTags.push(createSearchTag(searchTagValue, isOrGroup));
}
return searchTags;
}
/**
* @param {String} searchTagValue
* @param {Boolean} inOrGroup
* @returns {SearchTag}
*/
function createSearchTag(searchTagValue, inOrGroup) {
if (MetadataSearchTag.regex.test(searchTagValue)) {
return new MetadataSearchTag(searchTagValue, inOrGroup);
}
if (searchTagValue.includes("*")) {
return new WildCardSearchTag(searchTagValue, inOrGroup);
}
return new SearchTag(searchTagValue, inOrGroup);
}
class SearchCommand {
/**
* @param {SearchTag[]} searchTags
*/
static sortByLeastExpensive(searchTags) {
searchTags.sort((a, b) => {
return a.finalCost - b.finalCost;
});
}
/**
* @type {SearchTag[][]}
*/
orGroups;
/**
* @type {SearchTag[]}
*/
remainingSearchTags;
/**
* @type {Boolean}
*/
isEmpty;
/**
* @param {String} searchQuery
*/
constructor(searchQuery) {
this.orGroups = [];
this.remainingSearchTags = [];
this.isEmpty = searchQuery.trim() === "";
if (this.isEmpty) {
return;
}
const {orGroups, remainingSearchTags} = extractTagGroups(searchQuery);
for (const orGroup of orGroups) {
this.orGroups.push(createSearchTagGroup(orGroup, true));
}
this.remainingSearchTags = createSearchTagGroup(remainingSearchTags, false);
this.optimizeSearchCommand();
}
optimizeSearchCommand() {
for (const orGroup of this.orGroups) {
SearchCommand.sortByLeastExpensive(orGroup);
}
SearchCommand.sortByLeastExpensive(this.remainingSearchTags);
this.orGroups.sort((a, b) => {
return a.length - b.length;
});
}
}
/**
* @param {SearchCommand} searchCommand
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
function matchesSearch(searchCommand, thumbNode) {
if (searchCommand.isEmpty) {
return true;
}
if (!matchesAllRemainingSearchTags(searchCommand.remainingSearchTags, thumbNode)) {
return false;
}
return matchesAllOrGroups(searchCommand.orGroups, thumbNode);
}
/**
* @param {SearchTag[]} remainingSearchTags
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
function matchesAllRemainingSearchTags(remainingSearchTags, thumbNode) {
for (const searchTag of remainingSearchTags) {
if (!searchTag.matches(thumbNode)) {
return false;
}
}
return true;
}
/**
* @param {SearchTag[][]} orGroups
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
function matchesAllOrGroups(orGroups, thumbNode) {
for (const orGroup of orGroups) {
if (!atLeastOneThumbNodeTagIsInOrGroup(orGroup, thumbNode)) {
return false;
}
}
return true;
}
/**
* @param {SearchTag[]} orGroup
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
function atLeastOneThumbNodeTagIsInOrGroup(orGroup, thumbNode) {
for (const orTag of orGroup) {
if (orTag.matches(thumbNode)) {
return true;
}
}
return false;
}
/**
* @param {String} searchTag
* @param {String[]} tags
* @returns {Boolean}
*/
function tagsMatchWildcardSearchTag(searchTag, tags) {
const wildcardRegex = new RegExp(`^${searchTag.replaceAll(/\*/g, ".*")}$`);
return tags.some(tag => wildcardRegex.test(tag));
}
// loader.js
/* eslint-disable no-bitwise */
class FavoritesLoader {
static loadingState = {
initial: 0,
fetchingFavorites: 1,
allFavoritesLoaded: 2,
loadingFavoritesFromDatabase: 3
};
static currentLoadingState = FavoritesLoader.loadingState.initial;
static databaseName = "Favorites";
static objectStoreName = `user${getFavoritesPageId()}`;
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, metadata: String}]} favorites
*/
storeFavorites(favorites) {
this.openConnection()
.then((connectionEvent) => {
/**
* @type {IDBDatabase}
*/
const database = connectionEvent.target.result;
const transaction = database.transaction(this.objectStoreName, "readwrite");
const objectStore = transaction.objectStore(this.objectStoreName);
transaction.oncomplete = () => {
postMessage({
response: "finishedStoring"
});
database.close();
};
transaction.onerror = (event) => {
console.error(event);
};
favorites.forEach(favorite => {
this.addContentTypeToFavorite(favorite);
objectStore.put(favorite);
});
})
.catch((event) => {
const error = event.target.error;
if (error.name === "VersionError") {
this.version += 1;
this.storeFavorites(favorites);
} else {
console.error(error);
}
});
}
/**
* @param {String[]} idsToDelete
*/
async loadFavorites(idsToDelete) {
let loadedFavorites = {};
await this.openConnection()
.then(async(connectionEvent) => {
/**
* @type {IDBDatabase}
*/
const database = connectionEvent.target.result;
const transaction = database.transaction(this.objectStoreName, "readwrite");
const objectStore = transaction.objectStore(this.objectStoreName);
const index = objectStore.index("id");
transaction.onerror = (event) => {
console.error(event);
};
transaction.oncomplete = () => {
postMessage({
response: "finishedLoading",
favorites: loadedFavorites
});
database.close();
};
for (const id of idsToDelete) {
const deleteRequest = index.getKey(id);
await new Promise((resolve, reject) => {
deleteRequest.onsuccess = resolve;
deleteRequest.onerror = reject;
}).then((indexEvent) => {
const primaryKey = indexEvent.target.result;
if (primaryKey !== undefined) {
objectStore.delete(primaryKey);
}
}).catch((error) => {
console.error(error);
});
}
const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = (event) => {
loadedFavorites = event.target.result.reverse();
};
getAllRequest.onerror = (event) => {
console.error(event);
};
});
}
/**
* @param {[{id: String, tags: String, src: String, metadata: String}]} favorites
*/
updateFavorites(favorites) {
this.openConnection()
.then((event) => {
/**
* @type {IDBDatabase}
*/
const database = event.target.result;
const favoritesObjectStore = database
.transaction(this.objectStoreName, "readwrite")
.objectStore(this.objectStoreName);
const objectStoreIndex = favoritesObjectStore.index("id");
let updatedCount = 0;
favorites.forEach(favorite => {
const index = objectStoreIndex.getKey(favorite.id);
this.addContentTypeToFavorite(favorite);
index.onsuccess = (indexEvent) => {
const primaryKey = indexEvent.target.result;
favoritesObjectStore.put(favorite, primaryKey);
updatedCount += 1;
if (updatedCount >= favorites.length) {
database.close();
}
};
});
})
.catch((event) => {
const error = event.target.error;
if (error.name === "VersionError") {
this.version += 1;
this.updateFavorites(favorites);
} else {
console.error(error);
}
});
}
/**
* @param {{id: String, tags: String, src: String, metadata: 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.idsToDelete);
break;
case "update":
favoritesDatabase.updateFavorites(request.favorites);
break;
default:
break;
}
};
`
};
static tagNegation = {
useTagBlacklist: true,
negatedTagBlacklist: negateTags(TAG_BLACKLIST)
};
static parser = new DOMParser();
static get disabled() {
return !onFavoritesPage();
}
/**
* @type {{highestInsertedPageNumber : Number, emptying: Boolean, insertionQueue: {pageNumber: Number, thumbNodes: ThumbNode[], searchResults: ThumbNode[]}[]}}
*/
fetchedThumbNodes;
/**
* @type {ThumbNode[]}
*/
allThumbNodes;
/**
* @type {Number}
*/
finalPageNumber;
/**
* @type {HTMLLabelElement}
*/
matchCountLabel;
/**
* @type {Number}
*/
matchingFavoritesCount;
/**
* @type {Number}
*/
maxNumberOfFavoritesToDisplay;
/**
* @type {[{url: String, pageNumber: Number, retries: Number}]}
*/
failedFetchRequests;
/**
* @type {Number}
*/
expectedFavoritesCount;
/**
* @type {Boolean}
*/
expectedFavoritesCountFound;
/**
* @type {String}
*/
searchQuery;
/**
* @type {String}
*/
previousSearchQuery;
/**
* @type {Worker}
*/
databaseWorker;
/**
* @type {Boolean}
*/
searchResultsAreShuffled;
/**
/**
* @type {Boolean}
*/
searchResultsAreInverted;
/**
* @type {Boolean}
*/
searchResultsWereShuffled;
/**
/**
* @type {Boolean}
*/
searchResultsWereInverted;
/**
* @type {Number}
*/
currentFavoritesPageNumber;
/**
* @type {HTMLElement}
*/
paginationContainer;
/**
* @type {HTMLLabelElement}
*/
paginationLabel;
/**
* @type {Boolean}
*/
foundEmptyFavoritesPage;
/**
* @type {ThumbNode[]}
*/
searchResultsWhileFetching;
/**
* @type {Number}
*/
recentlyChangedMaxNumberOfFavoritesToDisplay;
/**
* @type {Number}
*/
maxPageNumberButtonCount;
/**
* @type {Boolean}
*/
newPageNeedsToBeCreated;
/**
* @type {Boolean}
*/
tagsWereModified;
/**
* @type {Boolean}
*/
excludeBlacklistClicked;
/**
* @type {Boolean}
*/
sortingParametersChanged;
/**
* @type {Boolean}
*/
allowedRatingsChanged;
/**
* @type {Number}
*/
allowedRatings;
/**
* @type {String[]}
*/
idsRequiringMetadataDatabaseUpdate;
/**
* @type {Number}
*/
newMetadataReceivedTimeout;
/**
* @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 (FavoritesLoader.disabled) {
return;
}
this.initializeFields();
this.addEventListeners();
this.initialize();
}
initializeFields() {
this.allThumbNodes = [];
this.searchResultsWhileFetching = [];
this.idsRequiringMetadataDatabaseUpdate = [];
this.finalPageNumber = this.getFinalFavoritesPageNumber();
this.matchCountLabel = document.getElementById("match-count-label");
this.maxNumberOfFavoritesToDisplay = getPreference("resultsPerPage", DEFAULTS.resultsPerPage);
this.allowedRatings = loadAllowedRatings();
this.fetchedThumbNodes = {};
this.failedFetchRequests = [];
this.expectedFavoritesCount = 53;
this.expectedFavoritesCountFound = false;
this.searchResultsAreShuffled = false;
this.searchResultsAreInverted = false;
this.searchResultsWereShuffled = false;
this.searchResultsWereInverted = false;
this.foundEmptyFavoritesPage = false;
this.newPageNeedsToBeCreated = false;
this.tagsWereModified = false;
this.recentlyChangedMaxNumberOfFavoritesToDisplay = false;
this.excludeBlacklistClicked = false;
this.sortingParametersChanged = false;
this.allowedRatingsChanged = false;
this.matchingFavoritesCount = 0;
this.maxPageNumberButtonCount = onMobileDevice() ? 3 : 5;
this.searchQuery = "";
this.databaseWorker = new Worker(getWorkerURL(FavoritesLoader.webWorkers.database));
this.paginationContainer = this.createPaginationContainer();
this.currentFavoritesPageNumber = 1;
}
addEventListeners() {
window.addEventListener("modifiedTags", () => {
this.tagsWereModified = true;
});
window.addEventListener("missingMetadata", (event) => {
this.addNewFavoriteMetadata(event.detail);
});
this.createDatabaseMessageHandler();
}
createDatabaseMessageHandler() {
this.databaseWorker.onmessage = async(message) => {
message = message.data;
switch (message.response) {
case "finishedLoading":
this.paginateSearchResults(this.reconstructContent(message.favorites));
this.onAllFavoritesLoaded();
await sleep(100);
this.findNewFavoritesOnReload(this.getAllFavoriteIds(), 0, []);
break;
case "finishedStoring":
break;
default:
break;
}
};
}
initialize() {
this.setExpectedFavoritesCount();
this.clearOriginalFavoritesPage();
this.searchFavorites();
}
setExpectedFavoritesCount() {
const profileURL = `https://rule34.xxx/index.php?page=account&s=profile&id=${getFavoritesPageId()}`;
fetch(profileURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status);
})
.then((html) => {
const table = FavoritesLoader.parser.parseFromString(html, "text/html").querySelector("table");
const favoritesURL = Array.from(table.querySelectorAll("a")).find(a => a.href.includes("page=favorites&s=view"));
const favoritesCount = parseInt(favoritesURL.textContent);
this.expectedFavoritesCountFound = true;
this.expectedFavoritesCount = favoritesCount;
})
.catch(() => {
console.error(`Could not find total favorites count from ${profileURL}, are you logged in?`);
});
}
clearOriginalFavoritesPage() {
const thumbs = Array.from(document.getElementsByClassName("thumb"));
document.getElementById("content").innerHTML = "";
setTimeout(() => {
dispatchEvent(new CustomEvent("originalFavoritesCleared", {
detail: thumbs
}));
}, 1000);
}
/**
* @param {String} searchQuery
*/
searchFavorites(searchQuery) {
this.setSearchQuery(searchQuery);
this.resetMatchCount();
dispatchEvent(new Event("searchStarted"));
this.showSearchResults();
}
/**
* @param {String} searchQuery
*/
setSearchQuery(searchQuery) {
if (searchQuery !== undefined) {
this.searchQuery = searchQuery;
}
}
showSearchResults() {
switch (FavoritesLoader.currentLoadingState) {
case FavoritesLoader.loadingState.fetchingFavorites:
this.showSearchResultsWhileFetchingFavorites();
break;
case FavoritesLoader.loadingState.allFavoritesLoaded:
this.showSearchResultsAfterAllFavoritesLoaded();
break;
case FavoritesLoader.loadingState.loadingFavoritesFromDatabase:
break;
case FavoritesLoader.loadingState.initial:
this.retrieveFavorites();
break;
default:
console.error(`Invalid FavoritesLoader state: ${FavoritesLoader.currentLoadingState}`);
break;
}
}
showSearchResultsWhileFetchingFavorites() {
this.searchResultsWhileFetching = this.getSearchResults(this.allThumbNodes);
this.paginateSearchResults(this.searchResultsWhileFetching);
}
showSearchResultsAfterAllFavoritesLoaded() {
this.paginateSearchResults(this.getSearchResults(this.allThumbNodes));
}
async retrieveFavorites() {
const databaseStatus = await this.getDatabaseStatus();
this.databaseWorker.postMessage({
command: "create",
objectStoreName: FavoritesLoader.objectStoreName,
version: databaseStatus.version
});
if (databaseStatus.objectStoreIsNotEmpty) {
this.loadFavoritesFromDatabase();
} else {
this.startFetchingFavorites();
}
}
/**
* @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 = new SearchCommand(this.finalSearchQuery);
const results = [];
stopIndex = stopIndex === undefined ? thumbNodes.length : stopIndex;
stopIndex = Math.min(stopIndex, thumbNodes.length);
for (let i = 0; i < stopIndex; i += 1) {
const thumbNode = thumbNodes[i];
if (matchesSearch(searchCommand, thumbNode)) {
results.push(thumbNode);
thumbNode.setMatched(true);
} else {
thumbNode.setMatched(false);
}
}
return results;
}
/**
* @param {Object.<String, ThumbNode>} allFavoriteIds
* @param {Number} currentPageNumber
* @param {ThumbNode[]} newFavoritesToAdd
*/
findNewFavoritesOnReload(allFavoriteIds, currentPageNumber, newFavoritesToAdd) {
const favoritesURL = `${document.location.href}&pid=${currentPageNumber}`;
const exceededFavoritesPageNumber = currentPageNumber > this.finalPageNumber;
let allNewFavoritesFound = false;
requestPageInformation(favoritesURL, (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 || exceededFavoritesPageNumber) {
this.allThumbNodes = newFavoritesToAdd.concat(this.allThumbNodes);
this.addNewFavoritesOnReload(newFavoritesToAdd);
} else {
this.findNewFavoritesOnReload(allFavoriteIds, currentPageNumber + 50, newFavoritesToAdd);
}
});
}
/**
* @param {ThumbNode[]} newFavorites
*/
addNewFavoritesOnReload(newFavorites) {
if (newFavorites.length === 0) {
dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
detail: {
empty: true,
thumbs: []
}
}));
return;
}
this.storeFavorites(newFavorites);
this.insertNewFavorites(newFavorites);
this.toggleLoadingUI(false);
}
initializeFetchedThumbNodesInsertionQueue() {
this.fetchedThumbNodes.highestInsertedPageNumber = -1;
this.fetchedThumbNodes.insertionQueue = [];
}
startFetchingFavorites() {
FavoritesLoader.currentLoadingState = FavoritesLoader.loadingState.fetchingFavorites;
this.toggleContentVisibility(true);
this.insertPaginationContainer();
this.updatePaginationUi(1, []);
this.initializeFetchedThumbNodesInsertionQueue();
dispatchEvent(new Event("readyToSearch"));
setTimeout(() => {
dispatchEvent(new Event("startedFetchingFavorites"));
}, 50);
this.fetchFavorites();
}
updateProgressWhileFetching() {
let progressText = `Fetching Favorites ${this.allThumbNodes.length}`;
if (this.expectedFavoritesCountFound) {
progressText = `${progressText} / ${this.expectedFavoritesCount}`;
}
this.setProgressText(progressText);
}
async fetchFavorites() {
let currentPageNumber = 0;
while (FavoritesLoader.currentLoadingState === FavoritesLoader.loadingState.fetchingFavorites) {
if (this.failedFetchRequests.length > 0) {
const failedRequest = this.failedFetchRequests.shift();
const waitTime = (7 ** (failedRequest.retries + 1)) + 300;
this.fetchFavoritesFromSinglePage(currentPageNumber, failedRequest);
await sleep(waitTime);
} else if (currentPageNumber * 50 <= this.finalPageNumber && !this.foundEmptyFavoritesPage) {
this.fetchFavoritesFromSinglePage(currentPageNumber);
currentPageNumber += 1;
await sleep(210);
} else if (this.isFinishedFetching(currentPageNumber)) {
this.onAllFavoritesLoaded();
this.storeFavorites();
} else {
await sleep(10000);
}
}
}
/**
* @param {Number} pageNumber
* @returns {Boolean}
*/
isFinishedFetching(pageNumber) {
pageNumber *= 50;
let done = this.allThumbNodes.length >= this.expectedFavoritesCount - 2;
done = done || this.foundEmptyFavoritesPage || pageNumber >= (this.finalPageNumber * 2) + 1;
return done && this.failedFetchRequests.length === 0;
}
/**
* @param {Number} pageNumber
* @param {{url: String, pageNumber: Number, retries: Number}} failedRequest
*/
fetchFavoritesFromSinglePage(pageNumber, failedRequest) {
const refetching = failedRequest !== undefined;
pageNumber = refetching ? failedRequest.pageNumber : pageNumber * 50;
const favoritesPage = refetching ? failedRequest.url : `${document.location.href}&pid=${pageNumber}`;
return fetch(favoritesPage)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(`${response.status}: Favorite page failed to fetch, ${favoritesPage}`);
})
.then((html) => {
const thumbNodes = this.extractThumbNodesFromFavoritesPage(html);
const searchResults = this.getSearchResults(thumbNodes);
this.addFetchedThumbNodesToInsertionQueue(pageNumber, thumbNodes, searchResults);
this.foundEmptyFavoritesPage = thumbNodes.length === 0;
})
.catch((error) => {
console.error(error);
if (refetching) {
failedRequest.retries += 1;
} else {
failedRequest = this.getFailedFetchRequest(favoritesPage, pageNumber);
}
this.failedFetchRequests.push(failedRequest);
});
}
/**
* @param {Number} pageNumber
* @param {ThumbNode[]} thumbNodes
* @param {ThumbNode[]} searchResults
*/
addFetchedThumbNodesToInsertionQueue(pageNumber, thumbNodes, searchResults) {
pageNumber = Math.floor(parseInt(pageNumber) / 50);
this.fetchedThumbNodes.insertionQueue.push({
pageNumber,
thumbNodes,
searchResults
});
this.fetchedThumbNodes.insertionQueue.sort((a, b) => a.pageNumber - b.pageNumber);
this.emptyInsertionQueue();
}
emptyInsertionQueue() {
if (this.fetchedThumbNodes.emptying) {
return;
}
this.fetchedThumbNodes.emptying = true;
while (this.fetchedThumbNodes.insertionQueue.length > 0) {
const element = this.fetchedThumbNodes.insertionQueue[0];
if (this.previousPageNumberIsPresent(element.pageNumber)) {
this.processFetchedThumbNodes(element.thumbNodes, element.searchResults);
this.fetchedThumbNodes.insertionQueue.shift();
this.fetchedThumbNodes.highestInsertedPageNumber += 1;
} else {
break;
}
}
this.fetchedThumbNodes.emptying = false;
}
/**
* @param {Number} pageNumber
* @returns {Boolean}
*/
previousPageNumberIsPresent(pageNumber) {
return this.fetchedThumbNodes.highestInsertedPageNumber + 1 === pageNumber;
}
/**
* @param {ThumbNode[]} thumbNodes
* @param {ThumbNode[]} searchResults
*/
processFetchedThumbNodes(thumbNodes, searchResults) {
this.searchResultsWhileFetching = this.searchResultsWhileFetching.concat(searchResults);
const searchResultsWhileFetchingWithAllowedRatings = this.getResultsWithAllowedRatings(this.searchResultsWhileFetching);
this.updateMatchCount(searchResultsWhileFetchingWithAllowedRatings.length);
dispatchEvent(new CustomEvent("favoritesFetched", {
detail: thumbNodes.map(thumbNode => thumbNode.root)
}));
this.allThumbNodes = this.allThumbNodes.concat(thumbNodes);
this.addFavoritesToContent(searchResults);
this.updateProgressWhileFetching();
}
/**
* @param {String} url
* @param {Number} pageNumber
* @returns {{url: String, pageNumber: Number, retries: Number}}
*/
getFailedFetchRequest(url, pageNumber) {
return {
url,
pageNumber,
retries: 0
};
}
/**
* @param {String} response
* @returns {ThumbNode[]}
*/
extractThumbNodesFromFavoritesPage(response) {
const dom = FavoritesLoader.parser.parseFromString(response, "text/html");
return Array.from(dom.getElementsByClassName("thumb")).map(thumb => new ThumbNode(thumb, false));
}
invertSearchResults() {
this.resetMatchCount();
this.allThumbNodes.forEach((thumbNode) => {
thumbNode.toggleMatched();
});
const invertedSearchResults = this.getThumbNodesMatchedByLastSearch();
this.searchResultsAreInverted = true;
this.paginateSearchResults(invertedSearchResults);
window.scrollTo(0, 0);
}
shuffleSearchResults() {
const matchedThumbNodes = this.getThumbNodesMatchedByLastSearch();
shuffleArray(matchedThumbNodes);
this.searchResultsAreShuffled = true;
this.paginateSearchResults(matchedThumbNodes);
}
getThumbNodesMatchedByLastSearch() {
return this.allThumbNodes.filter(thumbNode => thumbNode.matchedByMostRecentSearch);
}
onAllFavoritesLoaded() {
dispatchEvent(new Event("readyToSearch"));
FavoritesLoader.currentLoadingState = FavoritesLoader.loadingState.allFavoritesLoaded;
this.toggleLoadingUI(false);
dispatchEvent(new CustomEvent("favoritesLoaded", {
detail: this.allThumbNodes.map(thumbNode => thumbNode.root)
}));
}
/**
* @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 {ThumbNode[]}}
*/
reconstructContent(databaseRecords) {
if (databaseRecords === null) {
return null;
}
const searchCommand = new SearchCommand(this.finalSearchQuery);
const searchResults = [];
for (const record of databaseRecords) {
const thumbNode = new ThumbNode(record, true);
const isBlacklisted = !matchesSearch(searchCommand, thumbNode);
if (isBlacklisted) {
if (!userIsOnTheirOwnFavoritesPage()) {
continue;
}
thumbNode.setMatched(false);
} else {
searchResults.push(thumbNode);
}
this.allThumbNodes.push(thumbNode);
}
return searchResults;
}
loadFavoritesFromDatabase() {
FavoritesLoader.currentLoadingState = FavoritesLoader.loadingState.loadingFavoritesFromDatabase;
this.toggleLoadingUI(true);
let idsToDelete = [];
if (userIsOnTheirOwnFavoritesPage()) {
idsToDelete = getIdsToDeleteOnReload();
clearIdsToDeleteOnReload();
}
this.databaseWorker.postMessage({
command: "load",
idsToDelete
});
}
/**
* @param {ThumbNode[]} thumbNodes
*/
async storeFavorites(thumbNodes) {
const storeAll = thumbNodes === undefined;
await sleep(500);
thumbNodes = storeAll ? this.allThumbNodes : thumbNodes;
const records = thumbNodes.map(thumbNode => thumbNode.databaseRecord);
if (storeAll) {
records.reverse();
}
this.databaseWorker.postMessage({
command: "store",
favorites: records
});
}
/**
* @param {ThumbNode[]} thumbNodes
*/
updateFavorites(thumbNodes) {
this.databaseWorker.postMessage({
command: "update",
favorites: thumbNodes.map(thumbNode => thumbNode.databaseRecord)
});
}
/**
* @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() {
const message = `
Are you sure you want to reset?
This will delete all cached favorites, and preferences.
Tag modifications and saved searches will be preserved.
`;
if (confirm(message)) {
const persistentLocalStorageKeys = new Set(["customTags", "savedSearches"]);
Object.keys(localStorage).forEach((key) => {
if (!persistentLocalStorageKeys.has(key)) {
localStorage.removeItem(key);
}
});
indexedDB.deleteDatabase(FavoritesLoader.databaseName);
}
}
/**
* @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 {Number} value
*/
updateMatchCount(value) {
if (!this.matchCountLabelExists) {
return;
}
this.matchingFavoritesCount = value === undefined ? this.getSearchResults(this.allThumbNodes).length : value;
this.matchCountLabel.textContent = `${this.matchingFavoritesCount} Matches`;
}
/**
* @param {Number} 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 {ThumbNode[]} newThumbNodes
*/
async insertNewFavorites(newThumbNodes) {
const content = document.getElementById("content");
const searchCommand = new SearchCommand(this.finalSearchQuery);
const insertedThumbNodes = [];
const metadataPopulateWaitTime = 1000;
newThumbNodes.reverse();
if (this.allowedRatings !== 7) {
await sleep(metadataPopulateWaitTime);
}
for (const thumbNode of newThumbNodes) {
if (this.matchesSearchAndRating(searchCommand, thumbNode)) {
thumbNode.insertInDocument(content, "afterbegin");
insertedThumbNodes.push(thumbNode);
}
}
this.updatePaginationUi(this.currentFavoritesPageNumber, this.getThumbNodesMatchedByLastSearch());
setTimeout(() => {
dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
detail: {
empty: false,
thumbs: insertedThumbNodes.map(thumbNode => thumbNode.root)
}
}));
}, 250);
}
/**
* @param {ThumbNode[]} thumbNodes
*/
addFavoritesToContent(thumbNodes) {
thumbNodes = this.getResultsWithAllowedRatings(thumbNodes);
const searchResultsWhileFetchingWithAllowedRatings = this.getResultsWithAllowedRatings(this.searchResultsWhileFetching);
const pageNumberButtons = document.getElementsByClassName("pagination-number");
const lastPageButtonNumber = pageNumberButtons.length > 0 ? parseInt(pageNumberButtons[pageNumberButtons.length - 1].textContent) : 1;
const pageCount = this.getPageCount(searchResultsWhileFetchingWithAllowedRatings.length);
const needsToCreateNewPage = pageCount > lastPageButtonNumber;
const nextPageButton = document.getElementById("next-page");
const alreadyAtMaxPageNumberButtons = document.getElementsByClassName("pagination-number").length >= this.maxPageNumberButtonCount &&
nextPageButton !== null && nextPageButton.style.display !== "none" &&
nextPageButton.style.visibility !== "hidden";
if (needsToCreateNewPage && !alreadyAtMaxPageNumberButtons) {
this.updatePaginationUi(this.currentFavoritesPageNumber, searchResultsWhileFetchingWithAllowedRatings);
}
const onLastPage = (pageCount === this.currentFavoritesPageNumber);
const missingThumbNodeCount = this.maxNumberOfFavoritesToDisplay - getAllThumbs().length;
if (!onLastPage) {
if (missingThumbNodeCount > 0) {
thumbNodes = thumbNodes.slice(0, missingThumbNodeCount);
} else {
return;
}
}
const content = document.getElementById("content");
for (const thumbNode of thumbNodes) {
content.appendChild(thumbNode.root);
}
}
/**
* @returns {Object.<String, ThumbNode>}
*/
getAllFavoriteIds() {
const favoriteIds = {};
for (const thumbNode of this.allThumbNodes) {
favoriteIds[thumbNode.id] = thumbNode;
}
return favoriteIds;
}
/**
* @param {Boolean} value
*/
toggleTagBlacklistExclusion(value) {
FavoritesLoader.tagNegation.useTagBlacklist = value;
this.excludeBlacklistClicked = true;
}
/**
* @param {Number} searchResultsLength
* @returns {Number}
*/
getPageCount(searchResultsLength) {
return Math.floor(searchResultsLength / this.maxNumberOfFavoritesToDisplay) + 1;
}
/**
* @param {ThumbNode[]} searchResults
*/
paginateSearchResults(searchResults) {
searchResults = this.getResultsWithAllowedRatings(searchResults);
this.updateMatchCount(searchResults.length);
this.insertPaginationContainer();
this.changeResultsPage(1, searchResults);
}
insertPaginationContainer() {
if (document.getElementById(this.paginationContainer.id) === null) {
if (onMobileDevice()) {
document.getElementById("favorites-top-bar-panels").insertAdjacentElement("afterbegin", this.paginationContainer);
} else {
const placeToInsertPagination = document.getElementById("favorites-pagination-placeholder");
placeToInsertPagination.insertAdjacentElement("afterend", this.paginationContainer);
placeToInsertPagination.remove();
}
}
}
/**
* @returns {HTMLElement}
*/
createPaginationContainer() {
const container = document.createElement("span");
container.id = "favorites-pagination-container";
return container;
}
/**
* @param {Number} currentPageNumber
* @param {ThumbNode[]} searchResults
*/
createPageNumberButtons(currentPageNumber, searchResults) {
const pageCount = this.getPageCount(searchResults.length);
let numberOfButtonsCreated = 0;
for (let i = currentPageNumber; i <= pageCount && numberOfButtonsCreated < this.maxPageNumberButtonCount; i += 1) {
numberOfButtonsCreated += 1;
this.createPageNumberButton(currentPageNumber, i, searchResults);
}
if (numberOfButtonsCreated >= this.maxPageNumberButtonCount) {
return;
}
for (let j = currentPageNumber - 1; j >= 1 && numberOfButtonsCreated < this.maxPageNumberButtonCount; j -= 1) {
numberOfButtonsCreated += 1;
this.createPageNumberButton(currentPageNumber, j, searchResults, "afterbegin");
}
}
/**
* @param {Number} currentPageNumber
* @param {Number} pageNumber
* @param {ThumbNode[]} searchResults
* @param {String} position
*/
createPageNumberButton(currentPageNumber, pageNumber, searchResults, position = "beforeend") {
const pageNumberButton = document.createElement("button");
const selected = currentPageNumber === pageNumber;
pageNumberButton.id = `favorites-page-${pageNumber}`;
pageNumberButton.title = `Goto page ${pageNumber}`;
pageNumberButton.className = "pagination-number";
pageNumberButton.classList.toggle("selected", selected);
pageNumberButton.onclick = () => {
this.changeResultsPage(pageNumber, searchResults);
};
this.paginationContainer.insertAdjacentElement(position, pageNumberButton);
pageNumberButton.textContent = pageNumber;
}
/**
* @param {ThumbNode[]} searchResults
*/
createPageTraversalButtons(searchResults) {
const pageCount = this.getPageCount(searchResults.length);
const previousPage = document.createElement("button");
const firstPage = document.createElement("button");
const nextPage = document.createElement("button");
const finalPage = document.createElement("button");
previousPage.textContent = "<";
firstPage.textContent = "<<";
nextPage.textContent = ">";
finalPage.textContent = ">>";
previousPage.id = "previous-page";
firstPage.id = "first-page";
nextPage.id = "next-page";
finalPage.id = "final-page";
previousPage.onclick = () => {
if (this.currentFavoritesPageNumber - 1 >= 1) {
this.changeResultsPage(this.currentFavoritesPageNumber - 1, searchResults);
}
};
firstPage.onclick = () => {
this.changeResultsPage(1, searchResults);
};
nextPage.onclick = () => {
if (this.currentFavoritesPageNumber + 1 <= pageCount) {
this.changeResultsPage(this.currentFavoritesPageNumber + 1, searchResults);
}
};
finalPage.onclick = () => {
this.changeResultsPage(pageCount, searchResults);
};
this.paginationContainer.insertAdjacentElement("afterbegin", previousPage);
this.paginationContainer.insertAdjacentElement("afterbegin", firstPage);
this.paginationContainer.appendChild(nextPage);
this.paginationContainer.appendChild(finalPage);
this.updateVisibilityOfPageTraversalButtons(previousPage, firstPage, nextPage, finalPage, this.getPageCount(searchResults.length));
}
/**
* @param {ThumbNode[]} searchResults
*/
createGotoSpecificPageInputs(searchResults) {
if (this.firstPageNumberExists() && this.lastPageNumberExists(this.getPageCount(searchResults.length))) {
return;
}
const html = `
<input type="number" placeholder="page" style="width: 4em;">
<button>Go</button>
`;
const container = document.createElement("span");
container.innerHTML = html;
const input = container.children[0];
const button = container.children[1];
input.onkeydown = (event) => {
if (event.key === "Enter") {
button.click();
}
};
button.onclick = () => {
let pageNumber = parseInt(input.value);
if (!isNumber(pageNumber)) {
return;
}
pageNumber = clamp(pageNumber, 1, this.getPageCount(searchResults.length));
this.changeResultsPage(pageNumber, searchResults);
};
this.paginationContainer.appendChild(container);
}
/**
* @param {Number} pageNumber
* @param {ThumbNode[]} searchResults
*/
changeResultsPage(pageNumber, searchResults) {
if (!this.aNewSearchWillProduceDifferentResults(pageNumber)) {
return;
}
const {start, end} = this.getPaginationStartEndIndices(pageNumber);
this.currentFavoritesPageNumber = pageNumber;
this.updatePaginationUi(pageNumber, searchResults);
this.createPaginatedFavoritesPage(searchResults, start, end);
this.reAddAllThumbNodeEventListeners();
this.resetFlagsThatImplyDifferentSearchResults();
if (FavoritesLoader.currentLoadingState !== FavoritesLoader.loadingState.loadingFavoritesFromDatabase) {
dispatchEventWithDelay("changedPage");
}
}
resetFlagsThatImplyDifferentSearchResults() {
this.searchResultsWereShuffled = this.searchResultsAreShuffled;
this.searchResultsWereInverted = this.searchResultsAreInverted;
this.tagsWereModified = false;
this.excludeBlacklistClicked = false;
this.sortingParametersChanged = false;
this.allowedRatingsChanged = false;
this.searchResultsAreShuffled = false;
this.searchResultsAreInverted = false;
this.previousSearchQuery = this.searchQuery;
}
getPaginationStartEndIndices(pageNumber) {
return {
start: this.maxNumberOfFavoritesToDisplay * (pageNumber - 1),
end: this.maxNumberOfFavoritesToDisplay * pageNumber
};
}
/**
* @param {Number} pageNumber
* @param {ThumbNode[]} searchResults
*/
updatePaginationUi(pageNumber, searchResults) {
const {start, end} = this.getPaginationStartEndIndices(pageNumber);
const searchResultsLength = searchResults.length;
this.setPaginationLabel(start, end, searchResultsLength);
this.paginationContainer.innerHTML = "";
this.createPageNumberButtons(pageNumber, searchResults);
this.createPageTraversalButtons(searchResults);
this.createGotoSpecificPageInputs(searchResults);
}
reAddAllThumbNodeEventListeners() {
for (const thumbNode of this.allThumbNodes) {
thumbNode.setupAuxillaryButton();
}
}
/**
* @param {ThumbNode[]} searchResults
* @param {Number} start
* @param {Number} end
* @returns
*/
createPaginatedFavoritesPage(searchResults, start, end) {
const newThumbNodes = this.sortThumbNodes(searchResults).slice(start, end);
const content = document.getElementById("content");
const newContent = document.createDocumentFragment();
for (const thumbNode of newThumbNodes) {
newContent.appendChild(thumbNode.root);
}
content.innerHTML = "";
content.appendChild(newContent);
window.scrollTo(0, 0);
}
/**
* @param {Number} pageNumber
* @returns {Boolean}
*/
aNewSearchWillProduceDifferentResults(pageNumber) {
return this.currentFavoritesPageNumber !== pageNumber ||
this.searchQuery !== this.previousSearchQuery ||
FavoritesLoader.currentLoadingState !== FavoritesLoader.loadingState.allFavoritesLoaded ||
this.searchResultsAreShuffled ||
this.searchResultsAreInverted ||
this.searchResultsWereShuffled ||
this.searchResultsWereInverted ||
this.recentlyChangedMaxNumberOfFavoritesToDisplay ||
this.tagsWereModified ||
this.excludeBlacklistClicked ||
this.sortingParametersChanged ||
this.allowedRatingsChanged;
}
/**
* @param {Number} start
* @param {Number} end
* @param {Number} searchResults
* @returns
*/
setPaginationLabel(start, end, searchResultsLength) {
end = Math.min(end, searchResultsLength);
if (this.paginationLabel === undefined) {
this.paginationLabel = document.getElementById("pagination-label");
}
if (searchResultsLength <= this.maxNumberOfFavoritesToDisplay) {
this.paginationLabel.textContent = "";
return;
}
this.paginationLabel.textContent = `${start} - ${end}`;
}
/**
* @returns {Boolean}
*/
firstPageNumberExists() {
return document.getElementById("favorites-page-1") !== null;
}
/**
* @param {Number} pageCount
* @returns {Boolean}
*/
lastPageNumberExists(pageCount) {
return document.getElementById(`favorites-page-${pageCount}`) !== null;
}
/**
* @param {HTMLButtonElement} previousPage
* @param {HTMLButtonElement} firstPage
* @param {HTMLButtonElement} nextPage
* @param {HTMLButtonElement} finalPage
* @param {Number} pageCount
*/
updateVisibilityOfPageTraversalButtons(previousPage, firstPage, nextPage, finalPage, pageCount) {
const firstNumberExists = this.firstPageNumberExists();
const lastNumberExists = this.lastPageNumberExists(pageCount);
if (firstNumberExists && lastNumberExists) {
previousPage.style.visibility = "hidden";
firstPage.style.visibility = "hidden";
nextPage.style.visibility = "hidden";
finalPage.style.visibility = "hidden";
} else {
if (firstNumberExists) {
previousPage.style.visibility = "hidden";
firstPage.style.visibility = "hidden";
}
if (lastNumberExists) {
nextPage.style.visibility = "hidden";
finalPage.style.visibility = "hidden";
}
}
}
/**
* @param {Number} value
*/
updateMaxNumberOfFavoritesToDisplay(value) {
this.maxNumberOfFavoritesToDisplay = value;
this.recentlyChangedMaxNumberOfFavoritesToDisplay = true;
this.searchFavorites();
this.recentlyChangedMaxNumberOfFavoritesToDisplay = false;
}
/**
* @param {ThumbNode[]} thumbNodes
* @returns {ThumbNode[]}
*/
sortThumbNodes(thumbNodes) {
if (!FavoritesLoader.loadingState.allFavoritesLoaded) {
alert("Wait for all favorites to load before changing sort method");
return thumbNodes;
}
const sortedThumbNodes = thumbNodes.slice();
const sortingMethod = this.getSortingMethod();
if (sortingMethod !== "default") {
sortedThumbNodes.sort((b, a) => {
switch (sortingMethod) {
case "score":
return a.metadata.score - b.metadata.score;
case "width":
return a.metadata.width - b.metadata.width;
case "height":
return a.metadata.height - b.metadata.height;
case "create":
return a.metadata.creationTimestamp - b.metadata.creationTimestamp;
case "change":
return a.metadata.lastChangedTimestamp - b.metadata.lastChangedTimestamp;
case "id":
return a.metadata.id - b.metadata.id;
default:
return 0;
}
});
}
if (this.sortAscending()) {
sortedThumbNodes.reverse();
}
return sortedThumbNodes;
}
/**
* @returns {String}
*/
getSortingMethod() {
if (this.searchResultsAreShuffled) {
return "default";
}
const sortingMethodSelect = document.getElementById("sorting-method");
return sortingMethodSelect === null ? "default" : sortingMethodSelect.value;
}
/**
* @returns {Boolean}
*/
sortAscending() {
const sortFavoritesAscending = document.getElementById("sort-ascending");
return sortFavoritesAscending === null ? false : sortFavoritesAscending.checked;
}
onSortingParametersChanged() {
this.sortingParametersChanged = true;
const matchedThumbNodes = this.getThumbNodesMatchedByLastSearch();
this.paginateSearchResults(matchedThumbNodes);
}
/**
* @param {Number} allowedRatings
*/
onAllowedRatingsChanged(allowedRatings) {
this.allowedRatings = allowedRatings;
this.allowedRatingsChanged = true;
const matchedThumbNodes = this.getThumbNodesMatchedByLastSearch();
this.paginateSearchResults(matchedThumbNodes);
}
/**
* @param {String} postId
*/
addNewFavoriteMetadata(postId) {
if (!ThumbNode.allThumbNodes.has(postId)) {
return;
}
const batchSize = 500;
const waitTime = 1000;
clearTimeout(this.newMetadataReceivedTimeout);
this.idsRequiringMetadataDatabaseUpdate.push(postId);
if (this.idsRequiringMetadataDatabaseUpdate.length >= batchSize) {
this.updateFavoriteMetadataInDatabase();
return;
}
this.newMetadataReceivedTimeout = setTimeout(() => {
this.updateFavoriteMetadataInDatabase();
}, waitTime);
}
updateFavoriteMetadataInDatabase() {
this.updateFavorites(this.idsRequiringMetadataDatabaseUpdate.map(id => ThumbNode.allThumbNodes.get(id)));
this.idsRequiringMetadataDatabaseUpdate = [];
}
/**
* @param {ThumbNode} thumbNode
* @returns {Boolean}
*/
ratingIsAllowed(thumbNode) {
return (thumbNode.metadata.rating & this.allowedRatings) > 0;
}
/**
* @param {ThumbNode[]} searchResults
* @returns {ThumbNode[]}
*/
getResultsWithAllowedRatings(searchResults) {
if (this.allowedRatings === 7) {
return searchResults;
}
return searchResults.filter(thumbNode => this.ratingIsAllowed(thumbNode));
}
/**
* @param {{orGroups: String[][], remainingSearchTags: String[], isEmpty: Boolean}} searchCommand
* @param {ThumbNode} thumbNode
* @returns
*/
matchesSearchAndRating(searchCommand, thumbNode) {
return this.ratingIsAllowed(thumbNode) && matchesSearch(searchCommand, thumbNode);
}
}
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;
}
}
#left-favorites-panel {
>div:first-of-type {
margin-bottom: 5px;
>label {
align-content: center;
margin-right: 5px;
margin-top: 4px;
}
>button {
height: 35px;
border: none;
border-radius: 4px;
&:hover {
filter: brightness(140%);
}
}
>button[disabled] {
filter: none !important;
cursor: wait !important;
}
}
}
#right-favorites-panel {
margin-left: 10px;
}
textarea {
max-width: 100%;
height: 50px;
width: 95%;
padding: 10px;
border-radius: 6px;
resize: vertical;
}
button,
input[type="checkbox"] {
cursor: pointer;
}
.checkbox {
display: block;
padding: 2px 6px 2px 0px;
border-radius: 4px;
margin-left: -3px;
height: 27px;
>input {
vertical-align: -5px;
}
}
.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: 9990;
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);
}
}
.auxillary-button {
position: absolute;
left: 0px;
top: 0px;
width: 40%;
font-weight: bold;
background: none;
border: none;
z-index: 2;
filter: grayscale(50%);
&:active,
&:hover {
filter: none !important;
}
}
.remove-favorite-button {
color: red;
}
.add-favorite-button {
>svg {
fill: hotpink;
}
}
.thumb-node {
position: relative;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
>a,
>div {
overflow: hidden;
position: relative;
>img {
width: 100%;
z-index: 1;
}
&:has(.auxillary-button:hover) {
outline-style: solid !important;
outline-width: 5px !important;
}
&:has(.remove-favorite-button:hover) {
outline-color: red !important;
>.remove-favorite-button {
color: red;
}
}
&:has(.add-favorite-button:hover) {
outline-color: hotpink !important;
>.add-favorite-button {
svg {
fill: hotpink;
}
}
}
>a>div {
height: 100%;
}
}
&.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;
min-width: 50%;
>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"] {
-moz-appearance: textfield;
appearance: none;
width: 15px;
}
#column-resize-container {
>div {
align-content: center;
>button {
width: 30px;
height: 30px;
padding: 0;
margin: 0;
}
}
}
#column-resize-input {
margin: 0;
position: relative;
bottom: 9px;
width: 30px;
height: 25px;
font-size: larger;
}
#find-favorite {
margin-top: 7px;
>input {
border-radius: 6px;
height: 35px;
width: 75px;
border: 1px solid;
}
}
#favorites-pagination-container {
padding: 0px 10px 0px 10px;
>button {
background: transparent;
margin: 0px 2px;
padding: 2px 6px;
border: 1px solid black;
font-size: 14px;
color: black;
font-weight: normal;
&:hover {
background-color: #93b393;
}
&.selected {
border: none !important;
font-weight: bold;
pointer-events: none;
}
}
}
#content {
display: grid !important;
grid-template-columns: repeat(10, 1fr);
grid-gap: 1em;
}
#help-links-container {
margin-top: 17px;
}
#whats-new-link {
cursor: pointer;
padding: 0;
position: relative;
font-weight: bolder;
font-style: italic;
background: none;
text-decoration: none !important;
&.hidden:not(.persistent)>div {
display: none;
}
&.persistent,
&:hover {
&.light-green-gradient {
color: black;
}
&:not(.light-green-gradient) {
color: white;
}
}
}
#whats-new-container {
z-index: 10;
top: 20px;
left: 0px;
font-style: normal;
font-weight: normal;
/* left: 50%; */
/* transform: translateX(-50%); */
white-space: nowrap;
max-width: 100vw;
padding: 5px 20px;
position: absolute;
pointer-events: none;
text-shadow: none;
border-radius: 2px;
&.light-green-gradient {
outline: 2px solid black;
}
&:not(.light-green-gradient) {
outline: 1.5px solid white;
}
ul {
padding-left: 20px;
>li {
list-style: none;
}
}
h5,
h6 {
color: rgb(255, 0, 255);
}
}
.hotkey {
font-weight: bolder;
color: orange;
}
#left-favorites-panel-bottom-row {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
>div {
flex: 1;
}
}
#additional-favorite-options {
>div:first-child {
margin-top: 6px;
}
>div:not(:first-child) {
margin-top: 11px;
}
select {
cursor: pointer;
}
}
#performance-profile {
width: 150px;
}
#results-per-page-input {
width: 140px;
}
#show-ui-div {
max-width: 400px;
&.ui-hidden {
max-width: 100vw;
text-align: center;
align-content: center;
}
}
#rating-container {
margin-top: 3px !important;
}
#allowed-ratings {
margin-top: 5px;
font-size: 12px;
>label {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: 1px solid;
padding: 3px;
cursor: pointer;
opacity: 0.5;
position: relative;
}
>label[for="explicit-rating-checkbox"] {
border-radius: 7px 0px 0px 7px;
}
>label[for="questionable-rating-checkbox"] {
margin-left: -3px;
}
>label[for="safe-rating-checkbox"] {
margin-left: -3px;
border-radius: 0px 7px 7px 0px;
}
>input[type="checkbox"] {
display: none;
&:checked+label {
background-color: #0075FF;
color: white;
opacity: 1;
}
}
}
#sort-ascending {
margin: 0;
bottom: -6px;
position: relative;
}
.auxillary-button {
visibility: hidden;
}
#favorites-load-status {
>label {
display: inline-block;
width: 140px;
}
}
#favorites-fetch-progress-label {
color: #3498db;
}
</style>
<div id="favorites-top-bar-panels" style="display: flex;">
<div id="left-favorites-panel">
<h2 style="display: inline;">Search Favorites</h2>
<span id="favorites-load-status" style="margin-left: 5px;">
<label id="match-count-label"></label>
<label id="pagination-label" style="margin-left: 10px;"></label>
<label id="favorites-fetch-progress-label" style="padding-left: 20px; color: #3498db;"></label>
</span>
<div id="left-favorites-panel-top-row">
<button title="Search favorites\nctrl+click/right-click: Search all of rule34 in a new tab"
id="search-button">Search</button>
<button title="Randomize order of search results" id="shuffle-button">Shuffle</button>
<button title="Show results not matched by search" id="invert-button">Invert</button>
<button title="Empty the search box" id="clear-button">Clear</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>
<button title="Remove cached favorites and preferences" id="reset-button">Reset</button>
<span id="favorites-pagination-placeholder"></span>
<span id="help-links-container">
<a href="https://github.com/bruh3396/favorites-search-gallery#controls" target="_blank">Help</a>
|
<a href="https://sleazyfork.org/en/scripts/504184-rule34-favorites-search-gallery/feedback"
target="_blank">Feedback</a>
|
<a href="https://github.com/bruh3396/favorites-search-gallery/issues" target="_blank">Report Issue</a>
|
<a id="whats-new-link" href="" class="hidden light-green-gradient">What's new?
<div id="whats-new-container" class="light-green-gradient">
<h4>1.14:</h4>
<h5>Features:</h5>
<ul>
<li>Search with meta tags: score, width, height, id</li>
<li>Examples:</li>
<ul>
<li>score:>50 score:<100 -score:55</li>
<li>height:>width</li>
<li>( width:height ~ height:1920 ) id:<999 </li>
</ul>
<li>Notes:</li>
<ul>
<li> "12345" and "id:12345" are equivalent</li>
<li>Wildcard "*" does not work with meta tags</li>
</ul>
</ul>
<h4>1.13:</h4>
<h5>Features:</h5>
<ul>
<li>Wildcard search now works anywhere in tag</li>
<li>Examples:</li>
<ul>
<li>a*ple*auce</li>
<li>-*apple*</li>
<li>*ine*pple</li>
</ul>
<li>Blacklisted images removed from search pages</li>
</ul>
<h5>Performance:</h5>
<ul>
<li>Improved search speed</li>
<li>Fixed mobile gallery orientation</li>
</ul>
<h4>1.11:</h4>
<h5>Features:</h5>
<ul>
<li>Sort by score, upload date, etc.</li>
<li>"Add favorite" buttons on other users' favorites pages</li>
<li>Filter by rating</li>
</ul>
<h5>Gallery Hotkeys:</h5>
<ul>
<li><span class="hotkey">F</span> -- Add favorite</li>
<li><span class="hotkey">X</span> -- Remove favorite</li>
<li><span class="hotkey">M</span> -- Mute/unmute video</li>
<li><span class="hotkey">B</span> -- Toggle background</li>
</ul>
<h5>Other Controls:</h5>
<ul>
<li><span class="hotkey">Shift + Scroll Wheel</span> -- Change column count</li>
<li><span class="hotkey">T</span> -- Toggle tooltips</li>
<li><span class="hotkey">D</span> -- Toggle details</li>
</ul>
<span style="display: none;">
<h5>Performance:</h5>
<ul>
<li>Reduced memory/network usage</li>
<li>Reduced load time</li>
<li>Seamless video playback (desktop)</li>
</ul>
<h5>Planned Features:</h5>
<ul>
<li>Edit custom tags (basically folders/pools) on:</li>
<ul>
<li>search pages</li>
<li>post pages</li>
</ul>
<li>Fix comic strips</li>
<li>Gallery autoplay</li>
</span>
</ul>
</div>
</a>
</span>
</div>
<div>
<textarea name="tags" id="favorites-search-box" placeholder="Search with Tags and/or IDs"
spellcheck="false"></textarea>
</div>
<div id="left-favorites-panel-bottom-row">
<div id="favorite-options-container">
<div id="show-options"><label class="checkbox" title="Show more options"><input type="checkbox"
id="options-checkbox"> More Options</label></div>
<div id="favorite-options">
<div><label class="checkbox" title="Enable gallery and other features on search pages"><input
type="checkbox" id="enable-on-search-pages">
Enhance Search Pages</label></div>
<div style="display: none;"><label class="checkbox" title="Toggle remove buttons"><input type="checkbox"
id="show-remove-favorite-buttons">
Remove Buttons</label></div>
<div style="display: none;"><label class="checkbox" title="Toggle add favorite buttons"><input
type="checkbox" id="show-add-favorite-buttons">
Add Favorite 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><label class="checkbox" title="Enable fancy image hovering (experimental)"><input type="checkbox"
id="fancy-image-hovering-checkbox"> Fancy Hovering</label></div>
</div>
<div id="additional-favorite-options">
<div id="sort-container" title="Sort order of search results">
<div>
<label style="margin-right: 22px;" for="sorting-method">Sort By</label>
<label style="margin-left: 22px;" for="sort-ascending">Ascending</label>
</div>
<div style="position: relative; bottom: 4px;">
<select id="sorting-method" style="width: 150px;">
<option value="default">Default</option>
<option value="score">Score</option>
<option value="width">Width</option>
<option value="height">Height</option>
<option value="create">Date Uploaded</option>
<option value="change">Date Changed</option>
<!-- <option value="id">ID</option> -->
</select>
<input type="checkbox" id="sort-ascending">
</div>
</div>
<div id="rating-container" title="Filter search results by rating">
<label>Rating</label>
<br>
<div id="allowed-ratings">
<input type="checkbox" id="explicit-rating-checkbox" checked>
<label for="explicit-rating-checkbox">Explicit</label>
<input type="checkbox" id="questionable-rating-checkbox" checked>
<label for="questionable-rating-checkbox">Questionable</label>
<input type="checkbox" id="safe-rating-checkbox" checked>
<label for="safe-rating-checkbox" style="margin: -3px;">Safe</label>
</div>
</div>
<div id="performance-profile-container" title="Improve performance by disabling features">
<label for="performance-profile">Performance Profile</label>
<br>
<select id="performance-profile">
<option value="0">Normal</option>
<option value="1">Low (no gallery)</option>
<option value="2">Potato (only search)</option>
</select>
</div>
<div id="results-per-page-container"
title="Set the maximum number of search results to display on each page\nLower numbers improve responsiveness">
<label id="results-per-page-label" for="results-per-page-input">Results per Page</label>
<br>
<input type="number" id="results-per-page-input" min="50" max="10000" step="500">
</div>
<div id="column-resize-container" title="Set the number of favorites per row">
<div>
<label>Columns</label>
<br>
<button id="column-resize-minus">
<svg xmlns="http://www.w3.org/2000/svg" id="Isolation_Mode" data-name="Isolation Mode"
viewBox="0 0 24 24">
<rect x="6" y="10.5" width="12" height="3" />
</svg>
</button>
<input type="number" id="column-resize-input" min="2" max="20">
<button id="column-resize-plus">
<svg xmlns="http://www.w3.org/2000/svg" id="Isolation_Mode" data-name="Isolation Mode"
viewBox="0 0 24 24">
<polygon
points="18 10.5 13.5 10.5 13.5 6 10.5 6 10.5 10.5 6 10.5 6 13.5 10.5 13.5 10.5 18 13.5 18 13.5 13.5 18 13.5 18 10.5" />
</svg>
</button>
</div>
</div>
</div>
</div>
<div id="show-ui-container">
<div id="show-ui-div"><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: 1;"></div>
</div>
<div class="loading-wheel" id="loading-wheel" style="display: none;"></div>
</div>
`;/* eslint-disable no-bitwise */
if (onFavoritesPage()) {
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_PREFERENCES = {
showAuxillaryButtons: userIsOnTheirOwnFavoritesPage() ? "showRemoveButtons" : "showAddFavoriteButtons",
showOptions: "showOptions",
filterBlacklist: "filterBlacklistCheckbox",
searchHistory: "favoritesSearchHistory",
findFavorite: "findFavorite",
thumbSize: "thumbSize",
columnCount: "columnCount",
showUI: "showUI",
performanceProfile: "performanceProfile",
resultsPerPage: "resultsPerPage",
fancyImageHovering: "fancyImageHovering",
enableOnSearchPages: "enableOnSearchPages",
sortAscending: "sortAscending",
sortingMethod: "sortingMethod",
allowedRatings: "allowedRatings"
};
const FAVORITE_LOCAL_STORAGE = {
searchHistory: "favoritesSearchHistory"
};
const FAVORITE_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_CHECKBOXES = {
showOptions: document.getElementById("options-checkbox"),
showAuxillaryButtons: userIsOnTheirOwnFavoritesPage() ? document.getElementById("show-remove-favorite-buttons") : document.getElementById("show-add-favorite-buttons"),
filterBlacklist: document.getElementById("filter-blacklist-checkbox"),
showUI: document.getElementById("show-ui"),
fancyImageHovering: document.getElementById("fancy-image-hovering-checkbox"),
enableOnSearchPages: document.getElementById("enable-on-search-pages"),
sortAscending: document.getElementById("sort-ascending"),
explicitRating: document.getElementById("explicit-rating-checkbox"),
questionableRating: document.getElementById("questionable-rating-checkbox"),
safeRating: document.getElementById("safe-rating-checkbox")
};
const FAVORITE_INPUTS = {
searchBox: document.getElementById("favorites-search-box"),
findFavorite: document.getElementById("find-favorite-input"),
columnCount: document.getElementById("column-resize-input"),
performanceProfile: document.getElementById("performance-profile"),
resultsPerPage: document.getElementById("results-per-page-input"),
sortingMethod: document.getElementById("sorting-method"),
allowedRatings: document.getElementById("allowed-ratings")
};
const FAVORITE_SEARCH_LABELS = {
findFavorite: document.getElementById("find-favorite-label")
};
const columnWheelResizeCaptionCooldown = new Cooldown(500, true);
let searchHistory = [];
let searchHistoryIndex = 0;
let lastSearchQuery = "";
function initializeFavoritesPage() {
setMainButtonInteractability(false);
addEventListenersToFavoritesPage();
loadFavoritesPagePreferences();
removePaginatorFromFavoritesPage();
configureAuxillaryButtonOptionVisibility();
configureMobileUI();
configureDesktopUI();
setupWhatsNewDropdown();
}
function loadFavoritesPagePreferences() {
const userIsLoggedIn = getUserId() !== null;
const showAuxillaryButtonsDefault = !userIsOnTheirOwnFavoritesPage() && userIsLoggedIn;
const auxillaryFavoriteButtonsAreVisible = getPreference(FAVORITE_PREFERENCES.showAuxillaryButtons, showAuxillaryButtonsDefault);
FAVORITE_CHECKBOXES.showAuxillaryButtons.checked = auxillaryFavoriteButtonsAreVisible;
setTimeout(() => {
toggleAuxillaryButtons();
}, 100);
const showOptions = getPreference(FAVORITE_PREFERENCES.showOptions, false);
FAVORITE_CHECKBOXES.showOptions.checked = showOptions;
toggleFavoritesOptions(showOptions);
if (userIsOnTheirOwnFavoritesPage()) {
FAVORITE_CHECKBOXES.filterBlacklist.checked = getPreference(FAVORITE_PREFERENCES.filterBlacklist, false);
favoritesLoader.toggleTagBlacklistExclusion(FAVORITE_CHECKBOXES.filterBlacklist.checked);
} else {
FAVORITE_CHECKBOXES.filterBlacklist.checked = true;
FAVORITE_CHECKBOXES.filterBlacklist.parentElement.style.display = "none";
}
searchHistory = JSON.parse(localStorage.getItem(FAVORITE_LOCAL_STORAGE.searchHistory)) || [];
if (searchHistory.length > 0) {
FAVORITE_INPUTS.searchBox.value = searchHistory[0];
}
FAVORITE_INPUTS.findFavorite.value = getPreference(FAVORITE_PREFERENCES.findFavorite, "");
FAVORITE_INPUTS.columnCount.value = getPreference(FAVORITE_PREFERENCES.columnCount, DEFAULTS.columnCount);
changeColumnCount(FAVORITE_INPUTS.columnCount.value);
const showUI = getPreference(FAVORITE_PREFERENCES.showUI, true);
FAVORITE_CHECKBOXES.showUI.checked = showUI;
toggleUI(showUI);
const performanceProfile = getPerformanceProfile();
for (const option of FAVORITE_INPUTS.performanceProfile.children) {
if (parseInt(option.value) === performanceProfile) {
option.selected = "selected";
}
}
const resultsPerPage = parseInt(getPreference(FAVORITE_PREFERENCES.resultsPerPage, DEFAULTS.resultsPerPage));
changeResultsPerPage(resultsPerPage, false);
if (onMobileDevice()) {
toggleFancyImageHovering(false);
FAVORITE_CHECKBOXES.fancyImageHovering.parentElement.style.display = "none";
FAVORITE_CHECKBOXES.enableOnSearchPages.parentElement.style.display = "none";
} else {
const fancyImageHovering = getPreference(FAVORITE_PREFERENCES.fancyImageHovering, false);
FAVORITE_CHECKBOXES.fancyImageHovering.checked = fancyImageHovering;
toggleFancyImageHovering(fancyImageHovering);
}
FAVORITE_CHECKBOXES.enableOnSearchPages.checked = getPreference(FAVORITE_PREFERENCES.enableOnSearchPages, false);
FAVORITE_CHECKBOXES.sortAscending.checked = getPreference(FAVORITE_PREFERENCES.sortAscending, false);
const sortingMethod = getPreference(FAVORITE_PREFERENCES.sortingMethod, "default");
for (const option of FAVORITE_INPUTS.sortingMethod) {
if (option.value === sortingMethod) {
option.selected = "selected";
}
}
const allowedRatings = loadAllowedRatings();
FAVORITE_CHECKBOXES.explicitRating.checked = (allowedRatings & 4) === 4;
FAVORITE_CHECKBOXES.questionableRating.checked = (allowedRatings & 2) === 2;
FAVORITE_CHECKBOXES.safeRating.checked = (allowedRatings & 1) === 1;
preventUserFromUncheckingAllRatings(allowedRatings);
}
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_BUTTONS.search.onclick = (event) => {
const query = FAVORITE_INPUTS.searchBox.value;
if (event.ctrlKey) {
const queryWithFormattedIds = query.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
openSearchPage(queryWithFormattedIds);
} else {
hideAwesomplete(FAVORITE_INPUTS.searchBox);
favoritesLoader.searchFavorites(query);
addToFavoritesSearchHistory(query);
}
};
FAVORITE_BUTTONS.search.addEventListener("contextmenu", (event) => {
const queryWithFormattedIds = FAVORITE_INPUTS.searchBox.value.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
openSearchPage(queryWithFormattedIds);
event.preventDefault();
});
FAVORITE_INPUTS.searchBox.addEventListener("keydown", (event) => {
switch (event.key) {
case "Enter":
if (awesompleteIsUnselected(FAVORITE_INPUTS.searchBox)) {
event.preventDefault();
FAVORITE_BUTTONS.search.click();
} else {
clearAwesompleteSelection(FAVORITE_INPUTS.searchBox);
}
break;
case "ArrowUp":
case "ArrowDown":
if (awesompleteIsVisible(FAVORITE_INPUTS.searchBox)) {
updateLastSearchQuery();
} else {
event.preventDefault();
traverseFavoritesSearchHistory(event.key);
}
break;
default:
updateLastSearchQuery();
break;
}
});
FAVORITE_INPUTS.searchBox.addEventListener("wheel", (event) => {
if (event.shiftKey || event.ctrlKey) {
return;
}
const direction = event.deltaY > 0 ? "ArrowDown" : "ArrowUp";
traverseFavoritesSearchHistory(direction);
event.preventDefault();
});
FAVORITE_CHECKBOXES.showOptions.onchange = () => {
toggleFavoritesOptions(FAVORITE_CHECKBOXES.showOptions.checked);
setPreference(FAVORITE_PREFERENCES.showOptions, FAVORITE_CHECKBOXES.showOptions.checked);
};
FAVORITE_CHECKBOXES.showAuxillaryButtons.onchange = () => {
toggleAuxillaryButtons();
setPreference(FAVORITE_PREFERENCES.showAuxillaryButtons, FAVORITE_CHECKBOXES.showAuxillaryButtons.checked);
};
FAVORITE_BUTTONS.shuffle.onclick = () => {
favoritesLoader.shuffleSearchResults();
};
FAVORITE_BUTTONS.clear.onclick = () => {
FAVORITE_INPUTS.searchBox.value = "";
};
FAVORITE_CHECKBOXES.filterBlacklist.onchange = () => {
setPreference(FAVORITE_PREFERENCES.filterBlacklist, FAVORITE_CHECKBOXES.filterBlacklist.checked);
favoritesLoader.toggleTagBlacklistExclusion(FAVORITE_CHECKBOXES.filterBlacklist.checked);
favoritesLoader.searchFavorites();
};
FAVORITE_BUTTONS.invert.onclick = () => {
favoritesLoader.invertSearchResults();
};
FAVORITE_BUTTONS.reset.onclick = () => {
favoritesLoader.deletePersistentData();
};
FAVORITE_INPUTS.findFavorite.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
scrollToThumb(FAVORITE_INPUTS.findFavorite.value);
setPreference(FAVORITE_PREFERENCES.findFavorite, FAVORITE_INPUTS.findFavorite.value);
}
});
FAVORITE_BUTTONS.findFavorite.onclick = () => {
scrollToThumb(FAVORITE_INPUTS.findFavorite.value);
setPreference(FAVORITE_PREFERENCES.findFavorite, FAVORITE_INPUTS.findFavorite.value);
};
FAVORITE_BUTTONS.columnPlus.onclick = () => {
changeColumnCount(parseInt(FAVORITE_INPUTS.columnCount.value) + 1);
};
FAVORITE_BUTTONS.columnMinus.onclick = () => {
changeColumnCount(parseInt(FAVORITE_INPUTS.columnCount.value) - 1);
};
FAVORITE_INPUTS.columnCount.onchange = () => {
changeColumnCount(parseInt(FAVORITE_INPUTS.columnCount.value));
};
FAVORITE_CHECKBOXES.showUI.onchange = () => {
toggleUI(FAVORITE_CHECKBOXES.showUI.checked);
};
FAVORITE_INPUTS.performanceProfile.onchange = () => {
setPreference(FAVORITE_PREFERENCES.performanceProfile, parseInt(FAVORITE_INPUTS.performanceProfile.value));
window.location.reload();
};
FAVORITE_INPUTS.resultsPerPage.onchange = () => {
changeResultsPerPage(parseInt(FAVORITE_INPUTS.resultsPerPage.value));
};
if (!onMobileDevice()) {
FAVORITE_CHECKBOXES.fancyImageHovering.onchange = () => {
toggleFancyImageHovering(FAVORITE_CHECKBOXES.fancyImageHovering.checked);
setPreference(FAVORITE_PREFERENCES.fancyImageHovering, FAVORITE_CHECKBOXES.fancyImageHovering.checked);
};
}
FAVORITE_CHECKBOXES.enableOnSearchPages.onchange = () => {
setPreference(FAVORITE_PREFERENCES.enableOnSearchPages, FAVORITE_CHECKBOXES.enableOnSearchPages.checked);
};
FAVORITE_CHECKBOXES.sortAscending.onchange = () => {
setPreference(FAVORITE_PREFERENCES.sortAscending, FAVORITE_CHECKBOXES.sortAscending.checked);
favoritesLoader.onSortingParametersChanged();
};
FAVORITE_INPUTS.sortingMethod.onchange = () => {
setPreference(FAVORITE_PREFERENCES.sortingMethod, FAVORITE_INPUTS.sortingMethod.value);
favoritesLoader.onSortingParametersChanged();
};
FAVORITE_INPUTS.allowedRatings.onchange = () => {
changeAllowedRatings();
};
window.addEventListener("wheel", (event) => {
if (!event.shiftKey) {
return;
}
const delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
const columnAddend = delta > 0 ? -1 : 1;
if (columnWheelResizeCaptionCooldown.ready) {
forceHideCaptions(true);
}
changeColumnCount(parseInt(FAVORITE_INPUTS.columnCount.value) + columnAddend);
}, {
passive: true
});
columnWheelResizeCaptionCooldown.onDebounceEnd = () => {
forceHideCaptions(false);
};
columnWheelResizeCaptionCooldown.onCooldownEnd = () => {
if (!columnWheelResizeCaptionCooldown.debouncing) {
forceHideCaptions(false);
}
};
window.addEventListener("readyToSearch", () => {
setMainButtonInteractability(true);
}, {
once: true
});
document.addEventListener("keydown", (event) => {
if (event.key.toLowerCase() !== "r" || event.repeat || isTypeableInput(event.target)) {
return;
}
FAVORITE_CHECKBOXES.showAuxillaryButtons.click();
}, {
passive: true
});
}
function configureAuxillaryButtonOptionVisibility() {
FAVORITE_CHECKBOXES.showAuxillaryButtons.parentElement.parentElement.style.display = "block";
}
function updateLastSearchQuery() {
if (FAVORITE_INPUTS.searchBox.value !== lastSearchQuery) {
lastSearchQuery = FAVORITE_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_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_INPUTS.searchBox.value = lastSearchQuery;
} else {
FAVORITE_INPUTS.searchBox.value = searchHistory[searchHistoryIndex];
}
}
}
/**
* @param {Boolean} value
*/
function toggleFavoritesOptions(value) {
for (const option of FAVORITE_OPTIONS) {
option.style.display = value ? "block" : "none";
}
}
function toggleAuxillaryButtons() {
const value = FAVORITE_CHECKBOXES.showAuxillaryButtons.checked;
toggleAuxillaryButtonVisibility(value);
hideThumbHoverOutlines(value);
forceHideCaptions(value);
if (!value) {
dispatchEvent(new Event("captionOverrideEnd"));
}
}
/**
* @param {Boolean} hideOutlines
*/
function hideThumbHoverOutlines(hideOutlines) {
const style = hideOutlines ? STYLES.thumbHoverOutlineDisabled : STYLES.thumbHoverOutline;
injectStyleHTML(style, "thumb-hover-outlines");
}
/**
* @param {Boolean} value
*/
function toggleAuxillaryButtonVisibility(value) {
const visibility = value ? "visible" : "hidden";
injectStyleHTML(`
.auxillary-button {
visibility: ${visibility} !important;
}
`, "auxillary-button-visibility");
}
/**
* @param {Number} count
*/
function changeColumnCount(count) {
count = parseInt(count);
if (isNaN(count)) {
FAVORITE_INPUTS.columnCount.value = getPreference(FAVORITE_PREFERENCES.columnCount, DEFAULTS.columnCount);
return;
}
count = clamp(parseInt(count), 4, 20);
injectStyleHTML(`
#content {
grid-template-columns: repeat(${count}, 1fr) !important;
}
`, "columnCount");
FAVORITE_INPUTS.columnCount.value = count;
setPreference(FAVORITE_PREFERENCES.columnCount, count);
}
/**
* @param {Number} resultsPerPage
* @param {Boolean} search
*/
function changeResultsPerPage(resultsPerPage, search = true) {
resultsPerPage = parseInt(resultsPerPage);
if (isNaN(resultsPerPage)) {
FAVORITE_INPUTS.resultsPerPage.value = getPreference(FAVORITE_PREFERENCES.resultsPerPage, DEFAULTS.resultsPerPage);
return;
}
resultsPerPage = clamp(resultsPerPage, 50, 5000);
FAVORITE_INPUTS.resultsPerPage.value = resultsPerPage;
setPreference(FAVORITE_PREFERENCES.resultsPerPage, resultsPerPage);
if (search) {
favoritesLoader.updateMaxNumberOfFavoritesToDisplay(resultsPerPage);
}
}
/**
* @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";
}
showUIDiv.classList.toggle("ui-hidden", !value);
setPreference(FAVORITE_PREFERENCES.showUI, value);
}
function configureMobileUI() {
if (!onMobileDevice()) {
return;
}
FAVORITE_INPUTS.performanceProfile.parentElement.style.display = "none";
injectStyleHTML(`
.thumb, .thumb-node {
> div > canvas {
display: none;
}
}
.checkbox {
input[type="checkbox"] {
margin-right: 10px;
}
}
#favorites-top-bar-panels {
>div {
textarea {
width: 95% !important;
}
}
}
#container {
position: fixed !important;
z-index: 30;
width: 100vw;
}
#content {
margin-top: 300px !important;
}
#show-ui-container {
display: none;
}
#favorite-options-container {
display: block !important;
}
#favorites-top-bar-panels {
display: block !important;
}
#right-favorites-panel {
margin-left: 0px !important;
}
#left-favorites-panel-bottom-row {
margin-left: 10px !important;
}
`);
const container = document.createElement("div");
container.id = "container";
document.body.insertAdjacentElement("afterbegin", container);
container.appendChild(document.getElementById("header"));
container.appendChild(document.getElementById("favorites-top-bar"));
const helpLinksContainer = document.getElementById("help-links-container");
if (helpLinksContainer !== null) {
helpLinksContainer.innerHTML = "<a href=\"https://github.com/bruh3396/favorites-search-gallery#controls\" target=\"_blank\">Help</a>";
}
}
function configureDesktopUI() {
if (onMobileDevice()) {
return;
}
injectStyleHTML(`
.checkbox {
&:hover {
color: #000;
background: #93b393;
text-shadow: none;
cursor: pointer;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
}
#sort-ascending {
width: 20px;
height: 20px;
}
`);
}
function setupWhatsNewDropdown() {
if (onMobileDevice()) {
return;
}
const whatsNew = document.getElementById("whats-new-link");
if (whatsNew === null) {
return;
}
whatsNew.onclick = () => {
if (whatsNew.classList.contains("persistent")) {
whatsNew.classList.remove("persistent");
whatsNew.classList.add("hidden");
} else {
whatsNew.classList.add("persistent");
}
return false;
};
whatsNew.onblur = () => {
whatsNew.classList.remove("persistent");
whatsNew.classList.add("hidden");
};
whatsNew.onmouseenter = () => {
whatsNew.classList.remove("hidden");
};
whatsNew.onmouseleave = () => {
whatsNew.classList.add("hidden");
};
}
/**
* @returns {Number}
*/
function loadAllowedRatings() {
return parseInt(getPreference("allowedRatings", 7));
}
function changeAllowedRatings() {
let allowedRatings = 0;
if (FAVORITE_CHECKBOXES.explicitRating.checked) {
allowedRatings += 4;
}
if (FAVORITE_CHECKBOXES.questionableRating.checked) {
allowedRatings += 2;
}
if (FAVORITE_CHECKBOXES.safeRating.checked) {
allowedRatings += 1;
}
setPreference(FAVORITE_PREFERENCES.allowedRatings, allowedRatings);
favoritesLoader.onAllowedRatingsChanged(allowedRatings);
preventUserFromUncheckingAllRatings(allowedRatings);
}
/**
* @param {Number} allowedRatings
*/
function preventUserFromUncheckingAllRatings(allowedRatings) {
if (allowedRatings === 4) {
FAVORITE_CHECKBOXES.explicitRating.nextElementSibling.style.pointerEvents = "none";
} else if (allowedRatings === 2) {
FAVORITE_CHECKBOXES.questionableRating.nextElementSibling.style.pointerEvents = "none";
} else if (allowedRatings === 1) {
FAVORITE_CHECKBOXES.safeRating.nextElementSibling.style.pointerEvents = "none";
} else {
FAVORITE_CHECKBOXES.explicitRating.nextElementSibling.removeAttribute("style");
FAVORITE_CHECKBOXES.questionableRating.nextElementSibling.removeAttribute("style");
FAVORITE_CHECKBOXES.safeRating.nextElementSibling.removeAttribute("style");
}
}
function setMainButtonInteractability(value) {
const container = document.getElementById("left-favorites-panel-top-row");
if (container === null) {
return;
}
const mainButtons = Array.from(container.children).filter(child => child.tagName.toLowerCase() === "button" && child.textContent !== "Reset");
for (const button of mainButtons) {
button.disabled = !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;
}
}
console.error(`Could not find user with more than ${X} favorites`);
}
if (onFavoritesPage()) {
initializeFavoritesPage();
}
// gallery.js
const galleryHTML = `<style>
body {
width: 99.5vw;
overflow-x: hidden;
}
.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%);
}
#original-content-container {
>canvas,img {
float: left;
overflow: hidden;
pointer-events: none;
position: fixed;
height: 100vh;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
#original-video-container {
video {
display: none;
position:fixed;
z-index:9998;
pointer-events:none;
}
}
#low-resolution-canvas {
z-index: 9996;
}
#main-canvas {
z-index: 9997;
}
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 {
width: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 1;
}
}
}
.fullscreen-icon {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
pointer-events: none;
width: 30%;
}
</style>
`;/* eslint-disable no-useless-escape */
const galleryDebugHTML = `
.thumb,
.thumb-node {
&.debug-selected {
outline: 3px solid #0075FF !important;
}
&.loaded {
div {
outline: 2px solid transparent;
animation: outlineGlow 1s forwards;
}
.image {
opacity: 1;
}
}
>div>canvas {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 1;
visibility: hidden;
}
.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;
}
}
#main-canvas, #low-resolution-canvas {
opacity: 0.25;
}
#original-video-container {
video {
opacity: 0.15;
}
}
`;
class Gallery {
static clickTypes = {
left: 0,
middle: 1
};
static directions = {
d: "d",
a: "a",
right: "ArrowRight",
left: "ArrowLeft"
};
static preferences = {
showOnHover: "showImagesWhenHovering",
backgroundOpacity: "galleryBackgroundOpacity",
resolution: "galleryResolution",
enlargeOnClick: "enlargeOnClick",
autoplay: "autoplay",
videoVolume: "videoVolume",
videoMuted: "videoMuted"
};
static localStorageKeys = {
imageExtensions: "imageExtensions"
};
static webWorkers = {
renderer:
`
/* eslint-disable max-classes-per-file */
/* eslint-disable prefer-template */
/**
* @param {Number} milliseconds
* @returns {Promise}
*/
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
/**
* @param {Number} pixelCount
* @returns {Number}
*/
function estimateMegabyteSize(pixelCount) {
const rgb = 3;
const bytes = rgb * pixelCount;
const numberOfBytesInMegabyte = 1048576;
return bytes / numberOfBytesInMegabyte;
}
class RenderRequest {
/**
* @type {String}
*/
id;
/**
* @type {String}
*/
imageURL;
/**
* @type {String}
*/
extension;
/**
* @type {String}
*/
thumbURL;
/**
* @type {String}
*/
fetchDelay;
/**
* @type {Number}
*/
pixelCount;
/**
* @type {OffscreenCanvas}
*/
canvas;
/**
* @type {Number}
*/
resolutionFraction;
/**
* @type {AbortController}
*/
abortController;
/**
* @type {Boolean}
*/
alreadyStarted;
/**
* @param {{id: String, imageURL: String, extension: String, thumbURL: String, fetchDelay: String, pixelCount: Number, canvas: OffscreenCanvas, resolutionFraction: Number}} request
*/
constructor(request) {
this.id = request.id;
this.imageURL = request.imageURL;
this.extension = request.extension;
this.thumbURL = request.thumbURL;
this.fetchDelay = request.fetchDelay;
this.pixelCount = request.pixelCount;
this.canvas = request.canvas;
this.resolutionFraction = request.resolutionFraction;
this.abortController = new AbortController();
this.alreadyStarted = false;
}
}
class BatchRenderRequest {
static settings = {
megabyteMemoryLimit: 1000,
batchSizeMinimum: 10
};
/**
* @type {String}
*/
id;
/**
* @type {String}
*/
requestType;
/**
* @type {RenderRequest[]}
*/
renderRequests;
/**
* @type {RenderRequest[]}
*/
allRenderRequests;
get renderRequestIds() {
return new Set(this.renderRequests.map(request => request.id));
}
/**
* @param {{
* id: String,
* requestType: String,
* renderRequests: {id: String, imageURL: String, extension: String, thumbURL: String, fetchDelay: String, pixelCount: Number, canvas: OffscreenCanvas, resolutionFraction: Number}[]
* }} batchRequest
*/
constructor(batchRequest) {
this.id = batchRequest.id;
this.requestType = batchRequest.requestType;
this.renderRequests = batchRequest.renderRequests.map(r => new RenderRequest(r));
this.allRenderRequests = this.renderRequests;
this.truncateRenderRequestsExceedingMemoryLimit();
}
truncateRenderRequestsExceedingMemoryLimit() {
const truncatedRequest = [];
let currentMegabyteSize = 0;
for (const request of this.renderRequests) {
if (currentMegabyteSize < BatchRenderRequest.settings.megabyteMemoryLimit || truncatedRequest.length < BatchRenderRequest.settings.batchSizeMinimum) {
truncatedRequest.push(request);
currentMegabyteSize += estimateMegabyteSize(request.pixelCount);
} else {
postMessage({
action: "renderDeleted",
id: request.id
});
}
}
this.renderRequests = truncatedRequest;
}
}
class ImageFetcher {
/**
* @type {Set.<String>}
*/
static idsToFetchFromPostPages = new Set();
static get postPageFetchDelay() {
return ImageFetcher.idsToFetchFromPostPages.size * 250;
}
/**
* @param {RenderRequest} request
*/
static async setOriginalImageURLAndExtension(request) {
if (request.extension !== null && request.extension !== undefined) {
request.imageURL = request.imageURL.replace("jpg", request.extension);
} else {
// eslint-disable-next-line require-atomic-updates
request.imageURL = await ImageFetcher.getOriginalImageURL(request.id);
request.extension = ImageFetcher.getExtensionFromImageURL(request.imageURL);
}
}
/**
* @param {String} id
* @returns {String}
*/
static getOriginalImageURL(id) {
const apiURL = "https://api.rule34.xxx//index.php?page=dapi&s=post&q=index&id=" + id;
return fetch(apiURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status + ": " + id);
})
.then((html) => {
return (/ file_url="(.*?)"/).exec(html)[1].replace("api-cdn.", "");
}).catch(() => {
return ImageFetcher.getOriginalImageURLFromPostPage(id);
});
}
/**
* @param {String} id
* @returns {String}
*/
static async getOriginalImageURLFromPostPage(id) {
const postPageURL = "https://rule34.xxx/index.php?page=post&s=view&id=" + id;
ImageFetcher.idsToFetchFromPostPages.add(id);
await sleep(ImageFetcher.postPageFetchDelay);
return fetch(postPageURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status + ": " + postPageURL);
})
.then((html) => {
ImageFetcher.idsToFetchFromPostPages.delete(id);
return (/itemprop="image" content="(.*)"/g).exec(html)[1].replace("us.rule34", "rule34");
}).catch((error) => {
if (!error.message.includes("503")) {
console.error({
error,
url: postPageURL
});
return "https://rule34.xxx/images/r34chibi.png";
}
return ImageFetcher.getOriginalImageURLFromPostPage(id);
});
}
/**
* @param {String} imageURL
* @returns {String}
*/
static getExtensionFromImageURL(imageURL) {
try {
return (/\.(png|jpg|jpeg|gif)/g).exec(imageURL)[1];
} catch (error) {
return "jpg";
}
}
/**
* @param {RenderRequest} request
* @returns {Promise}
*/
static fetchImage(request) {
return fetch(request.imageURL, {
signal: request.abortController.signal
});
}
/**
* @param {RenderRequest} request
* @returns {Blob}
*/
static async fetchImageBlob(request) {
const response = await ImageFetcher.fetchImage(request);
return response.blob();
}
/**
* @param {String} id
* @returns {String}
*/
static async findImageExtensionFromId(id) {
const imageURL = await ImageFetcher.getOriginalImageURL(id);
const extension = ImageFetcher.getExtensionFromImageURL(imageURL);
postMessage({
action: "extensionFound",
id,
extension
});
}
}
class ThumbUpscaler {
static settings = {
maxCanvasHeight: 16000
};
/**
* @type {Map.<String, OffscreenCanvas>}
*/
canvases = new Map();
/**
* @type {Number}
*/
screenWidth;
/**
* @type {Boolean}
*/
onSearchPage;
/**
* @param {Number} screenWidth
* @param {Boolean} onSearchPage
*/
constructor(screenWidth, onSearchPage) {
this.screenWidth = screenWidth;
this.onSearchPage = onSearchPage;
}
/**
* @param {{id: String, imageURL: String, canvas: OffscreenCanvas, resolutionFraction: Number}[]} message
*/
async upscaleMultipleAnimatedCanvases(message) {
const requests = message.map(r => new RenderRequest(r));
requests.forEach((request) => {
this.collectCanvas(request);
});
for (const request of requests) {
ImageFetcher.fetchImage(request)
.then((response) => {
return response.blob();
})
.then((blob) => {
createImageBitmap(blob)
.then((imageBitmap) => {
this.upscaleCanvas(request, imageBitmap);
});
});
await sleep(50);
}
}
/**
* @param {RenderRequest} request
* @param {ImageBitmap} imageBitmap
*/
upscaleCanvas(request, imageBitmap) {
if (this.onSearchPage || imageBitmap === undefined || !this.canvases.has(request.id)) {
return;
}
this.setCanvasDimensions(request, imageBitmap);
this.drawCanvas(request.id, imageBitmap);
}
/**
* @param {RenderRequest} request
* @param {ImageBitmap} imageBitmap
*/
setCanvasDimensions(request, imageBitmap) {
const canvas = this.canvases.get(request.id);
let width = this.screenWidth / request.resolutionFraction;
let height = (width / imageBitmap.width) * imageBitmap.height;
if (width > imageBitmap.width) {
width = imageBitmap.width;
height = imageBitmap.height;
}
if (height > ThumbUpscaler.settings.maxCanvasHeight) {
width *= (ThumbUpscaler.settings.maxCanvasHeight / height);
height = ThumbUpscaler.settings.maxCanvasHeight;
}
canvas.width = width;
canvas.height = height;
}
/**
* @param {String} id
* @param {ImageBitmap} imageBitmap
*/
drawCanvas(id, imageBitmap) {
const canvas = this.canvases.get(id);
const context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(
imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height,
0, 0, canvas.width, canvas.height
);
}
deleteAllCanvases() {
for (const [id, canvas] of this.canvases.entries()) {
this.deleteCanvas(id, canvas);
}
this.canvases.clear();
}
/**
* @param {String} id
* @param {OffscreenCanvas} canvas
*/
deleteCanvas(id, canvas) {
const context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = 0;
canvas.height = 0;
canvas = null;
this.canvases.set(id, canvas);
this.canvases.delete(id);
}
/**
* @param {RenderRequest} request
*/
collectCanvas(request) {
if (request.canvas === undefined) {
return;
}
if (!this.canvases.has(request.id)) {
this.canvases.set(request.id, request.canvas);
}
}
/**
* @param {BatchRenderRequest} batchRequest
*/
collectCanvases(batchRequest) {
batchRequest.allRenderRequests.forEach((request) => {
this.collectCanvas(request);
});
}
}
class ImageRenderer {
/**
* @type {OffscreenCanvas}
*/
canvas;
/**
* @type {CanvasRenderingContext2D}
*/
context;
/**
* @type {ThumbUpscaler}
*/
thumbUpscaler;
/**
* @type {RenderRequest}
*/
renderRequest;
/**
* @type {BatchRenderRequest}
*/
batchRenderRequest;
/**
* @type {Map.<String, RenderRequest>}
*/
incompleteRenderRequests;
/**
* @type {Map.<String, {completed: Boolean, imageBitmap: ImageBitmap, request: RenderRequest}>}
*/
renders;
/**
* @type {String}
*/
lastRequestedDrawId;
/**
* @type {String}
*/
currentlyDrawnId;
/**
* @type {Boolean}
*/
onMobileDevice;
/**
* @type {Boolean}
*/
onSearchPage;
/**
* @type {Boolean}
*/
usingLandscapeOrientation;
get hasRenderRequest() {
return this.renderRequest !== undefined &&
this.renderRequest !== null;
}
get hasBatchRenderRequest() {
return this.batchRenderRequest !== undefined &&
this.batchRenderRequest !== null;
}
/**
*
* @param {{canvas: OffscreenCanvas, screenWidth: Number, onMobileDevice: Boolean, onSearchPage: Boolean }} message
*/
constructor(message) {
this.canvas = message.canvas;
this.context = this.canvas.getContext("2d");
this.thumbUpscaler = new ThumbUpscaler(message.screenWidth, message.onSearchPage);
this.renders = new Map();
this.incompleteRenderRequests = new Map();
this.lastRequestedDrawId = "";
this.currentlyDrawnId = "";
this.onMobileDevice = message.onMobileDevice;
this.onSearchPage = message.onSearchPage;
this.usingLandscapeOrientation = true;
}
/**
* @param {BatchRenderRequest} batchRenderRequest
*/
async renderMultipleImages(batchRenderRequest) {
const batchRequestId = batchRenderRequest.id;
batchRenderRequest.renderRequests = batchRenderRequest.renderRequests
.filter(request => !this.renderIsFinished(request.id));
for (const request of batchRenderRequest.renderRequests) {
if (request.alreadyStarted || this.renders.has(request.id)) {
continue;
}
this.renders.set(request.id, {
completed: false,
imageBitmap: undefined,
request
});
}
for (const request of batchRenderRequest.renderRequests) {
if (this.isApartOfOutdatedBatchRequest(batchRequestId)) {
continue;
}
if (request.alreadyStarted) {
continue;
}
this.renderImage(request, batchRequestId);
await sleep(request.fetchDelay);
}
}
/**
* @param {RenderRequest} request
* @param {*} batchRequestId
*/
async renderImage(request, batchRequestId) {
this.incompleteRenderRequests.set(request.id, request);
await ImageFetcher.setOriginalImageURLAndExtension(request);
let blob;
if (this.isApartOfOutdatedBatchRequest(batchRequestId)) {
return;
}
try {
blob = await ImageFetcher.fetchImageBlob(request);
} catch (error) {
if (error.name === "AbortError") {
this.deleteRender(request.id);
} else {
console.error({
error,
request
});
}
return;
}
if (this.isApartOfOutdatedBatchRequest(batchRequestId)) {
return;
}
const imageBitmap = await createImageBitmap(blob);
if (this.isApartOfOutdatedBatchRequest(batchRequestId)) {
return;
}
this.renders.set(request.id, {
completed: true,
imageBitmap,
request
});
this.incompleteRenderRequests.delete(request.id);
this.thumbUpscaler.upscaleCanvas(request, imageBitmap);
postMessage({
action: "renderCompleted",
extension: request.extension,
id: request.id
});
if (this.lastRequestedDrawId === request.id) {
this.drawCanvas(request.id);
}
}
/**
* @param {String} id
* @returns {Boolean}
*/
renderIsFinished(id) {
const render = this.renders.get(id);
return render !== undefined && render.completed;
}
/**
* @param {String} id
* @returns {Boolean}
*/
isApartOfOutdatedBatchRequest(id) {
if (id === undefined || id === null) {
return false;
}
if (!this.hasBatchRenderRequest) {
return true;
}
return this.batchRenderRequest.renderRequestIds.has(id);
}
/**
* @param {String} id
*/
drawCanvas(id) {
const render = this.renders.get(id);
if (render === undefined || render.imageBitmap === undefined) {
this.clearCanvas();
return;
}
if (this.currentlyDrawnId === id) {
return;
}
if (render.completed) {
this.currentlyDrawnCanvasId = id;
}
const ratio = Math.min(this.canvas.width / render.imageBitmap.width, this.canvas.height / render.imageBitmap.height);
const centerShiftX = (this.canvas.width - (render.imageBitmap.width * ratio)) / 2;
const centerShiftY = (this.canvas.height - (render.imageBitmap.height * ratio)) / 2;
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.drawImage(
render.imageBitmap, 0, 0, render.imageBitmap.width, render.imageBitmap.height,
centerShiftX, centerShiftY, render.imageBitmap.width * ratio, render.imageBitmap.height * ratio
);
}
/**
* @param {Boolean} usingLandscapeOrientation
*/
changeCanvasOrientation(usingLandscapeOrientation) {
if (usingLandscapeOrientation !== this.usingLandscapeOrientation) {
this.swapCanvasOrientation();
}
}
swapCanvasOrientation() {
const temp = this.canvas.width;
this.canvas.width = this.canvas.height;
this.canvas.height = temp;
this.usingLandscapeOrientation = !this.usingLandscapeOrientation;
}
clearCanvas() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
deleteAllRenders() {
this.thumbUpscaler.deleteAllCanvases();
this.abortAllFetchRequests();
for (const id of this.renders.keys()) {
this.deleteRender(id, true);
}
this.batchRenderRequest = undefined;
this.renderRequest = undefined;
this.renders.clear();
}
/**
* @param {BatchRenderRequest} newBatchRenderRequest
*/
deleteRendersNotInNewRequest(newBatchRenderRequest) {
const idsToRender = newBatchRenderRequest.renderRequestIds;
for (const id of this.renders.keys()) {
if (!idsToRender.has(id)) {
this.deleteRender(id);
}
}
}
/**
* @param {String} id
* @param {Boolean} initiatedByMainThread
*/
deleteRender(id, initiatedByMainThread = false) {
if (!this.renders.has(id)) {
return;
}
const imageBitmap = this.renders.get(id).imageBitmap;
if (imageBitmap !== null && imageBitmap !== undefined) {
imageBitmap.close();
}
this.renders.set(id, null);
this.renders.delete(id);
if (initiatedByMainThread) {
return;
}
postMessage({
action: "renderDeleted",
id
});
}
/**
* @param {BatchRenderRequest} newBatchRenderRequest
*/
abortOutdatedFetchRequests(newBatchRenderRequest) {
const newIds = newBatchRenderRequest.renderRequestIds;
for (const [id, request] of this.incompleteRenderRequests.entries()) {
if (!newIds.has(id)) {
request.abortController.abort();
this.incompleteRenderRequests.delete(id);
}
}
}
abortAllFetchRequests() {
for (const request of this.incompleteRenderRequests.values()) {
request.abortController.abort();
}
this.incompleteRenderRequests.clear();
}
/**
* @param {BatchRenderRequest} newBatchRenderRequest
*/
removeDuplicateRenderRequests(newBatchRenderRequest) {
if (!this.hasBatchRenderRequest) {
return;
}
const oldIds = this.batchRenderRequest.renderRequestIds;
newBatchRenderRequest.renderRequests.forEach((request) => {
request.alreadyStarted = oldIds.has(request.id);
});
}
onmessage(message) {
let batchRenderRequest;
switch (message.action) {
case "render":
this.renderRequest = new RenderRequest(message);
this.lastRequestedDrawId = message.id;
this.thumbUpscaler.collectCanvas(this.renderRequest);
this.renderImage(this.renderRequest);
break;
case "renderMultiple":
batchRenderRequest = new BatchRenderRequest(message);
this.thumbUpscaler.collectCanvases(batchRenderRequest);
this.abortOutdatedFetchRequests(batchRenderRequest);
this.removeDuplicateRenderRequests(batchRenderRequest);
this.deleteRendersNotInNewRequest(batchRenderRequest);
this.batchRenderRequest = batchRenderRequest;
this.renderMultipleImages(batchRenderRequest);
break;
case "deleteAllRenders":
this.deleteAllRenders();
break;
case "drawMainCanvas":
this.lastRequestedDrawId = message.id;
this.drawCanvas(message.id);
break;
case "clearMainCanvas":
this.clearCanvas();
break;
case "upscaleAnimatedThumbs":
this.thumbUpscaler.upscaleMultipleAnimatedCanvases(message.upscaleRequests);
break;
case "changeCanvasOrientation":
this.changeCanvasOrientation(message.usingLandscapeOrientation);
break;
default:
break;
}
}
}
/**
* @type {ImageRenderer}
*/
let imageRenderer;
onmessage = (message) => {
message = message.data;
switch (message.action) {
case "initialize":
BatchRenderRequest.settings.megabyteMemoryLimit = message.megabyteLimit;
BatchRenderRequest.settings.batchSizeMinimum = message.minimumImagesToRender;
imageRenderer = new ImageRenderer(message);
break;
case "findExtension":
ImageFetcher.findImageExtensionFromId(message.id);
break;
default:
imageRenderer.onmessage(message);
break;
}
};
`
};
static mainCanvasResolutions = {
search: onMobileDevice() ? "7680x4320" : "3840x2160",
favorites: "7680x4320"
};
static htmlAttributes = {
thumbIndex: "index"
};
static extensionDecodings = {
0: "jpg",
1: "png",
2: "jpeg",
3: "gif"
};
static extensionEncodings = {
"jpg": 0,
"png": 1,
"jpeg": 2,
"gif": 3
};
static swipeControls = {
threshold: 60,
touchStart: {
x: 0,
y: 0
},
touchEnd: {
x: 0,
y: 0
},
get deltaX() {
return this.touchStart.x - this.touchEnd.x;
},
get deltaY() {
return this.touchStart.y - this.touchEnd.y;
},
get right() {
return this.deltaX < -this.threshold;
},
get left() {
return this.deltaX > this.threshold;
},
get up() {
return this.deltaY > this.threshold;
},
get down() {
return this.deltaY < -this.threshold;
},
/**
* @param {TouchEvent} touchEvent
* @param {Boolean} atStart
*/
set(touchEvent, atStart) {
if (atStart) {
this.touchStart.x = touchEvent.changedTouches[0].screenX;
this.touchStart.y = touchEvent.changedTouches[0].screenY;
} else {
this.touchEnd.x = touchEvent.changedTouches[0].screenX;
this.touchEnd.y = touchEvent.changedTouches[0].screenY;
}
}
};
static settings = {
maxImagesToRenderInBackground: 50,
maxImagesToRenderAround: onMobileDevice() ? 2 : 50,
megabyteLimit: onMobileDevice() ? 0 : 375,
minImagesToRender: onMobileDevice() ? 3 : 8,
imageFetchDelay: 250,
imageFetchDelayWhenExtensionKnown: 25,
upscaledThumbResolutionFraction: 5,
upscaledAnimatedThumbResolutionFraction: 5,
extensionsFoundBeforeSavingCount: 5,
animatedThumbsToUpscaleRange: 20,
animatedThumbsToUpscaleDiscrete: 20,
traversalCooldownTime: 300,
renderOnPageChangeCooldownTime: 2000,
autoplayTime: 5000,
addFavoriteCooldownTime: 250,
additionalVideoPlayerCount: onMobileDevice() ? 0 : 2,
renderAroundAggressively: true,
debugEnabled: false,
developerMode: false
};
static traversalCooldown = new Cooldown(Gallery.settings.traversalCooldownTime);
static renderOnPageChangeCooldown = new Cooldown(Gallery.settings.renderOnPageChangeCooldownTime, true);
static autoplayCooldown = new Cooldown(Gallery.settings.autoplayTime);
static changeFavoriteCooldown = new Cooldown(Gallery.settings.addFavoriteCooldownTime, true);
/**
* @returns {Boolean}
*/
static get disabled() {
return (onMobileDevice() && onSearchPage()) || getPerformanceProfile() > 0 || onPostPage();
}
/**
* @type {HTMLCanvasElement}
*/
mainCanvas;
/**
* @type {HTMLCanvasElement}
*/
lowResolutionCanvas;
/**
* @type {CanvasRenderingContext2D}
*/
lowResolutionContext;
/**
* @type {HTMLDivElement}
*/
videoContainer;
/**
* @type {HTMLVideoElement[]}
*/
videoPlayers;
/**
* @type {HTMLImageElement}
*/
gifContainer;
/**
* @type {HTMLDivElement}
*/
background;
/**
* @type {HTMLElement}
*/
thumbUnderCursor;
/**
* @type {HTMLElement}
*/
lastEnteredThumb;
/**
* @type {Worker}
*/
imageRenderer;
/**
* @type {Set.<String>}
*/
startedRenders;
/**
* @type {Set.<String>}
*/
completedRenders;
/**
* @type {Map.<String, HTMLCanvasElement>}
*/
transferredCanvases;
/**
* @type {Map.<String, {start: Number, end:Number}>}
*/
videoClips;
/**
* @type {HTMLElement[]}
*/
visibleThumbs;
/**
* @type {Object.<Number, String>}
*/
imageExtensions;
/**
* @type {Number}
*/
recentlyDiscoveredImageExtensionCount;
/**
* @type {Number}
*/
currentlySelectedThumbIndex;
/**
* @type {Number}
*/
lastSelectedThumbIndexBeforeEnteringGallery;
/**
* @type {Number}
*/
currentBatchRenderRequestId;
/**
* @type {Boolean}
*/
inGallery;
/**
* @type {Boolean}
*/
recentlyExitedGallery;
/**
* @type {Boolean}
*/
leftPage;
/**
* @type {Boolean}
*/
favoritesWereFetched;
/**
* @type {Boolean}
*/
finishedLoading;
/**
* @type {Boolean}
*/
showOriginalContentOnHover;
/**
* @type {Boolean}
*/
enlargeOnClickOnMobile;
/**
* @type {Boolean}
*/
autoplayEnabled;
constructor() {
if (Gallery.disabled) {
return;
}
this.initializeFields();
this.setMainCanvasResolution();
this.createWebWorkers();
this.createVideoBackgrounds();
this.addEventListeners();
this.loadDiscoveredImageExtensions();
this.prepareSearchPage();
this.injectHTML();
this.updateBackgroundOpacity(getPreference(Gallery.preferences.backgroundOpacity, 1));
this.loadVideoClips();
this.setMainCanvasOrientation();
}
initializeFields() {
this.mainCanvas = document.createElement("canvas");
this.lowResolutionCanvas = document.createElement("canvas");
this.lowResolutionContext = this.lowResolutionCanvas.getContext("2d");
this.thumbUnderCursor = null;
this.lastEnteredThumb = null;
this.startedRenders = new Set();
this.completedRenders = new Set();
this.transferredCanvases = new Map();
this.videoClips = new Map();
this.visibleThumbs = [];
this.imageExtensions = {};
this.recentlyDiscoveredImageExtensionCount = 0;
this.currentlySelectedThumbIndex = 0;
this.lastSelectedThumbIndexBeforeEnteringGallery = 0;
this.currentBatchRenderRequestId = 0;
this.inGallery = false;
this.recentlyExitedGallery = false;
this.leftPage = false;
this.favoritesWereFetched = false;
this.finishedLoading = onSearchPage();
this.showOriginalContentOnHover = getPreference(Gallery.preferences.showOnHover, true);
this.enlargeOnClickOnMobile = getPreference(Gallery.preferences.enlargeOnClick, true);
// this.autoplayEnabled = getPreference(Gallery.preferences.autoplay, false);
this.autoplayEnabled = false;
Gallery.renderOnPageChangeCooldown.onDebounceEnd = () => {
this.renderImagesInTheBackground();
};
}
setMainCanvasResolution() {
const resolution = onSearchPage() ? Gallery.mainCanvasResolutions.search : Gallery.mainCanvasResolutions.favorites;
const dimensions = resolution.split("x").map(dimension => parseFloat(dimension));
this.mainCanvas.width = dimensions[0];
this.mainCanvas.height = dimensions[1];
}
createWebWorkers() {
const offscreenCanvas = this.mainCanvas.transferControlToOffscreen();
this.imageRenderer = new Worker(getWorkerURL(Gallery.webWorkers.renderer));
this.imageRenderer.postMessage({
action: "initialize",
canvas: offscreenCanvas,
onMobileDevice: onMobileDevice(),
screenWidth: window.screen.width,
megabyteLimit: Gallery.settings.megabyteLimit,
minimumImagesToRender: Gallery.settings.minImagesToRender,
onSearchPage: onSearchPage()
}, [offscreenCanvas]);
}
createVideoBackgrounds() {
document.createElement("canvas").toBlob((blob) => {
const videoBackgroundURL = URL.createObjectURL(blob);
for (const video of this.videoPlayers) {
video.setAttribute("poster", videoBackgroundURL);
}
});
}
addEventListeners() {
this.addGalleryEventListeners();
this.addFavoritesLoaderEventListeners();
this.addWebWorkerMessageHandlers();
this.addMobileEventListeners();
this.addMemoryManagementEventListeners();
}
addGalleryEventListeners() {
window.addEventListener("load", () => {
if (onSearchPage()) {
this.initializeThumbsForHovering.bind(this)();
this.enumerateVisibleThumbs();
}
this.hideCaptionsWhenShowingOriginalContent();
}, {
once: true,
passive: true
});
document.addEventListener("mousedown", (event) => {
const clickedOnAnImage = event.target.tagName.toLowerCase() === "img";
const clickedOnAThumb = clickedOnAnImage && getThumbFromImage(event.target).className.includes("thumb");
const thumb = clickedOnAThumb ? getThumbFromImage(event.target) : null;
switch (event.button) {
case Gallery.clickTypes.left:
if (this.inGallery) {
if (isVideo(this.getSelectedThumb()) && !onMobileDevice()) {
return;
}
this.exitGallery();
this.toggleAllVisibility(false);
return;
}
if (thumb === null) {
return;
}
if (onMobileDevice()) {
if (!this.enlargeOnClickOnMobile) {
this.openPostInNewPage(thumb);
return;
}
this.deleteAllRenders();
}
this.toggleAllVisibility(true);
this.enterGallery();
this.showOriginalContent(thumb);
break;
case Gallery.clickTypes.middle:
event.preventDefault();
if (thumb !== null || this.inGallery) {
this.openPostInNewPage();
} else if (!this.inGallery) {
this.toggleAllVisibility();
setPreference(Gallery.preferences.showOnHover, this.showOriginalContentOnHover);
}
break;
default:
break;
}
});
window.addEventListener("auxclick", (event) => {
if (event.button === Gallery.clickTypes.middle) {
event.preventDefault();
}
});
document.addEventListener("wheel", (event) => {
if (event.shiftKey) {
return;
}
if (this.inGallery) {
if (event.ctrlKey) {
return;
}
const delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
const direction = delta > 0 ? Gallery.directions.left : Gallery.directions.right;
this.traverseGallery.bind(this)(direction, false);
} else if (this.thumbUnderCursor !== null && this.showOriginalContentOnHover) {
let opacity = parseFloat(getPreference(Gallery.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) {
return;
}
switch (event.key) {
case Gallery.directions.a:
case Gallery.directions.d:
case Gallery.directions.left:
case Gallery.directions.right:
this.traverseGallery(event.key, event.repeat);
break;
case "X":
case "x":
this.unFavoriteSelectedContent();
break;
default:
break;
}
}, {
passive: true
});
window.addEventListener("keydown", async(event) => {
if (!this.inGallery) {
return;
}
switch (event.key) {
case "F":
case "f":
await this.addFavoriteInGallery(event);
break;
case "M":
case "m":
if (isVideo(this.getSelectedThumb())) {
this.getActiveVideoPlayer().muted = !this.getActiveVideoPlayer().muted;
}
break;
case "B":
case "b":
this.toggleBackgroundOpacity();
break;
case "Escape":
this.exitGallery();
this.toggleAllVisibility(false);
break;
default:
break;
}
}, {
passive: true
});
}
addFavoritesLoaderEventListeners() {
if (onSearchPage()) {
return;
}
window.addEventListener("favoritesFetched", (event) => {
this.initializeThumbsForHovering.bind(this)(event.detail);
this.enumerateVisibleThumbs();
});
window.addEventListener("newFavoritesFetchedOnReload", (event) => {
if (event.detail.empty) {
return;
}
this.initializeThumbsForHovering.bind(this)(event.detail.thumbs);
this.enumerateVisibleThumbs();
/**
* @type {HTMLElement[]}
*/
const thumbs = event.detail.thumbs.reverse();
if (thumbs.length > 0) {
const thumb = thumbs[0];
this.upscaleAnimatedThumbsAround(thumb);
this.renderImages(thumbs
.filter(t => isImage(t))
.slice(0, 20));
}
}, {
once: true
});
window.addEventListener("startedFetchingFavorites", () => {
this.favoritesWereFetched = true;
setTimeout(() => {
const thumb = document.querySelector(".thumb-node");
this.renderImagesInTheBackground();
if (thumb !== null && !this.finishedLoading) {
this.upscaleAnimatedThumbsAround(thumb);
}
}, 650);
}, {
once: true
});
window.addEventListener("favoritesLoaded", (event) => {
Gallery.renderOnPageChangeCooldown.waitTime = 1000;
this.finishedLoading = true;
this.initializeThumbsForHovering.bind(this)();
this.enumerateVisibleThumbs();
this.findImageExtensionsInTheBackground(event.detail);
if (!this.favoritesWereFetched) {
this.renderImagesInTheBackground();
}
}, {
once: true
});
window.addEventListener("changedPage", () => {
this.clearMainCanvas();
this.clearVideoSources();
this.toggleOriginalContentVisibility(false);
this.deleteAllRenders();
if (Gallery.settings.debugEnabled) {
Array.from(getAllThumbs()).forEach((thumb) => {
thumb.classList.remove("loaded");
thumb.classList.remove("debug-selected");
});
}
this.initializeThumbsForHovering.bind(this)();
this.enumerateVisibleThumbs();
if (Gallery.renderOnPageChangeCooldown.ready) {
this.renderImagesInTheBackground();
}
});
window.addEventListener("shuffle", () => {
this.enumerateVisibleThumbs();
this.deleteAllRenders();
this.renderImagesInTheBackground();
});
window.addEventListener("favoriteMetadataFetched", (event) => {
this.assignImageExtension(event.detail.id, event.detail.extension);
});
}
addWebWorkerMessageHandlers() {
this.imageRenderer.onmessage = (message) => {
message = message.data;
switch (message.action) {
case "renderCompleted":
this.onRenderCompleted(message);
break;
case "renderDeleted":
this.onRenderDeleted(message);
break;
case "extensionFound":
this.assignImageExtension(message.id, message.extension);
break;
default:
break;
}
};
}
addMobileEventListeners() {
if (!onMobileDevice()) {
return;
}
window.addEventListener("blur", () => {
this.deleteAllRenders();
});
document.addEventListener("touchstart", (event) => {
if (!this.inGallery) {
return;
}
event.preventDefault();
Gallery.swipeControls.set(event, true);
}, {
passive: false
});
document.addEventListener("touchend", (event) => {
if (!this.inGallery) {
return;
}
event.preventDefault();
Gallery.swipeControls.set(event, false);
if (Gallery.swipeControls.up) {
this.exitGallery();
this.toggleAllVisibility(false);
} else if (Gallery.swipeControls.left) {
this.traverseGallery(Gallery.directions.right, false);
} else if (Gallery.swipeControls.right) {
this.traverseGallery(Gallery.directions.left, false);
} else {
this.exitGallery();
this.toggleAllVisibility;
}
}, {
passive: false
});
window.addEventListener("orientationchange", () => {
if (this.imageRenderer !== null && this.imageRenderer !== undefined) {
this.setMainCanvasOrientation();
}
}, {
passive: true
});
}
setMainCanvasOrientation() {
if (!onMobileDevice()) {
return;
}
const usingLandscapeOrientation = window.screen.orientation.angle === 90;
this.imageRenderer.postMessage({
action: "changeCanvasOrientation",
usingLandscapeOrientation
});
if (!this.inGallery) {
return;
}
const thumb = this.getSelectedThumb();
if (thumb === undefined || thumb === null) {
return;
}
this.imageRenderer.postMessage(this.getRenderRequest(thumb));
}
addMemoryManagementEventListeners() {
// if (Gallery.settings.developerMode && onFavoritesPage()) {
if (onFavoritesPage()) {
return;
}
window.onblur = () => {
this.leftPage = true;
this.deleteAllRenders();
this.clearInactiveVideoSources();
};
window.onfocus = () => {
if (this.leftPage) {
this.renderImagesInTheBackground();
this.leftPage = false;
}
};
}
loadDiscoveredImageExtensions() {
this.imageExtensions = JSON.parse(localStorage.getItem(Gallery.localStorageKeys.imageExtensions)) || {};
}
async prepareSearchPage() {
if (!onSearchPage()) {
return;
}
const imageList = document.getElementsByClassName("image-list")[0];
const thumbs = Array.from(imageList.querySelectorAll(".thumb"));
const scripts = Array.from(imageList.querySelectorAll("script"));
for (const thumb of thumbs) {
removeTitleFromImage(getImageFromThumb(thumb));
assignContentType(thumb);
thumb.id = thumb.id.substring(1);
}
for (const script of scripts) {
script.remove();
}
await this.findImageExtensionsOnSearchPage();
this.renderImagesInTheBackground();
}
injectHTML() {
this.injectStyleHTML();
this.injectDebugHTML();
this.injectOptionsHTML();
this.injectOriginalContentContainerHTML();
}
injectStyleHTML() {
injectStyleHTML(galleryHTML);
}
injectDebugHTML() {
if (Gallery.settings.debugEnabled) {
injectStyleHTML(galleryDebugHTML, "gallery-debug");
}
}
injectOptionsHTML() {
this.injectShowOnHoverOption();
// this.injectAutoplayOption();
}
injectShowOnHoverOption() {
let optionId = "show-content-on-hover";
let optionText = "Fullscreen on Hover";
let optionTitle = "View full resolution images or play videos and GIFs when hovering over a thumbnail";
let optionIsChecked = this.showOriginalContentOnHover;
let onOptionChanged = (event) => {
setPreference(Gallery.preferences.showOnHover, event.target.checked);
this.toggleAllVisibility(event.target.checked);
};
if (onMobileDevice()) {
optionId = "open-post-in-new-page-on-mobile";
optionText = "Enlarge on Click";
optionTitle = "View full resolution images/play videos when a thumbnail is clicked";
optionIsChecked = this.enlargeOnClickOnMobile;
onOptionChanged = (event) => {
setPreference(Gallery.preferences.enlargeOnClick, event.target.checked);
this.enlargeOnClickOnMobile = event.target.checked;
};
}
addOptionToFavoritesPage(
optionId,
optionText,
optionTitle,
optionIsChecked,
onOptionChanged,
true
);
}
injectAutoplayOption() {
addOptionToFavoritesPage(
"autoplay",
"Autoplay",
"Enable autoplay in gallery.",
this.autoplayEnabled,
(event) => {
this.toggleAutoplay(event.target.checked);
},
true
);
}
injectOriginalContentContainerHTML() {
const originalContentContainerHTML = `
<div id="original-content-container">
<div id="original-video-container">
<video id="video-player-0" width="100%" height="100%" autoplay muted loop controlsList="nofullscreen" active></video>
</div>
<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.lowResolutionCanvas, originalContentContainer.firstChild);
originalContentContainer.insertBefore(this.mainCanvas, originalContentContainer.firstChild);
this.background = document.getElementById("original-content-background");
this.videoContainer = document.getElementById("original-video-container");
this.addAdditionalVideoPlayers();
this.videoPlayers = Array.from(this.videoContainer.querySelectorAll("video"));
this.addVideoPlayerEventListeners();
this.loadVideoVolume();
this.toggleAutoplay(this.autoplayEnabled);
this.gifContainer = document.getElementById("original-gif-container");
this.mainCanvas.id = "main-canvas";
this.lowResolutionCanvas.id = "low-resolution-canvas";
this.lowResolutionCanvas.width = this.mainCanvas.width;
this.lowResolutionCanvas.height = this.mainCanvas.height;
this.toggleOriginalContentVisibility(false);
}
addAdditionalVideoPlayers() {
const videoPlayerHTML = "<video width=\"100%\" height=\"100%\" autoplay muted loop controlsList=\"nofullscreen\"></video>";
for (let i = 0; i < Gallery.settings.additionalVideoPlayerCount; i += 1) {
this.videoContainer.insertAdjacentHTML("beforeend", videoPlayerHTML);
}
}
addVideoPlayerEventListeners() {
for (const video of this.videoPlayers) {
video.addEventListener("mousemove", () => {
if (!video.hasAttribute("controls")) {
video.setAttribute("controls", "");
}
}, {
passive: true
});
video.addEventListener("click", () => {
if (video.paused) {
video.play().catch(() => { });
} else {
video.pause();
}
}, {
passive: true
});
video.addEventListener("volumechange", (event) => {
if (!event.target.hasAttribute("active")) {
return;
}
setPreference(Gallery.preferences.videoVolume, video.volume);
setPreference(Gallery.preferences.videoMuted, video.muted);
for (const v of this.getInactiveVideoPlayers()) {
v.volume = video.volume;
v.muted = video.muted;
}
}, {
passive: true
});
video.addEventListener("ended", () => {
this.doAutoplay();
}, {
passive: true
});
}
}
loadVideoVolume() {
const video = this.getActiveVideoPlayer();
video.volume = parseFloat(getPreference(Gallery.preferences.videoVolume, 1));
video.muted = getPreference(Gallery.preferences.videoMuted, true);
}
/**
* @param {Number} opacity
*/
updateBackgroundOpacity(opacity) {
this.background.style.opacity = opacity;
setPreference(Gallery.preferences.backgroundOpacity, opacity);
}
/**
* @param {HTMLElement[]} thumbs
*/
initializeThumbsForHovering(thumbs) {
const thumbElements = thumbs === undefined ? getAllThumbs() : thumbs;
for (const thumbElement of thumbElements) {
this.addEventListenersToThumb(thumbElement);
}
}
renderImagesInTheBackground() {
if (onMobileDevice() && !this.enlargeOnClickOnMobile) {
return;
}
const animatedThumbsToUpscale = Array.from(getAllVisibleThumbs())
.slice(0, Gallery.settings.animatedThumbsToUpscaleDiscrete)
.filter(thumb => !isImage(thumb));
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
const imageThumbsToRender = this.getVisibleUnrenderedImageThumbs()
.slice(0, Gallery.settings.maxImagesToRenderInBackground);
this.renderImages(imageThumbsToRender, "background");
}
/**
* @param {HTMLElement[]} imagesToRender
* @param {String} requestType
*/
renderImages(imagesToRender, requestType) {
const renderRequests = imagesToRender.map(image => this.getRenderRequest(image));
const canvases = onSearchPage() ? [] : renderRequests
.filter(request => request.canvas !== undefined)
.map(request => request.canvas);
this.imageRenderer.postMessage({
action: "renderMultiple",
id: this.currentBatchRenderRequestId,
renderRequests,
requestType
}, canvases);
this.currentBatchRenderRequestId += 1;
if (this.currentBatchRenderRequestId >= 1000) {
this.currentBatchRenderRequestId = 0;
}
}
/**
* @param {Object} message
*/
onRenderCompleted(message) {
this.completedRenders.add(message.id);
const thumb = document.getElementById(message.id);
if (thumb !== null) {
if (Gallery.settings.debugEnabled) {
thumb.classList.add("loaded");
}
if (message.extension === "gif") {
getImageFromThumb(thumb).setAttribute("gif", true);
return;
}
}
this.assignImageExtension(message.id, message.extension);
}
onRenderDeleted(message) {
const thumb = document.getElementById(message.id);
if (thumb !== null) {
if (Gallery.settings.debugEnabled) {
thumb.classList.remove("loaded");
}
}
this.startedRenders.delete(message.id);
this.completedRenders.delete(message.id);
}
deleteAllRenders() {
this.startedRenders.clear();
this.completedRenders.clear();
this.deleteAllTransferredCanvases();
this.imageRenderer.postMessage({
action: "deleteAllRenders"
});
if (Gallery.settings.debugEnabled) {
this.visibleThumbs.forEach((thumb) => {
thumb.classList.remove("loaded");
});
}
}
deleteAllTransferredCanvases() {
if (onSearchPage()) {
return;
}
for (const id of this.transferredCanvases.keys()) {
this.transferredCanvases.get(id).remove();
this.transferredCanvases.delete(id);
}
this.transferredCanvases.clear();
setTimeout(() => {
}, 1000);
}
/**
* @returns {HTMLElement[]}
*/
getVisibleUnrenderedImageThumbs() {
let thumbs = Array.from(getAllVisibleThumbs()).filter((thumb) => {
return isImage(thumb) && !this.renderHasStarted(thumb);
});
if (onSearchPage()) {
thumbs = thumbs.filter(thumb => !thumb.classList.contains("blacklisted-image"));
}
return thumbs;
}
/**
* @param {HTMLElement} thumb
* @returns {HTMLCanvasElement}
*/
getCanvasFromThumb(thumb) {
let canvas = thumb.querySelector("canvas");
if (canvas === null) {
canvas = document.createElement("canvas");
thumb.children[0].appendChild(canvas);
}
return canvas;
}
/**
* @param {HTMLElement} thumb
* @returns {HTMLCanvasElement}
*/
getOffscreenCanvasFromThumb(thumb) {
const canvas = this.getCanvasFromThumb(thumb);
this.transferredCanvases.set(thumb.id, canvas);
return canvas.transferControlToOffscreen();
}
hideCaptionsWhenShowingOriginalContent() {
for (const caption of document.getElementsByClassName("caption")) {
if (this.showOriginalContentOnHover) {
caption.classList.add("hide");
} else {
caption.classList.remove("hide");
}
}
}
findImageExtensionsOnSearchPage() {
const searchPageAPIURL = this.getSearchPageAPIURL();
return fetch(searchPageAPIURL)
.then((response) => {
if (response.ok) {
return response.text();
}
return null;
}).then((html) => {
if (html === null) {
console.error(`Failed to fetch: ${searchPageAPIURL}`);
}
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 id = post.getAttribute("id");
const extension = getExtensionFromImageURL(originalImageURL);
this.assignImageExtension(id, extension);
}
});
}
/**
* @param {HTMLElement[]} thumbs
*/
async findImageExtensionsInTheBackground(thumbs) {
await sleep(1000);
const idsWithUnknownExtensions = this.getIdsWithUnknownExtensions(thumbs);
while (idsWithUnknownExtensions.length > 0) {
await sleep(3000);
while (idsWithUnknownExtensions.length > 0 && this.finishedLoading) {
const id = idsWithUnknownExtensions.pop();
if (id !== undefined && id !== null && !this.extensionIsKnown(id)) {
this.imageRenderer.postMessage({
action: "findExtension",
id
});
await sleep(10);
}
}
}
Gallery.settings.extensionsFoundBeforeSavingCount = 0;
}
/**
* @param {String} id
* @param {String} extension
*/
assignImageExtension(id, extension) {
if (this.imageExtensions[parseInt(id)] !== undefined) {
return;
}
this.setImageExtension(id, extension);
this.recentlyDiscoveredImageExtensionCount += 1;
if (this.recentlyDiscoveredImageExtensionCount >= Gallery.settings.extensionsFoundBeforeSavingCount) {
this.recentlyDiscoveredImageExtensionCount = 0;
if (!onSearchPage()) {
this.storeAllImageExtensions();
}
}
}
storeAllImageExtensions() {
localStorage.setItem(Gallery.localStorageKeys.imageExtensions, JSON.stringify(this.imageExtensions));
}
/**
* @param {String | Number} id
* @returns {String}
*/
getImageExtension(id) {
return Gallery.extensionDecodings[this.imageExtensions[parseInt(id)]];
}
/**
* @param {String | Number} id
* @param {String} extension
*/
setImageExtension(id, extension) {
this.imageExtensions[parseInt(id)] = Gallery.extensionEncodings[extension];
}
/**
* @param {String} id
* @returns {Boolean}
*/
extensionIsKnown(id) {
return this.getImageExtension(id) !== undefined;
}
/**
* @returns {String}
*/
getSearchPageAPIURL() {
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);
}
}
/**
* @param {HTMLElement} thumb
* @param {Number} index
*/
enumerateThumb(thumb, index) {
thumb.setAttribute(Gallery.htmlAttributes.thumbIndex, index);
}
/**
* @param {HTMLElement} thumb
*/
addEventListenersToThumb(thumb) {
if (onMobileDevice()) {
return;
}
const image = getImageFromThumb(thumb);
image.onmouseover = (event) => {
if (this.inGallery || this.recentlyExitedGallery || enteredOverCaptionTag(event)) {
return;
}
this.thumbUnderCursor = thumb;
this.lastEnteredThumb = thumb;
this.showOriginalContent(thumb);
};
image.onmouseout = (event) => {
this.thumbUnderCursor = null;
if (this.inGallery || enteredOverCaptionTag(event)) {
return;
}
this.stopAllVideos();
this.hideOriginalContent();
};
}
/**
*
* @param {HTMLElement} thumb
*/
openPostInNewPage(thumb) {
thumb = thumb === undefined ? this.getSelectedThumb() : thumb;
const firstChild = thumb.children[0];
if (firstChild.hasAttribute("href")) {
window.open(firstChild.getAttribute("href"), "_blank");
} else {
firstChild.click();
}
}
unFavoriteSelectedContent() {
if (!userIsOnTheirOwnFavoritesPage()) {
return;
}
const selectedThumb = this.getSelectedThumb();
if (selectedThumb === null) {
return;
}
const removeFavoriteButton = getRemoveFavoriteButtonFromThumb(selectedThumb);
if (removeFavoriteButton === null) {
return;
}
const showRemoveFavoriteButtons = document.getElementById("show-remove-favorite-buttons");
if (showRemoveFavoriteButtons === null) {
return;
}
if (!Gallery.changeFavoriteCooldown.ready) {
return;
}
if (!showRemoveFavoriteButtons.checked) {
showFullscreenIcon(ICONS.warning, 1000);
setTimeout(() => {
alert("The \"Remove Buttons\" option must be checked to use this hotkey");
}, 20);
return;
}
showFullscreenIcon(ICONS.heartMinus);
removeFavoriteButton.click();
}
enterGallery() {
const selectedThumb = this.getSelectedThumb();
this.lastSelectedThumbIndexBeforeEnteringGallery = this.currentlySelectedThumbIndex;
this.background.style.pointerEvents = "auto";
if (isVideo(selectedThumb)) {
this.toggleCursorVisibility(true);
this.toggleVideoControls(true);
}
this.inGallery = true;
dispatchEvent(new CustomEvent("showOriginalContent", {
detail: true
}));
this.startAutoplay(selectedThumb);
}
exitGallery() {
if (Gallery.settings.debugEnabled) {
getAllVisibleThumbs().forEach(thumb => thumb.classList.remove("debug-selected"));
}
this.toggleVideoControls(false);
this.background.style.pointerEvents = "none";
const thumbIndex = this.getIndexOfThumbUnderCursor();
if (thumbIndex !== this.lastSelectedThumbIndexBeforeEnteringGallery) {
this.hideOriginalContent();
if (thumbIndex !== null && this.showOriginalContentOnHover) {
this.showOriginalContent(this.visibleThumbs[thumbIndex]);
}
}
this.recentlyExitedGallery = true;
setTimeout(() => {
this.recentlyExitedGallery = false;
}, 300);
this.inGallery = false;
this.stopAutoplay();
}
/**
* @param {String} direction
* @param {Boolean} keyIsHeldDown
*/
traverseGallery(direction, keyIsHeldDown) {
if (Gallery.settings.debugEnabled) {
this.getSelectedThumb().classList.remove("debug-selected");
}
if (keyIsHeldDown && !Gallery.traversalCooldown.ready) {
return;
}
this.setNextSelectedThumbIndex(direction);
const selectedThumb = this.getSelectedThumb();
// if (this.autoplayEnabled) {
// if (isVideo(selectedThumb)) {
// Gallery.autoplayCooldown.stop();
// } else {
// Gallery.autoplayCooldown.restart();
// }
// }
this.clearOriginalContentSources();
this.stopAllVideos();
if (Gallery.settings.debugEnabled) {
selectedThumb.classList.add("debug-selected");
}
this.upscaleAnimatedThumbsAround(selectedThumb);
this.renderImagesAround(selectedThumb);
this.preloadInactiveVideoPlayers(selectedThumb);
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.toggleOriginalVideoContainer(false);
this.showOriginalGIF(selectedThumb);
} else {
this.toggleCursorVisibility(false);
this.toggleVideoControls(false);
this.toggleOriginalVideoContainer(false);
this.showOriginalImage(selectedThumb);
}
}
/**
* @param {String} direction
*/
setNextSelectedThumbIndex(direction) {
if (direction === Gallery.directions.left || direction === Gallery.directions.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.showOriginalContentOnHover);
if (this.thumbUnderCursor !== null) {
this.toggleBackgroundVisibility();
this.toggleScrollbarVisibility();
}
dispatchEvent(new CustomEvent("showOriginalContent", {
detail: this.showOriginalContentOnHover
}));
setPreference(Gallery.preferences.showOnHover, this.showOriginalContentOnHover);
const showOnHoverCheckbox = document.getElementById("show-content-on-hover-checkbox");
if (showOnHoverCheckbox !== null) {
showOnHoverCheckbox.checked = this.showOriginalContentOnHover;
}
}
hideOriginalContent() {
this.toggleBackgroundVisibility(false);
this.toggleScrollbarVisibility(true);
this.toggleCursorVisibility(true);
this.clearOriginalContentSources();
this.stopAllVideos();
this.clearMainCanvas();
this.toggleOriginalVideoContainer(false);
this.toggleOriginalGIF(false);
}
clearOriginalContentSources() {
this.mainCanvas.style.visibility = "hidden";
this.lowResolutionCanvas.style.visibility = "hidden";
this.gifContainer.src = "";
}
/**
* @returns {Boolean}
*/
currentlyHoveringOverVideoThumb() {
if (this.thumbUnderCursor === null) {
return false;
}
return isVideo(this.thumbUnderCursor);
}
/**
* @param {HTMLElement} thumb
*/
showOriginalContent(thumb) {
this.currentlySelectedThumbIndex = parseInt(thumb.getAttribute(Gallery.htmlAttributes.thumbIndex));
this.upscaleAnimatedThumbsAroundDiscrete(thumb);
if (!this.inGallery && Gallery.settings.renderAroundAggressively) {
this.renderImagesAround(thumb);
}
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.toggleMainCanvas(false);
this.videoContainer.style.display = "block";
this.playOriginalVideo(thumb);
if (!this.inGallery) {
this.toggleVideoControls(false);
}
}
/**
* @param {HTMLElement} initialThumb
*/
preloadInactiveVideoPlayers(initialThumb) {
if (!this.inGallery || Gallery.settings.additionalVideoPlayerCount < 1) {
return;
}
this.setActiveVideoPlayer(initialThumb);
const inactiveVideoPlayers = this.getInactiveVideoPlayers();
const videoThumbsAroundInitialThumb = this.getAdjacentVisibleThumbsLooped(initialThumb, inactiveVideoPlayers.length, (t) => {
return isVideo(t) && t.id !== initialThumb.id;
});
const loadedVideoSources = new Set(inactiveVideoPlayers
.map(video => video.src)
.filter(src => src !== ""));
const videoSourcesAroundInitialThumb = new Set(videoThumbsAroundInitialThumb.map(thumb => this.getVideoSource(thumb)));
const videoThumbsNotLoaded = videoThumbsAroundInitialThumb.filter(thumb => !loadedVideoSources.has(this.getVideoSource(thumb)));
const freeInactiveVideoPlayers = inactiveVideoPlayers.filter(video => !videoSourcesAroundInitialThumb.has(video.src));
for (let i = 0; i < freeInactiveVideoPlayers.length && i < videoThumbsNotLoaded.length; i += 1) {
this.setVideoSource(freeInactiveVideoPlayers[i], videoThumbsNotLoaded[i]);
}
this.stopAllVideos();
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
getVideoSource(thumb) {
return getOriginalImageURLFromThumb(thumb).replace("jpg", "mp4");
}
/**
* @param {HTMLVideoElement} video
* @param {HTMLElement} thumb
*/
setVideoSource(video, thumb) {
if (this.videoPlayerHasSource(video, thumb)) {
return;
}
this.createVideoClip(video, thumb);
video.src = this.getVideoSource(thumb);
}
/**
* @param {HTMLVideoElement} video
* @param {HTMLElement} thumb
*/
createVideoClip(video, thumb) {
const clip = this.videoClips.get(thumb.id);
if (clip === undefined) {
video.ontimeupdate = null;
return;
}
video.ontimeupdate = () => {
if (video.currentTime < clip.start || video.currentTime > clip.end) {
video.removeAttribute("controls");
video.currentTime = clip.start;
}
};
}
clearVideoSources() {
for (const video of this.videoPlayers) {
video.src = "";
}
}
clearInactiveVideoSources() {
const videoPlayers = this.inGallery ? this.getInactiveVideoPlayers() : this.videoPlayers;
for (const video of videoPlayers) {
video.src = "";
}
}
/**
* @param {HTMLVideoElement} video
* @returns {String | null}
*/
getSourceIdFromVideo(video) {
const regex = /\.mp4\?(\d+)/;
const match = regex.exec(video.src);
if (match === null) {
return null;
}
return match[1];
}
/**
* @param {HTMLElement} thumb
*/
playOriginalVideo(thumb) {
// this.setActiveVideoPlayer(thumb);
// this.preloadInactiveVideoPlayers(thumb);
this.stopAllVideos();
const video = this.getActiveVideoPlayer();
this.setVideoSource(video, thumb);
video.style.display = "block";
video.play().catch(() => { });
this.toggleVideoControls(true);
}
stopAllVideos() {
for (const video of this.videoPlayers) {
this.stopVideo(video);
}
}
stopAllInactiveVideos() {
for (const video of this.getInactiveVideoPlayers()) {
this.stopVideo(video);
}
}
/**
* @param {HTMLVideoElement} video
*/
stopVideo(video) {
video.style.display = "none";
video.pause();
video.removeAttribute("controls");
// video.currentTime = 0;
}
/**
* @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.isCompletelyRendered(thumb)) {
this.clearLowResolutionCanvas();
this.drawMainCanvas(thumb);
} else if (this.renderHasStarted(thumb)) {
this.drawLowResolutionCanvas(thumb);
this.clearMainCanvas();
this.drawMainCanvas(thumb);
} else {
this.renderOriginalImage(thumb);
if (!this.inGallery && !Gallery.settings.renderAroundAggressively) {
this.renderImagesAround(thumb);
}
}
this.toggleOriginalContentVisibility(this.showOriginalContentOnHover);
}
/**
* @param {HTMLElement} initialThumb
*/
renderImagesAround(initialThumb) {
if (onSearchPage()) {
return;
}
if (onMobileDevice() && !this.enlargeOnClickOnMobile) {
return;
}
const amountToRender = Gallery.settings.maxImagesToRenderAround;
const imageThumbsToRender = this.getAdjacentVisibleThumbsLooped(initialThumb, amountToRender, (thumb) => {
return isImage(thumb);
});
if (!this.renderHasStarted(initialThumb)) {
imageThumbsToRender.unshift(initialThumb);
}
this.renderImages(imageThumbsToRender, "adjacent");
}
/**
* @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);
}
traverseForward = this.getTraversalDirection(previousThumb, traverseForward, nextThumb);
currentThumb = traverseForward ? nextThumb : previousThumb;
if (currentThumb !== null) {
if (this.isVisible(currentThumb) && additionalQualifier(currentThumb)) {
adjacentVisibleThumbs.push(currentThumb);
}
}
}
return adjacentVisibleThumbs;
}
/**
* @param {HTMLElement} initialThumb
* @param {Number} limit
* @param {Function} additionalQualifier
* @returns {HTMLElement[]}
*/
getAdjacentVisibleThumbsLooped(initialThumb, limit, additionalQualifier) {
const adjacentVisibleThumbs = [];
const discoveredIds = new Set();
let currentThumb = initialThumb;
let previousThumb = initialThumb;
let nextThumb = initialThumb;
let traverseForward = true;
while (currentThumb !== null && adjacentVisibleThumbs.length < limit) {
if (traverseForward) {
nextThumb = this.getAdjacentVisibleThumbLooped(nextThumb, true);
} else {
previousThumb = this.getAdjacentVisibleThumbLooped(previousThumb, false);
}
traverseForward = !traverseForward;
currentThumb = traverseForward ? nextThumb : previousThumb;
if (currentThumb === undefined || discoveredIds.has(currentThumb.id)) {
break;
}
discoveredIds.add(currentThumb.id);
if (this.isVisible(currentThumb) && additionalQualifier(currentThumb)) {
adjacentVisibleThumbs.push(currentThumb);
}
}
return adjacentVisibleThumbs;
}
/**
* @param {HTMLElement} previousThumb
* @param {HTMLElement} traverseForward
* @param {HTMLElement} nextThumb
* @returns {Boolean}
*/
getTraversalDirection(previousThumb, traverseForward, nextThumb) {
if (previousThumb === null) {
traverseForward = true;
} else if (nextThumb === null) {
traverseForward = false;
}
return !traverseForward;
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} forward
* @returns {HTMLElement}
*/
getAdjacentVisibleThumb(thumb, forward) {
let adjacentThumb = this.getAdjacentThumb(thumb, forward);
while (adjacentThumb !== null && !this.isVisible(adjacentThumb)) {
adjacentThumb = this.getAdjacentThumb(adjacentThumb, forward);
}
return adjacentThumb;
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} forward
* @returns {HTMLElement}
*/
getAdjacentVisibleThumbLooped(thumb, forward) {
let adjacentThumb = this.getAdjacentThumb(thumb, forward);
while (adjacentThumb !== null && !this.isVisible(adjacentThumb)) {
adjacentThumb = this.getAdjacentThumb(adjacentThumb, forward);
}
if (adjacentThumb === null) {
adjacentThumb = forward ? this.visibleThumbs[0] : this.visibleThumbs[this.visibleThumbs.length - 1];
}
return adjacentThumb;
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} forward
* @returns {HTMLElement}
*/
getAdjacentThumb(thumb, forward) {
return forward ? thumb.nextElementSibling : thumb.previousElementSibling;
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
isVisible(thumb) {
return thumb.style.display !== "none";
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
renderHasStarted(thumb) {
return this.startedRenders.has(thumb.id);
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
isCompletelyRendered(thumb) {
return this.completedRenders.has(thumb.id);
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
canvasIsTransferrable(thumb) {
return !onMobileDevice() && !onSearchPage() && !this.transferredCanvases.has(thumb.id);
}
/**
* @param {HTMLElement} thumb
* @returns {{
* action: String,
* imageURL: String,
* id: String,
* extension: String,
* fetchDelay: Number,
* thumbURL: String,
* pixelCount: Number,
* canvas: OffscreenCanvas
* resolutionFraction: Number
* windowDimensions: {width: Number, height:Number}
* }}
*/
getRenderRequest(thumb) {
const request = {
action: "render",
imageURL: getOriginalImageURLFromThumb(thumb),
id: thumb.id,
extension: this.getImageExtension(thumb.id),
fetchDelay: this.getBaseImageFetchDelay(thumb.id),
thumbURL: getImageFromThumb(thumb).src.replace("us.rule", "rule"),
pixelCount: this.getPixelCount(thumb),
resolutionFraction: Gallery.settings.upscaledThumbResolutionFraction
};
this.startedRenders.add(thumb.id);
if (this.canvasIsTransferrable(thumb)) {
request.canvas = this.getOffscreenCanvasFromThumb(thumb);
}
if (onMobileDevice()) {
request.windowDimensions = {
width: window.innerWidth,
height: window.innerHeight
};
}
return request;
}
/**
* @param {HTMLElement} thumb
* @returns {Number}
*/
getPixelCount(thumb) {
if (onSearchPage()) {
return 0;
}
const defaultPixelCount = 2073600;
const pixelCount = ThumbNode.getPixelCount(thumb.id);
return pixelCount === 0 ? defaultPixelCount : pixelCount;
}
/**
* @param {HTMLElement} thumb
*/
renderOriginalImage(thumb) {
if (this.canvasIsTransferrable(thumb)) {
const request = this.getRenderRequest(thumb);
this.imageRenderer.postMessage(request, [request.canvas]);
} else if (!onSearchPage()) {
this.imageRenderer.postMessage(this.getRenderRequest(thumb));
}
}
/**
* @param {HTMLElement} thumb
*/
drawMainCanvas(thumb) {
this.imageRenderer.postMessage({
action: "drawMainCanvas",
id: thumb.id
});
}
clearMainCanvas() {
this.imageRenderer.postMessage({
action: "clearMainCanvas"
});
}
/**
* @param {Boolean} value
*/
toggleOriginalContentVisibility(value) {
this.toggleMainCanvas(value);
this.toggleOriginalGIF(value);
if (!value) {
this.toggleOriginalVideoContainer(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
*/
toggleBackgroundOpacity(value) {
if (value !== undefined) {
if (value) {
this.updateBackgroundOpacity(1);
} else {
this.updateBackgroundOpacity(0);
}
return;
}
const opacity = parseFloat(this.background.style.opacity);
if (opacity < 1) {
this.updateBackgroundOpacity(1);
} else {
this.updateBackgroundOpacity(0);
}
}
/**
* @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) {
const video = this.getActiveVideoPlayer();
if (value === undefined) {
video.style.pointerEvents = video.style.pointerEvents === "auto" ? "none" : "auto";
if (video.hasAttribute("controls")) {
video.removeAttribute("controls");
}
return;
}
video.style.pointerEvents = value ? "auto" : "none";
if (onMobileDevice()) {
video.controls = value ? "controls" : false;
} else if (!value) {
video.removeAttribute("controls");
}
}
/**
* @param {Boolean} value
*/
toggleMainCanvas(value) {
if (value === undefined) {
this.mainCanvas.style.visibility = this.mainCanvas.style.visibility === "visible" ? "hidden" : "visible";
this.lowResolutionCanvas.style.visibility = this.mainCanvas.style.visibility === "visible" ? "hidden" : "visible";
} else {
this.mainCanvas.style.visibility = value ? "visible" : "hidden";
this.lowResolutionCanvas.style.visibility = value ? "visible" : "hidden";
}
}
/**
* @param {Boolean} value
*/
toggleOriginalVideoContainer(value) {
if (value !== undefined) {
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 {HTMLElement} thumb
*/
setActiveVideoPlayer(thumb) {
for (const video of this.videoPlayers) {
video.removeAttribute("active");
}
for (const video of this.videoPlayers) {
if (this.videoPlayerHasSource(video, thumb)) {
video.setAttribute("active", "");
return;
}
}
this.videoPlayers[0].setAttribute("active", "");
}
/**
* @returns {HTMLVideoElement}
*/
getActiveVideoPlayer() {
return this.videoPlayers.find(video => video.hasAttribute("active")) || this.videoPlayers[0];
}
/**
* @param {HTMLVideoElement} video
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
videoPlayerHasSource(video, thumb) {
return video.src === this.getVideoSource(thumb);
}
/**
* @returns {HTMLVideoElement[]}
*/
getInactiveVideoPlayers() {
return this.videoPlayers.filter(video => !video.hasAttribute("active"));
}
/**
* @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";
}
}
/**
* @returns {Number}
*/
getIndexOfThumbUnderCursor() {
return this.thumbUnderCursor === null ? null : parseInt(this.thumbUnderCursor.getAttribute(Gallery.htmlAttributes.thumbIndex));
}
/**
* @returns {HTMLElement}
*/
getSelectedThumb() {
return this.visibleThumbs[this.currentlySelectedThumbIndex];
}
/**
* @param {HTMLElement[]} animatedThumbs
*/
upscaleAnimatedThumbs(animatedThumbs) {
if (onMobileDevice()) {
return;
}
const upscaleRequests = [];
for (const thumb of animatedThumbs) {
if (!this.canvasIsTransferrable(thumb)) {
continue;
}
let imageURL = getOriginalImageURL(getImageFromThumb(thumb).src);
if (isGif(thumb)) {
imageURL = imageURL.replace("jpg", "gif");
}
upscaleRequests.push({
id: thumb.id,
imageURL,
canvas: this.getOffscreenCanvasFromThumb(thumb),
resolutionFraction: Gallery.settings.upscaledAnimatedThumbResolutionFraction
});
}
this.imageRenderer.postMessage({
action: "upscaleAnimatedThumbs",
upscaleRequests
}, upscaleRequests.map(request => request.canvas));
}
/**
* @param {String} id
* @returns {Number}
*/
getBaseImageFetchDelay(id) {
if (this.extensionIsKnown(id)) {
return Gallery.settings.imageFetchDelayWhenExtensionKnown;
}
return Gallery.settings.imageFetchDelay;
}
/**
* @param {HTMLElement} thumb
*/
upscaleAnimatedThumbsAround(thumb) {
if (!onFavoritesPage() || onMobileDevice()) {
return;
}
const animatedThumbsToUpscale = this.getAdjacentVisibleThumbs(thumb, Gallery.settings.animatedThumbsToUpscaleRange, (t) => {
return !isImage(t) && !this.transferredCanvases.has(t.id);
});
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
}
/**
* @param {HTMLElement} thumb
*/
upscaleAnimatedThumbsAroundDiscrete(thumb) {
if (!onFavoritesPage() || onMobileDevice()) {
return;
}
const animatedThumbsToUpscale = this.getAdjacentVisibleThumbs(thumb, Gallery.settings.animatedThumbsToUpscaleDiscrete, (_) => {
return true;
}).filter(t => !isImage(t) && !this.transferredCanvases.has(t.id));
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
}
/**
* @param {HTMLElement[]} thumbs
* @returns {String[]}
*/
getIdsWithUnknownExtensions(thumbs) {
return thumbs
.filter(thumb => isImage(thumb) && !this.extensionIsKnown(thumb.id))
.map(thumb => thumb.id);
}
/**
* @param {String} id
*/
drawLowResolutionCanvas(thumb) {
if (onMobileDevice()) {
return;
}
const image = getImageFromThumb(thumb);
if (!imageIsLoaded(image)) {
return;
}
const ratio = Math.min(this.lowResolutionCanvas.width / image.naturalWidth, this.lowResolutionCanvas.height / image.naturalHeight);
const centerShiftX = (this.lowResolutionCanvas.width - (image.naturalWidth * ratio)) / 2;
const centerShiftY = (this.lowResolutionCanvas.height - (image.naturalHeight * ratio)) / 2;
this.clearLowResolutionCanvas();
this.lowResolutionContext.drawImage(
image, 0, 0, image.naturalWidth, image.naturalHeight,
centerShiftX, centerShiftY, image.naturalWidth * ratio, image.naturalHeight * ratio
);
}
clearLowResolutionCanvas() {
if (onMobileDevice()) {
return;
}
this.lowResolutionContext.clearRect(0, 0, this.lowResolutionCanvas.width, this.lowResolutionCanvas.height);
}
/**
* @param {Boolean} value
*/
toggleAutoplay(value) {
// setPreference(Gallery.preferences.autoplay, value);
// this.autoplayEnabled = value;
// if (value) {
// this.videoContainer.removeAttribute("loop");
// } else {
// this.videoContainer.setAttribute("loop", "");
// }
}
/**
* @param {HTMLElement} selectedThumb
*/
startAutoplay(selectedThumb) {
if (!this.autoplayEnabled) {
return;
}
Gallery.autoplayCooldown.onCooldownEnd = () => {
this.doAutoplay();
};
if (isImage(selectedThumb)) {
Gallery.autoplayCooldown.start();
}
}
stopAutoplay() {
Gallery.autoplayCooldown.onCooldownEnd = () => { };
Gallery.autoplayCooldown.stop();
}
doAutoplay() {
if (!this.autoplayEnabled || !this.inGallery) {
return;
}
this.traverseGallery(Gallery.directions.right, false);
}
loadVideoClips() {
}
/**
* @param {KeyboardEvent} event
*/
async addFavoriteInGallery(event) {
if (!this.inGallery || event.repeat || !Gallery.changeFavoriteCooldown.ready) {
return;
}
const selectedThumb = this.getSelectedThumb();
if (selectedThumb === undefined || selectedThumb === null) {
showFullscreenIcon(ICONS.error);
return;
}
const addedFavoriteStatus = await addFavorite(selectedThumb.id);
let svg = ICONS.error;
switch (addedFavoriteStatus) {
case ADDED_FAVORITE_STATUS.alreadyAdded:
svg = ICONS.heartCheck;
break;
case ADDED_FAVORITE_STATUS.success:
svg = ICONS.heartPlus;
dispatchEvent(new CustomEvent("favoriteAddedOrDeleted", {
detail: selectedThumb.id
}));
break;
default:
break;
}
showFullscreenIcon(svg);
}
}
const gallery = new Gallery();
// 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.05em;
}
#tooltip.visible {
visibility: visible;
opacity: 1;
}
</style>
<span id="tooltip" class="light-green-gradient"></span>
</div>`;
class Tooltip {
/**
* @type {Boolean}
*/
static get disabled() {
return onMobileDevice() || getPerformanceProfile() > 1 || onPostPage();
}
/**
* @type {HTMLDivElement}
*/
tooltip;
/**
* @type {String}
*/
defaultTransition;
/**
* @type {Boolean}
*/
visible;
/**
* @type {Object.<String,String>}
*/
searchTagColorCodes;
/**
* @type {HTMLTextAreaElement}
*/
searchBox;
/**
* @type {String}
*/
previousSearch;
/**
* @type {HTMLImageElement}
*/
currentImage;
constructor() {
if (Tooltip.disabled) {
return;
}
this.visible = getPreference("showTooltip", true);
document.body.insertAdjacentHTML("afterbegin", tooltipHTML);
this.tooltip = document.getElementById("tooltip");
this.defaultTransition = this.tooltip.style.transition;
this.searchTagColorCodes = {};
this.currentImage = null;
this.setTheme();
this.addEventListeners();
this.addFavoritesOptions();
this.assignColorsToMatchedTags();
}
addEventListeners() {
this.addAllPageEventListeners();
this.addSearchPageEventListeners();
this.addFavoritesPageEventListeners();
}
addAllPageEventListeners() {
document.addEventListener("keydown", (event) => {
if (event.key.toLowerCase() !== "t" || event.repeat || isTypeableInput(event.target)) {
return;
}
if (onFavoritesPage()) {
const showTooltipsCheckbox = document.getElementById("show-tooltips-checkbox");
if (showTooltipsCheckbox !== null) {
showTooltipsCheckbox.click();
if (this.currentImage !== null) {
if (this.visible) {
this.show(this.currentImage);
} else {
this.hide();
}
}
}
} else if (onSearchPage()) {
this.toggleVisibility();
if (this.currentImage !== null) {
this.hide();
}
}
}, {
passive: true
});
}
addSearchPageEventListeners() {
if (!onSearchPage()) {
return;
}
window.addEventListener("load", () => {
this.addEventListenersToThumbs.bind(this)();
}, {
once: true,
passive: true
});
}
addFavoritesPageEventListeners() {
if (!onFavoritesPage()) {
return;
}
window.addEventListener("favoritesFetched", (event) => {
this.addEventListenersToThumbs.bind(this)(event.detail);
});
window.addEventListener("favoritesLoaded", () => {
this.addEventListenersToThumbs.bind(this)();
}, {
once: true
});
window.addEventListener("changedPage", () => {
this.currentImage = null;
this.addEventListenersToThumbs.bind(this)();
});
window.addEventListener("newFavoritesFetchedOnReload", (event) => {
if (!event.detail.empty) {
this.addEventListenersToThumbs.bind(this)(event.detail.thumbs);
}
}, {
once: true
});
}
setTheme() {
if (usingDarkTheme()) {
this.tooltip.classList.remove("light-green-gradient");
this.tooltip.classList.add("dark-green-gradient");
}
}
assignColorsToMatchedTags() {
if (onSearchPage()) {
this.assignColorsToMatchedTagsOnSearchPage();
} 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);
image.onmouseenter = (event) => {
if (enteredOverCaptionTag(event)) {
return;
}
this.currentImage = image;
if (this.visible) {
this.show(image);
}
};
image.onmouseleave = (event) => {
if (!enteredOverCaptionTag(event)) {
this.currentImage = null;
this.hide();
}
};
}
}
/**
* @param {HTMLImageElement} image
*/
setPosition(image) {
const imageChangesSizeOnHover = document.getElementById("fancy-image-hovering") !== null;
let rect;
if (imageChangesSizeOnHover) {
const imageContainer = image.parentElement;
const sizeCalculationDiv = document.createElement("div");
sizeCalculationDiv.className = "size-calculation-div";
imageContainer.appendChild(sizeCalculationDiv);
rect = sizeCalculationDiv.getBoundingClientRect();
sizeCalculationDiv.remove();
} else {
rect = image.getBoundingClientRect();
}
const offset = 7;
let tooltipRect;
this.tooltip.style.top = `${rect.bottom + offset + window.scrollY}px`;
this.tooltip.style.left = `${rect.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 = `${rect.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 = `${rect.top + window.scrollY + (rect.height / 2) - offset}px`;
if (tooltipIsLeftOfCenter) {
this.tooltip.style.left = `${rect.right + offset}px`;
} else {
this.tooltip.style.left = `${rect.left - 750 - offset}px`;
}
}
/**
* @param {String} tags
*/
setText(tags) {
this.tooltip.innerHTML = this.formatHTML(tags);
}
/**
* @param {HTMLImageElement} image
*/
show(image) {
this.setText(this.getTagsFromImageWithIdRemoved(image));
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
* @returns {String}
*/
getTagsFromImageWithIdRemoved(image) {
const thumb = getThumbFromImage(image);
let tags = getTagsFromThumb(thumb);
if (this.searchTagColorCodes[thumb.id] === undefined) {
tags = removeExtraWhiteSpace(tags.replace(thumb.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.searchTagColorCodes = {};
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.searchTagColorCodes[tag] === undefined) {
this.searchTagColorCodes[tag] = color;
}
}
/**
* @param {String} tag
* @returns {String | null}
*/
getColorCode(tag) {
if (this.searchTagColorCodes[tag] !== undefined) {
return this.searchTagColorCodes[tag];
}
for (const searchTag of Object.keys(this.searchTagColorCodes)) {
if (tagsMatchWildcardSearchTag(searchTag, [tag])) {
return this.searchTagColorCodes[searchTag];
}
}
return undefined;
}
addFavoritesOptions() {
addOptionToFavoritesPage(
"show-tooltips",
" Tooltips",
"Show tags when hovering over a thumbnail and see which ones were matched by a search",
this.visible, (event) => {
this.toggleVisibility(event.target.checked);
},
true
);
}
/**
* @param {Boolean} value
*/
toggleVisibility(value) {
if (value === undefined) {
value = !this.visible;
}
setPreference("showTooltip", value);
this.visible = value;
}
/**
* @param {HTMLElement | null} thumb
*/
showOnLoadIfHoveringOverThumb(thumb) {
if (thumb !== null) {
this.show(getImageFromThumb(thumb));
}
}
assignColorsToMatchedTagsOnSearchPage() {
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 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>
<button title="Save result ids as search" id="save-results-button">Save Results</button>
</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">
<ul id="saved-search-list"></ul>
</div>
</div>
</div>
`;
class SavedSearches {
static preferences = {
textareaWidth: "savedSearchesTextAreaWidth",
textareaHeight: "savedSearchesTextAreaHeight",
savedSearches: "savedSearches",
visibility: "savedSearchVisibility",
tutorial: "savedSearchesTutorial"
};
static localStorageKeys = {
savedSearches: "savedSearches"
};
/**
* @type {Boolean}
*/
static get disabled() {
return !onFavoritesPage() || onMobileDevice();
}
/**
* @type {HTMLTextAreaElement}
*/
textarea;
/**
* @type {HTMLElement}
*/
savedSearchesList;
/**
* @type {HTMLButtonElement}
*/
stopEditingButton;
/**
* @type {HTMLButtonElement}
*/
saveButton;
/**
* @type {HTMLButtonElement}
*/
importButton;
/**
* @type {HTMLButtonElement}
*/
exportButton;
/**
* @type {HTMLButtonElement}
*/
saveSearchResultsButton;
constructor() {
if (SavedSearches.disabled) {
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, false);
const savedSearchesContainer = document.getElementById("right-favorites-panel");
savedSearchesContainer.insertAdjacentHTML("beforeend", savedSearchesHTML);
document.getElementById("right-favorites-panel").style.display = showSavedSearches ? "block" : "none";
const options = addOptionToFavoritesPage(
"show-saved-searches",
"Saved Searches",
"Toggle saved searches",
showSavedSearches,
(e) => {
savedSearchesContainer.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;
}
}, {
passive: true
});
this.exportButton.onclick = () => {
this.exportSavedSearches();
};
this.importButton.onclick = () => {
this.importSavedSearches();
};
this.saveSearchResultsButton.onclick = () => {
this.saveSearchResultsAsCustomSearch();
};
}
/**
* @param {String} newSavedSearch
* @param {Boolean} updateLocalStorage
*/
saveSearch(newSavedSearch, updateLocalStorage = true) {
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 = ICONS.edit;
removeButton.innerHTML = ICONS.delete;
moveToTopButton.innerHTML = 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.storeSavedSearches();
};
this.stopEditingButton.onclick = () => {
this.stopEditingSavedSearches(newListItem);
};
this.textarea.value = "";
if (updateLocalStorage) {
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], false);
}
}
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]);
}
}, {
once: true
});
}
/**
* @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();
}
}
saveSearchResultsAsCustomSearch() {
const searchResultIds = Array.from(ThumbNode.allThumbNodes.values())
.filter(thumbNode => thumbNode.matchedByMostRecentSearch)
.map(thumbNode => thumbNode.id);
if (searchResultIds.length === 0) {
return;
}
if (searchResultIds.length > 300) {
if (!confirm(`Are you sure you want to save ${searchResultIds.length} ids as one search?`)) {
return;
}
}
const customSearch = `( ${searchResultIds.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 .35s ease;
padding-top: 4px;
padding-left: 7px;
-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: #EFA1CF;
}
.metadata-tag {
color: #8FD9ED;
}
.caption-wrapper {
pointer-events: none;
position: absolute !important;
overflow: hidden;
top: -1px;
left: -1px;
width: 102%;
height: 102%;
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: "unknown",
3: "copyright",
4: "character",
5: "metadata"
};
static template = `
<ul id="caption-list">
<li id="caption-id" style="display: block;"><h6>ID</h6></li>
${Caption.getCategoryHeaderHTML()}
</ul>
`;
static findCategoriesOnPageChangeCooldown = new Cooldown(3000, true);
/**
* @type {Object.<String, Number>}
*/
static tagCategoryAssociations;
/**
* @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 encodeTagCategory(tagCategory) {
for (const [encoding, category] of Object.entries(Caption.tagCategoryEncodings)) {
if (category === tagCategory) {
return encoding;
}
}
return 0;
}
/**
* @type {Boolean}
*/
static get disabled() {
return !onFavoritesPage() || onMobileDevice() || getPerformanceProfile() > 1;
}
/**
* @type {Boolean}
*/
get hidden() {
return this.caption.classList.contains("hide") || this.caption.classList.contains("disabled") || this.caption.classList.contains("remove");
}
/**
* @type {HTMLDivElement}
*/
captionWrapper;
/**
* @type {HTMLDivElement}
*/
caption;
/**
* @type {HTMLElement}
*/
currentThumb;
/**
* @type {Set.<String>}
*/
problematicTags;
/**
* @type {String}
*/
currentThumbId;
/**
* @type {AbortController}
*/
abortController;
constructor() {
if (Caption.disabled) {
return;
}
this.initializeFields();
this.createHTMLElement();
this.injectHTML();
this.toggleVisibility(this.getVisibilityPreference());
this.addEventListeners();
}
initializeFields() {
Caption.tagCategoryAssociations = this.loadSavedTags();
Caption.findCategoriesOnPageChangeCooldown.onDebounceEnd = () => {
this.findTagCategoriesOnPageChange();
};
this.currentThumb = null;
this.problematicTags = new Set();
this.currentThumbId = null;
this.abortController = new AbortController();
}
createHTMLElement() {
this.captionWrapper = document.createElement("div");
this.captionWrapper.className = "caption-wrapper";
this.caption = document.createElement("div");
this.caption.className = "caption inactive";
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.toggleVisibility(event.target.checked);
},
true
);
}
/**
* @param {Boolean} value
*/
toggleVisibility(value) {
if (value === undefined) {
value = this.caption.classList.contains("disabled");
}
if (value) {
this.caption.classList.remove("disabled");
} else if (!this.caption.classList.contains("disabled")) {
this.caption.classList.add("disabled");
}
setPreference(Caption.preferences.visibility, value);
}
addEventListeners() {
this.addAllPageEventListeners();
this.addSearchPageEventListeners();
this.addFavoritesPageEventListeners();
}
addAllPageEventListeners() {
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");
});
window.addEventListener("showOriginalContent", (event) => {
const thumb = caption.parentElement;
if (event.detail) {
this.removeFromThumb(thumb);
this.caption.classList.add("hide");
} else {
this.caption.classList.remove("hide");
}
});
document.addEventListener("keydown", (event) => {
if (event.key.toLowerCase() !== "d" || event.repeat || isTypeableInput(event.target)) {
return;
}
if (onFavoritesPage()) {
const showCaptionsCheckbox = document.getElementById("show-captions-checkbox");
if (showCaptionsCheckbox !== null) {
showCaptionsCheckbox.click();
if (this.currentThumb !== null && !this.caption.classList.contains("remove")) {
if (showCaptionsCheckbox.checked) {
this.attachToThumbHelper(this.currentThumb);
} else {
this.removeFromThumbHelper(this.currentThumb);
}
}
}
} else if (onSearchPage()) {
// this.toggleVisibility();
}
}, {
passive: true
});
}
addSearchPageEventListeners() {
if (!onSearchPage()) {
return;
}
window.addEventListener("load", () => {
this.addEventListenersToThumbs.bind(this)();
}, {
once: true,
passive: true
});
}
addFavoritesPageEventListeners() {
window.addEventListener("favoritesLoaded", () => {
this.addEventListenersToThumbs.bind(this)();
Caption.findCategoriesOnPageChangeCooldown.waitTime = 1000;
}, {
once: true
});
window.addEventListener("favoritesFetched", () => {
this.addEventListenersToThumbs.bind(this)();
});
window.addEventListener("changedPage", () => {
this.addEventListenersToThumbs.bind(this)();
this.abortController.abort("ChangedPage");
this.abortController = new AbortController();
if (Caption.findCategoriesOnPageChangeCooldown.ready) {
this.findTagCategoriesOnPageChange();
}
});
window.addEventListener("originalFavoritesCleared", (event) => {
const thumbs = event.detail;
const tagNames = Array.from(thumbs)
.map(thumb => getImageFromThumb(thumb).title)
.join(" ")
.split(" ")
.filter(tagName => !isNumber(tagName) && Caption.tagCategoryAssociations[tagName] === undefined);
this.findTagCategories(tagNames, 10, () => {
this.saveTags();
});
}, {
once: true
});
window.addEventListener("newFavoritesFetchedOnReload", (event) => {
if (!event.detail.empty) {
this.addEventListenersToThumbs.bind(this)(event.detail.thumbs);
}
}, {
once: true
});
window.addEventListener("captionOverrideEnd", () => {
if (this.currentThumb !== null) {
this.attachToThumb(this.currentThumb);
}
});
}
/**
* @param {HTMLElement[]} thumbs
*/
async addEventListenersToThumbs(thumbs) {
await sleep(500);
thumbs = thumbs === undefined ? getAllThumbs() : thumbs;
for (const thumb of thumbs) {
const imageContainer = getImageFromThumb(thumb).parentElement;
imageContainer.onmouseenter = () => {
this.currentThumb = thumb;
this.attachToThumb(thumb);
};
imageContainer.onmouseleave = () => {
this.currentThumb = null;
this.removeFromThumb(thumb);
};
}
}
/**
* @param {HTMLElement} thumb
*/
attachToThumb(thumb) {
if (this.hidden || thumb === null) {
return;
}
this.attachToThumbHelper(thumb);
}
attachToThumbHelper(thumb) {
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 = this.caption.querySelector("#caption-id");
const captionIdTag = document.createElement("li");
captionIdTag.className = "caption-tag";
captionIdTag.textContent = thumb.id;
captionIdTag.onclick = (event) => {
event.stopPropagation();
this.tagOnClick(thumb.id, event);
};
captionIdTag.addEventListener("contextmenu", (event) => {
event.preventDefault();
this.tagOnClick(`-${thumb.id}`, event);
});
captionIdHeader.insertAdjacentElement("afterend", captionIdTag);
thumb.children[0].appendChild(this.captionWrapper);
this.populateTags(thumb);
}
/**
* @param {HTMLElement} thumb
*/
removeFromThumb(thumb) {
if (this.hidden) {
return;
}
this.removeFromThumbHelper(thumb);
}
/**
* @param {HTMLElement} thumb
*/
removeFromThumbHelper(thumb) {
if (thumb !== null && thumb !== undefined) {
this.animateRemoval(thumb);
}
this.animate(false);
this.caption.classList.add("inactive");
this.caption.classList.remove("transition-completed");
}
/**
* @param {HTMLElement} thumb
*/
animateRemoval(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 columnInput = document.getElementById("column-resize-input");
const heightCanBeDerivedWithoutRect = this.thumbMetadataExists(thumb) && columnInput !== null;
let height;
if (heightCanBeDerivedWithoutRect) {
height = this.estimateThumbHeightFromMetadata(thumb, columnInput);
} else {
height = getImageFromThumb(thumb).getBoundingClientRect().height;
}
const captionListRect = this.caption.children[0].getBoundingClientRect();
const ratio = height / captionListRect.height;
const scale = ratio > 1 ? Math.sqrt(ratio) : ratio * 0.85;
this.caption.parentElement.style.fontSize = `${roundToTwoDecimalPlaces(scale)}em`;
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
thumbMetadataExists(thumb) {
if (onSearchPage()) {
return false;
}
const thumbNode = ThumbNode.allThumbNodes.get(thumb.id);
if (thumbNode === undefined) {
return false;
}
if (thumbNode.metadata === undefined) {
return false;
}
if (thumbNode.metadata.width <= 0 || thumbNode.metadata.width <= 0) {
return false;
}
return true;
}
/**
* @param {HTMLElement} thumb
* @param {HTMLInputElement} columnInput
* @returns {Number}
*/
estimateThumbHeightFromMetadata(thumb, columnInput) {
const thumbNode = ThumbNode.allThumbNodes.get(thumb.id);
const gridGap = 16;
const columnCount = Math.max(1, parseInt(columnInput.value));
const thumbWidthEstimate = (window.innerWidth - (columnCount * gridGap)) / columnCount;
const thumbWidthScale = thumbNode.metadata.width / thumbWidthEstimate;
return thumbNode.metadata.height / thumbWidthScale;
}
/**
* @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 = (event) => {
event.stopPropagation();
this.tagOnClick(tagName, event);
};
tag.addEventListener("contextmenu", (event) => {
event.preventDefault();
this.tagOnClick(`-${this.replaceSpacesWithUnderscores(tag.textContent)}`, event);
});
}
/**
* @returns {Object.<String, Number>}
*/
loadSavedTags() {
return JSON.parse(localStorage.getItem(Caption.localStorageKeys.tagCategories) || "{}");
}
saveTags() {
localStorage.setItem(Caption.localStorageKeys.tagCategories, JSON.stringify(Caption.tagCategoryAssociations));
}
/**
* @param {String} value
* @param {MouseEvent} mouseEvent
*/
tagOnClick(value, mouseEvent) {
if (mouseEvent.ctrlKey) {
openSearchPage(value);
return;
}
const searchBox = onSearchPage() ? document.getElementsByName("tags")[0] : document.getElementById("favorites-search-box");
const searchBoxDoesNotIncludeTag = true;
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.replaceAll(/_/gm, " ");
}
/**
* @param {String} tagName
* @returns {String}
*/
replaceSpacesWithUnderscores(tagName) {
return tagName.replaceAll(/\s/gm, "_");
}
/**
* @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 = removeExtraWhiteSpace(getTagsFromThumb(thumb).replace(thumb.id, ""))
.split(" ");
const unknownThumbTags = tagNames
.filter(tag => Caption.tagCategoryAssociations[tag] === undefined && !CUSTOM_TAGS.has(tag));
this.currentThumbId = thumb.id;
if (this.allTagsAreProblematic(unknownThumbTags)) {
this.correctAllProblematicTagsFromThumb(thumb, () => {
this.addTags(tagNames, thumb);
});
return;
}
if (unknownThumbTags.length > 0) {
this.findTagCategories(unknownThumbTags, 3, () => {
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 = Caption.tagCategoryAssociations[tagName];
if (encoding === undefined) {
return "general";
}
return Caption.tagCategoryEncodings[encoding];
}
/**
* @param {String[]} tags
* @returns {Boolean}
*/
allTagsAreProblematic(tags) {
for (const tag of tags) {
if (!this.problematicTags.has(tag)) {
return false;
}
}
return tags.length > 0;
}
/**
* @param {HTMLElement} thumb
* @param {Function} onProblematicTagsCorrected
*/
correctAllProblematicTagsFromThumb(thumb, onProblematicTagsCorrected) {
fetch(`https://rule34.xxx/index.php?page=post&s=view&id=${thumb.id}`)
.then((response) => {
return response.text();
})
.then((html) => {
const tagCategoryMap = this.getTagCategoryMapFromPostPage(html);
for (const [tagName, tagCategory] of tagCategoryMap.entries()) {
if (this.problematicTags.has(tagName)) {
Caption.tagCategoryAssociations[tagName] = Caption.encodeTagCategory(tagCategory);
this.problematicTags.delete(tagName);
}
}
onProblematicTagsCorrected();
})
.catch((error) => {
console.error(error);
});
}
/**
* @param {String} html
* @returns {Map.<String, String>}
*/
getTagCategoryMapFromPostPage(html) {
const dom = new DOMParser().parseFromString(html, "text/html");
return Array.from(dom.querySelectorAll(".tag"))
.reduce((map, element) => {
const tagCategory = element.classList[0].replace("tag-type-", "");
const tagName = this.replaceSpacesWithUnderscores(element.children[1].textContent);
map.set(tagName, tagCategory);
return map;
}, new Map());
}
/**
* @param {String} tag
*/
setAsProblematic(tag) {
this.problematicTags.add(tag);
}
findTagCategoriesOnPageChange() {
const tagNames = this.getTagNamesWithUnknownCategories(getAllVisibleThumbs().slice(0, 200));
this.findTagCategories(tagNames, 10, () => {
this.saveTags();
});
}
/**
* @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) {
if (isNumber(tagName) && tagName.length > 5) {
Caption.tagCategoryAssociations[tagName] = 0;
continue;
}
if (tagName.includes("'")) {
this.setAsProblematic(tagName);
continue;
}
const apiURL = `https://api.rule34.xxx//index.php?page=dapi&s=tag&q=index&name=${encodeURIComponent(tagName)}`;
try {
fetch(apiURL, {
signal: this.abortController.signal
})
.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.setAsProblematic(tagName);
return;
}
Caption.tagCategoryAssociations[tagName] = parseInt(encoding);
if (tagName === lastTagName && onAllCategoriesFound !== undefined) {
onAllCategoriesFound();
}
}).catch(() => {
if (onAllCategoriesFound !== undefined) {
onAllCategoriesFound();
}
});
} catch (error) {
// if (error.name !== "TypeError") {
// throw error;
// }
}
await sleep(fetchDelay);
}
}
/**
* @param {HTMLElement[]} thumbs
* @returns {String[]}
*/
getTagNamesWithUnknownCategories(thumbs) {
return Array.from(thumbs)
.map(thumb => getTagsFromThumb(thumb).replace(/ \d+$/, ""))
.join(" ")
.split(" ")
.filter(tagName => !isNumber(tagName) && Caption.tagCategoryAssociations[tagName] === undefined);
}
}
const caption = new Caption();
// tag_modifier.js
const tagModifierHTML = `<div id="tag-modifier-container">
<style>
#tag-modifier-ui-container {
display: none;
>* {
margin-top: 10px;
}
}
#tag-modifier-ui-textarea {
width: 80%;
}
.thumb-node.tag-modifier-selected {
outline: 2px dashed white !important;
>div {
opacity: 1;
filter: grayscale(0%);
}
}
#tag-modifier-ui-status-label {
visibility: hidden;
}
.tag-type-custom>a, .tag-type-custom {
color: hotpink;
}
</style>
<div id="tag-modifier-option-container">
<label class="checkbox" title="Add or Remove custom or official tags to favorites">
<input type="checkbox" id="tag-modifier-option-checkbox">Modify Tags
</label>
</div>
<div id="tag-modifier-ui-container">
<label id="tag-modifier-ui-status-label">No Status</label>
<textarea id="tag-modifier-ui-textarea" placeholder="tags" spellcheck="false"></textarea>
<div id="tag-modifier-ui-modification-buttons">
<button id="tag-modifier-ui-add" title="Add tags to selected favorites">Add</button>
<button id="tag-modifier-remove" title="Remove tags from selected favorites">Remove</button>
</div>
<div id="tag-modifier-ui-selection-buttons">
<button id="tag-modifier-ui-select-all" title="Select all favorites for tag modification">Select all</button>
<button id="tag-modifier-ui-un-select-all" title="Unselect all favorites for tag modification">Unselect all</button>
</div>
<div id="tag-modifier-ui-reset-button-container">
<button id="tag-modifier-reset" title="Reset tag modifications">Reset</button>
</div>
<div id="tag-modifier-ui-configuration" style="display: none;">
<button id="tag-modifier-import" title="Import modified tags">Import</button>
<button id="tag-modifier-export" title="Export modified tags">Export</button>
</div>
</div>
</div>`;
class TagModifier {
/**
* @type {String}
*/
static databaseName = "AdditionalTags";
/**
* @type {String}
*/
static objectStoreName = "additionalTags";
/**
* @type {Boolean}
*/
static get currentlyModifyingTags() {
return document.getElementById("tag-edit-mode") !== null;
}
/**
* @type {Map.<String, String>}
*/
static tagModifications = new Map();
/**
* @type {Boolean}
*/
static get disabled() {
return !onFavoritesPage();
}
/**
* @type {{container: HTMLDivElement, checkbox: HTMLInputElement}}
*/
favoritesOption;
/**
* @type { {container: HTMLDivElement,
* textarea: HTMLTextAreaElement,
* statusLabel: HTMLLabelElement,
* add: HTMLButtonElement,
* remove: HTMLButtonElement,
* reset: HTMLButtonElement,
* selectAll: HTMLButtonElement,
* unSelectAll: HTMLButtonElement,
* import: HTMLButtonElement,
* export: HTMLButtonElement}}
*/
ui;
/**
* @type {ThumbNode[]}
*/
selectedThumbNodes;
/**
* @type {Boolean}
*/
atLeastOneFavoriteIsSelected;
constructor() {
if (TagModifier.disabled) {
return;
}
this.favoritesOption = {};
this.ui = {};
this.selectedThumbNodes = [];
this.atLeastOneFavoriteIsSelected = false;
this.loadTagModifications();
this.injectHTML();
this.addEventListeners();
}
injectHTML() {
document.getElementById("left-favorites-panel-bottom-row").lastElementChild.insertAdjacentHTML("beforebegin", tagModifierHTML);
this.favoritesOption.container = document.getElementById("tag-modifier-container");
this.favoritesOption.checkbox = document.getElementById("tag-modifier-option-checkbox");
this.ui.container = document.getElementById("tag-modifier-ui-container");
this.ui.statusLabel = document.getElementById("tag-modifier-ui-status-label");
this.ui.textarea = document.getElementById("tag-modifier-ui-textarea");
this.ui.add = document.getElementById("tag-modifier-ui-add");
this.ui.remove = document.getElementById("tag-modifier-remove");
this.ui.reset = document.getElementById("tag-modifier-reset");
this.ui.selectAll = document.getElementById("tag-modifier-ui-select-all");
this.ui.unSelectAll = document.getElementById("tag-modifier-ui-un-select-all");
this.ui.import = document.getElementById("tag-modifier-import");
this.ui.export = document.getElementById("tag-modifier-export");
}
addEventListeners() {
this.favoritesOption.checkbox.onchange = (event) => {
this.toggleTagEditMode(event.target.checked);
};
this.ui.selectAll.onclick = this.selectAll.bind(this);
this.ui.unSelectAll.onclick = this.unSelectAll.bind(this);
this.ui.add.onclick = this.addTagsToSelected.bind(this);
this.ui.remove.onclick = this.removeTagsFromSelected.bind(this);
this.ui.reset.onclick = this.resetTagModifications.bind(this);
this.ui.import.onclick = this.importTagModifications.bind(this);
this.ui.export.onclick = this.exportTagModifications.bind(this);
window.addEventListener("searchStarted", () => {
this.unSelectAll();
});
}
/**
* @param {Boolean} value
*/
toggleTagEditMode(value) {
this.toggleThumbInteraction(value);
this.toggleUi(value);
this.toggleTagEditModeEventListeners(value);
this.ui.unSelectAll.click();
}
/**
* @param {Boolean} value
*/
toggleThumbInteraction(value) {
if (!value) {
const tagEditModeStyle = document.getElementById("tag-edit-mode");
if (tagEditModeStyle !== null) {
tagEditModeStyle.remove();
}
return;
}
injectStyleHTML(`
.thumb-node {
cursor: pointer;
outline: 1px solid black;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
> div {
outline: none !important;
> img {
outline: none !important;
}
pointer-events:none;
opacity: 0.6;
filter: grayscale(90%);
transition: none !important;
}
}
`, "tag-edit-mode");
}
/**
* @param {Boolean} value
*/
toggleUi(value) {
this.ui.container.style.display = value ? "block" : "none";
}
/**
* @param {Boolean} value
*/
toggleTagEditModeEventListeners(value) {
for (const thumbNode of ThumbNode.allThumbNodes.values()) {
if (value) {
thumbNode.root.onclick = () => {
this.toggleThumbSelection(thumbNode.root);
};
} else {
thumbNode.root.onclick = null;
}
}
}
/**
* @param {String} text
*/
showStatus(text) {
this.ui.statusLabel.style.visibility = "visible";
this.ui.statusLabel.textContent = text;
setTimeout(() => {
const statusHasNotChanged = this.ui.statusLabel.textContent === text;
if (statusHasNotChanged) {
this.ui.statusLabel.style.visibility = "hidden";
}
}, 1000);
}
unSelectAll() {
if (!this.atLeastOneFavoriteIsSelected) {
return;
}
for (const thumbNode of ThumbNode.allThumbNodes.values()) {
this.toggleThumbSelection(thumbNode.root, false);
}
this.atLeastOneFavoriteIsSelected = false;
}
selectAll() {
for (const thumbNode of ThumbNode.thumbNodesMatchedBySearch.values()) {
this.toggleThumbSelection(thumbNode.root, true);
}
}
/**
* @param {HTMLElement} thumb
* @param {Boolean} value
*/
toggleThumbSelection(thumb, value) {
this.atLeastOneFavoriteIsSelected = true;
if (value === undefined) {
thumb.classList.toggle("tag-modifier-selected");
} else {
thumb.classList.toggle("tag-modifier-selected", value);
}
}
/**
* @param {String} tags
* @returns
*/
removeContentTypeTags(tags) {
return tags
.replace(/(?:^|\s*)(?:video|animated|mp4)(?:$|\s*)/g, "");
}
addTagsToSelected() {
this.modifyTagsOfSelected(false);
}
removeTagsFromSelected() {
this.modifyTagsOfSelected(true);
}
/**
*
* @param {Boolean} remove
*/
modifyTagsOfSelected(remove) {
const tags = this.ui.textarea.value.toLowerCase();
const tagsWithoutContentTypes = this.removeContentTypeTags(tags);
const tagsToModify = removeExtraWhiteSpace(tagsWithoutContentTypes);
const statusPrefix = remove ? "Removed tag(s) from" : "Added tag(s) to";
let modifiedTagsCount = 0;
if (tagsToModify === "") {
return;
}
for (const [id, thumbNode] of ThumbNode.allThumbNodes.entries()) {
if (thumbNode.root.classList.contains("tag-modifier-selected")) {
const additionalTags = remove ? thumbNode.removeAdditionalTags(tagsToModify) : thumbNode.addAdditionalTags(tagsToModify);
TagModifier.tagModifications.set(id, additionalTags);
modifiedTagsCount += 1;
}
}
if (modifiedTagsCount === 0) {
return;
}
if (tags !== tagsWithoutContentTypes) {
alert("Warning: video, animated, and mp4 tags are unchanged.\nThey cannot be modified.");
}
this.showStatus(`${statusPrefix} ${modifiedTagsCount} favorite(s)`);
dispatchEvent(new Event("modifiedTags"));
setCustomTags(tagsToModify);
this.storeTagModifications();
}
createDatabase(event) {
/**
* @type {IDBDatabase}
*/
const database = event.target.result;
database
.createObjectStore(TagModifier.objectStoreName, {
keyPath: "id"
});
}
storeTagModifications() {
const request = indexedDB.open(TagModifier.databaseName, 1);
request.onupgradeneeded = this.createDatabase;
request.onsuccess = (event) => {
/**
* @type {IDBDatabase}
*/
const database = event.target.result;
const objectStore = database
.transaction(TagModifier.objectStoreName, "readwrite")
.objectStore(TagModifier.objectStoreName);
const idsWithNoTagModifications = [];
for (const [id, tags] of TagModifier.tagModifications) {
if (tags === "") {
idsWithNoTagModifications.push(id);
objectStore.delete(id);
} else {
objectStore.put({
id,
tags
});
}
}
for (const id of idsWithNoTagModifications) {
TagModifier.tagModifications.delete(id);
}
database.close();
};
}
loadTagModifications() {
const request = indexedDB.open(TagModifier.databaseName, 1);
request.onupgradeneeded = this.createDatabase;
request.onsuccess = (event) => {
/**
* @type {IDBDatabase}
*/
const database = event.target.result;
const objectStore = database
.transaction(TagModifier.objectStoreName, "readonly")
.objectStore(TagModifier.objectStoreName);
objectStore.getAll().onsuccess = (successEvent) => {
const tagModifications = successEvent.target.result;
for (const record of tagModifications) {
TagModifier.tagModifications.set(record.id, record.tags);
}
};
database.close();
};
}
resetTagModifications() {
if (!confirm("Are you sure you want to delete all tag modifications?")) {
return;
}
CUSTOM_TAGS.clear();
indexedDB.deleteDatabase("AdditionalTags");
ThumbNode.allThumbNodes.forEach(thumbNode => {
thumbNode.resetAdditionalTags();
});
dispatchEvent(new Event("modifiedTags"));
localStorage.removeItem("customTags");
}
exportTagModifications() {
const modifications = JSON.stringify(mapToObject(TagModifier.tagModifications));
navigator.clipboard.writeText(modifications);
alert("Copied tag modifications to clipboard");
}
importTagModifications() {
let modifications;
try {
const object = JSON.parse(this.ui.textarea.value);
if (!(typeof object === "object")) {
throw new TypeError(`Input parsed as ${typeof (object)}, but expected object`);
}
modifications = objectToMap(object);
} catch (error) {
if (error.name === "SyntaxError" || error.name === "TypeError") {
alert("Import Unsuccessful. Failed to parse input, JSON object format expected.");
} else {
throw error;
}
return;
}
console.error(modifications);
}
}
const tagModifier = new TagModifier();
// 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 */
const CUSTOM_TAGS = loadCustomTags();
/**
* @returns {Set.<String>}
*/
function loadCustomTags() {
return new Set(JSON.parse(localStorage.getItem("customTags")) || []);
}
/**
* @param {String} tags
*/
async function setCustomTags(tags) {
for (const tag of removeExtraWhiteSpace(tags).split(" ")) {
if (tag === "" || CUSTOM_TAGS.has(tag)) {
continue;
}
const isAnOfficialTag = await isOfficialTag(tag);
if (!isAnOfficialTag) {
CUSTOM_TAGS.add(tag);
}
}
localStorage.setItem("customTags", JSON.stringify(Array.from(CUSTOM_TAGS)));
}
/**
* @param {{label: String, value: String, type: String}[]} officialTags
* @param {String} searchQuery
* @returns {{label: String, value: String, type: String}[]}
*/
function mergeOfficialTagsWithCustomTags(officialTags, searchQuery) {
const customTags = Array.from(CUSTOM_TAGS);
const officialTagValues = new Set(officialTags.map(officialTag => officialTag.value));
const mergedTags = officialTags;
for (const customTag of customTags) {
if (!officialTagValues.has(customTag) && customTag.startsWith(searchQuery)) {
mergedTags.unshift({
label: `${customTag} (custom)`,
value: customTag,
type: "custom"
});
}
}
return mergedTags;
}
class AwesompleteWrapper {
/**
* @type {Boolean}
*/
static get disabled() {
return !onFavoritesPage();
}
constructor() {
if (AwesompleteWrapper.disabled) {
return;
}
document.querySelectorAll("textarea").forEach((textarea) => {
this.addAwesompleteToInput(textarea);
});
document.querySelectorAll("input").forEach((input) => {
if (input.hasAttribute("needs-autocomplete")) {
this.addAwesompleteToInput(input);
}
});
}
/**
* @param {HTMLElement} 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) => {
insertSuggestion(awesomplete.input, decodeEntities(suggestion.value));
}
});
input.addEventListener("keydown", (event) => {
switch (event.key) {
case "Tab":
if (!awesomplete.isOpened || awesomplete.suggestions.length === 0) {
return;
}
awesomplete.next();
awesomplete.select();
event.preventDefault();
break;
case "Escape":
hideAwesomplete(input);
break;
default:
break;
}
});
input.oninput = () => {
this.populateAwesompleteList(this.getCurrentTag(input), awesomplete);
};
}
/**
* @param {String} prefix
* @param {Awesomplete_} awesomplete
*/
populateAwesompleteList(prefix, awesomplete) {
if (prefix.trim() === "") {
return;
}
fetch(`https://ac.rule34.xxx/autocomplete.php?q=${prefix}`)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status);
})
.then((suggestions) => {
const mergedSuggestions = mergeOfficialTagsWithCustomTags(JSON.parse(suggestions), prefix);
awesomplete.list = mergedSuggestions;
});
}
/**
* @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();